mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
Over Tor the redirect chain behind a login/logout/OIDC click routinely exceeds Playwright's fixed 30s action timeout, so the click throws before the flow's scaled waits run. Sweep all role Playwright suites and the shared persona helpers: give every navigation/auth-transition click an onion-scaled timeout (.click({ timeout: resolveTimeout(30_000) })) and route the remaining raw millisecond literals (timeout options, waitForTimeout, sleep-promise setTimeout, positional wait args) through resolveTimeout. Local UI clicks (menus, toggles, modal dismissals, DOM clicks inside evaluate) stay untouched.
Two new lints keep it that way: test_no_raw_timeout_literals forbids raw numeric timeout literals in Playwright specs (opt-out: nocheck: raw-timeout), test_persona_clicks_scaled requires every click in the shared persona auth flows to carry a resolveTimeout timeout (opt-out: nocheck: unscaled-click).
Verified: node --check on all 133 changed JS files, all 13 playwright lints green, make autoformat clean, make test green (2745+196+163+4 tests OK).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
54 lines
2.7 KiB
JavaScript
54 lines
2.7 KiB
JavaScript
const { test, expect } = require("@playwright/test");
|
|
const { resolveTimeout } = require("./timeouts");
|
|
const { skipUnlessServiceEnabled } = require("./service-gating");
|
|
const { decodeDotenvQuotedValue, normalizeBaseUrl, gotoOnion } = require("./personas");
|
|
|
|
const joomlaBaseUrl = normalizeBaseUrl(process.env.JOOMLA_BASE_URL);
|
|
const adminUsername = decodeDotenvQuotedValue(process.env.ADMIN_USERNAME);
|
|
const adminPassword = decodeDotenvQuotedValue(process.env.ADMIN_PASSWORD);
|
|
|
|
test.use({ ignoreHTTPSErrors: true });
|
|
|
|
async function performJoomlaAdminFormLogin(page, baseUrl, username, password) {
|
|
// Local Joomla form-login at /administrator?fallback=local. The
|
|
// `?fallback=local` query short-circuits the plg_system_keycloak
|
|
// redirect so the operator has an emergency hatch when Keycloak is
|
|
// unavailable (per the documented Modus 3 contract).
|
|
await gotoOnion(page, `${baseUrl}/administrator?fallback=local`, { waitUntil: "domcontentloaded" });
|
|
|
|
const usernameField = page.locator("input[name='username']");
|
|
const passwordField = page.locator("input[name='passwd']");
|
|
|
|
await usernameField.waitFor({ state: "visible", timeout: resolveTimeout(60_000) });
|
|
await usernameField.fill(username);
|
|
await passwordField.fill(password);
|
|
|
|
await Promise.all([
|
|
page.waitForLoadState("domcontentloaded"),
|
|
page.locator("button[type='submit'], input[type='submit']").first().click({ timeout: resolveTimeout(30_000) }),
|
|
]);
|
|
}
|
|
|
|
test("OIDC: /administrator?fallback=local hatch bypasses Keycloak and accepts the local Joomla form (Modus 3 emergency path)", async ({ page }) => {
|
|
// The local form-login fallback at /administrator?fallback=local
|
|
// is the operationally-mandated hatch when Keycloak is unavailable
|
|
// (per the documented Modus 3 contract). It MUST NOT redirect to the IdP,
|
|
// and the local form MUST accept the bootstrap administrator
|
|
// credentials.
|
|
expect(joomlaBaseUrl, "JOOMLA_BASE_URL must be set in the Playwright env file").toBeTruthy();
|
|
expect(adminUsername, "ADMIN_USERNAME must be set in the Playwright env file").toBeTruthy();
|
|
expect(adminPassword, "ADMIN_PASSWORD must be set in the Playwright env file").toBeTruthy();
|
|
await page.context().clearCookies();
|
|
skipUnlessServiceEnabled("sso");
|
|
const expectedJoomlaBaseUrl = joomlaBaseUrl.replace(/\/$/, "");
|
|
|
|
await performJoomlaAdminFormLogin(page, expectedJoomlaBaseUrl, adminUsername, adminPassword);
|
|
|
|
// Joomla 6 uses `body.com_cpanel` on the admin home; the broader
|
|
// fallback set covers future template tweaks.
|
|
const controlPanelMarker = page
|
|
.locator("body.com_cpanel, #sidebarmenu, nav[aria-label='Main menu'], a[href*='option=com_cpanel']")
|
|
.first();
|
|
await controlPanelMarker.waitFor({ state: "visible", timeout: resolveTimeout(60_000) });
|
|
});
|