Initial support for grid for live and media viewer.

This commit is contained in:
Dermot Duffy
2023-08-08 22:26:25 -07:00
parent 69249b6c33
commit b663e0b731
59 changed files with 3308 additions and 429 deletions
+66
View File
@@ -0,0 +1,66 @@
import { describe, expect, it } from 'vitest';
import { ViewMediaType } from '../../src/view/media';
import { ViewMediaClassifier } from '../../src/view/media-classifier';
import { TestViewMedia } from '../test-utils';
describe('ViewMediaClassifier', () => {
describe('isEvent', () => {
it.each([
['clip' as const, true],
['snapshot' as const, true],
['recording' as const, false],
])('%s', (mediaType: ViewMediaType, expectedResult: boolean) => {
expect(
ViewMediaClassifier.isEvent(new TestViewMedia({ mediaType: mediaType })),
).toBe(expectedResult);
});
});
describe('isRecording', () => {
it.each([
['clip' as const, false],
['snapshot' as const, false],
['recording' as const, true],
])('%s', (mediaType: ViewMediaType, expectedResult: boolean) => {
expect(
ViewMediaClassifier.isRecording(new TestViewMedia({ mediaType: mediaType })),
).toBe(expectedResult);
});
});
describe('isClip', () => {
it.each([
['clip' as const, true],
['snapshot' as const, false],
['recording' as const, false],
])('%s', (mediaType: ViewMediaType, expectedResult: boolean) => {
expect(
ViewMediaClassifier.isClip(new TestViewMedia({ mediaType: mediaType })),
).toBe(expectedResult);
});
});
describe('isSnapshot', () => {
it.each([
['clip' as const, false],
['snapshot' as const, true],
['recording' as const, false],
])('%s', (mediaType: ViewMediaType, expectedResult: boolean) => {
expect(
ViewMediaClassifier.isSnapshot(new TestViewMedia({ mediaType: mediaType })),
).toBe(expectedResult);
});
});
describe('isVideo', () => {
it.each([
['clip' as const, true],
['snapshot' as const, false],
['recording' as const, true],
])('%s', (mediaType: ViewMediaType, expectedResult: boolean) => {
expect(
ViewMediaClassifier.isVideo(new TestViewMedia({ mediaType: mediaType })),
).toBe(expectedResult);
});
});
});
+233
View File
@@ -0,0 +1,233 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ViewMedia } from '../../src/view/media';
import { MediaQueriesResults } from '../../src/view/media-queries-results';
import { generateViewMediaArray } from '../test-utils';
describe('dispatchViewContextChangeEvent', () => {
beforeEach(() => {
vi.useRealTimers();
});
it('should function with empty results', () => {
const fakeNow = new Date('2023-08-07T20:44');
vi.useFakeTimers();
vi.setSystemTime(fakeNow);
const results = new MediaQueriesResults();
expect(results.isSupersetOf(results)).toBeFalsy();
expect(results.getCameraIDs()).toEqual(new Set());
expect(results.getResults()).toEqual([]);
expect(results.getResultsCount()).toEqual(0);
expect(results.hasResults()).toBeFalsy();
expect(results.getResult(0)).toBeNull();
expect(results.getSelectedIndex()).toBeNull();
expect(results.getSelectedResult()).toBeNull();
expect(results.hasSelectedResult()).toBeFalsy();
expect(results.resetSelectedResult()).toBe(results);
expect(results.getResultsTimestamp()).toEqual(fakeNow);
expect(results.selectIndex(0)).toEqual(results);
expect(results.getSelectedResult()).toBeNull();
expect(results.selectResultIfFound((_media: ViewMedia) => true)).toEqual(results);
expect(results.getSelectedResult()).toBeNull();
expect(results.selectBestResult((_media: ViewMedia[]) => null)).toEqual(results);
expect(results.getSelectedResult()).toBeNull();
});
it('should function with basic results', () => {
const testResults = generateViewMediaArray();
const results = new MediaQueriesResults({ results: testResults });
expect(results.isSupersetOf(results)).toBeTruthy();
expect(results.getCameraIDs()).toEqual(new Set(['kitchen', 'office']));
expect(results.getResults()).toEqual(testResults);
expect(results.getResultsCount()).toEqual(200);
expect(results.hasResults()).toBeTruthy();
expect(results.getResult(0)).not.toBeNull();
expect(results.getSelectedIndex()).toBe(199);
expect(results.getSelectedResult()).not.toBeNull();
expect(results.hasSelectedResult()).toBeTruthy();
expect(results.resetSelectedResult()).toBe(results);
expect(results.getSelectedResult()).toBeNull();
expect(results.selectIndex(100)).toEqual(results);
expect(results.getSelectedIndex()).toBe(100);
expect(
results.selectResultIfFound(
(media: ViewMedia) => media.getID() === 'id-kitchen-42',
),
).toEqual(results);
expect(results.getSelectedResult()?.getID()).toBe('id-kitchen-42');
expect(
results.selectBestResult((mediaArray: ViewMedia[]) =>
mediaArray.findIndex((media) => media.getID() === 'id-kitchen-43'),
),
).toEqual(results);
expect(results.getSelectedResult()?.getID()).toBe('id-kitchen-43');
});
it('should function with camera slice', () => {
const testResults = generateViewMediaArray();
const results = new MediaQueriesResults({ results: testResults });
const slice = results.getSlice('office');
expect(slice).not.toBeNull();
expect(slice!.getResults()).toEqual(
testResults.filter((media) => media.getCameraID() === 'office'),
);
expect(slice!.getResultsCount()).toEqual(100);
expect(slice!.hasResults()).toBeTruthy();
expect(slice!.getResult(0)).not.toBeNull();
expect(slice!.getResult()).toBeNull();
expect(slice!.getSelectedIndex()).toBe(99);
expect(slice!.getSelectedResult()?.getID()).toEqual('id-office-99');
expect(slice!.hasSelectedResult()).toBeTruthy();
expect(slice!.resetSelectedResult());
expect(slice!.getSelectedResult()).toBeNull();
expect(slice!.selectIndex(10));
expect(slice!.getSelectedIndex()).toBe(10);
expect(slice!.selectIndex(10000));
expect(slice!.getSelectedIndex()).toBe(10);
expect(slice!.selectIndex(-10000));
expect(slice!.getSelectedIndex()).toBe(10);
slice!.selectResultIfFound((media: ViewMedia) => media.getID() === 'id-office-42');
expect(slice!.getSelectedResult()?.getID()).toBe('id-office-42');
slice!.selectBestResult((mediaArray: ViewMedia[]) =>
mediaArray.findIndex((media) => media.getID() === 'id-office-43'),
);
expect(slice!.getSelectedResult()?.getID()).toBe('id-office-43');
});
describe('should respect select approach during construction', () => {
it.each([
['first' as const, 0],
['last' as const, 199],
])('%s', async (selectApproach, expectedIndex) => {
const results = new MediaQueriesResults({
results: generateViewMediaArray(),
selectApproach: selectApproach,
});
expect(results.getSelectedIndex()).toBe(expectedIndex);
});
});
it('should respect selectIndex during construction', () => {
const results = new MediaQueriesResults({
results: generateViewMediaArray(),
selectedIndex: 42,
});
expect(results.getSelectedIndex()).toBe(42);
});
it('should correctly clone a slice', () => {
const results = new MediaQueriesResults({
results: generateViewMediaArray(),
});
const slice = results.getSlice('office');
const clone = slice?.clone();
expect(clone?.getResults()).toBe(slice?.getResults());
expect(clone?.getSelectedIndex()).toBe(slice?.getSelectedIndex());
});
it('should not get slice for non-existent camera', () => {
const results = new MediaQueriesResults({
results: generateViewMediaArray(),
});
expect(results.getSlice('not-a-camera')).toBeNull();
});
it('should get main slice', () => {
const results = new MediaQueriesResults({
results: generateViewMediaArray(),
});
expect(results.getSlice()?.getResults()).toBe(results.getResults());
});
it('should correctly clone', () => {
const results = new MediaQueriesResults({
results: generateViewMediaArray(),
});
const clone = results.clone();
expect(results.getResultsTimestamp()).toBe(clone.getResultsTimestamp());
expect(results.getResults()).toBe(clone.getResults());
for (const cameraID of results.getCameraIDs()) {
expect(results.getSlice(cameraID)?.getResults()).toBe(
clone.getSlice(cameraID)?.getResults(),
);
}
});
it('should not getResults on invalid slice', () => {
const results = new MediaQueriesResults({
results: generateViewMediaArray(),
});
expect(results.getResults('not-a-camera')).toBeNull();
expect(results.getResultsCount('not-a-camera')).toBe(0);
expect(results.hasSelectedResult('not-a-camera')).toBeFalsy();
});
it('should always demote main selection', () => {
const results = new MediaQueriesResults({
results: generateViewMediaArray(),
});
results
.getSlice('office')
?.selectResultIfFound((media) => media.getID() === 'id-office-42');
// Verify main and office selections are as expected.
expect(results.getSelectedIndex()).toBe(199);
expect(results.getSelectedResult('office')?.getID()).toBe('id-office-42');
// Select a different main result...
results?.selectResultIfFound((media) => media.getID() === 'id-office-80');
// ... and ensure that selection has been demoted into the camera slice.
expect(results.getSelectedResult('office')?.getID()).toBe('id-office-80');
});
it('should promote camera selection', () => {
const results = new MediaQueriesResults({
results: generateViewMediaArray(),
});
results
.getSlice('office')
?.selectResultIfFound((media) => media.getID() === 'id-office-42');
expect(results.getSelectedIndex()).toBe(199);
results.promoteCameraSelectionToMainSelection('office');
expect(results.getSelectedIndex()).not.toBe(199);
expect(results.getSelectedResult()?.getID()).toBe('id-office-42');
});
it('should selectBestResult via advanced selection criteria', () => {
const results = new MediaQueriesResults({
results: generateViewMediaArray(),
});
results.selectBestResult(
(mediaArray: ViewMedia[]) => {
const index = mediaArray.findIndex((media) => media.getID()?.endsWith('-42'));
return index < 0 ? null : index;
},
{ allCameras: true },
);
expect(results.getSelectedResult('office')?.getID()).toBe('id-office-42');
expect(results.getSelectedResult('kitchen')?.getID()).toBe('id-kitchen-42');
});
});
+102
View File
@@ -0,0 +1,102 @@
import { describe, expect, it } from 'vitest';
import {
EventQuery,
PartialEventQuery,
PartialRecordingQuery,
QueryType,
RecordingQuery,
} from '../../src/camera-manager/types';
import { setify } from '../../src/utils/basic';
import { EventMediaQueries, RecordingMediaQueries } from '../../src/view/media-queries';
describe('EventMediaQueries', () => {
const createRawEventQueries = (
cameraIDs: string | Set<string>,
query?: PartialEventQuery,
): EventQuery[] => {
return [
{
type: QueryType.Event,
cameraIDs: setify(cameraIDs),
...query,
},
];
};
it('should construct', () => {
const rawQueries = createRawEventQueries('office');
const queries = new EventMediaQueries(rawQueries);
expect(queries.getQueries()).toBe(rawQueries);
});
it('should set', () => {
const rawQueries = createRawEventQueries('office');
const queries = new EventMediaQueries(rawQueries);
const newRawQueries = createRawEventQueries('kitchen');
queries.setQueries(newRawQueries);
expect(queries.getQueries()).toBe(newRawQueries);
});
it('should determine if queries exist for CameraIDs', () => {
const rawQueries = createRawEventQueries(new Set(['office', 'kitchen']));
const queries = new EventMediaQueries(rawQueries);
expect(queries.hasQueriesForCameraIDs(new Set(['office']))).toBeTruthy();
expect(queries.hasQueriesForCameraIDs(new Set(['office', 'kitchen']))).toBeTruthy();
expect(
queries.hasQueriesForCameraIDs(new Set(['office', 'front_door'])),
).toBeFalsy();
});
it('should convert to clips querys', () => {
const rawQueries = createRawEventQueries('office', { hasSnapshot: true });
const queries = new EventMediaQueries(rawQueries);
expect(queries.convertToClipsQueries().getQueries()).toEqual([
{
type: QueryType.Event,
cameraIDs: new Set(['office']),
hasClip: true,
},
]);
});
it('should convert when queries are null', () => {
const queries = new EventMediaQueries();
expect(queries.convertToClipsQueries().getQueries()).toBeNull();
});
it('should clone', () => {
const rawQueries = createRawEventQueries('office', { hasSnapshot: true });
const queries = new EventMediaQueries(rawQueries);
expect(queries.clone().getQueries()).toEqual(queries.getQueries());
});
});
describe('RecordingMediaQueries', () => {
const createRawRecordingQueries = (
cameraIDs: string | Set<string>,
query?: PartialRecordingQuery,
): RecordingQuery[] => {
return [
{
type: QueryType.Recording,
cameraIDs: setify(cameraIDs),
...query,
},
];
};
it('should construct', () => {
const rawQueries = createRawRecordingQueries('office');
const queries = new RecordingMediaQueries(rawQueries);
expect(queries.getQueries()).toBe(rawQueries);
});
it('should clone', () => {
const rawQueries = createRawRecordingQueries('office');
const queries = new RecordingMediaQueries(rawQueries);
expect(queries.clone().getQueries()).toEqual(queries.getQueries());
});
});
+60
View File
@@ -0,0 +1,60 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { VideoContentType, ViewMedia } from '../../src/view/media';
import { TestViewMedia } from '../test-utils';
describe('ViewMedia', () => {
beforeEach(() => {
vi.useRealTimers();
});
it('should construct', () => {
const media = new ViewMedia('clip', 'camera');
expect(media.getCameraID()).toBe('camera');
expect(media.getMediaType()).toBe('clip');
expect(media.getVideoContentType()).toBeNull();
expect(media.getID()).toBeNull();
expect(media.getStartTime()).toBeNull();
expect(media.getEndTime()).toBeNull();
expect(media.getUsableEndTime()).toBeNull();
expect(media.inProgress()).toBeNull();
expect(media.getContentID()).toBeNull();
expect(media.getTitle()).toBeNull();
expect(media.getThumbnail()).toBeNull();
expect(media.getTitle()).toBeNull();
expect(media.includesTime(new Date())).toBeFalsy();
expect(media.getWhere()).toBeNull();
expect(media.setFavorite(true)).toBeUndefined();
expect(media.isFavorite()).toBeNull();
});
it('should correctly determine if a media item includes a time', () => {
const media = new TestViewMedia({
startTime: new Date('2023-08-08T17:00:00'),
endTime: new Date('2023-08-08T17:59:59'),
});
expect(media.includesTime(new Date('2023-08-08T17:30:30'))).toBeTruthy();
expect(media.includesTime(new Date('2023-08-08T18:00:00'))).toBeFalsy();
});
it('should correctly get usable end time for in-progress event', () => {
const media = new TestViewMedia({
startTime: new Date('2023-08-08T17:00:00'),
inProgress: true,
});
vi.useFakeTimers();
const fakeNow = new Date('2023-08-08T17:15:00');
vi.setSystemTime(fakeNow);
expect(media.getUsableEndTime()).toEqual(fakeNow)
});
});
describe('VideoContentType', () => {
it('MP4', () => {
expect(VideoContentType.MP4).toBe('mp4');
});
it('HLS', () => {
expect(VideoContentType.HLS).toBe('hls');
});
});
+87 -7
View File
@@ -1,10 +1,10 @@
import { describe, expect, it, test, vi } from 'vitest';
import { describe, expect, it, vi } from 'vitest';
import { QueryType } from '../../src/camera-manager/types';
import { ViewMedia } from '../../src/view/media';
import { EventMediaQueries, RecordingMediaQueries } from '../../src/view/media-queries';
import { MediaQueriesResults } from '../../src/view/media-queries-results';
import { View, dispatchViewContextChangeEvent } from '../../src/view/view';
import { createView } from '../test-utils';
import { createView, generateViewMediaArray } from '../test-utils';
// @vitest-environment jsdom
describe('View Basics', () => {
@@ -52,6 +52,7 @@ describe('View Basics', () => {
query: new EventMediaQueries(),
queryResults: new MediaQueriesResults(),
context: {},
displayMode: 'single',
});
const evolved = view.evolve({
@@ -60,12 +61,14 @@ describe('View Basics', () => {
query: new EventMediaQueries(),
queryResults: new MediaQueriesResults(),
context: {},
displayMode: 'grid',
});
expect(evolved.view).not.toBe(view.view);
expect(evolved.camera).not.toBe(view.camera);
expect(evolved.query).not.toBe(view.query);
expect(evolved.queryResults).not.toBe(view.queryResults);
expect(evolved.context).not.toBe(view.context);
expect(evolved.displayMode).not.toBe(view.displayMode);
});
it('should evolve with nothing set', () => {
@@ -119,6 +122,13 @@ describe('View Basics', () => {
expect(view.context).toEqual({});
});
it('should remove context property', () => {
const view = createView({ context: { live: { overrides: new Map() } } });
view.removeContextProperty('live', 'overrides');
expect(view.context).toEqual({ live: {} });
});
it('should detect gallery views', () => {
expect(createView({ view: 'clips' }).isGalleryView()).toBeTruthy();
expect(createView({ view: 'snapshots' }).isGalleryView()).toBeTruthy();
@@ -240,8 +250,8 @@ describe('View.isMajorMediaChange', () => {
it('should consider result change as major in other view', () => {
const media = [new ViewMedia('clip', 'camera-1'), new ViewMedia('clip', 'camera-2')];
const queryResults_1 = new MediaQueriesResults(media, 0);
const queryResults_2 = new MediaQueriesResults(media, 1);
const queryResults_1 = new MediaQueriesResults({ results: media, selectedIndex: 0 });
const queryResults_2 = new MediaQueriesResults({ results: media, selectedIndex: 1 });
expect(
View.isMajorMediaChange(
createView({ view: 'media', queryResults: queryResults_1 }),
@@ -252,8 +262,8 @@ describe('View.isMajorMediaChange', () => {
it('should not consider selected result change as major in live view', () => {
const media = [new ViewMedia('clip', 'camera-1'), new ViewMedia('clip', 'camera-2')];
const queryResults_1 = new MediaQueriesResults(media, 0);
const queryResults_2 = new MediaQueriesResults(media, 1);
const queryResults_1 = new MediaQueriesResults({ results: media, selectedIndex: 0 });
const queryResults_2 = new MediaQueriesResults({ results: media, selectedIndex: 1 });
expect(
View.isMajorMediaChange(
createView({ queryResults: queryResults_1 }),
@@ -282,7 +292,7 @@ describe('View.adoptFromViewIfAppropriate', () => {
expect(next.queryResults).toBe(queryResults);
});
test.each([
it.each([
[
new EventMediaQueries([
{ type: QueryType.Event, cameraIDs: new Set(['camera']), hasClip: true },
@@ -409,6 +419,76 @@ describe('View.adoptFromViewIfAppropriate', () => {
View.adoptFromViewIfAppropriate(next, current);
expect(next.context?.live).toEqual(current.context?.live);
});
it('should determine if display mode is grid', () => {
expect(createView({ displayMode: 'grid' }).isGrid()).toBeTruthy();
expect(createView({ displayMode: 'single' }).isGrid()).toBeFalsy();
expect(createView().isGrid()).toBeFalsy();
});
it('should determine if view supports multiple display modes', () => {
const resultsOne = new MediaQueriesResults({
results: generateViewMediaArray({ cameraIDs: ['office'] }),
});
const resultsTwo = new MediaQueriesResults({
results: generateViewMediaArray({ cameraIDs: ['office', 'kitchen'] }),
});
expect(createView({ view: 'live' }).hasMultipleDisplayModes()).toBeFalsy();
expect(createView({ view: 'live' }).hasMultipleDisplayModes(0)).toBeFalsy();
expect(createView({ view: 'live' }).hasMultipleDisplayModes(1)).toBeFalsy();
expect(createView({ view: 'live' }).hasMultipleDisplayModes(2)).toBeTruthy();
expect(createView({ view: 'media' }).hasMultipleDisplayModes()).toBeFalsy();
expect(
createView({ view: 'media', queryResults: resultsOne }).hasMultipleDisplayModes(),
).toBeFalsy();
expect(
createView({ view: 'media', queryResults: resultsTwo }).hasMultipleDisplayModes(),
).toBeTruthy();
expect(createView({ view: 'clip' }).hasMultipleDisplayModes()).toBeFalsy();
expect(
createView({ view: 'clip', queryResults: resultsOne }).hasMultipleDisplayModes(),
).toBeFalsy();
expect(
createView({ view: 'clip', queryResults: resultsTwo }).hasMultipleDisplayModes(),
).toBeTruthy();
expect(createView({ view: 'snapshot' }).hasMultipleDisplayModes()).toBeFalsy();
expect(
createView({
view: 'snapshot',
queryResults: resultsOne,
}).hasMultipleDisplayModes(),
).toBeFalsy();
expect(
createView({
view: 'snapshot',
queryResults: resultsTwo,
}).hasMultipleDisplayModes(),
).toBeTruthy();
expect(createView({ view: 'recording' }).hasMultipleDisplayModes()).toBeFalsy();
expect(
createView({
view: 'recording',
queryResults: resultsOne,
}).hasMultipleDisplayModes(),
).toBeFalsy();
expect(
createView({
view: 'recording',
queryResults: resultsTwo,
}).hasMultipleDisplayModes(),
).toBeTruthy();
expect(createView({ view: 'clips' }).hasMultipleDisplayModes()).toBeFalsy();
expect(createView({ view: 'snapshots' }).hasMultipleDisplayModes()).toBeFalsy();
expect(createView({ view: 'recordings' }).hasMultipleDisplayModes()).toBeFalsy();
expect(createView({ view: 'image' }).hasMultipleDisplayModes()).toBeFalsy();
expect(createView({ view: 'timeline' }).hasMultipleDisplayModes()).toBeFalsy();
});
});
// @vitest-environment jsdom