Skip to content

Commit

Permalink
Site traversal playwright tests (#1352)
Browse files Browse the repository at this point in the history
The following tests have been converted to playwright

Bellows E2E Page Traversal
✓ Explore signup page
✓ Explore forgot password page
✓ Explore login page
✓ Explore change password page
✓ Explore activity page
✓ Explore project page
✓ Explore site admin page
✓ Explore user profile page

* Link to testConstants.json so Typescript is happy

TypeScript was complaining about testConstants.json not being in the
rootDir folder, which is test/e2e. We want a single source of truth,
and we can't move testConstants.json out of test/app yet since we
want the e2e tests in test/app to continue to work for now. So we
create a symlink in test/e2e, which satisfies Typescript that the
file lives under rootDir but lets us still have one source of truth.

* Convert page traversal e2e tests from Protractor to Playwright

Run playwright tests as part of a pull request

* Add "change password" call to test control API

This function lives in a new file called testControl.ts. In the long
run, this file will grow to include all the necessary API calls needed
to set up tests for E2E: creating test projects, assigning and removing
project membership, and so on. For now it just has one function.

* Better fixture setup: save username, password, etc

The adminTab fixtures now save the username, password, etc. from the
testConstants file, so that resetting the password is as simply as doing
`changePassword(memberTab.username, memberTab.password)`.

Co-authored-by: Robin Munn <rmunn@pobox.com>
Co-authored-by: Chris Hirt <chris@hirtfamily.net>
  • Loading branch information
3 people authored Mar 22, 2022
1 parent 8bc2631 commit 5e6d7a5
Show file tree
Hide file tree
Showing 18 changed files with 329 additions and 21 deletions.
16 changes: 13 additions & 3 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
github_token: ${{ github.token }}
files: docker/PhpUnitTests.xml

build-app:
build-app-run-e2e-tests:
runs-on: ubuntu-latest

steps:
Expand All @@ -43,11 +43,21 @@ jobs:
node-version: '16.14.0'
cache: 'npm'
-
run: npm install
run: npm ci
-
run: docker-compose build app
-
run: docker-compose up -d app-for-playwright

# see https://playwright.dev/docs/ci#github-actions
-
run: npx playwright install --with-deps
working-directory: .
-
run: npx playwright test
working-directory: .

# e2e-tests:
# protractor-tests:
# runs-on: ubuntu-latest

# steps:
Expand Down
2 changes: 1 addition & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const config: PlaywrightTestConfig = {
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
reporter: process.env.CI ? 'github' : 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/example.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { APIRequestContext } from '@playwright/test';
import { expect } from '@playwright/test';
import constants from '../app/testConstants.json';
import constants from './testConstants.json';
import { testControl } from './utils/jsonrpc';
import type { UserTab } from './utils/fixtures';
import { test } from './utils/fixtures';
Expand Down
19 changes: 19 additions & 0 deletions test/e2e/pages/activity.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { expect, Locator, Page } from '@playwright/test';

export class ActivityPage {
readonly page: Page;
readonly pageName: Locator;
readonly activitiesList: Locator;
static readonly url: string = '/app/activity';

constructor(page: Page) {
this.page = page;
this.pageName = page.locator('.page-name >> text=Activity');
this.activitiesList = page.locator('.activity-content');
}

async goto() {
await this.page.goto(ActivityPage.url);
await expect(this.pageName).toBeVisible();
}
}
13 changes: 7 additions & 6 deletions test/e2e/pages/change-password.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ import { expect, Locator, Page } from '@playwright/test';

export class ChangePasswordPage {
readonly page: Page;
readonly password: Locator;
readonly confirm: Locator;
readonly passwordInput: Locator;
readonly confirmInput: Locator;
readonly passwordMatchImage: Locator;
readonly submitButton: Locator;
readonly noticeList: Locator;
static readonly url: string = '/app/changepassword';

constructor(page: Page) {
this.page = page;
this.password = page.locator('#change-password-input');
this.confirm = page.locator('#change-password-confirm-input');
this.passwordInput = page.locator('#change-password-input');
this.confirmInput = page.locator('#change-password-confirm-input');
this.passwordMatchImage = page.locator('#change-password-match');
this.submitButton = page.locator('#change-password-submit-button');
// Note ng-repeat here, not data-ng-repeat. Search for "notice in $ctrl.notices()"
Expand All @@ -20,7 +21,7 @@ export class ChangePasswordPage {
}

async goto() {
await this.page.goto('/app/changepassword');
await expect(this.password).toBeVisible();
await this.page.goto(ChangePasswordPage.url);
await expect(this.passwordInput).toBeVisible();
}
}
13 changes: 7 additions & 6 deletions test/e2e/pages/forgot-password.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { expect, Locator, Page } from '@playwright/test';

export class ForgotPasswordPage {
readonly page: Page;
form: Locator;
infoMessages: Locator;
errors: Locator;
usernameInput: Locator;
submitButton: Locator;
readonly form: Locator;
readonly infoMessages: Locator;
readonly errors: Locator;
readonly usernameInput: Locator;
readonly submitButton: Locator;
static readonly url: string = '/auth/forgot_password';

constructor(page: Page) {
this.page = page;
Expand All @@ -19,7 +20,7 @@ export class ForgotPasswordPage {
}

async goto() {
await this.page.goto('/auth/forgot_password');
await this.page.goto(ForgotPasswordPage.url);
await expect(this.form).toBeVisible();
}
}
31 changes: 31 additions & 0 deletions test/e2e/pages/login.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { expect, Locator, Page } from '@playwright/test';

export class LoginPage {
readonly page: Page;
readonly usernameInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
static readonly url: string = '/auth/login';

constructor(page: Page) {
this.page = page;
this.usernameInput = page.locator('#username');
this.passwordInput = page.locator('#password');
this.submitButton = page.locator('#login-submit')
}

async goto() {
await this.page.goto(LoginPage.url);
await expect(this.passwordInput).toBeVisible();
}

async loginAs(username: string, password: string) {
// navigate to login page if not already there
if (! this.page.url().endsWith(LoginPage.url)) {
await this.page.goto(LoginPage.url);
}
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
}
25 changes: 25 additions & 0 deletions test/e2e/pages/projects.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { expect, Locator, Page } from '@playwright/test';

export class ProjectsPage {
readonly page: Page;
readonly pageName: Locator;
readonly projectsList: Locator;
readonly projectNames: Locator;
readonly createButton: Locator;
static readonly url: string = '/app/projects';

constructor(page: Page) {
this.page = page;
this.pageName = page.locator('.page-name >> text=My Projects');
this.createButton = page.locator('button:has-text("Start or Join a New Project")');
this.projectsList = page.locator('[data-ng-repeat="project in visibleProjects"]');
this.projectNames = this.projectsList.locator('a[href^="/app/lexicon"]');
}

async goto() {
await this.page.goto(ProjectsPage.url);
await expect(this.pageName).toBeVisible();
}

// TODO: write feature request: implement a waiting spinning somthing indicator - create github issue as feature request
}
38 changes: 38 additions & 0 deletions test/e2e/pages/signup.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { expect, Locator, Page } from '@playwright/test';

type Captcha = {
expectedItemName: Locator;
blueSquareButton: Locator;
yellowCircleButton: Locator;
redTriangleButton: Locator;
};

export class SignupPage {
readonly page: Page;
readonly emailInput: Locator;
readonly nameInput: Locator;
readonly passwordInput: Locator;
readonly captchaDiv: Locator;
readonly captcha: Captcha;
static readonly url: string = '/public/signup';

constructor(page: Page) {
this.page = page;
this.emailInput = page.locator('#email');
this.nameInput = page.locator('#name');
this.passwordInput = page.locator('#password');

this.captchaDiv = page.locator('#pui-captcha');
this.captcha = {
expectedItemName: this.captchaDiv.locator('#expectedItemName'),
blueSquareButton: this.captchaDiv.locator('#captcha0'),
yellowCircleButton: this.captchaDiv.locator('#captcha1'),
redTriangleButton: this.captchaDiv.locator('#captcha2'),
}
}

async goto() {
await this.page.goto(SignupPage.url);
await expect(this.emailInput).toBeVisible();
}
}
39 changes: 39 additions & 0 deletions test/e2e/pages/site-admin.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { expect, Locator, Page } from '@playwright/test';

type Tabs = {
reports: Locator;
archivedProjects: Locator;
};

type ArchivedProjectsTab = {
republishButton: Locator;
deleteButton: Locator;
projectsList: Locator;
};

export class SiteAdminPage {
readonly page: Page;
readonly pageName: Locator;
readonly tabs: Tabs;
readonly archivedProjectsTab: ArchivedProjectsTab;
static readonly url: string = '/app/siteadmin';

constructor(page: Page) {
this.page = page;
this.pageName = page.locator('.page-name >> text=Site Administration');
this.tabs = {
reports: page.locator('#useres'),
archivedProjects: page.locator('#archivedprojects')
};
this.archivedProjectsTab = {
deleteButton: page.locator('#site-admin-delete-btn'),
republishButton: page.locator('#site-admin-republish-btn'),
projectsList: page.locator('[data-ng-repeat="project in visibleProjects"]')
};
}

async goto() {
await this.page.goto(SiteAdminPage.url);
await expect(this.pageName).toBeVisible();
}
}
29 changes: 29 additions & 0 deletions test/e2e/pages/user-profile.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { expect, Locator, Page } from '@playwright/test';

type Tabs = {
aboutMe: Locator;
myAccount: Locator;
};

export class UserProfilePage {
readonly page: Page;
readonly pageName: Locator;
readonly activitiesList: Locator;
readonly tabs: Tabs;
static readonly url: string = '/app/userprofile';

constructor(page: Page) {
this.page = page;
this.pageName = page.locator('.page-name >> text=Admin\'s User Profile');
this.activitiesList = page.locator('[data-ng-repeat="item in filteredActivities"]');
this.tabs = {
aboutMe: page.locator('#AboutMeTab'),
myAccount: page.locator('#myAccountTab')
};
}

async goto() {
await this.page.goto(UserProfilePage.url);
await expect(this.pageName).toBeVisible();
}
}
90 changes: 90 additions & 0 deletions test/e2e/site-traversal.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { expect } from '@playwright/test';
import { test } from './utils/fixtures';
import { ForgotPasswordPage } from './pages/forgot-password.page';
import { SignupPage } from './pages/signup.page';
import { LoginPage } from './pages/login.page';
import { ChangePasswordPage } from './pages/change-password.page';
import { ActivityPage } from './pages/activity.page';
import { ProjectsPage } from './pages/projects.page';
import { SiteAdminPage } from './pages/site-admin.page';
import { UserProfilePage } from './pages/user-profile.page';

/**
* page traversal without testing functionality
*/
test.describe('E2E Page Traversal', () => {

test('Explore signup page', async ({ page }) => {
const signupPage = new SignupPage(page);
await signupPage.goto();

await signupPage.emailInput.fill('');
await signupPage.nameInput.fill('');
await signupPage.passwordInput.fill('');
await signupPage.captcha.blueSquareButton.click();
await signupPage.captcha.yellowCircleButton.click();
await signupPage.captcha.redTriangleButton.click();
});

test('Explore forgot password page', async ({ page }) => {
const forgotPasswordPage = new ForgotPasswordPage(page);
await forgotPasswordPage.goto();

await forgotPasswordPage.usernameInput.fill('');
await forgotPasswordPage.submitButton.click();
});

test('Explore login page', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();

await loginPage.usernameInput.type('');
await loginPage.passwordInput.type('');
await loginPage.submitButton.click();
});

test('Explore change passsword page (admin)', async ({ adminTab }) => {
const changePasswordPage = new ChangePasswordPage(adminTab);
await changePasswordPage.goto();

await changePasswordPage.passwordInput.type('');
await changePasswordPage.confirmInput.type('');
await expect(changePasswordPage.submitButton).toBeDisabled();
});

test('Explore activity page (admin)', async ({ adminTab }) => {
const activityPage = new ActivityPage(adminTab);
await activityPage.goto();

await activityPage.activitiesList.count();
});

test('Explore project page (admin)', async ({ adminTab }) => {
const projectsPage = new ProjectsPage(adminTab);
await projectsPage.goto();

await projectsPage.projectsList.count();
await projectsPage.projectNames.count();
await projectsPage.createButton.click();
});

test('Explore site admin page', async ({ adminTab }) => {
const siteAdminPage = new SiteAdminPage(adminTab);
await siteAdminPage.goto();

await siteAdminPage.tabs.archivedProjects.click();
await expect(siteAdminPage.archivedProjectsTab.republishButton).toBeDisabled();
await expect(siteAdminPage.archivedProjectsTab.deleteButton).toBeDisabled();
await siteAdminPage.archivedProjectsTab.projectsList.count();
});

test('Explore user profile page (admin)', async ({ adminTab }) => {
const userProfilePage = new UserProfilePage(adminTab);
await userProfilePage.goto();

await userProfilePage.activitiesList.count();
await userProfilePage.tabs.aboutMe.click();
await userProfilePage.tabs.myAccount.click();
});

});
1 change: 1 addition & 0 deletions test/e2e/testConstants.json
Loading

0 comments on commit 5e6d7a5

Please sign in to comment.