feat: Surface the provider's error cause in the media_unavailable notification (#2599)

- Closes: #2592
This commit is contained in:
Dermot Duffy
2026-07-22 20:45:49 -07:00
committed by GitHub
parent 5a549c0326
commit 448fa2d9f1
18 changed files with 255 additions and 90 deletions
@@ -431,6 +431,29 @@ describe('MediaUnavailableIssue', () => {
]);
});
it('should drop a stale pending-timer target once its timer has stopped', () => {
const issue = new MediaUnavailableIssue(createAPI());
// A slow load arms the pending timer for camera.garden.
issue.detectDynamic({ targetID: 'camera.garden', view: 'live' });
// The view moves to a different target that already has a hard error.
// That path activates immediately and stops the timer, but the stale
// _timerTargetID (camera.garden) lingers.
issue.trigger({ targetID: 'camera.office', reason: 'playback_error' });
issue.detectDynamic({ targetID: 'camera.office', view: 'live' });
// Only the real error shows; the stale, no-longer-running pending target
// must not paint a "not loading" line.
const notification = issue.getNotification();
expect(notification.metadata).not.toContainEqual(
expect.objectContaining({ text: 'camera.garden: Media not loading' }),
);
expect(notification.metadata).toEqual([
expect.objectContaining({ text: 'camera.office: Playback error' }),
]);
});
it('should use camera title when available', () => {
const api = createAPI();
vi.mocked(api.getCameraManager().getCameraMetadata).mockReturnValue({
@@ -471,6 +494,37 @@ describe('MediaUnavailableIssue', () => {
]);
});
it('should render the free-text cause as context, keyed by camera title', () => {
const api = createAPI();
vi.mocked(api.getCameraManager().getCameraMetadata).mockReturnValue({
title: 'Office',
icon: { icon: 'mdi:cctv' },
});
const issue = new MediaUnavailableIssue(api);
issue.trigger({
targetID: 'camera.office',
reason: 'playback_error',
description: 'Failed to start WebRTC stream: no candidates',
});
const notification = issue.getNotification();
// The metadata line stays scannable; the long cause sits below it.
expect(notification.metadata).toEqual([
expect.objectContaining({ text: 'Office: Playback error' }),
]);
expect(notification.context).toEqual([
'Office: Failed to start WebRTC stream: no candidates',
]);
});
it('should omit context for targets without a free-text cause', () => {
const issue = new MediaUnavailableIssue(createAPI());
issue.trigger({ targetID: 'camera.office', reason: 'stalled' });
expect(issue.getNotification().context).toBeUndefined();
});
it('should include a retry control with wired callback', async () => {
const api = createCardAPI();
const issue = new MediaUnavailableIssue(api);