Files
kevinveenbirkenbachandClaude Opus 5 b582846ab3 fix(test-e2e-playwright): make the Keycloak redirect and login-control checks able to fail
performKeycloakLogin polled the page URL with toContain(canonicalDomain).
On the Keycloak auth page the app domain already sits in redirect_uri,
so the poll was satisfied before the login had completed. The denial
path and the biber/admin iframe checks used the same substring test.
All of them now compare hostnameOf(url) with the canonical domain, which
is a bare hostname (env.js reads it with /\.onion$/).

LOGIN_CONTROL_NAME never matched a login link that carries an
icon-font glyph: Playwright's accessible name includes the private-use
character, and the pattern only allowed whitespace before the text. The
post-login waitFor({state: "hidden"}) therefore passed at once, even
before oidc.js had swapped the link. The pattern now admits \p{Co}
before the text, so that wait really waits for the swap.

dom.test.js pins both: glyph-prefixed login controls match while Logout
and Accessories do not, an IdP URL carrying the app domain in
redirect_uri is rejected, a look-alike suffix is rejected, and
about:blank yields an empty host.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 11:15:10 +02:00

92 lines
2.9 KiB
JavaScript

/**
* Small DOM / locator utilities shared by Playwright specs.
*
* `isVisible(locator)`
* Resolve to `true` iff `locator.first()` is currently visible.
* Swallows the strict-mode / detached-element errors so callers
* can treat the result as a plain truthy check.
*
* `waitForFrameUrl(iframeLocator, matcher, timeout, errorMessage)`
* Poll the URL of the frame embedded by `iframeLocator` until it
* contains `matcher`. Used by specs that drive a flow inside a
* fullscreen iframe (dashboard wraps services this way) and need
* to assert the iframe URL settled on a known endpoint.
*
* `findFirstVisibleCandidate(candidates)`
* Walk a list of `{ locator, … }` candidates and return the first
* one whose `locator.first()` is visible. Returns `null` when none
* are visible. The returned object preserves caller-supplied
* metadata fields (e.g. role-specific labels) and rebinds `locator`
* to the resolved single-element locator.
*
* `escapeRegex(value)`
* Escape every character that has special meaning in a JavaScript
* regular-expression source string. Suitable for embedding a raw
* string into a `new RegExp(...)` or `getByRole({ name: new RegExp(`^${escapeRegex(s)}$`) })`.
*
* `hostnameOf(url)`
* Hostname of `url`, or "" when it does not parse.
*
* `LOGIN_CONTROL_NAME`
* Accessible-name pattern for a sign-in control, used by the
* post-login gates to assert no login affordance survived the
* round-trip. Anchored on purpose: an unanchored /sso/ matches
* "Acce(sso)ries" and a \blogin\b matches "Login Enabled", both of
* which are ordinary navigation entries on an authenticated page.
*/
const { expect } = require("@playwright/test");
async function isVisible(locator) {
return locator.first().isVisible().catch(() => false);
}
async function waitForFrameUrl(iframeLocator, matcher, timeout, errorMessage) {
await expect
.poll(
async () => {
const iframeHandle = await iframeLocator.elementHandle();
const frame = iframeHandle ? await iframeHandle.contentFrame() : null;
return frame ? frame.url() : "";
},
{
timeout,
message: errorMessage,
}
)
.toContain(matcher);
}
async function findFirstVisibleCandidate(candidates) {
for (const candidate of candidates) {
const locator = candidate.locator.first();
if (await locator.isVisible().catch(() => false)) {
return { ...candidate, locator };
}
}
return null;
}
function escapeRegex(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function hostnameOf(url) {
try {
return new URL(url).hostname;
} catch {
return "";
}
}
const LOGIN_CONTROL_NAME = /^[\s\p{Co}]*(log\s*in|sign\s*in|sso)(\s+with\s+\S.*)?\s*$/iu;
module.exports = {
isVisible,
waitForFrameUrl,
findFirstVisibleCandidate,
escapeRegex,
hostnameOf,
LOGIN_CONTROL_NAME,
};