From 0f32aef8340a9c515c67cf96abf70e6a1666ea3f Mon Sep 17 00:00:00 2001 From: JeanneSon <43119728+JeanneSon@users.noreply.github.com> Date: Wed, 23 Mar 2022 14:51:29 +0700 Subject: [PATCH 1/2] convert change password e2e tests from Protractor to Playwright --- test/e2e/change-password.spec.ts | 64 ++++++++++++++++++++++++++++++ test/e2e/pages/page-header.page.ts | 21 ++++++++++ test/e2e/pages/projects.page.ts | 2 - test/e2e/utils/login.ts | 5 ++- 4 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 test/e2e/change-password.spec.ts create mode 100644 test/e2e/pages/page-header.page.ts diff --git a/test/e2e/change-password.spec.ts b/test/e2e/change-password.spec.ts new file mode 100644 index 0000000000..addc5cfc54 --- /dev/null +++ b/test/e2e/change-password.spec.ts @@ -0,0 +1,64 @@ +import { expect } from '@playwright/test'; +import { test } from './utils/fixtures'; +import { ChangePasswordPage } from './pages/change-password.page'; +import { changePassword } from './utils/testControl'; +import { LoginPage } from './pages/login.page'; +import { PageHeader } from './pages/page-header.page'; + +test.describe('E2E Change Password app', () => { + const newPassword = '12345678'; + let changePasswordPage: ChangePasswordPage; + + test.beforeEach(async ({ memberTab }) => { + changePasswordPage = new ChangePasswordPage(memberTab); + await changePasswordPage.goto(); + }); + + test.afterEach(async ({ memberTab, request }) => { + // reset password back to original + await changePassword(request, memberTab.username, memberTab.password); + }); + + test('Refuses to allow form submission if the confirm input does not match', async () => { + await changePasswordPage.passwordInput.fill(newPassword); + await changePasswordPage.confirmInput.fill('blah12345'); + await expect (changePasswordPage.submitButton).toBeDisabled(); + }); + + test('Allows form submission if the confirm input matches', async () => { + await changePasswordPage.passwordInput.fill(newPassword); + await changePasswordPage.confirmInput.fill(newPassword); + await expect(changePasswordPage.submitButton).toBeEnabled(); + }); + + test('Should not allow a password less than 7 characters', async () => { + let shortPassword = '12345'; + await changePasswordPage.passwordInput.fill(shortPassword); + await changePasswordPage.confirmInput.fill(shortPassword); + await expect (changePasswordPage.submitButton).toBeDisabled(); + }); + + test('Can successfully change user\'s password after form submission', async ({ page, memberTab }) => { + await changePasswordPage.passwordInput.fill(newPassword); + await changePasswordPage.confirmInput.fill(newPassword); + await expect (changePasswordPage.passwordMatchImage).toBeVisible(); + await expect (changePasswordPage.submitButton).toBeEnabled(); + await changePasswordPage.submitButton.click(); + // when password is changed successfully, a notice appears on the page + const messageSuccessfulUpdate = '[data-ng-bind-html="notice.message"] >> text=Password updated successfully'; + await changePasswordPage.page.waitForSelector(messageSuccessfulUpdate, {strict: false, state: 'attached'}); + expect (await changePasswordPage.noticeList.locator(messageSuccessfulUpdate).count() + ).toBeGreaterThan(0); + + // test login with new password + + // await logout(memberTab); // CANNOT do this as it invalidates the session stored in storageState.json! - 2022-03 RM + // await login(memberTab, memberTab.username, newPassword); + + const loginPage = new LoginPage(page); + await loginPage.loginAs(memberTab.username, newPassword); + const pageHeader = new PageHeader(page); + await expect (pageHeader.myProjects.button).toBeVisible(); + }); + +}); diff --git a/test/e2e/pages/page-header.page.ts b/test/e2e/pages/page-header.page.ts new file mode 100644 index 0000000000..5e8e9d4a64 --- /dev/null +++ b/test/e2e/pages/page-header.page.ts @@ -0,0 +1,21 @@ +import { Locator, Page } from "@playwright/test"; + +type MyProjects = { + button: Locator; + links: Locator; +}; + +export class PageHeader { + readonly page: Page; + readonly myProjects: MyProjects; + readonly loginButton: Locator; + + constructor(page: Page) { + this.page = page; + this.myProjects = { + button: page.locator('#myProjectDropdownButton'), + links: page.locator('#myProjectDropdownMenu >> .dropdown-item') + }; + this.loginButton = page.locator('text=Login').nth(0); + } +} diff --git a/test/e2e/pages/projects.page.ts b/test/e2e/pages/projects.page.ts index bb90dd3054..141d615cd2 100644 --- a/test/e2e/pages/projects.page.ts +++ b/test/e2e/pages/projects.page.ts @@ -20,6 +20,4 @@ export class ProjectsPage { 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 } diff --git a/test/e2e/utils/login.ts b/test/e2e/utils/login.ts index 362dd2642a..36a64d3047 100644 --- a/test/e2e/utils/login.ts +++ b/test/e2e/utils/login.ts @@ -1,5 +1,6 @@ import { Browser, Page } from '@playwright/test'; import constants from '../testConstants.json'; +import type { usernamesForFixture } from './userFixtures'; export async function login(page: Page, username: string, password: string) { await page.goto('/auth/login'); @@ -15,7 +16,7 @@ export async function logout(page: Page) { return await page.goto('/auth/logout'); } -export function getLoginInfo(name: string) { +export function getLoginInfo(name: usernamesForFixture) { const usernameKey = `${name}Username`; const passwordKey = `${name}Password`; if (Object.hasOwnProperty.call(constants, usernameKey)) { @@ -28,7 +29,7 @@ export function getLoginInfo(name: string) { } } -export function loginAs(page: Page, name: string) { +export function loginAs(page: Page, name: usernamesForFixture) { const { username, password } = getLoginInfo(name); return login(page, username, password); } From 188262c3d30c75dbd59f5bfa51db13675022b2f0 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Thu, 24 Mar 2022 13:46:09 +0700 Subject: [PATCH 2/2] Improve E2E test fixtures * Tab fixtures: extract data setup into own function This is in preparation for creating new admin/member/etc. fixtures that parallel adminTab, memberTab, etc., but which don't create a new browser context; they will only carry data. * Add new UserDetails fixtures (faster than UserTab) If you just need user details like username, password, etc., it's overkill to request a whole adminTab or memberTab just to access its username and password. Instead, we create 'admin' and 'member' (and so on) fixtures to just contain the data. These fixtures are extremely fast to create, speeding up some tests that didn't need a full browser tab. * Speed up change password tests by reusing fixtures In some cases, it can make sense to reuse a browser context fixture for efficiency's sake (it can take a second or two to create a new browser context). The "change password" tests are one such case, because the tests don't actually need a new browser tab for the ones that simply verify whether a button is disabled. Note that when beforeEach was changed to beforeAll, we had to remove the page.close() and context.close() calls from the fixture setup. Otherwise we'd have closed changePasswordPage over that browser tab instance and then immediately closed it when the fixture went out of scope at the end of beforeAll. Which would result in every test failing due to its browser tab having been closed! In this commit, we also use the more-efficient member fixtures instead of memberTab when all we want is the username and password. * Improve TypeScript typing of test function With the `test.extend` feature in Playwright, it's better to extend it once with a large number of fixture definitions, rather than extend it multiple times with one fixture each. The former results in a much nicer TypeScript type overlay when you over over the `test` function later. --- test/e2e/change-password.spec.ts | 10 ++--- test/e2e/utils/fixtures.ts | 72 ++++++++++++++++++++++++++------ 2 files changed, 64 insertions(+), 18 deletions(-) diff --git a/test/e2e/change-password.spec.ts b/test/e2e/change-password.spec.ts index addc5cfc54..8d7a634e91 100644 --- a/test/e2e/change-password.spec.ts +++ b/test/e2e/change-password.spec.ts @@ -9,14 +9,14 @@ test.describe('E2E Change Password app', () => { const newPassword = '12345678'; let changePasswordPage: ChangePasswordPage; - test.beforeEach(async ({ memberTab }) => { + test.beforeAll(async ({ memberTab }) => { changePasswordPage = new ChangePasswordPage(memberTab); await changePasswordPage.goto(); }); - test.afterEach(async ({ memberTab, request }) => { + test.afterAll(async ({ member, request }) => { // reset password back to original - await changePassword(request, memberTab.username, memberTab.password); + await changePassword(request, member.username, member.password); }); test('Refuses to allow form submission if the confirm input does not match', async () => { @@ -38,7 +38,7 @@ test.describe('E2E Change Password app', () => { await expect (changePasswordPage.submitButton).toBeDisabled(); }); - test('Can successfully change user\'s password after form submission', async ({ page, memberTab }) => { + test('Can successfully change user\'s password after form submission', async ({ page, member }) => { await changePasswordPage.passwordInput.fill(newPassword); await changePasswordPage.confirmInput.fill(newPassword); await expect (changePasswordPage.passwordMatchImage).toBeVisible(); @@ -56,7 +56,7 @@ test.describe('E2E Change Password app', () => { // await login(memberTab, memberTab.username, newPassword); const loginPage = new LoginPage(page); - await loginPage.loginAs(memberTab.username, newPassword); + await loginPage.loginAs(member.username, newPassword); const pageHeader = new PageHeader(page); await expect (pageHeader.myProjects.button).toBeVisible(); }); diff --git a/test/e2e/utils/fixtures.ts b/test/e2e/utils/fixtures.ts index a52367f181..91bb4b4dd0 100644 --- a/test/e2e/utils/fixtures.ts +++ b/test/e2e/utils/fixtures.ts @@ -4,32 +4,78 @@ import type { Browser, Page } from '@playwright/test'; import type { usernamesForFixture } from './userFixtures'; import constants from '../testConstants.json'; -export type UserTab = Page & { +export type UserDetails = { username: string, password: string, name: string, email: string, } +export type UserTab = Page & UserDetails; + +function setupUserDetails(obj: any, username: usernamesForFixture) { + obj.username = constants[`${username}Username`] ?? username; + obj.name = constants[`${username}Name`] ?? username; + obj.password = constants[`${username}Password`] ?? 'x'; + obj.email = constants[`${username}Email`] ?? `${username}@example.com`; +} + const userTab = (username: usernamesForFixture) => async ({ browser, browserName }: { browser: Browser, browserName: string}, use: (r: UserTab) => Promise) => { const storageState = `${browserName}-${username}-storageState.json`; const context = await browser.newContext({ storageState }) const page = await context.newPage(); const tab = page as UserTab; - tab.username = constants[`${username}Username`] ?? username; - tab.name = constants[`${username}Name`] ?? username; - tab.password = constants[`${username}Password`] ?? 'x'; - tab.email = constants[`${username}Email`] ?? `${username}@example.com`; + setupUserDetails(tab, username); await use(tab); - await tab.close(); - await context.close(); } -// Extend basic test by providing a "todoPage" fixture. +// Add user fixtures to test function +// Two kinds of fixtures: userTab and user, where "user" is one of "admin", "manager", "member", "member2", or "observer" +// The userTab fixture represents a browser tab (a "page" in Playwright terms) that's already logged in as that user +// The user fixture just carries that user's details (username, password, name and email) +// Note: "Tab" was chosen instead of "Page" to avoid confusion with Page Object Model classes like SiteAdminPage export const test = (base - .extend<{ adminTab: UserTab }>({ adminTab: userTab('admin') }) - .extend<{ managerTab: UserTab }>({ managerTab: userTab('manager') }) - .extend<{ memberTab: UserTab }>({ memberTab: userTab('member') }) - .extend<{ member2Tab: UserTab }>({ member2Tab: userTab('member2') }) - .extend<{ observerTab: UserTab }>({ observerTab: userTab('observer') }) + .extend<{ + adminTab: UserTab, + managerTab: UserTab, + memberTab: UserTab, + member2Tab: UserTab, + observerTab: UserTab, + admin: UserDetails, + manager: UserDetails, + member: UserDetails, + member2: UserDetails, + observer: UserDetails, + }>({ + adminTab: userTab('admin'), + managerTab: userTab('manager'), + memberTab: userTab('member'), + member2Tab: userTab('member2'), + observerTab: userTab('observer'), + admin: async ({}, use) => { + let admin = {} as UserDetails; + setupUserDetails(admin, 'admin'); + await use(admin); + }, + manager: async ({}, use) => { + let manager = {} as UserDetails; + setupUserDetails(manager, 'manager'); + await use(manager); + }, + member: async ({}, use) => { + let member = {} as UserDetails; + setupUserDetails(member, 'member'); + await use(member); + }, + member2: async ({}, use) => { + let member2 = {} as UserDetails; + setupUserDetails(member2, 'member2'); + await use(member2); + }, + observer: async ({}, use) => { + let observer = {} as UserDetails; + setupUserDetails(observer, 'observer'); + await use(observer); + } + }) );