diff --git a/e2e/davinci-app/index.html b/e2e/davinci-app/index.html
index 361683dc55..c42a64f324 100644
--- a/e2e/davinci-app/index.html
+++ b/e2e/davinci-app/index.html
@@ -15,9 +15,7 @@
-
+
diff --git a/e2e/davinci-app/main.ts b/e2e/davinci-app/main.ts
index b5e3e2da8c..cd8dea7507 100644
--- a/e2e/davinci-app/main.ts
+++ b/e2e/davinci-app/main.ts
@@ -177,7 +177,6 @@ const urlParams = new URLSearchParams(window.location.search);
formEl.innerHTML = '';
const clientInfo = davinciClient.getClient();
- //const clientInfo = node.client;
let formName = '';
@@ -191,11 +190,12 @@ const urlParams = new URLSearchParams(window.location.search);
const error = davinciClient.getError();
if (error) {
- formEl.appendChild(document.createElement('div')).setAttribute('id', 'error-div');
- const errorDiv = formEl.querySelector('#error-div');
- if (errorDiv && clientInfo?.status === 'continue') {
+ const errorDiv = document.createElement('div');
+ formEl.appendChild(errorDiv).setAttribute('id', 'error-div');
+ if (errorDiv && clientInfo?.status === 'error') {
+ errorDiv.style.color = 'red';
errorDiv.innerHTML = `
- ${davinciClient.getError()?.message}
+ Error: ${davinciClient.getError()?.message}
`;
}
}
diff --git a/e2e/davinci-app/server-configs.ts b/e2e/davinci-app/server-configs.ts
index 10a49423de..2ddbc19c87 100644
--- a/e2e/davinci-app/server-configs.ts
+++ b/e2e/davinci-app/server-configs.ts
@@ -77,4 +77,16 @@ export const serverConfigs: Record = {
'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration',
},
},
+ /**
+ * Polling
+ */
+ '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0': {
+ clientId: '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0',
+ redirectUri: window.location.origin + '/',
+ scope: 'openid profile email revoke',
+ serverConfig: {
+ wellknown:
+ 'https://auth.pingone.ca/356a254c-cba3-4ade-be1a-860136e8df01/as/.well-known/openid-configuration',
+ },
+ },
};
diff --git a/e2e/davinci-app/style.css b/e2e/davinci-app/style.css
index ced966e4a6..fcc585cb69 100644
--- a/e2e/davinci-app/style.css
+++ b/e2e/davinci-app/style.css
@@ -81,6 +81,10 @@ input {
color: #888;
}
+.error {
+ color: red;
+}
+
button {
border-radius: 8px;
border: 1px solid transparent;
diff --git a/e2e/davinci-suites/src/polling.test.ts b/e2e/davinci-suites/src/polling.test.ts
new file mode 100644
index 0000000000..771b0ae700
--- /dev/null
+++ b/e2e/davinci-suites/src/polling.test.ts
@@ -0,0 +1,178 @@
+/*
+ * Copyright (c) 2026 Ping Identity Corporation. All rights reserved.
+ *
+ * This software may be modified and distributed under the terms
+ * of the MIT license. See the LICENSE file for details.
+ */
+import { expect, test } from '@playwright/test';
+import { asyncEvents } from './utils/async-events.js';
+
+test.describe('Challenge Polling', () => {
+ test('should succeed when opening magic link', async ({ page, browser }) => {
+ const clientId = '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0';
+ const davinciPolicy = 'f40b544a4dfb575daa0cf5e9487c206a';
+ const { navigate } = asyncEvents(page);
+ await navigate(`/?clientId=${clientId}&acr_values=${davinciPolicy}`);
+
+ await expect(page.url()).toBe(
+ `http://localhost:5829/?clientId=${clientId}&acr_values=${davinciPolicy}`,
+ );
+
+ await page.getByRole('button', { name: 'Sign On' }).click();
+ await expect(page.getByRole('heading', { name: 'Polling' })).toBeVisible();
+
+ // Get magic link
+ const linkLocator = page.getByText('Number Challenge https://auth.pingone');
+ await expect(linkLocator).toBeVisible();
+
+ const linkLocatorText = await linkLocator.innerText();
+ const magicLink = linkLocatorText.split('Number Challenge ')[1];
+ expect(magicLink.startsWith('https://auth.pingone'));
+
+ // Start polling
+ await page.getByRole('button', { name: 'Start polling' }).click();
+ await expect(page.getByText('Polling...')).toBeVisible();
+
+ // Go to magic link in another browser to complete challenge
+ const newContext = await browser.newContext();
+ const newPage = await newContext.newPage();
+ await newPage.goto(magicLink);
+ await expect(newPage.getByText('Close me')).toBeVisible();
+ await newContext.close();
+
+ // Check for success
+ await expect(page.getByText('Message: approved')).toBeVisible();
+ });
+
+ test('should timeout when retries are exhausted', async ({ page }) => {
+ const clientId = '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0';
+ const davinciPolicy = 'f40b544a4dfb575daa0cf5e9487c206a';
+ const { navigate } = asyncEvents(page);
+ await navigate(`/?clientId=${clientId}&acr_values=${davinciPolicy}`);
+
+ await expect(page.url()).toBe(
+ `http://localhost:5829/?clientId=${clientId}&acr_values=${davinciPolicy}`,
+ );
+
+ await page.getByRole('button', { name: 'Sign On' }).click();
+ await expect(page.getByRole('heading', { name: 'Polling' })).toBeVisible();
+
+ // Track poll retries
+ let numPollRequests = 0;
+ page.on('request', (request) => {
+ const method = request.method();
+ const requestUrl = request.url();
+
+ if (method === 'POST' && requestUrl.includes('/status')) {
+ numPollRequests++;
+ }
+ });
+
+ // Start polling
+ await page.getByRole('button', { name: 'Start polling' }).click();
+ await expect(page.getByText('Polling...')).toBeVisible();
+
+ // Wait for timeout
+ const pollInterval = 2000; // milliseconds
+ const maxRetries = 5;
+ await expect(page.getByText('Error: timedOut')).toBeVisible({
+ timeout: 2 * pollInterval * maxRetries,
+ });
+
+ // Check max retry count
+ expect(numPollRequests === maxRetries);
+ });
+
+ test('should return expired status after challenge expires', async ({ page }) => {
+ const clientId = '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0';
+ const davinciPolicy = 'f40b544a4dfb575daa0cf5e9487c206a';
+ const { navigate } = asyncEvents(page);
+ await navigate(`/?clientId=${clientId}&acr_values=${davinciPolicy}`);
+
+ await expect(page.url()).toBe(
+ `http://localhost:5829/?clientId=${clientId}&acr_values=${davinciPolicy}`,
+ );
+
+ await page.getByRole('button', { name: 'Sign On' }).click();
+ await expect(page.getByRole('heading', { name: 'Polling' })).toBeVisible();
+
+ // Track poll retries
+ let numPollRequests = 0;
+ page.on('request', (request) => {
+ const method = request.method();
+ const requestUrl = request.url();
+
+ if (method === 'POST' && requestUrl.includes('/status')) {
+ numPollRequests++;
+ }
+ });
+
+ // Wait for challenge to expire
+ const challengeExpiry = 15000; // milliseconds
+ await page.waitForTimeout(challengeExpiry + 5000);
+
+ // Start polling
+ await page.getByRole('button', { name: 'Start polling' }).click();
+ await expect(page.getByText('Polling...')).toBeVisible();
+
+ // Check for expired status
+ await expect(page.getByText('Error: expired')).toBeVisible();
+
+ // Check poll count
+ expect(numPollRequests === 0);
+ });
+});
+
+test.describe('Continue Polling', () => {
+ test('should succeed on QR code scan simulation', async ({ page }) => {
+ const clientId = '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0';
+ const davinciPolicy = '27aacf0efcc480dfcd00b04be8023cdc';
+ const { navigate } = asyncEvents(page);
+ await navigate(`/?clientId=${clientId}&acr_values=${davinciPolicy}`);
+
+ await expect(page.url()).toBe(
+ `http://localhost:5829/?clientId=${clientId}&acr_values=${davinciPolicy}`,
+ );
+
+ await expect(page.getByRole('heading', { name: 'Select Continue Polling Test' })).toBeVisible();
+ await page.getByRole('button', { name: 'Success' }).click();
+ await expect(page.getByRole('heading', { name: 'Polling' })).toBeVisible();
+
+ // Start polling
+ const numberCounterSuccess = 2;
+ for (let i = 0; i < numberCounterSuccess; i++) {
+ await page.getByRole('button', { name: 'Start polling' }).click();
+ await expect(page.getByText('Polling...')).toBeVisible();
+ await expect(page.getByRole('button', { name: 'Start polling' })).toBeDisabled();
+ }
+
+ // Check for success
+ await expect(page.getByText('Message: Done')).toBeVisible();
+ });
+
+ test('should timeout when retries are exhausted', async ({ page }) => {
+ const clientId = '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0';
+ const davinciPolicy = '27aacf0efcc480dfcd00b04be8023cdc';
+ const { navigate } = asyncEvents(page);
+ await navigate(`/?clientId=${clientId}&acr_values=${davinciPolicy}`);
+
+ await expect(page.url()).toBe(
+ `http://localhost:5829/?clientId=${clientId}&acr_values=${davinciPolicy}`,
+ );
+
+ await expect(page.getByRole('heading', { name: 'Select Continue Polling Test' })).toBeVisible();
+ await page.getByRole('button', { name: 'Timeout' }).click();
+ await expect(page.getByRole('heading', { name: 'Polling' })).toBeVisible();
+
+ // Start polling
+ const maxRetries = 3;
+ for (let i = 0; i < maxRetries + 1; i++) {
+ await page.getByRole('button', { name: 'Start polling' }).click();
+ await expect(page.getByText('Polling...')).toBeVisible();
+ await expect(page.getByRole('button', { name: 'Start polling' })).toBeDisabled();
+ }
+
+ // Check for timeout
+ await expect(page.getByText('Error: timedOut')).toBeVisible();
+ });
+});
diff --git a/e2e/davinci-suites/src/protect.test.ts b/e2e/davinci-suites/src/protect.test.ts
index 8534a6043c..4830bb6b22 100644
--- a/e2e/davinci-suites/src/protect.test.ts
+++ b/e2e/davinci-suites/src/protect.test.ts
@@ -17,7 +17,7 @@ test('Test Protect collector with Custom HTML component', async ({ page }) => {
await expect(page.getByText('JS Protect - Custom HTML Form')).toBeVisible();
- const requests = [];
+ const requests: string[] = [];
page.on('request', (request) => {
const method = request.method();
const requestUrl = request.url();
@@ -54,7 +54,7 @@ test('Test Protect collector with P1 Forms component', async ({ page }) => {
await expect(page.getByText('Example - Sign On')).toBeVisible();
- const requests = [];
+ const requests: string[] = [];
page.on('request', (request) => {
const method = request.method();
const requestUrl = request.url();
diff --git a/lefthook.yml b/lefthook.yml
index 812fd13b1d..39a02c602d 100644
--- a/lefthook.yml
+++ b/lefthook.yml
@@ -5,9 +5,6 @@ pre-commit:
nx-check:
run: pnpm nx affected -t typecheck lint build api-report --tui=false
stage_fixed: true
- format:
- run: pnpm nx format:write
- stage_fixed: true
interface-mapping:
glob: >-
{tools/interface-mapping-validator/**/*.ts,
@@ -24,7 +21,9 @@ pre-commit:
echo "Interface mapping is out of sync." &&
echo "Run: pnpm mapping:generate" &&
exit 1)
-
+ format:
+ run: pnpm nx format:write
+ stage_fixed: true
commit-msg:
commands:
commitlint: