mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
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>
92 lines
2.9 KiB
JavaScript
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,
|
|
};
|