From d14efbfa4bc471b21901c4a472ae1a299bc0176e Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Sat, 24 Oct 2026 19:50:47 +0100 Subject: [PATCH] Keep visible Infrastructure and Patrol text in accessible action names Name Add shortcuts by their action and group, retain update counts and affected systems, and let Patrol rows expose their full visible evidence. Preserve scoped handlers, read-only restrictions and native selection; keep the WCAG rule unchanged. Contract-Neutral: Accessible-name correction only at unchanged agent-lifecycle and API boundaries: no enrollment, update execution, API payload, authorisation, state or route changes. Frontend and Patrol contracts and registered consuming tests are updated. Change-source: pulse-maintainer --- .../subsystems/frontend-primitives.md | 13 ++++ .../subsystems/patrol-intelligence.md | 10 +++ .../Settings/InfrastructureSourceManager.tsx | 8 +-- .../InfrastructureSourceManager.test.tsx | 66 ++++++++++++++++- .../patrol/PatrolAttentionWorkbench.tsx | 1 - .../PatrolAttentionWorkbench.test.tsx | 71 +++++++++---------- 6 files changed, 125 insertions(+), 44 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 76c5f97463..166de986ed 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -20,6 +20,19 @@ ## Purpose +### Visible action text remains in accessible names (24 October 2026) + +Infrastructure section shortcuts include their visible Add action and group +identity in the accessible name. Update chips include the visible singular or +counted update label, the affected system and Agent Doctor destination, at both +responsive layouts. Read-only inventories expose no add/update action. Patrol +attention rows derive their name from their visible severity, title, summary, +decision/resource context and observation time rather than a title-only override. +Native buttons, pressed state, focus and touch selection remain unchanged. +Registered InfrastructureSourceManager and PatrolAttentionWorkbench tests cover +scoped actions, counts, read-only rendering and time-varying accessible content; +the consuming WCAG scan and its rules remain enforced. + ### Agent first-report status Machines, resource detail drawers and agent connection rows use the shared diff --git a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md index 0ddcde083b..a2f27c2eae 100644 --- a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md +++ b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md @@ -15,6 +15,16 @@ ## Purpose +### Attention selection retains visible evidence in its name (24 October 2026) + +Attention list buttons use their visible contents as the accessible name instead +of replacing severity, summary, decision state and Last seen evidence with an +Open-title label. Resource-leading titles, flapping/handled badges, pressed state +and native keyboard/touch selection are preserved. The registered workbench +suite checks the full accessible name, including changes to relative time, +and existing review/detail/focus behaviour. No provider, action execution, +authorisation or attention-state semantics change. + ### One-page Patrol setup on the Patrol page When Patrol is blocked because no provider is connected, Pulse Intelligence diff --git a/frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx b/frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx index 07cd5f93bb..277a3d31a6 100644 --- a/frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx +++ b/frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx @@ -940,7 +940,7 @@ export const InfrastructureSourceManager: Component handleGroupAddSource(group)} class={`${addSectionButtonClass} whitespace-nowrap`} - aria-label={group.actionLabel} + aria-label={`Add ${group.label}`} title={group.actionLabel} > @@ -1080,7 +1080,7 @@ export const InfrastructureSourceManager: Component {row.agentUpdateCount === 1 ? 'Agent update' @@ -1468,7 +1468,7 @@ export const InfrastructureSourceManager: Component handleGroupAddSource(group)} class={`${addSectionButtonClass} whitespace-nowrap`} - aria-label={group.actionLabel} + aria-label={`Add ${group.label}`} title={group.actionLabel} > @@ -1697,7 +1697,7 @@ export const InfrastructureSourceManager: Component {row.agentUpdateCount === 1 ? 'Agent update' diff --git a/frontend-modern/src/components/Settings/__tests__/InfrastructureSourceManager.test.tsx b/frontend-modern/src/components/Settings/__tests__/InfrastructureSourceManager.test.tsx index abe2f56b04..25e7b2278a 100644 --- a/frontend-modern/src/components/Settings/__tests__/InfrastructureSourceManager.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/InfrastructureSourceManager.test.tsx @@ -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', ); @@ -229,6 +229,64 @@ describe('InfrastructureSourceManager setup summary', () => { ); }); + it.each([1440, 390])('keeps visible Add and update labels in scoped names at %ipx', (width) => { + Object.defineProperty(window, 'innerWidth', { configurable: true, value: width }); + const onAddSource = vi.fn(); + const onOpenAgentDoctor = vi.fn(); + const outdated = connectionFixture({ id: 'agent:outdated', agentUpdateAvailable: true }); + const second = connectionFixture({ id: 'agent:second', agentUpdateAvailable: true }); + const current = connectionFixture({ id: 'agent:current', agentUpdateAvailable: false }); + const rows = [ + row({ + id: 'fleet', + name: 'Fleet alpha', + agentUpdateCount: 2, + connection: outdated, + attachedConnections: [second, current], + }), + ]; + const view = render(() => ( + rows} + 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'); + expect(add).toHaveAttribute('title', 'Install Pulse Agent'); + fireEvent.click(add); + expect(onAddSource).toHaveBeenCalledExactlyOnceWith('agent'); + const label = width < 1024 ? '2 agent updates' : '2 updates'; + const update = screen.getByRole('button', { + name: `${label}: open Agent Doctor for Fleet alpha`, + }); + expect(update).toHaveTextContent(label); + fireEvent.click(update); + expect(onOpenAgentDoctor).toHaveBeenCalledExactlyOnceWith(['agent:outdated', 'agent:second']); + view.unmount(); + render(() => ( + rows} + discoveredNodes={() => []} + discoveryEnabled={false} + discoveryScanStatus={() => ({ scanning: false })} + readOnly + onAddSource={onAddSource} + onOpenAgentDoctor={onOpenAgentDoctor} + /> + )); + expect(screen.queryByRole('button', { name: 'Add Pulse Agent hosts' })).not.toBeInTheDocument(); + expect( + screen.queryByRole('button', { name: /open Agent Doctor for Fleet alpha/ }), + ).not.toBeInTheDocument(); + expect(screen.getByText(label)).toBeInTheDocument(); + }); + it('keeps manual discovery observable while a scan is running', () => { const onRunDiscovery = vi.fn(); @@ -437,7 +495,9 @@ describe('InfrastructureSourceManager setup summary', () => { /> )); - fireEvent.click(screen.getByRole('button', { name: 'Open Agent Doctor for behind-host' })); + fireEvent.click( + screen.getByRole('button', { name: 'Agent update: open Agent Doctor for behind-host' }), + ); expect(onOpenAgentDoctor).toHaveBeenCalledWith(['agent:behind-host']); }); @@ -478,7 +538,7 @@ describe('InfrastructureSourceManager setup summary', () => { )); expect( - screen.getByRole('button', { name: 'Open Agent Doctor for outdated-host' }), + screen.getByRole('button', { name: 'Agent update: open Agent Doctor for outdated-host' }), ).toHaveTextContent('Agent update'); expect(screen.queryByText('Version behind')).toBeNull(); expect(screen.queryByText(/needs? attention/i)).toBeNull(); diff --git a/frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx b/frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx index 5463aa0abb..4e211e4c44 100644 --- a/frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx +++ b/frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx @@ -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)} >
diff --git a/frontend-modern/src/features/patrol/__tests__/PatrolAttentionWorkbench.test.tsx b/frontend-modern/src/features/patrol/__tests__/PatrolAttentionWorkbench.test.tsx index 3357415d91..6b60d58022 100644 --- a/frontend-modern/src/features/patrol/__tests__/PatrolAttentionWorkbench.test.tsx +++ b/frontend-modern/src/features/patrol/__tests__/PatrolAttentionWorkbench.test.tsx @@ -304,8 +304,11 @@ describe('PatrolAttentionWorkbench', () => { apiMocks.getDetail.mockResolvedValue(detail(active)); try { renderWorkbench(); - const row = await screen.findByRole('button', { name: 'Open Database VM · Disk pressure' }); - expect(row).toHaveTextContent('Last seen 1m ago'); + const row = await screen.findByRole('button', { name: /Database VM · Disk pressure/ }); + expect(row).not.toHaveAttribute('aria-label'); + expect(row).toHaveAccessibleName( + 'Critical Flapping · 11 changes in 24h Database VM · Disk pressure The database disk is nearly full. Last seen 1m ago', + ); fireEvent.click(row); const detailRegion = await screen.findByRole('complementary', { name: 'Database VM · Disk pressure', @@ -315,7 +318,9 @@ describe('PatrolAttentionWorkbench', () => { expect(detailRegion).toHaveTextContent(/first 10m ago.*latest 2m ago/); const held = vi.spyOn(patrolAttentionStore, 'load').mockResolvedValue(undefined); vi.advanceTimersByTime(60_000); - expect(row).toHaveTextContent('Last seen 2m ago'); + expect(row).toHaveAccessibleName( + 'Critical Flapping · 11 changes in 24h Database VM · Disk pressure The database disk is nearly full. 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 +379,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 +454,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 +469,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 \d/ })).toHaveLength(5); fireEvent.click(showAll); - expect(screen.getAllByRole('button', { name: /Open Decision/ })).toHaveLength(7); + expect(screen.getAllByRole('button', { name: /Decision \d/ })).toHaveLength(7); expect(screen.getByRole('button', { name: 'Show fewer decisions' })).toBeInTheDocument(); }); @@ -526,7 +531,7 @@ describe('PatrolAttentionWorkbench', () => { renderWorkbench(); const openButton = await screen.findByRole('button', { - name: 'Open Database VM · Disk pressure', + name: /Database VM · Disk pressure/, }); fireEvent.click(openButton); @@ -586,7 +591,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: /Database VM · Disk pressure/ })); 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 +614,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 +657,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,9 +708,7 @@ 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', }); @@ -755,7 +754,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 +790,7 @@ describe('PatrolAttentionWorkbench', () => { fireEvent.click( await screen.findByRole('button', { - name: 'Open Database VM · Disk pressure', + name: /Database VM · Disk pressure/, }), ); @@ -858,7 +857,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 +897,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 +934,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 +1022,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 +1099,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 +1150,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 +1179,7 @@ describe('PatrolAttentionWorkbench', () => { fireEvent.click( await screen.findByRole('button', { - name: 'Open Database VM · Disk pressure', + name: /Database VM · Disk pressure/, }), ); @@ -1243,7 +1242,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 +1300,7 @@ describe('PatrolAttentionWorkbench', () => { )); fireEvent.click( await screen.findByRole('button', { - name: 'Open Database VM · Disk pressure', + name: /Database VM · Disk pressure/, }), ); @@ -1331,7 +1330,7 @@ describe('PatrolAttentionWorkbench', () => { )); fireEvent.click( await screen.findByRole('button', { - name: 'Open Database VM · Disk pressure', + name: /Database VM · Disk pressure/, }), ); @@ -1378,7 +1377,7 @@ describe('PatrolAttentionWorkbench', () => { )); fireEvent.click( await screen.findByRole('button', { - name: 'Open Database VM · Disk pressure', + name: /Database VM · Disk pressure/, }), ); @@ -1422,7 +1421,7 @@ describe('PatrolAttentionWorkbench', () => { )); fireEvent.click( await screen.findByRole('button', { - name: 'Open Database VM · Disk pressure', + name: /Database VM · Disk pressure/, }), ); @@ -1482,7 +1481,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 +1585,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' });