fix: Improve review media filtering (#2322)

This commit is contained in:
Dermot Duffy
2026-01-24 16:22:12 -08:00
committed by GitHub
parent fc32727860
commit f4e905a7fb
37 changed files with 884 additions and 253 deletions
+7
View File
@@ -45,6 +45,12 @@ describe('ViewMedia', () => {
expect(media.isReviewed()).toBeNull();
expect(media.getIcon()).toBeNull();
expect(media.getSeverity()).toBeNull();
expect(media.getFolder()).toBeNull();
});
it('should construct without options', () => {
const media = new ViewMedia(ViewMediaType.Clip);
expect(media.getCameraID()).toBeNull();
});
it('should clone', () => {
@@ -111,5 +117,6 @@ describe('ViewFolder', () => {
expect(clone).not.toBe(item);
expect(clone).toBeInstanceOf(ViewFolder);
expect(clone.getFolder()).toEqual(folder);
expect(clone.getPath()).toEqual([]);
});
});
+83 -10
View File
@@ -36,8 +36,26 @@ const createMocks = () => {
const cameraManager = mock<CameraManager>();
const foldersManager = mock<FoldersManager>();
const store = mock<CameraManagerStore>();
cameraManager.getStore.mockReturnValue(store);
cameraManager.getDefaultQueryParameters.mockReturnValue({});
store.getCameraIDs.mockReturnValue(new Set());
store.getCameraIDsWithCapability.mockReturnValue(new Set());
store.getAllDependentCameras.mockReturnValue(new Set());
store.getCameraConfig.mockReturnValue(createCameraConfig());
// By default, make getCameraCapabilities return a set of capabilities that
// matches everything, unless the test mocks it otherwise.
cameraManager.getCameraCapabilities.mockReturnValue(
createCapabilities({
clips: true,
snapshots: true,
recordings: true,
reviews: true,
}),
);
return { cameraManager, foldersManager, store };
};
@@ -470,7 +488,6 @@ describe('UnifiedQueryBuilder', () => {
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
const folder = createFolder({ id: 'folder1', title: 'Test' });
const path: [FolderPathComponent] = [{ ha: { id: 'Root' } }];
const query = builder.buildFolderQueryWithPath(folder, path, {
limit: 10,
});
@@ -972,7 +989,7 @@ describe('UnifiedQueryBuilder', () => {
store.getCameraIDsWithCapability.mockReturnValue(new Set(['camera.office']));
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
const query = builder.buildCameraMediaQuery(MediaTypeSpec.clips());
const query = builder.buildCameraMediaQuery('events', { eventsSubtype: 'clips' });
assert(query);
expect(query.getNodes()[0]).toMatchObject({
@@ -986,7 +1003,9 @@ describe('UnifiedQueryBuilder', () => {
store.getCameraIDsWithCapability.mockReturnValue(new Set(['camera.office']));
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
const query = builder.buildCameraMediaQuery(MediaTypeSpec.snapshots());
const query = builder.buildCameraMediaQuery('events', {
eventsSubtype: 'snapshots',
});
assert(query);
expect(query.getNodes()[0]).toMatchObject({
@@ -1000,7 +1019,7 @@ describe('UnifiedQueryBuilder', () => {
store.getCameraIDsWithCapability.mockReturnValue(new Set(['camera.office']));
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
const query = builder.buildCameraMediaQuery(MediaTypeSpec.recordings());
const query = builder.buildCameraMediaQuery('recordings');
assert(query);
expect(query.getNodes()[0]).toMatchObject({ type: QueryType.Recording });
@@ -1011,7 +1030,7 @@ describe('UnifiedQueryBuilder', () => {
store.getCameraIDsWithCapability.mockReturnValue(new Set(['camera.office']));
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
const query = builder.buildCameraMediaQuery(MediaTypeSpec.reviews());
const query = builder.buildCameraMediaQuery('reviews');
assert(query);
expect(query.getNodes()[0]).toMatchObject({ type: QueryType.Review });
@@ -1022,8 +1041,9 @@ describe('UnifiedQueryBuilder', () => {
store.getAllDependentCameras.mockReturnValue(new Set(['camera.office', 'dep1']));
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
const query = builder.buildCameraMediaQuery(MediaTypeSpec.clips(), {
const query = builder.buildCameraMediaQuery('events', {
cameraID: 'camera.office',
eventsSubtype: 'clips',
});
assert(query);
@@ -1038,7 +1058,7 @@ describe('UnifiedQueryBuilder', () => {
store.getCameraIDsWithCapability.mockReturnValue(new Set(['camera.office']));
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
const query = builder.buildCameraMediaQuery(MediaTypeSpec.recordings(), {
const query = builder.buildCameraMediaQuery('recordings', {
limit: 30,
});
@@ -1054,7 +1074,7 @@ describe('UnifiedQueryBuilder', () => {
store.getCameraIDsWithCapability.mockReturnValue(new Set());
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
const query = builder.buildCameraMediaQuery(MediaTypeSpec.clips());
const query = builder.buildCameraMediaQuery('events', { eventsSubtype: 'clips' });
expect(query).toBeNull();
});
@@ -1063,7 +1083,7 @@ describe('UnifiedQueryBuilder', () => {
const { cameraManager, foldersManager } = createMocks();
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
const query = builder.buildCameraMediaQuery({ mediaType: 'folder' });
const query = builder.buildCameraMediaQuery('folder');
expect(query).toBeNull();
});
@@ -1073,7 +1093,7 @@ describe('UnifiedQueryBuilder', () => {
store.getCameraIDsWithCapability.mockReturnValue(new Set(['camera.office']));
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
const query = builder.buildCameraMediaQuery({ mediaType: 'events' });
const query = builder.buildCameraMediaQuery('events');
assert(query);
expect(query.getNodes()[0]).toMatchObject({ type: QueryType.Event });
@@ -1084,6 +1104,59 @@ describe('UnifiedQueryBuilder', () => {
anyCapabilities: ['clips', 'snapshots'],
});
});
it('should skip cameras without capabilities', () => {
const { cameraManager, foldersManager, store } = createMocks();
store.getCameraIDsWithCapability.mockReturnValue(
new Set(['capable', 'not-capable']),
);
cameraManager.getCameraCapabilities.mockImplementation((id) => {
if (id === 'capable') {
return createCapabilities({ clips: true });
}
return null;
});
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
const query = builder.buildCameraMediaQuery('events', { eventsSubtype: 'clips' });
assert(query);
expect(query.getNodes()).toHaveLength(1);
expect(query.getNodes()[0]).toMatchObject({
type: QueryType.Event,
cameraIDs: new Set(['capable']),
});
});
it('should build folder query when camera media type is folder', () => {
const { cameraManager, foldersManager, store } = createMocks();
store.getCameraIDs.mockReturnValue(new Set(['cam1']));
store.getCameraConfig.mockReturnValue(
createCameraConfig({
media: { type: 'folder', folders: ['good', 'bad'] },
}),
);
foldersManager.getFolder.mockImplementation((id) => {
if (id === 'good') {
return createFolder({ id });
}
return null;
});
foldersManager.getDefaultQueryParameters.mockImplementation((folder) => {
return folder ? createFolderQueryParams(folder, [{ ha: { id: 'id' } }]) : null;
});
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
const query = builder.buildDefaultCameraQuery();
assert(query);
// Expected result: good included, bad skipped.
expect(query.getNodes()).toHaveLength(1);
expect(query.getFolderQueries('good')).toHaveLength(1);
});
});
describe('default parameters merging', () => {
+9
View File
@@ -96,6 +96,15 @@ describe('UnifiedQuery', () => {
expect(query.getMediaQueries({ cameraID: 'garage' })).toHaveLength(0);
});
it('should filter media queries by type', () => {
const query = new UnifiedQuery();
query.addNode(createEventQuery('front'));
query.addNode(createRecordingQuery('front'));
expect(query.getMediaQueries({ type: QueryType.Event })).toHaveLength(1);
expect(query.getMediaQueries({ type: QueryType.Recording })).toHaveLength(1);
});
it('should check hasMediaQueriesOfType', () => {
const query = new UnifiedQuery();
query.addNode(createEventQuery('front'));
+82
View File
@@ -0,0 +1,82 @@
import { describe, expect, it } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { ViewFolder } from '../../../src/view/item';
import { UnifiedQuery } from '../../../src/view/unified-query';
import {
getReviewedQueryFilterFromConfig,
getReviewedQueryFilterFromQuery,
} from '../../../src/view/utils/query-filter';
import { createEventQuery, createFolder, TestViewMedia } from '../../test-utils';
describe('query-filter', () => {
describe('getReviewedQueryFilterFromQuery', () => {
it('should return undefined if query is missing', () => {
expect(getReviewedQueryFilterFromQuery(null)).toBeUndefined();
});
it('should return undefined if item is missing', () => {
expect(getReviewedQueryFilterFromQuery(mock<UnifiedQuery>())).toBeUndefined();
});
it('should return undefined if item is not media', () => {
const query = mock<UnifiedQuery>();
const item = new ViewFolder(createFolder(), []);
expect(getReviewedQueryFilterFromQuery(query, item)).toBeUndefined();
});
it('should return undefined if item has no cameraID', () => {
const query = mock<UnifiedQuery>();
const item = new TestViewMedia({ cameraID: null });
expect(getReviewedQueryFilterFromQuery(query, item)).toBeUndefined();
});
it('should return undefined if zero or multiple media queries match', () => {
const query = mock<UnifiedQuery>();
const item = new TestViewMedia({ cameraID: 'camera-1' });
query.getMediaQueries.mockReturnValue([]);
expect(getReviewedQueryFilterFromQuery(query, item)).toBeUndefined();
query.getMediaQueries.mockReturnValue([
createEventQuery('camera-1'),
createEventQuery('camera-2'),
]);
expect(getReviewedQueryFilterFromQuery(query, item)).toBeUndefined();
});
it('should return boolean if exactly one media query matches', () => {
const query = mock<UnifiedQuery>();
const item = new TestViewMedia({ cameraID: 'camera-1' });
query.getMediaQueries.mockReturnValue([
createEventQuery('camera-1', { reviewed: true }),
]);
expect(getReviewedQueryFilterFromQuery(query, item)).toBe(true);
query.getMediaQueries.mockReturnValue([
createEventQuery('camera-1', { reviewed: false }),
]);
expect(getReviewedQueryFilterFromQuery(query, item)).toBe(false);
query.getMediaQueries.mockReturnValue([createEventQuery('camera-1')]);
expect(getReviewedQueryFilterFromQuery(query, item)).toBeUndefined();
});
});
describe('getReviewedQueryFilterFromConfig', () => {
it('should return true for reviewed', () => {
expect(getReviewedQueryFilterFromConfig('reviewed')).toBe(true);
});
it('should return undefined for all', () => {
expect(getReviewedQueryFilterFromConfig('all')).toBeUndefined();
expect(getReviewedQueryFilterFromConfig(undefined)).toBe(false);
});
it('should return false for unreviewed', () => {
expect(getReviewedQueryFilterFromConfig('unreviewed')).toBe(false);
});
});
});