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) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-09-10 15:46:32 +02:00
1 parent 60b9006506
commit 0b79afdc0c
2 files changed
+23 -10

No files matched your search

@@ -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) },
)
@@ -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 <script> with proper query param
function loadScriptSequential(url) {