feat: Add support for Frigate reviews / detections [initial PR] (#2315)

- Add support for Frigate reviews / detections.
 - Add support for GenAI metadata.
- Significant internal refactor to more flexible "UnifiedQuery" to allow
mixing cameras with simple metadata and review metadata (e.g. a timeline
view of a Frigate camera with reviews, and a Reolink camera with simple
metadata).
 - Add support for folder media as camera media.

There are a few more PRs to commit prior to this going live, but
commiting this for now due to the scale of the change.

BREAKING CHANGE: `media_type` and `events_type` are retired under
`live`, `viewer` and `timeline` configuration sections, instead media
type is associated (once) with the camera under `media`.
This commit is contained in:
Dermot Duffy
2026-01-19 13:32:50 -08:00
committed by GitHub
parent 6eb0a87ca8
commit fc32727860
215 changed files with 14491 additions and 6160 deletions
+38 -2
View File
@@ -6,11 +6,11 @@ import { createFolder, TestViewMedia } from '../test-utils';
describe('ViewItemClassifier', () => {
it('isMedia', () => {
expect(ViewItemClassifier.isMedia(new TestViewMedia())).toBe(true);
expect(ViewItemClassifier.isMedia(new ViewFolder(createFolder()))).toBe(false);
expect(ViewItemClassifier.isMedia(new ViewFolder(createFolder(), []))).toBe(false);
});
it('isFolder', () => {
expect(ViewItemClassifier.isFolder(new ViewFolder(createFolder()))).toBe(true);
expect(ViewItemClassifier.isFolder(new ViewFolder(createFolder(), []))).toBe(true);
expect(ViewItemClassifier.isFolder(new TestViewMedia())).toBe(false);
});
@@ -73,4 +73,40 @@ describe('ViewItemClassifier', () => {
).toBe(expectedResult);
});
});
describe('supportsTimeline', () => {
it('should return false when item is not a media item with a start time', () => {
expect(ViewItemClassifier.supportsTimeline(null)).toBe(false);
expect(ViewItemClassifier.supportsTimeline(undefined)).toBe(false);
expect(
ViewItemClassifier.supportsTimeline(new ViewFolder(createFolder(), [])),
).toBe(false);
expect(
ViewItemClassifier.supportsTimeline(
new TestViewMedia({ mediaType: ViewMediaType.Clip, startTime: null }),
),
).toBe(false);
});
it('should return true when item is a media item with a start time', () => {
expect(
ViewItemClassifier.supportsTimeline(
new TestViewMedia({ mediaType: ViewMediaType.Clip, startTime: new Date() }),
),
).toBe(true);
expect(
ViewItemClassifier.supportsTimeline(
new TestViewMedia({ mediaType: ViewMediaType.Review, startTime: new Date() }),
),
).toBe(true);
expect(
ViewItemClassifier.supportsTimeline(
new TestViewMedia({
mediaType: ViewMediaType.Recording,
startTime: new Date(),
}),
),
).toBe(true);
});
});
});
+29 -1
View File
@@ -35,13 +35,28 @@ describe('ViewMedia', () => {
expect(media.inProgress()).toBeNull();
expect(media.getContentID()).toBeNull();
expect(media.getTitle()).toBeNull();
expect(media.getDescription()).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();
expect(media.isReviewed()).toBeNull();
expect(media.getIcon()).toBeNull();
expect(media.getSeverity()).toBeNull();
});
it('should clone', () => {
const media = new ViewMedia(ViewMediaType.Clip, {
cameraID: 'camera',
});
const clone = media.clone();
expect(clone).not.toBe(media);
expect(clone).toBeInstanceOf(ViewMedia);
expect(clone.getCameraID()).toBe('camera');
});
it('should correctly determine if a media item includes a time', () => {
@@ -70,7 +85,7 @@ describe('ViewMedia', () => {
describe('ViewFolder', () => {
it('should construct', () => {
const folder = createFolder();
const item = new ViewFolder(folder, {
const item = new ViewFolder(folder, [], {
icon: 'icon',
id: 'id',
title: 'title',
@@ -80,8 +95,21 @@ describe('ViewFolder', () => {
expect(item.getFolder()).toEqual(folder);
expect(item.getID()).toBe('id');
expect(item.getTitle()).toBe('title');
expect(item.getDescription()).toBeNull();
expect(item.getThumbnail()).toBe('thumbnail');
expect(item.getIcon()).toBe('icon');
expect(item.isFavorite()).toBeNull();
expect(item.getSeverity()).toBeNull();
});
it('should clone', () => {
const folder = createFolder();
const item = new ViewFolder(folder, []);
const clone = item.clone();
expect(clone).not.toBe(item);
expect(clone).toBeInstanceOf(ViewFolder);
expect(clone.getFolder()).toEqual(folder);
});
});
-138
View File
@@ -1,138 +0,0 @@
import { describe, expect, it } from 'vitest';
import { QueryType } from '../../src/camera-manager/types';
import {
EventMediaQuery,
FolderViewQuery,
RecordingMediaQuery,
} from '../../src/view/query';
import { QueryClassifier } from '../../src/view/query-classifier';
import { createFolder } from '../test-utils';
describe('QueryClassifier', () => {
it('isEventQuery', () => {
expect(QueryClassifier.isEventQuery(new EventMediaQuery())).toBeTruthy();
expect(QueryClassifier.isEventQuery(new RecordingMediaQuery())).toBeFalsy();
});
it('isRecordingQuery', () => {
expect(QueryClassifier.isRecordingQuery(new RecordingMediaQuery())).toBeTruthy();
expect(QueryClassifier.isRecordingQuery(new EventMediaQuery())).toBeFalsy();
});
it('getQueryType', () => {
expect(QueryClassifier.getQueryType(new EventMediaQuery())).toBe('event');
expect(QueryClassifier.getQueryType(new FolderViewQuery())).toBe('folder');
expect(QueryClassifier.getQueryType(new RecordingMediaQuery())).toBe('recording');
expect(QueryClassifier.getQueryType()).toBeNull();
});
it('isClipsQuery', () => {
expect(
QueryClassifier.isClipsQuery(
new EventMediaQuery([
{ type: QueryType.Event, cameraIDs: new Set(['camera']), hasClip: true },
]),
),
).toBeTruthy();
expect(
QueryClassifier.isClipsQuery(
new EventMediaQuery([
{ type: QueryType.Event, cameraIDs: new Set(['camera']), hasClip: true },
{ type: QueryType.Event, cameraIDs: new Set(['camera']), hasClip: false },
]),
),
).toBeFalsy();
expect(
QueryClassifier.isClipsQuery(
new EventMediaQuery([
{ type: QueryType.Event, cameraIDs: new Set(['camera']), hasClip: true },
{ type: QueryType.Event, cameraIDs: new Set(['camera']) },
]),
),
).toBeFalsy();
});
it('isSnapshotQuery', () => {
expect(
QueryClassifier.isSnapshotQuery(
new EventMediaQuery([
{ type: QueryType.Event, cameraIDs: new Set(['camera']), hasSnapshot: true },
]),
),
).toBeTruthy();
expect(
QueryClassifier.isSnapshotQuery(
new EventMediaQuery([
{ type: QueryType.Event, cameraIDs: new Set(['camera']), hasSnapshot: true },
{ type: QueryType.Event, cameraIDs: new Set(['camera']), hasSnapshot: false },
]),
),
).toBeFalsy();
expect(
QueryClassifier.isSnapshotQuery(
new EventMediaQuery([
{ type: QueryType.Event, cameraIDs: new Set(['camera']), hasSnapshot: true },
{ type: QueryType.Event, cameraIDs: new Set(['camera']) },
]),
),
).toBeFalsy();
});
it('isFolderQuery', () => {
expect(
QueryClassifier.isFolderQuery(
new EventMediaQuery([
{ type: QueryType.Event, cameraIDs: new Set(['camera']), hasSnapshot: true },
]),
),
).toBeFalsy();
expect(
QueryClassifier.isFolderQuery(
new RecordingMediaQuery([
{ type: QueryType.Recording, cameraIDs: new Set(['camera']) },
]),
),
).toBeFalsy();
expect(
QueryClassifier.isFolderQuery(new FolderViewQuery({ folder: createFolder() })),
).toBeTruthy();
});
it('getMediaType', () => {
expect(
QueryClassifier.getMediaType(
new EventMediaQuery([
{ type: QueryType.Event, cameraIDs: new Set(['camera']), hasSnapshot: true },
{ type: QueryType.Event, cameraIDs: new Set(['camera']), hasSnapshot: true },
]),
),
).toBe('snapshots');
expect(
QueryClassifier.getMediaType(
new EventMediaQuery([
{ type: QueryType.Event, cameraIDs: new Set(['camera']), hasClip: true },
{ type: QueryType.Event, cameraIDs: new Set(['camera']), hasClip: true },
]),
),
).toBe('clips');
expect(
QueryClassifier.getMediaType(
new RecordingMediaQuery([
{ type: QueryType.Recording, cameraIDs: new Set(['camera']) },
{ type: QueryType.Recording, cameraIDs: new Set(['camera']) },
]),
),
).toBe('recordings');
expect(
QueryClassifier.getMediaType(
new EventMediaQuery([
{ type: QueryType.Event, cameraIDs: new Set(['camera']), hasSnapshot: true },
{ type: QueryType.Event, cameraIDs: new Set(['camera']), hasSnapshot: false },
]),
),
).toBeNull();
});
});
+147 -3
View File
@@ -1,7 +1,7 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { assert, beforeEach, describe, expect, it, vi } from 'vitest';
import { ViewFolder, ViewItem } from '../../src/view/item';
import { QueryResults } from '../../src/view/query-results';
import { createFolder, generateViewMediaArray } from '../test-utils';
import { createFolder, generateViewMediaArray, TestViewMedia } from '../test-utils';
describe('dispatchViewContextChangeEvent', () => {
beforeEach(() => {
@@ -281,7 +281,7 @@ describe('dispatchViewContextChangeEvent', () => {
});
it('should keep selected folder in main slice but not camera slices', () => {
const folder = new ViewFolder(createFolder());
const folder = new ViewFolder(createFolder(), []);
const results = new QueryResults({
results: [
folder,
@@ -295,4 +295,148 @@ describe('dispatchViewContextChangeEvent', () => {
expect(results.getSelectedResult()).toBe(folder);
expect(results.getSelectedResult('camera.office')).not.toBe(folder);
});
describe('removeItem', () => {
it('should remove item from results', () => {
const testResults = generateViewMediaArray();
const results = new QueryResults({ results: testResults });
const itemToRemove = testResults[50];
expect(results.getResults()?.includes(itemToRemove)).toBeTruthy();
expect(results.getResultsCount()).toBe(200);
results.removeItem(itemToRemove);
expect(results.getResults()?.includes(itemToRemove)).toBeFalsy();
expect(results.getResultsCount()).toBe(199);
});
it('should not remove item that is not in results', () => {
const testResults = generateViewMediaArray();
const results = new QueryResults({ results: testResults });
const outsideItem = generateViewMediaArray({ cameraIDs: ['other'] })[0];
const countBefore = results.getResultsCount();
results.removeItem(outsideItem);
expect(results.getResultsCount()).toBe(countBefore);
});
it('should adjust selection when removing selected item', () => {
const testResults = generateViewMediaArray();
const results = new QueryResults({ results: testResults, selectedIndex: 50 });
const itemToRemove = testResults[50];
results.removeItem(itemToRemove);
// After removing selected item at index 50, Math.min(50, 199-1) = 50
// which is still valid in the new 199-element array
expect(results.getSelectedIndex()).toBe(50);
});
it('should adjust selection when removing item before selected', () => {
const testResults = generateViewMediaArray();
const results = new QueryResults({ results: testResults, selectedIndex: 50 });
const itemToRemove = testResults[10];
results.removeItem(itemToRemove);
// Selection should decrement
expect(results.getSelectedIndex()).toBe(49);
});
it('should not change selection when removing item after selected', () => {
const testResults = generateViewMediaArray();
const results = new QueryResults({ results: testResults, selectedIndex: 50 });
const itemToRemove = testResults[100];
results.removeItem(itemToRemove);
// Selection should stay the same
expect(results.getSelectedIndex()).toBe(50);
});
it('should set selection to null when removing last remaining selected item', () => {
const testResults = generateViewMediaArray({ cameraIDs: ['camera'], count: 1 });
const results = new QueryResults({ results: testResults, selectedIndex: 0 });
results.removeItem(testResults[0]);
// Selection should be null when no items remain
expect(results.getSelectedIndex()).toBeNull();
});
it('should handle removing folder item (non-media)', () => {
const folder = new ViewFolder(createFolder(), []);
const testResults = generateViewMediaArray();
const results = new QueryResults({ results: [folder, ...testResults] });
const countBefore = results.getResultsCount();
results.removeItem(folder);
expect(results.getResultsCount()).toBe(countBefore - 1);
});
});
describe('replaceItem', () => {
it('should replace item in slice', () => {
const testResults = generateViewMediaArray();
const results = new QueryResults({ results: testResults });
const slice = results.getSlice('office');
assert(slice);
const oldItem = slice.getResults()[42];
const newItem = oldItem.clone();
expect(slice?.replaceItem(oldItem, newItem)).toBeTruthy();
expect(slice?.getResults()[42]).toBe(newItem);
});
it('should fail to replace item not in camera slice', () => {
const results = new QueryResults({ results: generateViewMediaArray() });
const slice = results.getSlice('office');
assert(slice);
const foreignItem = new TestViewMedia({
cameraID: 'other',
});
const newItem = foreignItem.clone();
expect(slice.replaceItem(foreignItem, newItem)).toBeFalsy();
});
it('should replace item in main slice', () => {
const testResults = generateViewMediaArray();
const results = new QueryResults({ results: testResults });
const oldItem = testResults[42];
const newItem = oldItem.clone();
expect(results.replaceItem(oldItem, newItem)).toBe(results);
expect(results.getResults()?.[42]).toBe(newItem);
expect(results.getSlice('kitchen')?.getResults().includes(newItem)).toBeTruthy();
expect(results.getSlice('kitchen')?.getResults().includes(oldItem)).toBeFalsy();
});
it('should fail to replace item not in main slice', () => {
const results = new QueryResults({ results: generateViewMediaArray() });
const outsideItem = generateViewMediaArray({ cameraIDs: ['other'] })[0];
const newItem = outsideItem.clone();
expect(results.replaceItem(outsideItem, newItem)).toBe(results);
});
it('should replace folder in main slice', () => {
const folder = new ViewFolder(createFolder(), []);
const results = new QueryResults({ results: [folder] });
const newFolder = folder.clone();
expect(results.replaceItem(folder, newFolder)).toBe(results);
expect(results.getResults()?.[0]).toBe(newFolder);
});
});
});
-322
View File
@@ -1,322 +0,0 @@
import { describe, expect, it } from 'vitest';
import {
EventQuery,
PartialEventQuery,
PartialRecordingQuery,
QueryType,
RecordingQuery,
} from '../../src/camera-manager/types';
import { FolderQuery } from '../../src/card-controller/folders/types';
import { setify } from '../../src/utils/basic';
import {
EventMediaQuery,
FolderViewQuery,
RecordingMediaQuery,
} from '../../src/view/query';
import { createFolder } from '../test-utils';
describe('EventMediaQuery', () => {
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 query = new EventMediaQuery(rawQueries);
expect(query.getQuery()).toBe(rawQueries);
});
it('should set', () => {
const rawQueries = createRawEventQueries('office');
const query = new EventMediaQuery(rawQueries);
const newRawQueries = createRawEventQueries('kitchen');
query.setQuery(newRawQueries);
expect(query.getQuery()).toBe(newRawQueries);
});
it('should determine if queries exist for CameraIDs', () => {
const rawQueries = createRawEventQueries(new Set(['office', 'kitchen']));
const query = new EventMediaQuery(rawQueries);
expect(query.hasQueriesForCameraIDs(new Set(['office']))).toBeTruthy();
expect(query.hasQueriesForCameraIDs(new Set(['office', 'kitchen']))).toBeTruthy();
expect(query.hasQueriesForCameraIDs(new Set(['office', 'front_door']))).toBeFalsy();
});
it('should convert to clips querys', () => {
const rawQueries = createRawEventQueries('office', { hasSnapshot: true });
const query = new EventMediaQuery(rawQueries);
expect(query.convertToClipsQueries().getQuery()).toEqual([
{
type: QueryType.Event,
cameraIDs: new Set(['office']),
hasClip: true,
},
]);
});
it('should convert when queries are null', () => {
const query = new EventMediaQuery();
expect(query.convertToClipsQueries().getQuery()).toBeNull();
});
describe('should determine equality', () => {
it('should return true when query is equal', () => {
const rawQueries_1 = createRawEventQueries('office', { hasSnapshot: true });
const query_1 = new EventMediaQuery(rawQueries_1);
const rawQueries_2 = createRawEventQueries('office', { hasSnapshot: true });
const query_2 = new EventMediaQuery(rawQueries_2);
expect(query_1.isEqual(query_2)).toBeTruthy();
});
it('should return false when query is not equal', () => {
const rawQueries_1 = createRawEventQueries('office', { hasSnapshot: true });
const query_1 = new EventMediaQuery(rawQueries_1);
const rawQueries_2 = createRawEventQueries('office', { hasSnapshot: false });
const query_2 = new EventMediaQuery(rawQueries_2);
expect(query_1.isEqual(query_2)).toBeFalsy();
});
});
it('should clone', () => {
const rawQueries = createRawEventQueries('office', { hasSnapshot: true });
const query = new EventMediaQuery(rawQueries);
expect(query.clone().getQuery()).toEqual(query.getQuery());
});
it('should get camera IDs when queries are null', () => {
expect(new EventMediaQuery().getQueryCameraIDs()).toBeNull();
});
it('should get camera IDs', () => {
const cameraIDs = ['office', 'kitchen'];
const query = new EventMediaQuery(createRawEventQueries(new Set(cameraIDs)));
expect(query.getQueryCameraIDs()).toEqual(new Set(cameraIDs));
});
it('should set camera IDs when queries are null', () => {
expect(
new EventMediaQuery().setQueryCameraIDs(new Set(['office'])).getQueryCameraIDs(),
).toBeNull();
});
it('should set camera IDs', () => {
const query = new EventMediaQuery(createRawEventQueries('sitting_room'));
const newCameraIDs = new Set(['office', 'kitchen']);
expect(query.setQueryCameraIDs(newCameraIDs).getQueryCameraIDs()).toEqual(
newCameraIDs,
);
});
describe('should determine when queries are a superset', () => {
it('should return true with itself', () => {
const query_1 = new EventMediaQuery([
{
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2025-03-07T00:00:00.000Z'),
end: new Date('2025-03-08T00:00:00.000Z'),
},
]);
expect(query_1.isSupersetOf(query_1)).toBeTruthy();
});
it('should return true with an identical but shorter query', () => {
const query_1 = new EventMediaQuery([
{
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2025-03-07T00:00:00.000Z'),
end: new Date('2025-03-08T00:00:00.000Z'),
},
]);
const query_2 = new EventMediaQuery([
{
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2025-03-07T01:00:00.000Z'),
end: new Date('2025-03-07T23:00:00.000Z'),
},
]);
expect(query_1.isSupersetOf(query_2)).toBeTruthy();
});
it('should return false with an identical but longer query', () => {
const query_1 = new EventMediaQuery([
{
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2025-03-07T00:00:00.000Z'),
end: new Date('2025-03-08T00:00:00.000Z'),
},
]);
const query_2 = new EventMediaQuery([
{
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2025-03-06T00:00:00.000Z'),
end: new Date('2025-03-08T00:00:00.000Z'),
},
]);
expect(query_1.isSupersetOf(query_2)).toBeFalsy();
});
it('should return false with a non-matching query', () => {
const query_1 = new EventMediaQuery([
{
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2025-03-07T00:00:00.000Z'),
end: new Date('2025-03-08T00:00:00.000Z'),
},
]);
const query_2 = new EventMediaQuery([
{
type: QueryType.Event,
cameraIDs: new Set(['DIFFERENT']),
start: new Date('2025-03-07T00:00:00.000Z'),
end: new Date('2025-03-08T00:00:00.000Z'),
},
]);
expect(query_1.isSupersetOf(query_2)).toBeFalsy();
});
it('should return true with a matching query where the source has multiple', () => {
const query_1 = new EventMediaQuery([
{
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2025-03-07T00:00:00.000Z'),
end: new Date('2025-03-08T00:00:00.000Z'),
},
{
type: QueryType.Event,
cameraIDs: new Set(['kitchen']),
start: new Date('2025-03-07T00:00:00.000Z'),
end: new Date('2025-03-08T00:00:00.000Z'),
},
]);
const query_2 = new EventMediaQuery([
{
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2025-03-07T00:00:00.000Z'),
end: new Date('2025-03-08T00:00:00.000Z'),
},
]);
expect(query_1.isSupersetOf(query_2)).toBeTruthy();
});
it('should return false with a matching query where the target has multiple', () => {
const query_1 = new EventMediaQuery([
{
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2025-03-07T00:00:00.000Z'),
end: new Date('2025-03-08T00:00:00.000Z'),
},
]);
const query_2 = new EventMediaQuery([
{
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2025-03-07T00:00:00.000Z'),
end: new Date('2025-03-08T00:00:00.000Z'),
},
{
type: QueryType.Event,
cameraIDs: new Set(['kitchen']),
start: new Date('2025-03-07T00:00:00.000Z'),
end: new Date('2025-03-08T00:00:00.000Z'),
},
]);
expect(query_1.isSupersetOf(query_2)).toBeFalsy();
});
it('should return true when queries do not have start or end', () => {
const query_1 = new EventMediaQuery([
{
type: QueryType.Event,
cameraIDs: new Set(['office']),
},
]);
const query_2 = new EventMediaQuery([
{
type: QueryType.Event,
cameraIDs: new Set(['office']),
},
]);
expect(query_1.isSupersetOf(query_2)).toBeTruthy();
});
it('should return true when target has no queries', () => {
const query_1 = new EventMediaQuery([
{
type: QueryType.Event,
cameraIDs: new Set(['office']),
},
]);
const query_2 = new EventMediaQuery();
expect(query_1.isSupersetOf(query_2)).toBeTruthy();
});
it('should return false when source has no queries', () => {
const query_1 = new EventMediaQuery();
const query_2 = new EventMediaQuery([
{
type: QueryType.Event,
cameraIDs: new Set(['office']),
},
]);
expect(query_1.isSupersetOf(query_2)).toBeFalsy();
});
});
});
describe('RecordingMediaQuery', () => {
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 query = new RecordingMediaQuery(rawQueries);
expect(query.getQuery()).toBe(rawQueries);
});
});
describe('FolderViewQuery', () => {
it('should construct', () => {
const rawQuery: FolderQuery = {
folder: createFolder(),
path: [{}],
};
const query = new FolderViewQuery(rawQuery);
expect(query.getQuery()).toBe(rawQuery);
});
});
File diff suppressed because it is too large Load Diff
+224
View File
@@ -0,0 +1,224 @@
import { assert, describe, expect, it } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { CameraManager } from '../../src/camera-manager/manager';
import { FoldersManager } from '../../src/card-controller/folders/manager';
import { ConditionStateManagerReadonlyInterface } from '../../src/conditions/types';
import { QuerySource } from '../../src/query-source';
import { ViewMedia } from '../../src/view/item';
import { UnifiedQuery } from '../../src/view/unified-query';
import { UnifiedQueryRunner } from '../../src/view/unified-query-runner';
import { createEventQuery, createFolderQuery } from '../test-utils';
describe('UnifiedQueryRunner', () => {
describe('execute', () => {
it('should aggregate results from camera and folder sources', async () => {
const cameraManager = mock<CameraManager>();
const foldersManager = mock<FoldersManager>();
const conditionStateManager = mock<ConditionStateManagerReadonlyInterface>();
const runner = new UnifiedQueryRunner(
cameraManager,
foldersManager,
conditionStateManager,
);
const cameraItem = mock<ViewMedia>();
const folderItem = mock<ViewMedia>();
cameraManager.executeMediaQueries.mockResolvedValue([cameraItem]);
foldersManager.expandFolder.mockResolvedValue([folderItem]);
const query = new UnifiedQuery();
query.addNode(createEventQuery('camera1'));
query.addNode(createFolderQuery('f1'));
const results = await runner.execute(query);
expect(results).toHaveLength(2);
expect(results).toContain(cameraItem);
expect(results).toContain(folderItem);
expect(cameraManager.executeMediaQueries).toHaveBeenCalledWith(
[expect.objectContaining({ source: QuerySource.Camera })],
expect.anything(),
);
expect(foldersManager.expandFolder).toHaveBeenCalledWith(
expect.objectContaining({ source: QuerySource.Folder }),
undefined,
expect.objectContaining({ useCache: undefined }),
);
});
it('should handle empty query gracefully', async () => {
const cameraManager = mock<CameraManager>();
const foldersManager = mock<FoldersManager>();
const runner = new UnifiedQueryRunner(
cameraManager,
foldersManager,
mock<ConditionStateManagerReadonlyInterface>(),
);
const results = await runner.execute(new UnifiedQuery());
expect(results).toHaveLength(0);
expect(cameraManager.executeMediaQueries).not.toHaveBeenCalled();
expect(foldersManager.expandFolder).not.toHaveBeenCalled();
});
it('should handle null returns from managers', async () => {
const cameraManager = mock<CameraManager>();
const foldersManager = mock<FoldersManager>();
const runner = new UnifiedQueryRunner(
cameraManager,
foldersManager,
mock<ConditionStateManagerReadonlyInterface>(),
);
cameraManager.executeMediaQueries.mockResolvedValue(null);
foldersManager.expandFolder.mockResolvedValue(null);
const query = new UnifiedQuery();
query.addNode(createEventQuery('camera1'));
query.addNode(createFolderQuery('f1'));
const results = await runner.execute(query);
expect(results).toHaveLength(0);
});
it('should pass options to managers', async () => {
const cameraManager = mock<CameraManager>();
const runner = new UnifiedQueryRunner(
cameraManager,
mock<FoldersManager>(),
mock<ConditionStateManagerReadonlyInterface>(),
);
const query = new UnifiedQuery();
query.addNode(createEventQuery('camera1'));
await runner.execute(query, { useCache: false });
expect(cameraManager.executeMediaQueries).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ useCache: false }),
);
});
});
describe('areResultsFresh', () => {
it('should return true if managers report fresh', () => {
const cameraManager = mock<CameraManager>();
const foldersManager = mock<FoldersManager>();
cameraManager.areMediaQueriesResultsFresh.mockReturnValue(true);
foldersManager.areResultsFresh.mockReturnValue(true);
const runner = new UnifiedQueryRunner(
cameraManager,
foldersManager,
mock<ConditionStateManagerReadonlyInterface>(),
);
const query = new UnifiedQuery();
query.addNode(createEventQuery('camera1'));
query.addNode(createFolderQuery('f1'));
expect(runner.areResultsFresh(new Date(), query)).toBe(true);
});
it('should return false if any manager reports stale', () => {
const cameraManager = mock<CameraManager>();
cameraManager.areMediaQueriesResultsFresh.mockReturnValue(false);
const runner = new UnifiedQueryRunner(
cameraManager,
mock<FoldersManager>(),
mock<ConditionStateManagerReadonlyInterface>(),
);
const query = new UnifiedQuery([createEventQuery('camera1')]);
expect(runner.areResultsFresh(new Date(), query)).toBe(false);
});
it('should return false if folder manager reports stale', () => {
const foldersManager = mock<FoldersManager>();
foldersManager.areResultsFresh.mockReturnValue(false);
const runner = new UnifiedQueryRunner(
mock<CameraManager>(),
foldersManager,
mock<ConditionStateManagerReadonlyInterface>(),
);
const query = new UnifiedQuery([createFolderQuery('f1')]);
expect(runner.areResultsFresh(new Date(), query)).toBe(false);
});
});
describe('extend', () => {
it('should extend media queries and preserve folder queries', async () => {
const cameraManager = mock<CameraManager>();
const runner = new UnifiedQueryRunner(
cameraManager,
mock<FoldersManager>(),
mock<ConditionStateManagerReadonlyInterface>(),
);
const existingItem = mock<ViewMedia>();
const newItem = mock<ViewMedia>();
const extendedMediaMatch = createEventQuery('camera1');
cameraManager.extendMediaQueries.mockResolvedValue({
queries: [extendedMediaMatch],
results: [existingItem, newItem],
});
const query = new UnifiedQuery();
query.addNode(createEventQuery('camera1'));
query.addNode(createFolderQuery('f1'));
const result = await runner.extend(query, [existingItem], 'later');
assert(result);
expect(result.results).toHaveLength(2);
expect(result.query.getNodeCount()).toBe(2);
expect(result.query.getMediaQueries()).toHaveLength(1);
expect(result.query.getFolderQueries()).toHaveLength(1);
expect(cameraManager.extendMediaQueries).toHaveBeenCalledWith(
[expect.objectContaining({ source: QuerySource.Camera })],
[existingItem],
'later',
expect.anything(),
);
});
it('should return null for query with no media nodes', async () => {
const runner = new UnifiedQueryRunner(
mock<CameraManager>(),
mock<FoldersManager>(),
mock<ConditionStateManagerReadonlyInterface>(),
);
const query = new UnifiedQuery([createFolderQuery('f1')]);
const result = await runner.extend(query, [], 'earlier');
expect(result).toBeNull();
});
it('should return null if camera manager fails to extend', async () => {
const cameraManager = mock<CameraManager>();
cameraManager.extendMediaQueries.mockResolvedValue(null);
const runner = new UnifiedQueryRunner(
cameraManager,
mock<FoldersManager>(),
mock<ConditionStateManagerReadonlyInterface>(),
);
const query = new UnifiedQuery([createEventQuery('camera1')]);
const result = await runner.extend(query, [], 'earlier');
expect(result).toBeNull();
});
});
});
@@ -0,0 +1,145 @@
import { assert, describe, expect, it } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { CameraManager } from '../../src/camera-manager/manager';
import { CameraManagerStore } from '../../src/camera-manager/store';
import { FoldersManager } from '../../src/card-controller/folders/manager';
import { UnifiedQueryBuilder } from '../../src/view/unified-query-builder';
import { UnifiedQueryTransformer } from '../../src/view/unified-query-transformer';
import {
createCapabilities,
createFolder,
isEventQuery,
isFolderQuery,
isRecordingQuery,
} from '../test-utils';
const createMocks = () => {
const cameraManager = mock<CameraManager>();
const foldersManager = mock<FoldersManager>();
const store = mock<CameraManagerStore>();
cameraManager.getStore.mockReturnValue(store);
store.getCameraIDs.mockReturnValue(new Set(['camera.office']));
store.getAllDependentCameras.mockImplementation((id) => new Set([id]));
cameraManager.getCameraCapabilities.mockReturnValue(
createCapabilities({ clips: true, snapshots: true, recordings: true }),
);
return { cameraManager, foldersManager, store };
};
describe('UnifiedQueryTransformer', () => {
describe('stripTimeRange', () => {
it('should remove start and end from camera queries', () => {
const { cameraManager, foldersManager } = createMocks();
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
const query = builder.buildClipsQuery(new Set(['camera.office']), {
start: new Date('2024-01-01'),
end: new Date('2024-01-02'),
limit: 10,
});
assert(query);
const stripped = UnifiedQueryTransformer.stripTimeRange(query);
const node = stripped.getNodes()[0];
assert(isEventQuery(node));
expect(node).not.toHaveProperty('start');
expect(node).not.toHaveProperty('end');
expect(node.limit).toBe(10); // Other props preserved
});
it('should not affect folder queries', () => {
const { cameraManager, foldersManager } = createMocks();
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
const folder = createFolder({ id: 'f1', title: 'Test' });
const query = builder.buildFolderQueryWithPath(folder, [{ ha: { id: 'Root' } }]);
const stripped = UnifiedQueryTransformer.stripTimeRange(query);
const node = stripped.getNodes()[0];
assert(isFolderQuery(node));
expect(node.folder.id).toBe('f1');
});
});
describe('rebuildQuery', () => {
it('should apply new options to camera queries', () => {
const { cameraManager, foldersManager } = createMocks();
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
const query = builder.buildClipsQuery(new Set(['camera.office']));
assert(query);
const start = new Date('2024-06-01');
const end = new Date('2024-06-02');
const rebuilt = UnifiedQueryTransformer.rebuildQuery(query, {
start,
end,
limit: 100,
});
const node = rebuilt.getNodes()[0];
assert(isEventQuery(node));
expect(node.start).toEqual(start);
expect(node.end).toEqual(end);
expect(node.limit).toBe(100);
});
it('should not affect folder queries', () => {
const { cameraManager, foldersManager } = createMocks();
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
const folder = createFolder({ id: 'f1', title: 'Test' });
const query = builder.buildFolderQueryWithPath(folder, [{ ha: { id: 'Root' } }]);
const rebuilt = UnifiedQueryTransformer.rebuildQuery(query, {
start: new Date(),
end: new Date(),
});
const node = rebuilt.getNodes()[0];
assert(isFolderQuery(node));
expect(node).not.toHaveProperty('start');
expect(node).not.toHaveProperty('end');
});
});
describe('convertToClips', () => {
it('should convert event query to clips', () => {
const { cameraManager, foldersManager } = createMocks();
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
const query = builder.buildSnapshotsQuery(new Set(['camera.office']));
assert(query);
const converted = UnifiedQueryTransformer.convertToClips(query);
const node = converted.getNodes()[0];
assert(isEventQuery(node));
expect(node.hasClip).toBe(true);
expect(node.hasSnapshot).toBeUndefined();
});
it('should not affect recording queries', () => {
const { cameraManager, foldersManager } = createMocks();
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
const query = builder.buildRecordingsQuery(new Set(['camera.office']));
assert(query);
const converted = UnifiedQueryTransformer.convertToClips(query);
const node = converted.getNodes()[0];
assert(isRecordingQuery(node));
expect(node).not.toHaveProperty('hasClip');
});
it('should not affect folder queries', () => {
const { cameraManager, foldersManager } = createMocks();
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
const folder = createFolder({ id: 'f1', title: 'Test' });
const query = builder.buildFolderQueryWithPath(folder, [{ ha: { id: 'Root' } }]);
const converted = UnifiedQueryTransformer.convertToClips(query);
const node = converted.getNodes()[0];
assert(isFolderQuery(node));
expect(node.folder.id).toBe('f1');
});
});
});
+334
View File
@@ -0,0 +1,334 @@
import { assert, describe, expect, it } from 'vitest';
import { QueryType } from '../../src/camera-manager/types';
import { QuerySource } from '../../src/query-source';
import { UnifiedQuery } from '../../src/view/unified-query';
import {
createEventQuery,
createFolderQuery,
createRecordingQuery,
createReviewQuery,
isEventQuery,
isFolderQuery,
} from '../test-utils';
describe('UnifiedQuery', () => {
describe('Node Management', () => {
it('should construct empty', () => {
const query = new UnifiedQuery();
expect(query.hasNodes()).toBe(false);
});
it('should construct with initial nodes', () => {
const nodes = [createEventQuery('front')];
const query = new UnifiedQuery(nodes);
expect(query.hasNodes()).toBe(true);
});
it('should add node', () => {
const query = new UnifiedQuery();
query.addNode(createEventQuery('front'));
expect(query.getMediaQueries()).toHaveLength(1);
});
it('should allow multiple nodes for same camera', () => {
const query = new UnifiedQuery();
query.addNode(createEventQuery('front'));
query.addNode(createReviewQuery('front'));
query.addNode(createRecordingQuery('front'));
expect(query.getMediaQueries()).toHaveLength(3);
});
});
describe('Lookup Methods', () => {
it('should get queries by folder', () => {
const query = new UnifiedQuery();
query.addNode(createEventQuery('front'));
query.addNode(createFolderQuery('clips'));
query.addNode(createFolderQuery('recordings'));
const clipsQueries = query.getFolderQueries('clips');
expect(clipsQueries).toHaveLength(1);
});
it('should get all media types', () => {
const query = new UnifiedQuery();
query.addNode(createEventQuery('front', { hasClip: true }));
query.addNode(createEventQuery('back', { hasSnapshot: true }));
query.addNode(createRecordingQuery('garage'));
query.addNode(createReviewQuery('office'));
const types = query.getAllMediaTypes();
expect(types.size).toBe(4);
expect(types.has('clips')).toBe(true);
expect(types.has('snapshots')).toBe(true);
expect(types.has('recordings')).toBe(true);
expect(types.has('reviews')).toBe(true);
});
it('should ignore non-media nodes in getAllMediaTypes', () => {
const query = new UnifiedQuery();
query.addNode(createEventQuery('front', { hasClip: true }));
query.addNode(createFolderQuery('clips'));
const types = query.getAllMediaTypes();
expect(types.size).toBe(1);
expect(types.has('clips')).toBe(true);
});
it('should ignore unhandled media types in getAllMediaTypes', () => {
const query = new UnifiedQuery();
query.addNode({
source: QuerySource.Camera,
type: QueryType.RecordingSegments,
cameraIDs: new Set(['camera1']),
});
const types = query.getAllMediaTypes();
expect(types.size).toBe(0);
});
it('should filter media queries by camera ID', () => {
const query = new UnifiedQuery();
query.addNode(createEventQuery('front'));
query.addNode(createEventQuery('back'));
expect(query.getMediaQueries({ cameraID: 'front' })).toHaveLength(1);
expect(query.getMediaQueries({ cameraID: 'garage' })).toHaveLength(0);
});
it('should check hasMediaQueriesOfType', () => {
const query = new UnifiedQuery();
query.addNode(createEventQuery('front'));
expect(query.hasMediaQueriesOfType(QueryType.Event)).toBe(true);
expect(query.hasMediaQueriesOfType(QueryType.Review)).toBe(false);
});
});
describe('Camera Operations', () => {
it('should get all camera IDs', () => {
const query = new UnifiedQuery();
query.addNode(createEventQuery('front'));
query.addNode(createReviewQuery('back'));
query.addNode(createFolderQuery('clips'));
const cameraIDs = query.getAllCameraIDs();
expect(cameraIDs.size).toBe(2);
expect(cameraIDs.has('front')).toBe(true);
expect(cameraIDs.has('back')).toBe(true);
});
});
describe('Clone', () => {
it('should create deep clone', () => {
const query = new UnifiedQuery();
query.addNode(
createEventQuery('front', {
what: new Set(['person']),
where: new Set(['driveway']),
}),
);
const cloned = query.clone();
// Modify original
const originalEvent = query.getMediaQueries()[0];
assert(isEventQuery(originalEvent));
originalEvent.what?.add('car');
// Clone should be unchanged
const clonedEvent = cloned.getMediaQueries()[0];
assert(isEventQuery(clonedEvent));
expect(clonedEvent.what?.has('car')).toBe(false);
expect(clonedEvent.what?.has('person')).toBe(true);
});
it('should clone path arrays', () => {
const query = new UnifiedQuery();
query.addNode(createFolderQuery('clips'));
const cloned = query.clone();
// Modify original
const originalFolder = query.getFolderQueries()[0];
assert(isFolderQuery(originalFolder));
// We need to bypass readonly for testing the clone's independence
(originalFolder as { path: unknown }).path = [
...originalFolder.path,
{ ha: { id: 'Child' } },
];
// Clone should be unchanged
const clonedFolder = cloned.getFolderQueries()[0];
assert(isFolderQuery(clonedFolder));
expect(clonedFolder.path).toHaveLength(1);
});
});
describe('Equality', () => {
it('should return true for equal queries', () => {
const query1 = new UnifiedQuery();
query1.addNode(
createEventQuery('front', {
what: new Set(['person']),
}),
);
const query2 = new UnifiedQuery();
query2.addNode(
createEventQuery('front', {
what: new Set(['person']),
}),
);
expect(query1.isEqual(query2)).toBe(true);
});
it('should return false for different node counts', () => {
const query1 = new UnifiedQuery();
query1.addNode(createEventQuery('front'));
const query2 = new UnifiedQuery();
query2.addNode(createEventQuery('front'));
query2.addNode(createEventQuery('back'));
expect(query1.isEqual(query2)).toBe(false);
});
it('should return false for different node values', () => {
const query1 = new UnifiedQuery();
query1.addNode(createEventQuery('front', { hasClip: true }));
const query2 = new UnifiedQuery();
query2.addNode(createEventQuery('front', { hasClip: false }));
expect(query1.isEqual(query2)).toBe(false);
});
it('should handle Set comparison correctly', () => {
const query1 = new UnifiedQuery();
query1.addNode(
createEventQuery('front', {
what: new Set(['person', 'car']),
}),
);
const query2 = new UnifiedQuery();
query2.addNode(
createEventQuery('front', {
what: new Set(['car', 'person']), // Same values, different order
}),
);
expect(query1.isEqual(query2)).toBe(true);
});
});
describe('isSupersetOf', () => {
it('should return true for identical queries', () => {
const query1 = new UnifiedQuery();
query1.addNode(createEventQuery('front'));
const query2 = new UnifiedQuery();
query2.addNode(createEventQuery('front'));
expect(query1.isSupersetOf(query2)).toBe(true);
});
it('should return true when superset contains all nodes', () => {
const superset = new UnifiedQuery();
superset.addNode(createEventQuery('front'));
superset.addNode(createEventQuery('back'));
const subset = new UnifiedQuery();
subset.addNode(createEventQuery('front'));
expect(superset.isSupersetOf(subset)).toBe(true);
});
it('should return false when target has node not in source', () => {
const query1 = new UnifiedQuery();
query1.addNode(createEventQuery('front'));
const query2 = new UnifiedQuery();
query2.addNode(createEventQuery('front'));
query2.addNode(createEventQuery('back'));
expect(query1.isSupersetOf(query2)).toBe(false);
});
it('should return true when time range is wider', () => {
const superset = new UnifiedQuery();
superset.addNode(
createEventQuery('front', {
start: new Date('2024-01-01'),
end: new Date('2024-01-31'),
}),
);
const subset = new UnifiedQuery();
subset.addNode(
createEventQuery('front', {
start: new Date('2024-01-10'),
end: new Date('2024-01-20'),
}),
);
expect(superset.isSupersetOf(subset)).toBe(true);
});
it('should return false when superset time range is narrower', () => {
const narrower = new UnifiedQuery();
narrower.addNode(
createEventQuery('front', {
start: new Date('2024-01-10'),
end: new Date('2024-01-20'),
}),
);
const wider = new UnifiedQuery();
wider.addNode(
createEventQuery('front', {
start: new Date('2024-01-01'),
end: new Date('2024-01-31'),
}),
);
expect(narrower.isSupersetOf(wider)).toBe(false);
});
it('should handle null times correctly', () => {
const query1 = new UnifiedQuery();
query1.addNode(createEventQuery('front'));
const query2 = new UnifiedQuery();
query2.addNode(createEventQuery('front'));
expect(query1.isSupersetOf(query2)).toBe(true);
});
it('should return true for empty target query', () => {
const superset = new UnifiedQuery();
superset.addNode(createEventQuery('front'));
const empty = new UnifiedQuery();
expect(superset.isSupersetOf(empty)).toBe(true);
});
it('should support folder nodes', () => {
const superset = new UnifiedQuery();
superset.addNode(createFolderQuery('f1'));
const subset = new UnifiedQuery();
subset.addNode(createFolderQuery('f1'));
expect(superset.isSupersetOf(subset)).toBe(true);
const nonSubset = new UnifiedQuery();
nonSubset.addNode(createFolderQuery('f2'));
expect(superset.isSupersetOf(nonSubset)).toBe(false);
});
});
});
+2
View File
@@ -79,6 +79,8 @@ describe('getCameraIDsForViewName', () => {
['snapshots' as const, 'snapshots' as const],
['recording' as const, 'recordings' as const],
['recordings' as const, 'recordings' as const],
['review' as const, 'reviews' as const],
['reviews' as const, 'reviews' as const],
])('%s', (viewName: AdvancedCameraCardView, capabilityKey: CapabilityKey) => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
+16 -12
View File
@@ -1,11 +1,11 @@
import { describe, expect, it } from 'vitest';
import { EventMediaQuery } from '../../src/view/query';
import { UnifiedQuery } from '../../src/view/unified-query';
import { QueryResults } from '../../src/view/query-results';
import { createView } from '../test-utils';
describe('View Basics', () => {
it('should construct from parameters', () => {
const query = new EventMediaQuery();
const query = new UnifiedQuery();
const queryResults = new QueryResults();
const context = {};
@@ -29,7 +29,7 @@ describe('View Basics', () => {
const view = createView({
view: 'live',
camera: 'camera',
query: new EventMediaQuery(),
query: new UnifiedQuery(),
queryResults: new QueryResults(),
context: {},
});
@@ -52,7 +52,7 @@ describe('View Basics', () => {
const view = createView({
view: 'live',
camera: 'camera-1',
query: new EventMediaQuery(),
query: new UnifiedQuery(),
queryResults: new QueryResults(),
context: {},
displayMode: 'single',
@@ -61,7 +61,7 @@ describe('View Basics', () => {
const evolved = view.evolve({
view: 'clips',
camera: 'camera-2',
query: new EventMediaQuery(),
query: new UnifiedQuery(),
queryResults: new QueryResults(),
context: {},
displayMode: 'grid',
@@ -78,7 +78,7 @@ describe('View Basics', () => {
const view = createView({
view: 'live',
camera: 'camera-1',
query: new EventMediaQuery(),
query: new UnifiedQuery(),
queryResults: new QueryResults(),
context: {},
});
@@ -184,15 +184,16 @@ describe('View Basics', () => {
describe('should detect gallery views', () => {
it('should detect gallery views', () => {
expect(createView({ view: 'clips' }).isMediaGalleryView()).toBeTruthy();
expect(createView({ view: 'folders' }).isMediaGalleryView()).toBeTruthy();
expect(createView({ view: 'snapshots' }).isMediaGalleryView()).toBeTruthy();
expect(createView({ view: 'recordings' }).isMediaGalleryView()).toBeTruthy();
expect(createView({ view: 'clips' }).isGalleryView()).toBeTruthy();
expect(createView({ view: 'folders' }).isGalleryView()).toBeTruthy();
expect(createView({ view: 'snapshots' }).isGalleryView()).toBeTruthy();
expect(createView({ view: 'recordings' }).isGalleryView()).toBeTruthy();
expect(createView({ view: 'reviews' }).isGalleryView()).toBeTruthy();
});
it('should not detect gallery view', () => {
expect(createView({ view: 'live' }).isMediaGalleryView()).toBeFalsy();
expect(createView({ view: 'timeline' }).isMediaGalleryView()).toBeFalsy();
expect(createView({ view: 'live' }).isGalleryView()).toBeFalsy();
expect(createView({ view: 'timeline' }).isGalleryView()).toBeFalsy();
});
});
@@ -202,6 +203,7 @@ describe('View Basics', () => {
expect(createView({ view: 'snapshot' }).isViewerView()).toBeTruthy();
expect(createView({ view: 'media' }).isViewerView()).toBeTruthy();
expect(createView({ view: 'recording' }).isViewerView()).toBeTruthy();
expect(createView({ view: 'review' }).isViewerView()).toBeTruthy();
expect(createView({ view: 'folder' }).isAnyMediaView()).toBeTruthy();
});
@@ -222,6 +224,8 @@ describe('View Basics', () => {
expect(createView({ view: 'recordings' }).getDefaultMediaType()).toBe(
'recordings',
);
expect(createView({ view: 'review' }).getDefaultMediaType()).toBe('reviews');
expect(createView({ view: 'reviews' }).getDefaultMediaType()).toBe('reviews');
});
it('should not get default media type', () => {