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>
106 lines
4.0 KiB
JavaScript
106 lines
4.0 KiB
JavaScript
const { expect } = require("@playwright/test");
|
|
const { resolveTimeout } = require("./timeouts");
|
|
|
|
const { decodeDotenvQuotedValue, normalizeBaseUrl, performKeycloakLoginForm, gotoOnion } = require("./personas");
|
|
const { isServiceEnabled, skipUnlessServiceEnabled } = require("./service-gating");
|
|
|
|
const oidcEnabled = isServiceEnabled("sso");
|
|
const oidcIssuerUrl = normalizeBaseUrl(process.env.OIDC_ISSUER_URL || "");
|
|
const erpnextBaseUrl = normalizeBaseUrl(process.env.ERPNEXT_BASE_URL || "");
|
|
const adminUsername = decodeDotenvQuotedValue(process.env.ADMIN_USERNAME);
|
|
const adminEmail = decodeDotenvQuotedValue(process.env.ADMIN_EMAIL);
|
|
const adminPassword = decodeDotenvQuotedValue(process.env.ADMIN_PASSWORD);
|
|
const adminNativePassword = decodeDotenvQuotedValue(process.env.ADMIN_NATIVE_PASSWORD);
|
|
const biberUsername = decodeDotenvQuotedValue(process.env.BIBER_USERNAME);
|
|
const biberPassword = decodeDotenvQuotedValue(process.env.BIBER_PASSWORD);
|
|
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN);
|
|
|
|
async function erpnextLogout(page) {
|
|
await gotoOnion(page, `${erpnextBaseUrl}/api/method/logout`, { waitUntil: "commit" }).catch(() => {});
|
|
if (oidcIssuerUrl) {
|
|
await gotoOnion(page, `${oidcIssuerUrl}/protocol/openid-connect/logout`, { waitUntil: "commit" }).catch(() => {});
|
|
}
|
|
await page.context().clearCookies();
|
|
}
|
|
|
|
// Frappe's /login form serves both native local users and LDAP-federated users
|
|
// (local DB lookup first, LDAP fallback when LDAP Settings is enabled).
|
|
async function signInViaErpnextLocal(page, username, password, personaLabel) {
|
|
await gotoOnion(page, `${erpnextBaseUrl}/login`);
|
|
await page.fill("input#login_email", username);
|
|
await page.fill("input#login_password", password);
|
|
await Promise.all([
|
|
page.waitForURL((url) => url.toString().includes(canonicalDomain) && !url.toString().includes("/login"), {
|
|
timeout: resolveTimeout(60_000),
|
|
}),
|
|
page.click("button.btn-login, button[type='submit']", { timeout: resolveTimeout(30_000) }).catch(() => page.keyboard.press("Enter")),
|
|
]);
|
|
await expect
|
|
.poll(() => page.url(), {
|
|
timeout: resolveTimeout(60_000),
|
|
message: `${personaLabel}: expected post-login redirect on ${canonicalDomain} (not /login)`,
|
|
})
|
|
.not.toContain("/login");
|
|
}
|
|
|
|
async function signInViaErpnextOidc(page, username, password, personaLabel) {
|
|
const expectedOidcAuthUrl = `${oidcIssuerUrl}/protocol/openid-connect/auth`;
|
|
|
|
await gotoOnion(page, `${erpnextBaseUrl}/login`);
|
|
|
|
const oidcSignIn = page
|
|
.locator("a, button")
|
|
.filter({ hasText: /sso\s*login|sign\s*in\s+with|continue\s+with|single\s+sign[-\s]*on|keycloak|infinito/i })
|
|
.first();
|
|
|
|
await expect(
|
|
oidcSignIn,
|
|
`${personaLabel}: the Keycloak SSO button must render on /login (Social Login Key not picked up by the workers?)`,
|
|
).toBeVisible({ timeout: resolveTimeout(30_000) });
|
|
await oidcSignIn.click({ timeout: resolveTimeout(30_000) });
|
|
|
|
await expect
|
|
.poll(() => page.url(), {
|
|
timeout: resolveTimeout(60_000),
|
|
message: `${personaLabel}: expected redirect to Keycloak OIDC auth (${expectedOidcAuthUrl})`,
|
|
})
|
|
.toContain(expectedOidcAuthUrl);
|
|
|
|
await performKeycloakLoginForm(page, username, password);
|
|
|
|
await expect
|
|
.poll(() => page.url(), {
|
|
timeout: resolveTimeout(60_000),
|
|
message: `${personaLabel}: expected redirect back to ERPNext at ${erpnextBaseUrl}`,
|
|
})
|
|
.toContain(canonicalDomain);
|
|
}
|
|
|
|
async function beforeEach({ page }) {
|
|
await page.setViewportSize({ width: 1440, height: 1100 });
|
|
|
|
expect(erpnextBaseUrl, "ERPNEXT_BASE_URL must be set").toBeTruthy();
|
|
expect(canonicalDomain, "CANONICAL_DOMAIN must be set").toBeTruthy();
|
|
await page.context().clearCookies();
|
|
}
|
|
|
|
module.exports = {
|
|
env: {
|
|
oidcEnabled,
|
|
oidcIssuerUrl,
|
|
erpnextBaseUrl,
|
|
adminUsername,
|
|
adminEmail,
|
|
adminPassword,
|
|
adminNativePassword,
|
|
biberUsername,
|
|
biberPassword,
|
|
canonicalDomain,
|
|
},
|
|
signInViaErpnextOidc,
|
|
signInViaErpnextLocal,
|
|
erpnextLogout,
|
|
beforeEach,
|
|
skipUnlessServiceEnabled,
|
|
};
|