SPB Git

Repository Public

helpers.ts 4.9 KB 95 lines typescript

1import { createHmac } from 'node:crypto';2import { expect, type Page, type ConsoleMessage } from '@playwright/test';34export const SEED = { username: 'admin', password: 'correct-horse-battery', totpSecret: 'JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP' };5export const MOCK = `http://127.0.0.1:${process.env.MOCK_PORT ?? 8621}/api/v1`;67export function totp(secret: string, t = Date.now()): string {8  const B32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';9  let bits = '';10  for (const c of secret) bits += B32.indexOf(c).toString(2).padStart(5, '0');11  const key = Buffer.from(bits.match(/.{8}/g)!.map((b) => parseInt(b, 2)));12  const counter = Buffer.alloc(8);13  counter.writeBigUInt64BE(BigInt(Math.floor(t / 30000)));14  const h = createHmac('sha1', key).update(counter).digest();15  const o = h[h.length - 1]! & 0xf;16  return String((h.readUInt32BE(o) & 0x7fffffff) % 1e6).padStart(6, '0');17}1819/** Collects console errors, page errors and CSP violations; assert with `expectClean()`. */20export function watch(page: Page): { errors: string[]; expectClean: () => void } {21  const errors: string[] = [];22  page.on('console', (m: ConsoleMessage) => {23    const t = m.text();24    if (m.type() === 'warning' && /Could not create web worker/.test(t)) errors.push(t); // Monaco fell back to the main thread25    if (m.type() !== 'error') return;26    // Expected: 401 on /auth/* before sign-in, deliberate 409/413/415/429 exercised by the tests, dropped sockets.27    if (/Failed to load resource: the server responded with a status of (401|404|409|413|415|429|403)/.test(t)) return; // 404s are tracked by URL below28    if (/WebSocket connection to .* failed/.test(t)) return;29    errors.push(t);30  });31  page.on('pageerror', (e) => errors.push(`pageerror: ${e.message}`));32  page.on('response', (r) => {33    if (r.status() === 404 && !r.url().includes('/api/')) errors.push(`404 ${r.url()}`);34  });35  // CSP violations are reported by the browser as console errors; also record the event itself.36  void page.addInitScript(() => {37    document.addEventListener('securitypolicyviolation', (e) => console.error(`CSP violation: ${e.effectiveDirective} ${e.blockedURI}`));38  });39  return { errors, expectClean: () => expect(errors, 'console errors / CSP violations').toEqual([]) };40}4142/** Sign in through the API (the browser context shares the cookie jar). */43export async function apiLogin(page: Page): Promise<void> {44  const r = await page.request.post('/api/v1/auth/login', { data: { username: SEED.username, password: SEED.password, totp: totp(SEED.totpSecret) } });45  expect(r.ok()).toBeTruthy();46}47export async function csrf(page: Page): Promise<string> {48  return (await (await page.request.get('/api/v1/auth/status')).json()).csrfToken as string;49}50export async function apiPost<T = unknown>(page: Page, path: string, data: unknown = {}): Promise<T> {51  const r = await page.request.post(`/api/v1${path}`, { data, headers: { 'X-CSRF-Token': await csrf(page) } });52  expect(r.ok(), `${path} → ${r.status()}`).toBeTruthy();53  return (await r.json()) as T;54}55export async function demoProjectId(page: Page): Promise<string> {56  const list = (await (await page.request.get('/api/v1/projects')).json()) as { id: string; name: string }[];57  return list.find((p) => p.name === 'demo')!.id;58}59export async function resetMock(page: Page, seed = true): Promise<void> {60  await page.request.post(`${MOCK}/__mock/reset`, { data: { seed } });61}6263interface CrTerm {64  ids(): string[];65  text(id: string): string | null;66  status(id: string): string | null;67  send(id: string, data: string): void;68  dropSocket(id: string): void;69  sent(id: string): string[] | null;70  size(id: string): { cols: number; rows: number; fontSize: number } | null;71  hasFocus(id: string): boolean;72  scroll(id: string): { viewportY: number; baseY: number; type: string } | null;73}74declare global {75  interface Window {76    __crTerm: CrTerm;77  }78}79export const termText = (page: Page, id: string) => page.evaluate((i) => window.__crTerm?.text(i) ?? '', id);80export const termStatus = (page: Page, id: string) => page.evaluate((i) => window.__crTerm?.status(i) ?? null, id);81export async function waitTermOpen(page: Page, id: string): Promise<void> {82  await expect.poll(() => termStatus(page, id), { timeout: 15_000 }).toBe('open');83  await expect.poll(() => termText(page, id)).toContain('mock$');84}85export async function expectTerm(page: Page, id: string, text: string | RegExp): Promise<void> {86  await expect.poll(() => termText(page, id), { timeout: 15_000 }).toMatch(typeof text === 'string' ? new RegExp(text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) : text);87}88/** Create a shell session through the API and open it in the terminal page. */89export async function openShell(page: Page, title = 'e2e shell'): Promise<string> {90  const s = await apiPost<{ id: string }>(page, '/sessions', { projectId: await demoProjectId(page), kind: 'shell', title });91  await page.goto(`/terminal/?s=${s.id}`);92  await waitTermOpen(page, s.id);93  return s.id;94}95
Files main
Menu

Theme