Skip to content

Commit 62dc8ad

Browse files
authored
Merge pull request #634 from ForgeRock/SDKS-4929-polling-e2e
test(davinci-client): add polling e2e tests
2 parents 5f55b08 + b8bb92d commit 62dc8ad

7 files changed

Lines changed: 205 additions & 14 deletions

File tree

e2e/davinci-app/index.html

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@
1515
<a href="https://www.typescriptlang.org/" target="_blank">
1616
<img src="./public/typescript.svg" class="logo vanilla" alt="TypeScript logo" />
1717
</a>
18-
<form id="form">
19-
<div class="error-div"></div>
20-
</form>
18+
<form id="form"></form>
2119
</div>
2220
</div>
2321
<script type="module" src="main.ts"></script>

e2e/davinci-app/main.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,6 @@ const urlParams = new URLSearchParams(window.location.search);
177177
formEl.innerHTML = '';
178178

179179
const clientInfo = davinciClient.getClient();
180-
//const clientInfo = node.client;
181180

182181
let formName = '';
183182

@@ -191,11 +190,12 @@ const urlParams = new URLSearchParams(window.location.search);
191190

192191
const error = davinciClient.getError();
193192
if (error) {
194-
formEl.appendChild(document.createElement('div')).setAttribute('id', 'error-div');
195-
const errorDiv = formEl.querySelector('#error-div');
196-
if (errorDiv && clientInfo?.status === 'continue') {
193+
const errorDiv = document.createElement('div');
194+
formEl.appendChild(errorDiv).setAttribute('id', 'error-div');
195+
if (errorDiv && clientInfo?.status === 'error') {
196+
errorDiv.style.color = 'red';
197197
errorDiv.innerHTML = `
198-
<div>${davinciClient.getError()?.message}</div>
198+
<p><strong>Error</strong>: ${davinciClient.getError()?.message}</p>
199199
`;
200200
}
201201
}

e2e/davinci-app/server-configs.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,16 @@ export const serverConfigs: Record<string, DaVinciConfig> = {
7777
'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration',
7878
},
7979
},
80+
/**
81+
* Polling
82+
*/
83+
'31a587ce-9aa4-4f36-a09f-78cd8a0a74a0': {
84+
clientId: '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0',
85+
redirectUri: window.location.origin + '/',
86+
scope: 'openid profile email revoke',
87+
serverConfig: {
88+
wellknown:
89+
'https://auth.pingone.ca/356a254c-cba3-4ade-be1a-860136e8df01/as/.well-known/openid-configuration',
90+
},
91+
},
8092
};

e2e/davinci-app/style.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ input {
8181
color: #888;
8282
}
8383

84+
.error {
85+
color: red;
86+
}
87+
8488
button {
8589
border-radius: 8px;
8690
border: 1px solid transparent;
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/*
2+
* Copyright (c) 2026 Ping Identity Corporation. All rights reserved.
3+
*
4+
* This software may be modified and distributed under the terms
5+
* of the MIT license. See the LICENSE file for details.
6+
*/
7+
import { expect, test } from '@playwright/test';
8+
import { asyncEvents } from './utils/async-events.js';
9+
10+
test.describe('Challenge Polling', () => {
11+
test('should succeed when opening magic link', async ({ page, browser }) => {
12+
const clientId = '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0';
13+
const davinciPolicy = 'f40b544a4dfb575daa0cf5e9487c206a';
14+
const { navigate } = asyncEvents(page);
15+
await navigate(`/?clientId=${clientId}&acr_values=${davinciPolicy}`);
16+
17+
await expect(page.url()).toBe(
18+
`http://localhost:5829/?clientId=${clientId}&acr_values=${davinciPolicy}`,
19+
);
20+
21+
await page.getByRole('button', { name: 'Sign On' }).click();
22+
await expect(page.getByRole('heading', { name: 'Polling' })).toBeVisible();
23+
24+
// Get magic link
25+
const linkLocator = page.getByText('Number Challenge https://auth.pingone');
26+
await expect(linkLocator).toBeVisible();
27+
28+
const linkLocatorText = await linkLocator.innerText();
29+
const magicLink = linkLocatorText.split('Number Challenge ')[1];
30+
expect(magicLink).toContain('https://auth.pingone');
31+
32+
// Start polling
33+
await page.getByRole('button', { name: 'Start polling' }).click();
34+
await expect(page.getByText('Polling...')).toBeVisible();
35+
36+
// Go to magic link in another browser to complete challenge
37+
const newContext = await browser.newContext();
38+
const newPage = await newContext.newPage();
39+
await newPage.goto(magicLink);
40+
await expect(newPage.getByText('Close me')).toBeVisible();
41+
await newContext.close();
42+
43+
// Check for success
44+
await expect(page.getByText('Message: approved')).toBeVisible();
45+
});
46+
47+
test('should timeout when retries are exhausted', async ({ page }) => {
48+
const clientId = '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0';
49+
const davinciPolicy = 'f40b544a4dfb575daa0cf5e9487c206a';
50+
const { navigate } = asyncEvents(page);
51+
await navigate(`/?clientId=${clientId}&acr_values=${davinciPolicy}`);
52+
53+
await expect(page.url()).toBe(
54+
`http://localhost:5829/?clientId=${clientId}&acr_values=${davinciPolicy}`,
55+
);
56+
57+
await page.getByRole('button', { name: 'Sign On' }).click();
58+
await expect(page.getByRole('heading', { name: 'Polling' })).toBeVisible();
59+
60+
// Track poll retries
61+
let numPollRequests = 0;
62+
page.on('request', (request) => {
63+
const method = request.method();
64+
const requestUrl = request.url();
65+
66+
if (method === 'POST' && requestUrl.includes('/status')) {
67+
numPollRequests++;
68+
}
69+
});
70+
71+
// Start polling
72+
await page.getByRole('button', { name: 'Start polling' }).click();
73+
await expect(page.getByText('Polling...')).toBeVisible();
74+
75+
// Wait for timeout
76+
const pollInterval = 2000; // milliseconds
77+
const maxRetries = 5;
78+
await expect(page.getByText('Error: timedOut')).toBeVisible({
79+
timeout: 2 * pollInterval * maxRetries,
80+
});
81+
82+
// Check max retry count
83+
expect(numPollRequests).toBe(maxRetries);
84+
});
85+
86+
test('should return expired status after challenge expires', async ({ page }) => {
87+
const clientId = '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0';
88+
const davinciPolicy = 'f40b544a4dfb575daa0cf5e9487c206a';
89+
const { navigate } = asyncEvents(page);
90+
await navigate(`/?clientId=${clientId}&acr_values=${davinciPolicy}`);
91+
92+
await expect(page.url()).toBe(
93+
`http://localhost:5829/?clientId=${clientId}&acr_values=${davinciPolicy}`,
94+
);
95+
96+
await page.getByRole('button', { name: 'Sign On' }).click();
97+
await expect(page.getByRole('heading', { name: 'Polling' })).toBeVisible();
98+
99+
// Track poll retries
100+
let numPollRequests = 0;
101+
page.on('request', (request) => {
102+
const method = request.method();
103+
const requestUrl = request.url();
104+
105+
if (method === 'POST' && requestUrl.includes('/status')) {
106+
numPollRequests++;
107+
}
108+
});
109+
110+
// Wait for challenge to expire
111+
const challengeExpiry = 15000; // milliseconds
112+
await page.waitForTimeout(challengeExpiry + 5000);
113+
114+
// Start polling
115+
await page.getByRole('button', { name: 'Start polling' }).click();
116+
await expect(page.getByText('Polling...')).toBeVisible();
117+
118+
// Check for expired status
119+
await expect(page.getByText('Error: expired')).toBeVisible();
120+
121+
// Check poll count
122+
expect(numPollRequests).toBe(1);
123+
});
124+
});
125+
126+
test.describe('Continue Polling', () => {
127+
test('should succeed on QR code scan simulation', async ({ page }) => {
128+
const clientId = '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0';
129+
const davinciPolicy = '27aacf0efcc480dfcd00b04be8023cdc';
130+
const { navigate } = asyncEvents(page);
131+
await navigate(`/?clientId=${clientId}&acr_values=${davinciPolicy}`);
132+
133+
await expect(page.url()).toBe(
134+
`http://localhost:5829/?clientId=${clientId}&acr_values=${davinciPolicy}`,
135+
);
136+
137+
await expect(page.getByRole('heading', { name: 'Select Continue Polling Test' })).toBeVisible();
138+
await page.getByRole('button', { name: 'Success' }).click();
139+
await expect(page.getByRole('heading', { name: 'Polling' })).toBeVisible();
140+
141+
// Start polling
142+
const numberCounterSuccess = 2;
143+
for (let i = 0; i < numberCounterSuccess; i++) {
144+
await page.getByRole('button', { name: 'Start polling' }).click();
145+
await expect(page.getByText('Polling...')).toBeVisible();
146+
await expect(page.getByRole('button', { name: 'Start polling' })).toBeDisabled();
147+
}
148+
149+
// Check for success
150+
await expect(page.getByText('Message: Done')).toBeVisible();
151+
});
152+
153+
test('should timeout when retries are exhausted', async ({ page }) => {
154+
const clientId = '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0';
155+
const davinciPolicy = '27aacf0efcc480dfcd00b04be8023cdc';
156+
const { navigate } = asyncEvents(page);
157+
await navigate(`/?clientId=${clientId}&acr_values=${davinciPolicy}`);
158+
159+
await expect(page.url()).toBe(
160+
`http://localhost:5829/?clientId=${clientId}&acr_values=${davinciPolicy}`,
161+
);
162+
163+
await expect(page.getByRole('heading', { name: 'Select Continue Polling Test' })).toBeVisible();
164+
await page.getByRole('button', { name: 'Timeout' }).click();
165+
await expect(page.getByRole('heading', { name: 'Polling' })).toBeVisible();
166+
167+
// Start polling
168+
const maxRetries = 3;
169+
for (let i = 0; i < maxRetries + 1; i++) {
170+
await page.getByRole('button', { name: 'Start polling' }).click();
171+
await expect(page.getByText('Polling...')).toBeVisible();
172+
await expect(page.getByRole('button', { name: 'Start polling' })).toBeDisabled();
173+
}
174+
175+
// Check for timeout
176+
await expect(page.getByText('Error: timedOut')).toBeVisible();
177+
});
178+
});

e2e/davinci-suites/src/protect.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ test('Test Protect collector with Custom HTML component', async ({ page }) => {
1717

1818
await expect(page.getByText('JS Protect - Custom HTML Form')).toBeVisible();
1919

20-
const requests = [];
20+
const requests: string[] = [];
2121
page.on('request', (request) => {
2222
const method = request.method();
2323
const requestUrl = request.url();
@@ -54,7 +54,7 @@ test('Test Protect collector with P1 Forms component', async ({ page }) => {
5454

5555
await expect(page.getByText('Example - Sign On')).toBeVisible();
5656

57-
const requests = [];
57+
const requests: string[] = [];
5858
page.on('request', (request) => {
5959
const method = request.method();
6060
const requestUrl = request.url();

lefthook.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@ pre-commit:
55
nx-check:
66
run: pnpm nx affected -t typecheck lint build api-report --tui=false
77
stage_fixed: true
8-
format:
9-
run: pnpm nx format:write
10-
stage_fixed: true
118
interface-mapping:
129
glob: >-
1310
{tools/interface-mapping-validator/**/*.ts,
@@ -24,7 +21,9 @@ pre-commit:
2421
echo "Interface mapping is out of sync." &&
2522
echo "Run: pnpm mapping:generate" &&
2623
exit 1)
27-
24+
format:
25+
run: pnpm nx format:write
26+
stage_fixed: true
2827
commit-msg:
2928
commands:
3029
commitlint:

0 commit comments

Comments
 (0)