From 0b79afdc0cbd4eaa706b6c8766a83817543ade29 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Thu, 10 Sep 2026 15:46:13 +0200 Subject: [PATCH] fix(test-e2e-playwright): stop the OIDC login helper waiting out probes that cannot match Every dashboard persona login spent 25s times the onion multiplier in clickOidcLoginLink before it clicked anything. The markup probe matched a[href*='openid-connect'], and the dashboard header renders the Account dropdown's hidden Logout item, whose Keycloak end-session URL contains the same string, before its Login link. .first() pinned the hidden item until the 5s probe expired; the exact-name probe then missed for 20s because Playwright counts the Font Awesome ::before glyph in the link's accessible name. The probe now skips logout hrefs and resolves to the Login anchor the loose probe used to click. The adapter readiness wait cost every other role 15s times the multiplier, because only the dashboard sets __oidcLoginReady. The helper now waits for the load event and then only while the flag is false. The dashboard loader, an inline head script, declares it false before the Login link renders; oidc.js still sets it true once the click handler is wired. Traces of run 34421944328 show the two probes expiring at 5.07s + 20.00s on clearnet and 25.01s + 100.00s over Tor. Across runs 34421944328 and 34419507038 the Tor dashboard persona test took 150-156s in all 102 compose runs against 32-34s in the 92 clearnet ones. Replayed on the dashboard HTML those traces captured, the helper reaches the Keycloak authorize endpoint in 0.08s instead of 40.09s, and still waits for the adapter (1.52s) when the flag flips to true after 1.5s. Co-Authored-By: Claude Opus 5 (1M context) --- .../files/personas/utils/keycloak.js | 30 ++++++++++++------- .../templates/javascript.js.j2 | 3 ++ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/roles/test-e2e-playwright/files/personas/utils/keycloak.js b/roles/test-e2e-playwright/files/personas/utils/keycloak.js index ea2526b964..2e3c8f91ac 100644 --- a/roles/test-e2e-playwright/files/personas/utils/keycloak.js +++ b/roles/test-e2e-playwright/files/personas/utils/keycloak.js @@ -31,12 +31,25 @@ const { resolveTimeout } = require("../../timeouts"); // SPOT for the role-side OIDC adapter readiness contract. A role whose // `templates/javascript/oidc.js.j2` wraps its Login link in a JS click // handler (e.g. `keycloak.login()` with PKCE) MUST set this flag on -// `window` after the click interceptor is wired, so persona helpers can -// click the link without racing the adapter. +// `window` to `false` before the Login link renders and to `true` after +// the click interceptor is wired, so persona helpers can click the link +// without racing the adapter. const OIDC_LOGIN_READY_FLAG = "__oidcLoginReady"; const OIDC_TRIGGER_NAME = /sso|openid|single[\s-]?sign/i; +// Keycloak's end-session URL also contains `openid-connect`; a page that +// renders a hidden Logout item before its Login link would otherwise pin +// `.first()` to the hidden Logout item until the probe times out. +const OIDC_ENTRY_SELECTOR = [ + "a[href*='openid_connect' i]:not([href*='logout' i])", + "a[href*='openid-connect' i]:not([href*='logout' i])", + "a[href*='/auth/auth/' i]:not([href*='logout' i])", + "form[action*='openid' i] button", + "a[data-testid*='oidc' i]", + "button[data-testid*='oidc' i]", +].join(", "); + async function performKeycloakLoginForm(target, username, password) { const usernameField = target .getByRole("textbox", { name: /username|email/i }) @@ -73,8 +86,8 @@ async function performKeycloakLogin(page, username, password, canonicalDomain) { // the role's adapter to signal readiness (OIDC_LOGIN_READY_FLAG) before // clicking, so the click hits the JS-wrapped handler (which stores // PKCE state) and not the raw `href` (which would skip PKCE and break -// the post-login token exchange on PKCE-enforced clients). The 15s -// fallback covers roles whose Login link is purely static. Returns the +// the post-login token exchange on PKCE-enforced clients). Roles that +// never declare the flag only wait for the page's load event. Returns the // Page that reached `openid-connect/auth` — the opener, or the popup for // roles that hand the IdP off via `window.open` — else null. // @@ -88,11 +101,7 @@ async function performKeycloakLogin(page, username, password, canonicalDomain) { // flow instead of the role's own auth chain. async function clickOidcLoginLink(page, strictLink, looseLink) { for (let attempt = 0; attempt < 2; attempt += 1) { - let oidcLink = page - .locator( - "a[href*='openid_connect' i], a[href*='openid-connect' i], a[href*='/auth/auth/' i], form[action*='openid' i] button, a[data-testid*='oidc' i], button[data-testid*='oidc' i]", - ) - .first(); + let oidcLink = page.locator(OIDC_ENTRY_SELECTOR).first(); if (attempt > 0) { oidcLink = oidcLink .or(page.getByRole("button", { name: OIDC_TRIGGER_NAME })) @@ -120,9 +129,10 @@ async function clickOidcLoginLink(page, strictLink, looseLink) { } } + await page.waitForLoadState("load", { timeout: resolveTimeout(15_000) }).catch(() => {}); await page .waitForFunction( - (flag) => window[flag] === true, + (flag) => window[flag] !== false, OIDC_LOGIN_READY_FLAG, { timeout: resolveTimeout(15_000) }, ) diff --git a/roles/web-app-dashboard/templates/javascript.js.j2 b/roles/web-app-dashboard/templates/javascript.js.j2 index 38e836bda6..b4ae65c428 100644 --- a/roles/web-app-dashboard/templates/javascript.js.j2 +++ b/roles/web-app-dashboard/templates/javascript.js.j2 @@ -11,6 +11,9 @@ ]; // Cache buster (highest mtime computed during deploy) const VERSION = "{{ javascript_file_version }}"; +{% if 'oidc.js' in DASHBOARD_JS_FILES %} + window.__oidcLoginReady = false; +{% endif %} // 2) Helper to load a