SPB Git

Repository Public

console.spec.ts 25.2 KB 391 lines typescript

1import { expect, test, type Page } from '@playwright/test';2import { apiGet, apiPost, clientSeqs, FIX, MOCK, mission, ROUTES, rowKeys, seeded, serverSeqs, watch } from './helpers';34test.beforeEach(async ({ page }) => {5  await seeded(page);6});78const timeline = (page: Page) => page.getByTestId('timeline');9const tool = (page: Page, name: string) => timeline(page).locator(`[data-testid="tool-row"][data-tool="${name}"]`);10const billingId = async (page: Page) => (await apiGet<{ id: string; name: string }[]>(page, '/projects')).find((p) => p.name === 'fixture-billing')!.id;1112test('create a mission, watch it live: streamed turn → committed steps → diff → shell block → approval → finish', async ({ page }) => {13  const w = watch(page);14  await page.goto('/home/');15  await page.getByTestId('new-mission').click();16  const form = page.getByTestId('new-mission-form');17  await expect(form).toBeVisible();18  await expect(page.getByTestId('mission-submit')).toBeDisabled(); // no objective yet19  await page.getByTestId('mission-objective').fill('Totals are off by one cent on some invoices. Fix it and prove it with a test.');20  // criteria: explained when empty; add two, remove one21  await expect(page.getByTestId('criteria-hint')).toContainText('proposes criteria first and asks you to confirm');22  await page.getByTestId('criterion-add').click();23  await page.getByTestId('criterion-input').first().fill('npm test passes with a new regression test');24  await page.getByTestId('criterion-add').click();25  await page.getByTestId('criterion-input').nth(1).fill('to be removed');26  await page.getByRole('button', { name: 'Remove criterion 2' }).click();27  await page.getByTestId('criterion-add').click();28  await page.getByTestId('criterion-input').nth(1).fill('The fix is committed');29  await expect(page.getByTestId('criteria-hint')).toHaveCount(0);30  // models: grouped by provider with prices; the unavailable one is disabled and says so31  const picker = page.getByTestId('model-picker');32  await expect(picker.locator('p').first()).toContainText('Anthropic');33  await expect(picker.locator('p').nth(1)).toContainText('OpenAI');34  await expect(page.getByTestId('model-claude-opus-5-5')).toHaveAttribute('aria-checked', 'true'); // default from settings35  await expect(page.getByTestId('model-claude-opus-5-5')).toContainText('$4 / $20');36  await expect(page.getByTestId('model-gpt-6-astra')).toBeDisabled();37  await expect(page.getByTestId('model-gpt-6-astra')).toContainText('Not available on this key');38  await page.getByTestId('model-claude-sonnet-5-5').click();39  await expect(page.getByTestId('mission-budget')).toHaveAttribute('placeholder', '15'); // default mission budget40  await expect(form).toContainText('Daily cap $60.00');41  await page.getByTestId('mission-budget').fill('4');42  await expect(page.getByTestId('network-risk')).toContainText('Recommended');43  await page.getByTestId('mission-network').click();44  await expect(page.getByTestId('network-risk')).toContainText('could send workspace content elsewhere');45  await page.getByTestId('mission-network').click();46  await page.getByTestId('mission-submit').click();4748  await expect(page).toHaveURL(/\/mission\/\?id=/);49  await expect(page.getByTestId('mission-title')).toContainText('Totals are off by one cent');50  await expect(page.getByTestId('mission-model')).toContainText('Claude Sonnet 5.5');51  // streamed text is shown as it arrives…52  await expect(timeline(page).locator('[data-testid="assistant-text"][data-streaming]')).toBeVisible();53  // …then replaced by the committed step54  await expect(tool(page, 'read_file')).toHaveAttribute('data-state', 'ok');55  // the script pauses on the approval: the wait appears inline, the timeline is pinned to it56  const wait = timeline(page).getByTestId('timeline-wait').filter({ has: page.getByTestId('wait-approve') });57  await expect(wait).toBeVisible({ timeout: 30_000 });58  await expect(page.getByTestId('status-badge').first()).toHaveAttribute('data-status', 'waiting');59  await expect(wait.getByTestId('risk-badge')).toHaveAttribute('data-risk', 'external');60  await expect(wait.getByTestId('wait-args')).toContainText('"remote": "origin"');61  // shell: terminal block with exit code, artifact downloadable62  const shell = tool(page, 'shell');63  await expect(shell).toHaveAttribute('data-state', 'ok');64  await shell.getByRole('button').first().click();65  await expect(shell.getByTestId('shell-block')).toContainText('$npm test');66  await expect(shell.getByTestId('shell-output')).toContainText('Tests  13 passed (13)');67  await expect(shell.getByTestId('exit-code')).toHaveText('exit 0');68  const href = await shell.getByTestId('artifact-link').getAttribute('href');69  expect(href).toMatch(/^\/api\/v1\/artifacts\/[0-9a-f]{64}\?name=shell-[0-9a-f]{16}\.txt$/);70  const dl = await page.request.get(href!);71  expect(dl.headers()['content-disposition']).toContain('attachment');72  // back to the start of the run (the list is virtualised: rows far from the viewport are not in the DOM)73  await page.getByTestId('timeline-scroll').evaluate((el) => { el.scrollTop = 0; });74  await expect(page.getByTestId('jump-latest')).toBeVisible();75  await expect(timeline(page).locator('[data-streaming]')).toHaveCount(0);76  await expect(timeline(page).getByTestId('assistant-text').filter({ hasText: 'I will start by reading the total computation' })).toHaveCount(1);77  // reasoning summary is collapsible78  const reasoning = timeline(page).getByTestId('reasoning').first();79  await reasoning.getByRole('button').click();80  await expect(reasoning).toContainText('read it before deciding anything');81  // edit_file is a real diff (small diffs open by default)82  const edit = tool(page, 'edit_file').first();83  await expect(edit.getByTestId('diff-view')).toBeVisible();84  await expect(edit.locator('[data-line="del"]')).toHaveCount(3);85  await expect(edit.locator('[data-line="add"]')).toHaveCount(4);86  await expect(edit.locator('[data-line="add"]').first()).toContainText('sum in integer cents');87  // a call the model issued that the engine refused (protected path) has no journal step: shown as "not executed"88  const refused = timeline(page).getByTestId('refused-call');89  await expect(refused).toHaveCount(1);90  await expect(refused).toHaveAttribute('data-tool', 'write_file');91  await expect(refused).toContainText('.git/hooks/pre-commit');92  await refused.getByRole('button').click();93  await expect(refused.getByTestId('refused-reason')).toContainText('Denied by policy: protected path');94  await refused.getByRole('button').first().click();95  // minimal Markdown: code block escaped, bold rendered, nothing clickable96  const said = timeline(page).getByTestId('assistant-text').filter({ hasText: 'The bug is in' });97  await expect(said.locator('pre')).toContainText('cents += Math.round');98  await expect(said.locator('strong')).toHaveText('binary floats');99  await expect(timeline(page).locator('a:not([data-testid="artifact-link"]), img')).toHaveCount(0);100  await page.getByTestId('jump-latest').click();101  await expect(wait).toBeVisible();102  // steer the agent while it waits103  await page.getByTestId('composer-input').fill('Also mention the fix in the changelog.');104  await page.getByTestId('composer-send').click();105  await expect(timeline(page).locator('[data-testid="owner-message"][data-pending]')).toContainText('Also mention the fix in the changelog.');106  await wait.getByTestId('wait-approve').click();107  // resolution → push → finish108  await expect(tool(page, 'git_push')).toHaveAttribute('data-state', 'ok');109  await expect(timeline(page).getByTestId('timeline-wait').filter({ hasText: 'Approved' })).toBeVisible();110  await expect(page.getByTestId('run-result')).toContainText('integer cents');111  await expect(page.getByTestId('mission-page')).toHaveAttribute('data-status', 'done');112  // the steering message was read at the next turn: journaled once, no longer pending113  await expect(timeline(page).getByTestId('owner-message')).toHaveCount(1);114  await expect(timeline(page).locator('[data-testid="owner-message"][data-pending]')).toHaveCount(0);115  await expect(timeline(page).getByTestId('assistant-text').filter({ hasText: 'Noted — “Also mention the fix in the changelog.”' })).toHaveCount(1);116  // criteria flipped with evidence; spend is real and under budget117  const side = page.getByTestId('mission-side');118  await expect(side.locator('[data-testid="criterion"][data-passes="true"]')).toHaveCount(2);119  await expect(side.getByTestId('criterion-evidence').first()).toContainText('13 passed');120  await expect(page.getByTestId('composer-ended')).toBeVisible();121  const id = new URL(page.url()).searchParams.get('id')!;122  const m = await apiGet<{ spentUsd: number; rootRunId: string }>(page, `/missions/${id}`);123  expect(m.spentUsd).toBeGreaterThan(0);124  // nothing duplicated, nothing missing125  expect(await clientSeqs(page, m.rootRunId)).toEqual(await serverSeqs(page, m.rootRunId));126  const keys = await rowKeys(page);127  expect(new Set(keys).size).toBe(keys.length);128  w.expectClean();129});130131test('without criteria the agent asks first: answer the question, criteria appear', async ({ page }) => {132  const w = watch(page);133  const m = await apiPost<{ id: string }>(page, '/missions', { projectId: await billingId(page), objective: 'Fix the totals.', model: 'claude-haiku-5-5' });134  await page.goto(`/mission/?id=${m.id}`);135  await expect(page.getByTestId('criteria-empty')).toContainText('the agent proposes them first');136  const q = timeline(page).getByTestId('timeline-wait').filter({ has: page.getByTestId('wait-option') });137  await expect(q).toContainText('The agent proposes these done criteria. Do they describe the outcome you want?');138  await expect(q).toContainText('3. The branch is pushed to origin');139  await expect(q.getByTestId('wait-option')).toHaveText(['Yes — use these criteria']);140  // a reply that is not an acceptance sends the agent back to revise: it proposes again141  await q.getByTestId('wait-answer').fill('Drop the push: keep the branch local.');142  await q.getByTestId('wait-answer-send').click();143  await expect(timeline(page).getByTestId('timeline-wait').filter({ hasText: 'Answered' })).toContainText('You answered: Drop the push: keep the branch local.');144  await expect(q).not.toContainText('The branch is pushed to origin', { timeout: 20_000 });145  await expect(page.getByTestId('criteria-empty')).toBeVisible(); // nothing is confirmed yet146  await q.getByTestId('wait-option').click();147  // criteria appear as soon as the engine announces them (system.notice {code:'criteria'})148  await expect(page.getByTestId('mission-side').getByTestId('criterion')).toHaveCount(2);149  await expect(page.getByTestId('mission-page')).toHaveAttribute('data-status', 'done', { timeout: 30_000 });150  await expect(tool(page, 'git_push')).toHaveCount(0); // the answer was honoured151  w.expectClean();152});153154test('events socket drops mid-run: resume from the cursor without duplicated or missing items; gap notice refetches', async ({ page }) => {155  const w = watch(page);156  const m = await apiPost<{ id: string; rootRunId: string }>(page, '/missions', { projectId: await billingId(page), objective: 'Fix the totals.', doneCriteria: ['tests pass', 'committed', 'pushed'] });157  await page.goto(`/mission/?id=${m.id}`);158  await expect(tool(page, 'read_file')).toBeVisible();159  for (let i = 0; i < 3; i++) {160    const before = await page.evaluate(() => window.__aos.cursor());161    await page.evaluate(() => window.__aos.dropEvents());162    await expect(page.getByTestId('conn-indicator')).toHaveAttribute('data-tone', 'success'); // reconnected163    await expect.poll(() => page.evaluate(() => window.__aos.cursor())).toBeGreaterThanOrEqual(before ?? 0);164    await page.waitForTimeout(350);165  }166  const wait = timeline(page).getByTestId('timeline-wait').filter({ has: page.getByTestId('wait-approve') });167  await expect(wait).toBeVisible({ timeout: 30_000 });168  await expect.poll(async () => JSON.stringify(await clientSeqs(page, m.rootRunId))).toBe(JSON.stringify(await serverSeqs(page, m.rootRunId)));169  const seqs = (await clientSeqs(page, m.rootRunId))!;170  expect(seqs).toEqual(Array.from({ length: seqs.length }, (_, i) => i)); // contiguous from 0: nothing missing171  let keys = await rowKeys(page);172  expect(new Set(keys).size).toBe(keys.length); // nothing rendered twice173  await expect(timeline(page)).toHaveAttribute('data-steps', String(seqs.length));174  // the cursor caught up with the server's last persisted event175  const info = (await (await page.request.get(`${MOCK}/__mock/info`)).json()) as { eventSeq: number };176  await expect.poll(() => page.evaluate(() => window.__aos.cursor())).toBe(info.eventSeq);177  // a "gap" notice makes the console refetch the journal from the start178  const refetch = page.waitForRequest((r) => r.url().includes(`/runs/${m.rootRunId}/steps`) && !r.url().includes('after='));179  await page.request.post(`${MOCK}/__mock/gap`);180  await refetch;181  await expect(wait).toBeVisible();182  keys = await rowKeys(page);183  expect(new Set(keys).size).toBe(keys.length);184  w.expectClean();185});186187test('long timeline: scrolled up shows "jump to latest" and keeps the reading position', async ({ page }) => {188  const m = await mission(page, FIX.done);189  await page.setViewportSize({ width: 1440, height: 560 });190  await page.goto(`/mission/?id=${m.id}`);191  await expect(page.getByTestId('run-result')).toBeVisible(); // pinned to the latest item on load192  const scroller = page.getByTestId('timeline-scroll');193  await scroller.evaluate((el) => { el.scrollTop = 0; });194  await expect(page.getByTestId('jump-latest')).toBeVisible();195  await expect(page.getByTestId('turn-header').first()).toBeVisible();196  await page.getByTestId('jump-latest').click();197  await expect(page.getByTestId('run-result')).toBeVisible();198  await expect(page.getByTestId('jump-latest')).toHaveCount(0);199});200201test('cancel a running mission with confirmation', async ({ page }) => {202  const w = watch(page);203  const m = await mission(page, FIX.running);204  await page.goto(`/mission/?id=${m.id}`);205  await expect(tool(page, 'shell')).toHaveAttribute('data-state', 'running');206  await page.getByTestId('mission-cancel').click();207  const dialog = page.getByRole('dialog', { name: 'Cancel this mission?' });208  await expect(dialog).toContainText('Spent so far');209  await dialog.getByRole('button', { name: 'Cancel mission' }).click();210  await expect(page.getByTestId('mission-page')).toHaveAttribute('data-status', 'cancelled');211  await expect(page.getByTestId('mission-cancel')).toHaveCount(0);212  await expect(page.getByTestId('composer-ended')).toContainText('cancelled');213  // the command had no result when the run ended: shown as interrupted, never as still running214  await expect(tool(page, 'shell')).toHaveAttribute('data-state', 'interrupted');215  await expect(timeline(page).getByTestId('thinking')).toHaveCount(0);216  w.expectClean();217});218219test('approvals inbox: exact call, diff, countdown; deny with a note; approve-and-remember explains and creates a rule', async ({ page }) => {220  const w = watch(page);221  await page.goto('/approvals/');222  await expect(page.getByTestId('nav-approvals').getByTestId('pending-badge')).toHaveText('2');223  const approval = page.locator('[data-testid="wait-card"][data-kind="approval"]');224  await expect(approval).toContainText('Push aos/');225  await expect(approval).toContainText(FIX.approval);226  await expect(approval.getByTestId('risk-badge')).toHaveText('External effect');227  await expect(approval.getByTestId('wait-args')).toContainText('"branch": "aos/');228  await expect(approval.getByTestId('wait-diff-toggle')).toContainText('2 files +8 −3');229  await expect(approval.getByTestId('diff-view').locator('[data-line="add"]').first()).toBeVisible(); // open by default where there is room230  await approval.getByTestId('wait-diff-toggle').click();231  await expect(approval.getByTestId('diff-view')).toHaveCount(0);232  await expect(approval.getByTestId('wait-expiry')).toContainText(/expires in 2 d 23 h|expires in 3 d/);233  // deny needs a second, deliberate step and takes an optional note; "Back" returns234  await approval.getByTestId('wait-deny').click();235  await approval.getByTestId('wait-deny-note').fill('not yet');236  await approval.getByRole('button', { name: 'Back' }).click();237  // approve and remember: explained, confirmed, then visible as a rule238  await approval.getByTestId('wait-remember').click();239  const explain = page.getByTestId('remember-explain');240  await expect(explain).toContainText('allow rule will match the tool git_push');241  await expect(explain).toContainText('without asking you again');242  await expect(approval).toBeVisible(); // nothing happened yet243  await page.getByTestId('wait-remember-confirm').click();244  await expect(approval).toHaveCount(0);245  await expect(page.getByTestId('nav-approvals').getByTestId('pending-badge')).toHaveText('1');246  await page.getByTestId('nav-policy').click();247  const rule = page.getByTestId('rule-row').filter({ hasText: 'git_push' });248  await expect(rule).toHaveAttribute('data-decision', 'allow');249  await expect(rule).toContainText('Project: fixture-billing');250  // the question is answered with free text251  await page.getByTestId('nav-approvals').click();252  const question = page.locator('[data-testid="wait-card"][data-kind="question"]');253  await question.getByTestId('wait-answer').fill('Yes, and add an examples section.');254  await question.getByTestId('wait-answer-send').click();255  await expect(page.getByTestId('approvals-empty')).toContainText('Inbox zero');256  await expect(page.getByTestId('nav-approvals').getByTestId('pending-badge')).toHaveCount(0);257  w.expectClean();258});259260test('deny from the inbox sends the note to the agent', async ({ page }) => {261  await page.goto('/approvals/');262  const approval = page.locator('[data-testid="wait-card"][data-kind="approval"]');263  await approval.getByTestId('wait-deny').click();264  await approval.getByTestId('wait-deny-note').fill('Open a pull request instead.');265  await approval.getByTestId('wait-deny-confirm').click();266  await expect(approval).toHaveCount(0);267  const m = await mission(page, FIX.approval);268  await page.goto(`/mission/?id=${m.id}`);269  await expect(page.getByTestId('mission-page')).toHaveAttribute('data-status', 'partial', { timeout: 20_000 });270  await expect(tool(page, 'git_push')).toHaveCount(0); // a denied call is never executed: no tool row271  await expect(timeline(page).getByTestId('timeline-wait').filter({ hasText: 'Denied' })).toContainText('Your note: Open a pull request instead.');272  await expect(page.getByTestId('mission-page')).toHaveAttribute('data-status', 'partial', { timeout: 20_000 });273});274275test('spend page shows the real numbers of the API', async ({ page }) => {276  const w = watch(page);277  const s = await apiGet<{ todayUsd: number; dailyBudgetUsd: number; last7Days: { usd: number }[]; byModel: { model: string; usd: number }[] }>(page, '/spend');278  await page.goto('/spend/');279  const usd = (n: number, d = n === 0 || n >= 1 ? 2 : n >= 0.01 ? 3 : 4) => `$${n.toLocaleString('en-CA', { minimumFractionDigits: d, maximumFractionDigits: d })}`;280  await expect(page.getByTestId('spend-today')).toHaveText(usd(s.todayUsd));281  await expect(page.getByTestId('spend-cap')).toHaveText(usd(s.dailyBudgetUsd, 2));282  await expect(page.getByTestId('spend-week')).toHaveText(usd(s.last7Days.reduce((n, d) => n + d.usd, 0), 2));283  await expect(page.getByTestId('week-bar')).toHaveCount(7);284  await expect(page.getByRole('table', { name: /Spend per day/ }).locator('tbody tr')).toHaveCount(7); // accessible equivalent of the chart285  const rows = page.getByTestId('by-model').locator('tbody tr');286  await expect(rows).toHaveCount(s.byModel.length);287  const top = [...s.byModel].sort((a, b) => b.usd - a.usd)[0]!;288  await expect(rows.first()).toContainText(top.model);289  await expect(rows.first()).toContainText(usd(top.usd, 2));290  // the daily cap comes from settings: change it there, it changes here and on Home291  await page.goto('/settings/');292  await page.getByTestId('set-daily-budget').fill('25');293  await page.getByTestId('set-daily-budget').press('Enter');294  await expect.poll(async () => (await apiGet<{ dailyBudgetUsd: number }>(page, '/settings')).dailyBudgetUsd).toBe(25);295  await page.getByTestId('nav-home').click();296  await expect(page.getByTestId('spend-today')).toContainText('of $25.00 daily cap');297  w.expectClean();298});299300test('policy rules: create, list, delete', async ({ page }) => {301  const w = watch(page);302  await page.goto('/policy/');303  await expect(page.getByTestId('rule-row')).toHaveCount(2);304  await page.getByTestId('rule-new').click();305  await page.getByTestId('rule-decision-deny').click();306  await page.getByTestId('rule-tool').fill('shell');307  await page.getByRole('combobox', { name: 'Matcher' }).click();308  await page.getByRole('option', { name: 'Command starts with' }).click();309  await page.getByTestId('rule-match-value').fill('curl');310  await page.getByRole('combobox', { name: 'Scope' }).click();311  await page.getByRole('option', { name: 'Project: fixture-docs' }).click();312  await page.getByTestId('rule-note').fill('no downloads in docs');313  await page.getByTestId('rule-submit').click();314  const row = page.getByTestId('rule-row').filter({ hasText: '“curl”' });315  await expect(row).toHaveAttribute('data-decision', 'deny');316  await expect(row).toContainText('command starts with “curl”');317  await expect(row).toContainText('Project: fixture-docs');318  await expect(page.getByTestId('rule-row').first()).toHaveAttribute('data-decision', 'deny'); // deny rules first319  const rules = await apiGet<{ tool: string; match: Record<string, string>; decision: string }[]>(page, '/policy/rules');320  expect(rules.some((r) => r.decision === 'deny' && r.tool === 'shell' && r.match.commandPrefix === 'curl')).toBe(true);321  await row.getByTestId('rule-delete').click();322  await page.getByRole('dialog').getByRole('button', { name: 'Delete rule' }).click();323  await expect(page.getByTestId('rule-row')).toHaveCount(2);324  w.expectClean();325});326327test('missions list filters; projects; settings defaults persist', async ({ page }) => {328  const w = watch(page);329  await page.goto('/missions/');330  await expect(page.getByTestId('mission-row')).toHaveCount(6);331  await page.getByRole('radio', { name: /Needs you/ }).click();332  await expect(page.getByTestId('mission-row')).toHaveCount(2);333  await page.getByRole('radio', { name: /Stopped/ }).click();334  await expect(page.getByTestId('mission-row')).toHaveCount(2);335  await page.getByRole('radio', { name: /^All/ }).click();336  await page.getByTestId('missions-search').fill('rounding');337  await expect(page.getByTestId('mission-row')).toHaveCount(1);338  await expect(page.getByTestId('mission-row')).toContainText('Claude Opus 5.5');339  await page.getByTestId('nav-projects').click();340  await expect(page.getByTestId('project-card')).toHaveCount(2);341  await page.getByTestId('project-new').click();342  await page.getByTestId('project-name').fill('scratch');343  await page.getByTestId('project-create').click();344  await expect(page.getByTestId('project-card')).toHaveCount(3);345  await page.getByTestId('nav-settings').click();346  await page.getByRole('combobox', { name: 'Default effort' }).click();347  await page.getByRole('option', { name: 'Medium' }).click();348  await page.getByTestId('set-mission-budget').fill('9.5');349  await page.getByTestId('set-mission-budget').press('Enter');350  await expect.poll(async () => { const s = await apiGet<{ defaultEffort: string; missionBudgetUsd: number }>(page, '/settings'); return `${s.defaultEffort} ${s.missionBudgetUsd}`; }).toBe('medium 9.5');351  await expect(page.getByText('Devices and sessions')).toBeVisible();352  w.expectClean();353});354355test('theme toggle: applied at once, stored locally and on the server, theme-color follows', async ({ page }) => {356  await page.goto('/home/');357  await expect(page.locator('html')).toHaveAttribute('data-theme', 'light'); // light by default358  await page.getByTestId('theme-toggle').click();359  await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');360  await expect(page.locator('meta[name="theme-color"]')).toHaveAttribute('content', '#080B10');361  expect(await page.evaluate(() => localStorage.getItem('aos.theme'))).toBe('dark');362  await expect.poll(async () => (await apiGet<{ theme: string }>(page, '/settings')).theme).toBe('dark');363  await page.reload();364  await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark'); // before paint, from /theme-init.js365  await page.getByTestId('theme-toggle').click();366  await expect(page.locator('html')).toHaveAttribute('data-theme', 'light');367});368369test('every route is free of console errors and CSP violations under the production CSP', async ({ page }) => {370  const w = watch(page);371  const m = await mission(page, FIX.done);372  for (const route of [...ROUTES, `/mission/?id=${m.id}`]) {373    const res = await page.goto(route);374    const csp = res?.headers()['content-security-policy'] ?? '';375    expect(csp, route).toContain("default-src 'self'");376    expect(csp, route).not.toContain("'unsafe-eval'");377    expect(csp.match(/script-src[^;]*/)?.[0] ?? '', route).not.toContain("'unsafe-inline'");378    await expect(page.getByTestId('page-title').or(page.getByTestId('mission-title')).first()).toBeVisible();379    await page.waitForLoadState('networkidle');380    await page.waitForTimeout(250);381  }382  // overlays too: command palette and the new-mission dialog383  await page.goto('/home/');384  await page.keyboard.press('ControlOrMeta+k');385  await page.getByTestId('palette-input').fill('rounding');386  await page.keyboard.press('Enter');387  await expect(page.getByTestId('mission-title')).toHaveText(FIX.approval);388  expect(await page.evaluate(() => typeof window.__aos)).toBe('object'); // this is the E2E build389  w.expectClean();390});391
Files main
Menu

Theme