Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,11 @@ test('SessionCompletionBell shows localized environmental guidance without a rep
] as const satisfies readonly (readonly [CompletionNotificationReason, string])[]) {
const html = await renderBell({ error });
assert.match(html, new RegExp(message.replace(/[.?]/g, '\\$&')), `${error} explains what prevents notifications`);
assert.match(html, /class="max-w-48 text-xs text-destructive"/, `${error} guidance is visible`);
// Visible text in the row's action slot squeezed the open button; the guidance is a tooltip.
assert.doesNotMatch(html, /max-w-48|text-xs text-destructive/, `${error} guidance is not visible row text`);
assert.match(html, /class="sr-only" role="status"/, `${error} guidance is still announced`);
const bellTitle = /<button[^>]*title="([^"]*)"/.exec(html)?.[1] ?? '';
assert.ok(bellTitle.includes(message.replace(/'/g, '&#x27;')), `${error} guidance is the bell's tooltip`);
assert.equal((html.match(/<button/g) ?? []).length, 1, `${error} does not offer repair`);
}
});
Expand All @@ -161,6 +165,17 @@ test('SessionCompletionBell announces errors without changing pressed owner inte
}
});

test('SessionCompletionBell shows repair as a wrench with the sentence as its tooltip, never as row text', async () => {
const watchedItem = item(true);
const html = await renderBell({ item: watchedItem, target: watchedItem.target ?? null, deviceRepairRequired: true, globalPaused: true });
const repair = enSidebar.completionNotifications.repair;
assert.match(html, /lucide-wrench/);
assert.ok(html.includes(`title="${repair}" aria-label="${repair}"`), 'the wrench carries the repair sentence');
assert.doesNotMatch(html, /max-w-48|text-xs text-destructive/, 'no visible status text in the row');
assert.match(html, /class="sr-only" role="status"/);
assert.equal((html.match(/<button/g) ?? []).length, 2);
});

test('SessionCompletionBell composes paused status with concurrent error and pending state', async () => {
const watchedItem = item(true);
const pausedWithError = await renderBell({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,19 @@ export default function SessionCompletionBell({ descriptor, className }: Session
const environmentalReason = reason !== null && ENVIRONMENTAL_REASONS.includes(reason);

const statusRequiresAction = environmentalReason || repairRequired;
// Visible text here squeezed the row's open button to nothing and pushed the
// wrench out of the sidebar, so the status is only read out and shown as a
// tooltip: on the wrench when repair is required, otherwise on the bell.
const bellTitle = statusRequiresAction && !repairRequired && statusText
? `${watchLabel}\n${statusText}`
: watchLabel;

return (
<span className={cn('flex shrink-0 items-center gap-1', className)}>
{statusText && (
<span
id={statusId}
className={statusRequiresAction ? 'max-w-48 text-xs text-destructive' : 'sr-only'}
className="sr-only"
role="status"
aria-live="polite"
>
Expand All @@ -121,7 +127,7 @@ export default function SessionCompletionBell({ descriptor, className }: Session
)}
<button
type="button"
title={watchLabel}
title={bellTitle}
aria-label={watchLabel}
aria-describedby={statusText ? statusId : undefined}
aria-pressed={watched}
Expand Down