Files
advanced-camera-card/tests/components/viewer/carousel.browser.test.ts
T
2026-08-07 16:27:28 -07:00

196 lines
6.8 KiB
TypeScript

import { assert, describe, expect, it, onTestFinished, vi } from 'vitest';
import { MEDIA_LOADING_TIMEOUT_SECONDS } from '../../../src/components-lib/media-load-watchdog-controller';
import type { PartialAdvancedCameraCardConfig } from '../../../src/config/types';
import { clickElement, deepQuery } from '../../browser/dom';
import {
createTestFrigateEvent,
EVENT_TIME_NEWER,
EVENT_TIME_OLDER,
mountCardWithFrigate,
} from '../../browser/fake-frigate';
import type { MountedCard } from '../../browser/mounted-card';
import { createFailingMediaURL, useTestMedia } from '../../browser/test-media';
import {
clickNextPreviousMedia,
clickThumbnail,
getMediaViewerMediaURLs,
getSelectedThumbnail,
getStatusBarItem,
getStatusBarStrings,
getThumbnails,
waitForMediaViewerMedia,
waitForThumbnails,
} from '../../browser/test-utils';
const EVENTS = [
createTestFrigateEvent('older', EVENT_TIME_OLDER),
createTestFrigateEvent('newer', EVENT_TIME_NEWER),
];
// What the status bar calls a media failure, which is where the viewer shows
// the issue: the media it cannot show is still on screen behind the carousel.
const MEDIA_ISSUE_TITLE = 'Media unavailable';
// The thumbnail carousel sits in a drawer by default, which is harder to click.
const CONFIG_THUMBNAILS_BELOW: PartialAdvancedCameraCardConfig = {
media_viewer: { controls: { thumbnails: { mode: 'below' } } },
};
interface ViewerOptions {
config?: PartialAdvancedCameraCardConfig;
// Which gallery thumbnail to open the viewer on. The newest event is first.
thumbnail?: number;
}
const mountViewer = async (
events = EVENTS,
options?: ViewerOptions,
): Promise<MountedCard> => {
const { card } = await mountCardWithFrigate(events, {
view: { default: 'clips' },
...options?.config,
});
await waitForThumbnails(card, events.length);
await clickThumbnail(card.card, options?.thumbnail ?? 0);
await card.events.waitForFirst('advanced-camera-card:media:loaded');
return card;
};
// One test here has a clip that never loads, which is served from within the
// page rather than by the dev server.
useTestMedia();
describe('AdvancedCameraCardViewerCarousel', () => {
it('should play the clip that was opened', async () => {
const card = await mountViewer();
expect(getMediaViewerMediaURLs(card.card)).toEqual([
expect.stringContaining('clip.webm?event=newer'),
]);
expect(deepQuery(card.card, 'advanced-camera-card-video-player')).not.toBeNull();
});
it('should show a snapshot as a still image', async () => {
const card = await mountViewer(EVENTS, {
config: { view: { default: 'snapshots' } },
});
expect(getMediaViewerMediaURLs(card.card)).toEqual([
expect.stringContaining('still-red.png?event=newer'),
]);
expect(deepQuery(card.card, 'advanced-camera-card-image-player')).not.toBeNull();
expect(deepQuery(card.card, 'advanced-camera-card-video-player')).toBeNull();
});
it('should show the next media when the next control is used', async () => {
// Opened on the older event, so there is a newer one to move on to.
const card = await mountViewer(EVENTS, { thumbnail: 1 });
await clickNextPreviousMedia(card.card, 'right');
await waitForMediaViewerMedia(card, 'clip.webm?event=newer');
expect(getSelectedThumbnail(card.card)).toBe(getThumbnails(card.card)[1]);
});
it('should show the media selected in the thumbnail carousel', async () => {
const card = await mountViewer(EVENTS, { config: CONFIG_THUMBNAILS_BELOW });
// The viewer runs oldest first (gallery is the opposite). This is the oldest.
await clickThumbnail(card.card, 0);
await waitForMediaViewerMedia(card, 'clip.webm?event=older');
expect(getSelectedThumbnail(card.card)).toBe(getThumbnails(card.card)[0]);
});
it('should name the media being viewed in the status bar', async () => {
const card = await mountViewer(
[
createTestFrigateEvent('older', EVENT_TIME_OLDER, { label: 'car' }),
createTestFrigateEvent('newer', EVENT_TIME_NEWER, { label: 'person' }),
],
{ config: { status_bar: { style: 'outside' }, ...CONFIG_THUMBNAILS_BELOW } },
);
expect(getStatusBarStrings(card.card).join(' ')).toContain('Person');
await clickThumbnail(card.card, 0);
await waitForMediaViewerMedia(card, 'clip.webm?event=older');
expect(getStatusBarStrings(card.card).join(' ')).toContain('Car');
});
it('should play the clip when a snapshot is clicked', async () => {
const card = await mountViewer(EVENTS, {
config: { view: { default: 'snapshots' } },
});
const snapshot = deepQuery(card.card, 'advanced-camera-card-image-player');
assert(snapshot);
await clickElement(snapshot);
await waitForMediaViewerMedia(card, 'clip.webm?event=newer');
});
it('should pause the media that is moved away from', async () => {
// Opened on the older event, so there is a newer one to move on to.
const card = await mountViewer(EVENTS, { thumbnail: 1 });
// `auto_play` covers the selected media, so the clip starts on its own.
await card.events.waitForFirst('advanced-camera-card:media:play');
await clickNextPreviousMedia(card.card, 'right');
// Leaving a clip playing behind the one on screen would have two videos
// running at once, and the user hearing the one they cannot see.
await card.events.waitForFirst('advanced-camera-card:media:pause');
});
it('should report media that cannot be loaded', async () => {
vi.useFakeTimers();
onTestFinished(() => {
vi.useRealTimers();
});
const { card, frigate } = await mountCardWithFrigate(EVENTS, {
view: { default: 'clips' },
status_bar: { style: 'outside' },
});
frigate.setMediaURL('newer', 'clips', createFailingMediaURL());
await waitForThumbnails(card, EVENTS.length);
await clickThumbnail(card.card, 0);
// The card starts its load timer when the player is rendered, so waiting for
// the player means the timer is running and the clock can be jumped.
await card.waitForSelector('video');
// A clip that refuses to load says nothing of its own, so the card has only
// silence to go on and triggers an issue once it has waited long enough.
await card.advanceSeconds(MEDIA_LOADING_TIMEOUT_SECONDS);
await card.waitForRender(
() => getStatusBarItem(card.card, MEDIA_ISSUE_TITLE),
`the ${MEDIA_ISSUE_TITLE} issue being reported`,
);
});
it('should return to the gallery from the viewer', async () => {
const card = await mountViewer(EVENTS, {
config: {
// The clips button is hidden by default.
menu: { style: 'outside', buttons: { clips: { enabled: true } } },
},
});
await card.clickControl('Clips gallery');
await card.waitForSelector('advanced-camera-card-gallery');
expect(deepQuery(card.card, 'advanced-camera-card-viewer-carousel')).toBeNull();
});
});