From a7f3ae6e91fea8fd3910ea472a0dd11887b01d9d Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 23 Aug 2026 20:27:53 +0100 Subject: [PATCH] Restore frontend and stable browser contracts Canonicalize legacy Proxmox backup bookmarks through the shared route tabs, restore the mobile node web-interface affordance, and strengthen the frontend source-shape guardrails around shared primitives and canonical workload IDs. Align stale stable-tier browser selectors with the accessible product contract while retaining the routing, visibility, history, and tolerance assertions. Incident: 9c1208a480c48dd6 --- frontend-modern/browser-verification.json | 38 ++++++------ .../scripts/shared-template-registry.json | 1 + .../SharedPrimitives.guardrails.test.ts | 7 ++- .../features/proxmox/ProxmoxBackupsTable.tsx | 24 ++++++- .../features/proxmox/ProxmoxNodesTable.tsx | 2 +- .../__tests__/ProxmoxBackupsTable.test.tsx | 22 +++++++ .../__tests__/ProxmoxNodesTable.test.tsx | 26 ++++++++ .../frontendResourceTypeBoundaries.test.ts | 20 +++++- .../tests/17-proxmox-backups-layout.spec.ts | 62 +++++++++++++++---- .../27-truenas-storage-disk-history.spec.ts | 12 +++- .../tests/60-page-header-consistency.spec.ts | 7 ++- ...65-offline-proxmox-node-visibility.spec.ts | 11 +++- ...patrol-assistant-operator-briefing.spec.ts | 25 ++++---- .../tests/85-powered-off-tolerance.spec.ts | 12 ++-- 14 files changed, 211 insertions(+), 58 deletions(-) diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 49e433881..773cef25c 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,38 +1,40 @@ { "version": 1, - "base_sha": "10116cc17efb0004e3820f1a250335899d3b9bf0", - "verified_at": "2026-08-23T19:17:24Z", + "base_sha": "679497119925fca16420fdc3cb7b5d940a50578f", + "verified_at": "2026-08-23T19:26:55Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/components/Workloads/useWorkloadViewportSync.ts" + "frontend-modern/src/features/proxmox/ProxmoxBackupsTable.tsx", + "frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx" ], "content_sha256": { - "frontend-modern/src/components/Workloads/useWorkloadViewportSync.ts": "f21b70f169cc595b31337584e3df20022342e39d50bdfc9eac96a04770bcf7ba" + "frontend-modern/src/features/proxmox/ProxmoxBackupsTable.tsx": "abd090fa70e905ccdabbca1599088288330ec380b3bcfc7bd84624db3a76c7f3", + "frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx": "5ac5e1670e9613b4b11a1596602c6f19a0e0df4982287cfbb491a0f09d993a31" }, "routes": [ - "/proxmox/overview" + "/proxmox", + "/proxmox/backups", + "/proxmox/backups/date", + "/proxmox/backups/coverage" ], "viewports": [ { - "width": 1280, - "height": 800 + "width": 1440, + "height": 900 }, { - "width": 390, - "height": 844 + "width": 393, + "height": 727 } ], "states": [ - "The connected public mock estate rendered 50 nodes and 929 guests with the grouped workload window active", - "Across 24 consecutive 600-pixel desktop wheel steps from scrollTop 10000 to 24400, every sample retained at least 26 visible guest rows", - "Across 24 consecutive 600-pixel wheel steps at 390x844 from scrollTop 10000 to 24400, every sample retained at least 22 visible guest rows", - "Desktop and narrow verification both retained the 140-row mounted guest budget throughout rapid scrolling", - "The verified route reported no browser error-level runtime logs during responsive and sustained-scroll checks" + "A legacy /proxmox/backups bookmark replaced itself with canonical /proxmox/backups/date, while date selection retained the canonical route plus its day query", + "The one-year backup activity view and trailing backup-server columns remained inside the 1440-pixel desktop viewport", + "At the Pixel 5 viewport, the offline Proxmox node remained visible with dashed stale metrics and a visible safe web-interface link; the unsafe JavaScript URL remained blocked with its warning" ], "interactions": [ - "Reloaded /proxmox/overview and confirmed the complete large-estate mock snapshot and bounded workload window", - "Applied repeated physical wheel input through the workload viewport and sampled visible and mounted rows immediately after every desktop step", - "Repeated the same sustained wheel-input sequence at a 390x844 phone viewport and visually inspected the populated table", - "Inspected browser runtime logs after both responsive scroll sequences" + "Opened the legacy backups bookmark, followed the shared Backups and view-tab links, selected a populated day, and exercised the one-year range", + "Opened the offline node's HTTPS web-interface link in a new page and confirmed that the adjacent control did not expand the node row", + "Verified the mobile link hit target, blocked unsafe URL, retained details control, and absence of horizontal viewport overflow" ] } diff --git a/frontend-modern/scripts/shared-template-registry.json b/frontend-modern/scripts/shared-template-registry.json index f53bebf60..cca56eb3d 100644 --- a/frontend-modern/scripts/shared-template-registry.json +++ b/frontend-modern/scripts/shared-template-registry.json @@ -2950,6 +2950,7 @@ "requiredConsumers": [ { "path": "src/features/docker/DockerPageSurface.tsx" }, { "path": "src/features/kubernetes/KubernetesPageSurface.tsx" }, + { "path": "src/features/proxmox/ProxmoxBackupsTable.tsx" }, { "path": "src/features/proxmox/ProxmoxPageSurface.tsx" }, { "path": "src/features/standalone/StandalonePageSurface.tsx" }, { "path": "src/features/truenas/TrueNASPageSurface.tsx" }, diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts index 3d6fd8741..819fe3f4c 100644 --- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts +++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts @@ -533,7 +533,8 @@ describe('shared primitive guardrails', () => { expect(patrolIntelligenceHeaderSource).not.toContain( 'flex-1 py-1.5 px-2 text-xs font-semibold rounded-md transition-all duration-200', ); - expect(proxmoxBackupsTableSource).toContain('FilterSegmentedControl'); + expect(proxmoxBackupsTableSource).toContain('PlatformSectionTabs'); + expect(proxmoxBackupsTableSource).not.toContain('FilterSegmentedControl'); expect(proxmoxBackupsTableSource).not.toContain('FilterButtonGroup'); expect(proxmoxBackupsTableSource).not.toContain('const viewButtonClass'); expect(proxmoxBackupsTableSource).not.toContain( @@ -7252,6 +7253,7 @@ describe('shared primitive guardrails', () => { const requiredConsumerPaths = [ 'src/features/docker/DockerPageSurface.tsx', 'src/features/kubernetes/KubernetesPageSurface.tsx', + 'src/features/proxmox/ProxmoxBackupsTable.tsx', 'src/features/proxmox/ProxmoxPageSurface.tsx', 'src/features/standalone/StandalonePageSurface.tsx', 'src/features/truenas/TrueNASPageSurface.tsx', @@ -7260,6 +7262,7 @@ describe('shared primitive guardrails', () => { const platformPageSurfaceSources = [ dockerPageSurfaceSource, kubernetesPageSurfaceSource, + proxmoxBackupsTableSource, proxmoxPageSurfaceSource, standalonePageSurfaceSource, truenasPageSurfaceSource, @@ -7306,7 +7309,7 @@ describe('shared primitive guardrails', () => { expect(sharedPlatformPageSource).toContain( 'export const PLATFORM_TABLE_NARROW_IDENTITY_WIDTH_PERCENT = 40', ); - expect(proxmoxNodesTableSource).toContain("usesCondensedIdentity() ? '[&>a]:hidden' : ''"); + expect(proxmoxNodesTableSource).not.toContain("'[&>a]:hidden'"); expect(proxmoxNodesTableSource).toContain( "layoutMode() === 'narrow' && nativeNodeName().length", ); diff --git a/frontend-modern/src/features/proxmox/ProxmoxBackupsTable.tsx b/frontend-modern/src/features/proxmox/ProxmoxBackupsTable.tsx index ef64f0899..b240e05be 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxBackupsTable.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxBackupsTable.tsx @@ -1,6 +1,14 @@ -import { Show, createMemo, createResource, createSignal, type Component, type JSX } from 'solid-js'; +import { + Show, + createEffect, + createMemo, + createResource, + createSignal, + type Component, + type JSX, +} from 'solid-js'; import ChevronRightIcon from 'lucide-solid/icons/chevron-right'; -import { useLocation, useSearchParams } from '@solidjs/router'; +import { useLocation, useNavigate, useSearchParams } from '@solidjs/router'; import { FilterBar, type FilterDef, type FilterSelectOption } from '@/components/shared/FilterBar'; import { useBreakpoint } from '@/hooks/useBreakpoint'; import { useProtectionPostures } from '@/hooks/useProtectionPostures'; @@ -110,6 +118,7 @@ export const ProxmoxBackupsTable: Component<{ const [pbsBackups] = createResource(fetchPBSBackups); const { isMobile } = useBreakpoint(); const location = useLocation(); + const navigate = useNavigate(); const protectionPostures = useProtectionPostures(() => (props.workloads ?? []).map((workload) => workload.id), ); @@ -395,6 +404,17 @@ export const ProxmoxBackupsTable: Component<{ BACKUP_VIEW_TABS.map((tab) => ({ ...tab, path: backupViewPath(tab.id) })), ); + // Keep old bookmarks and saved links working, but replace the legacy base + // route and query-param view with the canonical route segment. Replacement + // avoids adding a dead compatibility URL to browser history. + createEffect(() => { + const normalizedPath = location.pathname.replace(/\/+$/, ''); + if (normalizedPath !== PROXMOX_BACKUPS_PATH) return; + navigate(backupViewPath(legacyQueryView() ?? PROXMOX_BACKUPS_DEFAULT_VIEW), { + replace: true, + }); + }); + // Recoverable artifact feed: PBS snapshots, PVE backup files, and guest snapshot rows. const RECOVERABLE_SEGMENT_KINDS: readonly BackupActivitySegmentKind[] = [ 'pbs', diff --git a/frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx b/frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx index ed91d572d..6169f13f8 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx @@ -498,7 +498,7 @@ export const ProxmoxNodesTable: Component<{ a]:hidden' : ''}`} + class="min-w-0" nameClass="truncate font-semibold text-base-content" title={`Open ${name()} web interface`} > diff --git a/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx b/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx index 8900ee621..49f5ea3db 100644 --- a/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx +++ b/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx @@ -343,6 +343,28 @@ describe('ProxmoxBackupsTable', () => { expect(screen.queryByRole('button', { name: /pbs artifacts/i })).not.toBeInTheDocument(); }); + it('normalizes legacy backup bookmarks to the canonical route without losing filters', async () => { + mockBackupAPIs(); + window.history.replaceState( + {}, + '', + '/proxmox/backups?view=coverage&q=pbs-docker&source=pbs&day=2026-05-25', + ); + + renderInRouter(() => ( + } workloads={[workloadResource]} /> + )); + + await waitFor(() => { + expect(window.location.pathname).toBe('/proxmox/backups/coverage'); + const params = new URLSearchParams(window.location.search); + expect(params.get('view')).toBeNull(); + expect(params.get('q')).toBe('pbs-docker'); + expect(params.get('source')).toBeNull(); + expect(params.get('day')).toBeNull(); + }); + }); + it('hydrates the complete saved-view filter state from the URL and clears it atomically', async () => { mockBackupAPIs(); window.history.replaceState( diff --git a/frontend-modern/src/features/proxmox/__tests__/ProxmoxNodesTable.test.tsx b/frontend-modern/src/features/proxmox/__tests__/ProxmoxNodesTable.test.tsx index ff8edcf57..002717900 100644 --- a/frontend-modern/src/features/proxmox/__tests__/ProxmoxNodesTable.test.tsx +++ b/frontend-modern/src/features/proxmox/__tests__/ProxmoxNodesTable.test.tsx @@ -295,6 +295,32 @@ describe('ProxmoxNodesTable', () => { expect(screen.getByText('pve1')).toBeInTheDocument(); }); + it('keeps the adjacent web-interface link visible and touch-sized on phones', () => { + render(() => ( + 390} + emptyIcon={} + emptyTitle="No Proxmox VE nodes" + emptyDescription="No nodes" + /> + )); + + const link = screen.getByRole('link', { name: 'Open web interface for pve-node-1' }); + expect(link).toBeVisible(); + expect(link).toHaveClass('min-h-6', 'min-w-6'); + expect(link.parentElement).not.toHaveClass('[&>a]:hidden'); + }); + it('passes alert-backed temperature thresholds into the node temperature gauge', () => { render(() => ( { expect(workloadsSurfaceSource).not.toContain( 'createMemo(() => getCanonicalWorkloadId(guest()))', ); - expect(workloadPanelSource).toContain('createMemo(() => getCanonicalWorkloadId(guest()))'); + expect(workloadPanelSource).toContain( + 'new Map(groupGuests().map((guest) => [getCanonicalWorkloadId(guest), guest]))', + ); + expect(workloadPanelSource).toContain( + 'createMemo(() => groupGuests().map(getCanonicalWorkloadId))', + ); + expect(workloadPanelSource).toContain(' keyedGuestId'); expect(workloadsWorkloadTableSource).not.toContain( 'createMemo(() => getCanonicalWorkloadId(guest()))', ); @@ -913,7 +921,15 @@ describe('frontend resource type boundaries', () => { expect(workloadTableHeaderSource).not.toContain('NodeGroupHeader'); expect(workloadPanelSource).toContain('NodeGroupHeader'); expect(workloadPanelSource).toContain('GuestDrawer'); - expect(workloadPanelSource).toContain('createMemo(() => getCanonicalWorkloadId(guest()))'); + expect(workloadPanelSource).toContain( + 'new Map(groupGuests().map((guest) => [getCanonicalWorkloadId(guest), guest]))', + ); + expect(workloadPanelSource).toContain( + 'createMemo(() => groupGuests().map(getCanonicalWorkloadId))', + ); + expect(workloadPanelSource).toContain(' keyedGuestId'); expect(workloadPanelSource).not.toContain('TableHead'); expect(emptyStateSource).toContain('getEmptyStatePresentation'); expect(emptyStateSource).not.toContain('const iconBgClass: Record ='); diff --git a/tests/integration/tests/17-proxmox-backups-layout.spec.ts b/tests/integration/tests/17-proxmox-backups-layout.spec.ts index 4cda4e5f4..7dcab7040 100644 --- a/tests/integration/tests/17-proxmox-backups-layout.spec.ts +++ b/tests/integration/tests/17-proxmox-backups-layout.spec.ts @@ -15,8 +15,13 @@ async function openProxmoxBackups(page: Page) { name: "Proxmox sections", }); await expect(sections).toBeVisible({ timeout: 60_000 }); - await sections.getByRole("link", { name: "Backups", exact: true }).click(); - await expect(page).toHaveURL(/\/proxmox\/backups$/); + const backupsLink = sections.getByRole("link", { + name: "Backups", + exact: true, + }); + await expect(backupsLink).toHaveAttribute("href", "/proxmox/backups/date"); + await backupsLink.click(); + await expect(page).toHaveURL(/\/proxmox\/backups\/date$/); } // Layout guards for the Proxmox Backups section, which replaced the retired @@ -35,13 +40,27 @@ test.describe("Proxmox backups layout guards", () => { await page.setViewportSize(DESKTOP_VIEWPORT); await ensureAuthenticated(page); + + // Existing bookmarks remain compatible, but the route is normalized to + // the canonical route-segment view before the table is used. + await page.goto("/proxmox/backups", { waitUntil: "domcontentloaded" }); + await expect(page).toHaveURL(/\/proxmox\/backups\/date$/, { + timeout: 60_000, + }); await openProxmoxBackups(page); - // The guest-centric Coverage view is the default whenever anything needs - // attention; the day-activity strip lives in the By date view. - const byDateButton = page.getByRole("button", { name: "By date" }); - await expect(byDateButton).toBeVisible({ timeout: 60_000 }); - await byDateButton.click(); + const byDateLink = page.getByRole("link", { name: "By date", exact: true }); + const coverageLink = page.getByRole("link", { + name: "Coverage", + exact: true, + }); + await expect(byDateLink).toHaveAttribute("href", "/proxmox/backups/date"); + await expect(coverageLink).toHaveAttribute( + "href", + "/proxmox/backups/coverage", + ); + await byDateLink.click(); + await expect(page).toHaveURL(/\/proxmox\/backups\/date$/); await expect(page.getByText("Backups per day").first()).toBeVisible(); const dayButtons = page.getByRole("button", { name: /: \d+ backups?$/ }); await expect.poll(() => dayButtons.count()).toBeGreaterThanOrEqual(7); @@ -54,8 +73,19 @@ test.describe("Proxmox backups layout guards", () => { .getByRole("button", { name: /: [1-9]\d* backups?$/ }) .last(); await activeDay.click(); - await expect(page).toHaveURL(/\/proxmox\/backups/); + await expect(page).toHaveURL( + /\/proxmox\/backups\/date\?day=\d{4}-\d{2}-\d{2}$/, + ); await expect(page.getByText(/^\d+ of \d+ backups$/).first()).toBeVisible(); + + const filteredScreenshotPath = testInfo.outputPath( + "proxmox-backups-by-date-filtered.png", + ); + await page.screenshot({ path: filteredScreenshotPath, fullPage: true }); + await testInfo.attach("proxmox-backups-by-date-filtered", { + path: filteredScreenshotPath, + contentType: "image/png", + }); }); test("long-range activity keeps the page inside the horizontal viewport", async ({ @@ -70,9 +100,10 @@ test.describe("Proxmox backups layout guards", () => { await ensureAuthenticated(page); await openProxmoxBackups(page); - const byDateButton = page.getByRole("button", { name: "By date" }); - await expect(byDateButton).toBeVisible({ timeout: 60_000 }); - await byDateButton.click(); + const byDateLink = page.getByRole("link", { name: "By date", exact: true }); + await expect(byDateLink).toHaveAttribute("href", "/proxmox/backups/date"); + await byDateLink.click(); + await expect(page).toHaveURL(/\/proxmox\/backups\/date$/); await page .getByRole("group", { name: "Activity range" }) @@ -112,5 +143,14 @@ test.describe("Proxmox backups layout guards", () => { expect(dedupBox!.x + dedupBox!.width).toBeLessThanOrEqual( wrapperBox!.x + wrapperBox!.width + 1, ); + + const yearScreenshotPath = testInfo.outputPath( + "proxmox-backups-one-year-layout.png", + ); + await page.screenshot({ path: yearScreenshotPath, fullPage: true }); + await testInfo.attach("proxmox-backups-one-year-layout", { + path: yearScreenshotPath, + contentType: "image/png", + }); }); }); diff --git a/tests/integration/tests/27-truenas-storage-disk-history.spec.ts b/tests/integration/tests/27-truenas-storage-disk-history.spec.ts index 12af049e0..08b493d9b 100644 --- a/tests/integration/tests/27-truenas-storage-disk-history.spec.ts +++ b/tests/integration/tests/27-truenas-storage-disk-history.spec.ts @@ -58,7 +58,10 @@ test.describe('TrueNAS storage disk history', () => { await page.goto('/truenas/storage', { waitUntil: 'domcontentloaded' }); await page - .getByRole('textbox', { name: /Search TrueNAS/ }) + .getByRole('combobox', { + name: 'Search TrueNAS pools, datasets, or disks', + exact: true, + }) .fill('sdc'); const diskRow = page.locator('tr').filter({ hasText: 'sdc' }).first(); @@ -90,5 +93,12 @@ test.describe('TrueNAS storage disk history', () => { // Serial when available, disk:: composite otherwise; both // must stay canonical metrics-store keys. expect(resourceId).toMatch(/^(WD-|disk:truenas-main:)/); + + const screenshotPath = testInfo.outputPath('truenas-disk-history.png'); + await page.screenshot({ path: screenshotPath, fullPage: true }); + await testInfo.attach('truenas-disk-history', { + path: screenshotPath, + contentType: 'image/png', + }); }); }); diff --git a/tests/integration/tests/60-page-header-consistency.spec.ts b/tests/integration/tests/60-page-header-consistency.spec.ts index ba4d630e3..5cc0809c2 100644 --- a/tests/integration/tests/60-page-header-consistency.spec.ts +++ b/tests/integration/tests/60-page-header-consistency.spec.ts @@ -29,7 +29,8 @@ const PAGE_HEADER_ROUTES = [ slug: "patrol", route: "/patrol", title: "Patrol", - description: "Patrol checks your infrastructure and shows current issues.", + description: + "See what needs a decision, choose the next step, and keep a verified record.", }, ] as const; @@ -101,7 +102,9 @@ test.describe("Top-level page header consistency", () => { }); } - test("keeps primary page headings vertically aligned", async ({ page }, testInfo) => { + test("keeps primary page headings vertically aligned", async ({ + page, + }, testInfo) => { test.skip( testInfo.project.name.startsWith("mobile-"), "Phone headers use responsive framing and intentionally do not share desktop Y coordinates", diff --git a/tests/integration/tests/65-offline-proxmox-node-visibility.spec.ts b/tests/integration/tests/65-offline-proxmox-node-visibility.spec.ts index 7e9ecce9c..ba47ac54b 100644 --- a/tests/integration/tests/65-offline-proxmox-node-visibility.spec.ts +++ b/tests/integration/tests/65-offline-proxmox-node-visibility.spec.ts @@ -115,7 +115,7 @@ test.describe("Offline Proxmox node visibility", () => { test("keeps an offline Proxmox node visible on desktop and mobile surfaces", async ({ page, - }) => { + }, testInfo) => { await page.context().route("https://pve5:8006/**", async (route) => { await route.fulfill({ status: 200, @@ -198,5 +198,14 @@ test.describe("Offline Proxmox node visibility", () => { name: "Expand details for Disaster Recovery B", }), ).toBeVisible(); + + const screenshotPath = testInfo.outputPath( + "offline-proxmox-node-visible-with-web-interface.png", + ); + await page.screenshot({ path: screenshotPath, fullPage: true }); + await testInfo.attach("offline-proxmox-node-visible-with-web-interface", { + path: screenshotPath, + contentType: "image/png", + }); }); }); diff --git a/tests/integration/tests/73-patrol-assistant-operator-briefing.spec.ts b/tests/integration/tests/73-patrol-assistant-operator-briefing.spec.ts index 2e7a2684e..4f7f32859 100644 --- a/tests/integration/tests/73-patrol-assistant-operator-briefing.spec.ts +++ b/tests/integration/tests/73-patrol-assistant-operator-briefing.spec.ts @@ -11,8 +11,6 @@ type WorkerFixtures = { authStorageStatePath: string; }; -const SCREENSHOT_PATH = "/tmp/patrol-assistant-operator-briefing.png"; - const test = base.extend<{}, WorkerFixtures>({ storageState: async ({ authStorageStatePath }, use) => { await use(authStorageStatePath); @@ -44,7 +42,7 @@ test.describe("Patrol Assistant operator briefing", () => { test("shows attention and operator-decision context in the Assistant drawer", async ({ page, - }) => { + }, testInfo) => { const approvalRequestedAt = new Date(Date.now() - 60_000).toISOString(); const approvalExpiresAt = new Date(Date.now() + 10 * 60_000).toISOString(); let includePendingApproval = true; @@ -605,9 +603,7 @@ test.describe("Patrol Assistant operator briefing", () => { await page.goto("/patrol", { waitUntil: "domcontentloaded" }); await page.getByRole("tab", { name: "Activity", exact: true }).click(); - await page - .getByRole("button", { name: /Findings and run records/ }) - .click(); + await page.getByRole("button", { name: /^Patrol records(?:$|\s)/ }).click(); await page.getByText("High CPU usage").click(); const findingReview = page.locator( @@ -634,7 +630,14 @@ test.describe("Patrol Assistant operator briefing", () => { assistantContext.getByText("systemctl restart workload.service"), ).toHaveCount(0); - await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true }); + const screenshotPath = testInfo.outputPath( + "patrol-assistant-operator-briefing.png", + ); + await page.screenshot({ path: screenshotPath, fullPage: true }); + await testInfo.attach("patrol-assistant-operator-briefing", { + path: screenshotPath, + contentType: "image/png", + }); await page.getByTitle("Pulse Assistant sessions").click(); const highCPUFollowUp = page.getByRole("option", { @@ -744,9 +747,7 @@ test.describe("Patrol Assistant operator briefing", () => { includePendingApproval = false; await page.reload({ waitUntil: "domcontentloaded" }); await page.getByRole("tab", { name: "Activity", exact: true }).click(); - await page - .getByRole("button", { name: /Findings and run records/ }) - .click(); + await page.getByRole("button", { name: /^Patrol records(?:$|\s)/ }).click(); const queuedFindingTitle = page.getByText("High CPU usage").first(); await expect(queuedFindingTitle).toBeVisible(); await queuedFindingTitle.click(); @@ -777,9 +778,7 @@ test.describe("Patrol Assistant operator briefing", () => { includeInvestigationProposedFix = true; await page.reload({ waitUntil: "domcontentloaded" }); await page.getByRole("tab", { name: "Activity", exact: true }).click(); - await page - .getByRole("button", { name: /Findings and run records/ }) - .click(); + await page.getByRole("button", { name: /^Patrol records(?:$|\s)/ }).click(); const expiredFindingTitle = page.getByText("High CPU usage").first(); await expect(expiredFindingTitle).toBeVisible(); await expiredFindingTitle.click(); diff --git a/tests/integration/tests/85-powered-off-tolerance.spec.ts b/tests/integration/tests/85-powered-off-tolerance.spec.ts index d371e0bf3..c06b49ebc 100644 --- a/tests/integration/tests/85-powered-off-tolerance.spec.ts +++ b/tests/integration/tests/85-powered-off-tolerance.spec.ts @@ -117,13 +117,13 @@ test("powered-off tolerance preserves inheritance, explicit zero, and strict val await expect(tolerance).toHaveValue(""); await expect( page.getByText( - "Blank inherits the existing policy; 0 alerts on the first stopped observation.", + "Blank inherits the existing policy. 0 alerts on the first stopped observation.", { exact: true }, ), ).toBeVisible(); - await expect(page.getByRole("combobox", { name: "Resource" })).toHaveValue( - "vm:1567", - ); + await expect( + page.getByRole("combobox", { name: "Resource", exact: true }), + ).toHaveValue("vm:1567"); await tolerance.fill("300"); await page.getByRole("button", { name: "Save defaults" }).click(); await expect( @@ -169,8 +169,10 @@ test("powered-off tolerance preserves inheritance, explicit zero, and strict val graceSeconds: 0, }); + const screenshotPath = testInfo.outputPath("powered-off-tolerance.png"); + await page.screenshot({ path: screenshotPath, fullPage: true }); await testInfo.attach("powered-off-tolerance", { - body: await page.screenshot(), + path: screenshotPath, contentType: "image/png", }); });