test: Add browser tests for the media viewing gallery and viewer (#2661)
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { FrigateEvent } from '../../src/camera-manager/frigate/types';
|
||||
import type { ResolvedMedia } from '../../src/ha/types';
|
||||
import { createFrigateEvent } from '../test-utils';
|
||||
import {
|
||||
createFrigateCameraDescription,
|
||||
FakeFrigate,
|
||||
FRIGATE_CLIENT_ID,
|
||||
getTestFrigateCameraName,
|
||||
type FrigateMediaType,
|
||||
} from './fake-frigate';
|
||||
import type { FakeHASS } from './fake-hass';
|
||||
import { CAMERA_ENTITY, createCameraHASS } from './test-utils';
|
||||
|
||||
const CAMERA_NAME = getTestFrigateCameraName(CAMERA_ENTITY);
|
||||
const OTHER_CAMERA_NAME = 'hallway';
|
||||
|
||||
// Frigate timestamps are Unix seconds. Only their order matters here.
|
||||
const EARLIER = 1000;
|
||||
const LATER = 2000;
|
||||
|
||||
interface FrigateFixture {
|
||||
hass: FakeHASS;
|
||||
frigate: FakeFrigate;
|
||||
}
|
||||
|
||||
const createFrigate = (events: FrigateEvent[]): FrigateFixture => {
|
||||
const hass = createCameraHASS([createFrigateCameraDescription()]);
|
||||
const frigate = new FakeFrigate(hass);
|
||||
frigate.setEvents(events);
|
||||
return { hass, frigate };
|
||||
};
|
||||
|
||||
const createEvent = (id: string, event?: Partial<FrigateEvent>): FrigateEvent =>
|
||||
createFrigateEvent({
|
||||
camera: CAMERA_NAME,
|
||||
id,
|
||||
start_time: EARLIER,
|
||||
end_time: LATER,
|
||||
...event,
|
||||
});
|
||||
|
||||
const queryEvents = async (
|
||||
hass: FakeHASS,
|
||||
parameters?: Record<string, unknown>,
|
||||
): Promise<FrigateEvent[]> => {
|
||||
const answer = await hass.getHASS().callWS<string>({
|
||||
type: 'frigate/events/get',
|
||||
instance_id: FRIGATE_CLIENT_ID,
|
||||
...parameters,
|
||||
});
|
||||
const events: FrigateEvent[] = JSON.parse(answer);
|
||||
return events;
|
||||
};
|
||||
|
||||
const getEventIDs = async (
|
||||
hass: FakeHASS,
|
||||
parameters?: Record<string, unknown>,
|
||||
): Promise<string[]> => (await queryEvents(hass, parameters)).map((event) => event.id);
|
||||
|
||||
const createContentID = (
|
||||
eventID: string,
|
||||
mediaType: FrigateMediaType,
|
||||
options?: { clientID?: string; camera?: string },
|
||||
): string =>
|
||||
[
|
||||
'media-source://frigate',
|
||||
options?.clientID ?? FRIGATE_CLIENT_ID,
|
||||
'event',
|
||||
mediaType,
|
||||
options?.camera ?? CAMERA_NAME,
|
||||
eventID,
|
||||
].join('/');
|
||||
|
||||
const resolveMedia = async (hass: FakeHASS, contentID: string): Promise<ResolvedMedia> =>
|
||||
await hass.getHASS().callWS<ResolvedMedia>({
|
||||
type: 'media_source/resolve_media',
|
||||
media_content_id: contentID,
|
||||
});
|
||||
|
||||
const createThumbnailPath = (eventID: string, clientID = FRIGATE_CLIENT_ID): string =>
|
||||
`/api/frigate/${clientID}/thumbnail/${eventID}`;
|
||||
|
||||
describe('FakeFrigate', () => {
|
||||
describe('requests', () => {
|
||||
it('should refuse a request meant for another instance', async () => {
|
||||
const { hass } = createFrigate([]);
|
||||
|
||||
await expect(
|
||||
hass
|
||||
.getHASS()
|
||||
.callWS({ type: 'frigate/events/get', instance_id: 'other-frigate' }),
|
||||
).rejects.toThrow('FakeFrigate was asked for another instance: other-frigate');
|
||||
|
||||
await expect(
|
||||
hass
|
||||
.getHASS()
|
||||
.callWS({ type: 'frigate/ptz/info', instance_id: 'other-frigate' }),
|
||||
).rejects.toThrow('FakeFrigate was asked for another instance: other-frigate');
|
||||
});
|
||||
|
||||
it('should refuse a request carrying something it does not read', async () => {
|
||||
const { hass } = createFrigate([]);
|
||||
|
||||
await expect(queryEvents(hass, { camera: CAMERA_NAME })).rejects.toThrow(
|
||||
'FakeFrigate was sent unknown parameters: camera',
|
||||
);
|
||||
});
|
||||
|
||||
it('should refuse a parameter of the wrong type', async () => {
|
||||
const { hass } = createFrigate([]);
|
||||
|
||||
await expect(queryEvents(hass, { cameras: CAMERA_NAME })).rejects.toThrow(
|
||||
`FakeFrigate was sent a 'cameras' that is not a list of strings: "${CAMERA_NAME}"`,
|
||||
);
|
||||
await expect(queryEvents(hass, { has_clip: 'yes' })).rejects.toThrow(
|
||||
`FakeFrigate was sent a 'has_clip' that is not true or false: "yes"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('events', () => {
|
||||
it('should return every event when nothing narrows the query', async () => {
|
||||
const { hass } = createFrigate([createEvent('one'), createEvent('two')]);
|
||||
|
||||
expect(await getEventIDs(hass)).toEqual(['one', 'two']);
|
||||
});
|
||||
|
||||
it('should return the newest event first', async () => {
|
||||
const { hass } = createFrigate([
|
||||
createEvent('older', { start_time: EARLIER }),
|
||||
createEvent('newer', { start_time: LATER }),
|
||||
]);
|
||||
|
||||
expect(await getEventIDs(hass)).toEqual(['newer', 'older']);
|
||||
});
|
||||
|
||||
it('should return only the events for the cameras asked for', async () => {
|
||||
const { hass } = createFrigate([
|
||||
createEvent('here'),
|
||||
createEvent('elsewhere', { camera: OTHER_CAMERA_NAME }),
|
||||
]);
|
||||
|
||||
expect(await getEventIDs(hass, { cameras: [CAMERA_NAME] })).toEqual(['here']);
|
||||
});
|
||||
|
||||
it('should return only the events holding the media asked for', async () => {
|
||||
const { hass } = createFrigate([
|
||||
createEvent('clip-only', { has_clip: true, has_snapshot: false }),
|
||||
createEvent('snapshot-only', { has_clip: false, has_snapshot: true }),
|
||||
]);
|
||||
|
||||
expect(await getEventIDs(hass, { has_clip: true })).toEqual(['clip-only']);
|
||||
expect(await getEventIDs(hass, { has_snapshot: true })).toEqual(['snapshot-only']);
|
||||
});
|
||||
|
||||
it('should return only the events within the period asked for', async () => {
|
||||
const { hass } = createFrigate([
|
||||
createEvent('before', { start_time: 100, end_time: 200 }),
|
||||
createEvent('during', { start_time: 400, end_time: 600 }),
|
||||
createEvent('after', { start_time: 800, end_time: 900 }),
|
||||
]);
|
||||
|
||||
expect(await getEventIDs(hass, { after: 300, before: 700 })).toEqual(['during']);
|
||||
});
|
||||
|
||||
it('should return an event that was still running when the period began', async () => {
|
||||
const { hass } = createFrigate([
|
||||
createEvent('running', { start_time: 100, end_time: 500 }),
|
||||
]);
|
||||
|
||||
expect(await getEventIDs(hass, { after: 300 })).toEqual(['running']);
|
||||
});
|
||||
|
||||
it('should return only the events for the requested label', async () => {
|
||||
const { hass } = createFrigate([
|
||||
createEvent('person', { label: 'person' }),
|
||||
createEvent('car', { label: 'car' }),
|
||||
]);
|
||||
|
||||
expect(await getEventIDs(hass, { labels: ['car'] })).toEqual(['car']);
|
||||
});
|
||||
|
||||
it('should return only the events in the requested zones', async () => {
|
||||
const { hass } = createFrigate([
|
||||
createEvent('driveway', { zones: ['driveway'] }),
|
||||
createEvent('garden', { zones: ['garden'] }),
|
||||
]);
|
||||
|
||||
expect(await getEventIDs(hass, { zones: ['garden'] })).toEqual(['garden']);
|
||||
});
|
||||
|
||||
it('should return the events carrying any of the sub labels asked for', async () => {
|
||||
// Frigate keeps several sub labels on one event as a comma separated
|
||||
// string, so asking for one of them has to reach into it.
|
||||
const { hass } = createFrigate([
|
||||
createEvent('known', { sub_label: 'alice, bob' }),
|
||||
createEvent('stranger', { sub_label: null }),
|
||||
]);
|
||||
|
||||
expect(await getEventIDs(hass, { sub_labels: ['bob'] })).toEqual(['known']);
|
||||
});
|
||||
|
||||
it('should return only the favorites', async () => {
|
||||
const { hass } = createFrigate([
|
||||
createEvent('kept', { retain_indefinitely: true }),
|
||||
createEvent('ordinary', { retain_indefinitely: false }),
|
||||
]);
|
||||
|
||||
expect(await getEventIDs(hass, { favorites: true })).toEqual(['kept']);
|
||||
});
|
||||
|
||||
it('should return no more events than the limit asked for', async () => {
|
||||
const { hass } = createFrigate([
|
||||
createEvent('older', { start_time: EARLIER }),
|
||||
createEvent('newer', { start_time: LATER }),
|
||||
]);
|
||||
|
||||
expect(await getEventIDs(hass, { limit: 1 })).toEqual(['newer']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('media', () => {
|
||||
it('should resolve a clip to a video and a snapshot to an image', async () => {
|
||||
const { hass } = createFrigate([createEvent('one')]);
|
||||
|
||||
expect(await resolveMedia(hass, createContentID('one', 'clips'))).toEqual({
|
||||
url: expect.stringContaining('clip.webm?event=one'),
|
||||
mime_type: 'video/webm',
|
||||
});
|
||||
expect(await resolveMedia(hass, createContentID('one', 'snapshots'))).toEqual({
|
||||
url: expect.stringContaining('still-red.png?event=one'),
|
||||
mime_type: 'image/png',
|
||||
});
|
||||
});
|
||||
|
||||
it('should refuse media for an event it does not have', async () => {
|
||||
const { hass } = createFrigate([createEvent('one')]);
|
||||
|
||||
await expect(resolveMedia(hass, createContentID('two', 'clips'))).rejects.toThrow(
|
||||
'FakeFrigate has no such media',
|
||||
);
|
||||
});
|
||||
|
||||
it('should refuse media the event does not have', async () => {
|
||||
const { hass } = createFrigate([createEvent('one', { has_clip: false })]);
|
||||
|
||||
await expect(resolveMedia(hass, createContentID('one', 'clips'))).rejects.toThrow(
|
||||
'FakeFrigate has no such media',
|
||||
);
|
||||
});
|
||||
|
||||
it('should refuse media asked for under the wrong camera or instance', async () => {
|
||||
const { hass } = createFrigate([createEvent('one')]);
|
||||
|
||||
await expect(
|
||||
resolveMedia(hass, createContentID('one', 'clips', { camera: 'elsewhere' })),
|
||||
).rejects.toThrow('FakeFrigate has no such media');
|
||||
await expect(
|
||||
resolveMedia(hass, createContentID('one', 'clips', { clientID: 'other' })),
|
||||
).rejects.toThrow('FakeFrigate has no such media');
|
||||
});
|
||||
|
||||
it('should honor set media URL', async () => {
|
||||
const { hass, frigate } = createFrigate([createEvent('one')]);
|
||||
|
||||
frigate.setMediaURL('one', 'clips', '/somewhere-else.webm');
|
||||
|
||||
expect((await resolveMedia(hass, createContentID('one', 'clips'))).url).toBe(
|
||||
'/somewhere-else.webm',
|
||||
);
|
||||
|
||||
// The snapshot is a separate media item and is left where it was.
|
||||
expect(
|
||||
(await resolveMedia(hass, createContentID('one', 'snapshots'))).url,
|
||||
).toContain('still-red.png');
|
||||
});
|
||||
});
|
||||
|
||||
describe('thumbnails', () => {
|
||||
it('should serve a picture for an event', async () => {
|
||||
const { hass } = createFrigate([createEvent('one')]);
|
||||
|
||||
const response = await hass.getHASS().fetchWithAuth(createThumbnailPath('one'));
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
expect(response.headers.get('Content-Type')).toContain('image/png');
|
||||
});
|
||||
|
||||
it('should refuse a thumbnail for an event it does not have', async () => {
|
||||
const { hass } = createFrigate([createEvent('one')]);
|
||||
|
||||
await expect(
|
||||
hass.getHASS().fetchWithAuth(createThumbnailPath('two')),
|
||||
).rejects.toThrow('FakeFrigate has no thumbnail at');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user