Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,13 @@ E2E_LIVE_MODE=true npx playwright test

The mock and seeded projects run in the **GitHub Actions** pull-request workflow. The live project is intended for a protected pipeline with an Entra identity or API keys.

For ordinary interactions with startup controls, call `waitForAppReady(page)`
from `e2e/_runtime.ts` after navigation or reload. A visible control can still
move as runtime readiness and configured defaults arrive. Tests that deliberately
interact before startup finishes should control those responses instead.
Before reloading to check a preference change, wait for its value in localStorage;
an empty Operation picker can mean its saved-ID lookup is still loading.

CI runs two isolated mock shards, one serial seeded job, and one isolated
analytics job, with one worker per job. Mock and seeded jobs start their own
Vite server and Python backend because mock specs still depend on backend auth
Expand Down
10 changes: 10 additions & 0 deletions frontend/e2e/_runtime.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import { expect, type Page } from '@playwright/test'

import type { RuntimeReadiness, RuntimeStatus } from '../src/types'

export async function waitForAppReady(page: Page): Promise<void> {
await expect(page.getByRole('region', { name: 'Default Labels', exact: true })).toBeVisible()
await expect(page.getByText(/^PyRIT runtime:/)).toHaveCount(0)
await expect(page.getByText(
'New attacks and scans are unavailable until default labels are loaded.', { exact: false },
)).toHaveCount(0)
}

export const READY_RUNTIME: RuntimeReadiness = {
ready: true,
state: 'ready',
Expand Down
19 changes: 19 additions & 0 deletions frontend/e2e/operations-acceptance.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { test, expect, type APIRequestContext, type Page } from '@playwright/tes

import type { AttackSummary, Finding, Operation, ScenarioRunListItem, ScenarioRunSummary } from '../src/types'
import { compatibilityHeaders } from './_compatibility'
import { waitForAppReady } from './_runtime'

const LEGACY_ATTACK_ID = '11111111-1111-4111-8111-111111111111'
const PROPOSED_NAME = 'browser_proposed'
Expand Down Expand Up @@ -88,13 +89,23 @@ for (const width of [1280, 390]) {
)).toEqual({ id: operation.id })

await bar.getByRole('button', { name: 'Clear operation', exact: true }).click()
await expect.poll(() => page.evaluate(() =>
JSON.parse(localStorage.getItem('pyrit.userPreferences.v1.local') ?? '{}').operation,
)).toBeNull()
await page.reload()
await waitForAppReady(page)
await expect(bar.getByRole('combobox', { name: 'Operation', exact: true })).toHaveValue('')
await page.evaluate(() => localStorage.removeItem('pyrit.userPreferences.v1.local'))
await page.reload()
await waitForAppReady(page)
await expect(bar.getByRole('combobox', { name: 'Operation', exact: true })).toHaveValue(PROPOSED_NAME)
await expect(bar.getByRole('combobox', { name: 'Operation', exact: true })).toHaveAccessibleDescription('Not registered')
await bar.getByRole('combobox', { name: 'Operation', exact: true }).click()
await page.getByRole('option', { name: PROPOSED_NAME, exact: true }).click()
await expect(bar.getByText('Not registered', { exact: true })).toHaveCount(0)
await expect.poll(() => page.evaluate(() =>
JSON.parse(localStorage.getItem('pyrit.userPreferences.v1.local') ?? '{}').operation,
)).toEqual({ id: operation.id })

const newName = `Restored ${width} ${Date.now()}`
await page.evaluate((name: string) => {
Expand Down Expand Up @@ -122,9 +133,13 @@ test('persists selected IDs for chat and scanner history, and clear survives rel
test.setTimeout(90_000)
const operation = await createOperation(request, `Activity ${Date.now()}`)
await page.goto('/')
await waitForAppReady(page)
const bar = page.getByRole('region', { name: 'Default Labels' })
await bar.getByRole('combobox', { name: 'Operation', exact: true }).click()
await page.getByRole('option', { name: operation.name, exact: true }).click()
await expect.poll(() => page.evaluate(() =>
JSON.parse(localStorage.getItem('pyrit.userPreferences.v1.local') ?? '{}').operation,
)).toEqual({ id: operation.id })
const chat = await sendChat(page, request, 'Selected Operation chat.')
expect(chat.operation_id).toBe(operation.id)

Expand Down Expand Up @@ -159,7 +174,11 @@ test('persists selected IDs for chat and scanner history, and clear survives rel
await expect(page.getByTestId(`scenario-history-row-${run.scenario_result_id}`)).toBeVisible()

await bar.getByRole('button', { name: 'Clear operation', exact: true }).click()
await expect.poll(() => page.evaluate(() =>
JSON.parse(localStorage.getItem('pyrit.userPreferences.v1.local') ?? '{}').operation,
)).toBeNull()
await page.reload()
await waitForAppReady(page)
await expect(bar.getByRole('combobox', { name: 'Operation', exact: true })).toHaveValue('')
const unassigned = await sendChat(page, request, 'Unassigned after reload.')
expect(unassigned.operation_id).toBeNull()
Expand Down
5 changes: 5 additions & 0 deletions frontend/e2e/operations.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { test, expect } from '@playwright/test'

import type { Finding, Operation } from '../src/types'
import { waitForAppReady } from './_runtime'

for (const width of [1280, 390]) {
test(`wraps long finding text in detail and evidence picker at ${width}px @seeded`, async ({ page, request }) => {
Expand Down Expand Up @@ -178,6 +179,7 @@ test('selects saved operations inline and revisits ID-scoped attack history @see
test.setTimeout(90_000)
const name = `Engagement / α% & ${Date.now()}`
await page.goto('/')
await waitForAppReady(page)
const bar = page.getByRole('region', { name: 'Default Labels' })
await bar.getByRole('combobox', { name: 'Operation', exact: true }).click()
await page.getByRole('option', { name: 'New operation…', exact: true }).click()
Expand All @@ -191,6 +193,9 @@ test('selects saved operations inline and revisits ID-scoped attack history @see
await expect(bar.getByRole('combobox', { name: 'Operation' })).toHaveValue(name)
await expect(bar.getByRole('combobox', { name: 'Operation', exact: true })).toBeFocused()
await expect(page).toHaveURL(/\/$/)
await expect.poll(() => page.evaluate(() =>
JSON.parse(localStorage.getItem('pyrit.userPreferences.v1.local') ?? '{}').operation,
)).toEqual({ id: operation.id })
await page.reload()
await expect(bar.getByRole('combobox', { name: 'Operation' })).toHaveValue(name)

Expand Down
Loading