mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-03 12:47:49 +00:00
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
This commit is contained in:
parent
6794971199
commit
a7f3ae6e91
14 changed files with 211 additions and 58 deletions
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<PBSBackupsPayload>(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',
|
||||
|
|
|
|||
|
|
@ -498,7 +498,7 @@ export const ProxmoxNodesTable: Component<{
|
|||
<ResourceNameWithWebInterfaceLink
|
||||
name={name()}
|
||||
url={externalUrl()}
|
||||
class={`min-w-0 ${usesCondensedIdentity() ? '[&>a]:hidden' : ''}`}
|
||||
class="min-w-0"
|
||||
nameClass="truncate font-semibold text-base-content"
|
||||
title={`Open ${name()} web interface`}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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(() => (
|
||||
<ProxmoxBackupsTable emptyIcon={<span />} 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(
|
||||
|
|
|
|||
|
|
@ -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(() => (
|
||||
<ProxmoxNodesTable
|
||||
nodes={[
|
||||
makeNodeResource({
|
||||
proxmox: {
|
||||
clusterName: 'homelab',
|
||||
nodeName: 'pve-node-1',
|
||||
guestUrl: 'https://pve.example.com:8006',
|
||||
},
|
||||
}),
|
||||
]}
|
||||
guests={[]}
|
||||
layoutWidth={() => 390}
|
||||
emptyIcon={<span />}
|
||||
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(() => (
|
||||
<ProxmoxNodesTable
|
||||
|
|
|
|||
|
|
@ -602,7 +602,15 @@ describe('frontend resource type boundaries', () => {
|
|||
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('<For each={groupGuestIds()}');
|
||||
expect(workloadPanelSource).toContain('groupGuestById().get(keyedGuestId)');
|
||||
expect(workloadPanelSource).toContain('const guestId = () => 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('<For each={groupGuestIds()}');
|
||||
expect(workloadPanelSource).toContain('groupGuestById().get(keyedGuestId)');
|
||||
expect(workloadPanelSource).toContain('const guestId = () => keyedGuestId');
|
||||
expect(workloadPanelSource).not.toContain('TableHead');
|
||||
expect(emptyStateSource).toContain('getEmptyStatePresentation');
|
||||
expect(emptyStateSource).not.toContain('const iconBgClass: Record<EmptyStateTone, string> =');
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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:<node>:<device> 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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue