Files
computer-playbook/roles/web-app-nextcloud/files/playwright/addons/integration_mattermost.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

95 lines
4.4 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");
// Full-coupling check for nextcloud/integration_mattermost.
//
// The addon hook (tasks/addons/integration_mattermost_provision.yml) registers an
// OAuth 2.0 application on the partner Mattermost and persists oauth_instance_url +
// client_id + client_secret into the app. This spec proves the coupling behaviourally:
//
// 1) the integration_mattermost app is enabled (admin app-detail "Disable"),
// 2) the personal "Connected accounts" page exposes the Mattermost connect control,
// 3) clicking connect performs the OAuth authorize redirect to the PARTNER Mattermost
// host (not Nextcloud) carrying the provisioned client_id and response_type=code —
// which can only happen once the hook persisted client_id + oauth_instance_url. A
// missing coupling would keep the browser on Nextcloud.
//
// The partner bounces the unauthenticated request through /login?redirect_to=<authorize>,
// so the authorize URL can be percent-encoded in the landing URL — decode before matching.
test("integration integration_mattermost: OAuth client provisioned and connects to Mattermost", async ({ browser }) => {
skipUnlessAddonEnabled("integration_mattermost");
test.setTimeout(resolveTimeout(120_000));
const context = await browser.newContext({ ignoreHTTPSErrors: true });
const page = await context.newPage();
const nextcloudHost = new URL(shared.env.nextcloudBaseUrl).host;
try {
await shared.loginToStandaloneNextcloud(page);
// 1) App is installed AND enabled.
await gotoOnion(page,
new URL("settings/apps/installed/integration_mattermost", shared.env.nextcloudBaseUrl).toString(),
{ waitUntil: "domcontentloaded", timeout: resolveTimeout(60_000) }
);
await shared.dismissBlockingNextcloudModals(page, page).catch(() => {});
await page.waitForLoadState("networkidle").catch(() => {});
await expect(
page.getByRole("button", { name: /^disable$/i }).or(page.locator('input[value="Disable"]')).first(),
"integration_mattermost must be enabled (admin app-detail Disable action)"
).toBeVisible({ timeout: resolveTimeout(60_000) });
// 2) Personal connect surface renders (only when the app is enabled).
await gotoOnion(page,
new URL("settings/user/connected-accounts", shared.env.nextcloudBaseUrl).toString(),
{ waitUntil: "domcontentloaded", timeout: resolveTimeout(60_000) }
);
await shared.dismissBlockingNextcloudModals(page, page).catch(() => {});
await page.waitForLoadState("networkidle").catch(() => {});
const connect = page
.locator("#mattermost-connect")
.or(page.getByRole("button", { name: /connect to mattermost/i }))
.or(page.getByRole("link", { name: /connect to mattermost/i }))
.first();
await expect(
connect,
"the Mattermost connect control must render on Connected accounts when the app is enabled"
).toBeVisible({ timeout: resolveTimeout(60_000) });
// 3) Connect performs the partner OAuth authorize redirect with the provisioned client.
const popupPromise = page.waitForEvent("popup", { timeout: resolveTimeout(15_000) }).catch(() => null);
await Promise.all([
page.waitForEvent("framenavigated", { timeout: resolveTimeout(60_000) }).catch(() => {}),
connect.click({ timeout: resolveTimeout(30_000) }),
]);
const popup = await popupPromise;
const rawUrl = () => (popup ? popup.url() : page.url());
const landing = () => decodeURIComponent(rawUrl());
await expect
.poll(landing, {
timeout: resolveTimeout(60_000),
message: "connect must redirect to the partner Mattermost OAuth authorize endpoint",
})
.toMatch(/\/oauth\/authorize/i);
const decoded = landing();
expect(
new URL(rawUrl()).host,
"the OAuth authorize must be served by the partner Mattermost host, not Nextcloud"
).not.toBe(nextcloudHost);
expect(decoded, "the OAuth authorize must carry the provisioned Mattermost client_id").toMatch(/client_id=/i);
expect(decoded, "the OAuth authorize must be an authorization-code-flow request").toMatch(/response_type=code/i);
if (popup) await popup.close().catch(() => {});
} finally {
await page.close().catch(() => {});
await context.close().catch(() => {});
}
});