fix: Improve review media filtering (#2322)
This commit is contained in:
1 parent
fc32727860
commit
f4e905a7fb
37 files changed
+884
-253
No files matched your search
@@ -822,6 +822,110 @@ describe('CameraManager', () => {
|
||||
|
||||
expect(await manager.executeMediaQueries([baseEventQuery])).toEqual([]);
|
||||
});
|
||||
|
||||
describe('should merge compatible queries', () => {
|
||||
it('merges queries with identical properties', async () => {
|
||||
const api = createCardAPI();
|
||||
const engine = mock<CameraManagerEngine>();
|
||||
vi.mocked(engine.getEngineType).mockReturnValue(Engine.Generic);
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||
|
||||
const manager = createCameraManager(api, engine, [
|
||||
{ config: createCameraConfig({ ...baseCameraConfig, id: 'cam1' }) },
|
||||
{ config: createCameraConfig({ ...baseCameraConfig, id: 'cam2' }) },
|
||||
]);
|
||||
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
|
||||
|
||||
const query1: EventQuery = {
|
||||
source: QuerySource.Camera,
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set(['cam1']),
|
||||
reviewed: false,
|
||||
};
|
||||
const query2: EventQuery = {
|
||||
source: QuerySource.Camera,
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set(['cam2']),
|
||||
reviewed: false,
|
||||
};
|
||||
|
||||
engine.getEvents.mockResolvedValue(new Map());
|
||||
engine.generateMediaFromEvents.mockReturnValue([]);
|
||||
|
||||
await manager.executeMediaQueries([query1, query2]);
|
||||
|
||||
// Should be called once with merged cameraIDs
|
||||
expect(engine.getEvents).toHaveBeenCalledTimes(1);
|
||||
expect(engine.getEvents).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
cameraIDs: new Set(['cam1', 'cam2']),
|
||||
reviewed: false,
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not merge queries with different properties', async () => {
|
||||
const api = createCardAPI();
|
||||
const engine = mock<CameraManagerEngine>();
|
||||
vi.mocked(engine.getEngineType).mockReturnValue(Engine.Generic);
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||
|
||||
const manager = createCameraManager(api, engine, [
|
||||
{ config: createCameraConfig({ ...baseCameraConfig, id: 'cam1' }) },
|
||||
{ config: createCameraConfig({ ...baseCameraConfig, id: 'cam2' }) },
|
||||
]);
|
||||
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
|
||||
|
||||
const query1: EventQuery = {
|
||||
source: QuerySource.Camera,
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set(['cam1']),
|
||||
reviewed: false,
|
||||
};
|
||||
const query2: EventQuery = {
|
||||
source: QuerySource.Camera,
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set(['cam2']),
|
||||
reviewed: true,
|
||||
};
|
||||
|
||||
engine.getEvents.mockResolvedValue(new Map());
|
||||
engine.generateMediaFromEvents.mockReturnValue([]);
|
||||
|
||||
await manager.executeMediaQueries([query1, query2]);
|
||||
|
||||
// Should be called twice for different queries
|
||||
expect(engine.getEvents).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('handles single query without merging', async () => {
|
||||
const api = createCardAPI();
|
||||
const engine = mock<CameraManagerEngine>();
|
||||
vi.mocked(engine.getEngineType).mockReturnValue(Engine.Generic);
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||
|
||||
const manager = createCameraManager(api, engine);
|
||||
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
|
||||
|
||||
engine.getEvents.mockResolvedValue(
|
||||
new Map([[baseEventQuery, baseEventQueryResults]]),
|
||||
);
|
||||
engine.generateMediaFromEvents.mockReturnValue([]);
|
||||
|
||||
await manager.executeMediaQueries([baseEventQuery]);
|
||||
|
||||
expect(engine.getEvents).toHaveBeenCalledTimes(1);
|
||||
expect(engine.getEvents).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
baseEventQuery,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('should extend media queries', () => {
|
||||
|
||||
@@ -1,11 +1,41 @@
|
||||
import { assert, describe, expect, it, vi } from 'vitest';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { SetReviewAction } from '../../../../src/card-controller/actions/actions/set-review';
|
||||
import { ViewMedia, ViewMediaType } from '../../../../src/view/item';
|
||||
import { ViewMediaType } from '../../../../src/view/item';
|
||||
import { QueryResults } from '../../../../src/view/query-results';
|
||||
import { createCardAPI, createView, TestViewMedia } from '../../../test-utils';
|
||||
|
||||
describe('SetReviewAction', () => {
|
||||
it('should mark item as reviewed', async () => {
|
||||
it('should toggle item reviewed state', async () => {
|
||||
const api = createCardAPI();
|
||||
const item = new TestViewMedia({
|
||||
cameraID: 'camera.office',
|
||||
mediaType: 'review' as ViewMediaType,
|
||||
reviewed: false,
|
||||
});
|
||||
|
||||
const queryResults = new QueryResults({ results: [item], selectedIndex: 0 });
|
||||
const view = createView({ queryResults });
|
||||
vi.mocked(api.getViewManager().getView).mockReturnValue(view);
|
||||
|
||||
const action = new SetReviewAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'set_review',
|
||||
},
|
||||
);
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getViewItemManager().reviewMedia).toBeCalledWith(item, true);
|
||||
|
||||
// toggleReviewed mutates the item in-place
|
||||
expect(item.isReviewed()).toBe(true);
|
||||
|
||||
// Verify UI update is triggered to refresh menu icon
|
||||
expect(api.getCardElementManager().update).toBeCalled();
|
||||
});
|
||||
|
||||
it('should set reviewed to true when requested and currently false', async () => {
|
||||
const api = createCardAPI();
|
||||
const item = new TestViewMedia({
|
||||
cameraID: 'camera.office',
|
||||
@@ -28,22 +58,10 @@ describe('SetReviewAction', () => {
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getViewItemManager().reviewMedia).toBeCalledWith(item, true);
|
||||
|
||||
// Original item is NOT mutated; a clone is created and replaced.
|
||||
expect(item.isReviewed()).toBe(false);
|
||||
|
||||
const setViewParams = vi.mocked(api.getViewManager().setViewByParameters).mock
|
||||
.calls[0][0];
|
||||
const newResults = setViewParams?.params?.queryResults;
|
||||
expect(newResults).toBeInstanceOf(QueryResults);
|
||||
|
||||
const newItem = newResults?.getSelectedResult();
|
||||
expect(newItem).not.toBe(item);
|
||||
assert(newItem instanceof ViewMedia);
|
||||
expect(newItem?.isReviewed()).toBe(true);
|
||||
expect(item.isReviewed()).toBe(true);
|
||||
});
|
||||
|
||||
it('should toggle review status when reviewed is not specified', async () => {
|
||||
it('should not act when requested state matches current state', async () => {
|
||||
const api = createCardAPI();
|
||||
const item = new TestViewMedia({
|
||||
cameraID: 'camera.office',
|
||||
@@ -60,25 +78,12 @@ describe('SetReviewAction', () => {
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'set_review',
|
||||
reviewed: true,
|
||||
},
|
||||
);
|
||||
await action.execute(api);
|
||||
|
||||
// Toggle: isReviewed was true, so reviewed should be set to false.
|
||||
expect(api.getViewItemManager().reviewMedia).toBeCalledWith(item, false);
|
||||
|
||||
// Original item is NOT mutated; a clone is created and replaced.
|
||||
expect(item.isReviewed()).toBe(true);
|
||||
|
||||
const setViewParams = vi.mocked(api.getViewManager().setViewByParameters).mock
|
||||
.calls[0][0];
|
||||
const newResults = setViewParams?.params?.queryResults;
|
||||
expect(newResults).toBeInstanceOf(QueryResults);
|
||||
|
||||
const newItem = newResults?.getSelectedResult();
|
||||
expect(newItem).not.toBe(item);
|
||||
assert(newItem instanceof ViewMedia);
|
||||
expect(newItem?.isReviewed()).toBe(false);
|
||||
expect(api.getViewItemManager().reviewMedia).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should not act on non-review media', async () => {
|
||||
@@ -139,4 +144,32 @@ describe('SetReviewAction', () => {
|
||||
|
||||
expect(api.getViewItemManager().reviewMedia).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should not update UI if review action fails', async () => {
|
||||
const api = createCardAPI();
|
||||
const item = new TestViewMedia({
|
||||
cameraID: 'camera.office',
|
||||
mediaType: 'review' as ViewMediaType,
|
||||
reviewed: false,
|
||||
});
|
||||
|
||||
const queryResults = new QueryResults({ results: [item], selectedIndex: 0 });
|
||||
const view = createView({ queryResults });
|
||||
vi.mocked(api.getViewManager().getView).mockReturnValue(view);
|
||||
vi.mocked(api.getViewItemManager().reviewMedia).mockRejectedValue(
|
||||
new Error('error'),
|
||||
);
|
||||
|
||||
const action = new SetReviewAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'set_review',
|
||||
},
|
||||
);
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getViewItemManager().reviewMedia).toBeCalledWith(item, true);
|
||||
expect(api.getCardElementManager().update).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
@@ -2,11 +2,16 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { EffectsController } from '../../../src/components-lib/effects/effects-controller';
|
||||
import { EffectComponent } from '../../../src/components-lib/effects/types';
|
||||
import { EffectName } from '../../../src/types';
|
||||
import { flushPromises } from '../../test-utils';
|
||||
|
||||
vi.mock('../../../src/components/effects/fireworks', () => ({
|
||||
AdvancedCameraCardEffectFireworks: vi.fn(() => createMockEffectComponent()),
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/components/effects/check', () => ({
|
||||
AdvancedCameraCardEffectCheck: vi.fn(() => createMockEffectComponent()),
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/components/effects/ghost', () => ({
|
||||
AdvancedCameraCardEffectGhost: vi.fn(() => createMockEffectComponent()),
|
||||
}));
|
||||
@@ -39,10 +44,12 @@ describe('EffectsController', () => {
|
||||
container = document.createElement('div');
|
||||
controller = new EffectsController();
|
||||
controller.setContainer(container);
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('startEffect', () => {
|
||||
@@ -86,6 +93,40 @@ describe('EffectsController', () => {
|
||||
expect(container.children.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should load and start check effect', async () => {
|
||||
expect(container.children.length).toBe(0);
|
||||
|
||||
await controller.startEffect('check');
|
||||
|
||||
expect(container.children.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should stop effect automatically after duration', async () => {
|
||||
const effectPromise = controller.startEffect('snow', { duration: 1 });
|
||||
|
||||
// Wait for the effect to be added to the DOM.
|
||||
await flushPromises();
|
||||
|
||||
expect(container.children.length).toBe(1);
|
||||
|
||||
vi.advanceTimersByTime(1000);
|
||||
await effectPromise;
|
||||
|
||||
expect(container.children.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should clean up timer when stopped manually', async () => {
|
||||
// Don't await startEffect as it waits for the duration
|
||||
controller.startEffect('snow', { duration: 10 });
|
||||
|
||||
// Wait for the effect to be added to the DOM.
|
||||
await flushPromises();
|
||||
|
||||
await controller.stopEffect('snow');
|
||||
|
||||
expect(container.children.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should set fadeIn to true by default', async () => {
|
||||
await controller.startEffect('snow');
|
||||
|
||||
|
||||
@@ -19,16 +19,15 @@ describe('MediaActions', () => {
|
||||
describe('toggleReviewed', () => {
|
||||
it('should return false if item is not review', async () => {
|
||||
const item = new TestViewMedia({ mediaType: ViewMediaType.Clip });
|
||||
const options = { viewItemManager: mock<ViewItemManager>() };
|
||||
const viewItemManager = mock<ViewItemManager>();
|
||||
|
||||
expect(await toggleReviewed(item, options)).toBe(false);
|
||||
expect(await toggleReviewed(item, viewItemManager)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if manager is missing', async () => {
|
||||
const item = new TestViewMedia({ mediaType: ViewMediaType.Review });
|
||||
const options = {};
|
||||
|
||||
expect(await toggleReviewed(item, options)).toBe(false);
|
||||
expect(await toggleReviewed(item)).toBe(false);
|
||||
});
|
||||
|
||||
it('should toggle review status and update view', async () => {
|
||||
@@ -48,9 +47,9 @@ describe('MediaActions', () => {
|
||||
queryResults.clone.mockReturnValue(queryResults);
|
||||
queryResults.removeItem.mockReturnValue(queryResults);
|
||||
|
||||
const options = { viewItemManager, viewManagerEpoch };
|
||||
|
||||
expect(await toggleReviewed(item, options)).toBe(true);
|
||||
expect(await toggleReviewed(item, viewItemManager, viewManagerEpoch, false)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(viewItemManager.reviewMedia).toHaveBeenCalledWith(item, true);
|
||||
expect(item.isReviewed()).toBe(true);
|
||||
expect(viewManager.setViewByParameters).toHaveBeenCalled();
|
||||
@@ -70,9 +69,9 @@ describe('MediaActions', () => {
|
||||
viewManager.getView.mockReturnValue(view);
|
||||
view.queryResults = null;
|
||||
|
||||
const options = { viewItemManager, viewManagerEpoch };
|
||||
|
||||
expect(await toggleReviewed(item, options)).toBe(true);
|
||||
expect(await toggleReviewed(item, viewItemManager, viewManagerEpoch, false)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(viewManager.setViewByParameters).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -86,9 +85,8 @@ describe('MediaActions', () => {
|
||||
viewItemManager.reviewMedia.mockRejectedValue(error);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const options = { viewItemManager };
|
||||
|
||||
expect(await toggleReviewed(item, options)).toBe(false);
|
||||
expect(await toggleReviewed(item, viewItemManager)).toBe(false);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(error.message);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
@@ -97,16 +95,17 @@ describe('MediaActions', () => {
|
||||
|
||||
describe('toggleFavorite', () => {
|
||||
it('should return false if item is not media', async () => {
|
||||
const options = { viewItemManager: mock<ViewItemManager>() };
|
||||
const viewItemManager = mock<ViewItemManager>();
|
||||
|
||||
expect(await toggleFavorite(null as unknown as ViewItem, options)).toBe(false);
|
||||
expect(await toggleFavorite(null as unknown as ViewItem, viewItemManager)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false if manager is missing', async () => {
|
||||
const item = new TestViewMedia({ mediaType: ViewMediaType.Clip });
|
||||
const options = {};
|
||||
|
||||
expect(await toggleFavorite(item, options)).toBe(false);
|
||||
expect(await toggleFavorite(item)).toBe(false);
|
||||
});
|
||||
|
||||
it('should toggle favorite status', async () => {
|
||||
@@ -115,9 +114,8 @@ describe('MediaActions', () => {
|
||||
favorite: false,
|
||||
});
|
||||
const viewItemManager = mock<ViewItemManager>();
|
||||
const options = { viewItemManager };
|
||||
|
||||
expect(await toggleFavorite(item, options)).toBe(true);
|
||||
expect(await toggleFavorite(item, viewItemManager)).toBe(true);
|
||||
expect(viewItemManager.favorite).toHaveBeenCalledWith(item, true);
|
||||
});
|
||||
|
||||
@@ -131,9 +129,8 @@ describe('MediaActions', () => {
|
||||
viewItemManager.favorite.mockRejectedValue(error);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const options = { viewItemManager };
|
||||
|
||||
expect(await toggleFavorite(item, options)).toBe(false);
|
||||
expect(await toggleFavorite(item, viewItemManager)).toBe(false);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(error.message);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
@@ -143,17 +140,15 @@ describe('MediaActions', () => {
|
||||
describe('downloadMedia', () => {
|
||||
it('should return false if manager is missing', async () => {
|
||||
const item = new TestViewMedia({ mediaType: ViewMediaType.Clip });
|
||||
const options = {};
|
||||
|
||||
expect(await downloadMedia(item, options)).toBe(false);
|
||||
expect(await downloadMedia(item)).toBe(false);
|
||||
});
|
||||
|
||||
it('should download media', async () => {
|
||||
const item = new TestViewMedia({ mediaType: ViewMediaType.Clip });
|
||||
const viewItemManager = mock<ViewItemManager>();
|
||||
const options = { viewItemManager };
|
||||
|
||||
expect(await downloadMedia(item, options)).toBe(true);
|
||||
expect(await downloadMedia(item, viewItemManager)).toBe(true);
|
||||
expect(viewItemManager.download).toHaveBeenCalledWith(item);
|
||||
});
|
||||
|
||||
@@ -164,9 +159,8 @@ describe('MediaActions', () => {
|
||||
viewItemManager.download.mockRejectedValue(error);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const options = { viewItemManager };
|
||||
|
||||
expect(await downloadMedia(item, options)).toBe(false);
|
||||
expect(await downloadMedia(item, viewItemManager)).toBe(false);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(error.message);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
@@ -176,9 +170,8 @@ describe('MediaActions', () => {
|
||||
describe('navigateToTimeline', () => {
|
||||
it('should return early if epoch is missing', () => {
|
||||
const item = new TestViewMedia({ mediaType: ViewMediaType.Clip });
|
||||
const options = {};
|
||||
|
||||
expect(navigateToTimeline(item, options)).toBeUndefined();
|
||||
expect(navigateToTimeline(item)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should navigate to timeline with correct parameters', () => {
|
||||
@@ -199,9 +192,7 @@ describe('MediaActions', () => {
|
||||
return queryResults;
|
||||
});
|
||||
|
||||
const options = { viewManagerEpoch };
|
||||
|
||||
navigateToTimeline(item, options);
|
||||
navigateToTimeline(item, viewManagerEpoch);
|
||||
|
||||
expect(viewManager.setViewByParameters).toHaveBeenCalledWith({
|
||||
params: {
|
||||
@@ -220,9 +211,7 @@ describe('MediaActions', () => {
|
||||
viewManagerEpoch.manager = viewManager;
|
||||
viewManager.getView.mockReturnValue(null);
|
||||
|
||||
const options = { viewManagerEpoch };
|
||||
|
||||
navigateToTimeline(item, options);
|
||||
navigateToTimeline(item, viewManagerEpoch);
|
||||
|
||||
expect(viewManager.setViewByParameters).toHaveBeenCalledWith({
|
||||
params: {
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user