ccd7c2f2fd
CI / Test (rest) (pull_request) Successful in 8s
CI / Test (store) (pull_request) Successful in 8s
CI / Test (server-http) (pull_request) Successful in 12s
CI / Build (windows/amd64) (pull_request) Successful in 9s
CI / Lint (pull_request) Successful in 20s
CI / Build (linux/arm64) (pull_request) Successful in 9s
CI / Build (linux/amd64) (pull_request) Successful in 17s
e2e / Playwright vs docker-compose (pull_request) Failing after 4m7s
Two issues uncovered by the page-snapshot dump after the agent state-dir fix: * The host page server-renders `Run backup now` as disabled while repo_status != ready, and the page has no live-refresh on that field. The test was navigating right after status flipped to 'online' but before auto-init had completed (~3s later), so the rendered HTML still showed init_running and the click was a no-op. Wait for repo_status === 'ready' before navigating. * playwright.config.ts pinned the per-test timeout at 60s, but the test itself uses 60s + 120s of internal waits. Bump to 240s so the test fails on real regressions instead of timing out on its own internal budget. Renamed the test description away from "under a minute" since it overpromises against the new timeout. The performance SLO belongs in a separate test if we want to assert it.
116 lines
4.2 KiB
TypeScript
116 lines
4.2 KiB
TypeScript
// Helpers used by every test. The shape favours the JSON API for
|
|
// reads + accept/dispatch (deterministic, easy to assert) and the
|
|
// browser for human-facing surfaces (login form, dashboard render).
|
|
|
|
import { APIRequestContext, expect, Page } from '@playwright/test';
|
|
|
|
export const baseURL = process.env.RM_BASE_URL ?? 'http://127.0.0.1:8080';
|
|
|
|
export interface HostJSON {
|
|
id: string;
|
|
name: string;
|
|
status: string;
|
|
repo_status?: string;
|
|
last_backup_status?: string;
|
|
}
|
|
|
|
export async function readBootstrapToken(): Promise<string> {
|
|
const tok = process.env.RM_BOOTSTRAP_TOKEN;
|
|
if (!tok) {
|
|
throw new Error('RM_BOOTSTRAP_TOKEN not set — the harness scrapes it from server logs');
|
|
}
|
|
return tok;
|
|
}
|
|
|
|
export async function bootstrapAdmin(
|
|
request: APIRequestContext,
|
|
{
|
|
username = 'admin',
|
|
password = 'e2e-test-password-1234',
|
|
}: { username?: string; password?: string } = {},
|
|
): Promise<{ username: string; password: string }> {
|
|
const token = await readBootstrapToken();
|
|
const res = await request.post(`${baseURL}/api/bootstrap`, {
|
|
data: { token, username, password },
|
|
});
|
|
if (!res.ok() && res.status() !== 409 /* already bootstrapped */) {
|
|
throw new Error(`bootstrap: ${res.status()} ${await res.text()}`);
|
|
}
|
|
return { username, password };
|
|
}
|
|
|
|
export async function loginViaUI(page: Page, username: string, password: string): Promise<void> {
|
|
await page.goto(`${baseURL}/login`);
|
|
await page.locator('#login-username').fill(username);
|
|
await page.locator('#login-password').fill(password);
|
|
await Promise.all([
|
|
page.waitForURL(new RegExp(`^${baseURL}/?$`)),
|
|
page.locator('form[action="/login"] button[type="submit"]').click(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Polls the dashboard until a pending host card is visible, then
|
|
* extracts its pending-id from the inline accept form's action URL.
|
|
*/
|
|
export async function waitForPendingHostID(page: Page): Promise<string> {
|
|
const formLocator = page.locator('form[action^="/api/pending-hosts/"][action$="/accept"]').first();
|
|
await expect(formLocator).toBeVisible({ timeout: 60_000 });
|
|
const action = await formLocator.getAttribute('action');
|
|
if (!action) throw new Error('pending host form has no action attribute');
|
|
const m = action.match(/\/api\/pending-hosts\/([^/]+)\/accept/);
|
|
if (!m) throw new Error(`unexpected action URL: ${action}`);
|
|
return m[1];
|
|
}
|
|
|
|
export async function acceptPending(
|
|
request: APIRequestContext,
|
|
cookie: string,
|
|
pendingID: string,
|
|
repo: { url: string; username?: string; password: string },
|
|
): Promise<void> {
|
|
const res = await request.post(`${baseURL}/api/pending-hosts/${pendingID}/accept`, {
|
|
headers: { cookie, 'content-type': 'application/json' },
|
|
data: {
|
|
repo_url: repo.url,
|
|
repo_username: repo.username ?? '',
|
|
repo_password: repo.password,
|
|
},
|
|
});
|
|
if (!res.ok()) {
|
|
throw new Error(`accept: ${res.status()} ${await res.text()}`);
|
|
}
|
|
}
|
|
|
|
export async function listHosts(request: APIRequestContext, cookie: string): Promise<HostJSON[]> {
|
|
const res = await request.get(`${baseURL}/api/hosts`, { headers: { cookie } });
|
|
if (!res.ok()) throw new Error(`list hosts: ${res.status()} ${await res.text()}`);
|
|
const body = (await res.json()) as { items?: HostJSON[]; hosts?: HostJSON[] };
|
|
return body.items ?? body.hosts ?? [];
|
|
}
|
|
|
|
export async function waitForHostStatus(
|
|
request: APIRequestContext,
|
|
cookie: string,
|
|
matcher: (h: HostJSON) => boolean,
|
|
timeoutMs = 60_000,
|
|
): Promise<HostJSON> {
|
|
const deadline = Date.now() + timeoutMs;
|
|
let last: HostJSON | undefined;
|
|
while (Date.now() < deadline) {
|
|
const hosts = await listHosts(request, cookie);
|
|
const hit = hosts.find(matcher);
|
|
if (hit) return hit;
|
|
last = hosts[0];
|
|
await new Promise((r) => setTimeout(r, 1_000));
|
|
}
|
|
throw new Error(`waitForHostStatus: timeout. Last seen: ${JSON.stringify(last)}`);
|
|
}
|
|
|
|
export async function getSessionCookie(page: Page): Promise<string> {
|
|
const cookies = await page.context().cookies();
|
|
const c = cookies.find((c) => c.name === 'rm_session');
|
|
if (!c) throw new Error('rm_session cookie not set after login');
|
|
return `${c.name}=${c.value}`;
|
|
}
|