Files
computer-playbook/roles/web-app-nextcloud/files/playwright/addons/integration_google.spec.js
kevinveenbirkenbachandClaude Opus 4.8 07c6ad8e7c test(e2e): scale every nav click + timeout literal for onion targets
Over Tor the redirect chain behind a login/logout/OIDC click routinely exceeds Playwright's fixed 30s action timeout, so the click throws before the flow's scaled waits run. Sweep all role Playwright suites and the shared persona helpers: give every navigation/auth-transition click an onion-scaled timeout (.click({ timeout: resolveTimeout(30_000) })) and route the remaining raw millisecond literals (timeout options, waitForTimeout, sleep-promise setTimeout, positional wait args) through resolveTimeout. Local UI clicks (menus, toggles, modal dismissals, DOM clicks inside evaluate) stay untouched.

Two new lints keep it that way: test_no_raw_timeout_literals forbids raw numeric timeout literals in Playwright specs (opt-out: nocheck: raw-timeout), test_persona_clicks_scaled requires every click in the shared persona auth flows to carry a resolveTimeout timeout (opt-out: nocheck: unscaled-click).

Verified: node --check on all 133 changed JS files, all 13 playwright lints green, make autoformat clean, make test green (2745+196+163+4 tests OK).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 11:35:53 +02:00

82 lines
3.3 KiB
JavaScript

const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("../timeouts");
const { skipUnlessAddonEnabled } = require("../addon-gating");
const shared = require("../_shared");
const { gotoOnion } = require("../personas");
test("integration integration_google: per-user OAuth connect reaches the Google authorize endpoint", async ({ browser }) => {
skipUnlessAddonEnabled("integration_google");
test.setTimeout(resolveTimeout(120_000));
const context = await browser.newContext({ ignoreHTTPSErrors: true });
const page = await context.newPage();
const connectFor = (target) =>
target.getByRole("button", { name: /sign in with google/i });
try {
await shared.loginToStandaloneNextcloud(page);
await gotoOnion(page,
new URL("settings/user/migration", shared.env.nextcloudBaseUrl).toString(),
{ waitUntil: "domcontentloaded", timeout: resolveTimeout(60_000) }
);
await shared.dismissBlockingNextcloudModals(page, page);
let connect = connectFor(page);
if ((await connect.count()) === 0) {
await gotoOnion(page,
new URL("settings/user/connected-accounts", shared.env.nextcloudBaseUrl).toString(),
{ waitUntil: "domcontentloaded", timeout: resolveTimeout(60_000) }
);
await shared.dismissBlockingNextcloudModals(page, page);
connect = connectFor(page);
}
await expect(
connect.first(),
"the 'Sign in with Google' control must render once the integration_google OAuth client_id is provisioned — its absence means the meta/addons client_id/client_secret coupling failed to land"
).toBeVisible({ timeout: resolveTimeout(60_000) });
const popupPromise = context.waitForEvent("page", { timeout: resolveTimeout(15_000) }).catch(() => null);
await Promise.all([
page
.waitForURL((u) => /^accounts\.google\.com$/i.test(new URL(u).host), { timeout: resolveTimeout(60_000) })
.catch(() => {}),
connect.first().click({ timeout: resolveTimeout(30_000) }),
]);
const popup = await popupPromise;
const target = popup || page;
await target.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
const authorize = new URL(target.url());
const nextcloudHost = new URL(shared.env.nextcloudBaseUrl).host;
expect(
authorize.host,
`the per-user connect must hand the browser off to the Google authorize endpoint, not stay on Nextcloud (got ${authorize.href})`
).toBe("accounts.google.com");
expect(
authorize.host,
"the Google OAuth authorize must be served by the partner (accounts.google.com), not Nextcloud"
).not.toBe(nextcloudHost);
expect(
authorize.pathname,
"the connect must drive the Google OAuth2 authorize path"
).toMatch(/^\/o\/oauth2(\/v2)?\/auth/);
expect(
(authorize.searchParams.get("client_id") || "").length,
"the authorize request must carry the provisioned Google OAuth client_id (proves the client_id from meta/addons is live)"
).toBeGreaterThan(0);
expect(
authorize.searchParams.get("response_type"),
"the coupling must use the authorization-code grant"
).toBe("code");
if (popup) await popup.close().catch(() => {});
} finally {
await page.close().catch(() => {});
await context.close().catch(() => {});
}
});