fix(e2e): preserve accessible controls and deep drawer navigation

Preserve source provenance in REST-loaded resources so owning-snapshot
consumers retain provider context and host placement. Keep visible control wording in accessible names and retain readable command
and navigation text in both themes. Token fixtures now await mounted browser
bootstrap before handing control to the next journey.

Measure bounded platform table items together with their inline details so
scrolling down a tall drawer cannot unmount the resource being inspected.
Retain regression coverage for bootstrap timing, drawer state and scroll
extent, responsive controls, Patrol and availability, and align navigation
assertions with current read-only capabilities and composed resource queries.
Provider fixtures retain one authoritative inventory across transport updates.
This commit is contained in:
rcourtman
2026-10-24 21:41:34 +01:00
parent 074c3a4743
commit 58f9398a41
42 changed files with 757 additions and 128 deletions
@@ -15,6 +15,19 @@
## Purpose
Infrastructure Add shortcuts and update chips follow the frontend primitives
visible-label naming contract. Their names preserve `Add` or the displayed
update wording and append source or host context, while keeping the existing
installer destination and affected-agent scope. An Agent Doctor destination
does not replace the update chip's visible action in its accessible name.
Agent Doctor keeps evidence at its notice tone's full text contrast. Its
scrollable repair, inspection, removal and credential commands are native
keyboard focus targets with visible focus, so long commands remain readable
without a pointer; copy actions and execution authority are unchanged.
The command-access description in the installer must remain readable against
its consent panel in both themes through the shared semantic text tokens. The
operator needs that explanation before choosing host command authority.
### Command access is a named token-step choice, not a legacy option (21 October 2026)
The installer's command-access checkbox reads **Allow Pulse to run commands on
@@ -2830,7 +2830,10 @@ thought-signature, and provider-result normalization as Assistant and MCP —
destination in `AppLayout`'s tab lists shares the nav identity-stability
contract owned by `frontend-primitives` (`stableNavTabs.ts`): a websocket
state frame with structurally unchanged tab content must not recreate the
Patrol button DOM or its attention-count badge element. Patrol and the peer
Patrol button DOM or its attention-count badge element. Utility chrome
shares the app-shell light/dark accessibility proof, including
readable small count badges, without changing their attention or capability
sources. Patrol and the peer
utility destinations commit navigation without awaiting best-effort module
warming, so lazy-route work cannot make the shared shell drop or defer a
tap.
@@ -3334,6 +3334,9 @@ a new API state machine, queue contract, or verification-accounting field.
attention projection by severity, then actionable review posture, then
observation recency, but it must preserve every API-owned lifecycle,
evidence, protection, action, and verification fact in selected detail.
Attention-row control names expose the displayed record in source order,
through the Patrol and frontend primitives visible-label contract; rendering
must not replace those facts with a title-only accessible name.
The first-party `Inbox`, `Protection`, and `Activity` modes are a mutually
exclusive presentation boundary, not API query modes: Inbox consumes the
canonical attention projection, Protection consumes retained objectives,
@@ -1072,7 +1072,10 @@ responsive shell appear to ignore an operator.
App-shell navigation rendered by `frontend-modern/src/AppLayout.tsx` must
also keep decorative icon titles out of tab accessible names, so hosted and
self-hosted chrome announce the canonical tab label plus meaningful badge
counts rather than duplicating branded SVG titles. The Patrol-owned utility
counts rather than duplicating branded SVG titles. Small navigation counts
must retain readable contrast in
both themes; changing a theme must not replace a readable critical badge
with a lighter background that fails text contrast. The Patrol-owned utility
destination remains `Patrol` over the same `/patrol` route; it must not
alter hosted org bootstrap, entitlement loading, or commercial posture
loading. Its badge may consume only the existing Patrol attention summary.
@@ -3014,6 +3014,14 @@ artifact-selection behaviour.
deterministic token authentication after setup, and may use the server setup
API as a fallback only when the UI wizard fails to complete cleanly under the
managed hot-dev runtime.
A successful token probe and a protected workspace URL do not establish
completed browser bootstrap. Token and cookie reuse must wait for the mounted
authenticated app shell before returning control to a journey, so a pending
workspace redirect cannot overwrite its next deep link. Browser rejection
remains an authentication failure rather than a reason to accept the URL or
retry login. `tests/integration/scripts/fixture-auth.test.mjs` exercises the
actual token helper with delayed and rejected bootstrap; the runtime-home
onboarding browser contract also holds a real protected-state request.
The built `index.html` must not modulepreload lazy route chunks: the SRI
plugin runs with `preloadDynamicChunks: false` so cold start fetches only the
entry and its static vendor imports (route-level code splitting stays
@@ -6487,6 +6495,13 @@ truth inside Playwright helpers. `tests/integration/tests/helpers.ts` may
offer request trackers for browser-shell contract proofs, but those helpers
must observe page-originated traffic only and must not blur browser runtime
requests together with `page.request` or other direct API helper calls.
Provider inventory fixtures must own every inventory input consumed by their
journey. `tests/integration/tests/vmware-workload-fixture.ts` serves its canonical
REST snapshot with a mocked empty socket so a live frame cannot replace its VM
identity during drawer inspection. Its resource-detail and exclusion-integrity
browser proofs exercise read-only context and retain provider-admin prohibitions.
This synthetic fixture does not establish live vCenter or websocket acceptance.
The shared authenticated browser fixture must not report the default mock
runtime ready from inventory alone while historical chart initialization is
still running. When Core E2E requests the default mock-readiness gate, the
@@ -165,7 +165,23 @@ native QGA/backup or update-permission recovery.
shared history query and Docs with synthetic observations at desktop/phone
widths, including keyboard/touch guidance and same-node updates.
### Navigation labels retain their visible badge text (24 October 2026)
### Control names retain their visible wording
Action names include their visible text in source order. Context may extend a
short label, but must not replace it with a different action name. Infrastructure
group shortcuts retain `Add` before their source label; update chips retain
their visible count and update wording before the host context. Patrol attention
rows use their native button contents so severity, title, summary and observation
age remain part of the name as the displayed record changes.
Scrollable command displays are native keyboard focus targets with visible
focus. A sibling Copy button does not make the command's own scroll area
keyboard reachable.
Semantic muted text must meet normal-text contrast on the surface and hover
backgrounds where controls and disclosures use it in either theme. Dark muted
text uses slate-300 so slate-700 hover/consent panels remain readable; individual
callers must not compensate for an inaccessible shared token. The authenticated
browser accessibility proof covers both themes and an explicitly hovered
Patrol row as well as expanded installer and Agent Doctor states.
Desktop and mobile utility links include their visible label and badge numbers
contiguously, in source order, in the accessible name (WCAG 2.5.3). Mobile
@@ -1316,6 +1332,14 @@ the mounted window advances far faster than the real scroll position and drops a
group's rows mid-scroll. The controller samples several leading siblings and
keeps the tallest, so uniform tables still measure their real row height while
mixed group/content lists keep a content-scale estimate.
Native table rows additionally measure each mounted item's summary and any
`InlineDetailTableRow` marked `data-inline-detail-row` as one logical item.
Keyed measurements own prefix offsets, native spacer extent and pixel-to-item
lookup, so scrolling within a tall drawer cannot advance past and unmount its
owner. Offscreen items retain their last measured size until remeasured.
Resize and direct-child mutation observers update those measurements outside
the scroll/wheel path. The mounted-item budget and native touch path remain
unchanged. List surfaces retain representative height estimation.
Shared workload, node, Docker-host, and resource-drawer history presentation
must scope retained observations to the exact resource type, resource ID and
@@ -15,6 +15,16 @@
## Purpose
Attention-row buttons derive their accessible names from their visible content,
following the frontend primitives control-name contract. Severity, title,
summary and relative observation age remain available to assistive technology
in display order. A title-only override must not hide the other visible label
text; selection, keyboard review and detail focus restoration keep their existing
owners.
Mobile return controls retain `Back to list` in their contextual names. The
expanded resource-copy control retains its visible `Copy ID` name, including
after copying; the fuller explanation remains its tooltip.
### One-page Patrol setup on the Patrol page
When Patrol is blocked because no provider is connected, Pulse Intelligence
@@ -1777,6 +1777,13 @@ change may globally weaken the Task 03 lifecycle-state idempotency invariant.
state, mounted-row budgets, viewport sync, and reveal timing; it must not
fork the repeated `TableRow` / `TableCell` / surface-alt / click-containment
shell inside workload, infrastructure, or platform table render paths.
Bounded platform tables use keyed measured item heights, including their
native inline details, when mapping scroll pixels to their mounted runway
and spacers. Reading a tall drawer must keep its owner mounted without
disabling windowing or increasing the row budget. Measurements update on
resize and row mutation rather than adding per-row layout reads to each
scroll event. The mounted `PlatformWindowedRows.test.tsx` regression covers
retained editing state inside a tall drawer and its offscreen scroll extent.
Pod-mode workload filters must use the workload-owned
`K8s cluster` / `All K8s clusters` labels for Kubernetes context selection,
rather than a generic `Cluster` label that can be confused with Proxmox
@@ -1884,7 +1884,8 @@
"frontend-modern/src/api/__tests__/agentProfiles.test.ts",
"frontend-modern/src/api/__tests__/monitoring.test.ts",
"frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsModel.test.tsx",
"frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts"
"frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts",
"tests/integration/tests/83-product-trust-accessibility.spec.ts"
]
},
{
@@ -4430,7 +4431,8 @@
"exact_files": [
"frontend-modern/src/__tests__/App.architecture.test.ts",
"frontend-modern/src/utils/__tests__/apiClient.issue1640.test.ts",
"frontend-modern/src/utils/__tests__/apiClient.org.test.ts"
"frontend-modern/src/utils/__tests__/apiClient.org.test.ts",
"tests/integration/tests/83-product-trust-accessibility.spec.ts"
]
},
{
@@ -4732,6 +4734,7 @@
"tests/integration/scripts/relay-mobile-token-helper.go",
"tests/integration/tests/helpers.ts",
"tests/integration/tests/runtime-defaults.ts",
"tests/integration/tests/vmware-workload-fixture.ts",
"VERSION"
],
"release_cycle_artifact_globs": [
@@ -5238,6 +5241,20 @@
"tests/integration/tests/16-dev-runtime-recovery.spec.ts"
]
},
{
"id": "browser-fixture-helpers",
"label": "authenticated browser fixture and onboarding handoff proof",
"match_prefixes": [],
"match_files": [
"tests/integration/tests/helpers.ts"
],
"allow_same_subsystem_tests": false,
"test_prefixes": [],
"exact_files": [
"tests/integration/scripts/fixture-auth.test.mjs",
"tests/integration/tests/62-runtime-home-onboarding-contract.spec.ts"
]
},
{
"id": "dev-runtime-orchestration",
"label": "dev runtime orchestration proof",
@@ -5264,7 +5281,6 @@
"tests/integration/README.md",
"tests/integration/scripts/managed-dev-runtime.mjs",
"tests/integration/scripts/managed-local-backend.mjs",
"tests/integration/tests/helpers.ts",
"tests/integration/tests/runtime-defaults.ts"
],
"allow_same_subsystem_tests": false,
@@ -5352,6 +5368,20 @@
"scripts/installtests/native_unix_contract_runner_test.go",
"scripts/tests/test_native_installer_contract_runner.py"
]
},
{
"id": "browser-provider-inventory-fixture",
"label": "authoritative browser provider inventory fixture proof",
"match_prefixes": [],
"match_files": [
"tests/integration/tests/vmware-workload-fixture.ts"
],
"allow_same_subsystem_tests": false,
"test_prefixes": [],
"exact_files": [
"tests/integration/tests/39-vmware-resource-detail-drawer.spec.ts",
"tests/integration/tests/41-vmware-phase1-exclusion-integrity.spec.ts"
]
}
],
"match_files": null
@@ -5583,7 +5613,8 @@
"test_prefixes": [],
"exact_files": [
"frontend-modern/src/__tests__/App.architecture.test.ts",
"frontend-modern/src/__tests__/AppLayout.test.tsx"
"frontend-modern/src/__tests__/AppLayout.test.tsx",
"tests/integration/tests/83-product-trust-accessibility.spec.ts"
]
},
{
@@ -6131,6 +6162,7 @@
"frontend-modern/src/components/Workloads/__tests__/DrawerDiskListCard.readings.test.tsx",
"frontend-modern/src/components/Workloads/__tests__/HostDrawer.filesystem-readings.test.tsx",
"frontend-modern/src/components/Workloads/__tests__/NodeDrawerOverview.updateEvidence.test.tsx",
"frontend-modern/src/features/platformPage/__tests__/PlatformWindowedRows.test.tsx",
"frontend-modern/src/features/proxmox/__tests__/ProxmoxNodesTable.test.tsx",
"frontend-modern/src/utils/__tests__/proxmoxUpdateEvidence.test.ts"
]
@@ -1943,6 +1943,10 @@ recovery scope, or a storage/recovery-owned secret source.
when the policy exposes them, and they must not add edition- or
license-sniffing of their own to re-suppress or re-expose paid prompts.
3. Route canonical storage/recovery resource selection through `frontend-modern/src/hooks/useUnifiedResources.ts` and the owning `unified-resources` contract
REST projection retains its validated source list on `resource.sources` as
well as platform metadata, so owning-snapshot consumers keep the same
provider context as websocket consumers. That provenance describes inventory;
it supplies no fresh storage observation, protection verdict or recovery grant.
That shared hook now also projects resource `clusterId` through the shared cluster-name helper, so storage and recovery links keep the same cluster-context label as other unified-resource consumers instead of rebuilding a local fallback chain.
That shared hook plus the adjacent websocket/store adapter path must keep
realtime transport merges canonical for storage/recovery consumers too:
@@ -23,6 +23,15 @@ and sort the complete canonical change table while startup and ingestion wait.
## Purpose
### REST snapshots retain canonical source provenance
`useUnifiedResources` preserves the validated source list on the canonical
resource as well as its platform metadata. Owning-snapshot consumers such as
Workloads derive their provider context from `resource.sources`; a REST refresh
must not erase vCenter placement, host grouping or source-specific behavior
that the websocket projection supplies. The REST mapping regression and the
authoritative VMware drawer browser fixture cover that consuming boundary.
### Drawer history follows current source authority
Resource drawers withdraw current chart fallbacks and cached live-only replies
@@ -0,0 +1,67 @@
{
"version": 1,
"base_sha": "074c3a474374b01d6a230a0ca08428de4f7cca73",
"verified_at": "2026-10-24T20:32:18.504070Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/AppLayout.tsx",
"frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx",
"frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx",
"frontend-modern/src/components/shared/InlineDetailTableRow.tsx",
"frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx",
"frontend-modern/src/features/platformPage/PlatformWindowedRows.tsx",
"frontend-modern/src/features/platformPage/usePlatformWindowedItems.ts",
"frontend-modern/src/hooks/useUnifiedResources.ts",
"frontend-modern/src/index.css"
],
"content_sha256": {
"frontend-modern/src/AppLayout.tsx": "fc905dacedc6a82ba59083ada7d0f05a9412ef2c30d75965d100adce5b3439af",
"frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx": "cbdd63b9d70ccfee5483a7b2be48da624fe8b9653c83b0620655b94e35d5dcb4",
"frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx": "4c4cca60550911d8ee0138742b00c88f04a9907aab33c9b91f08eaf3e59339a0",
"frontend-modern/src/components/shared/InlineDetailTableRow.tsx": "54eb91c75ca566ddd4f9675f77d9734adfeea543da56556cebaeb93ac6fa9616",
"frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx": "33d93ce9eb37bfa6710433d6808bcfd9721a6f9cb718b90041fb37a0453e8efe",
"frontend-modern/src/features/platformPage/PlatformWindowedRows.tsx": "6b3182ee0c925f9188717a01e997edb35b7146c9b0bc5e73a5a8f20dd9f71f05",
"frontend-modern/src/features/platformPage/usePlatformWindowedItems.ts": "83ca49dbba523d74962dd4a0551ed844c8cad2cdacb2d2b2b3b5072ace235ad5",
"frontend-modern/src/hooks/useUnifiedResources.ts": "24a2e5520cc806fa482ad6adcc258dda93dfc099b7050d340f34d28207f1da86",
"frontend-modern/src/index.css": "8cd0ad3d260d7ac40fb65ac65e5bccec9d8ea9b8a464859289b3383dafe9cced"
},
"routes": [
"/settings/infrastructure",
"/settings/infrastructure/agent-doctor?agents=<affected-agent>",
"/patrol",
"/docker/overview",
"/kubernetes/overview",
"/vmware/overview"
],
"viewports": [
{
"width": 1440,
"height": 1000
},
{
"width": 768,
"height": 1024
},
{
"width": 390,
"height": 844
},
{
"width": 1280,
"height": 720
}
],
"states": [
"Final staged formatting was applied before rebuilding production assets and the embedded runtime from parent 074c3a474374b01d6a230a0ca08428de4f7cca73; the formatter then reported zero pending changes across all fourteen staged frontend files. Private native mock runtime, canonical release metadata and Playwright 1.56.1. Current accessibility suite passed 15/15 across desktop Chromium, mobile Chromium and mobile WebKit; VMware read-only journeys passed four tests with two existing desktop-only exclusions, followed by a dedicated on-screen metadata capture. The manual matrix passed forty WCAG scans across desktop/tablet/phone light and desktop/phone dark, with a fresh page and asserted HTML theme in each case.",
"Native pixel inspection of final captures: vSphere desktop expanded VM with host grouping and on-screen vCenter/datacenter/cluster Details; update controls tablet light; installer phone dark; Agent Doctor desktop dark; Patrol desktop dark evidence and selected inbox; Docker phone light deep identity details; Kubernetes desktop dark deep identity details. Text, wrapping, focus rings, selected states, overlay placement, viewport scrolling and below-fold content remained readable and reachable.",
"Add controls retain their visible Add wording and platform context. Update chips retain visible update wording and affected-agent scope. Doctor notices retain contrast and command blocks expose keyboard scrolling. Semantic muted text and critical navigation badges retain contrast in both themes, including hovered Patrol rows.",
"Long Docker and Kubernetes inline drawers remain mounted while scrolling to and opening their Details disclosure, including phone identity/parent fields below the charts. Shared bounded row budgets and native touch scrolling remain in effect. Synthetic local proof does not establish physical device, installed agent, live provider, recovery or release qualification.",
"Authoritative REST-only VMware fixture retained its VM identity with the live socket mocked. Shared canonical source provenance reaches Workloads, preserving esxi-01.lab.local host grouping plus Lab vCenter, Primary DC and Production Cluster in the shared Details disclosure. Provider-local administration and recovery requests remain prohibited in the exclusion journey, including mobile touch navigation."
],
"interactions": [
"Focus Add Pulse Agent hosts, activate with Enter, inspect installer, dismiss with Escape and verify focus returns. Activate affected-agent update chip; verify scoped Doctor route. Focus its command block and use ArrowRight to reveal horizontal content; return to infrastructure.",
"Activate a Patrol attention item with Enter, await loaded detail, verify selected desktop row, expand Evidence and history, activate Copy ID and verify actual clipboard content. Close detail or Back to list on narrow layouts and verify focus returns to the same item. Check no page-level horizontal overflow.",
"Open first Docker container and Kubernetes pod from the actual platform table with desktop expansion button or narrow row tap. Scroll through the drawer to Details, open it, reveal Primary ID or Parent through native actionability scrolling, verify the disclosure and owning drawer stay open, inspect final pixels and run WCAG checks.",
"Expand the vSphere VM and its technical Details, inspect host placement and the canonical network identity, and verify the read-only vCenter context. Exercise hidden demo billing-route fallback without commercial requests, persisted Alerts override removal across reload and in-app cleanup navigation, and Backups metadata refresh with no viewport jump."
]
}
+1 -1
View File
@@ -879,7 +879,7 @@ export function AppLayout(props: AppLayoutProps) {
return (
<span class="inline-flex items-center gap-1">
{tab.breakdown && tab.breakdown.critical > 0 && (
<span class="inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 text-[10px] font-bold text-white bg-red-600 dark:bg-red-500 rounded-full">
<span class="inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 text-[10px] font-bold text-white bg-red-600 rounded-full">
{tab.breakdown.critical}{' '}
</span>
)}
@@ -709,7 +709,7 @@ export const InfrastructureAgentDoctorPage: Component<InfrastructureAgentDoctorP
>
<p class="font-medium">{reason.message}</p>
<Show when={reason.evidence?.length}>
<p class="mt-1 text-xs opacity-80 wrap-anywhere">
<p class="mt-1 text-xs wrap-anywhere">
{(reason.evidence ?? []).join(' · ')}
</p>
</Show>
@@ -742,7 +742,10 @@ export const InfrastructureAgentDoctorPage: Component<InfrastructureAgentDoctorP
title="Copy host-local agent repair command"
label={`Copy ${target.needsCredentialRepair ? 'authentication repair' : 'update'} command for ${target.displayName}`}
/>
<pre class="overflow-x-auto rounded-md bg-page p-3 pr-12 text-xs text-base-content">
<pre
tabIndex={0}
class="overflow-x-auto rounded-md bg-page p-3 pr-12 text-xs text-base-content focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500"
>
<code>{command()}</code>
</pre>
<Show
@@ -825,7 +828,10 @@ export const InfrastructureAgentDoctorPage: Component<InfrastructureAgentDoctorP
title="Copy host-local agent uninstall command"
label={`Copy ${entry.label} uninstall command for ${target.displayName}`}
/>
<pre class="overflow-x-auto rounded-md bg-page p-3 pr-12 text-xs text-base-content">
<pre
tabIndex={0}
class="overflow-x-auto rounded-md bg-page p-3 pr-12 text-xs text-base-content focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500"
>
<code>
{operations.getPlatformUninstallCommand(
entry.platform,
@@ -877,7 +883,10 @@ export const InfrastructureAgentDoctorPage: Component<InfrastructureAgentDoctorP
}
label="Copy safe-profile inspection command"
/>
<pre class="overflow-x-auto rounded-md bg-page p-3 pr-12 text-xs text-base-content">
<pre
tabIndex={0}
class="overflow-x-auto rounded-md bg-page p-3 pr-12 text-xs text-base-content focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500"
>
<code>
{buildSafeCollectorInspectCommand({
pulseUrl: operations.selectedAgentUrl(),
@@ -905,7 +914,10 @@ export const InfrastructureAgentDoctorPage: Component<InfrastructureAgentDoctorP
}
label="Copy safe-profile apply command"
/>
<pre class="overflow-x-auto rounded-md bg-page p-3 pr-12 text-xs text-base-content">
<pre
tabIndex={0}
class="overflow-x-auto rounded-md bg-page p-3 pr-12 text-xs text-base-content focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500"
>
<code>
{buildSafeCollectorApplyCommand({
pulseUrl: operations.selectedAgentUrl(),
@@ -975,7 +987,10 @@ export const InfrastructureAgentDoctorPage: Component<InfrastructureAgentDoctorP
onClick={() => void copyCommand(tokenFileCommand())}
label="Copy private token-file command"
/>
<pre class="overflow-x-auto rounded-md bg-page p-3 pr-12 text-xs text-base-content">
<pre
tabIndex={0}
class="overflow-x-auto rounded-md bg-page p-3 pr-12 text-xs text-base-content focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500"
>
<code>{tokenFileCommand()}</code>
</pre>
</div>
@@ -990,7 +1005,10 @@ export const InfrastructureAgentDoctorPage: Component<InfrastructureAgentDoctorP
title="Copy one-time action-runner credential"
label={`Copy action-runner credential for ${target.displayName}`}
/>
<pre class="overflow-x-auto rounded-md bg-page p-3 pr-12 text-xs text-base-content">
<pre
tabIndex={0}
class="overflow-x-auto rounded-md bg-page p-3 pr-12 text-xs text-base-content focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500"
>
<code>{reveal().token}</code>
</pre>
</div>
@@ -1004,7 +1022,10 @@ export const InfrastructureAgentDoctorPage: Component<InfrastructureAgentDoctorP
onClick={() => void copyCommand(installCommand())}
label="Copy action-runner installer command"
/>
<pre class="overflow-x-auto rounded-md bg-page p-3 pr-12 text-xs text-base-content">
<pre
tabIndex={0}
class="overflow-x-auto rounded-md bg-page p-3 pr-12 text-xs text-base-content focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500"
>
<code>{installCommand()}</code>
</pre>
</div>
@@ -940,7 +940,7 @@ export const InfrastructureSourceManager: Component<InfrastructureSourceManagerP
type="button"
onClick={() => handleGroupAddSource(group)}
class={`${addSectionButtonClass} whitespace-nowrap`}
aria-label={group.actionLabel}
aria-label={`Add ${group.label}`}
title={group.actionLabel}
>
<Plus class="h-3.5 w-3.5" />
@@ -1080,11 +1080,11 @@ export const InfrastructureSourceManager: Component<InfrastructureSourceManagerP
),
)
}
aria-label={`Open Agent Doctor for ${row.name}`}
>
{row.agentUpdateCount === 1
? 'Agent update'
: `${row.agentUpdateCount} updates`}
<span class="sr-only"> for {row.name}</span>
</button>
</Show>
</Show>
@@ -1468,7 +1468,7 @@ export const InfrastructureSourceManager: Component<InfrastructureSourceManagerP
type="button"
onClick={() => handleGroupAddSource(group)}
class={`${addSectionButtonClass} whitespace-nowrap`}
aria-label={group.actionLabel}
aria-label={`Add ${group.label}`}
title={group.actionLabel}
>
<Plus class="h-3.5 w-3.5" />
@@ -1697,11 +1697,11 @@ export const InfrastructureSourceManager: Component<InfrastructureSourceManagerP
agentConnectionIDsForInfrastructureRow(row, true),
)
}
aria-label={`Open Agent Doctor for ${row.name}`}
>
{row.agentUpdateCount === 1
? 'Agent update'
: `${row.agentUpdateCount} agent updates`}
<span class="sr-only"> for {row.name}</span>
</button>
</Show>
</Show>
@@ -137,6 +137,16 @@ describe('InfrastructureAgentDoctorPage action-runner enrollment', () => {
]),
);
expect(commands.every((command) => !command.includes('--update'))).toBe(true);
const inspection = screen
.getByRole('button', { name: 'Copy safe-profile inspection command' })
.parentElement!.querySelector('pre')!;
inspection.focus();
expect(inspection).toHaveFocus();
const apply = screen
.getByRole('button', { name: 'Copy safe-profile apply command' })
.parentElement!.querySelector('pre')!;
apply.focus();
expect(apply).toHaveFocus();
});
it('offers explicit rotation for an issued credential when the runner is disconnected', () => {
@@ -220,7 +220,7 @@ describe('InfrastructureSourceManager setup summary', () => {
'min-h-11',
'lg:min-h-9',
);
expect(screen.getByRole('button', { name: /^Install Pulse Agent$/i })).toHaveClass(
expect(screen.getByRole('button', { name: /^Add Pulse Agent hosts$/i })).toHaveClass(
'min-h-11',
'lg:min-h-0',
);
@@ -437,10 +437,43 @@ describe('InfrastructureSourceManager setup summary', () => {
/>
));
fireEvent.click(screen.getByRole('button', { name: 'Open Agent Doctor for behind-host' }));
fireEvent.click(screen.getByRole('button', { name: 'Agent update for behind-host' }));
expect(onOpenAgentDoctor).toHaveBeenCalledWith(['agent:behind-host']);
});
it.each([390, 1440])('keeps visible action wording in contextual names at %dpx', (width) => {
Object.defineProperty(window, 'innerWidth', { configurable: true, value: width });
const onAddSource = vi.fn();
const onOpenAgentDoctor = vi.fn();
render(() => (
<InfrastructureSourceManager
rows={() => [
row({
agentUpdateCount: 2,
connection: connectionFixture({ agentUpdateAvailable: true }),
}),
]}
discoveredNodes={() => []}
discoveryEnabled={false}
discoveryScanStatus={() => ({ scanning: false })}
readOnly={false}
onAddSource={onAddSource}
onOpenAgentDoctor={onOpenAgentDoctor}
/>
));
const add = screen.getByRole('button', { name: 'Add Pulse Agent hosts' });
expect(add).toHaveTextContent('Add');
fireEvent.click(add);
expect(onAddSource).toHaveBeenCalledWith('agent');
const update = screen.getByRole('button', { name: /2 (agent )?updates for host-1/ });
expect(update).toHaveAccessibleName(
width === 390 ? '2 agent updates for host-1' : '2 updates for host-1',
);
fireEvent.click(update);
expect(onOpenAgentDoctor).toHaveBeenCalledWith(['agent:host-1']);
});
it('flags an outdated agent once, as an update rather than as attention', () => {
const outdated = connectionFixture({
id: 'agent:outdated-host',
@@ -478,7 +511,7 @@ describe('InfrastructureSourceManager setup summary', () => {
));
expect(
screen.getByRole('button', { name: 'Open Agent Doctor for outdated-host' }),
screen.getByRole('button', { name: 'Agent update for outdated-host' }),
).toHaveTextContent('Agent update');
expect(screen.queryByText('Version behind')).toBeNull();
expect(screen.queryByText(/needs? attention/i)).toBeNull();
@@ -137,7 +137,7 @@ export function InlineDetailTableRow(props: InlineDetailTableRowProps) {
);
return (
<TableRow ref={detailRow} class={local.class} {...rest}>
<TableRow ref={detailRow} class={local.class} {...rest} data-inline-detail-row>
<TableCell
id={local.cellId}
colspan={effectiveColspan()}
@@ -732,7 +732,6 @@ function AttentionList(props: {
: 'hover:bg-surface-hover'
}`}
aria-pressed={props.selectedItemId === item.id}
aria-label={`Open ${displayTitle()}`}
onClick={() => props.onSelect(item.id)}
>
<div class="flex min-w-0 items-start gap-3">
@@ -956,8 +955,8 @@ function AttentionDetail(props: {
class="inline-flex min-h-11 shrink-0 items-center justify-center gap-2 rounded-sm px-2 text-sm font-medium text-muted hover:bg-surface-hover hover:text-base-content focus:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500 lg:hidden"
aria-label={
props.queueLabel === 'Handled issue'
? 'Back to handled issues'
: 'Back to attention list'
? 'Back to list of handled issues'
: 'Back to list of Patrol attention'
}
onClick={props.onClose}
>
@@ -1082,7 +1081,8 @@ function AttentionDetail(props: {
value={loaded().item.subjectResourceId}
copied={copiedResourceId() === loaded().item.subjectResourceId}
onCopyValue={copyResourceId}
label="Copy resource identifier"
label="Copy ID"
title="Copy resource identifier"
variant="ghost"
size="xs"
class="min-h-11 sm:min-h-0"
@@ -304,7 +304,10 @@ describe('PatrolAttentionWorkbench', () => {
apiMocks.getDetail.mockResolvedValue(detail(active));
try {
renderWorkbench();
const row = await screen.findByRole('button', { name: 'Open Database VM · Disk pressure' });
const row = await screen.findByRole('button', { name: /Database VM · Disk pressure/ });
expect(row).toHaveAccessibleName(
/Critical.*Database VM · Disk pressure.*The database disk is nearly full\..*Last seen 1m ago/,
);
expect(row).toHaveTextContent('Last seen 1m ago');
fireEvent.click(row);
const detailRegion = await screen.findByRole('complementary', {
@@ -316,6 +319,7 @@ describe('PatrolAttentionWorkbench', () => {
const held = vi.spyOn(patrolAttentionStore, 'load').mockResolvedValue(undefined);
vi.advanceTimersByTime(60_000);
expect(row).toHaveTextContent('Last seen 2m ago');
expect(row).toHaveAccessibleName(/Last seen 2m ago/);
expect(detailRegion).toHaveTextContent('Last seen 2m ago');
expect(detailRegion).toHaveTextContent(/first 11m ago.*latest 3m ago/);
expect(apiMocks.getList).toHaveBeenCalledOnce();
@@ -374,9 +378,7 @@ describe('PatrolAttentionWorkbench', () => {
await screen.findByRole('heading', { name: '1 decision needs you' }),
).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Start review' })).toBeInTheDocument();
expect(
screen.getByRole('button', { name: 'Open Database VM · Disk pressure' }),
).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Database VM · Disk pressure/ })).toBeInTheDocument();
expect(screen.queryByText("Today's Patrol briefing")).not.toBeInTheDocument();
expect(screen.queryByRole('heading', { name: 'Decision inbox' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Review this decision' })).not.toBeInTheDocument();
@@ -451,7 +453,9 @@ describe('PatrolAttentionWorkbench', () => {
/1 other current issue is continuing without a decision under Safe auto-fix/i,
),
).toBeInTheDocument();
expect(screen.queryByRole('button', { name: /Open Database VM/ })).not.toBeInTheDocument();
expect(
screen.queryByRole('button', { name: /Database VM · Disk pressure/ }),
).not.toBeInTheDocument();
});
it('keeps a long decision queue compact until the operator asks for all of it', async () => {
@@ -464,10 +468,10 @@ describe('PatrolAttentionWorkbench', () => {
renderWorkbench();
const showAll = await screen.findByRole('button', { name: 'Show all 7 decisions' });
expect(screen.getAllByRole('button', { name: /Open Decision/ })).toHaveLength(5);
expect(screen.getAllByRole('button', { name: /Decision/ })).toHaveLength(5);
fireEvent.click(showAll);
expect(screen.getAllByRole('button', { name: /Open Decision/ })).toHaveLength(7);
expect(screen.getAllByRole('button', { name: /Decision/ })).toHaveLength(7);
expect(screen.getByRole('button', { name: 'Show fewer decisions' })).toBeInTheDocument();
});
@@ -526,7 +530,7 @@ describe('PatrolAttentionWorkbench', () => {
renderWorkbench();
const openButton = await screen.findByRole('button', {
name: 'Open Database VM · Disk pressure',
name: /Database VM · Disk pressure/,
});
fireEvent.click(openButton);
@@ -535,9 +539,7 @@ describe('PatrolAttentionWorkbench', () => {
});
expect(within(detailRegion).getByText('Writes may fail.')).toBeInTheDocument();
expect(within(detailRegion).getByText('Evidence and history')).toBeInTheDocument();
expect(
within(detailRegion).getByRole('button', { name: 'Copy resource identifier' }),
).toBeInTheDocument();
expect(within(detailRegion).getByRole('button', { name: 'Copy ID' })).toBeInTheDocument();
expect(within(detailRegion).queryByText('pve:vm:101')).not.toBeInTheDocument();
expect(
within(detailRegion).getByText(/latest backup has not been verified/i),
@@ -545,7 +547,7 @@ describe('PatrolAttentionWorkbench', () => {
expect(within(detailRegion).getByText('Proxmox VE')).toBeInTheDocument();
expect(within(detailRegion).getByText('Observing to Open')).toBeInTheDocument();
expect(
within(detailRegion).getByRole('button', { name: 'Back to attention list' }),
within(detailRegion).getByRole('button', { name: 'Back to list of Patrol attention' }),
).toHaveTextContent('Back to list');
expect(
within(detailRegion).getByRole('button', { name: 'Close attention detail' }),
@@ -586,7 +588,7 @@ describe('PatrolAttentionWorkbench', () => {
apiMocks.getDetail.mockResolvedValue(detail(active));
renderWorkbench();
fireEvent.click(await screen.findByRole('button', { name: /^Open .*Database VM/ }));
fireEvent.click(await screen.findByRole('button', { name: /Disk pressure on Database VM/ }));
const link = await screen.findByRole('link', { name: 'Open resource' });
await waitFor(() => expect(link).toHaveAttribute('href', '/vmware/overview'));
expect(getResource).toHaveBeenCalledWith('agent-b602', expect.anything());
@@ -609,9 +611,7 @@ describe('PatrolAttentionWorkbench', () => {
try {
renderWorkbench();
fireEvent.click(
await screen.findByRole('button', { name: 'Open Database VM · Disk pressure' }),
);
fireEvent.click(await screen.findByRole('button', { name: /Database VM · Disk pressure/ }));
const detailRegion = await screen.findByRole('complementary', {
name: 'Database VM · Disk pressure',
});
@@ -654,9 +654,7 @@ describe('PatrolAttentionWorkbench', () => {
try {
renderWorkbench();
fireEvent.click(await screen.findByRole('button', { name: 'Reviewed and suppressed (1)' }));
fireEvent.click(
await screen.findByRole('button', { name: 'Open Database VM · Disk pressure' }),
);
fireEvent.click(await screen.findByRole('button', { name: /Database VM · Disk pressure/ }));
const detailRegion = await screen.findByRole('complementary', {
name: 'Database VM · Disk pressure',
});
@@ -707,14 +705,12 @@ describe('PatrolAttentionWorkbench', () => {
apiMocks.getDetail.mockResolvedValue(detail(active));
renderWorkbench();
fireEvent.click(
await screen.findByRole('button', { name: 'Open Database VM · Disk pressure' }),
);
fireEvent.click(await screen.findByRole('button', { name: /Database VM · Disk pressure/ }));
const detailRegion = await screen.findByRole('complementary', {
name: 'Database VM · Disk pressure',
});
const header = within(detailRegion)
.getByRole('button', { name: 'Back to attention list' })
.getByRole('button', { name: 'Back to list of Patrol attention' })
.closest('div.sticky');
expect(header).toHaveClass('top-0', 'bg-surface/95');
@@ -755,7 +751,7 @@ describe('PatrolAttentionWorkbench', () => {
apiMocks.getDetail.mockResolvedValue(detail(unknownTrust));
renderWorkbench();
const row = await screen.findByRole('button', { name: 'Open Database VM · Disk pressure' });
const row = await screen.findByRole('button', { name: /Database VM · Disk pressure/ });
expect(within(row).queryByText(/Evidence/)).not.toBeInTheDocument();
expect(within(row).queryByText(/timing/i)).not.toBeInTheDocument();
expect(within(row).queryByText('Unknown / Complete')).not.toBeInTheDocument();
@@ -791,7 +787,7 @@ describe('PatrolAttentionWorkbench', () => {
fireEvent.click(
await screen.findByRole('button', {
name: 'Open Database VM · Disk pressure',
name: /Database VM · Disk pressure/,
}),
);
@@ -858,7 +854,7 @@ describe('PatrolAttentionWorkbench', () => {
fireEvent.click(
await screen.findByRole('button', {
name: 'Open Database VM · Disk pressure',
name: /Database VM · Disk pressure/,
}),
);
expect(await screen.findByText('Decision 1 of 2')).toBeInTheDocument();
@@ -898,7 +894,7 @@ describe('PatrolAttentionWorkbench', () => {
fireEvent.click(
screen.getByRole('button', {
name: 'Open Database VM · Disk pressure',
name: /Database VM · Disk pressure/,
}),
);
fireEvent.click(await screen.findByRole('button', { name: 'Return to decision inbox' }));
@@ -935,7 +931,7 @@ describe('PatrolAttentionWorkbench', () => {
fireEvent.click(
await screen.findByRole('button', {
name: 'Open Database VM · Disk pressure',
name: /Database VM · Disk pressure/,
}),
);
fireEvent.click(await screen.findByRole('button', { name: 'Suppress temporarily' }));
@@ -1023,7 +1019,7 @@ describe('PatrolAttentionWorkbench', () => {
fireEvent.click(await screen.findByRole('button', { name: 'Reviewed and suppressed (1)' }));
fireEvent.click(
await screen.findByRole('button', {
name: 'Open Database VM · Disk pressure',
name: /Database VM · Disk pressure/,
}),
);
fireEvent.click(await screen.findByRole('button', { name: 'Suppress temporarily' }));
@@ -1100,7 +1096,7 @@ describe('PatrolAttentionWorkbench', () => {
fireEvent.click(await screen.findByRole('button', { name: 'Reviewed and suppressed (1)' }));
fireEvent.click(
await screen.findByRole('button', {
name: 'Open Database VM · Disk pressure',
name: /Database VM · Disk pressure/,
}),
);
const returnToAttention = await screen.findByRole('button', {
@@ -1151,7 +1147,7 @@ describe('PatrolAttentionWorkbench', () => {
fireEvent.click(await screen.findByRole('button', { name: 'Reviewed and suppressed (1)' }));
fireEvent.click(
await screen.findByRole('button', {
name: 'Open Database VM · Disk pressure',
name: /Database VM · Disk pressure/,
}),
);
fireEvent.click(await screen.findByRole('button', { name: 'End suppression' }));
@@ -1180,7 +1176,7 @@ describe('PatrolAttentionWorkbench', () => {
fireEvent.click(
await screen.findByRole('button', {
name: 'Open Database VM · Disk pressure',
name: /Database VM · Disk pressure/,
}),
);
@@ -1243,7 +1239,7 @@ describe('PatrolAttentionWorkbench', () => {
));
fireEvent.click(
await screen.findByRole('button', {
name: 'Open Database VM · Disk pressure',
name: /Database VM · Disk pressure/,
}),
);
const decisions = within(await screen.findByRole('list', { name: 'Lasting decisions' }));
@@ -1301,7 +1297,7 @@ describe('PatrolAttentionWorkbench', () => {
));
fireEvent.click(
await screen.findByRole('button', {
name: 'Open Database VM · Disk pressure',
name: /Database VM · Disk pressure/,
}),
);
@@ -1331,7 +1327,7 @@ describe('PatrolAttentionWorkbench', () => {
));
fireEvent.click(
await screen.findByRole('button', {
name: 'Open Database VM · Disk pressure',
name: /Database VM · Disk pressure/,
}),
);
@@ -1378,7 +1374,7 @@ describe('PatrolAttentionWorkbench', () => {
));
fireEvent.click(
await screen.findByRole('button', {
name: 'Open Database VM · Disk pressure',
name: /Database VM · Disk pressure/,
}),
);
@@ -1422,7 +1418,7 @@ describe('PatrolAttentionWorkbench', () => {
));
fireEvent.click(
await screen.findByRole('button', {
name: 'Open Database VM · Disk pressure',
name: /Database VM · Disk pressure/,
}),
);
@@ -1482,7 +1478,7 @@ describe('PatrolAttentionWorkbench', () => {
renderWorkbench();
const row = await screen.findByRole('button', {
name: 'Open shlink-web-clientX · Docker container state',
name: /shlink-web-clientX · Docker container state/,
});
expect(within(row).getByText('Flapping · 11 changes in 24h')).toBeInTheDocument();
fireEvent.click(row);
@@ -1586,7 +1582,7 @@ describe('PatrolAttentionWorkbench', () => {
fireEvent.click(
await screen.findByRole('button', {
name: 'Open API container is unhealthy',
name: /API container is unhealthy/,
}),
);
const trigger = await screen.findByRole('button', { name: 'Review and approve' });
@@ -129,6 +129,7 @@ export function PlatformWindowedRows<Row>(props: PlatformWindowedRowsProps<Row>)
);
const windowing = usePlatformWindowedItems({
items: renderItems,
measureTableRows: true,
estimatedItemHeight: props.estimatedRowHeight,
enableThreshold: props.enableThreshold,
windowSize: props.windowSize,
@@ -1,10 +1,11 @@
import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library';
import { createSignal, onCleanup } from 'solid-js';
import { cleanup, fireEvent, render, screen, waitFor } from '@solidjs/testing-library';
import { createSignal, onCleanup, Show } from 'solid-js';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { PlatformWindowedRows } from '../PlatformWindowedRows';
import { PlatformWindowedList } from '../PlatformWindowedList';
import platformWindowedItemsSource from '../usePlatformWindowedItems.ts?raw';
import { InlineDetailTableRow } from '@/components/shared/InlineDetailTableRow';
describe('PlatformWindowedRows', () => {
afterEach(cleanup);
@@ -220,6 +221,97 @@ describe('PlatformWindowedRows', () => {
);
});
it.each([1200, 2400])(
'keeps an expanded %ipx drawer mounted while scrolling inside it',
async (drawerHeight) => {
let scrollTop = 0;
const rect = vi
.spyOn(Element.prototype, 'getBoundingClientRect')
.mockImplementation(function (this: Element) {
const top = this.getAttribute('data-platform-window-spacer') === 'top' ? -scrollTop : 0;
const height = this.hasAttribute('data-test-drawer') ? drawerHeight : 40;
return {
x: 0,
y: top,
top,
left: 0,
right: 200,
bottom: top + height,
width: 200,
height,
toJSON: () => ({}),
} as DOMRect;
});
const height = Object.getOwnPropertyDescriptor(window, 'innerHeight');
Object.defineProperty(window, 'innerHeight', { configurable: true, value: 120 });
const visibility = (Element.prototype as Element & { checkVisibility?: () => boolean })
.checkVisibility;
(Element.prototype as Element & { checkVisibility?: () => boolean }).checkVisibility = () =>
true;
const [expanded, setExpanded] = createSignal(false);
const items = Array.from({ length: 100 }, (_, id) => ({ id }));
try {
const { container } = render(() => (
<table>
<tbody>
<PlatformWindowedRows items={() => items} windowSize={8} enableThreshold={8}>
{(item) => (
<>
<tr data-row={item.id}>
<td>Resource {item.id}</td>
</tr>
<Show when={item.id === 0 && expanded()}>
<InlineDetailTableRow data-test-drawer colSpan={1}>
<input aria-label="Drawer draft" />
</InlineDetailTableRow>
</Show>
</>
)}
</PlatformWindowedRows>
</tbody>
</table>
));
setExpanded(true);
const draft = await screen.findByRole('textbox', { name: 'Drawer draft' });
await fireEvent.input(draft, { target: { value: 'keep while reading Details' } });
scrollTop = drawerHeight - 200;
await fireEvent.scroll(window);
expect(screen.getByRole('textbox', { name: 'Drawer draft' })).toBe(draft);
expect(draft).toHaveValue('keep while reading Details');
expect(container.querySelectorAll('[data-row]')).toHaveLength(8);
// Leaving the drawer advances the bounded window, while its measured
// height remains in the native scroll extent above the mounted rows.
scrollTop = drawerHeight + 400;
await fireEvent.scroll(window);
await waitFor(() => expect(container.querySelector('[data-row="0"]')).toBeNull());
const spacer = container.querySelector<HTMLElement>('[data-platform-window-spacer="top"]')!;
expect(parseFloat(spacer.style.height)).toBeGreaterThan(drawerHeight);
expect(container.querySelectorAll('[data-row]')).toHaveLength(8);
scrollTop = 0;
await fireEvent.scroll(window);
await screen.findByRole('textbox', { name: 'Drawer draft' });
setExpanded(false);
await waitFor(() =>
expect(screen.queryByRole('textbox', { name: 'Drawer draft' })).toBeNull(),
);
scrollTop = 400;
await fireEvent.scroll(window);
await waitFor(() => expect(parseFloat(spacer.style.height)).toBeLessThan(drawerHeight));
expect(parseFloat(spacer.style.height)).toBeGreaterThan(0);
} finally {
cleanup();
rect.mockRestore();
if (height) Object.defineProperty(window, 'innerHeight', height);
if (visibility)
(Element.prototype as Element & { checkVisibility?: () => boolean }).checkVisibility =
visibility;
else Reflect.deleteProperty(Element.prototype, 'checkVisibility');
}
},
);
it('keeps estate-sized card lists within their configured mounted-item budget', () => {
const items = () => Array.from({ length: 1_000 }, (_, index) => index);
const { container } = render(() => (
@@ -28,6 +28,8 @@ export interface PlatformWindowedItemsOptions<Item> {
estimatedItemHeight?: number;
enableThreshold?: number;
windowSize?: number;
/** Native table items may include a variable-height inline detail row. */
measureTableRows?: boolean;
}
/**
@@ -52,6 +54,30 @@ export function usePlatformWindowedItems<Item>(options: PlatformWindowedItemsOpt
windowSize: resolvedWindowSize,
enableThreshold: options.enableThreshold ?? resolvedWindowSize,
});
// Measurements belong to keyed items, so sorting and leaving the mounted
// runway preserve their scroll extent. Offscreen items retain their last
// measured size until they are mounted and measured again.
const [measuredHeights, setMeasuredHeights] = createSignal<ReadonlyMap<Item, number>>(new Map());
const measuredLayout = createMemo(() => {
if (!options.measureTableRows) return undefined;
const heights = measuredHeights();
const estimate = estimatedItemHeight();
const offsets = [0];
for (const item of options.items()) {
offsets.push(offsets[offsets.length - 1]! + (heights.get(item) ?? estimate));
}
const indexAt = (offset: number) => {
let low = 0;
let high = Math.max(0, offsets.length - 2);
while (low < high) {
const middle = Math.ceil((low + high) / 2);
if (offsets[middle]! <= offset) low = middle;
else high = middle - 1;
}
return low;
};
return { offsets, indexAt };
});
const visibleItems = createMemo<readonly Item[]>(() => {
const items = options.items();
@@ -59,20 +85,55 @@ export function usePlatformWindowedItems<Item>(options: PlatformWindowedItemsOpt
return items.slice(windowing.startIndex(), windowing.endIndex());
});
const topSpacerHeight = createMemo(() =>
windowing.isWindowed() ? windowing.startIndex() * estimatedItemHeight() : 0,
windowing.isWindowed()
? (measuredLayout()?.offsets[windowing.startIndex()] ??
windowing.startIndex() * estimatedItemHeight())
: 0,
);
const bottomSpacerHeight = createMemo(() =>
windowing.isWindowed()
? Math.max(0, options.items().length - windowing.endIndex()) * estimatedItemHeight()
? measuredLayout()
? measuredLayout()!.offsets[options.items().length]! -
measuredLayout()!.offsets[windowing.endIndex()]!
: Math.max(0, options.items().length - windowing.endIndex()) * estimatedItemHeight()
: 0,
);
const measureTableRows = () => {
if (!options.measureTableRows) return;
const anchor = anchorRef();
if (!anchor || isWindowedSurfaceHidden(anchor)) return;
const items = visibleItems();
const measurements = new Map<Item, number>();
let index = -1;
let sibling = anchor.nextElementSibling;
while (sibling && !sibling.hasAttribute('data-platform-window-spacer')) {
const height = sibling.getBoundingClientRect().height;
if (!sibling.hasAttribute('data-inline-detail-row')) index += 1;
const item = items[index];
if (item !== undefined && height > 0) {
measurements.set(item, (measurements.get(item) ?? 0) + height);
}
sibling = sibling.nextElementSibling;
}
const previous = measuredHeights();
const activeItems = new Set(options.items());
const next = new Map([...previous].filter(([item]) => activeItems.has(item)));
for (const [item, height] of measurements) next.set(item, height);
if (
next.size !== previous.size ||
[...next].some(([item, height]) => previous.get(item) !== height)
) {
setMeasuredHeights(next);
}
};
const syncWindowToViewport = (projectedScrollDelta = 0, measureItems = false) => {
if (typeof window === 'undefined' || !windowing.isWindowed()) return;
const anchor = anchorRef();
if (!anchor || isWindowedSurfaceHidden(anchor)) return;
if (measureItems) {
if (measureItems && !options.measureTableRows) {
let measuredHeight = 0;
let sibling = anchor.nextElementSibling;
for (let sampled = 0; sampled < MEASUREMENT_SAMPLE_SIZE && sibling; sampled += 1) {
@@ -85,20 +146,28 @@ export function usePlatformWindowedItems<Item>(options: PlatformWindowedItemsOpt
const segmentRect = anchor.getBoundingClientRect();
const scrollContainer = findWindowedPageScrollContainer(anchor);
const updateWindow = (scrollTop: number, viewportHeight: number) => {
const layout = measuredLayout();
if (!layout) {
windowing.onScroll(scrollTop, viewportHeight, estimatedItemHeight());
return;
}
// Map native pixels into the controller's logical row coordinates. A
// viewport within a tall detail still belongs to its owning item.
const first = layout.indexAt(scrollTop);
const last = layout.indexAt(scrollTop + viewportHeight);
const estimate = estimatedItemHeight();
windowing.onScroll(first * estimate, (last - first + 1) * estimate, estimate);
};
if (scrollContainer) {
const containerRect = scrollContainer.getBoundingClientRect();
windowing.onScroll(
updateWindow(
Math.max(0, containerRect.top - segmentRect.top + projectedScrollDelta),
scrollContainer.clientHeight || window.innerHeight,
estimatedItemHeight(),
);
return;
}
windowing.onScroll(
Math.max(0, -segmentRect.top + projectedScrollDelta),
window.innerHeight,
estimatedItemHeight(),
);
updateWindow(Math.max(0, -segmentRect.top + projectedScrollDelta), window.innerHeight);
};
createEffect(() => {
@@ -117,7 +186,24 @@ export function usePlatformWindowedItems<Item>(options: PlatformWindowedItemsOpt
if (wheelEvent.deltaY === 0) return;
syncWindowToViewport(wheelDeltaInPixels(wheelEvent, viewportHeight()));
};
const handleResize = () => syncWindowToViewport(0, true);
const handleResize = () => {
measureTableRows();
syncWindowToViewport(0, true);
};
// Native disclosure, history and live-content height changes are measured
// outside the scroll/wheel hot path. Observe the owning tbody so newly
// mounted detail rows participate without consumer-specific callbacks.
let resizeObserver: ResizeObserver | undefined;
let mutationObserver: MutationObserver | undefined;
if (options.measureTableRows && anchor.parentElement) {
if (typeof ResizeObserver !== 'undefined') {
resizeObserver = new ResizeObserver(handleResize);
resizeObserver.observe(anchor.parentElement);
}
mutationObserver = new MutationObserver(handleResize);
mutationObserver.observe(anchor.parentElement, { childList: true });
}
// The initial measurement reads windowing signals the pass itself moves.
// Keep those reads outside this setup effect's dependency graph so runway
@@ -131,6 +217,10 @@ export function usePlatformWindowedItems<Item>(options: PlatformWindowedItemsOpt
onResize: handleResize,
}),
);
onCleanup(() => {
resizeObserver?.disconnect();
mutationObserver?.disconnect();
});
});
return {
@@ -2289,6 +2289,8 @@ describe('useUnifiedResources', () => {
await result!.refetch();
expect(result!.resources()[0].platformType).toBe('vmware-vsphere');
expect(result!.resources()[0].sourceType).toBe('api');
// Owning-snapshot consumers read the canonical source list directly.
expect(result!.resources()[0].sources).toEqual(['vmware']);
expect(result!.resources()[0].platformData?.sources).toEqual(['vmware']);
expect(result!.resources()[0].vmware).toMatchObject({
connectionId: 'vc-1',
@@ -846,6 +846,7 @@ const toResource = (v2: APIResource): Resource => {
platformId,
platformType,
platformScopes,
sources,
sourceType: resolveSourceTypeFromSources(sources),
parentId: v2.parentId,
parentName: v2.parentName,
+1 -1
View File
@@ -299,7 +299,7 @@
--color-border-subtle: var(--color-slate-800);
--color-border-base: var(--color-slate-700);
--color-text-base: var(--color-slate-100);
--color-text-muted: var(--color-slate-400);
--color-text-muted: var(--color-slate-300);
--color-summary-row-bg: rgba(56, 189, 248, 0.08);
--color-summary-row-bg-hover: rgba(56, 189, 248, 0.12);
--color-summary-row-border: rgba(56, 189, 248, 0.18);
@@ -1714,6 +1714,7 @@ class CanonicalCompletionGuardTest(unittest.TestCase):
"frontend-modern/src/api/__tests__/monitoring.test.ts",
"frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsModel.test.tsx",
"frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts",
"tests/integration/tests/83-product-trust-accessibility.spec.ts",
],
}
],
@@ -1831,6 +1832,7 @@ class CanonicalCompletionGuardTest(unittest.TestCase):
"frontend-modern/src/api/__tests__/monitoring.test.ts",
"frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsModel.test.tsx",
"frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts",
"tests/integration/tests/83-product-trust-accessibility.spec.ts",
],
}
],
@@ -3482,6 +3482,7 @@ class SubsystemLookupTest(unittest.TestCase):
"frontend-modern/src/api/__tests__/monitoring.test.ts",
"frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsModel.test.tsx",
"frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts",
"tests/integration/tests/83-product-trust-accessibility.spec.ts",
],
)
@@ -3584,6 +3585,7 @@ class SubsystemLookupTest(unittest.TestCase):
"frontend-modern/src/api/__tests__/monitoring.test.ts",
"frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsModel.test.tsx",
"frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts",
"tests/integration/tests/83-product-trust-accessibility.spec.ts",
],
)
@@ -230,7 +230,7 @@ test('API admission cannot hide browser rejection at the unchanged protected URL
const f = fixture(t, { browserRejectSession: true });
f.save([cookie('pulse_session', 'saved-live')]);
const page = f.createPage();
await assert.rejects(() => f.ensureAuthenticated(page), /Shared cookie session was rejected by browser bootstrap/);
await assert.rejects(() => f.ensureAuthenticated(page), /Session was rejected by browser bootstrap/);
assert.equal(page.url(), 'http://127.0.0.1:7655/infrastructure');
assert.deepEqual(page.navigations, ['/infrastructure']);
assert.deepEqual(f.bootstrapChecks, ['login']);
@@ -294,6 +294,75 @@ test('primary-token fixtures keep their token auth and import no ambient cookie
assert.equal((await page.context().cookies()).length, 0);
});
function primaryBootstrapFixture({ rejected = false } = {}) {
let currentURL = 'about:blank';
let releaseBootstrap;
const bootstrap = new Promise(resolve => { releaseBootstrap = resolve; });
let notifyWaitStarted;
const waitStarted = new Promise(resolve => { notifyWaitStarted = resolve; });
let ready = false;
const page = {
goto: async target => { currentURL = new URL(target, 'http://127.0.0.1:7655').href; },
url: () => currentURL,
waitForURL: async () => { currentURL = 'http://127.0.0.1:7655/infrastructure'; },
locator: () => ({ waitFor: () => new Promise(() => {}) }),
waitForFunction: async predicate => {
const evaluate = () => runInNewContext(`(${predicate.toString()})()`, {
document: { querySelector: selector => {
if (!ready) return null;
const present = selector === '.app-scroll-shell' ? !rejected : rejected;
return present ? { getClientRects: () => [1] } : null;
} },
getComputedStyle: () => ({ visibility: 'visible' }),
});
assert.equal(evaluate(), false, 'protected bootstrap is still loading');
notifyWaitStarted();
await bootstrap;
return { jsonValue: async () => evaluate(), dispose: async () => {} };
},
};
const authenticate = runInNewContext(stripTypeScriptTypes([
source.match(/const AUTHENTICATED_URL =[\s\S]*?;\n/)[0],
fragment('async function waitForAuthenticatedAppShell(', 'export async function waitForPulseReady('),
fragment('async function authenticateWithPrimaryAPIToken(', 'export async function ensureAuthenticated('),
'authenticateWithPrimaryAPIToken;',
].join('\n')), {
URL, encodeURIComponent,
primaryAPITokenCandidates: () => [{ token: 'fixture-token' }],
apiRequest: async () => response(200),
rememberPrimaryAPIToken: () => {},
waitForAppShell: async () => {},
});
return {
page, authenticate, waitStarted,
completeBootstrap: () => {
ready = true;
currentURL = 'http://127.0.0.1:7655/proxmox/overview';
releaseBootstrap();
},
};
}
test('an admitted token and workspace URL cannot race a subsequent onboarding navigation', async () => {
const f = primaryBootstrapFixture();
let completed = false;
const authentication = f.authenticate(f.page).then(value => { completed = true; return value; });
await Promise.race([f.waitStarted, authentication]);
assert.equal(completed, false, 'token auth must wait for the mounted authenticated shell');
f.completeBootstrap();
assert.equal(await authentication, true);
await f.page.goto('/settings/infrastructure?add=agent');
assert.equal(f.page.url(), 'http://127.0.0.1:7655/settings/infrastructure?add=agent');
});
test('a valid token probe cannot hide a rejected browser bootstrap', async () => {
const f = primaryBootstrapFixture({ rejected: true });
const authentication = f.authenticate(f.page);
await Promise.race([f.waitStarted, authentication]);
f.completeBootstrap();
await assert.rejects(authentication, /Session was rejected by browser bootstrap/);
});
test('explicit session fixtures still get independent password sessions', async t => {
const f = fixture(t, { tokenAuth: true });
f.save([cookie('pulse_session', 'saved-live')]);
+3 -2
View File
@@ -196,7 +196,7 @@ test.describe.serial('Core E2E flows', () => {
await page.reload();
if (!/\/alerts\/thresholds/.test(page.url())) {
await page.goto('/alerts/overview');
await page.goto('/alerts/overview', { waitUntil: 'domcontentloaded' });
await expect(page.getByRole('heading', { name: 'Alerts Overview' })).toBeVisible();
const toggle = page.getByRole('checkbox', {
name: 'Send notifications',
@@ -228,7 +228,8 @@ test.describe.serial('Core E2E flows', () => {
// Restore prior activation state to keep the test safe against real instances.
if (!alertsInitiallyEnabled) {
await page.goto('/alerts/overview');
await page.getByRole('button', { name: 'Overview', exact: true }).click();
await expect(page).toHaveURL(/\/alerts\/overview/);
await expect(page.getByRole('heading', { name: 'Alerts Overview' })).toBeVisible();
const alertsToggleRestore = page.getByRole('checkbox', {
name: 'Send notifications',
+5 -3
View File
@@ -300,8 +300,8 @@ test.describe("Mobile viewport flows", () => {
const table = page.locator("table.workload-table--mobile");
await expect(table).toBeVisible({ timeout: 30_000 });
await expect(table).toHaveClass(/min-w-\[0px\]/);
await expect(table.locator("xpath=..")).toHaveClass(/overflow-x-auto/);
await expect(table).toHaveCSS("min-width", "0px");
await expect(table.locator("xpath=..")).toHaveCSS("overflow-x", "auto");
});
test("utility destinations stay pinned in the fixed rail", async ({
@@ -524,7 +524,9 @@ test.describe("Mobile viewport flows", () => {
"resource-identity-section",
"resource-docker-container-section",
],
identityLabel: "Aliases",
// The native ID is a primary identity row. It is deliberately not
// repeated in Aliases after identity deduplication.
identityLabel: "Primary ID",
},
{
route: "/kubernetes",
@@ -7,7 +7,6 @@ import { createAuthenticatedStorageState } from './helpers';
import { installVmwareWorkloadResourceRoute } from './vmware-workload-fixture';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SCREENSHOT_PATH = '/tmp/vmware-resource-detail-drawer.png';
type WorkerFixtures = {
authStorageStatePath: string;
@@ -69,20 +68,24 @@ test.describe('VMware resource detail drawer', () => {
drawer.getByRole('heading', { level: 2, name: 'warehouse-api-01' }),
).toBeVisible();
// Placement context comes from the vCenter inventory, rendered read-only.
await expect(drawer.getByRole('heading', { name: 'vSphere' })).toBeVisible();
await expect(drawer.getByText('Lab vCenter', { exact: true })).toBeVisible();
await expect(drawer.getByText('Primary DC', { exact: true })).toBeVisible();
await expect(drawer.getByText('Production Cluster', { exact: true })).toBeVisible();
// Placement evidence lives in the shared technical disclosure.
const details = drawer.getByTestId('guest-technical-details');
await details.locator('summary').click();
await expect(details).toHaveAttribute('open', '');
await expect(details.getByText('vSphere', { exact: true })).toBeVisible();
await expect(details.getByText('Lab vCenter', { exact: true })).toBeVisible();
await expect(details.getByText('Primary DC', { exact: true })).toBeVisible();
await expect(details.getByText('Production Cluster', { exact: true })).toBeVisible();
// Host placement and guest identity surface alongside.
await expect(drawer.getByText('esxi-01.lab.local')).toBeVisible();
await expect(drawer.getByRole('heading', { name: 'Guest Info' })).toBeVisible();
// The guest's canonical network identity remains visible alongside.
await expect(drawer.getByText('10.42.10.121', { exact: true })).toBeVisible();
await expect(page.getByText('esxi-01.lab.local', { exact: true })).toBeVisible();
// Phase-1 VMware stays on shared read paths: the browser never talks to
// provider-local vmware endpoints for drawer context.
expect(unexpectedVmwareApiCall).toBeNull();
await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true });
await details.getByText('Lab vCenter', { exact: true }).click({ trial: true });
await page.screenshot({ path: testInfo.outputPath('vmware-resource-detail.png'), fullPage: true });
});
});
@@ -7,7 +7,6 @@ import { createAuthenticatedStorageState } from './helpers';
import { installVmwareWorkloadResourceRoute } from './vmware-workload-fixture';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SCREENSHOT_PATH = '/tmp/vmware-phase1-exclusion-integrity.png';
type WorkerFixtures = {
authStorageStatePath: string;
@@ -45,7 +44,7 @@ test.describe('VMware phase-1 exclusion integrity', () => {
test('keeps VMware resources out of recovery and provider-local admin routes', async ({
page,
}) => {
}, testInfo) => {
let unexpectedVmwareApiCall: string | null = null;
let unexpectedRecoveryApiCall: string | null = null;
@@ -74,16 +73,25 @@ test.describe('VMware phase-1 exclusion integrity', () => {
const expandButton = page.getByRole('button', { name: 'Expand warehouse-api-01' });
await expect(expandButton).toBeVisible({ timeout: 30_000 });
await expandButton.click();
if (testInfo.project.name.startsWith('mobile-')) {
// Compact rows expose a full name-cell touch target; their disclosure
// button is screen-reader-only until focused from the keyboard.
await page.locator('tr').filter({ has: expandButton }).locator('td').first().tap();
} else {
await expandButton.click();
}
const drawer = page.getByRole('region', { name: 'warehouse-api-01' });
await expect(drawer).toBeVisible();
// The action surface points at agent onboarding instead of offering
// native lifecycle controls for an API-backed VM.
await expect(
drawer.getByRole('link', { name: 'Add agent for AI actions' }),
).toBeVisible();
// Coverage is evidence in Details; API-backed guests do not expose
// the retired action-oriented onboarding link or native lifecycle controls.
const details = drawer.getByTestId('guest-technical-details');
await details.locator('summary').click();
await expect(details).toHaveAttribute('open', '');
await expect(details.getByText('Pulse coverage', { exact: true })).toBeVisible();
await expect(details.getByText('Agent recommended', { exact: true })).toBeVisible();
await expect(drawer.getByRole('link', { name: 'Add agent for AI actions' })).toHaveCount(0);
await expect(page.getByRole('link', { name: /Open related recovery/i })).toHaveCount(0);
await expect(page.getByRole('link', { name: /Open in Recovery/i })).toHaveCount(0);
@@ -94,6 +102,6 @@ test.describe('VMware phase-1 exclusion integrity', () => {
expect(unexpectedVmwareApiCall).toBeNull();
expect(unexpectedRecoveryApiCall).toBeNull();
await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true });
await page.screenshot({ path: testInfo.outputPath('vmware-exclusion-integrity.png'), fullPage: true });
});
});
@@ -332,9 +332,9 @@ base.describe('Demo mode commercial boundary', () => {
await page.goto('/settings/system/billing/usage?details=counting-rules', {
waitUntil: 'domcontentloaded',
});
// The demo shell now redirects hidden billing routes to the settings
// infrastructure root (the /install sub-route was retired).
await page.waitForURL('**/settings/infrastructure', { timeout: 15_000 });
// This read-only session has no infrastructure-management capability.
// Hidden billing routes resolve to the reachable user preferences panel.
await page.waitForURL('**/settings/system-general', { timeout: 15_000 });
await expect
.poll(() => runtimeCapabilitiesRequests, {
@@ -347,7 +347,7 @@ base.describe('Demo mode commercial boundary', () => {
await expect(
page.getByText('Demo instance with mock data (read-only)', { exact: true }),
).toBeVisible();
await expect(page.getByRole('heading', { level: 1, name: 'Infrastructure' })).toBeVisible();
await expect(page.getByRole('heading', { level: 1, name: 'General', exact: true })).toBeVisible();
const settingsNavigation = await getVisibleSettingsNavigation(
page,
testInfo.project.name,
@@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
import { test as base, expect } from "@playwright/test";
import {
createAuthenticatedStorageState,
E2E_CREDENTIALS,
ensureAuthenticated,
} from "./helpers";
@@ -106,6 +107,47 @@ test.describe("runtime-home onboarding contract", () => {
).toHaveCount(0);
});
test("waits for token bootstrap before navigating to the agent install handoff", async ({ page }) => {
await ensureAuthenticated(page);
expect(E2E_CREDENTIALS.primaryApiToken, "the runtime fixture must retain its primary token").toBeTruthy();
let tokenNavigationStarted = false;
const onNavigation = () => {
if (new URL(page.url()).searchParams.has("token")) tokenNavigationStarted = true;
};
page.on("framenavigated", onNavigation);
let releaseBootstrap!: () => void;
const bootstrapHold = new Promise<void>(resolve => { releaseBootstrap = resolve; });
let notifyBlocked!: () => void;
const blocked = new Promise<void>(resolve => { notifyBlocked = resolve; });
const holdState = async (route: import("@playwright/test").Route) => {
if (tokenNavigationStarted) {
notifyBlocked();
await bootstrapHold;
}
await route.continue();
};
await page.route("**/api/state/summary", holdState);
let authentication: Promise<void> | undefined;
try {
let completed = false;
authentication = ensureAuthenticated(page).then(() => { completed = true; });
await Promise.race([blocked, authentication]);
expect(completed, "token authentication must await protected browser bootstrap").toBe(false);
releaseBootstrap();
await authentication;
await page.goto("/settings/infrastructure?add=agent", { waitUntil: "domcontentloaded" });
await expect(page).toHaveURL(/\/settings\/infrastructure\?add=agent$/);
await expect(page.getByRole("dialog").getByRole("heading", {
name: "Add Pulse Agent", exact: true,
})).toBeVisible();
} finally {
releaseBootstrap();
await authentication?.catch(() => {});
await page.unroute("**/api/state/summary", holdState);
page.off("framenavigated", onNavigation);
}
});
test("retires legacy platform-management paths to the not-found page", async ({
page,
}) => {
@@ -484,7 +484,8 @@ test.describe.serial("Workloads Proxmox refresh stability", () => {
const url = new URL(response.url());
return (
url.pathname === "/api/resources" &&
url.searchParams.get("type") === "pbs" &&
// Backups composes provider inventory with its reporting agents.
(url.searchParams.get("type") ?? "").split(",").sort().join(",") === "agent,pbs" &&
url.searchParams.get("source") === "pbs"
);
});
@@ -749,7 +749,7 @@ test.describe("Monitor-first Patrol workbench browser contract", () => {
).toBeVisible();
await expect(
page.getByRole("button", {
name: "Open High CPU pressure on pve-main",
name: /pve-main · High CPU pressure/,
}),
).toBeVisible();
await expect(
@@ -757,11 +757,11 @@ test.describe("Monitor-first Patrol workbench browser contract", () => {
).toHaveCount(0);
await page
.getByRole("button", {
name: "Open High CPU pressure on pve-main",
name: /pve-main · High CPU pressure/,
})
.click();
const attentionDetail = page.getByRole("complementary", {
name: "High CPU pressure on pve-main",
name: "pve-main · High CPU pressure",
});
await expect(attentionDetail).toBeVisible();
await expect(
@@ -160,10 +160,16 @@ test("Actions remains named, directly reachable, keyboard accessible, and free o
});
});
test("representative authenticated surfaces have no automatically detectable WCAG A/AA violations", async ({
for (const theme of ["light", "dark"] as const) {
test(`representative authenticated surfaces have no automatically detectable WCAG A/AA violations (${theme})`, async ({
page,
}) => {
await page.emulateMedia({ reducedMotion: "reduce" });
await page.addInitScript(preference => {
localStorage.setItem("pulseThemePreference", preference);
localStorage.setItem("darkMode", String(preference === "dark"));
localStorage.removeItem("pulse_dark_mode");
}, theme);
const surfaces = [
{ route: "/alerts/overview", heading: "Alerts Overview" },
{ route: "/settings/infrastructure", heading: "Infrastructure" },
@@ -176,6 +182,10 @@ test("representative authenticated surfaces have no automatically detectable WCA
await expect(
page.getByRole("heading", { level: 1, name: surface.heading }),
).toBeVisible();
if (surface.route === "/patrol") {
await page.getByRole("list", { name: "Patrol attention items", exact: true })
.getByRole("button").first().hover();
}
expect(
await scanForWcagViolations(page),
`${surface.route} should have no automatically detectable WCAG A/AA violations`,
@@ -202,9 +212,53 @@ test("representative authenticated surfaces have no automatically detectable WCA
await page.keyboard.press("Escape");
await expect(dialog).toBeHidden();
await expect(addButton).toBeFocused();
const installAgent = page.getByRole("button", { name: "Add Pulse Agent hosts", exact: true });
await installAgent.focus();
await page.keyboard.press("Enter");
const installer = page.getByRole("dialog");
await expect(installer.getByRole("heading", { name: "Add Pulse Agent", exact: true })).toBeVisible();
expect(await scanForWcagViolations(page)).toEqual([]);
await page.keyboard.press("Escape");
await expect(installer).toBeHidden();
await expect(installAgent).toBeFocused();
await page.getByRole("button", {
name: /^(Agent update|\d+ (agent )?updates) for /,
}).first().click();
await expect(page.getByRole("heading", { name: "Agent Doctor", exact: true })).toBeVisible();
expect(new URL(page.url()).searchParams.getAll("agents").length).toBeGreaterThan(0);
const command = page.locator("pre").first();
await expect(command).toBeVisible();
await command.focus();
await expect(command).toBeFocused();
await page.keyboard.press("ArrowRight");
await expect.poll(() => command.evaluate(node => node.scrollLeft)).toBeGreaterThan(0);
expect(await scanForWcagViolations(page)).toEqual([]);
}
if (surface.route === "/patrol") {
const list = page.getByRole("list", { name: "Patrol attention items", exact: true });
const item = list.getByRole("button").first();
await item.focus();
await page.keyboard.press("Enter");
const detail = page.getByRole("complementary");
await expect(detail).toBeVisible();
await expect(detail).toHaveAttribute("aria-busy", "false");
const selectedTitle = await detail.getByRole("heading", { level: 3 }).first().innerText();
expect(await scanForWcagViolations(page)).toEqual([]);
await detail.getByText("Evidence and history", { exact: true }).click();
await expect(detail.getByRole("button", { name: "Copy ID", exact: true })).toBeVisible();
expect(await scanForWcagViolations(page)).toEqual([]);
const back = detail.getByRole("button", { name: "Back to list of Patrol attention", exact: true });
if (await back.isVisible()) await back.click();
else await detail.getByRole("button", { name: "Close attention detail", exact: true }).click();
await expect(detail).toBeHidden();
await expect(list.locator("button:focus")).toContainText(selectedTitle);
}
}
});
}
test("populated platform tables expose one named disclosure control per resource", async ({
page,
@@ -742,7 +742,7 @@ test("starts from the normal monitor shell and reaches the canonical attention q
page.getByRole("region", { name: "Patrol decision inbox" }),
).toBeVisible();
await expect(
page.getByRole("button", { name: "Open pve-main · CPU pressure" }),
page.getByRole("button", { name: /pve-main · CPU pressure/ }),
).toBeVisible();
await expect(
page.getByRole("tab", { name: "Inbox", exact: true }),
@@ -810,7 +810,7 @@ test("makes active operational work primary and preserves the evidence boundary"
).toHaveCount(0);
const itemButton = queue.getByRole("button", {
name: "Open pve-main · CPU pressure",
name: /pve-main · CPU pressure/,
});
await itemButton.focus();
await page.keyboard.press("Enter");
@@ -870,7 +870,7 @@ test("makes active operational work primary and preserves the evidence boundary"
).toBeVisible();
await expect(
attentionList.getByRole("button", {
name: "Open pve-main · CPU pressure",
name: /pve-main · CPU pressure/,
}),
).toHaveCount(0);
@@ -879,12 +879,12 @@ test("makes active operational work primary and preserves the evidence boundary"
});
const closeDetailButton =
(page.viewportSize()?.width ?? 1280) < 1024
? advancedDetail.getByRole("button", { name: "Back to attention list" })
? advancedDetail.getByRole("button", { name: "Back to list of Patrol attention" })
: advancedDetail.getByRole("button", { name: "Close attention detail" });
await closeDetailButton.click();
await expect(
attentionList.getByRole("button", {
name: "Open Connection state unknown for pve-edge",
name: /Connection state unknown for pve-edge/,
}),
).toBeFocused();
await expect(page).not.toHaveURL(/attention=/);
@@ -899,7 +899,7 @@ test("puts the selected detail in view on a phone without page overflow", async
await page.goto("/patrol", { waitUntil: "domcontentloaded" });
const itemButton = page.getByRole("button", {
name: "Open pve-main · CPU pressure",
name: /pve-main · CPU pressure/,
});
await itemButton.click();
@@ -914,7 +914,7 @@ test("puts the selected detail in view on a phone without page overflow", async
),
).toBeVisible();
const backToList = detailPanel.getByRole("button", {
name: "Back to attention list",
name: "Back to list of Patrol attention",
});
await expect(backToList).toBeVisible();
await expect(detailPanel.getByText("Decision 1 of 2")).toBeVisible();
@@ -1394,7 +1394,7 @@ for (const verification of [
await page.goto("/patrol", { waitUntil: "domcontentloaded" });
const itemButton = page.getByRole("button", {
name: "Open API container is unhealthy",
name: /API container is unhealthy/,
});
await itemButton.focus();
await page.keyboard.press("Enter");
@@ -1453,14 +1453,14 @@ for (const verification of [
if (verification === "confirmed") {
await expect(
detailPanel.getByText(/restart postcondition was confirmed/i),
detailPanel.getByText(/action postcondition was confirmed/i),
).toBeVisible();
await expect(
detailPanel.getByText(/issue stays open until fresh health evidence/i),
).toBeVisible();
} else {
await expect(
detailPanel.getByText(/restart did not satisfy its postcondition/i),
detailPanel.getByText(/action did not satisfy its postcondition/i),
).toBeVisible();
await expect(detailPanel.getByText(/issue remains open/i)).toBeVisible();
const overflows = await page.evaluate(
@@ -270,10 +270,11 @@ test.describe("Operational trust availability resource facet", () => {
hostRow.getByTitle(/TCP availability probe.*192\.0\.2\.18:8007/i).last(),
).toBeVisible();
await hostRow.click();
await hostRow.getByRole("button", { name: /^Expand details for / }).click();
const drawer = dockerPage.getByTestId("docker-host-drawer");
await expect(drawer).toBeVisible();
await drawer.locator('[data-testid="docker-host-technical-details"] > summary').click();
const cards = drawer.getByTestId("availability-probe-status");
await expect(cards).toHaveCount(2);
@@ -607,7 +608,7 @@ test.describe("Operational trust availability resource facet", () => {
!requestUrl.pathname.endsWith("/summary")
);
});
await queue.getByRole("button", { name: `Open ${item.title}` }).click();
await queue.getByRole("button").filter({ hasText: item.title }).click();
const detailResponse = await detailResponsePromise;
const routedDetail = (await detailResponse.json()) as typeof detail;
@@ -621,6 +622,7 @@ test.describe("Operational trust availability resource facet", () => {
const detailPanel = page.getByRole("complementary", { name: item.title });
await expect(detailPanel).toBeVisible();
await detailPanel.getByText("Evidence and history", { exact: true }).click();
await expect(
detailPanel.getByText("Availability", { exact: true }),
).toBeVisible();
+5 -1
View File
@@ -182,7 +182,7 @@ async function waitForAuthenticatedAppShell(page: Page, timeoutMs = 20_000) {
);
try {
if (await outcome.jsonValue() !== "authenticated") {
throw new Error("Shared cookie session was rejected by browser bootstrap");
throw new Error("Session was rejected by browser bootstrap");
}
} finally {
await outcome.dispose();
@@ -1097,6 +1097,10 @@ async function authenticateWithPrimaryAPIToken(page: Page): Promise<boolean> {
]);
if (state === "authenticated") {
// The workspace URL can appear before protected bootstrap has mounted
// the application. Its pending default-workspace redirect must finish
// before the caller navigates to a specific onboarding route.
await waitForAuthenticatedAppShell(page);
return true;
}
@@ -54,6 +54,9 @@ const VMWARE_WORKLOAD_RESOURCE = {
} as const;
export async function installVmwareWorkloadResourceRoute(page: Page): Promise<void> {
// This REST snapshot owns the test inventory. Do not let a live backend
// frame replace its VM identity while a drawer is under inspection.
await page.routeWebSocket('**/ws*', () => {});
await page.route('**/api/resources?*', async (route) => {
const url = new URL(route.request().url());
const resourceTypes = (url.searchParams.get('type') ?? '').split(',');