diff --git a/tests/integration/tests/19-telemetry-disclosure.spec.ts b/tests/integration/tests/19-telemetry-disclosure.spec.ts index ed1cef827..6e14eeb36 100644 --- a/tests/integration/tests/19-telemetry-disclosure.spec.ts +++ b/tests/integration/tests/19-telemetry-disclosure.spec.ts @@ -1,8 +1,13 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { test as base, expect, type Locator, type Page } from '@playwright/test'; -import { createAuthenticatedStorageState } from './helpers'; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { + test as base, + expect, + type Locator, + type Page, +} from "@playwright/test"; +import { createAuthenticatedStorageState } from "./helpers"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -14,23 +19,26 @@ const test = base.extend<{}, WorkerFixtures>({ storageState: async ({ authStorageStatePath }, use) => { await use(authStorageStatePath); }, - authStorageStatePath: [async ({ browser }, use, workerInfo) => { - const storageStatePath = path.resolve( - __dirname, - '..', - '..', - 'tmp', - 'playwright-auth', - `telemetry-disclosure-${workerInfo.project.name}.json`, - ); - fs.mkdirSync(path.dirname(storageStatePath), { recursive: true }); - await createAuthenticatedStorageState(browser, storageStatePath); - try { - await use(storageStatePath); - } finally { - fs.rmSync(storageStatePath, { force: true }); - } - }, { scope: 'worker' }], + authStorageStatePath: [ + async ({ browser }, use, workerInfo) => { + const storageStatePath = path.resolve( + __dirname, + "..", + "..", + "tmp", + "playwright-auth", + `telemetry-disclosure-${workerInfo.project.name}.json`, + ); + fs.mkdirSync(path.dirname(storageStatePath), { recursive: true }); + await createAuthenticatedStorageState(browser, storageStatePath); + try { + await use(storageStatePath); + } finally { + fs.rmSync(storageStatePath, { force: true }); + } + }, + { scope: "worker" }, + ], }); async function expectPopupDoc( @@ -39,70 +47,83 @@ async function expectPopupDoc( pathname: string, expectedText: string, ) { - const [popup] = await Promise.all([ - page.waitForEvent('popup'), - link.click(), - ]); + const [popup] = await Promise.all([page.waitForEvent("popup"), link.click()]); - await popup.waitForLoadState('domcontentloaded'); + await popup.waitForLoadState("domcontentloaded"); expect(new URL(popup.url()).pathname).toBe(pathname); - await expect(popup.locator('body')).toContainText(expectedText); + await expect(popup.locator("body")).toContainText(expectedText); await popup.close(); } async function readTelemetryPreview(page: Page) { const preview = page.locator('pre[aria-label="Telemetry payload preview"]'); await expect(preview).toBeVisible(); - return JSON.parse(await preview.textContent() ?? '{}') as { + return JSON.parse((await preview.textContent()) ?? "{}") as { install_id: string; event: string; }; } -test.describe('Telemetry disclosure', () => { +test.describe("Telemetry disclosure", () => { test.setTimeout(180_000); - test('general settings opens the shipped privacy document', async ({ page }, testInfo) => { - test.skip(testInfo.project.name.startsWith('mobile-'), 'Desktop-only telemetry disclosure coverage'); + test("general settings opens the shipped privacy document", async ({ + page, + }, testInfo) => { + test.skip( + testInfo.project.name.startsWith("mobile-"), + "Desktop-only telemetry disclosure coverage", + ); - await page.goto('/settings/system-general', { waitUntil: 'domcontentloaded' }); + await page.goto("/settings/system-general", { + waitUntil: "domcontentloaded", + }); await page.waitForURL(/\/settings/, { timeout: 15_000 }); const telemetrySummary = page.getByText( /rotating pseudonymous install ID, normalized release identity, runtime platform/i, ); await expect(telemetrySummary).toBeVisible(); - await expect(telemetrySummary).toContainText('Telemetry rows are retained for up to 90 days'); await expect(telemetrySummary).toContainText( - 'are not stored in telemetry rows', + "Telemetry rows are retained for up to 90 days", + ); + await expect(telemetrySummary).toContainText( + "are not stored in telemetry rows", ); - const disclosureLink = page.getByRole('link', { name: 'Full details' }).first(); - await expect(disclosureLink).toHaveAttribute('href', '/docs/PRIVACY.md'); + const disclosureLink = page + .getByRole("link", { name: "Full details" }) + .first(); + await expect(disclosureLink).toHaveAttribute("href", "/docs/PRIVACY"); await expectPopupDoc( page, disclosureLink, - '/docs/PRIVACY.md', - 'Pulse has one outbound usage-data scope', + "/docs/PRIVACY", + "Pulse has one outbound usage-data scope", ); }); - test('general settings lets operators preview and rotate the telemetry payload', async ({ + test("general settings lets operators preview and rotate the telemetry payload", async ({ page, }, testInfo) => { - test.skip(testInfo.project.name.startsWith('mobile-'), 'Desktop-only telemetry disclosure coverage'); + test.skip( + testInfo.project.name.startsWith("mobile-"), + "Desktop-only telemetry disclosure coverage", + ); - await page.goto('/settings/system-general', { waitUntil: 'domcontentloaded' }); + await page.goto("/settings/system-general", { + waitUntil: "domcontentloaded", + }); await page.waitForURL(/\/settings/, { timeout: 15_000 }); - await page.getByRole('button', { name: 'Preview payload' }).click(); + await page.getByRole("button", { name: "Preview payload" }).click(); const initialPreview = await readTelemetryPreview(page); - expect(initialPreview.event).toBe('heartbeat'); + expect(initialPreview.event).toBe("heartbeat"); expect(initialPreview.install_id).toBeTruthy(); - page.once('dialog', (dialog) => dialog.accept()); - await page.getByRole('button', { name: 'Reset ID' }).click(); + page.once("dialog", (dialog) => dialog.accept()); + await page.getByRole("button", { name: "Reset ID" }).click(); await expect .poll(async () => { diff --git a/tests/integration/tests/26-truenas-alert-thresholds.spec.ts b/tests/integration/tests/26-truenas-alert-thresholds.spec.ts index 23de9e673..4db528e69 100644 --- a/tests/integration/tests/26-truenas-alert-thresholds.spec.ts +++ b/tests/integration/tests/26-truenas-alert-thresholds.spec.ts @@ -1,8 +1,8 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { test as base, expect } from '@playwright/test'; -import { createAuthenticatedStorageState } from './helpers'; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { test as base, expect } from "@playwright/test"; +import { createAuthenticatedStorageState } from "./helpers"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -10,80 +10,87 @@ type WorkerFixtures = { authStorageStatePath: string; }; -const SCREENSHOT_PATH = '/tmp/truenas-alert-thresholds.png'; +const SCREENSHOT_PATH = "/tmp/truenas-alert-thresholds.png"; const test = base.extend<{}, WorkerFixtures>({ storageState: async ({ authStorageStatePath }, use) => { await use(authStorageStatePath); }, - authStorageStatePath: [async ({ browser }, use, workerInfo) => { - const storageStatePath = path.resolve( - __dirname, - '..', - '..', - 'tmp', - 'playwright-auth', - `truenas-alert-thresholds-${workerInfo.project.name}.json`, - ); - fs.mkdirSync(path.dirname(storageStatePath), { recursive: true }); - await createAuthenticatedStorageState(browser, storageStatePath); - try { - await use(storageStatePath); - } finally { - fs.rmSync(storageStatePath, { force: true }); - } - }, { scope: 'worker' }], + authStorageStatePath: [ + async ({ browser }, use, workerInfo) => { + const storageStatePath = path.resolve( + __dirname, + "..", + "..", + "tmp", + "playwright-auth", + `truenas-alert-thresholds-${workerInfo.project.name}.json`, + ); + fs.mkdirSync(path.dirname(storageStatePath), { recursive: true }); + await createAuthenticatedStorageState(browser, storageStatePath); + try { + await use(storageStatePath); + } finally { + fs.rmSync(storageStatePath, { force: true }); + } + }, + { scope: "worker" }, + ], }); // The thresholds page renders live websocket state, so REST stubs of // /api/resources get overwritten by the next state frame. The assertions // pin the mock scenario's TrueNAS fixture (truenas-main with pool tank, // tank/* datasets, and physical disks) instead of stubbed payloads. -test.describe('TrueNAS alert thresholds', () => { +test.describe("TrueNAS alert thresholds", () => { test.setTimeout(180_000); - // Desktop presentation spec. The mobile layout collapses the platform - // scope filter into the FilterBar sheet, so the 'TrueNAS' scope button - // this spec clicks does not exist there and the click falls through to - // the platform nav tab; mobile coverage lives in 04-mobile.spec.ts. - test.skip(({ isMobile }) => Boolean(isMobile), 'desktop-presentation spec'); + // Desktop presentation spec. Mobile coverage lives in 04-mobile.spec.ts. + test.skip(({ isMobile }) => Boolean(isMobile), "desktop-presentation spec"); - test('surfaces TrueNAS systems, pools, datasets, and disks under the TrueNAS thresholds scope', async ({ + test("surfaces TrueNAS systems, pools, datasets, and disks under the TrueNAS thresholds scope", async ({ page, }) => { // Fresh test backends boot with alerts deactivated, which gates the // thresholds route behind the Alerts Overview activation screen. The // config endpoint is REST-fed and stubable (same pattern as the Ceph // thresholds spec); entity data below still comes from live mock state. - await page.route('**/api/alerts/config', async (route) => { + await page.route("**/api/alerts/config", async (route) => { await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({ enabled: true, - activationState: 'active', + activationState: "active", overrides: {}, }), }); }); - await page.goto('/alerts/thresholds/infrastructure', { - waitUntil: 'domcontentloaded', + await page.goto("/alerts/thresholds/infrastructure", { + waitUntil: "domcontentloaded", }); await expect(page).toHaveURL(/\/alerts\/thresholds/); - await expect(page.getByRole('heading', { name: 'Alert Thresholds' })).toBeVisible(); + await expect( + page.getByRole("heading", { name: "Alert Thresholds" }), + ).toBeVisible(); // Threshold scopes are platform-first; TrueNAS entities live under // their own scope instead of the retired neutral groupings. - await page.getByRole('button', { name: 'TrueNAS', exact: true }).click(); + await page + .getByRole("tablist", { name: "Threshold platform" }) + .getByRole("tab", { name: "TrueNAS", exact: true }) + .click(); await expect(page).toHaveURL(/\/alerts\/thresholds\/truenas/); - await expect(page.getByRole('heading', { name: 'Systems' })).toBeVisible(); - await expect(page.getByRole('heading', { name: 'Pools' })).toBeVisible(); - await expect(page.getByRole('heading', { name: 'Datasets' })).toBeVisible(); - await expect(page.getByRole('heading', { name: 'Disks' })).toBeVisible(); - await expect(page.getByText('truenas-main', { exact: true }).first()).toBeVisible(); - await expect(page.getByText('tank', { exact: true }).first()).toBeVisible(); + await expect(page.getByRole("heading", { name: "Systems" })).toBeVisible(); + await expect(page.getByRole("heading", { name: "Pools" })).toBeVisible(); + await expect(page.getByRole("heading", { name: "Datasets" })).toBeVisible(); + await expect(page.getByRole("heading", { name: "Disks" })).toBeVisible(); + await expect( + page.getByText("truenas-main", { exact: true }).first(), + ).toBeVisible(); + await expect(page.getByText("tank", { exact: true }).first()).toBeVisible(); await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true }); }); diff --git a/tests/integration/tests/28-truenas-alert-resource-links.spec.ts b/tests/integration/tests/28-truenas-alert-resource-links.spec.ts index c3b8b5e41..42bb74e09 100644 --- a/tests/integration/tests/28-truenas-alert-resource-links.spec.ts +++ b/tests/integration/tests/28-truenas-alert-resource-links.spec.ts @@ -1,8 +1,8 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { test as base, expect } from '@playwright/test'; -import { createAuthenticatedStorageState } from './helpers'; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { test as base, expect } from "@playwright/test"; +import { createAuthenticatedStorageState } from "./helpers"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -10,70 +10,74 @@ type WorkerFixtures = { authStorageStatePath: string; }; -const SCREENSHOT_PATH = '/tmp/truenas-alert-resource-links.png'; +const SCREENSHOT_PATH = "/tmp/truenas-alert-resource-links.png"; const test = base.extend<{}, WorkerFixtures>({ storageState: async ({ authStorageStatePath }, use) => { await use(authStorageStatePath); }, - authStorageStatePath: [async ({ browser }, use, workerInfo) => { - const storageStatePath = path.resolve( - __dirname, - '..', - '..', - 'tmp', - 'playwright-auth', - `truenas-alert-resource-links-${workerInfo.project.name}.json`, - ); - fs.mkdirSync(path.dirname(storageStatePath), { recursive: true }); - await createAuthenticatedStorageState(browser, storageStatePath); - try { - await use(storageStatePath); - } finally { - fs.rmSync(storageStatePath, { force: true }); - } - }, { scope: 'worker' }], + authStorageStatePath: [ + async ({ browser }, use, workerInfo) => { + const storageStatePath = path.resolve( + __dirname, + "..", + "..", + "tmp", + "playwright-auth", + `truenas-alert-resource-links-${workerInfo.project.name}.json`, + ); + fs.mkdirSync(path.dirname(storageStatePath), { recursive: true }); + await createAuthenticatedStorageState(browser, storageStatePath); + try { + await use(storageStatePath); + } finally { + fs.rmSync(storageStatePath, { force: true }); + } + }, + { scope: "worker" }, + ], }); const ALERT_START = new Date(Date.now() - 45 * 60 * 1000).toISOString(); const ALERT_LAST_SEEN = new Date(Date.now() - 30 * 60 * 1000).toISOString(); -test.describe('TrueNAS alert resource links', () => { +test.describe("TrueNAS alert resource links", () => { test.setTimeout(180_000); - test('keeps TrueNAS alert investigation on the scoped resource incidents panel', async ({ + test("keeps TrueNAS alert investigation on the scoped resource incidents panel", async ({ page, + isMobile, }) => { - await page.route('**/api/resources**', async (route) => { + await page.route("**/api/resources**", async (route) => { const requestUrl = new URL(route.request().url()); - if (requestUrl.pathname !== '/api/resources') { + if (requestUrl.pathname !== "/api/resources") { await route.continue(); return; } await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({ data: [ { - id: 'truenas-main', - type: 'agent', - name: 'truenas-main', - displayName: 'TrueNAS Main', - platformId: 'truenas-main', - platformType: 'truenas', - sourceType: 'hybrid', - sources: ['agent', 'truenas'], - status: 'online', + id: "truenas-main", + type: "agent", + name: "truenas-main", + displayName: "TrueNAS Main", + platformId: "truenas-main", + platformType: "truenas", + sourceType: "hybrid", + sources: ["agent", "truenas"], + status: "online", lastSeen: ALERT_LAST_SEEN, canonicalIdentity: { - displayName: 'TrueNAS Main', - hostname: 'truenas-main', - platformId: 'truenas-main', + displayName: "TrueNAS Main", + hostname: "truenas-main", + platformId: "truenas-main", }, platformData: { - sources: ['agent', 'truenas'], + sources: ["agent", "truenas"], }, }, ], @@ -87,57 +91,57 @@ test.describe('TrueNAS alert resource links', () => { }); }); - await page.route('**/api/alerts/config', async (route) => { + await page.route("**/api/alerts/config", async (route) => { await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({ enabled: true, - activationState: 'active', + activationState: "active", overrides: {}, }), }); }); - await page.route('**/api/alerts/active', async (route) => { + await page.route("**/api/alerts/active", async (route) => { await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify([]), }); }); - await page.route('**/api/alerts/history**', async (route) => { + await page.route("**/api/alerts/history**", async (route) => { await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify([ { - id: 'truenas-alert-1', - type: 'host-offline', - level: 'critical', + id: "truenas-alert-1", + type: "host-offline", + level: "critical", startTime: ALERT_START, lastSeen: ALERT_LAST_SEEN, - resourceId: 'truenas-main', - resourceName: 'TrueNAS Main', - message: 'TrueNAS Main is offline', + resourceId: "truenas-main", + resourceName: "TrueNAS Main", + message: "TrueNAS Main is offline", acknowledged: false, - node: 'truenas-main', - nodeDisplayName: 'TrueNAS Main', + node: "truenas-main", + nodeDisplayName: "TrueNAS Main", metadata: { - resourceType: 'agent', + resourceType: "agent", }, }, ]), }); }); - await page.route('**/api/alerts/incidents**', async (route) => { + await page.route("**/api/alerts/incidents**", async (route) => { const requestUrl = new URL(route.request().url()); - if (requestUrl.searchParams.get('resource_id') !== 'truenas-main') { + if (requestUrl.searchParams.get("resource_id") !== "truenas-main") { await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify(null), }); return; @@ -145,22 +149,22 @@ test.describe('TrueNAS alert resource links', () => { await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify([ { - id: 'incident-truenas-1', - alertType: 'Host Offline', - level: 'critical', - status: 'open', + id: "incident-truenas-1", + alertType: "Host Offline", + level: "critical", + status: "open", acknowledged: false, openedAt: ALERT_START, - message: 'TrueNAS Main is offline', + message: "TrueNAS Main is offline", events: [ { - id: 'incident-event-1', - type: 'opened', + id: "incident-event-1", + type: "opened", timestamp: ALERT_START, - summary: 'Alert opened', + summary: "Alert opened", }, ], }, @@ -168,18 +172,18 @@ test.describe('TrueNAS alert resource links', () => { }); }); - await page.route('**/api/notifications/email', async (route) => { + await page.route("**/api/notifications/email", async (route) => { await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({ enabled: false, - provider: '', - server: '', + provider: "", + server: "", port: 587, - username: '', - password: '', - from: '', + username: "", + password: "", + from: "", to: [], tls: false, startTLS: false, @@ -187,39 +191,48 @@ test.describe('TrueNAS alert resource links', () => { }); }); - await page.route('**/api/notifications/apprise', async (route) => { + await page.route("**/api/notifications/apprise", async (route) => { await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({ enabled: false, }), }); }); - await page.goto('/alerts/history', { - waitUntil: 'domcontentloaded', + await page.goto("/alerts/history", { + waitUntil: "domcontentloaded", }); - await expect(page.getByRole('heading', { name: 'Alert History' })).toBeVisible(); - const alertRow = page - .locator('tr') - .filter({ hasText: 'TrueNAS Main' }) - .filter({ has: page.getByRole('button', { name: 'Resource' }) }) + await expect( + page.getByRole("heading", { name: "Alert History" }), + ).toBeVisible(); + const alertItem = ( + isMobile + ? page.getByTestId("alert-history-mobile-list").locator("article") + : page.locator("tr") + ) + .filter({ hasText: "TrueNAS Main" }) + .filter({ has: page.getByRole("button", { name: "Resource" }) }) .first(); - await expect(alertRow).toBeVisible(); - await alertRow.getByRole('button', { name: 'Resource' }).click(); + await expect(alertItem).toBeVisible(); + await alertItem.getByRole("button", { name: "Resource" }).click(); // Cross-link affordances into the retired standalone routes were removed // with platform-first navigation; the canonical handoff is the resource // incidents panel scoped to the alerting resource. - await expect(page.getByRole('heading', { name: 'Resource incidents' })).toBeVisible(); + await expect( + page.getByRole("heading", { name: "Resource incidents" }), + ).toBeVisible(); const incidentsPanel = page - .locator('div') - .filter({ has: page.getByRole('heading', { name: 'Resource incidents' }) }) + .locator("div") + .filter({ + has: page.getByRole("heading", { name: "Resource incidents" }), + }) .last(); - await expect(page.getByText('TrueNAS Main').first()).toBeVisible(); - await expect(page.getByText('· 1 incident')).toBeVisible(); + await expect(page.getByText("TrueNAS Main").first()).toBeVisible(); + await expect(page.getByText("· 1 incident")).toBeVisible(); await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true }); }); diff --git a/tests/integration/tests/36-vmware-alert-history-resource-incidents.spec.ts b/tests/integration/tests/36-vmware-alert-history-resource-incidents.spec.ts index 35c5a436e..7f6e193fa 100644 --- a/tests/integration/tests/36-vmware-alert-history-resource-incidents.spec.ts +++ b/tests/integration/tests/36-vmware-alert-history-resource-incidents.spec.ts @@ -7,11 +7,14 @@ test.describe("VMware alert history resource incidents", () => { test("opens VMware resource incidents through the shared alert history surface", async ({ page, + isMobile, }) => { // Recent timestamps keep the fixture inside the history view's default // period window; fixed dates silently age out of it. const INCIDENT_START = new Date(Date.now() - 30 * 60 * 1000).toISOString(); - const INCIDENT_LAST_SEEN = new Date(Date.now() - 28 * 60 * 1000).toISOString(); + const INCIDENT_LAST_SEEN = new Date( + Date.now() - 28 * 60 * 1000, + ).toISOString(); await page.route("**/api/alerts/config", async (route) => { await route.fulfill({ status: 200, @@ -186,24 +189,33 @@ test.describe("VMware alert history resource incidents", () => { await expect( page.getByRole("heading", { name: "Alert History" }), ).toBeVisible(); - await expect(page.getByText("app-01")).toBeVisible(); - const historyRow = page.locator("tr").filter({ hasText: "app-01" }).first(); - await expect(historyRow).toContainText( + const historyItem = ( + isMobile + ? page.getByTestId("alert-history-mobile-list").locator("article") + : page.locator("tr") + ) + .filter({ hasText: "app-01" }) + .first(); + await expect(historyItem).toContainText( "VM vm-201 has VMware alarm VM replication fault (red)", ); - await historyRow.getByRole("button", { name: "Resource" }).click(); + await historyItem.getByRole("button", { name: "Resource" }).click(); - // The message renders in both the history table cell and the opened - // resource drawer; assert the drawer copy specifically. + const incidentsHeading = page.getByRole("heading", { + name: "Resource incidents", + }); + const incidentsPanel = incidentsHeading.locator("xpath=../../.."); await expect( - page - .getByRole("paragraph") - .filter({ hasText: "VM vm-201 has VMware alarm VM replication fault (red)" }), + incidentsPanel.getByText( + "VM vm-201 has VMware alarm VM replication fault (red)", + ), ).toBeVisible(); - await expect(page.getByText("Resource incidents")).toBeVisible(); + await expect(incidentsHeading).toBeVisible(); // "Resource Health" renders as both the incident label and a type badge // inside the drawer. - await expect(page.getByText("Resource Health").first()).toBeVisible(); + await expect( + incidentsPanel.getByText("Resource Health").first(), + ).toBeVisible(); await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true }); }); diff --git a/tests/integration/tests/91-operational-trust-attention-workbench.spec.ts b/tests/integration/tests/91-operational-trust-attention-workbench.spec.ts index 9c5517334..bc9519c47 100644 --- a/tests/integration/tests/91-operational-trust-attention-workbench.spec.ts +++ b/tests/integration/tests/91-operational-trust-attention-workbench.spec.ts @@ -2,6 +2,21 @@ import { expect, test, type Page, type Route } from "@playwright/test"; type AttentionMode = "active" | "calm" | "failed"; +async function selectAttentionState(page: Page, label: string, value: string) { + const queue = page.getByRole("region", { name: "Needs attention" }); + const segmentedChoice = queue.getByRole("button", { + name: label, + exact: true, + }); + if (await segmentedChoice.isVisible()) { + await segmentedChoice.click(); + return; + } + await queue + .getByRole("combobox", { name: "Attention state" }) + .selectOption(value); +} + test.beforeEach(async ({ page }) => { const pageErrors: string[] = []; const consoleErrors: string[] = []; @@ -714,31 +729,23 @@ test("makes active operational work primary and preserves the evidence boundary" page.getByRole("button", { name: "Explain with Assistant" }), ).toHaveCount(0); - await queue - .getByRole("button", { name: "Acknowledged 1", exact: true }) - .click(); + await selectAttentionState(page, "Acknowledged 1", "acknowledged"); await expect( queue.getByText("Memory pressure acknowledged on pve-lab"), ).toBeVisible(); - await queue - .getByRole("button", { name: "Suppressed 1", exact: true }) - .click(); + await selectAttentionState(page, "Suppressed 1", "suppressed"); await expect( queue.getByText("Maintenance alert suppressed on pve-maintenance"), ).toBeVisible(); - await queue - .getByRole("button", { name: "Stale or unknown 1", exact: true }) - .click(); + await selectAttentionState(page, "Stale or unknown 1", "stale_unknown"); await expect( queue.getByText("Connection state unknown for pve-edge"), ).toBeVisible(); - await queue - .getByRole("button", { name: "Recent resolved 1", exact: true }) - .click(); + await selectAttentionState(page, "Recent resolved 1", "resolved"); await expect( queue.getByText("Storage pressure resolved on pve-recovered"), ).toBeVisible(); - await queue.getByRole("button", { name: "Active 2", exact: true }).click(); + await selectAttentionState(page, "Active 2", "active"); const itemButton = queue.getByRole("button", { name: "Open CPU pressure on pve-main", diff --git a/tests/integration/tests/journeys/07-audit-log-resilience.spec.ts b/tests/integration/tests/journeys/07-audit-log-resilience.spec.ts index 64cd866c5..859043f70 100644 --- a/tests/integration/tests/journeys/07-audit-log-resilience.spec.ts +++ b/tests/integration/tests/journeys/07-audit-log-resilience.spec.ts @@ -130,7 +130,9 @@ test("audit log fails closed, recovers, pages atomically, and ignores stale resp mode = "busy"; await page.getByRole("button", { name: "Refresh" }).click(); - await expect(page.locator("main").getByRole("alert")).toContainText("storage is busy"); + await expect(page.locator("main").getByRole("alert")).toContainText( + "storage is busy", + ); await expect(page.getByText("initial-row", { exact: true })).toHaveCount(0); mode = "recovered"; @@ -139,6 +141,7 @@ test("audit log fails closed, recovers, pages atomically, and ignores stale resp await expect(page.locator("main").getByRole("alert")).toHaveCount(0); mode = "page-size"; + await page.getByRole("button", { name: "View", exact: true }).click(); await page.getByLabel("Audit page size").selectOption("25"); await expect(page.getByText("Showing 1-25 of 250")).toBeVisible(); expect( @@ -159,6 +162,8 @@ test("audit log fails closed, recovers, pages atomically, and ignores stale resp mode = "invalid"; await page.getByRole("button", { name: "Refresh" }).click(); - await expect(page.locator("main").getByRole("alert")).toContainText("invalid response"); + await expect(page.locator("main").getByRole("alert")).toContainText( + "invalid response", + ); await expect(page.getByText("latest-row", { exact: true })).toHaveCount(0); });