mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
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:
1 parent
dfa7f68982
commit
35dee215c8
262 files changed
+1581
-1078
No files matched your search
@@ -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
Reference in new issue
Block a user