test(playwright): route all timeouts through resolveTimeout for onion-compat

Add the global TIMEOUT_FACTOR and a shared timeouts.js helper: resolveTimeout(base) scales by TIMEOUT_FACTOR and, over a .onion target, an extra multiplier, so Tor's per-request latency no longer trips hard-coded timeouts. Migrate every raw timeout in the specs (timeout:/timeout=/setTimeout/waitForTimeout) to resolveTimeout, stage the helper in all three staging paths, and add test_no_raw_timeouts to forbid raw numeric timeouts. Clearnet behaviour is unchanged (factor 1: resolveTimeout(N) === N).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 4.8 committed 2026-07-08 10:38:06 +02:00
1 parent dfa7f68982
commit 35dee215c8
262 files changed
+1581 -1078

No files matched your search

+2
View File
@@ -64,3 +64,5 @@ POSTGRES_WORK_MEM: "{{ POSTGRES_WORK_MEM_MB ~ 'MB' }}"
POSTGRES_MAINTENANCE_WORK_MEM_MB: "{{ [ (((POSTGRES_TOTAL_RAM_MB | int) * 5) // 100), 64 ] | max }}"
POSTGRES_MAINTENANCE_WORK_MEM: "{{ POSTGRES_MAINTENANCE_WORK_MEM_MB ~ 'MB' }}"
POSTGRES_DELAY: 2
TIMEOUT_FACTOR: 1
@@ -0,0 +1,43 @@
/**
* Shared timeout helper for every Playwright spec (staged next to the spec as
* `./timeouts`; persona flows reach it via `../timeouts`). Scales a base
* timeout by the global `TIMEOUT_FACTOR` and, when the target is a `.onion`
* (Tor) service, by an extra multiplier — Tor circuits add per-request latency.
*/
function _canonicalDomain() {
const raw = process.env.CANONICAL_DOMAIN || "";
const unquoted =
raw.length >= 2 && raw.startsWith('"') && raw.endsWith('"') ? raw.slice(1, -1) : raw;
return unquoted.trim();
}
function isOnionTarget() {
return /\.onion$/i.test(_canonicalDomain());
}
function timeoutFactor() {
const f = Number(process.env.PLAYWRIGHT_TIMEOUT_FACTOR);
return Number.isFinite(f) && f > 0 ? f : 1;
}
function onionTimeoutMultiplier() {
const m = Number(process.env.PLAYWRIGHT_ONION_TIMEOUT_MULTIPLIER);
return Number.isFinite(m) && m > 0 ? m : 5;
}
/**
* Effective timeout (ms) for the current run: `base * TIMEOUT_FACTOR`, times the
* onion multiplier when the target is a `.onion` service.
*/
function resolveTimeout(baseMs) {
const onionMult = isOnionTarget() ? onionTimeoutMultiplier() : 1;
return Math.round(baseMs * timeoutFactor() * onionMult);
}
module.exports = {
isOnionTarget,
timeoutFactor,
onionTimeoutMultiplier,
resolveTimeout,
};
@@ -183,6 +183,12 @@
dest: "{{ _playwright_stage_tests_dir }}/addon-gating.js"
mode: "0644"
- name: Copy shared timeouts helper into staging tests directory
ansible.builtin.copy:
src: "{{ playbook_dir }}/roles/test-e2e-playwright/files/timeouts.js"
dest: "{{ _playwright_stage_tests_dir }}/timeouts.js"
mode: "0644"
# Stage the shared persona-flow helpers.
# The personas/ tree ships every persona flow runner at the top level
# (biber.js, admin.js, guest.js, index.js) and every helper under
@@ -336,6 +342,8 @@
- "PLAYWRIGHT_FULLY_PARALLEL={{ TEST_E2E_PLAYWRIGHT_FULLY_PARALLEL }}"
- -e
- "PLAYWRIGHT_TEST_TIMEOUT={{ TEST_E2E_PLAYWRIGHT_TEST_TIMEOUT }}"
- -e
- "PLAYWRIGHT_TIMEOUT_FACTOR={{ TIMEOUT_FACTOR }}"
- -v
- "{{ _playwright_stage_dir }}:/e2e"
- -v
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { runAdminFlow } = require("./personas");
test.use({ ignoreHTTPSErrors: true });
@@ -11,12 +12,12 @@ test("administrator: app → universal logout", async ({ page }) => {
const settingsLink = interactivePage
.getByRole("link", { name: /^(settings|administration|users|companies)$/i })
.first();
if (await settingsLink.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await settingsLink.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await settingsLink.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/general settings|users|companies|categories|currencies|invoice/i,
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
);
}
},
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { skipUnlessServiceEnabled } = require("./service-gating");
const { decodeDotenvQuotedValue, normalizeBaseUrl, performKeycloakLoginForm } = require("./personas");
@@ -20,8 +21,8 @@ test("OIDC: oauth2-proxy redirects unauthenticated visitors through Keycloak (va
const expectedAuth = `${oidcIssuerUrl}/protocol/openid-connect/auth`;
const expectedBase = baseUrl.replace(/\/$/, "");
await page.goto(`${expectedBase}/`);
await expect.poll(() => page.url(), { timeout: 60_000 }).toContain(expectedAuth);
await expect.poll(() => page.url(), { timeout: resolveTimeout(60_000) }).toContain(expectedAuth);
await performKeycloakLoginForm(page, adminUsername, adminPassword);
await expect.poll(() => page.url(), { timeout: 90_000 }).toContain(expectedBase);
await expect(page.locator("body")).toBeVisible({ timeout: 60_000 });
await expect.poll(() => page.url(), { timeout: resolveTimeout(90_000) }).toContain(expectedBase);
await expect(page.locator("body")).toBeVisible({ timeout: resolveTimeout(60_000) });
});
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { skipUnlessServiceEnabled } = require("./service-gating");
const {
runSeaweedfsStorageCheck,
@@ -23,7 +24,7 @@ test.use({ ignoreHTTPSErrors: true });
test("seaweedfs: an uploaded Akaunting company logo is stored in the SeaweedFS bucket", async ({ page, browser }) => {
skipUnlessServiceEnabled("seaweedfs");
test.setTimeout(180_000);
test.setTimeout(resolveTimeout(180_000));
expect(baseUrl, "AKAUNTING_BASE_URL must be set").toBeTruthy();
expect(canonicalDomain, "CANONICAL_DOMAIN must be set").toBeTruthy();
@@ -41,11 +42,11 @@ test("seaweedfs: an uploaded Akaunting company logo is stored in the SeaweedFS b
await appPage.goto(`${expectedBase}/`, { waitUntil: "domcontentloaded" });
if (appPage.url().includes("openid-connect/auth")) {
await performKeycloakLoginForm(appPage, adminUsername, adminPassword);
await expect.poll(() => appPage.url(), { timeout: 90_000 }).toContain(expectedBase);
await expect.poll(() => appPage.url(), { timeout: resolveTimeout(90_000) }).toContain(expectedBase);
}
await appPage.goto(`${expectedBase}/auth/login`, { waitUntil: "domcontentloaded" });
await appPage.waitForSelector('#app input[name="email"]', { state: "visible", timeout: 60_000 });
await appPage.waitForSelector('#app input[name="email"]', { state: "visible", timeout: resolveTimeout(60_000) });
await appPage
.waitForFunction(
() => {
@@ -53,14 +54,14 @@ test("seaweedfs: an uploaded Akaunting company logo is stored in the SeaweedFS b
const f = document.querySelector("#app form, form#auth");
return Boolean((f && f.__vue__) || window.Vue || (el && el.__vue__));
},
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
)
.catch(() => {});
await appPage.locator('#app input[name="email"]').first().fill(akauntingAdminEmail);
await appPage.locator('#app input[name="password"]').first().fill(akauntingAdminPassword);
const loginResp = appPage.waitForResponse(
(r) => /\/auth\/login$/.test(r.url()) && r.request().method() === "POST",
{ timeout: 60_000 },
{ timeout: resolveTimeout(60_000) },
);
await appPage
.getByRole("button", { name: /login|sign in|enter/i })
@@ -70,20 +71,20 @@ test("seaweedfs: an uploaded Akaunting company logo is stored in the SeaweedFS b
await loginResp.catch(() => {});
await expect
.poll(() => appPage.url(), {
timeout: 90_000,
timeout: resolveTimeout(90_000),
message: "expected Akaunting native login to authenticate and leave /auth/login",
})
.not.toContain("/auth/login");
await appPage.waitForLoadState("networkidle", { timeout: 30_000 }).catch(() => {});
await appPage.waitForLoadState("networkidle", { timeout: resolveTimeout(30_000) }).catch(() => {});
await appPage.goto(`${expectedBase}/1/settings/company`, { waitUntil: "domcontentloaded" });
await expect
.poll(() => appPage.url(), { timeout: 90_000, message: "expected the Akaunting company settings page" })
.poll(() => appPage.url(), { timeout: resolveTimeout(90_000), message: "expected the Akaunting company settings page" })
.toContain("settings/company");
const marker = `infinito-storage-check-${Date.now()}.png`;
const fileInput = appPage.locator('input[type="file"]').first();
await fileInput.waitFor({ state: "attached", timeout: 60_000 });
await fileInput.waitFor({ state: "attached", timeout: resolveTimeout(60_000) });
await fileInput.setInputFiles({ name: marker, mimeType: "image/png", buffer: PNG_1x1 });
const saveAction = appPage
@@ -93,10 +94,10 @@ test("seaweedfs: an uploaded Akaunting company logo is stored in the SeaweedFS b
await expect(
saveAction,
"the Akaunting company settings form must expose a Save action to persist the attached logo",
).toBeVisible({ timeout: 60_000 });
).toBeVisible({ timeout: resolveTimeout(60_000) });
await saveAction.click();
await appPage.waitForLoadState("networkidle", { timeout: 90_000 }).catch(() => {});
await appPage.waitForLoadState("networkidle", { timeout: resolveTimeout(90_000) }).catch(() => {});
},
});
});
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { runAdminFlow } = require("./personas");
test.use({ ignoreHTTPSErrors: true });
@@ -9,12 +10,12 @@ test("administrator: app -> universal logout", async ({ page }) => {
const settingsLink = interactivePage
.getByRole("link", { name: /^(workspaces?|settings|admin|users)$/i })
.first();
if (await settingsLink.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await settingsLink.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await settingsLink.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/workspace|settings|users|members|database|table/i,
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
);
}
},
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { skipUnlessServiceEnabled } = require("./service-gating");
const { decodeDotenvQuotedValue, normalizeBaseUrl, performKeycloakLoginForm } = require("./personas");
@@ -21,13 +22,13 @@ test("OIDC: oauth2-proxy + trusted-header bridge mint a Baserow JWT session", as
const expectedBase = baseUrl.replace(/\/$/, "");
await page.goto(`${expectedBase}/`);
await expect
.poll(() => page.url(), { timeout: 60_000, message: `expected redirect to ${expectedAuth}` })
.poll(() => page.url(), { timeout: resolveTimeout(60_000), message: `expected redirect to ${expectedAuth}` })
.toContain(expectedAuth);
await performKeycloakLoginForm(page, adminUsername, adminPassword);
await expect
.poll(() => page.url(), { timeout: 90_000, message: `expected redirect back to ${expectedBase}` })
.poll(() => page.url(), { timeout: resolveTimeout(90_000), message: `expected redirect back to ${expectedBase}` })
.toContain(expectedBase);
await expect(page.locator("body")).toBeVisible({ timeout: 60_000 });
await expect(page.locator("body")).toBeVisible({ timeout: resolveTimeout(60_000) });
const tokenResponse = await page.request.get(`${expectedBase}/api/infinito/sso/token/`);
const tokenBody = await tokenResponse.text();
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { skipUnlessServiceEnabled, isServiceEnabled } = require("./service-gating");
const { normalizeBaseUrl, decodeDotenvQuotedValue, performKeycloakLoginForm } = require("./personas");
@@ -30,7 +31,7 @@ test("oidc-security: a forged identity header cannot bypass the oauth2-proxy gat
await expect
.poll(() => page.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: "a forged identity header must be bounced to Keycloak, never into Baserow",
})
.toContain("openid-connect/auth");
@@ -121,7 +122,7 @@ test("oidc-security: injected identity headers cannot re-identify an authenticat
await page.goto(`${expectedBase}/`);
await performKeycloakLoginForm(page, adminUsername, adminPassword);
await expect
.poll(() => page.url(), { timeout: 90_000, message: `expected redirect back to ${expectedBase}` })
.poll(() => page.url(), { timeout: resolveTimeout(90_000), message: `expected redirect back to ${expectedBase}` })
.toContain(expectedBase.replace(/^https?:\/\//, ""));
const forgedMarker = "forgedescalationprobe";
@@ -7,6 +7,7 @@
// check proves that the configured bucket gained an object.
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { skipUnlessServiceEnabled } = require("./service-gating");
const {
runSeaweedfsStorageCheck,
@@ -32,9 +33,9 @@ async function getBaserowSession(appPage, baseUrl, adminUsername, adminPassword)
}
await expect
.poll(() => appPage.url(), { timeout: 90_000, message: `expected redirect back to ${baseUrl}` })
.poll(() => appPage.url(), { timeout: resolveTimeout(90_000), message: `expected redirect back to ${baseUrl}` })
.toContain(baseUrl.replace(/^https?:\/\//, ""));
await appPage.waitForLoadState("networkidle", { timeout: 60_000 }).catch(() => {});
await appPage.waitForLoadState("networkidle", { timeout: resolveTimeout(60_000) }).catch(() => {});
const tokenResponse = await appPage.request.get(`${baseUrl}/api/infinito/sso/token/`);
const tokenData = await readJson(tokenResponse, "trusted-header token request");
@@ -110,7 +111,7 @@ async function createBaserowFileRow(appPage, baseUrl, accessToken) {
test("seaweedfs: an uploaded Baserow file-field document is stored in the SeaweedFS bucket", async ({ page, browser }) => {
skipUnlessServiceEnabled("seaweedfs");
skipUnlessServiceEnabled("sso");
test.setTimeout(180_000);
test.setTimeout(resolveTimeout(180_000));
const baseUrl = normalizeBaseUrl(process.env.BASEROW_BASE_URL || "");
const adminUsername = decodeDotenvQuotedValue(process.env.ADMIN_USERNAME || "");
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { skipUnlessServiceEnabled } = require("./service-gating");
const { assertCspResponseHeader, decodeDotenvQuotedValue, expectNoCspViolations, installCspViolationObserver, normalizeBaseUrl, performKeycloakLoginForm, runGuestFlow } = require("./personas");
@@ -83,25 +84,25 @@ async function signInViaBbbOidc(page, username, password, personaLabel) {
// user on `/signin` instead. Fall back to clicking the explicit OIDC button
// that Greenlight renders when `OPENID_CONNECT_*` env is set.
await page
.waitForURL((u) => u.toString().includes(expectedOidcAuthUrl), { timeout: 10_000 })
.waitForURL((u) => u.toString().includes(expectedOidcAuthUrl), { timeout: resolveTimeout(10_000) })
.catch(async () => {
const oidcButton = page
.locator("a, button")
.filter({ hasText: /sign\s*in\s*with|openid|oidc|sso|single\s*sign[-\s]*on/i })
.first();
if (await oidcButton.isVisible({ timeout: 5_000 }).catch(() => false)) {
if (await oidcButton.isVisible({ timeout: resolveTimeout(5_000) }).catch(() => false)) {
await oidcButton.click().catch(() => {});
}
});
await page.waitForURL((u) => u.toString().includes(expectedOidcAuthUrl), {
timeout: 120_000
timeout: resolveTimeout(120_000)
});
await performKeycloakLoginForm(page, username, password);
await page.waitForURL((u) => u.toString().startsWith(`${bbbBaseUrl}/`) && !u.toString().includes("/auth/openid_connect") && !u.toString().includes("?sso=true"), {
timeout: 120_000
timeout: resolveTimeout(120_000)
});
// Authenticated Greenlight renders a header "Sign Out" control in the nav
@@ -111,7 +112,7 @@ async function signInViaBbbOidc(page, username, password, personaLabel) {
await page.goto(`${bbbBaseUrl}/rooms`, { waitUntil: "domcontentloaded" });
await expect
.poll(() => page.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `${personaLabel}: expected /rooms to remain accessible post-login (not to be redirected to / or /signin)`
})
.toMatch(new RegExp(`^${bbbBaseUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/rooms(/|\\?|$)`));
@@ -142,7 +143,7 @@ async function assertLoggedOut(page, bbbBaseUrl, personaLabel) {
return "pending";
},
{
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `${personaLabel}: expected bigbluebutton to require a new sign-in after logout`
}
)
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
exports.register = function (shared) {
test("OIDC: oauth2-proxy + login-broker drop a Bluesky session into social-app via Keycloak (variant A+)", async ({ page }) => {
@@ -28,7 +29,7 @@ exports.register = function (shared) {
await expect
.poll(() => page.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `expected redirect to Keycloak OIDC auth (${expectedOidcAuthUrl})`,
})
.toContain(expectedOidcAuthUrl);
@@ -45,15 +46,15 @@ exports.register = function (shared) {
// not be under `/oauth2/`.
await expect
.poll(() => page.url(), {
timeout: 90_000,
timeout: resolveTimeout(90_000),
message: `expected post-OIDC URL to land on Bluesky outside /oauth2/* (got: ${page.url()})`,
})
.toMatch(new RegExp(`^${expectedBaseUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/(?!oauth2/)`));
// Wait for the network to settle so the broker's handoff JS can
// commit localStorage + cookie before we inspect them.
await page.waitForLoadState("networkidle", { timeout: 30_000 }).catch(() => {});
await expect(page.locator("body")).toBeVisible({ timeout: 60_000 });
await page.waitForLoadState("networkidle", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(page.locator("body")).toBeVisible({ timeout: resolveTimeout(60_000) });
// The broker writes a schema-compliant `BSKY_STORAGE` payload
// built from upstream `social-app@1.121.0`'s persisted defaults
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { runAdminFlow } = require("./personas");
test.use({ ignoreHTTPSErrors: true });
@@ -9,12 +10,12 @@ test("administrator: app → universal logout", async ({ page }) => {
const settingsLink = interactivePage
.getByRole("link", { name: /^(settings|administration|users|federation|reports)$/i })
.first();
if (await settingsLink.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await settingsLink.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await settingsLink.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/site settings|users|federation|reports|library/i,
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
);
}
},
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { skipUnlessServiceEnabled } = require("./service-gating");
const { normalizeBaseUrl, decodeDotenvQuotedValue, performKeycloakLoginForm } = require("./personas");
@@ -19,14 +20,14 @@ test("OIDC: oauth2-proxy + trusted-header SSO sign the visitor into BookWyrm (va
const expectedAuth = `${oidcIssuerUrl}/protocol/openid-connect/auth`;
const expectedBase = baseUrl.replace(/\/$/, "");
await page.goto(`${expectedBase}/`);
await expect.poll(() => page.url(), { timeout: 60_000 }).toContain(expectedAuth);
await expect.poll(() => page.url(), { timeout: resolveTimeout(60_000) }).toContain(expectedAuth);
await performKeycloakLoginForm(page, adminUsername, adminPassword);
await expect.poll(() => page.url(), { timeout: 90_000 }).toContain(expectedBase);
await expect(page.locator("body")).toBeVisible({ timeout: 60_000 });
await expect.poll(() => page.url(), { timeout: resolveTimeout(90_000) }).toContain(expectedBase);
await expect(page.locator("body")).toBeVisible({ timeout: resolveTimeout(60_000) });
await page.goto(`${expectedBase}/preferences/profile`, { waitUntil: "domcontentloaded" });
await expect(
page.locator('form[name="edit-profile"]'),
"trusted-header SSO must sign the visitor into a real BookWyrm session (auth-only edit form must render, not BookWyrm's login form)",
).toBeVisible({ timeout: 30_000 });
).toBeVisible({ timeout: resolveTimeout(30_000) });
});
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { skipUnlessServiceEnabled, isServiceEnabled } = require("./service-gating");
const { normalizeBaseUrl, decodeDotenvQuotedValue, performKeycloakLoginForm } = require("./personas");
@@ -23,7 +24,7 @@ test("oidc-security: a forged identity header cannot bypass the oauth2-proxy gat
await page.goto(`${expectedBase}/preferences/profile`, { waitUntil: "domcontentloaded" });
await expect
.poll(() => page.url(), { timeout: 60_000 })
.poll(() => page.url(), { timeout: resolveTimeout(60_000) })
.toContain("openid-connect/auth");
await expect(
page.locator('form[name="edit-profile"]'),
@@ -65,14 +66,14 @@ test("oidc-security: injected identity headers cannot re-identify an authenticat
await page.goto(`${expectedBase}/`);
await performKeycloakLoginForm(page, adminUsername, adminPassword);
await expect
.poll(() => page.url(), { timeout: 90_000, message: `expected redirect back to ${expectedBase}` })
.poll(() => page.url(), { timeout: resolveTimeout(90_000), message: `expected redirect back to ${expectedBase}` })
.toContain(expectedBase.replace(/^https?:\/\//, ""));
await page.goto(`${expectedBase}/preferences/profile`, { waitUntil: "domcontentloaded" });
await expect(
page.locator('form[name="edit-profile"]'),
"the genuine oauth2 session must be authenticated before the injection probe",
).toBeVisible({ timeout: 30_000 });
).toBeVisible({ timeout: resolveTimeout(30_000) });
const forgedMarker = "forgedescalationprobe";
await page.setExtraHTTPHeaders({
@@ -89,7 +90,7 @@ test("oidc-security: injected identity headers cannot re-identify an authenticat
await expect(
page.locator('form[name="edit-profile"]'),
"the genuine oauth2 session must survive the injection probe",
).toBeVisible({ timeout: 30_000 });
).toBeVisible({ timeout: resolveTimeout(30_000) });
expect(
(await page.content()).toLowerCase(),
"the oauth2-proxy identity must win; an injected header must not switch the BookWyrm session",
@@ -9,6 +9,7 @@
// Filer UI.
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { skipUnlessServiceEnabled } = require("./service-gating");
const {
runSeaweedfsStorageCheck,
@@ -24,7 +25,7 @@ const PNG_AVATAR_BASE64 =
test("seaweedfs: a BookWyrm avatar upload is stored in the SeaweedFS bucket", async ({ page, browser }) => {
skipUnlessServiceEnabled("seaweedfs");
test.setTimeout(180_000);
test.setTimeout(resolveTimeout(180_000));
const baseUrl = normalizeBaseUrl(process.env.BOOKWYRM_BASE_URL || "");
const adminUsername = decodeDotenvQuotedValue(process.env.ADMIN_USERNAME || "");
@@ -43,7 +44,7 @@ test("seaweedfs: a BookWyrm avatar upload is stored in the SeaweedFS bucket", as
if (/openid-connect\/auth|\/oauth2\//.test(appPage.url())) {
await performKeycloakLoginForm(appPage, adminUsername, adminPassword);
await expect
.poll(() => appPage.url(), { timeout: 90_000 })
.poll(() => appPage.url(), { timeout: resolveTimeout(90_000) })
.toContain(baseUrl.replace(/^https?:\/\//, ""));
}
@@ -53,7 +54,7 @@ test("seaweedfs: a BookWyrm avatar upload is stored in the SeaweedFS bucket", as
.locator('input[type="file"][name="avatar"]')
.or(appPage.locator('input[type="file"]'))
.first();
await avatarInput.waitFor({ state: "attached", timeout: 30_000 });
await avatarInput.waitFor({ state: "attached", timeout: resolveTimeout(30_000) });
await avatarInput.setInputFiles({
name: `infinito-storage-check-${Date.now()}.png`,
mimeType: "image/png",
@@ -72,9 +73,9 @@ test("seaweedfs: a BookWyrm avatar upload is stored in the SeaweedFS bucket", as
// — that is the only signal the avatar POST was accepted and an object was
// written to S3.
await expect
.poll(() => appPage.url(), { timeout: 60_000 })
.poll(() => appPage.url(), { timeout: resolveTimeout(60_000) })
.not.toContain("/preferences/profile");
await appPage.waitForLoadState("networkidle", { timeout: 60_000 }).catch(() => {});
await appPage.waitForLoadState("networkidle", { timeout: resolveTimeout(60_000) }).catch(() => {});
},
});
});
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow } = require("./personas");
test.use({ ignoreHTTPSErrors: true });
@@ -59,12 +60,12 @@ test("administrator: app → universal logout", async ({ page }) => {
const link = interactivePage
.getByRole("link", { name: /^(admin|status|federation)$/i })
.first();
if (await link.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await link.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await link.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/status|federation|web|fediverse/i,
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
);
}
},
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow } = require("./personas");
@@ -37,11 +38,11 @@ test("administrator: app → admin surface → universal logout", async ({ page
adminInteraction: async (interactivePage) => {
// Checkmk admin-only surface: the Setup menu is admin-gated.
const setup = interactivePage.getByRole("link", { name: /^setup$/i }).first();
if (await setup.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await setup.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await setup.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(/setup|hosts|services|checkmk/i, {
timeout: 30_000,
timeout: resolveTimeout(30_000),
});
}
},
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { skipUnlessServiceEnabled } = require("./service-gating");
const { decodeDotenvQuotedValue, normalizeBaseUrl, performKeycloakLoginForm } = require("./personas");
@@ -11,7 +12,7 @@ test.use({ ignoreHTTPSErrors: true });
test("OIDC: oauth2-proxy gate + X-Remote-User header establish a real Checkmk session", async ({ page }) => {
skipUnlessServiceEnabled("sso");
test.setTimeout(120_000); // oauth2-proxy + Keycloak round-trip
test.setTimeout(resolveTimeout(120_000)); // oauth2-proxy + Keycloak round-trip
expect(baseUrl, "CHECKMK_BASE_URL must be set").toBeTruthy();
expect(adminUsername, "ADMIN_USERNAME must be set").toBeTruthy();
expect(adminPassword, "ADMIN_PASSWORD must be set").toBeTruthy();
@@ -23,7 +24,7 @@ test("OIDC: oauth2-proxy gate + X-Remote-User header establish a real Checkmk se
await page.goto(`${expectedBase}/`);
await expect
.poll(() => page.url(), { timeout: 60_000, message: `expected redirect to ${expectedAuth}` })
.poll(() => page.url(), { timeout: resolveTimeout(60_000), message: `expected redirect to ${expectedAuth}` })
.toContain(expectedAuth);
await performKeycloakLoginForm(page, adminUsername, adminPassword);
@@ -31,10 +32,10 @@ test("OIDC: oauth2-proxy gate + X-Remote-User header establish a real Checkmk se
// Back on the canonical domain, authenticated into the Checkmk GUI via the
// trusted X-Remote-User header (no second login form).
await expect
.poll(() => page.url(), { timeout: 90_000, message: `expected redirect back to ${expectedBase}` })
.poll(() => page.url(), { timeout: resolveTimeout(90_000), message: `expected redirect back to ${expectedBase}` })
.toContain(expectedBase.replace(/^https?:\/\//, ""));
await expect
.poll(() => page.url(), { timeout: 60_000, message: "expected to leave the Checkmk login page" })
.poll(() => page.url(), { timeout: resolveTimeout(60_000), message: "expected to leave the Checkmk login page" })
.not.toContain("login.py");
await expect(page.locator("body")).toContainText(/checkmk|dashboard|monitor|overview/i, { timeout: 60_000 });
await expect(page.locator("body")).toContainText(/checkmk|dashboard|monitor|overview/i, { timeout: resolveTimeout(60_000) });
});
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { skipUnlessServiceEnabled } = require("./service-gating");
const { normalizeBaseUrl } = require("./personas");
@@ -27,7 +28,7 @@ test("oidc-security: a forged X-Remote-User header cannot bypass the oauth2-prox
await expect
.poll(() => page.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: "a forged X-Remote-User must be bounced to Keycloak, never into Checkmk",
})
.toContain("openid-connect/auth");
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls } = require("./personas");
test.use({ ignoreHTTPSErrors: true });
@@ -53,12 +54,12 @@ test("administrator: app → universal logout", async ({ page }) => {
const link = interactivePage
.getByRole("link", { name: /^(play|board|profile|admin|tournament)$/i })
.first();
if (await link.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await link.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await link.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/chess|game|board|move|tournament/i,
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
);
}
},
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls } = require("./personas");
test.use({ ignoreHTTPSErrors: true });
@@ -53,12 +54,12 @@ test("administrator: app → universal logout", async ({ page }) => {
const link = interactivePage
.getByRole("link", { name: /^(spaces|create|admin|settings)$/i })
.first();
if (await link.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await link.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await link.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/space|page|wiki|content|administration|confluence/i,
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
);
}
},
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { runAdminFlow } = require("./personas");
@@ -10,12 +11,12 @@ exports.register = function () {
const link = interactivePage
.getByRole("link", { name: /^(admin|tiles|navigation|tile)$/i })
.first();
if (await link.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await link.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await link.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/tiles|navigation|admin|services/i,
{ timeout: 30_000 }
{ timeout: resolveTimeout(30_000) }
);
}
},
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue } = require("./personas");
@@ -10,7 +11,7 @@ async function getCurrentImageSource(locator) {
}
async function expectImageLoaded(locator, label, expectedUrl) {
await expect(locator).toBeVisible({ timeout: 60_000 });
await expect(locator).toBeVisible({ timeout: resolveTimeout(60_000) });
const loaded = await locator.evaluate((img) => ({
source: img.currentSrc || img.src || "",
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
exports.register = function (shared) {
test("dashboard card icons resolve to the public Simple Icons URL when enabled, and to a Font Awesome icon otherwise", async ({ page }) => {
@@ -8,7 +9,7 @@ exports.register = function (shared) {
await expect(
page.locator(".card-img-top").first(),
"Expected at least one dashboard card with a rendered icon"
).toBeVisible({ timeout: 60_000 });
).toBeVisible({ timeout: resolveTimeout(60_000) });
// Regression guard for the original bug: no card may still reference the
// internal Docker bridge IP that the buggy DASHBOARD_SIMPLEICONS_SYNC_URL_BASE
@@ -23,7 +24,7 @@ exports.register = function (shared) {
await expect(
simpleiconsImg,
"Expected at least one card icon to load from the public Simple Icons domain (icon.<DOMAIN>)"
).toBeVisible({ timeout: 60_000 });
).toBeVisible({ timeout: resolveTimeout(60_000) });
const src = await simpleiconsImg.getAttribute("src");
expect(
@@ -34,7 +35,7 @@ exports.register = function (shared) {
await expect(
page.locator(".card-img-top i[class*='fa']:visible").first(),
"Expected card icons to fall back to a Font Awesome icon when the Simple Icons service is disabled"
).toBeVisible({ timeout: 60_000 });
).toBeVisible({ timeout: resolveTimeout(60_000) });
}
});
};
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, isVisible, normalizeBaseUrl } = require("./personas");
@@ -28,7 +29,7 @@ async function waitForFirstVisible(locators, timeout, errorMessage) {
async function getHeaderNavigation(page) {
const headerNav = page.locator("nav.menu-header").first();
await expect(headerNav).toBeVisible({ timeout: 60_000 });
await expect(headerNav).toBeVisible({ timeout: resolveTimeout(60_000) });
return headerNav;
}
@@ -46,14 +47,14 @@ async function expectLoggedOutHeaderAuthState(page) {
await expect
.poll(async () => await isVisible(controls.loginTrigger), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: "Expected dashboard OIDC JavaScript to expose Login before authentication",
})
.toBe(true);
await expect
.poll(async () => await isVisible(controls.accountTrigger), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: "Expected dashboard OIDC JavaScript to keep Account hidden before authentication",
})
.toBe(false);
@@ -66,16 +67,16 @@ async function expectLoggedInHeaderAuthState(page) {
await expect
.poll(async () => await isVisible(controls.accountTrigger), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: "Expected dashboard OIDC JavaScript to automatically switch the header button to Account",
})
.toBe(true);
await expect(controls.accountTrigger).toContainText(/Account/, { timeout: 60_000 });
await expect(controls.accountTrigger).toContainText(/Account/, { timeout: resolveTimeout(60_000) });
await expect
.poll(async () => await isVisible(controls.loginTrigger), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: "Expected dashboard OIDC JavaScript to hide Login after authentication",
})
.toBe(false);
@@ -97,7 +98,7 @@ async function isDropdownMenuOpen(trigger, menu) {
return expanded === "true" || (menuVisible && (menuHasShowClass || interactiveItemVisible));
}
async function waitForDropdownMenuOpen(trigger, menu, label, timeout = 3_000) {
async function waitForDropdownMenuOpen(trigger, menu, label, timeout = resolveTimeout(3_000)) {
await expect
.poll(async () => isDropdownMenuOpen(trigger, menu), {
timeout,
@@ -127,7 +128,7 @@ async function openDropdownMenu(trigger, menu, label) {
await attempt().catch(() => {});
try {
await waitForDropdownMenuOpen(trigger, menu, label, 2_500);
await waitForDropdownMenuOpen(trigger, menu, label, resolveTimeout(2_500));
return;
} catch {
// Try the next interaction strategy.
@@ -186,20 +187,20 @@ exports.register = function (shared) {
.poll(
async () => page.url().includes(expectedOidcAuthUrl) || (await isVisible(usernameField)),
{
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `Expected the dashboard login flow to reach the Keycloak auth page: ${expectedOidcAuthUrl}`,
}
)
.toBe(true);
await expect(usernameField).toBeVisible({ timeout: 60_000 });
await expect(usernameField).toBeVisible({ timeout: resolveTimeout(60_000) });
await usernameField.fill(loginUsername);
await passwordField.fill(loginPassword);
await signInButton.click();
await expect
.poll(async () => page.url().startsWith(appBaseUrl), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `Expected Keycloak login to redirect back to the dashboard: ${appBaseUrl}`,
})
.toBe(true);
@@ -209,7 +210,7 @@ exports.register = function (shared) {
await openDropdownMenu(loggedInControls.accountTrigger, loggedInControls.accountMenu, "Account");
const logoutEntry = await findAccountLogoutItem(loggedInControls.accountMenu);
await expect(logoutEntry).toBeVisible({ timeout: 10_000 });
await expect(logoutEntry).toBeVisible({ timeout: resolveTimeout(10_000) });
await expect(logoutEntry).toContainText(/logout/i);
await logoutEntry.click();
@@ -218,7 +219,7 @@ exports.register = function (shared) {
async () =>
page.url().includes("/protocol/openid-connect/logout") || page.url().startsWith(appBaseUrl),
{
timeout: 30_000,
timeout: resolveTimeout(30_000),
message: "Expected dashboard logout to reach Keycloak logout or redirect back to the dashboard",
}
)
@@ -1,8 +1,9 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { escapeRegex } = require("./personas");
async function waitForBoundingBoxStable(locator, { samples = 3, interval = 100, timeout = 10_000 } = {}) {
async function waitForBoundingBoxStable(locator, { samples = 3, interval = 100, timeout = resolveTimeout(10_000) } = {}) {
const deadline = Date.now() + timeout;
let previous = null;
let stableCount = 0;
@@ -50,11 +51,11 @@ async function expectStableCardHover(page, cardTitle) {
})
.first();
await expect(card, `Expected the ${cardTitle} card to be visible`).toBeVisible({ timeout: 60_000 });
await expect(card, `Expected the ${cardTitle} card to be visible`).toBeVisible({ timeout: resolveTimeout(60_000) });
const stretchedLink = card.locator("a.btn.stretched-link").first();
await expect(stretchedLink, `Expected the ${cardTitle} card to expose a stretched-link button`).toBeVisible({
timeout: 60_000,
timeout: resolveTimeout(60_000),
});
await card.scrollIntoViewIfNeeded();
@@ -82,7 +83,7 @@ async function expectStableCardHover(page, cardTitle) {
return stretchedLink.evaluate((element) => element.matches(":hover"));
},
{
timeout: 5_000,
timeout: resolveTimeout(5_000),
intervals: [100, 150, 250, 500],
message: `Expected the ${cardTitle} stretched-link overlay to stay hovered at y=${point.y * 100}% of the card`,
}
@@ -114,10 +115,10 @@ exports.register = function (shared) {
await expect(
simpleiconCard,
"Expected at least one dashboard card to render a Simple Icons-backed SVG or cached image asset"
).toBeVisible({ timeout: 60_000 });
).toBeVisible({ timeout: resolveTimeout(60_000) });
await expect(
simpleiconCard.locator(".card-img-top svg, .card-img-top img[src^='http://'], .card-img-top img[src^='https://'], .card-img-top img[src^='/static/']").first()
).toBeVisible({ timeout: 60_000 });
).toBeVisible({ timeout: resolveTimeout(60_000) });
await expectStableCardHover(page, "Keycloak");
});
};
@@ -7,6 +7,7 @@
// app URL.
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const dashboardTargetRoles = (() => {
const raw = process.env.DASHBOARD_TARGET_ROLES_JSON || "[]";
@@ -20,7 +21,7 @@ const dashboardTargetRoles = (() => {
async function findVisibleTile(page, canonicalDomain) {
const tile = page.locator(`a[href*="${canonicalDomain}"]:visible`).first();
const visible = await tile.isVisible({ timeout: 5_000 }).catch(() => false);
const visible = await tile.isVisible({ timeout: resolveTimeout(5_000) }).catch(() => false);
if (!visible) {
// Tile may be hidden inside a collapsed Bootstrap dropdown / accordion.
@@ -30,15 +31,15 @@ async function findVisibleTile(page, canonicalDomain) {
const triggerCount = await triggers.count().catch(() => 0);
for (let i = 0; i < triggerCount; i++) {
const t = triggers.nth(i);
if (!(await t.isVisible({ timeout: 200 }).catch(() => false))) continue;
await t.click({ timeout: 1_000 }).catch(() => {});
if (!(await t.isVisible({ timeout: resolveTimeout(200) }).catch(() => false))) continue;
await t.click({ timeout: resolveTimeout(1_000) }).catch(() => {});
}
}
await expect(
tile,
`dashboard tile for ${canonicalDomain} MUST be visible`
).toBeVisible({ timeout: 30_000 });
).toBeVisible({ timeout: resolveTimeout(30_000) });
return tile;
}
@@ -59,7 +60,7 @@ async function assertTileLoadsInIframe(page, target) {
// top-level navigation.
await expect
.poll(() => page.url(), {
timeout: 30_000,
timeout: resolveTimeout(30_000),
message: `Expected dashboard URL to embed ${target.canonical_domain} via ?iframe=... after clicking the ${target.id} tile`,
})
.toContain(target.canonical_domain);
@@ -68,7 +69,7 @@ async function assertTileLoadsInIframe(page, target) {
await expect(
iframe,
`Expected #main iframe to be present after clicking the ${target.id} tile`
).toBeVisible({ timeout: 30_000 });
).toBeVisible({ timeout: resolveTimeout(30_000) });
const iframeSrc = await iframe.getAttribute("src");
expect(
@@ -90,14 +91,14 @@ async function assertTabButtonOpensNewTab(page, context, target) {
await expect(
tabButton,
`Expected the dashboard header "Tab" button to be visible for the ${target.id} tile`
).toBeVisible({ timeout: 30_000 });
).toBeVisible({ timeout: resolveTimeout(30_000) });
const [popup] = await Promise.all([
context.waitForEvent("page", { timeout: 30_000 }),
context.waitForEvent("page", { timeout: resolveTimeout(30_000) }),
tabButton.click(),
]);
await popup.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await popup.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
expect(
popup.url(),
`Expected popup tab URL to contain ${target.canonical_domain}, got ${popup.url()}`
@@ -1,5 +1,6 @@
// @ts-check
const { test, expect } = require('@playwright/test');
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, runAdminFlow, runBiberFlow, runGuestFlow } = require("./personas");
const { skipUnlessServiceEnabled } = require("./service-gating");
@@ -38,7 +39,7 @@ async function login(page, email, password) {
}
}
const emailInput = page.getByLabel(/email/i).first();
await emailInput.waitFor({ state: "attached", timeout: 60000 });
await emailInput.waitFor({ state: "attached", timeout: resolveTimeout(60000) });
await emailInput.fill(email);
await page.locator("input[type='password']").first().fill(password);
await page.getByRole('button', { name: /log in|sign in/i }).first().click();
@@ -52,7 +53,7 @@ async function dismissCookieBanner(page) {
const acceptBtn = page.getByRole("button", { name: /accept all|accept only essential|accept/i }).first();
if (await acceptBtn.isVisible().catch(() => false)) {
await acceptBtn.click().catch(() => {});
await page.waitForLoadState("networkidle", { timeout: 5000 }).catch(() => {});
await page.waitForLoadState("networkidle", { timeout: resolveTimeout(5000) }).catch(() => {});
}
}
@@ -70,7 +71,7 @@ async function oidcLogin(page, username, password) {
const ssoHref = await ssoButton.getAttribute("href");
console.log("SSO href:", ssoHref);
await page.waitForFunction(() => document.readyState === "complete");
const navigated = page.waitForURL(/auth\.infinito\.example/, { timeout: 30000 });
const navigated = page.waitForURL(/auth\.infinito\.example/, { timeout: resolveTimeout(30000) });
await page.evaluate((href) => {
const csrfToken = document.querySelector("meta[name='csrf-token']")?.getAttribute("content") || "";
const form = document.createElement("form");
@@ -92,7 +93,7 @@ async function oidcLogin(page, username, password) {
await page.getByRole("textbox", { name: /password/i }).fill(password);
await page.locator("#kc-login").click();
console.log("Login clicked, waiting for redirect back...");
await page.waitForLoadState("networkidle", { timeout: 30000 }).catch(e => console.log("networkidle error:", e.message));
await page.waitForLoadState("networkidle", { timeout: resolveTimeout(30000) }).catch(e => console.log("networkidle error:", e.message));
console.log("After login URL:", page.url());
await dismissCookieBanner(page);
@@ -110,9 +111,9 @@ async function oidcLogin(page, username, password) {
// sets the continue flag and re-submits the form.
await page.evaluate(() => document.getElementById("omniauth-register-form").requestSubmit());
const declineBtn = page.locator("#sign-up-newsletter-modal [data-check='false']").first();
await declineBtn.waitFor({ state: "visible", timeout: 10000 });
await declineBtn.waitFor({ state: "visible", timeout: resolveTimeout(10000) });
await Promise.all([
page.waitForURL((url) => !/auth\/openid_connect\/callback/.test(url.toString()), { timeout: 30000 }).catch(e => console.log("post-submit nav error:", e.message)),
page.waitForURL((url) => !/auth\/openid_connect\/callback/.test(url.toString()), { timeout: resolveTimeout(30000) }).catch(e => console.log("post-submit nav error:", e.message)),
declineBtn.click({ force: true }),
]);
console.log("After registration URL:", page.url());
@@ -185,12 +186,12 @@ test("administrator: app → universal logout", async ({ page }) => {
const link = interactivePage
.getByRole("link", { name: /^(admin|administration|configuration|participants)$/i })
.first();
if (await link.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await link.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await link.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/admin|administration|participants|processes|assemblies|moderation/i,
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
);
}
},
@@ -11,6 +11,7 @@
// check proves the bucket grew via the Filer UI.
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { skipUnlessServiceEnabled } = require("./service-gating");
const { runSeaweedfsStorageCheck, decodeDotenvQuotedValue } = require("./personas");
@@ -23,7 +24,7 @@ test.use({ ignoreHTTPSErrors: true });
test("seaweedfs: an uploaded Decidim avatar is stored in the SeaweedFS bucket", async ({ page, browser }) => {
skipUnlessServiceEnabled("seaweedfs");
test.setTimeout(180_000);
test.setTimeout(resolveTimeout(180_000));
const baseUrl = decodeDotenvQuotedValue(process.env.DECIDIM_BASE_URL || process.env.APP_BASE_URL).replace(/\/$/, "");
const adminEmail = decodeDotenvQuotedValue(process.env.ADMIN_EMAIL);
@@ -48,7 +49,7 @@ test("seaweedfs: an uploaded Decidim avatar is stored in the SeaweedFS bucket",
}
const emailInput = appPage.getByLabel(/email/i).first();
await emailInput.waitFor({ state: "attached", timeout: 60_000 });
await emailInput.waitFor({ state: "attached", timeout: resolveTimeout(60_000) });
await emailInput.fill(adminEmail);
await appPage.locator("input[type='password']").first().fill(adminPassword);
await appPage.getByRole("button", { name: /log in|sign in/i }).first().click();
@@ -62,7 +63,7 @@ test("seaweedfs: an uploaded Decidim avatar is stored in the SeaweedFS bucket",
.locator('[data-upload] button, .upload-container button, [data-upload-modal]')
.or(appPage.getByRole("button", { name: /avatar|add file|add image|edit image|replace image|upload|change/i }))
.first();
if (await openModal.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await openModal.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await openModal.click().catch(() => {});
await appPage.waitForLoadState("networkidle").catch(() => {});
}
@@ -70,14 +71,14 @@ test("seaweedfs: an uploaded Decidim avatar is stored in the SeaweedFS bucket",
const fileInput = appPage
.locator('[data-upload] input[type="file"], .upload-modal input[type="file"], input[type="file"]')
.first();
await fileInput.waitFor({ state: "attached", timeout: 60_000 });
await fileInput.waitFor({ state: "attached", timeout: resolveTimeout(60_000) });
const marker = `infinito-storage-check-${Date.now()}.png`;
await fileInput.setInputFiles({ name: marker, mimeType: "image/png", buffer: PNG_64x64 });
const saveModal = appPage.locator("[data-dropzone-save]").first();
if (await saveModal.isVisible().catch(() => false)) {
await expect(saveModal).toBeEnabled({ timeout: 60_000 });
await expect(saveModal).toBeEnabled({ timeout: resolveTimeout(60_000) });
await saveModal.click().catch(() => {});
}
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("../timeouts");
const { skipUnlessAddonEnabled } = require("../addon-gating");
const { normalizeBaseUrl, decodeDotenvQuotedValue, performKeycloakLoginForm } = require("../personas");
@@ -27,7 +28,7 @@ async function signInViaOidc(page) {
await expect
.poll(() => page.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `expected redirect to Keycloak OIDC auth (${expectedOidcAuthUrl})`,
})
.toContain(expectedOidcAuthUrl);
@@ -36,7 +37,7 @@ async function signInViaOidc(page) {
await expect
.poll(() => page.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `expected redirect back to discourse at ${discourseBaseUrl}`,
})
.toContain(discourseBaseUrl);
@@ -62,7 +63,7 @@ test("discourse-activity-pub: federation plugin installed and the live ActivityP
await expect(page.locator("body")).toContainText(
/topic|category|welcome|latest|discourse/i,
{ timeout: 60_000 },
{ timeout: resolveTimeout(60_000) },
);
const siteSettings = await page.evaluate(async (base) => {
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("../timeouts");
const { skipUnlessAddonEnabled } = require("../addon-gating");
const { normalizeBaseUrl, decodeDotenvQuotedValue, performKeycloakLoginForm } = require("../personas");
@@ -27,7 +28,7 @@ async function signInViaOidc(page) {
await expect
.poll(() => page.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `expected redirect to Keycloak OIDC auth (${expectedOidcAuthUrl})`,
})
.toContain(expectedOidcAuthUrl);
@@ -36,7 +37,7 @@ async function signInViaOidc(page) {
await expect
.poll(() => page.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `expected redirect back to discourse at ${discourseBaseUrl}`,
})
.toContain(discourseBaseUrl);
@@ -60,7 +61,7 @@ test("discourse-akismet: spam-filtering plugin is installed and coupled to the A
await expect(page.locator("body")).toContainText(
/topic|category|welcome|latest|discourse/i,
{ timeout: 60_000 },
{ timeout: resolveTimeout(60_000) },
);
const siteSettings = await page.evaluate(async (base) => {
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("../timeouts");
const { skipUnlessAddonEnabled } = require("../addon-gating");
const { skipUnlessServiceEnabled } = require("../service-gating");
const { normalizeBaseUrl, decodeDotenvQuotedValue, performKeycloakLoginForm } = require("../personas");
@@ -28,7 +29,7 @@ async function signInViaOidc(page) {
await expect
.poll(() => page.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `expected redirect to Keycloak OIDC auth (${expectedOidcAuthUrl})`,
})
.toContain(expectedOidcAuthUrl);
@@ -37,7 +38,7 @@ async function signInViaOidc(page) {
await expect
.poll(() => page.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `expected redirect back to discourse at ${discourseBaseUrl}`,
})
.toContain(discourseBaseUrl);
@@ -56,7 +57,7 @@ function settingValue(settings, name) {
test("discourse-ldap-auth: LDAP auth plugin binds Discourse to the distinct LDAP partner directory", async ({ page }) => {
skipUnlessAddonEnabled("discourse-ldap-auth");
skipUnlessServiceEnabled("ldap");
test.setTimeout(120_000);
test.setTimeout(resolveTimeout(120_000));
expect(oidcIssuerUrl, "OIDC_ISSUER_URL must be set").toBeTruthy();
expect(discourseBaseUrl, "DISCOURSE_BASE_URL must be set").toBeTruthy();
@@ -71,7 +72,7 @@ test("discourse-ldap-auth: LDAP auth plugin binds Discourse to the distinct LDAP
await expect(page.locator("body")).toContainText(
/topic|category|welcome|latest|discourse/i,
{ timeout: 60_000 },
{ timeout: resolveTimeout(60_000) },
);
const siteSettings = await page.evaluate(async (base) => {
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("../timeouts");
const { skipUnlessAddonEnabled } = require("../addon-gating");
const { normalizeBaseUrl, decodeDotenvQuotedValue, performKeycloakLoginForm } = require("../personas");
@@ -27,7 +28,7 @@ async function signInViaOidc(page) {
await expect
.poll(() => page.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `expected redirect to Keycloak OIDC auth (${expectedOidcAuthUrl})`,
})
.toContain(expectedOidcAuthUrl);
@@ -36,7 +37,7 @@ async function signInViaOidc(page) {
await expect
.poll(() => page.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `expected redirect back to discourse at ${discourseBaseUrl}`,
})
.toContain(discourseBaseUrl);
@@ -60,7 +61,7 @@ test("docker_manager: the docker_manager plugin is installed and registered on t
await expect(page.locator("body")).toContainText(
/topic|category|welcome|latest|discourse/i,
{ timeout: 60_000 },
{ timeout: resolveTimeout(60_000) },
);
const plugins = await page.evaluate(async (base) => {
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { assertCspMetaParity, assertCspResponseHeader, decodeDotenvQuotedValue, expectNoCspViolations, installCspViolationObserver, normalizeBaseUrl, performKeycloakLoginForm, runGuestFlow } = require("./personas");
const { skipUnlessServiceEnabled } = require("./service-gating");
@@ -101,7 +102,7 @@ async function signInViaDashboardOidc(page, username, password, personaLabel) {
await expect
.poll(() => page.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `${personaLabel}: expected redirect to Keycloak OIDC auth (${expectedOidcAuthUrl})`
})
.toContain(expectedOidcAuthUrl);
@@ -110,7 +111,7 @@ async function signInViaDashboardOidc(page, username, password, personaLabel) {
await expect
.poll(() => page.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `${personaLabel}: expected redirect back to discourse at ${discourseBaseUrl}`
})
.toContain(discourseBaseUrl);
@@ -122,7 +123,7 @@ test("administrator: discourse OIDC login and logout", async ({ page }) => {
await signInViaDashboardOidc(page, adminUsername, adminPassword, "administrator");
await expect(page.locator("body")).toContainText(/topic|category|welcome|latest|discourse/i, { timeout: 60_000 });
await expect(page.locator("body")).toContainText(/topic|category|welcome|latest|discourse/i, { timeout: resolveTimeout(60_000) });
await discourseLogout(page, discourseBaseUrl);
@@ -137,7 +138,7 @@ test("administrator: discourse OIDC login and logout", async ({ page }) => {
.count()
.catch(() => 0)) > 0,
{
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: "Expected discourse to require a new sign-in after logout"
}
)
@@ -152,7 +153,7 @@ test("biber: discourse OIDC login and logout", async ({ page }) => {
await signInViaDashboardOidc(page, biberUsername, biberPassword, "biber");
await expect(page.locator("body")).toContainText(/topic|category|welcome|latest|discourse/i, { timeout: 60_000 });
await expect(page.locator("body")).toContainText(/topic|category|welcome|latest|discourse/i, { timeout: resolveTimeout(60_000) });
await discourseLogout(page, discourseBaseUrl);
@@ -167,7 +168,7 @@ test("biber: discourse OIDC login and logout", async ({ page }) => {
.count()
.catch(() => 0)) > 0,
{
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: "Expected discourse to require a new sign-in after logout"
}
)
@@ -1,4 +1,5 @@
const { expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, performKeycloakLoginForm } = require("./personas");
const { isServiceEnabled, skipUnlessServiceEnabled } = require("./service-gating");
@@ -30,13 +31,13 @@ async function signInViaErpnextLocal(page, username, password, personaLabel) {
await page.fill("input#login_password", password);
await Promise.all([
page.waitForURL((url) => url.toString().includes(canonicalDomain) && !url.toString().includes("/login"), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
}),
page.click("button.btn-login, button[type='submit']").catch(() => page.keyboard.press("Enter")),
]);
await expect
.poll(() => page.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `${personaLabel}: expected post-login redirect on ${canonicalDomain} (not /login)`,
})
.not.toContain("/login");
@@ -55,12 +56,12 @@ async function signInViaErpnextOidc(page, username, password, personaLabel) {
await expect(
oidcSignIn,
`${personaLabel}: the Keycloak SSO button must render on /login (Social Login Key not picked up by the workers?)`,
).toBeVisible({ timeout: 30_000 });
).toBeVisible({ timeout: resolveTimeout(30_000) });
await oidcSignIn.click();
await expect
.poll(() => page.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `${personaLabel}: expected redirect to Keycloak OIDC auth (${expectedOidcAuthUrl})`,
})
.toContain(expectedOidcAuthUrl);
@@ -69,7 +70,7 @@ async function signInViaErpnextOidc(page, username, password, personaLabel) {
await expect
.poll(() => page.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `${personaLabel}: expected redirect back to ERPNext at ${erpnextBaseUrl}`,
})
.toContain(canonicalDomain);
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
exports.register = function (shared) {
test("administrator: ERPNext OIDC login lands on Frappe desk", async ({ page }) => {
@@ -9,7 +10,7 @@ exports.register = function (shared) {
await shared.signInViaErpnextOidc(page, shared.env.adminUsername, shared.env.adminPassword, "administrator");
await expect(page.locator("body")).toContainText(/desk|workspace|erpnext|home|dashboard/i, { timeout: 60_000 });
await expect(page.locator("body")).toContainText(/desk|workspace|erpnext|home|dashboard/i, { timeout: resolveTimeout(60_000) });
await shared.erpnextLogout(page);
});
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
exports.register = function (shared) {
test("biber: ERPNext OIDC login lands on authenticated surface", async ({ page }) => {
@@ -9,7 +10,7 @@ exports.register = function (shared) {
await shared.signInViaErpnextOidc(page, shared.env.biberUsername, shared.env.biberPassword, "biber");
await expect(page.locator("body")).toContainText(/desk|workspace|erpnext|home|dashboard|portal/i, { timeout: 60_000 });
await expect(page.locator("body")).toContainText(/desk|workspace|erpnext|home|dashboard|portal/i, { timeout: resolveTimeout(60_000) });
await shared.erpnextLogout(page);
});
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
exports.register = function (shared) {
// Break-glass path: not gated on any service flag (must work in every variant).
@@ -7,7 +8,7 @@ exports.register = function (shared) {
await shared.signInViaErpnextLocal(page, "Administrator", shared.env.adminNativePassword, "administrator-native");
await expect(page.locator("body")).toContainText(/desk|workspace|erpnext|home|dashboard/i, { timeout: 60_000 });
await expect(page.locator("body")).toContainText(/desk|workspace|erpnext|home|dashboard/i, { timeout: resolveTimeout(60_000) });
await shared.erpnextLogout(page);
});
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls } = require("./personas");
test.use({ ignoreHTTPSErrors: true });
@@ -53,12 +54,12 @@ test("administrator: app → universal logout", async ({ page }) => {
const link = interactivePage
.getByRole("link", { name: /^(admin|leads|accounts|contacts)$/i })
.first();
if (await link.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await link.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await link.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/lead|account|contact|opportunity|crm|administration/i,
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
);
}
},
@@ -2,6 +2,7 @@
// the cross-Fediverse posting flow, wall-content assertions, and the
// `beforeEach` env-presence guard. `playwright.spec.js` wires the
// lifecycle hook and `require()`s one test module per scenario so each
const { resolveTimeout } = require("./timeouts");
// test stays atomar.
const { expect } = require("@playwright/test");
@@ -48,7 +49,7 @@ async function beforeEach({ page }) {
async function loginToMastodonViaOidc(page, baseUrl) {
await page.goto(`${baseUrl}/auth/sign_in`);
const oidcLink = page.locator("a[href*='/auth/auth/openid_connect'], a[href*='/auth/openid_connect']").first();
await expect(oidcLink, "Expected Mastodon OIDC sign-in link").toBeVisible({ timeout: 30_000 });
await expect(oidcLink, "Expected Mastodon OIDC sign-in link").toBeVisible({ timeout: resolveTimeout(30_000) });
await Promise.all([
// eslint-disable-next-line playwright/no-wait-for-navigation -- the navigation target depends on the OIDC issuer; waitForURL would need a runtime-built pattern. The Promise.all is the documented Playwright pattern for "click triggers navigation".
@@ -86,7 +87,7 @@ async function loginToMastodonViaOidc(page, baseUrl) {
.locator("button[type='submit'], button")
.filter({ hasText: /save and continue/i })
.first();
if (await saveAndContinue.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await saveAndContinue.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await saveAndContinue.click();
}
@@ -100,7 +101,7 @@ async function loginToMastodonViaOidc(page, baseUrl) {
)
.first(),
"Expected Mastodon compose textarea after OIDC sign-in"
).toBeVisible({ timeout: 60_000 });
).toBeVisible({ timeout: resolveTimeout(60_000) });
}
async function postOnMastodonViaUi(page, baseUrl, statusText) {
@@ -118,7 +119,7 @@ async function postOnMastodonViaUi(page, baseUrl, statusText) {
// Mastodon clears the textarea after a successful post.
await expect(compose, "Expected Mastodon compose textarea to clear after publish").toHaveValue(
"",
{ timeout: 30_000 }
{ timeout: resolveTimeout(30_000) }
);
}
@@ -137,7 +138,7 @@ async function postOnMastodonViaUi(page, baseUrl, statusText) {
// v2 no oauth2-proxy gating, /login is friendica's own form directly.
async function loginViaFriendicaForm(page) {
const passwordField = page.locator("input[name='password']").first();
await passwordField.waitFor({ state: "visible", timeout: 30_000 });
await passwordField.waitFor({ state: "visible", timeout: resolveTimeout(30_000) });
const usernameField = page.locator("input[name='username']").first();
const loginForm = page.locator("form").filter({ has: passwordField });
const signInButton = loginForm
@@ -164,7 +165,7 @@ async function postOnFriendicaViaUi(page, baseUrl, statusText) {
.poll(() => {
try { return new URL(page.url()).host; } catch { return ""; }
}, {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `Expected page host to return to ${friendicaHost} after Keycloak login`,
})
.toBe(friendicaHost);
@@ -175,8 +176,8 @@ async function postOnFriendicaViaUi(page, baseUrl, statusText) {
// trusted SSO addon; fill it so ldapauth's else-branch starts the
// friendica session. In v2 the password form is already visible from
// step 1's `/` redirect to `/login`.
await page.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
if (await page.locator("input[name='password']").first().isVisible({ timeout: 30_000 }).catch(() => false)) {
await page.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
if (await page.locator("input[name='password']").first().isVisible({ timeout: resolveTimeout(30_000) }).catch(() => false)) {
await loginViaFriendicaForm(page);
}
@@ -185,7 +186,7 @@ async function postOnFriendicaViaUi(page, baseUrl, statusText) {
await expect(
compose,
"Expected Friendica /compose textarea after ldapauth sign-in"
).toBeVisible({ timeout: 60_000 });
).toBeVisible({ timeout: resolveTimeout(60_000) });
await compose.fill(statusText);
await page.locator("button[name='submit'][type='submit']").first().click();
@@ -196,7 +197,7 @@ async function postOnFriendicaViaUi(page, baseUrl, statusText) {
await expect(
page.locator(".wall-item-body, .wall-item-content").filter({ hasText: statusText }).first(),
"Expected Friendica profile timeline to surface the new status"
).toBeVisible({ timeout: 60_000 });
).toBeVisible({ timeout: resolveTimeout(60_000) });
}
async function expectPostVisibleOnWall(page, wallUrl, needle, timeoutMs = 60000) {
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
exports.register = function (shared) {
test("administrator: app → universal logout", async ({ page }) => {
@@ -8,12 +9,12 @@ exports.register = function (shared) {
const link = interactivePage
.getByRole("link", { name: /^(admin|configuration|settings)$/i })
.first();
if (await link.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await link.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await link.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/configuration|admin|fediwall|settings/i,
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
);
}
},
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, performKeycloakLoginForm, runAdminFlow, runBiberFlow, runGuestFlow } = require("./personas");
const { isServiceEnabled } = require("./service-gating");
@@ -26,14 +27,14 @@ async function clickFiderSsoButton(locator) {
// Click "Sign in" in the Fider header
const signInLink = locator.getByRole("link", { name: /sign in/i });
await signInLink.first().waitFor({ state: "visible", timeout: 30_000 });
await signInLink.first().waitFor({ state: "visible", timeout: resolveTimeout(30_000) });
await signInLink.first().click();
// Fider shows a "Join the conversation" modal with a "Continue with ... SSO" button.
// The display_name is set to "{{ SOFTWARE_NAME }} SSO" = "Infinito.Nexus SSO".
const ssoButton = locator.getByRole("link", { name: /continue with/i });
await ssoButton.first().waitFor({ state: "visible", timeout: 15_000 });
await ssoButton.first().waitFor({ state: "visible", timeout: resolveTimeout(15_000) });
// force: true bypasses aria-disabled which Fider sets on the button during modal render
await ssoButton.first().click({ force: true });
}
@@ -65,14 +66,14 @@ test("fider: admin sso login, verify ui, logout", async ({ page }) => {
// 4. After login Fider redirects back — verify the admin
// is logged in (.c-menu-user is only rendered when fider.session.isAuthenticated)
await expect(page.locator(".c-menu-user").first()).toBeVisible({ timeout: 60_000 });
await expect(page.locator(".c-menu-user").first()).toBeVisible({ timeout: resolveTimeout(60_000) });
// 5. Logout — navigate to Fider's sign-out endpoint
await page.goto(`${expectedFiderBaseUrl}/signout`, { waitUntil: "domcontentloaded" }).catch(() => {});
// 6. Verify signout — the public Fider page should no longer show the user menu
await page.goto(`${expectedFiderBaseUrl}/`);
await expect(page.locator(".c-menu-user")).not.toBeAttached({ timeout: 10_000 });
await expect(page.locator(".c-menu-user")).not.toBeAttached({ timeout: resolveTimeout(10_000) });
});
// Scenario II: biber logs in directly to Fider as a regular user → verifies access → logs out
@@ -101,7 +102,7 @@ test("fider: biber sso login as regular user, verify access, logout", async ({ b
// 3. Wait for Keycloak OIDC auth
await expect
.poll(() => biberPage.url(), {
timeout: 30_000,
timeout: resolveTimeout(30_000),
message: `Expected redirect to Keycloak OIDC auth: ${expectedOidcAuthUrl}`
})
.toContain(expectedOidcAuthUrl);
@@ -112,18 +113,18 @@ test("fider: biber sso login as regular user, verify access, logout", async ({ b
// 5. After login Fider redirects back
await expect
.poll(() => biberPage.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `Expected redirect back to Fider after biber login: ${expectedFiderBaseUrl}`
})
.toContain(expectedFiderBaseUrl);
// 6. Verify biber is logged in — .c-menu-user is only rendered when authenticated
await expect(biberPage.locator(".c-menu-user").first()).toBeVisible({ timeout: 30_000 });
await expect(biberPage.locator(".c-menu-user").first()).toBeVisible({ timeout: resolveTimeout(30_000) });
// 7. Verify biber is NOT shown admin controls.
// The admin link is inside the dropdown AND gated by isCollaborator — so it is
// never in the DOM for a regular user (regardless of dropdown state).
await expect(biberPage.locator("a[href='/admin']").first()).not.toBeAttached({ timeout: 5_000 });
await expect(biberPage.locator("a[href='/admin']").first()).not.toBeAttached({ timeout: resolveTimeout(5_000) });
// 8. Logout
await biberPage.goto(`${expectedFiderBaseUrl}/signout`, { waitUntil: "domcontentloaded" }).catch(() => {});
@@ -152,12 +153,12 @@ test("administrator: app → universal logout", async ({ page }) => {
const link = interactivePage
.getByRole("link", { name: /^(admin|posts|users|invitations|settings)$/i })
.first();
if (await link.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await link.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await link.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/posts|administration|invitations|users|settings|tags/i,
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
);
}
},
@@ -16,6 +16,7 @@
// and the SEAWEEDFS_* keys consumed by runSeaweedfsStorageCheck.
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { skipUnlessServiceEnabled } = require("./service-gating");
const { runSeaweedfsStorageCheck, performKeycloakLoginForm, decodeDotenvQuotedValue } = require("./personas");
@@ -33,11 +34,11 @@ const LOGO_PNG = Buffer.from(
async function clickFiderSsoButton(locator) {
const signInLink = locator.getByRole("link", { name: /sign in/i });
await signInLink.first().waitFor({ state: "visible", timeout: 30_000 });
await signInLink.first().waitFor({ state: "visible", timeout: resolveTimeout(30_000) });
await signInLink.first().click();
const ssoButton = locator.getByRole("link", { name: /continue with/i });
await ssoButton.first().waitFor({ state: "visible", timeout: 15_000 });
await ssoButton.first().waitFor({ state: "visible", timeout: resolveTimeout(15_000) });
await ssoButton.first().click({ force: true });
}
@@ -45,7 +46,7 @@ test.use({ ignoreHTTPSErrors: true });
test("seaweedfs: an uploaded Fider tenant logo is stored in the SeaweedFS bucket", async ({ page, browser }) => {
skipUnlessServiceEnabled("seaweedfs");
test.setTimeout(180_000);
test.setTimeout(resolveTimeout(180_000));
const fiderBaseUrl = decodeDotenvQuotedValue(process.env.FIDER_BASE_URL || "");
const adminUsername = decodeDotenvQuotedValue(process.env.ADMIN_USERNAME || "");
@@ -68,7 +69,7 @@ test("seaweedfs: an uploaded Fider tenant logo is stored in the SeaweedFS bucket
await expect(
appPage.locator(".c-menu-user").first(),
"the administrator must be authenticated before opening the admin settings",
).toBeVisible({ timeout: 60_000 });
).toBeVisible({ timeout: resolveTimeout(60_000) });
await appPage.goto(`${baseUrl}/admin`, { waitUntil: "domcontentloaded" });
@@ -76,7 +77,7 @@ test("seaweedfs: an uploaded Fider tenant logo is stored in the SeaweedFS bucket
await expect(
fileInput,
"the Fider admin General settings page must expose the 'Your Logo' image input",
).toBeAttached({ timeout: 60_000 });
).toBeAttached({ timeout: resolveTimeout(60_000) });
const marker = `infinito-storage-check-${Date.now()}.png`;
await fileInput.setInputFiles({
@@ -88,17 +89,17 @@ test("seaweedfs: an uploaded Fider tenant logo is stored in the SeaweedFS bucket
await expect(
appPage.locator('.c-image-upload img[src^="data:image"], .preview img[src^="data:image"]').first(),
"the selected logo must be loaded into the form (preview rendered) before saving",
).toBeVisible({ timeout: 30_000 });
).toBeVisible({ timeout: resolveTimeout(30_000) });
const saveButton = appPage.getByRole("button", { name: /^save$/i }).first();
await expect(
saveButton,
"the Fider admin General settings page must expose a Save action that persists the logo",
).toBeVisible({ timeout: 60_000 });
).toBeVisible({ timeout: resolveTimeout(60_000) });
const [settingsResp] = await Promise.all([
appPage.waitForResponse(
(r) => /\/_api\/admin\/settings\/general/.test(r.url()) && r.request().method() === "POST",
{ timeout: 60_000 },
{ timeout: resolveTimeout(60_000) },
),
saveButton.click(),
]);
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { runAdminFlow } = require("./personas");
test.use({ ignoreHTTPSErrors: true });
@@ -10,12 +11,12 @@ test("administrator: app → universal logout", async ({ page }) => {
const link = interactivePage
.getByRole("link", { name: /^(admin|api keys|chatflows|tools|credentials|users)$/i })
.first();
if (await link.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await link.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await link.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/api keys|chatflows|tools|credentials|users|workspaces?/i,
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
);
}
},
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { skipUnlessServiceEnabled } = require("./service-gating");
const { decodeDotenvQuotedValue, normalizeBaseUrl, performKeycloakLoginForm } = require("./personas");
@@ -24,16 +25,16 @@ test("OIDC: oauth2-proxy redirects unauthenticated visitors through Keycloak (va
await page.goto(`${expectedBaseUrl}/`);
await expect
.poll(() => page.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `expected redirect to Keycloak OIDC auth (${expectedOidcAuthUrl})`
})
.toContain(expectedOidcAuthUrl);
await performKeycloakLoginForm(page, adminUsername, adminPassword);
await expect
.poll(() => page.url(), {
timeout: 90_000,
timeout: resolveTimeout(90_000),
message: `expected redirect back to Flowise at ${expectedBaseUrl}`
})
.toContain(expectedBaseUrl);
await expect(page.locator("body")).toBeVisible({ timeout: 60_000 });
await expect(page.locator("body")).toBeVisible({ timeout: resolveTimeout(60_000) });
});
@@ -1,6 +1,7 @@
// Shared Friendica Playwright spec state: env vars, login/logout flow
// helpers, and the variant-aware path selectors. `playwright.spec.js`
// wires the lifecycle hook and `require()`s one test module per scenario
const { resolveTimeout } = require("./timeouts");
// so each test stays atomar and individually inspectable.
//
// Variant matrix (see meta/variants.yml):
@@ -28,7 +29,7 @@ function trimmedBaseUrl() {
async function loginViaFriendicaForm(page, username, password) {
const passwordField = page.locator("input[name='password']").first();
await passwordField.waitFor({ state: "visible", timeout: 30_000 });
await passwordField.waitFor({ state: "visible", timeout: resolveTimeout(30_000) });
const usernameField = page.locator("input[name='username']").first();
const loginForm = page.locator("form").filter({ has: passwordField });
@@ -68,14 +69,14 @@ async function loginViaOauth2ProxyAndFriendica(page, username, password) {
.poll(() => {
try { return new URL(page.url()).hostname; } catch { return ""; }
}, {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `Expected page hostname to become ${canonicalDomain} after Keycloak login`,
})
.toBe(canonicalDomain);
// Step 2: Friendica's own login form (post-oauth2-proxy).
await page.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
if (await page.locator("input[name='password']").first().isVisible({ timeout: 30_000 }).catch(() => false)) {
await page.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
if (await page.locator("input[name='password']").first().isVisible({ timeout: resolveTimeout(30_000) }).catch(() => false)) {
await loginViaFriendicaForm(page, username, password);
}
@@ -86,7 +87,7 @@ async function loginViaOauth2ProxyAndFriendica(page, username, password) {
// fallback for less-canonical themes.
await page.goto(`${baseUrl}/network`, { waitUntil: "domcontentloaded" }).catch(() => {});
const profileMenu = page.locator("#topbar-first, #navbar-apps-menu, a[href*='/logout']").first();
await profileMenu.waitFor({ state: "visible", timeout: 60_000 });
await profileMenu.waitFor({ state: "visible", timeout: resolveTimeout(60_000) });
}
// v2 path: no oauth2-proxy gating, friendica's `/login` is directly reachable.
@@ -99,7 +100,7 @@ async function loginViaFriendicaDirect(page, username, password) {
await loginViaFriendicaForm(page, username, password);
await page.goto(`${baseUrl}/network`, { waitUntil: "domcontentloaded" }).catch(() => {});
const profileMenu = page.locator("#topbar-first, #navbar-apps-menu, a[href*='/logout']").first();
await profileMenu.waitFor({ state: "visible", timeout: 60_000 });
await profileMenu.waitFor({ state: "visible", timeout: resolveTimeout(60_000) });
}
function pickLoginPath() {
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("../timeouts");
const { skipUnlessAddonEnabled } = require("../addon-gating");
const { skipUnlessServiceEnabled } = require("../service-gating");
const shared = require("../_shared");
@@ -32,7 +33,7 @@ test("addon ldapauth: a valid LDAP credential binds against openldap and a bogus
await expect(
authenticatedSurface,
"Expected an authenticated Friendica surface after the ldapauth-backed login"
).toBeVisible({ timeout: 60_000 });
).toBeVisible({ timeout: resolveTimeout(60_000) });
await expect(
page.locator("input[name='password']"),
@@ -49,13 +50,13 @@ test("addon ldapauth: a valid LDAP credential binds against openldap and a bogus
const hasLoginForm = await page
.locator("input[name='password']")
.first()
.isVisible({ timeout: 2_000 })
.isVisible({ timeout: resolveTimeout(2_000) })
.catch(() => false);
if (onAdminAddons && !hasLoginForm) {
await expect(
page.locator("a[href*='admin/addons/ldapauth'], :text('ldapauth')").first(),
"Expected ldapauth to appear in Friendica's admin addon list when the admin panel is reachable"
).toBeVisible({ timeout: 15_000 });
).toBeVisible({ timeout: resolveTimeout(15_000) });
}
await shared.friendicaLogout(page);
@@ -79,7 +80,7 @@ test("addon ldapauth: a valid LDAP credential binds against openldap and a bogus
const hasForm = await negPage
.locator("input[name='password']")
.first()
.isVisible({ timeout: 30_000 })
.isVisible({ timeout: resolveTimeout(30_000) })
.catch(() => false);
// Only meaningful where Friendica's own login form is directly reachable
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
// Administrator follows biber on friendica.
//
@@ -60,7 +61,7 @@ exports.register = function (shared) {
// search form appears earlier in the document so a generic form.first()
// selector would hit the wrong target.
const submitButton = adminPage.locator("#dfrn-request-submit-button");
await submitButton.waitFor({ state: "visible", timeout: 60_000 });
await submitButton.waitFor({ state: "visible", timeout: resolveTimeout(60_000) });
await Promise.all([
adminPage.waitForLoadState("domcontentloaded"),
submitButton.click(),
@@ -70,7 +71,7 @@ exports.register = function (shared) {
// detail page of the freshly-persisted local contact row.
await expect
.poll(() => adminPage.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: "Expected /contact/follow POST to land on /contact/<id> after persisting biber as a contact",
})
.toMatch(/\/contact\/\d+(?:[/?#]|$)/);
@@ -81,7 +82,7 @@ exports.register = function (shared) {
await expect(
adminPage.locator("body"),
`Expected biber's handle "${expectedHandle}" on /contact/<id> after follow`
).toContainText(expectedHandle, { timeout: 30_000 });
).toContainText(expectedHandle, { timeout: resolveTimeout(30_000) });
} finally {
await adminContext.close().catch(() => {});
}
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
// Anonymous (logged-out) visitor reaches biber's friendica profile.
//
@@ -47,7 +48,7 @@ exports.register = function (shared) {
await expect(
anonPage.locator("body"),
`Expected biber's profile body to mention "${shared.env.biberUsername}"`
).toContainText(shared.env.biberUsername, { timeout: 30_000 });
).toContainText(shared.env.biberUsername, { timeout: resolveTimeout(30_000) });
// No authenticated nav element. Anonymous viewers must not see
// their own /logout link (frio / vier both attach it to the
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
// Administrator native login — picks the variant-specific path at runtime.
// Skips entirely when neither oauth2 nor ldap is enabled (v1 has no auth
@@ -18,6 +19,6 @@ exports.register = function (shared) {
await shared.friendicaLogout(page);
await page.goto(`${shared.trimmedBaseUrl()}/network`, { waitUntil: "domcontentloaded" }).catch(() => {});
await expect(page.locator("a[href*='/logout']")).not.toBeAttached({ timeout: 10_000 });
await expect(page.locator("a[href*='/logout']")).not.toBeAttached({ timeout: resolveTimeout(10_000) });
});
};
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
// biber + administrator side-by-side: two isolated browser contexts log in
// independently, biber logs out first, admin session must persist. Exercises
@@ -24,7 +25,7 @@ exports.register = function (shared) {
await adminPage.goto(`${shared.trimmedBaseUrl()}/network`, { waitUntil: "domcontentloaded" });
await expect(
adminPage.locator("#topbar-first, #navbar-apps-menu, a[href*='/logout']").first(),
).toBeVisible({ timeout: 10_000 });
).toBeVisible({ timeout: resolveTimeout(10_000) });
await shared.friendicaLogout(adminPage);
} finally {
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls } = require("./personas");
test.use({ ignoreHTTPSErrors: true });
@@ -53,12 +54,12 @@ test("administrator: app → universal logout", async ({ page }) => {
const link = interactivePage
.getByRole("link", { name: /^(library|favorites|playlists|admin)$/i })
.first();
if (await link.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await link.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await link.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/library|track|album|artist|playlist|funkwhale/i,
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
);
}
},
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { skipUnlessServiceEnabled } = require("./service-gating");
const { decodeDotenvQuotedValue, normalizeBaseUrl, performKeycloakLoginForm, runAdminFlow, runBiberFlow, runGuestFlow } = require("./personas");
@@ -29,10 +30,10 @@ test("OIDC: oauth2-proxy redirects unauthenticated visitors through Keycloak (va
const expectedAuth = `${oidcIssuerUrl}/protocol/openid-connect/auth`;
const expectedBase = baseUrl.replace(/\/$/, "");
await page.goto(`${expectedBase}/`);
await expect.poll(() => page.url(), { timeout: 60_000 }).toContain(expectedAuth);
await expect.poll(() => page.url(), { timeout: resolveTimeout(60_000) }).toContain(expectedAuth);
await performKeycloakLoginForm(page, adminUsername, adminPassword);
await expect.poll(() => page.url(), { timeout: 90_000 }).toContain(expectedBase);
await expect(page.getByRole("button", { name: /sign in/i })).toBeVisible({ timeout: 60_000 });
await expect.poll(() => page.url(), { timeout: resolveTimeout(90_000) }).toContain(expectedBase);
await expect(page.getByRole("button", { name: /sign in/i })).toBeVisible({ timeout: resolveTimeout(60_000) });
});
test("LDAP: FusionDirectory backend points at svc-db-openldap (variant 1)", async ({ page }) => {
@@ -61,12 +62,12 @@ test("administrator: app → universal logout", async ({ page }) => {
const link = interactivePage
.getByRole("link", { name: /^(configuration|administration|users|groups|departments)$/i })
.first();
if (await link.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await link.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await link.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/configuration|administration|users|groups|departments|posix/i,
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
);
}
},
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
exports.register = function (shared) {
test("administrator: app → universal logout", async ({ page }) => {
@@ -8,12 +9,12 @@ exports.register = function (shared) {
const link = interactivePage
.getByRole("link", { name: /^(site administration|admin|user accounts|repositories)$/i })
.first();
if (await link.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await link.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await link.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/site administration|repositories|users|integrations|actions/i,
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
);
}
},
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls } = require("./personas");
test.use({ ignoreHTTPSErrors: true });
@@ -53,12 +54,12 @@ test("administrator: app → universal logout", async ({ page }) => {
const link = interactivePage
.getByRole("link", { name: /^(projects|groups|admin|settings)$/i })
.first();
if (await link.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await link.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await link.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/project|group|merge request|issue|admin area|gitlab/i,
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
);
}
},
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls } = require("./personas");
test.use({ ignoreHTTPSErrors: true });
@@ -71,12 +72,12 @@ test("administrator: app → universal logout", async ({ page }) => {
const link = interactivePage
.getByRole("link", { name: /^(admin|posts|content|tags)$/i })
.first();
if (await link.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await link.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await link.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/admin|posts|content|tags|baseurl/i,
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
);
}
},
@@ -1,4 +1,5 @@
const { expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, performKeycloakLoginForm } = require("./personas");
const { isServiceEnabled, skipUnlessServiceEnabled } = require("./service-gating");
@@ -28,12 +29,12 @@ async function assertAuthenticated(page, label) {
.or(page.getByRole("button", { name: "Search" }))
.first(),
`${label}: authenticated Jellyfin chrome must be visible`,
).toBeVisible({ timeout: 60_000 });
).toBeVisible({ timeout: resolveTimeout(60_000) });
}
async function fillManualLogin(page, username, password) {
const userField = page.locator("#txtManualName, input[name='username'], input[type='text']").first();
await userField.waitFor({ state: "visible", timeout: 30_000 });
await userField.waitFor({ state: "visible", timeout: resolveTimeout(30_000) });
await userField.fill(username);
await page.locator("#txtManualPassword, input[type='password']").first().fill(password);
await page
@@ -65,12 +66,12 @@ async function signInViaOidc(page, username, password, label) {
await expect(
oidcButton,
`${label}: the SSO provider button must render on the Jellyfin login page`,
).toBeVisible({ timeout: 30_000 });
).toBeVisible({ timeout: resolveTimeout(30_000) });
await oidcButton.click();
await expect
.poll(() => page.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `${label}: expected redirect to Keycloak (${oidcIssuerUrl}/protocol/openid-connect/auth)`,
})
.toContain(`${oidcIssuerUrl}/protocol/openid-connect/auth`);
@@ -86,15 +87,15 @@ async function logout(page, label = "session") {
.or(page.locator(".headerUserButton, .mainDrawerButton"))
.first();
if (await menu.count()) {
await menu.click({ timeout: 5_000 }).catch(() => {});
await menu.click({ timeout: resolveTimeout(5_000) }).catch(() => {});
}
const signOut = page
.getByRole("link", { name: /sign\s*out|log\s*out|logout|abmelden/i })
.or(page.locator("a, button, .listItem").filter({ hasText: /sign\s*out|log\s*out|logout|abmelden/i }))
.first();
if (await signOut.count()) {
await signOut.click({ timeout: 5_000 }).catch(() => {});
await page.waitForLoadState("domcontentloaded", { timeout: 8_000 }).catch(() => {});
await signOut.click({ timeout: resolveTimeout(5_000) }).catch(() => {});
await page.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(8_000) }).catch(() => {});
}
} catch {
/* best-effort */
@@ -115,7 +116,7 @@ async function logout(page, label = "session") {
await gotoLogin(page);
await expect
.poll(() => onLoginSurface(page), {
timeout: 30_000,
timeout: resolveTimeout(30_000),
message: `${label}: expected logged-out landing on the Jellyfin login surface`,
})
.toBe(true);
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("../timeouts");
const { skipUnlessAddonEnabled } = require("../addon-gating");
const shared = require("../_shared");
@@ -15,11 +16,11 @@ test("addon ldap-authentication: biber signs in via the LDAP plugin and lands on
shared.env.ssoEnabled === true,
"SSO is active: biber is OIDC-bound; the LDAP login path runs in the ldap-only variant",
);
test.setTimeout(60_000);
test.setTimeout(resolveTimeout(60_000));
expect(shared.env.biberUsername, "BIBER_USERNAME must be set").toBeTruthy();
expect(shared.env.biberPassword, "BIBER_PASSWORD must be set").toBeTruthy();
await shared.signInViaLdap(page, shared.env.biberUsername, shared.env.biberPassword, "biber-ldap");
await expect(page.locator("body")).toContainText(/home|library|media|jellyfin/i, { timeout: 60_000 });
await expect(page.locator("body")).toContainText(/home|library|media|jellyfin/i, { timeout: resolveTimeout(60_000) });
await shared.logout(page, "biber-ldap");
});
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("../timeouts");
const { skipUnlessAddonEnabled } = require("../addon-gating");
const shared = require("../_shared");
@@ -8,12 +9,12 @@ test.beforeEach(shared.beforeEach);
test("addon sso-authentication: biber signs in via the SSO/OIDC plugin and lands on the Jellyfin home", async ({ page }) => {
skipUnlessAddonEnabled("sso-authentication");
test.setTimeout(90_000);
test.setTimeout(resolveTimeout(90_000));
expect(shared.env.biberUsername, "BIBER_USERNAME must be set").toBeTruthy();
expect(shared.env.biberPassword, "BIBER_PASSWORD must be set").toBeTruthy();
expect(shared.env.oidcIssuerUrl, "OIDC_ISSUER_URL must be set").toBeTruthy();
await shared.signInViaOidc(page, shared.env.biberUsername, shared.env.biberPassword, "biber-oidc");
await expect(page.locator("body")).toContainText(/home|library|media|jellyfin/i, { timeout: 60_000 });
await expect(page.locator("body")).toContainText(/home|library|media|jellyfin/i, { timeout: resolveTimeout(60_000) });
await shared.logout(page, "biber-oidc");
});
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { assertCspResponseHeader, assertCspMetaParity } = require("./personas");
@@ -19,7 +20,7 @@ exports.register = function (shared) {
// the login surface, not render the authenticated header controls.
await expect
.poll(() => shared.onLoginSurface(page), {
timeout: 30_000,
timeout: resolveTimeout(30_000),
message: "guest must remain on the Jellyfin login surface",
})
.toBe(true);
@@ -1,15 +1,16 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
exports.register = function (shared) {
// Break-glass local admin seeded via the first-run wizard; ungated (works in every variant).
test("administrator: native local login (break-glass) lands on the Jellyfin home", async ({ page }) => {
test.setTimeout(60_000);
test.setTimeout(resolveTimeout(60_000));
expect(shared.env.adminUsername, "ADMIN_USERNAME must be set").toBeTruthy();
expect(shared.env.adminNativePassword, "ADMIN_NATIVE_PASSWORD must be set").toBeTruthy();
await shared.signInViaLocal(page, shared.env.adminUsername, shared.env.adminNativePassword, "administrator-native");
await expect(page.locator("body")).toContainText(/home|library|media|jellyfin|dashboard/i, { timeout: 60_000 });
await expect(page.locator("body")).toContainText(/home|library|media|jellyfin|dashboard/i, { timeout: resolveTimeout(60_000) });
await shared.logout(page, "administrator-native");
});
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { skipUnlessServiceEnabled } = require("./service-gating");
const { decodeDotenvQuotedValue, normalizeBaseUrl, performKeycloakLoginForm, runAdminFlow, runBiberFlow, runGuestFlow } = require("./personas");
@@ -28,10 +29,10 @@ test("OIDC: oic-auth plugin redirects unauthenticated visitors through Keycloak
const expectedAuth = `${oidcIssuerUrl}/protocol/openid-connect/auth`;
const expectedBase = baseUrl.replace(/\/$/, "");
await page.goto(`${expectedBase}/`);
await expect.poll(() => page.url(), { timeout: 60_000 }).toContain(expectedAuth);
await expect.poll(() => page.url(), { timeout: resolveTimeout(60_000) }).toContain(expectedAuth);
await performKeycloakLoginForm(page, adminUsername, adminPassword);
await expect.poll(() => page.url(), { timeout: 90_000 }).toContain(expectedBase);
await expect(page.locator("body")).toBeVisible({ timeout: 60_000 });
await expect.poll(() => page.url(), { timeout: resolveTimeout(90_000) }).toContain(expectedBase);
await expect(page.locator("body")).toBeVisible({ timeout: resolveTimeout(60_000) });
});
test("LDAP: Jenkins LDAP plugin authenticates against svc-db-openldap (variant 1)", async ({ page }) => {
@@ -43,11 +44,11 @@ test("LDAP: Jenkins LDAP plugin authenticates against svc-db-openldap (variant 1
await page.goto(`${expectedBase}/login`);
const u = page.locator("input[name='j_username']").first();
const p = page.locator("input[name='j_password']").first();
await expect(u).toBeVisible({ timeout: 60_000 });
await expect(u).toBeVisible({ timeout: resolveTimeout(60_000) });
await u.fill(adminUsername);
await p.fill(adminPassword);
await page.locator("button[name='Submit'], input[type='submit']").first().click();
await expect(page.locator("body")).toBeVisible({ timeout: 60_000 });
await expect(page.locator("body")).toBeVisible({ timeout: resolveTimeout(60_000) });
});
// Persona scenarios.
@@ -69,12 +70,12 @@ test("administrator: app → universal logout", async ({ page }) => {
const link = interactivePage
.getByRole("link", { name: /^(manage jenkins|configure system|nodes|plugins|users)$/i })
.first();
if (await link.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await link.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await link.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/manage jenkins|configure system|nodes|plugins|users|credentials/i,
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
);
}
},
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls } = require("./personas");
test.use({ ignoreHTTPSErrors: true });
@@ -53,12 +54,12 @@ test("administrator: app → universal logout", async ({ page }) => {
const link = interactivePage
.getByRole("link", { name: /^(issues|projects|boards|admin)$/i })
.first();
if (await link.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await link.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await link.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/issue|project|board|backlog|jira/i,
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
);
}
},
@@ -1,4 +1,5 @@
const { expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const {
decodeDotenvQuotedValue,
@@ -25,11 +26,11 @@ async function reachJitsiPrejoin(page, personaLabel, roomSuffix) {
.or(page.locator('[data-testid="prejoin.joinMeeting"], #premeeting-screen'))
.first();
await expect(prejoin, `${personaLabel}: prejoin surface must render`).toBeVisible({
timeout: 60_000,
timeout: resolveTimeout(60_000),
});
await expect
.poll(() => page.url(), {
timeout: 30_000,
timeout: resolveTimeout(30_000),
message: `${personaLabel}: URL must include the room path`,
})
.toContain(`/${roomName}`);
@@ -48,7 +49,7 @@ async function openJitsiSettingsPanel(page, personaLabel) {
await expect(
settingsTrigger,
`${personaLabel}: a Settings / More-options control must be visible in the DOM`,
).toBeVisible({ timeout: 30_000 });
).toBeVisible({ timeout: resolveTimeout(30_000) });
}
async function beforeEach({ page }) {
@@ -1,8 +1,9 @@
const { test } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
exports.register = function (shared) {
test("administrator: app → keycloak → settings panel → logout", async ({ page }) => {
test.setTimeout(180_000);
test.setTimeout(resolveTimeout(180_000));
await shared.runAdminFlow(page, {
adminInteraction: async (p) => {
await shared.reachJitsiPrejoin(p, "admin", "admin-room");
@@ -1,8 +1,9 @@
const { test } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
exports.register = function (shared) {
test("biber: app → keycloak → join Jitsi room → logout", async ({ page }) => {
test.setTimeout(180_000);
test.setTimeout(resolveTimeout(180_000));
await shared.runBiberFlow(page, {
biberInteraction: async (p) => {
await shared.reachJitsiPrejoin(p, "biber", "biber-room");
@@ -1,4 +1,5 @@
const { test } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
// Dedicated LDAP scenario per the playwright contract: when both OIDC and LDAP
// are enabled, each persona's primary login path uses OIDC, and each persona
@@ -8,7 +9,7 @@ const { test } = require("@playwright/test");
exports.register = function (shared) {
test("biber: ldap-bound login through keycloak", async ({ page }) => {
shared.skipUnlessServiceEnabled("ldap");
test.setTimeout(180_000);
test.setTimeout(resolveTimeout(180_000));
await shared.runBiberFlow(page, {
biberInteraction: async (p) => {
await shared.reachJitsiPrejoin(p, "biber", "biber-ldap-room");
@@ -18,7 +19,7 @@ exports.register = function (shared) {
test("administrator: ldap-bound login through keycloak", async ({ page }) => {
shared.skipUnlessServiceEnabled("ldap");
test.setTimeout(180_000);
test.setTimeout(resolveTimeout(180_000));
await shared.runAdminFlow(page, {
adminInteraction: async (p) => {
await shared.reachJitsiPrejoin(p, "admin", "admin-ldap-room");
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("../timeouts");
const { skipUnlessAddonEnabled } = require("../addon-gating");
const { skipUnlessServiceEnabled } = require("../service-gating");
const {
@@ -47,7 +48,7 @@ test("addon plg_system_keycloak: OIDC handshake couples Joomla to the configured
await expect
.poll(() => page.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `expected the plg_system_keycloak guard to redirect to the configured Keycloak OIDC auth endpoint (${expectedOidcAuthUrl})`,
})
.toContain(expectedOidcAuthUrl);
@@ -70,7 +71,7 @@ test("addon plg_system_keycloak: OIDC handshake couples Joomla to the configured
await expect
.poll(() => page.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `expected the OIDC callback to land back on Joomla at ${expectedJoomlaBaseUrl}`,
})
.toContain(expectedJoomlaBaseUrl);
@@ -81,7 +82,7 @@ test("addon plg_system_keycloak: OIDC handshake couples Joomla to the configured
// here means the callback failed to provision/log in the user — i.e. the
// integration is not actually coupled.
await page.goto(`${expectedJoomlaBaseUrl}/`);
await page.waitForLoadState("domcontentloaded", { timeout: 60_000 }).catch(() => {});
await page.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(60_000) }).catch(() => {});
expect(
page.url(),
@@ -95,5 +96,5 @@ test("addon plg_system_keycloak: OIDC handshake couples Joomla to the configured
await expect(
page.locator("body"),
"Expected the authenticated Joomla front-end to render after the plg_system_keycloak OIDC handshake",
).toBeVisible({ timeout: 60_000 });
).toBeVisible({ timeout: resolveTimeout(60_000) });
});
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { runAdminFlow } = require("./personas");
test.use({ ignoreHTTPSErrors: true });
@@ -10,12 +11,12 @@ test("administrator: app → universal logout", async ({ page }) => {
const link = interactivePage
.getByRole("link", { name: /^(administrator|users|content|menus|extensions|configuration)$/i })
.first();
if (await link.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await link.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await link.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/users|content|menus|extensions|configuration|articles/i,
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
);
}
},
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { skipUnlessServiceEnabled } = require("./service-gating");
const { decodeDotenvQuotedValue, normalizeBaseUrl } = require("./personas");
@@ -31,7 +32,7 @@ test("LDAP: Joomla core LDAP plugin authenticates the administrator at /administ
const usernameField = page.locator("input[name='username']");
const passwordField = page.locator("input[name='passwd']");
await usernameField.waitFor({ state: "visible", timeout: 60_000 });
await usernameField.waitFor({ state: "visible", timeout: resolveTimeout(60_000) });
await usernameField.fill(adminUsername);
await passwordField.fill(adminPassword);
await Promise.all([
@@ -53,9 +54,9 @@ test("LDAP: Joomla core LDAP plugin authenticates the administrator at /administ
.filter({ hasText: /Username and password do not match|Login failed|invalid|incorrect/i })
.first();
await Promise.race([
controlPanelMarker.waitFor({ state: "visible", timeout: 120_000 }),
controlPanelMarker.waitFor({ state: "visible", timeout: resolveTimeout(120_000) }),
loginErrorAlert
.waitFor({ state: "visible", timeout: 120_000 })
.waitFor({ state: "visible", timeout: resolveTimeout(120_000) })
.then(async () => {
const errorText = (await loginErrorAlert.textContent().catch(() => "")) || "(unknown)";
throw new Error(`Joomla rejected the admin login: ${errorText.trim()}`);
@@ -67,5 +68,5 @@ test("LDAP: Joomla core LDAP plugin authenticates the administrator at /administ
// After logout, /administrator must render the login form again rather than
// the control panel.
await page.goto(`${expectedJoomlaBaseUrl}/administrator`, { waitUntil: "domcontentloaded" }).catch(() => {});
await expect(page.locator("input[name='username']")).toBeVisible({ timeout: 15_000 });
await expect(page.locator("input[name='username']")).toBeVisible({ timeout: resolveTimeout(15_000) });
});
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { skipUnlessServiceEnabled } = require("./service-gating");
const { decodeDotenvQuotedValue, normalizeBaseUrl } = require("./personas");
@@ -18,7 +19,7 @@ async function performJoomlaAdminFormLogin(page, baseUrl, username, password) {
const usernameField = page.locator("input[name='username']");
const passwordField = page.locator("input[name='passwd']");
await usernameField.waitFor({ state: "visible", timeout: 60_000 });
await usernameField.waitFor({ state: "visible", timeout: resolveTimeout(60_000) });
await usernameField.fill(username);
await passwordField.fill(password);
@@ -48,5 +49,5 @@ test("OIDC: /administrator?fallback=local hatch bypasses Keycloak and accepts th
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: 60_000 });
await controlPanelMarker.waitFor({ state: "visible", timeout: resolveTimeout(60_000) });
});
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { skipUnlessServiceEnabled } = require("./service-gating");
const { decodeDotenvQuotedValue, normalizeBaseUrl, performKeycloakLoginForm } = require("./personas");
@@ -34,7 +35,7 @@ test("OIDC: native plg_system_keycloak redirects unauthenticated visitors to Key
await expect
.poll(() => page.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `expected redirect to Keycloak OIDC auth (${expectedOidcAuthUrl})`
})
.toContain(expectedOidcAuthUrl);
@@ -43,7 +44,7 @@ test("OIDC: native plg_system_keycloak redirects unauthenticated visitors to Key
await expect
.poll(() => page.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `expected redirect back to Joomla at ${expectedJoomlaBaseUrl}`
})
.toContain(expectedJoomlaBaseUrl);
@@ -51,5 +52,5 @@ test("OIDC: native plg_system_keycloak redirects unauthenticated visitors to Key
// Joomla front-end renders after the OIDC handshake. RBAC mapping
// gave the administrator persona Super Users (id 8), so the
// administrator landing nav link is visible to logged-in users.
await expect(page.locator("body")).toBeVisible({ timeout: 60_000 });
await expect(page.locator("body")).toBeVisible({ timeout: resolveTimeout(60_000) });
});
@@ -6,6 +6,7 @@
// stays in the respective `test-*.js` file.
const { expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, performKeycloakLoginForm, runBiberFlow, runGuestFlow } = require("./personas");
const { isServiceEnabled, skipUnlessServiceEnabled } = require("./service-gating");
@@ -23,7 +24,7 @@ async function performKixLogin(page, username, password) {
const passwordInput = page.locator('input[type="password"]').first();
const submitButton = page.locator('button[type="submit"], input[type="submit"], button:has-text("Login")').first();
await usernameInput.waitFor({ state: "visible", timeout: 30_000 });
await usernameInput.waitFor({ state: "visible", timeout: resolveTimeout(30_000) });
await usernameInput.fill(username);
await passwordInput.fill(password);
await submitButton.click();
@@ -36,7 +37,7 @@ async function runKixLoginLogoutFlow(page, username, password) {
await expect
.poll(() => page.url(), {
timeout: 30_000,
timeout: resolveTimeout(30_000),
message: `Expected redirect to Keycloak OIDC auth: ${expectedAuthUrl}`,
})
.toContain(expectedAuthUrl);
@@ -44,7 +45,7 @@ async function runKixLoginLogoutFlow(page, username, password) {
await expect
.poll(() => page.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `Expected redirect back to canonical KIX URL on ${canonicalDomain}`,
})
.toContain(canonicalDomain);
@@ -54,7 +55,7 @@ async function runKixLoginLogoutFlow(page, username, password) {
await expect
.poll(() => page.url(), {
timeout: 30_000,
timeout: resolveTimeout(30_000),
message: `Expected SPA to leave /auth after successful LDAP login as ${username}`,
})
.not.toContain("/auth");
@@ -65,7 +66,7 @@ async function runKixLoginLogoutFlow(page, username, password) {
await page.goto(`${appBaseUrl}/`, { waitUntil: "domcontentloaded" });
await expect
.poll(() => page.url(), {
timeout: 30_000,
timeout: resolveTimeout(30_000),
message: `Expected post-logout request to be re-gated to ${expectedAuthUrl}`,
})
.toContain(expectedAuthUrl);
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls } = require("./personas");
test.use({ ignoreHTTPSErrors: true });
@@ -53,12 +54,12 @@ test("administrator: app → universal logout", async ({ page }) => {
const link = interactivePage
.getByRole("link", { name: /^(users|groups|tools|admin)$/i })
.first();
if (await link.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await link.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await link.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/user|group|tools|configuration|ldap|account/i,
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
);
}
},
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls } = require("./personas");
test.use({ ignoreHTTPSErrors: true });
@@ -53,12 +54,12 @@ test("administrator: app → universal logout", async ({ page }) => {
const link = interactivePage
.getByRole("link", { name: /^(campaigns|subscribers|lists|admin)$/i })
.first();
if (await link.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await link.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await link.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/campaign|subscriber|list|template|listmonk/i,
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
);
}
},
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls } = require("./personas");
test.use({ ignoreHTTPSErrors: true });
@@ -53,12 +54,12 @@ test("administrator: app → universal logout", async ({ page }) => {
const link = interactivePage
.getByRole("link", { name: /^(play|game|demo)$/i })
.first();
if (await link.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await link.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await link.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/littlejs|game|engine|canvas|play/i,
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
);
}
},
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls } = require("./personas");
test.use({ ignoreHTTPSErrors: true });
@@ -53,12 +54,12 @@ test("administrator: app → universal logout", async ({ page }) => {
const link = interactivePage
.getByRole("link", { name: /^(catalog|customers|sales|admin)$/i })
.first();
if (await link.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await link.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await link.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/catalog|customer|sales|order|magento/i,
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
);
}
},
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls } = require("./personas");
test.use({ ignoreHTTPSErrors: true });
@@ -53,12 +54,12 @@ test("administrator: app → universal logout", async ({ page }) => {
const link = interactivePage
.getByRole("link", { name: /^(notifications|profile|admin|federation)$/i })
.first();
if (await link.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await link.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await link.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/notification|profile|federation|mastodon|administration/i,
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
);
}
},
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { isServiceEnabled } = require("./service-gating");
const { assertCspMetaParity, assertCspResponseHeader, assertInjectedAssetLoadsWithoutCspBlock, decodeDotenvQuotedValue, expectNoCspViolations, installCspViolationObserver, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow } = require("./personas");
@@ -86,19 +87,19 @@ test("matomo local administrator logs in and logs out", async ({ page }) => {
.locator("input#login_form_submit, button#login_form_submit, button[type='submit'], input[type='submit']")
.first();
await expect(usernameField, "Expected Matomo login form username field").toBeVisible({ timeout: 60_000 });
await expect(usernameField, "Expected Matomo login form username field").toBeVisible({ timeout: resolveTimeout(60_000) });
await usernameField.fill(adminUsername);
await passwordField.fill(adminPassword);
await submitButton.click();
await expect
.poll(() => page.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: "Expected Matomo login to leave the Login module"
})
.not.toContain("module=Login");
await expect(page.locator("body")).toContainText(/dashboard|websites|matomo/i, { timeout: 60_000 });
await expect(page.locator("body")).toContainText(/dashboard|websites|matomo/i, { timeout: resolveTimeout(60_000) });
await page.goto(`${appBaseUrl}/index.php?module=Login&action=logout`);
@@ -111,7 +112,7 @@ test("matomo local administrator logs in and logs out", async ({ page }) => {
.count()
.catch(() => 0)) > 0,
{
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: "Expected Matomo login form to reappear after logout"
}
)
@@ -149,7 +150,7 @@ test("matomo: biber is denied access at the admin surface", async ({ browser })
const callbackResponsePromise = biberPage
.waitForResponse(
(res) => res.url().includes("/oauth2/callback"),
{ timeout: 60_000 },
{ timeout: resolveTimeout(60_000) },
)
.catch(() => null);
@@ -157,7 +158,7 @@ test("matomo: biber is denied access at the admin surface", async ({ browser })
await expect
.poll(() => biberPage.url(), {
timeout: 30_000,
timeout: resolveTimeout(30_000),
message: `Expected redirect to Keycloak OIDC auth: ${expectedOidcAuthUrl}`,
})
.toContain(expectedOidcAuthUrl);
@@ -175,7 +176,7 @@ test("matomo: biber is denied access at the admin surface", async ({ browser })
.or(biberPage.locator("input#kc-login, button#kc-login, button[type='submit'], input[type='submit']"))
.first();
await usernameField.waitFor({ state: "visible", timeout: 60_000 });
await usernameField.waitFor({ state: "visible", timeout: resolveTimeout(60_000) });
await usernameField.fill(biberUsername);
await usernameField.press("Tab").catch(() => {});
await passwordField.fill(biberPassword);
@@ -197,7 +198,7 @@ test("matomo: biber is denied access at the admin surface", async ({ browser })
// Fallback: no callback observed — verify the URL did not settle
// on the authenticated Matomo surface, AND the body does not
// expose the admin DOM markers (body validation).
await biberPage.waitForLoadState("domcontentloaded", { timeout: 60_000 }).catch(() => {});
await biberPage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(60_000) }).catch(() => {});
const finalUrl = biberPage.url();
const onAuthDenialChain =
/openid-connect\/auth/.test(finalUrl) ||
@@ -296,7 +297,7 @@ test("matomo SitesManager registers a tracker site for every consumer role", asy
.click();
await expect
.poll(() => page.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: "Expected Matomo login to leave the Login module before SitesManager probe",
})
.not.toContain("module=Login");
@@ -414,12 +415,12 @@ test("administrator: app → universal logout", async ({ page }) => {
const settingsLink = interactivePage
.getByRole("link", { name: /administration|settings|websites/i })
.first();
if (await settingsLink.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await settingsLink.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await settingsLink.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/websites|administration|users|general settings/i,
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
);
}
},
@@ -1,4 +1,5 @@
const { expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { isServiceEnabled, skipUnlessServiceEnabled } = require("./service-gating");
const {
assertCspResponseHeader,
@@ -74,14 +75,14 @@ async function signInViaElementOidc(page, username, password, personaLabel) {
.locator("button, a, div[role='button']")
.filter({ hasText: /single\s*sign[- ]*on|continue\s+with\s+(sso|oidc|keycloak|openid)|sign\s+in\s+with\s+(sso|oidc|keycloak|openid)/i })
.first();
const candidate = (await ssoButton.isVisible({ timeout: 15_000 }).catch(() => false))
const candidate = (await ssoButton.isVisible({ timeout: resolveTimeout(15_000) }).catch(() => false))
? ssoButton
: ssoTextButton;
await expect(candidate, `${personaLabel}: Element SSO entry button on #/login must be visible`).toBeVisible({ timeout: 30_000 });
await expect(candidate, `${personaLabel}: Element SSO entry button on #/login must be visible`).toBeVisible({ timeout: resolveTimeout(30_000) });
await candidate.click();
await page.waitForURL((u) => u.toString().includes(expectedOidcAuthUrl), {
timeout: 120_000
timeout: resolveTimeout(120_000)
});
await performKeycloakLoginForm(page, username, password);
@@ -92,7 +93,7 @@ async function signInViaElementOidc(page, username, password, personaLabel) {
// token to Element. First-time logins also display a username-selection form
// asking the user to pick their Matrix localpart before this confirmation.
const usernameSelectField = page.locator("input[name='username'], input#field-username").first();
if (await usernameSelectField.isVisible({ timeout: 5_000 }).catch(() => false)) {
if (await usernameSelectField.isVisible({ timeout: resolveTimeout(5_000) }).catch(() => false)) {
await usernameSelectField.fill(username);
const submit = page.locator("button[type='submit'], input[type='submit']").first();
await submit.click();
@@ -102,7 +103,7 @@ async function signInViaElementOidc(page, username, password, personaLabel) {
.locator("a, button")
.filter({ hasText: /^\s*continue\s*$/i })
.first();
await expect(continueLink, `${personaLabel}: Synapse SSO confirmation "Continue" link must appear`).toBeVisible({ timeout: 60_000 });
await expect(continueLink, `${personaLabel}: Synapse SSO confirmation "Continue" link must appear`).toBeVisible({ timeout: resolveTimeout(60_000) });
await continueLink.click();
// Element consumes `?loginToken=…` during SPA bootstrap. The token is
@@ -119,7 +120,7 @@ async function signInViaElementOidc(page, username, password, personaLabel) {
if (/#\/welcome/.test(url) || /#\/home/.test(url) || /#\/room/.test(url)) return true;
if (u.pathname === "/" && (!u.hash || u.hash === "#" || u.hash === "#/")) return true;
return false;
}, { timeout: 120_000 });
}, { timeout: resolveTimeout(120_000) });
// With `feature_rust_crypto: true`, Element renders a full-screen "Confirm
// your digital identity" / "Skip verification for now" interstitial on each
@@ -215,9 +216,9 @@ async function signInViaElementOidc(page, username, password, personaLabel) {
// requires a few seconds per slot. Wait 45s to leave margin — clicking
// "Try again" sooner just re-triggers M_LIMIT_EXCEEDED and burns the
// next burst slot, extending the outage.
await page.waitForTimeout(45_000);
await page.waitForTimeout(resolveTimeout(45_000));
const tryAgain = rateLimitDialog.getByRole("button", { name: /^try again$/i }).first();
await tryAgain.click({ timeout: 5_000 }).catch(() => {});
await tryAgain.click({ timeout: resolveTimeout(5_000) }).catch(() => {});
return true;
}
@@ -251,7 +252,7 @@ async function signInViaElementOidc(page, username, password, personaLabel) {
.or(page.locator("a, button, input[type='submit']").filter({ hasText: /^\s*continue\s*$/i }))
.first();
const clickResult = await continueAction
.click({ timeout: 10_000 })
.click({ timeout: resolveTimeout(10_000) })
.then(() => "ok")
.catch((e) => e.message || "click-failed");
// Only throttle subsequent attempts when the click actually landed.
@@ -274,18 +275,18 @@ async function signInViaElementOidc(page, username, password, personaLabel) {
if (await passSynapseConsentPage()) continue;
if (await retryOnRateLimitDialog()) continue;
if (await skipVerificationButton.isVisible().catch(() => false)) {
await skipVerificationButton.click({ timeout: 5_000 }).catch(() => {});
await skipVerificationButton.click({ timeout: resolveTimeout(5_000) }).catch(() => {});
// Element may show a secondary confirm dialog for the skip choice.
// MUST NOT click "Reset identity" — that destroys the device identity
// and logs the user back out to #/login.
const confirmSkip = page
.getByRole("button", { name: /^\s*(skip(\s+anyway)?|i'?ll\s+verify\s+later|continue)\s*$/i })
.first();
await confirmSkip.waitFor({ state: "visible", timeout: 5_000 }).catch(() => {});
await confirmSkip.waitFor({ state: "visible", timeout: resolveTimeout(5_000) }).catch(() => {});
if (await confirmSkip.isVisible().catch(() => false)) {
await confirmSkip.click({ timeout: 5_000 }).catch(() => {});
await confirmSkip.click({ timeout: resolveTimeout(5_000) }).catch(() => {});
}
await page.waitForTimeout(2_000);
await page.waitForTimeout(resolveTimeout(2_000));
continue;
}
// If Element has bounced back to its own #/login page AND no auth signal
@@ -305,18 +306,18 @@ async function signInViaElementOidc(page, username, password, personaLabel) {
// Short click timeout: if the click is blocked (e.g. by a stale
// backdrop), we want to fall back to the next poll iteration quickly
// rather than burning the entire test budget on click-retry.
await ssoRetry.click({ timeout: 5_000 }).catch(() => {});
await page.waitForURL((u) => !/#\/login(\/|$|\?)/.test(u.toString()), { timeout: 30_000 }).catch(() => {});
await ssoRetry.click({ timeout: resolveTimeout(5_000) }).catch(() => {});
await page.waitForURL((u) => !/#\/login(\/|$|\?)/.test(u.toString()), { timeout: resolveTimeout(30_000) }).catch(() => {});
}
}
await page.waitForTimeout(2_000);
await page.waitForTimeout(resolveTimeout(2_000));
}
// Final gate: poll the DOM a few more times. If any of our signals show
// up, we're authenticated. Otherwise, fail with a descriptive message.
await expect
.poll(authenticatedSignalPresent, {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `${personaLabel}: authenticated Element UI (user menu / room list / welcome) must render`
})
.toBe(true);
@@ -333,7 +334,7 @@ async function signInViaElementPassword(page, username, password, personaLabel)
const userField = page
.locator('input[name="username"], #mx_LoginForm_username, input[name="mxid"], input[data-testid="login_field_mx_id"]')
.first();
await expect(userField, `${personaLabel}: Element password login username field must appear`).toBeVisible({ timeout: 30_000 });
await expect(userField, `${personaLabel}: Element password login username field must appear`).toBeVisible({ timeout: resolveTimeout(30_000) });
await userField.fill(username);
const pwField = page
@@ -386,23 +387,23 @@ async function signInViaElementPassword(page, username, password, personaLabel)
throw new Error(`${personaLabel}: Element rejected credentials: "${rejection}"`);
}
if (await skipBtn.isVisible().catch(() => false)) {
await skipBtn.click({ timeout: 5_000 }).catch(() => {});
await skipBtn.click({ timeout: resolveTimeout(5_000) }).catch(() => {});
const confirmSkip = page
.getByRole("button", { name: /^\s*(skip(\s+anyway)?|i'?ll\s+verify\s+later|continue)\s*$/i })
.first();
await confirmSkip.waitFor({ state: "visible", timeout: 5_000 }).catch(() => {});
await confirmSkip.waitFor({ state: "visible", timeout: resolveTimeout(5_000) }).catch(() => {});
if (await confirmSkip.isVisible().catch(() => false)) {
await confirmSkip.click({ timeout: 5_000 }).catch(() => {});
await confirmSkip.click({ timeout: resolveTimeout(5_000) }).catch(() => {});
}
await page.waitForTimeout(2_000);
await page.waitForTimeout(resolveTimeout(2_000));
continue;
}
await page.waitForTimeout(2_000);
await page.waitForTimeout(resolveTimeout(2_000));
}
await expect
.poll(authenticatedSignalPresent, {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `${personaLabel}: authenticated Element UI (user menu / room list / welcome) must render after password auth`
})
.toBe(true);
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("../timeouts");
const { skipUnlessAddonEnabled } = require("../addon-gating");
const shared = require("../_shared");
@@ -6,7 +7,7 @@ test.use({ ignoreHTTPSErrors: true });
test("mautrix-meta addon: bridge appservice registers @metabot on the partner Synapse homeserver", async ({ request }) => {
skipUnlessAddonEnabled("mautrix-meta");
test.setTimeout(120_000);
test.setTimeout(resolveTimeout(120_000));
const { matrixBaseUrl, matrixServerName } = shared.env;
expect(matrixBaseUrl, "MATRIX_BASE_URL must be set to reach the partner homeserver").toBeTruthy();
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("../timeouts");
const { skipUnlessAddonEnabled } = require("../addon-gating");
const shared = require("../_shared");
@@ -6,7 +7,7 @@ test.use({ ignoreHTTPSErrors: true });
test("mautrix-signal addon: bridge appservice bot is provisioned and reachable on the Synapse homeserver", async ({ request }) => {
skipUnlessAddonEnabled("mautrix-signal");
test.setTimeout(60_000);
test.setTimeout(resolveTimeout(60_000));
const { matrixBaseUrl, matrixServerName } = shared.env;
expect(matrixBaseUrl, "MATRIX_BASE_URL must be set").toBeTruthy();
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("../timeouts");
const { skipUnlessAddonEnabled } = require("../addon-gating");
const shared = require("../_shared");
@@ -6,7 +7,7 @@ test.use({ ignoreHTTPSErrors: true });
test("mautrix-slack addon: Slack bridge appservice bot is provisioned and reachable on Synapse", async ({ request }) => {
skipUnlessAddonEnabled("mautrix-slack");
test.setTimeout(60_000);
test.setTimeout(resolveTimeout(60_000));
const matrixBaseUrl = shared.env.matrixBaseUrl;
const matrixServerName = shared.env.matrixServerName;
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("../timeouts");
const { skipUnlessAddonEnabled } = require("../addon-gating");
const shared = require("../_shared");
@@ -8,7 +9,7 @@ const TELEGRAM_BOT_LOCALPART = "telegrambot";
test("mautrix-telegram addon: appservice bot is provisioned on the Synapse homeserver", async ({ request }) => {
skipUnlessAddonEnabled("mautrix-telegram");
test.setTimeout(60_000);
test.setTimeout(resolveTimeout(60_000));
const matrixBaseUrl = shared.env.matrixBaseUrl;
const matrixServerName = shared.env.matrixServerName;
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("../timeouts");
const { skipUnlessAddonEnabled } = require("../addon-gating");
const shared = require("../_shared");
@@ -6,7 +7,7 @@ test.use({ ignoreHTTPSErrors: true });
test("mautrix-whatsapp addon: bridge appservice registers @whatsappbot on the partner Synapse homeserver", async ({ request }) => {
skipUnlessAddonEnabled("mautrix-whatsapp");
test.setTimeout(120_000);
test.setTimeout(resolveTimeout(120_000));
const { matrixBaseUrl, matrixServerName } = shared.env;
expect(matrixBaseUrl, "MATRIX_BASE_URL must be set to reach the partner homeserver").toBeTruthy();
@@ -1,4 +1,5 @@
const { test } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const shared = require("./_shared");
@@ -14,7 +15,7 @@ test.use({
// tests in the same spec have drained Synapse's rc_login burst, each
// sign-in can spend 5+ minutes cycling through consent↔M_LIMIT_EXCEEDED
// ping-pong before authenticating. 1200s (20 min) covers the worst case.
test.setTimeout(1_200_000);
test.setTimeout(resolveTimeout(1_200_000));
test.beforeEach(shared.beforeEach);
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
exports.register = function (shared) {
test.describe("matrix DM", () => {
@@ -41,11 +42,11 @@ exports.register = function (shared) {
// test's state machine isn't forced to spend most of its deadline
// cycling through consent↔M_LIMIT_EXCEEDED retries. Running the DM
// test in isolation doesn't need this, but the extra wait is cheap.
await adminPage.waitForTimeout(120_000);
await adminPage.waitForTimeout(resolveTimeout(120_000));
await shared.signInViaElement(adminPage, adminUsername, adminPassword, "administrator");
// Same reasoning between admin and biber: two back-to-back SSO
// logins easily exhaust rc_login. 30s lets the burst refill.
await adminPage.waitForTimeout(30_000);
await adminPage.waitForTimeout(resolveTimeout(30_000));
await shared.signInViaElement(biberPage, biberUsername, biberPassword, "biber");
const marker = `hello-from-admin-${Date.now()}`;
@@ -64,12 +65,12 @@ exports.register = function (shared) {
await adminPage.goto(`${elementBaseUrl}/#/user/${encodeURIComponent(biberMatrixId)}`);
const profilePanel = adminPage.getByRole("complementary").filter({ hasText: biberMatrixId });
await expect(profilePanel, "admin: biber profile panel must render").toBeVisible({ timeout: 60_000 });
await expect(profilePanel, "admin: biber profile panel must render").toBeVisible({ timeout: resolveTimeout(60_000) });
const profileSendMessageButton = profilePanel
.getByRole("button", { name: /^send message$/i })
.first();
await expect(profileSendMessageButton, "admin: profile 'Send message' button must be visible").toBeVisible({ timeout: 30_000 });
await expect(profileSendMessageButton, "admin: profile 'Send message' button must be visible").toBeVisible({ timeout: resolveTimeout(30_000) });
await profileSendMessageButton.click();
// After clicking "Send message", Element navigates into the DM room and
@@ -77,12 +78,12 @@ exports.register = function (shared) {
// name before looking for the composer, so we don't match a stale
// textbox from the previous view (e.g. a search field).
const roomHeader = adminPage.getByRole("heading", { name: /harry beaver|biber/i }).first();
await expect(roomHeader, "admin: DM room header with biber must render").toBeVisible({ timeout: 30_000 });
await expect(roomHeader, "admin: DM room header with biber must render").toBeVisible({ timeout: resolveTimeout(30_000) });
const composer = adminPage
.locator("div[role='textbox'][contenteditable='true'], textarea[aria-label*='message' i], div[aria-label*='message' i][contenteditable='true']")
.last();
await expect(composer, "admin: message composer must appear").toBeVisible({ timeout: 60_000 });
await expect(composer, "admin: message composer must appear").toBeVisible({ timeout: resolveTimeout(60_000) });
await composer.click();
// Element keeps the DM in a pending "Send your first message to invite …"
@@ -140,7 +141,7 @@ exports.register = function (shared) {
);
}).catch(() => false);
}, {
timeout: 120_000,
timeout: resolveTimeout(120_000),
message: "biber: expected invite acceptance to produce a message composer",
})
.toBe(true);
@@ -148,7 +149,7 @@ exports.register = function (shared) {
// Give admin's client a moment to observe biber's join and establish a
// megolm session before sending the marker. Element batches outbound
// session creation on membership events; 5s is generous.
await adminPage.waitForTimeout(5_000);
await adminPage.waitForTimeout(resolveTimeout(5_000));
await composer.click();
await adminPage.keyboard.type(marker);
await adminPage.keyboard.press("Enter");
@@ -159,7 +160,7 @@ exports.register = function (shared) {
.poll(async () => {
return (await biberPage.locator("body").innerText().catch(() => "")).includes(marker);
}, {
timeout: 120_000,
timeout: resolveTimeout(120_000),
message: `biber: expected to receive message "${marker}" from administrator`,
})
.toBe(true);
@@ -17,6 +17,7 @@
// runSeaweedfsStorageCheck.
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { skipUnlessServiceEnabled } = require("./service-gating");
const { runSeaweedfsStorageCheck } = require("./personas");
const shared = require("./_shared");
@@ -34,7 +35,7 @@ test("seaweedfs: an uploaded Matrix avatar is stored in the SeaweedFS bucket", a
!(process.env.MATRIX_FLAVOR || "").toLowerCase().includes("ansible"),
"Matrix media is offloaded to SeaweedFS only in the ansible flavor; the compose flavor stores media on local disk, so the bucket never grows.",
);
test.setTimeout(600_000);
test.setTimeout(resolveTimeout(600_000));
await runSeaweedfsStorageCheck(page, browser, {
label: "a Matrix profile avatar upload",
@@ -56,7 +57,7 @@ test("seaweedfs: an uploaded Matrix avatar is stored in the SeaweedFS bucket", a
await expect(
fileInput,
"Element user settings must expose a file input to set a profile avatar",
).toBeAttached({ timeout: 60_000 });
).toBeAttached({ timeout: resolveTimeout(60_000) });
const marker = `infinito-storage-check-${Date.now()}.png`;
await fileInput.setInputFiles({
@@ -68,7 +69,7 @@ test("seaweedfs: an uploaded Matrix avatar is stored in the SeaweedFS bucket", a
const saveButton = appPage
.getByRole("button", { name: /^(save|apply|upload|confirm)$/i })
.first();
if (await saveButton.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await saveButton.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await saveButton.click().catch(() => {});
}
},
@@ -1,4 +1,5 @@
const { expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue } = require("./personas");
const { isServiceEnabled } = require("./service-gating");
@@ -22,7 +23,7 @@ function expectedMattermostBaseUrl() {
return env.mattermostBaseUrl.replace(/\/$/, "");
}
async function waitForFirstVisible(locators, timeout = 60_000) {
async function waitForFirstVisible(locators, timeout = resolveTimeout(60_000)) {
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
for (const locator of locators) {
@@ -45,11 +46,11 @@ async function startMattermostSsoFlow(page, baseUrl) {
let lastErr;
for (let attempt = 0; attempt < 2; attempt++) {
try {
await page.goto(`${base}/login`, { waitUntil: "load", timeout: 60_000 });
await page.goto(`${base}/login`, { waitUntil: "load", timeout: resolveTimeout(60_000) });
if (page.url().includes("/landing")) {
await page.goto(`${base}/login`, { waitUntil: "load", timeout: 60_000 });
await page.goto(`${base}/login`, { waitUntil: "load", timeout: resolveTimeout(60_000) });
}
await page.locator("#input_loginId").waitFor({ state: "visible", timeout: 60_000 });
await page.locator("#input_loginId").waitFor({ state: "visible", timeout: resolveTimeout(60_000) });
lastErr = undefined;
break;
} catch (e) {
@@ -58,7 +59,7 @@ async function startMattermostSsoFlow(page, baseUrl) {
}
if (lastErr) throw lastErr;
const ssoButton = page.locator("a[href='/oauth/gitlab/login']");
await ssoButton.waitFor({ state: "visible", timeout: 30_000 });
await ssoButton.waitFor({ state: "visible", timeout: resolveTimeout(30_000) });
await ssoButton.click();
}
@@ -74,7 +75,7 @@ async function dismissMattermostPopups(frame) {
for (let round = 0; round < 3; round++) {
for (const sel of dismissSelectors) {
if (await sel.first().isVisible({ timeout: 2000 }).catch(() => false)) {
if (await sel.first().isVisible({ timeout: resolveTimeout(2000) }).catch(() => false)) {
await sel.first().click({ force: true }).catch(() => {});
await new Promise(r => setTimeout(r, 500));
}
@@ -88,7 +89,7 @@ async function dismissMattermostPopups(frame) {
}
}
async function waitForMattermostChannelView(frame, timeout = 60_000) {
async function waitForMattermostChannelView(frame, timeout = resolveTimeout(60_000)) {
const channelSidebar = frame.locator(
".SidebarChannel, [data-testid='channel_sidebar'], #sidebar-left, .SidebarNavContainer"
);
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { runAdminFlow } = require("./personas");
@@ -9,12 +10,12 @@ exports.register = function () {
const link = interactivePage
.getByRole("link", { name: /^(system console|admin|workspaces|teams|channels)$/i })
.first();
if (await link.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await link.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await link.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/system console|workspace|teams|channels|users|reporting/i,
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
);
}
},
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { performKeycloakLoginForm } = require("./personas");
@@ -19,7 +20,7 @@ exports.register = function (shared) {
await shared.startMattermostSsoFlow(biberPage, baseUrl);
await expect
.poll(() => biberPage.url(), {
timeout: 30_000,
timeout: resolveTimeout(30_000),
message: `Expected redirect to Keycloak OIDC: ${oidcAuthUrl}`,
})
.toContain(oidcAuthUrl);
@@ -28,7 +29,7 @@ exports.register = function (shared) {
await expect
.poll(() => biberPage.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: "Expected redirect back to Mattermost after biber login",
})
.toContain(baseUrl);
@@ -44,7 +45,7 @@ exports.register = function (shared) {
const messageInput = biberPage
.locator("#post_textbox, [data-testid='post_textbox'], div[contenteditable='true'].post-create__input")
.first();
await messageInput.waitFor({ state: "visible", timeout: 30_000 });
await messageInput.waitFor({ state: "visible", timeout: resolveTimeout(30_000) });
await messageInput.click({ force: true });
// keyboard.type() bypasses fill()'s contenteditable-skipping React onChange gap.
await biberPage.keyboard.type(testMessage);
@@ -52,7 +53,7 @@ exports.register = function (shared) {
await expect(
biberPage.getByTestId("postContent").getByText(testMessage)
).toBeVisible({ timeout: 15_000 });
).toBeVisible({ timeout: resolveTimeout(15_000) });
await shared.mattermostLogout(biberPage, baseUrl);
@@ -61,7 +62,7 @@ exports.register = function (shared) {
await shared.startMattermostSsoFlow(adminPage, baseUrl);
await expect
.poll(() => adminPage.url(), {
timeout: 30_000,
timeout: resolveTimeout(30_000),
message: `Expected redirect to Keycloak OIDC: ${oidcAuthUrl}`,
})
.toContain(oidcAuthUrl);
@@ -70,19 +71,19 @@ exports.register = function (shared) {
await expect
.poll(() => adminPage.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: "Expected redirect back to Mattermost after admin login",
})
.toContain(baseUrl);
await shared.dismissMattermostPopups(adminPage);
await shared.waitForMattermostChannelView(adminPage, 30_000);
await shared.waitForMattermostChannelView(adminPage, resolveTimeout(30_000));
await adminPage.goto(`${baseUrl}/main/messages/@${shared.env.biberUsername}`);
await expect(
adminPage.getByTestId("postContent").getByText(testMessage)
).toBeVisible({ timeout: 30_000 });
).toBeVisible({ timeout: resolveTimeout(30_000) });
await shared.mattermostLogout(adminPage, baseUrl);
} finally {
@@ -14,6 +14,7 @@
// draft preview and the posted attachment overlay.
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { skipUnlessServiceEnabled } = require("./service-gating");
const { runSeaweedfsStorageCheck } = require("./personas");
const { performKeycloakLoginForm } = require("./personas");
@@ -23,7 +24,7 @@ test.use({ ignoreHTTPSErrors: true });
test("seaweedfs: a Mattermost message attachment is stored in the SeaweedFS bucket", async ({ page, browser }) => {
skipUnlessServiceEnabled("seaweedfs");
test.setTimeout(180_000);
test.setTimeout(resolveTimeout(180_000));
await runSeaweedfsStorageCheck(page, browser, {
label: "a Mattermost message attachment upload",
@@ -33,7 +34,7 @@ test("seaweedfs: a Mattermost message attachment is stored in the SeaweedFS buck
await shared.startMattermostSsoFlow(appPage, baseUrl);
await expect
.poll(() => appPage.url(), {
timeout: 30_000,
timeout: resolveTimeout(30_000),
message: `Expected redirect to Keycloak OIDC: ${shared.expectedOidcAuthUrl()}`,
})
.toContain(shared.expectedOidcAuthUrl());
@@ -42,7 +43,7 @@ test("seaweedfs: a Mattermost message attachment is stored in the SeaweedFS buck
await expect
.poll(() => appPage.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: "Expected redirect back to Mattermost after admin login",
})
.toContain(baseUrl);
@@ -51,19 +52,19 @@ test("seaweedfs: a Mattermost message attachment is stored in the SeaweedFS buck
await appPage.goto(`${baseUrl}/main/channels/town-square`, { waitUntil: "domcontentloaded" });
await shared.dismissMattermostPopups(appPage);
await shared.waitForMattermostChannelView(appPage, 60_000);
await shared.waitForMattermostChannelView(appPage, resolveTimeout(60_000));
const messageInput = appPage
.locator("#post_textbox, [data-testid='post_textbox'], div[contenteditable='true'].post-create__input")
.first();
await messageInput.waitFor({ state: "visible", timeout: 30_000 });
await messageInput.waitFor({ state: "visible", timeout: resolveTimeout(30_000) });
const marker = `infinito-storage-check-${Date.now()}.txt`;
const markerPrefix = marker.slice(0, 30);
const fileInput = appPage
.locator("input[type='file'][data-testid='fileUploadInput'], .file-input, input[type='file']")
.first();
await fileInput.waitFor({ state: "attached", timeout: 30_000 });
await fileInput.waitFor({ state: "attached", timeout: resolveTimeout(30_000) });
await fileInput.setInputFiles({
name: marker,
mimeType: "text/plain",
@@ -77,7 +78,7 @@ test("seaweedfs: a Mattermost message attachment is stored in the SeaweedFS buck
await expect(
attachmentPreview,
`the attachment '${marker}' must appear in the post preview before sending`,
).toBeVisible({ timeout: 30_000 });
).toBeVisible({ timeout: resolveTimeout(30_000) });
await messageInput.click({ force: true });
await appPage.keyboard.type(`storage check ${marker}`);
@@ -86,7 +87,7 @@ test("seaweedfs: a Mattermost message attachment is stored in the SeaweedFS buck
await expect(
appPage.getByTestId("postContent").getByText(markerPrefix, { exact: false }).first(),
`the sent attachment '${marker}' must appear in the channel timeline`,
).toBeVisible({ timeout: 30_000 });
).toBeVisible({ timeout: resolveTimeout(30_000) });
},
});
});
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { performKeycloakLoginForm } = require("./personas");
@@ -13,7 +14,7 @@ exports.register = function (shared) {
await expect
.poll(() => page.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `Expected redirect to Keycloak OIDC: ${oidcAuthUrl}`,
})
.toContain(oidcAuthUrl);
@@ -22,20 +23,20 @@ exports.register = function (shared) {
await expect
.poll(() => page.url(), {
timeout: 60_000,
timeout: resolveTimeout(60_000),
message: `Expected redirect back to Mattermost: ${baseUrl}`,
})
.toContain(baseUrl);
await shared.dismissMattermostPopups(page);
await shared.waitForMattermostChannelView(page, 30_000);
await shared.waitForMattermostChannelView(page, resolveTimeout(30_000));
await shared.mattermostLogout(page, baseUrl);
// Mattermost v11+ defaults to /landing#/ rather than /login for unauthenticated requests.
await page.goto(`${baseUrl}/`, { waitUntil: "domcontentloaded" });
await expect
.poll(() => page.url(), {
timeout: 15_000,
timeout: resolveTimeout(15_000),
message: "Expected Mattermost to redirect to /login or /landing after logout",
})
.toMatch(/\/(login|landing)/);
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("../timeouts");
const { skipUnlessAddonEnabled } = require("../addon-gating");
const { skipUnlessServiceEnabled } = require("../service-gating");
@@ -48,9 +49,9 @@ test("OpenIDConnect: wiki login hands off to the Keycloak OIDC authorize endpoin
page
.goto(`${appBaseUrl}/index.php?title=Special:UserLogin&returnto=Main+Page`, {
waitUntil: "domcontentloaded",
timeout: 60_000,
timeout: resolveTimeout(60_000),
})
.then(() => page.waitForLoadState("networkidle", { timeout: 30_000 }).catch(() => {}))
.then(() => page.waitForLoadState("networkidle", { timeout: resolveTimeout(30_000) }).catch(() => {}))
.then(() => {
if (/\/protocol\/openid-connect\/auth/.test(page.url())) {
done(page.url());
@@ -91,7 +92,7 @@ test("OpenIDConnect: wiki login hands off to the Keycloak OIDC authorize endpoin
.or(interactivePage.locator("#mw-panel"))
.or(interactivePage.locator("body.mediawiki"))
.first();
await expect(wikiSurface).toBeVisible({ timeout: 60_000 });
await expect(wikiSurface).toBeVisible({ timeout: resolveTimeout(60_000) });
const authenticatedMarker = interactivePage
.locator("#pt-logout")
@@ -104,7 +105,7 @@ test("OpenIDConnect: wiki login hands off to the Keycloak OIDC authorize endpoin
}),
)
.first();
await expect(authenticatedMarker).toBeVisible({ timeout: 60_000 });
await expect(authenticatedMarker).toBeVisible({ timeout: resolveTimeout(60_000) });
},
});
});
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("../timeouts");
const { skipUnlessAddonEnabled } = require("../addon-gating");
const { skipUnlessServiceEnabled } = require("../service-gating");
@@ -28,11 +29,11 @@ test("PluggableAuth: framework replaces local login and is bound to the OIDC iss
await page.context().clearCookies();
const loginUrl = `${appBaseUrl}/index.php?title=Special:UserLogin`;
await page.goto(loginUrl, { waitUntil: "domcontentloaded", timeout: 60_000 });
await page.goto(loginUrl, { waitUntil: "domcontentloaded", timeout: resolveTimeout(60_000) });
// Let an EnableAutoLogin handoff to the IdP settle.
await page
.waitForLoadState("networkidle", { timeout: 60_000 })
.waitForLoadState("networkidle", { timeout: resolveTimeout(60_000) })
.catch(() => {});
const finalUrl = page.url();
@@ -66,7 +67,7 @@ test("PluggableAuth: framework replaces local login and is bound to the OIDC iss
await expect(page.locator("body")).toContainText(
/sign in|log ?in|username|password|keycloak|openid/i,
{ timeout: 60_000 },
{ timeout: resolveTimeout(60_000) },
);
return;
}
@@ -78,7 +79,7 @@ test("PluggableAuth: framework replaces local login and is bound to the OIDC iss
await expect(body).toContainText(
/single sign[- ]?on|openid|\bsso\b|log ?in with|continue with|keycloak/i,
{ timeout: 60_000 },
{ timeout: resolveTimeout(60_000) },
);
// Stock MediaWiki local login has a password input named "wpPassword". With
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls } = require("./personas");
test.use({ ignoreHTTPSErrors: true });
@@ -53,12 +54,12 @@ test("administrator: app → universal logout", async ({ page }) => {
const link = interactivePage
.getByRole("link", { name: /^(edit|view source|talk|history|admin)$/i })
.first();
if (await link.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await link.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await link.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/wiki|edit|page|main page|history|mediawiki/i,
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
);
}
},
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls } = require("./personas");
test.use({ ignoreHTTPSErrors: true });
@@ -58,12 +59,12 @@ test("administrator: app → universal logout", async ({ page }) => {
const link = interactivePage
.getByRole("link", { name: /^(admin|content|configuration|menu)$/i })
.first();
if (await link.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await link.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await link.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/admin|content|configuration|menu|article|page/i,
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
);
}
},
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls } = require("./personas");
test.use({ ignoreHTTPSErrors: true });
@@ -53,12 +54,12 @@ test("administrator: app → universal logout", async ({ page }) => {
const link = interactivePage
.getByRole("link", { name: /^(generate|qr|admin)$/i })
.first();
if (await link.isVisible({ timeout: 10_000 }).catch(() => false)) {
if (await link.isVisible({ timeout: resolveTimeout(10_000) }).catch(() => false)) {
await link.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/qr|code|generator/i,
{ timeout: 30_000 },
{ timeout: resolveTimeout(30_000) },
);
}
},
@@ -1,4 +1,5 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { skipUnlessServiceEnabled } = require("./service-gating");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runBiberFlow, runGuestFlow } = require("./personas");
@@ -27,7 +28,7 @@ async function minioConsoleFormLogin(page, baseUrl, username, password) {
.locator("button[type='submit'], input[type='submit']")
.first();
await expect(usernameField, "expected MinIO Console login form").toBeVisible({ timeout: 60_000 });
await expect(usernameField, "expected MinIO Console login form").toBeVisible({ timeout: resolveTimeout(60_000) });
await usernameField.fill(username);
await passwordField.fill(password);
await submitButton.click();
@@ -70,7 +71,7 @@ test("minio console serves canonical domain over HTTPS", async ({ page }) => {
`Expected canonical domain "${canonicalDomain}" (from applications lookup) to back the MinIO Console URL`
).toBe(true);
await expect(page.locator("body")).toBeVisible({ timeout: 60_000 });
await expect(page.locator("body")).toBeVisible({ timeout: resolveTimeout(60_000) });
});
test("administrator: OIDC integrated login path via STS AssumeRoleWithWebIdentity", async ({ request }) => {
@@ -130,7 +131,7 @@ test("administrator: MinIO Console form login under LDAP variant", async ({ page
await minioConsoleFormLogin(page, consoleBaseUrl, adminUsername, adminPassword);
await expect(page.locator("body")).toContainText(/object browser|buckets|access keys|monitoring/i, { timeout: 60_000 });
await expect(page.locator("body")).toContainText(/object browser|buckets|access keys|monitoring/i, { timeout: resolveTimeout(60_000) });
await minioConsoleLogout(page, consoleBaseUrl);
@@ -139,7 +140,7 @@ test("administrator: MinIO Console form login under LDAP variant", async ({ page
page
.locator("input[name='accessKey'], input[name='username'], input#accessKey, input#username")
.first()
).toBeVisible({ timeout: 60_000 });
).toBeVisible({ timeout: resolveTimeout(60_000) });
});
// Persona scenarios.
Loaded 100 of 262 files, more files were not shown because too many files have changed in this diff. Show more