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
This commit is contained in:
pulse-triage[bot]
2026-10-24 19:50:47 +01:00
parent 806b9ac516
commit d14efbfa4b
6 changed files with 125 additions and 44 deletions
@@ -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
@@ -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
@@ -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,7 +1080,7 @@ export const InfrastructureSourceManager: Component<InfrastructureSourceManagerP
),
)
}
aria-label={`Open Agent Doctor for ${row.name}`}
aria-label={`${row.agentUpdateCount === 1 ? 'Agent update' : `${row.agentUpdateCount} updates`}: open Agent Doctor for ${row.name}`}
>
{row.agentUpdateCount === 1
? 'Agent update'
@@ -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,7 +1697,7 @@ export const InfrastructureSourceManager: Component<InfrastructureSourceManagerP
agentConnectionIDsForInfrastructureRow(row, true),
)
}
aria-label={`Open Agent Doctor for ${row.name}`}
aria-label={`${row.agentUpdateCount === 1 ? 'Agent update' : `${row.agentUpdateCount} agent updates`}: open Agent Doctor for ${row.name}`}
>
{row.agentUpdateCount === 1
? 'Agent update'
@@ -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(() => (
<InfrastructureSourceManager
rows={() => 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(() => (
<InfrastructureSourceManager
rows={() => 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();
@@ -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">
@@ -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' });