diff --git a/e2e/davinci-suites/src/basic.test.ts b/e2e/davinci-suites/src/basic.test.ts index 1da585de47..6648690aa2 100644 --- a/e2e/davinci-suites/src/basic.test.ts +++ b/e2e/davinci-suites/src/basic.test.ts @@ -21,7 +21,7 @@ test('Test happy paths on test page', async ({ page }) => { await page.getByRole('button', { name: 'Sign On' }).click(); - await expect(page.getByText('Complete')).toBeVisible(); + await expect(async () => await expect(page.getByText('Complete')).toBeVisible()).toPass(); const sessionToken = await page.locator('#sessionToken').innerText(); const authCode = await page.locator('#authCode').innerText(); diff --git a/e2e/davinci-suites/src/phone-number-field.test.ts b/e2e/davinci-suites/src/phone-number-field.test.ts index 77617e2b72..6ca225473e 100644 --- a/e2e/davinci-suites/src/phone-number-field.test.ts +++ b/e2e/davinci-suites/src/phone-number-field.test.ts @@ -18,8 +18,18 @@ test.describe('Device registration tests', () => { await page.getByRole('textbox', { name: 'Password' }).fill(password); await page.getByRole('button', { name: 'Sign On' }).click(); - await page.getByRole('button', { name: 'USER_DELETE' }).click(); - await expect(page.getByRole('heading', { name: 'Success' })).toBeVisible(); + /** + * This implements a retry automatically on a timeout + * because this code path is not critical to the functionality + * we should consider this pattern because its flakey. + * + */ + await expect( + async () => await page.getByRole('button', { name: 'USER_DELETE' }).click(), + ).toPass(); + expect( + async () => await expect(page.getByRole('heading', { name: 'Success' })).toBeVisible(), + ).toPass(); }); test('Login - add email device - authenticate with email device', async ({ page }) => { @@ -40,9 +50,12 @@ test.describe('Device registration tests', () => { await page.getByRole('textbox', { name: 'Given Name' }).fill('demouser'); await page.getByRole('textbox', { name: 'Family Name' }).fill('demouser'); await page.getByRole('button', { name: 'Continue' }).click(); - await expect(page.getByRole('heading', { name: 'Registration Complete' })).toBeVisible(); + await expect( + async () => + await expect(page.getByRole('heading', { name: 'Registration Complete' })).toBeVisible(), + ).toPass(); await page.getByRole('button', { name: 'Continue' }).click(); - await page.getByRole('button', { name: 'Logout' }).click(); + expect(async () => await page.getByRole('button', { name: 'Logout' }).click()).toPass(); /*** * Login with the new user @@ -52,7 +65,7 @@ test.describe('Device registration tests', () => { await expect(page.getByText('SDK Automation - Sign On')).toBeVisible(); await page.getByRole('textbox', { name: 'Username' }).fill(username); await page.getByRole('textbox', { name: 'Password' }).fill(password); - await page.getByRole('button', { name: 'Sign On' }).click(); + expect(async () => await page.getByRole('button', { name: 'Sign On' }).click()).toPass(); /** Register a device */ await expect(page.getByText('Select Test Form')).toBeVisible(); @@ -83,10 +96,10 @@ test.describe('Device registration tests', () => { await page.getByRole('textbox', { name: 'Password' }).fill(password); await page.getByRole('textbox', { name: 'Given Name' }).fill('demouser'); await page.getByRole('textbox', { name: 'Family Name' }).fill('demouser'); - await page.getByRole('button', { name: 'Continue' }).click(); + expect(async () => await page.getByRole('button', { name: 'Continue' }).click()).toPass(); await expect(page.getByRole('heading', { name: 'Registration Complete' })).toBeVisible(); await page.getByRole('button', { name: 'Continue' }).click(); - await page.getByRole('button', { name: 'Logout' }).click(); + expect(async () => await page.getByRole('button', { name: 'Logout' }).click()).toPass(); /** * Login with the new user @@ -96,7 +109,7 @@ test.describe('Device registration tests', () => { await expect(page.getByText('SDK Automation - Sign On')).toBeVisible(); await page.getByRole('textbox', { name: 'Username' }).fill(username); await page.getByRole('textbox', { name: 'Password' }).fill(password); - await page.getByRole('button', { name: 'Sign On' }).click(); + expect(async () => await page.getByRole('button', { name: 'Sign On' }).click()).toPass(); /** Register a Device */ await expect(page.getByText('Select Test Form')).toBeVisible(); @@ -107,6 +120,6 @@ test.describe('Device registration tests', () => { await page.getByRole('textbox', { name: 'Enter Phone Number' }).fill('3035550100'); await page.getByRole('button', { name: 'Submit' }).click(); await expect(page.getByText('SMS/Voice MFA Registered')).toBeVisible(); - await page.getByRole('button', { name: 'Continue' }).click(); + expect(async () => await page.getByRole('button', { name: 'Continue' }).click()).toPass(); }); }); diff --git a/e2e/oidc-app/src/index.ts b/e2e/oidc-app/src/index.ts index 0f3a635122..5e4f4e84d2 100644 --- a/e2e/oidc-app/src/index.ts +++ b/e2e/oidc-app/src/index.ts @@ -8,3 +8,7 @@ */ import './styles.css'; + +// window.addEventListener('load', () => { +// console.log('loaded parent'); +// }); diff --git a/e2e/oidc-app/src/ping-am/index.html b/e2e/oidc-app/src/ping-am/index.html index f1021f29e4..ca16cca49e 100644 --- a/e2e/oidc-app/src/ping-am/index.html +++ b/e2e/oidc-app/src/ping-am/index.html @@ -7,12 +7,16 @@ #logout { display: none; } + #app { + display: none; + } + Home +

OIDC App | PingAM Login

+

Loading...

- Home -

OIDC App | PingAM Login

diff --git a/e2e/oidc-app/src/ping-am/main.ts b/e2e/oidc-app/src/ping-am/main.ts index ed4dcf7f91..e5c459dad6 100644 --- a/e2e/oidc-app/src/ping-am/main.ts +++ b/e2e/oidc-app/src/ping-am/main.ts @@ -22,5 +22,9 @@ const config = { 'https://openam-sdks.forgeblocks.com/am/oauth2/alpha/.well-known/openid-configuration', }, }; - -oidcApp({ config, urlParams }); +document.addEventListener('DOMContentLoaded', async () => { + console.log('loaded outside'); + // (async () => { + await oidcApp({ config, urlParams }); + // })(); +}); diff --git a/e2e/oidc-app/src/ping-one/index.html b/e2e/oidc-app/src/ping-one/index.html index f1a50104e1..6779d5b9b3 100644 --- a/e2e/oidc-app/src/ping-one/index.html +++ b/e2e/oidc-app/src/ping-one/index.html @@ -7,12 +7,16 @@ #logout { display: none; } + #app { + display: none; + } + Home +

OIDC App | PingOne Login

+

Loading...

- Home -

OIDC App | P1 Login

diff --git a/e2e/oidc-app/src/utils/oidc-app.ts b/e2e/oidc-app/src/utils/oidc-app.ts index eaa099bfff..78528d1304 100644 --- a/e2e/oidc-app/src/utils/oidc-app.ts +++ b/e2e/oidc-app/src/utils/oidc-app.ts @@ -18,6 +18,7 @@ import type { let tokenIndex = 0; function displayError(error) { + // const appEl = document.getElementById('app'); const errorEl = document.createElement('div'); errorEl.innerHTML = `

Error: ${JSON.stringify(error, null, 2)}

`; document.body.appendChild(errorEl); @@ -52,39 +53,51 @@ export async function oidcApp({ config, urlParams }) { const oidcClient = await oidc({ config }); if ('error' in oidcClient) { displayError(oidcClient); + } else if (oidcClient) { + document.getElementById('app').style.display = 'block'; + document.getElementById('loading').style.display = 'none'; } - document.getElementById('login-background').addEventListener('click', async () => { - const authorizeOptions: GetAuthorizationUrlOptions = - piflow === 'true' - ? { - clientId: config.clientId, - redirectUri: config.redirectUri, - scope: config.scope, - responseType: config.responseType ?? 'code', - responseMode: 'pi.flow', - } - : undefined; - const response = await oidcClient.authorize.background(authorizeOptions); - - if ('error' in response) { - console.error('Authorization Error:', response); - displayError(response); - - if (response.redirectUrl) { - window.location.assign(response.redirectUrl); - } else { - console.log('Authorization failed with no ability to redirect:', response); + console.log('oidc app called'); + // window.addEventListener('load', () => { + // console.log('loaded'); + const myButton = document.getElementById('login-background'); + if (myButton) { + console.log('button found'); + myButton.addEventListener('click', async () => { + const authorizeOptions: GetAuthorizationUrlOptions = + piflow === 'true' + ? { + clientId: config.clientId, + redirectUri: config.redirectUri, + scope: config.scope, + responseType: config.responseType ?? 'code', + responseMode: 'pi.flow', + } + : undefined; + const response = await oidcClient.authorize.background(authorizeOptions); + + if ('error' in response) { + console.error('Authorization Error:', response); + displayError(response); + + if (response.redirectUrl) { + window.location.assign(response.redirectUrl); + } else { + console.log('Authorization failed with no ability to redirect:', response); + } + return; + + // Handle success response from background authorization + } else if ('code' in response) { + console.log('Authorization Code:', response.code); + const tokenResponse = await oidcClient.token.exchange(response.code, response.state); + displayTokenResponse(tokenResponse); } - return; - - // Handle success response from background authorization - } else if ('code' in response) { - console.log('Authorization Code:', response.code); - const tokenResponse = await oidcClient.token.exchange(response.code, response.state); - displayTokenResponse(tokenResponse); - } - }); + }); + } else { + console.log('not found'); + } document.getElementById('login-redirect').addEventListener('click', async () => { const authorizeUrl = await oidcClient.authorize.url(); @@ -153,6 +166,7 @@ export async function oidcApp({ config, urlParams }) { window.location.assign(window.location.origin + window.location.pathname); } }); + // }); if (code && state) { const response = await oidcClient.token.exchange(code, state); diff --git a/e2e/oidc-suites/eslint.config.mjs b/e2e/oidc-suites/eslint.config.mjs index 98b9e5ae50..1ce0316b6a 100644 --- a/e2e/oidc-suites/eslint.config.mjs +++ b/e2e/oidc-suites/eslint.config.mjs @@ -1,4 +1,5 @@ import baseConfig from '../../eslint.config.mjs'; +import playwright from 'eslint-plugin-playwright'; export default [ ...baseConfig, @@ -19,4 +20,13 @@ export default [ // Override or add rules here rules: {}, }, + { + ...playwright.configs['flat/recommended'], + files: ['src/*.spec.ts', 'src/utils/async-events.ts'], + rules: { + ...playwright.configs['flat/recommended'].rules, + // Customize Playwright rules + // ... + }, + }, ]; diff --git a/e2e/oidc-suites/src/login.spec.ts b/e2e/oidc-suites/src/login.spec.ts index 1443be4c3d..113d5e338f 100644 --- a/e2e/oidc-suites/src/login.spec.ts +++ b/e2e/oidc-suites/src/login.spec.ts @@ -17,9 +17,11 @@ import { asyncEvents } from './utils/async-events.js'; test.describe('PingAM login and get token tests', () => { test('background login with valid credentials', async ({ page }) => { - const { navigate, clickButton } = asyncEvents(page); - await navigate('/ping-am/'); + const { /* navigate, */ clickButton } = asyncEvents(page); + // await page.goto('/ping-am/'); + await page.goto('/ping-am/'); expect(page.url()).toBe('http://localhost:8443/ping-am/'); + await expect(page.locator('#loading')).toBeHidden(); await clickButton('Login (Background)', '/authorize'); @@ -33,9 +35,10 @@ test.describe('PingAM login and get token tests', () => { }); test('redirect login with valid credentials', async ({ page }) => { - const { navigate, clickButton } = asyncEvents(page); - await navigate('/ping-am/'); + const { clickButton } = asyncEvents(page); + await page.goto('/ping-am/'); expect(page.url()).toBe('http://localhost:8443/ping-am/'); + await expect(page.locator('#loading')).toBeHidden(); await clickButton('Login (Redirect)', '/authorize'); @@ -49,9 +52,10 @@ test.describe('PingAM login and get token tests', () => { }); test('background login with invalid client id fails', async ({ page }) => { - const { navigate } = asyncEvents(page); - await navigate('/ping-am/?clientid=bad-id'); + // const { navigate } = asyncEvents(page); + await page.goto('/ping-am/?clientid=bad-id'); expect(page.url()).toBe('http://localhost:8443/ping-am/?clientid=bad-id'); + await expect(page.locator('#loading')).toBeHidden(); await page.getByRole('button', { name: 'Login (Background)' }).click(); @@ -65,9 +69,10 @@ test.describe('PingAM login and get token tests', () => { test.describe('PingOne login and get token tests', () => { test('background login with valid credentials', async ({ page }) => { - const { navigate, clickButton } = asyncEvents(page); - await navigate('/ping-one/'); + const { clickButton } = asyncEvents(page); + await page.goto('/ping-one/'); expect(page.url()).toBe('http://localhost:8443/ping-one/'); + await expect(page.locator('#loading')).toBeHidden(); await clickButton('Login (Background)', '/authorize'); @@ -82,9 +87,10 @@ test.describe('PingOne login and get token tests', () => { }); test('redirect login with valid credentials', async ({ page }) => { - const { navigate, clickButton } = asyncEvents(page); - await navigate('/ping-one/'); + const { clickButton } = asyncEvents(page); + await page.goto('/ping-one/'); expect(page.url()).toBe('http://localhost:8443/ping-one/'); + await expect(page.locator('#loading')).toBeHidden(); await clickButton('Login (Redirect)', '/authorize'); @@ -99,9 +105,10 @@ test.describe('PingOne login and get token tests', () => { }); test('login with invalid client id fails', async ({ page }) => { - const { navigate } = asyncEvents(page); - await navigate('/ping-one/?clientid=bad-id'); + // const { navigate } = asyncEvents(page); + await page.goto('/ping-one/?clientid=bad-id'); expect(page.url()).toBe('http://localhost:8443/ping-one/?clientid=bad-id'); + await expect(page.locator('#loading')).toBeHidden(); await page.getByRole('button', { name: 'Login (Background)' }).click(); @@ -113,11 +120,12 @@ test.describe('PingOne login and get token tests', () => { }); test('login with pi.flow response mode', async ({ page }) => { - const { navigate, clickButton } = asyncEvents(page); - await navigate('/ping-one/?piflow=true'); + const { clickButton } = asyncEvents(page); + await page.goto('/ping-one/?piflow=true'); expect(page.url()).toBe('http://localhost:8443/ping-one/?piflow=true'); + await expect(page.locator('#loading')).toBeHidden(); - await page.on('request', (request) => { + page.on('request', (request) => { const method = request.method(); const requestUrl = request.url(); @@ -140,9 +148,10 @@ test.describe('PingOne login and get token tests', () => { }); test('login with invalid state fails with error', async ({ page }) => { - const { navigate } = asyncEvents(page); - await navigate('/ping-am/?code=12345&state=abcxyz'); + // const { navigate } = asyncEvents(page); + await page.goto('/ping-am/?code=12345&state=abcxyz'); expect(page.url()).toBe('http://localhost:8443/ping-am/?code=12345&state=abcxyz'); + await expect(page.locator('#loading')).toBeHidden(); await expect(page.locator('.error')).toContainText(`"error": "State mismatch"`); await expect(page.locator('.error')).toContainText(`"type": "state_error"`); @@ -152,9 +161,10 @@ test('login with invalid state fails with error', async ({ page }) => { }); test('oidc client fails to initialize with bad wellknown', async ({ page }) => { - const { navigate } = asyncEvents(page); - await navigate('/ping-am/?wellknown=bad-wellknown'); + // const { navigate } = asyncEvents(page); + await page.goto('/ping-am/?wellknown=bad-wellknown'); expect(page.url()).toBe('http://localhost:8443/ping-am/?wellknown=bad-wellknown'); + await expect(page.locator('#loading')).toBeHidden(); await page.getByRole('button', { name: 'Login (Background)' }).click(); diff --git a/e2e/oidc-suites/src/logout.spec.ts b/e2e/oidc-suites/src/logout.spec.ts index e62f4eda9d..905b57a44e 100644 --- a/e2e/oidc-suites/src/logout.spec.ts +++ b/e2e/oidc-suites/src/logout.spec.ts @@ -38,10 +38,14 @@ test.describe('Logout tests', () => { await page.getByLabel('User Name').fill(pingAmUsername); await page.getByRole('textbox', { name: 'Password' }).fill(pingAmPassword); - await Promise.all([ - page.waitForURL('http://localhost:8443/ping-am/**'), - page.getByRole('button', { name: 'Next' }).click(), - ]); + const promise = page.waitForURL('http://localhost:8443/ping-am/**'); + await page.getByRole('button', { name: 'Next' }).click(); + + /** + * This block is flakey, changing to this pattern + * https://playwright.dev/docs/network#network-events + **/ + await promise; expect(page.url()).toContain('code'); expect(page.url()).toContain('state'); await expect(page.getByRole('button', { name: 'Login (Background)' })).toBeHidden(); diff --git a/e2e/oidc-suites/src/token.spec.ts b/e2e/oidc-suites/src/token.spec.ts index 6905eb4f1b..7b0ba011eb 100644 --- a/e2e/oidc-suites/src/token.spec.ts +++ b/e2e/oidc-suites/src/token.spec.ts @@ -105,17 +105,20 @@ test.describe('PingAM tokens', () => { test.describe('PingOne tokens', () => { test('login and get tokens', async ({ page }) => { - const { navigate, clickButton } = asyncEvents(page); - await navigate('/ping-one/'); + const { clickButton } = asyncEvents(page); + await page.goto('/ping-one/'); expect(page.url()).toBe('http://localhost:8443/ping-one/'); + await expect(page.locator('#loading')).toBeHidden(); await clickButton('Login (Background)', 'https://apps.pingone.ca/'); await page.getByLabel('Username').fill(pingOneUsername); await page.getByRole('textbox', { name: 'Password' }).fill(pingOnePassword); + const promise = page.waitForURL('http://localhost:8443/ping-one/**'); await page.getByRole('button', { name: 'Sign On' }).click(); - await page.waitForURL('http://localhost:8443/ping-one/**', { waitUntil: 'networkidle' }); + await promise; + await expect(page.locator('#loading')).toBeHidden(); expect(page.url()).toContain('code'); expect(page.url()).toContain('state'); @@ -128,17 +131,20 @@ test.describe('PingOne tokens', () => { }); test('login and renew tokens', async ({ page }) => { - const { navigate, clickButton } = asyncEvents(page); - await navigate('/ping-one/'); + const { clickButton } = asyncEvents(page); + await page.goto('/ping-one/'); expect(page.url()).toBe('http://localhost:8443/ping-one/'); + await expect(page.locator('#loading')).toBeHidden(); await clickButton('Login (Background)', 'https://apps.pingone.ca/'); await page.getByLabel('Username').fill(pingOneUsername); await page.getByRole('textbox', { name: 'Password' }).fill(pingOnePassword); + const promise = page.waitForURL('http://localhost:8443/ping-one/**'); await page.getByRole('button', { name: 'Sign On' }).click(); - await page.waitForURL('http://localhost:8443/ping-one/**', { waitUntil: 'networkidle' }); + await promise; + await expect(page.locator('#loading')).toBeHidden(); expect(page.url()).toContain('code'); expect(page.url()).toContain('state'); @@ -153,17 +159,20 @@ test.describe('PingOne tokens', () => { }); test('login and revoke tokens', async ({ page }) => { - const { navigate, clickButton } = asyncEvents(page); - await navigate('/ping-one/'); + const { clickButton } = asyncEvents(page); + await page.goto('/ping-one/'); expect(page.url()).toBe('http://localhost:8443/ping-one/'); + await expect(page.locator('#loading')).toBeHidden(); await clickButton('Login (Background)', 'https://apps.pingone.ca/'); await page.getByLabel('Username').fill(pingOneUsername); await page.getByRole('textbox', { name: 'Password' }).fill(pingOnePassword); + const promise = page.waitForURL('http://localhost:8443/ping-one/**'); await page.getByRole('button', { name: 'Sign On' }).click(); - await page.waitForURL('http://localhost:8443/ping-one/**', { waitUntil: 'networkidle' }); + await promise; + await expect(page.locator('#loading')).toBeHidden(); expect(page.url()).toContain('code'); expect(page.url()).toContain('state'); @@ -176,9 +185,10 @@ test.describe('PingOne tokens', () => { }); test('renew tokens without logging in should error', async ({ page }) => { - const { navigate } = asyncEvents(page); - await navigate('/ping-one/'); + // const { clickButton } = asyncEvents(page); + await page.goto('/ping-one/'); expect(page.url()).toBe('http://localhost:8443/ping-one/'); + await expect(page.locator('#loading')).toBeHidden(); await page.getByRole('button', { name: 'Renew Tokens' }).click(); diff --git a/e2e/oidc-suites/src/utils/async-events.ts b/e2e/oidc-suites/src/utils/async-events.ts index b0874e9145..1563894a59 100644 --- a/e2e/oidc-suites/src/utils/async-events.ts +++ b/e2e/oidc-suites/src/utils/async-events.ts @@ -4,7 +4,9 @@ * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ -export function asyncEvents(page) { +import type { Page } from '@playwright/test'; + +export function asyncEvents(page: Page) { return { async clickButton(text, endpoint) { if (!endpoint) @@ -74,6 +76,6 @@ export async function verifyUserInfo(page, expect, type) { // Just wait for one of them to be visible await name.waitFor(); - expect(await name.textContent()).toBe(nameString); - expect(await email.textContent()).toBe(emailString); + await expect(name).toHaveText(nameString); + await expect(email).toHaveText(emailString); } diff --git a/package.json b/package.json index 0796bb1c0f..8a8209db59 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "eslint-config-prettier": "10.1.8", "eslint-plugin-import": "2.31.0", "eslint-plugin-package-json": "0.30.0", - "eslint-plugin-playwright": "^2.0.0", + "eslint-plugin-playwright": "^2.2.2", "eslint-plugin-prettier": "^5.2.3", "fast-check": "^4.0.0", "husky": "^9.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 70ee1a0f92..7c4e773f4e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -170,7 +170,7 @@ importers: specifier: 0.30.0 version: 0.30.0(@types/estree@1.0.8)(eslint@9.38.0(jiti@2.6.1))(jsonc-eslint-parser@2.4.1) eslint-plugin-playwright: - specifier: ^2.0.0 + specifier: ^2.2.2 version: 2.2.2(eslint@9.38.0(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.2.3