feat: Implement basic general folder support (#2051)

- Related: #1748
This commit is contained in:
Dermot Duffy
2025-05-21 19:59:21 -07:00
committed by GitHub
parent 2eb0d9e35e
commit c6a4c8aea2
350 changed files with 12837 additions and 4509 deletions
@@ -0,0 +1,282 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { ViewItemManager } from '../../../src/card-controller/view/item-manager';
import { homeAssistantSignPath } from '../../../src/ha/sign-path.js';
import { downloadURL } from '../../../src/utils/download';
import { ViewFolder, ViewMediaType } from '../../../src/view/item';
import {
createCardAPI,
createFolder,
createHASS,
TestViewMedia,
} from '../../test-utils';
vi.mock('../../../src/utils/download');
vi.mock('../../../src/ha/sign-path.js');
describe('ViewItemManager', () => {
describe('getCapabilities', () => {
it('should return null for unsupported item types', () => {
const api = createCardAPI();
const manager = new ViewItemManager(api);
expect(manager.getCapabilities(new TestViewMedia({ cameraID: null }))).toBeNull();
});
it('should return capabilities for camera media', () => {
const api = createCardAPI();
const capabilities = {
canDownload: true,
canFavorite: true,
};
vi.mocked(api.getCameraManager().getMediaCapabilities).mockReturnValue(
capabilities,
);
const manager = new ViewItemManager(api);
const item = new TestViewMedia({ cameraID: 'camera.office' });
expect(manager.getCapabilities(item)).toEqual(capabilities);
});
it('should return capabilities for folder media', () => {
const api = createCardAPI();
const capabilities = {
canDownload: true,
canFavorite: true,
};
vi.mocked(api.getFoldersManager().getItemCapabilities).mockReturnValue(
capabilities,
);
const manager = new ViewItemManager(api);
const item = new TestViewMedia({ folder: createFolder() });
expect(manager.getCapabilities(item)).toEqual(capabilities);
});
});
describe('download', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('should not download without hass', async () => {
const api = createCardAPI();
const manager = new ViewItemManager(api);
const item = new TestViewMedia({ cameraID: 'camera.office' });
expect(await manager.download(item)).toBe(true);
expect(downloadURL).not.toHaveBeenCalled();
});
it('should throw error when signing fails', async () => {
const consoleSpy = vi.spyOn(global.console, 'warn').mockReturnValue(undefined);
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = new ViewItemManager(api);
const item = new TestViewMedia({ cameraID: 'camera.office' });
vi.mocked(api.getCameraManager().getMediaDownloadPath).mockResolvedValue({
sign: true,
endpoint: 'foo',
});
const signError = new Error('sign-error');
vi.mocked(homeAssistantSignPath).mockRejectedValue(signError);
expect(await manager.download(item)).toBe(false);
expect(api.getMessageManager().setErrorIfHigherPriority).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Could not sign media URL for download',
}),
);
expect(consoleSpy).toHaveBeenCalledWith('sign-error');
});
it('should download camera media', async () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = new ViewItemManager(api);
const item = new TestViewMedia({ cameraID: 'camera.office' });
vi.mocked(api.getCameraManager().getMediaDownloadPath).mockResolvedValue({
sign: true,
endpoint: 'foo',
});
vi.mocked(homeAssistantSignPath).mockResolvedValue('http://foo/signed-url');
expect(await manager.download(item)).toBe(true);
expect(downloadURL).toBeCalledWith(
'http://foo/signed-url',
'camera-office_id.mp4',
);
});
it('should download folder media', async () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = new ViewItemManager(api);
const item = new TestViewMedia({ cameraID: null, folder: createFolder() });
vi.mocked(api.getFoldersManager().getDownloadPath).mockResolvedValue({
sign: true,
endpoint: 'foo',
});
vi.mocked(homeAssistantSignPath).mockResolvedValue('http://foo/signed-url');
expect(await manager.download(item)).toBe(true);
expect(downloadURL).toBeCalledWith('http://foo/signed-url', 'media_id.mp4');
});
it('should download media without signing', async () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = new ViewItemManager(api);
const item = new TestViewMedia({ cameraID: 'camera.office' });
vi.mocked(api.getCameraManager().getMediaDownloadPath).mockResolvedValue({
sign: false,
endpoint: 'foo',
});
expect(homeAssistantSignPath).not.toBeCalled();
expect(await manager.download(item)).toBe(true);
expect(downloadURL).toBeCalledWith('foo', 'camera-office_id.mp4');
});
it('should download media without camera or folder', async () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = new ViewItemManager(api);
const item = new TestViewMedia({ cameraID: null, folder: null });
expect(await manager.download(item)).toBe(false);
expect(api.getMessageManager().setErrorIfHigherPriority).toHaveBeenCalledWith(
expect.objectContaining({
message: 'No media to download',
}),
);
});
describe('should generate useful download filenames', () => {
it('should generate filename for media without ID', async () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = new ViewItemManager(api);
const item = new TestViewMedia({ id: null });
vi.mocked(api.getCameraManager().getMediaDownloadPath).mockResolvedValue({
sign: false,
endpoint: 'foo',
});
expect(await manager.download(item)).toBe(true);
expect(downloadURL).toBeCalledWith('foo', 'camera.mp4');
});
it('should generate filename for media with start time', async () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = new ViewItemManager(api);
const item = new TestViewMedia({ startTime: new Date('2025-05-03T17:41:00Z') });
vi.mocked(api.getCameraManager().getMediaDownloadPath).mockResolvedValue({
sign: false,
endpoint: 'foo',
});
expect(await manager.download(item)).toBe(true);
expect(downloadURL).toBeCalledWith('foo', 'camera_id_2025-05-03-17-41-00.mp4');
});
it('should generate filename for snapshot', async () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = new ViewItemManager(api);
const item = new TestViewMedia({ mediaType: ViewMediaType.Snapshot });
vi.mocked(api.getCameraManager().getMediaDownloadPath).mockResolvedValue({
sign: false,
endpoint: 'foo',
});
expect(await manager.download(item)).toBe(true);
expect(downloadURL).toBeCalledWith('foo', 'camera_id.jpg');
});
it('should generate filename for folder without title', async () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = new ViewItemManager(api);
const item = new ViewFolder(createFolder());
vi.mocked(api.getFoldersManager().getDownloadPath).mockResolvedValue({
sign: false,
endpoint: 'foo',
});
expect(await manager.download(item)).toBe(true);
expect(downloadURL).toBeCalledWith('foo', 'media');
});
it('should generate filename for folder with title', async () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = new ViewItemManager(api);
const item = new ViewFolder(createFolder(), { title: 'title' });
vi.mocked(api.getFoldersManager().getDownloadPath).mockResolvedValue({
sign: false,
endpoint: 'foo',
});
expect(await manager.download(item)).toBe(true);
expect(downloadURL).toBeCalledWith('foo', 'title');
});
});
});
describe('favorite', () => {
it('should favorite camera media', async () => {
const api = createCardAPI();
const manager = new ViewItemManager(api);
const item = new TestViewMedia({
cameraID: 'camera.office',
mediaType: ViewMediaType.Snapshot,
});
await manager.favorite(item, true);
expect(api.getCameraManager().favoriteMedia).toBeCalledWith(item, true);
});
it('should favorite folder media', async () => {
const api = createCardAPI();
const manager = new ViewItemManager(api);
const item = new TestViewMedia({
folder: createFolder(),
mediaType: ViewMediaType.Snapshot,
});
await manager.favorite(item, true);
expect(api.getFoldersManager().favorite).toBeCalledWith(item, true);
});
});
});
@@ -2,15 +2,15 @@ import { expect, it } from 'vitest';
import { applyViewModifiers } from '../../../../src/card-controller/view/modifiers';
import { MergeContextViewModifier } from '../../../../src/card-controller/view/modifiers/merge-context';
import { SetQueryViewModifier } from '../../../../src/card-controller/view/modifiers/set-query';
import { EventMediaQueries } from '../../../../src/view/media-queries';
import { MediaQueriesResults } from '../../../../src/view/media-queries-results';
import { EventMediaQuery } from '../../../../src/view/query';
import { QueryResults } from '../../../../src/view/query-results';
import { createView } from '../../../test-utils';
it('should apply view modifiers', () => {
const view = createView();
const query = new EventMediaQueries();
const queryResults = new MediaQueriesResults();
const query = new EventMediaQuery();
const queryResults = new QueryResults();
const context = {
timeline: { window: { start: new Date(), end: new Date() } },
@@ -1,8 +1,8 @@
import { expect, it } from 'vitest';
import { SetQueryViewModifier } from '../../../../src/card-controller/view/modifiers/set-query';
import { EventMediaQuery } from '../../../../src/view/query';
import { QueryResults } from '../../../../src/view/query-results';
import { createView } from '../../../test-utils';
import { EventMediaQueries } from '../../../../src/view/media-queries';
import { MediaQueriesResults } from '../../../../src/view/media-queries-results';
it('should do nothing without arguments', () => {
const view = createView();
@@ -16,8 +16,8 @@ it('should do nothing without arguments', () => {
it('should set query and results', () => {
const view = createView();
const query = new EventMediaQueries();
const queryResults = new MediaQueriesResults();
const query = new EventMediaQuery();
const queryResults = new QueryResults();
const modifier = new SetQueryViewModifier({
query: query,
+101 -34
View File
@@ -1,13 +1,15 @@
import { add } from 'date-fns';
import { describe, expect, it, vi } from 'vitest';
import { QueryType } from '../../../src/camera-manager/types';
import { FolderQuery } from '../../../src/card-controller/folders/types';
import { QueryExecutor } from '../../../src/card-controller/view/query-executor';
import { ClipsOrSnapshotsOrAll } from '../../../src/types';
import { EventMediaQueries } from '../../../src/view/media-queries';
import { EventMediaQuery, FolderViewQuery } from '../../../src/view/query';
import {
TestViewMedia,
createCameraManager,
createCardAPI,
createFolder,
createStore,
generateViewMediaArray,
} from '../../test-utils';
@@ -59,7 +61,7 @@ describe('executeDefaultEventQuery', () => {
eventsMediaType: mediaType,
});
expect(results?.query.getQueries()).toEqual(rawQueries);
expect(results?.query.getQuery()).toEqual(rawQueries);
expect(results?.queryResults.getResults()).toEqual(media);
expect(api.getCameraManager().generateDefaultEventQueries).toBeCalledWith(
new Set(['camera.office']),
@@ -90,7 +92,7 @@ describe('executeDefaultEventQuery', () => {
const executor = new QueryExecutor(api);
const results = await executor.executeDefaultEventQuery();
expect(results?.query.getQueries()).toEqual(rawQueries);
expect(results?.query.getQuery()).toEqual(rawQueries);
expect(results?.queryResults.getResults()).toEqual(media);
expect(api.getCameraManager().generateDefaultEventQueries).toBeCalledWith(
new Set(['camera.office', 'camera.kitchen']),
@@ -170,7 +172,7 @@ describe('executeDefaultRecordingQuery', () => {
cameraID: 'camera.office',
});
expect(results?.query.getQueries()).toEqual(rawQueries);
expect(results?.query.getQuery()).toEqual(rawQueries);
expect(results?.queryResults.getResults()).toEqual(media);
expect(api.getCameraManager().generateDefaultRecordingQueries).toBeCalledWith(
new Set(['camera.office']),
@@ -197,7 +199,7 @@ describe('executeDefaultRecordingQuery', () => {
const executor = new QueryExecutor(api);
const results = await executor.executeDefaultRecordingQuery();
expect(results?.query.getQueries()).toEqual(rawQueries);
expect(results?.query.getQuery()).toEqual(rawQueries);
expect(results?.queryResults.getResults()).toEqual(media);
expect(api.getCameraManager().generateDefaultRecordingQueries).toBeCalledWith(
new Set(['camera.office', 'camera.kitchen']),
@@ -233,10 +235,10 @@ describe('executeDefaultRecordingQuery', () => {
});
});
describe('execute', () => {
describe('executeQuery', () => {
it('should return null when query is empty', async () => {
const executor = new QueryExecutor(createCardAPI());
expect(await executor.execute(new EventMediaQueries())).toBeNull();
expect(await executor.executeQuery(new EventMediaQuery())).toBeNull();
});
describe('should handle result rejections', () => {
@@ -245,23 +247,7 @@ describe('execute', () => {
const media = generateViewMediaArray();
vi.mocked(api.getCameraManager().executeMediaQueries).mockResolvedValue(media);
const query = new EventMediaQueries([
{
type: QueryType.Event as const,
cameraIDs: new Set(['camera.office']),
},
]);
const executor = new QueryExecutor(api);
expect(await executor.execute(query, { rejectResults: (_) => true })).toBeNull();
});
it('not rejected', async () => {
const api = createPopulatedAPI();
const media = generateViewMediaArray();
vi.mocked(api.getCameraManager().executeMediaQueries).mockResolvedValue(media);
const query = new EventMediaQueries([
const query = new EventMediaQuery([
{
type: QueryType.Event as const,
cameraIDs: new Set(['camera.office']),
@@ -270,7 +256,27 @@ describe('execute', () => {
const executor = new QueryExecutor(api);
expect(
await executor.execute(query, { rejectResults: (_) => false }),
// eslint-disable-next-line @typescript-eslint/no-unused-vars
await executor.executeQuery(query, { rejectResults: (_) => true }),
).toBeNull();
});
it('not rejected', async () => {
const api = createPopulatedAPI();
const media = generateViewMediaArray();
vi.mocked(api.getCameraManager().executeMediaQueries).mockResolvedValue(media);
const query = new EventMediaQuery([
{
type: QueryType.Event as const,
cameraIDs: new Set(['camera.office']),
},
]);
const executor = new QueryExecutor(api);
expect(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
await executor.executeQuery(query, { rejectResults: (_) => false }),
).not.toBeNull();
});
});
@@ -284,7 +290,7 @@ describe('execute', () => {
});
vi.mocked(api.getCameraManager().executeMediaQueries).mockResolvedValue(media);
const query = new EventMediaQueries([
const query = new EventMediaQuery([
{
type: QueryType.Event as const,
cameraIDs: new Set(['camera.office']),
@@ -292,10 +298,12 @@ describe('execute', () => {
]);
const executor = new QueryExecutor(api);
const results = await executor.execute(query, {
const results = await executor.executeMediaQuery(query, {
selectResult: { id: 'id-camera.office-42' },
});
expect(results?.getSelectedResult()?.getID()).toBe('id-camera.office-42');
expect(results?.queryResults.getSelectedResult()?.getID()).toBe(
'id-camera.office-42',
);
});
});
@@ -307,7 +315,7 @@ describe('execute', () => {
});
vi.mocked(api.getCameraManager().executeMediaQueries).mockResolvedValue(media);
const query = new EventMediaQueries([
const query = new EventMediaQuery([
{
type: QueryType.Event as const,
cameraIDs: new Set(['camera.office']),
@@ -315,10 +323,12 @@ describe('execute', () => {
]);
const executor = new QueryExecutor(api);
const results = await executor.execute(query, {
const results = await executor.executeMediaQuery(query, {
selectResult: { func: (media) => media.getID() === 'id-camera.office-43' },
});
expect(results?.getSelectedResult()?.getID()).toBe('id-camera.office-43');
expect(results?.queryResults.getSelectedResult()?.getID()).toBe(
'id-camera.office-43',
);
});
it('by time', async () => {
@@ -344,7 +354,7 @@ describe('execute', () => {
];
vi.mocked(api.getCameraManager().executeMediaQueries).mockResolvedValue(media);
const query = new EventMediaQueries([
const query = new EventMediaQuery([
{
type: QueryType.Event as const,
cameraIDs: new Set(['camera.office']),
@@ -352,9 +362,66 @@ describe('execute', () => {
]);
const executor = new QueryExecutor(api);
const results = await executor.execute(query, {
const results = await executor.executeMediaQuery(query, {
selectResult: { time: { time: add(now, { seconds: 1 }) } },
});
expect(results?.getSelectedResult()?.getID()).toBe('id-camera.office-1');
expect(results?.queryResults.getSelectedResult()?.getID()).toBe(
'id-camera.office-1',
);
});
describe('should handle folder query', () => {
it('should return null without raw query', async () => {
const executor = new QueryExecutor(createCardAPI());
expect(await executor.executeQuery(new FolderViewQuery())).toBeNull();
});
it('should return null when query expansion fails', async () => {
const api = createCardAPI();
vi.mocked(api.getFoldersManager().getFolder).mockReturnValue(createFolder());
vi.mocked(api.getFoldersManager().expandFolder).mockResolvedValue(null);
const executor = new QueryExecutor(api);
expect(await executor.executeDefaultFolderQuery()).toBeNull();
});
});
});
describe('executeDefaultFolderQuery', () => {
it('should return null without folders', async () => {
const executor = new QueryExecutor(createCardAPI());
expect(await executor.executeDefaultFolderQuery()).toBeNull();
});
it('should execute query against first folder', async () => {
const api = createCardAPI();
const items = [new TestViewMedia()];
const folder = createFolder();
const query: FolderQuery = {
folder,
path: ['path'],
};
vi.mocked(api.getFoldersManager().generateDefaultFolderQuery).mockReturnValue(query);
vi.mocked(api.getFoldersManager().expandFolder).mockResolvedValue(items);
const executor = new QueryExecutor(api);
const result = await executor.executeDefaultFolderQuery();
expect(result?.query.getQuery()).toEqual(query);
expect(result?.queryResults.getResults()).toEqual(items);
});
it('should return null without folder results', async () => {
const api = createCardAPI();
const folder = createFolder();
const query: FolderQuery = {
folder,
path: ['path'],
};
vi.mocked(api.getFoldersManager().generateDefaultFolderQuery).mockReturnValue(query);
vi.mocked(api.getFoldersManager().expandFolder).mockResolvedValue(null);
const executor = new QueryExecutor(api);
expect(await executor.executeDefaultFolderQuery()).toBeNull();
});
});
+71
View File
@@ -0,0 +1,71 @@
import { describe, expect, it } from 'vitest';
import { sortItems } from '../../../src/card-controller/view/sort.js';
import { createFolder, TestViewMedia } from '../../test-utils.js';
import { ViewFolder } from '../../../src/view/item.js';
describe('sortMedia', () => {
const media_1 = new TestViewMedia({
id: 'id-1',
startTime: new Date('2023-04-29T14:25'),
cameraID: 'camera-1',
});
const media_2 = new TestViewMedia({
id: 'id-2',
startTime: new Date('2023-04-29T14:26'),
cameraID: 'camera-1',
});
const media_3_dup_id = new TestViewMedia({
id: 'id-2',
startTime: new Date('2023-04-29T14:26'),
cameraID: 'camera-1',
});
const media_4_no_id = new TestViewMedia({
id: null,
startTime: new Date('2023-04-29T14:27'),
cameraID: 'camera-1',
});
const folder_1 = new ViewFolder(createFolder(), { id: 'folder_1' });
const folder_2 = new ViewFolder(createFolder(), { id: 'folder_2' });
it('should sort sorted media', () => {
const media = [media_1, media_2];
expect(sortItems(media)).toEqual(media);
});
it('should sort unsorted media', () => {
expect(sortItems([media_2, media_1])).toEqual([media_1, media_2]);
});
it('should remove duplicate id', () => {
expect(sortItems([media_1, media_2, media_3_dup_id])).toEqual([media_1, media_2]);
});
it('should sort by id when time not available', () => {
const folder = createFolder();
expect(
sortItems([
new TestViewMedia({ id: 'snake' }),
new TestViewMedia({ id: 'zebra' }),
new TestViewMedia({ id: 'aardvark' }),
new ViewFolder(folder, { id: 'folder' }),
]),
).toEqual([
new ViewFolder(folder, { id: 'folder' }),
new TestViewMedia({ id: 'aardvark' }),
new TestViewMedia({ id: 'snake' }),
new TestViewMedia({ id: 'zebra' }),
]);
});
it('should remove de-duplicate by object if no id', () => {
expect(sortItems([media_1, media_2, media_4_no_id, media_4_no_id])).toEqual([
media_1,
media_2,
media_4_no_id,
]);
});
it('should sort folders at the start', () => {
expect(sortItems([media_1, media_2, folder_1, folder_2])).toEqual([
folder_1,
folder_2,
media_1,
media_2,
]);
});
});
+105 -44
View File
@@ -2,6 +2,7 @@ import { ViewContext } from 'view';
import { describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { QueryType } from '../../../src/camera-manager/types';
import { CardController } from '../../../src/card-controller/controller';
import { ViewFactory } from '../../../src/card-controller/view/factory';
import { SetQueryViewModifier } from '../../../src/card-controller/view/modifiers/set-query';
import {
@@ -11,9 +12,9 @@ import {
import { ViewManager } from '../../../src/card-controller/view/view-manager';
import { ViewQueryExecutor } from '../../../src/card-controller/view/view-query-executor';
import { AdvancedCameraCardView } from '../../../src/config/schema/common/const';
import { ViewMedia } from '../../../src/view/media';
import { EventMediaQueries } from '../../../src/view/media-queries';
import { MediaQueriesResults } from '../../../src/view/media-queries-results';
import { ViewMedia, ViewMediaType } from '../../../src/view/item';
import { EventMediaQuery } from '../../../src/view/query';
import { QueryResults } from '../../../src/view/query-results';
import { View } from '../../../src/view/view';
import {
createCameraManager,
@@ -23,6 +24,14 @@ import {
createView,
} from '../../test-utils';
const createInitializedCardAPI = (initialized?: boolean): CardController => {
const api = createCardAPI();
vi.mocked(api.getInitializationManager().isInitialized).mockReturnValue(
initialized ?? true,
);
return api;
};
describe('should act correctly when view is set', () => {
it('basic view', () => {
const view = createView({
@@ -34,7 +43,7 @@ describe('should act correctly when view is set', () => {
const factory = mock<ViewFactory>();
factory.getViewDefault.mockReturnValue(view);
const api = createCardAPI();
const api = createInitializedCardAPI();
const manager = new ViewManager(api, { viewFactory: factory });
manager.setViewDefault();
@@ -61,7 +70,7 @@ describe('should act correctly when view is set', () => {
const factory = mock<ViewFactory>();
factory.getViewDefault.mockReturnValue(view_1);
const api = createCardAPI();
const api = createInitializedCardAPI();
const manager = new ViewManager(api, { viewFactory: factory });
manager.setViewDefault();
@@ -88,7 +97,7 @@ describe('should act correctly when view is set', () => {
});
it('setViewWithMergedContext', () => {
const api = createCardAPI();
const api = createInitializedCardAPI();
const factory = mock<ViewFactory>();
const manager = new ViewManager(api, { viewFactory: factory });
@@ -120,7 +129,7 @@ it('getEpoch', () => {
it('reset', () => {
const factory = mock<ViewFactory>();
const manager = new ViewManager(createCardAPI(), { viewFactory: factory });
const manager = new ViewManager(createInitializedCardAPI(), { viewFactory: factory });
manager.reset();
expect(manager.getView()).toBeNull();
@@ -138,11 +147,37 @@ it('reset', () => {
expect(manager.hasView()).toBeFalsy();
});
describe('should not set view without cameras being initialized', () => {
it('should ignore requests to set generic view without cameras being initialized', () => {
const factory = mock<ViewFactory>();
factory.getViewDefault.mockReturnValue(createView());
const manager = new ViewManager(createInitializedCardAPI(false), {
viewFactory: factory,
});
manager.setViewDefault();
expect(manager.getView()).toBeNull();
});
it('should ignore requests to set view then modify without cameras being initialized', () => {
const factory = mock<ViewFactory>();
factory.getViewDefault.mockReturnValue(createView());
const manager = new ViewManager(createInitializedCardAPI(false), {
viewFactory: factory,
});
manager.setViewDefaultWithNewQuery();
expect(manager.getView()).toBeNull();
});
});
it('setViewDefault', () => {
const factory = mock<ViewFactory>();
factory.getViewDefault.mockReturnValue(createView());
const manager = new ViewManager(createCardAPI(), { viewFactory: factory });
const manager = new ViewManager(createInitializedCardAPI(), { viewFactory: factory });
manager.setViewDefault();
expect(manager.getView()?.view).toBe('live');
@@ -153,7 +188,7 @@ it('setViewByParameters', () => {
const factory = mock<ViewFactory>();
factory.getViewByParameters.mockReturnValue(createView());
const manager = new ViewManager(createCardAPI(), { viewFactory: factory });
const manager = new ViewManager(createInitializedCardAPI(), { viewFactory: factory });
manager.setViewByParameters();
expect(manager.getView()?.view).toBe('live');
@@ -167,7 +202,7 @@ it('setViewDefaultWithNewQuery', async () => {
const viewQueryExecutor = mock<ViewQueryExecutor>();
viewQueryExecutor.getNewQueryModifiers.mockResolvedValue([]);
const manager = new ViewManager(createCardAPI(), {
const manager = new ViewManager(createInitializedCardAPI(), {
viewFactory: viewFactory,
viewQueryExecutor: viewQueryExecutor,
});
@@ -184,7 +219,7 @@ it('setViewByParametersWithNewQuery', async () => {
const viewQueryExecutor = mock<ViewQueryExecutor>();
viewQueryExecutor.getNewQueryModifiers.mockResolvedValue([]);
const manager = new ViewManager(createCardAPI(), {
const manager = new ViewManager(createInitializedCardAPI(), {
viewFactory: viewFactory,
viewQueryExecutor: viewQueryExecutor,
});
@@ -201,7 +236,7 @@ it('setViewByParametersWithExistingQuery', async () => {
const viewQueryExecutor = mock<ViewQueryExecutor>();
viewQueryExecutor.getNewQueryModifiers.mockResolvedValue([]);
const manager = new ViewManager(createCardAPI(), {
const manager = new ViewManager(createInitializedCardAPI(), {
viewFactory: viewFactory,
viewQueryExecutor: viewQueryExecutor,
});
@@ -220,7 +255,7 @@ describe('should handle exceptions', () => {
throw error;
});
const api = createCardAPI();
const api = createInitializedCardAPI();
const manager = new ViewManager(api, { viewFactory: viewFactory });
manager.setViewDefault();
@@ -235,7 +270,7 @@ describe('should handle exceptions', () => {
throw error;
});
const api = createCardAPI();
const api = createInitializedCardAPI();
const manager = new ViewManager(api, { viewFactory: viewFactory });
await manager.setViewDefaultWithNewQuery();
@@ -250,7 +285,7 @@ describe('should handle exceptions', () => {
const viewQueryExecutor = mock<ViewQueryExecutor>();
viewQueryExecutor.getNewQueryModifiers.mockRejectedValue(error);
const api = createCardAPI();
const api = createInitializedCardAPI();
const manager = new ViewManager(api, {
viewFactory: viewFactory,
viewQueryExecutor: viewQueryExecutor,
@@ -280,7 +315,7 @@ describe('isViewSupportedByCamera', () => {
['timeline' as const, false],
['media' as const, false],
])('%s', (viewName: AdvancedCameraCardView, expected: boolean) => {
const api = createCardAPI();
const api = createInitializedCardAPI();
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
@@ -317,7 +352,9 @@ describe('hasMajorMediaChange', () => {
const factory = mock<ViewFactory>();
factory.getViewDefault.mockReturnValue(createView({ view: 'live' }));
const manager = new ViewManager(createCardAPI(), { viewFactory: factory });
const manager = new ViewManager(createInitializedCardAPI(), {
viewFactory: factory,
});
manager.setViewDefault();
expect(manager.hasMajorMediaChange(createView({ view: 'clips' }))).toBeTruthy();
@@ -327,7 +364,9 @@ describe('hasMajorMediaChange', () => {
const factory = mock<ViewFactory>();
factory.getViewDefault.mockReturnValue(createView({ camera: 'camera-1' }));
const manager = new ViewManager(createCardAPI(), { viewFactory: factory });
const manager = new ViewManager(createInitializedCardAPI(), {
viewFactory: factory,
});
manager.setViewDefault();
expect(manager.hasMajorMediaChange(createView({ camera: 'camera-2' }))).toBeTruthy();
@@ -345,7 +384,9 @@ describe('hasMajorMediaChange', () => {
createView({ context: { live: { overrides: overrides_1 } } }),
);
const manager = new ViewManager(createCardAPI(), { viewFactory: factory });
const manager = new ViewManager(createInitializedCardAPI(), {
viewFactory: factory,
});
manager.setViewDefault();
expect(
@@ -367,7 +408,9 @@ describe('hasMajorMediaChange', () => {
createView({ view: 'clips', context: { live: { overrides: overrides_1 } } }),
);
const manager = new ViewManager(createCardAPI(), { viewFactory: factory });
const manager = new ViewManager(createInitializedCardAPI(), {
viewFactory: factory,
});
manager.setViewDefault();
expect(
@@ -378,16 +421,25 @@ describe('hasMajorMediaChange', () => {
});
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({ results: media, selectedIndex: 0 });
const queryResults_2 = new MediaQueriesResults({ results: media, selectedIndex: 1 });
const media = [
new ViewMedia(ViewMediaType.Clip, {
cameraID: 'camera-1',
}),
new ViewMedia(ViewMediaType.Clip, {
cameraID: 'camera-2',
}),
];
const queryResults_1 = new QueryResults({ results: media, selectedIndex: 0 });
const queryResults_2 = new QueryResults({ results: media, selectedIndex: 1 });
const factory = mock<ViewFactory>();
factory.getViewDefault.mockReturnValue(
createView({ view: 'media', queryResults: queryResults_1 }),
);
const manager = new ViewManager(createCardAPI(), { viewFactory: factory });
const manager = new ViewManager(createInitializedCardAPI(), {
viewFactory: factory,
});
manager.setViewDefault();
expect(
@@ -398,14 +450,23 @@ describe('hasMajorMediaChange', () => {
});
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({ results: media, selectedIndex: 0 });
const queryResults_2 = new MediaQueriesResults({ results: media, selectedIndex: 1 });
const media = [
new ViewMedia(ViewMediaType.Clip, {
cameraID: 'camera-1',
}),
new ViewMedia(ViewMediaType.Clip, {
cameraID: 'camera-2',
}),
];
const queryResults_1 = new QueryResults({ results: media, selectedIndex: 0 });
const queryResults_2 = new QueryResults({ results: media, selectedIndex: 1 });
const factory = mock<ViewFactory>();
factory.getViewDefault.mockReturnValue(createView({ queryResults: queryResults_1 }));
const manager = new ViewManager(createCardAPI(), { viewFactory: factory });
const manager = new ViewManager(createInitializedCardAPI(), {
viewFactory: factory,
});
manager.setViewDefault();
expect(
@@ -424,7 +485,7 @@ describe('should initialize', () => {
const viewFactory = mock<ViewFactory>();
viewFactory.getViewDefault.mockReturnValue(view);
const api = createCardAPI();
const api = createInitializedCardAPI();
const manager = new ViewManager(api, {
viewFactory: viewFactory,
});
@@ -452,14 +513,14 @@ it('should adopt query and results when changing to gallery from viewer', async
const baseView = createView({
view: 'media',
camera: 'camera.office',
query: new EventMediaQueries([
query: new EventMediaQuery([
{
type: QueryType.Event,
cameraIDs: new Set(['camera.office']),
hasClip: true,
},
]),
queryResults: new MediaQueriesResults(),
queryResults: new QueryResults(),
});
const viewFactory = mock<ViewFactory>();
@@ -470,7 +531,7 @@ it('should adopt query and results when changing to gallery from viewer', async
const viewQueryExecutor = mock<ViewQueryExecutor>();
viewQueryExecutor.getNewQueryModifiers.mockResolvedValue([]);
const manager = new ViewManager(createCardAPI(), {
const manager = new ViewManager(createInitializedCardAPI(), {
viewFactory: viewFactory,
viewQueryExecutor: viewQueryExecutor,
});
@@ -496,8 +557,8 @@ describe('should apply async view modifications', () => {
const viewFactory = mock<ViewFactory>();
viewFactory.getViewDefault.mockReturnValue(createView({ view: 'live' }));
const query = new EventMediaQueries();
const queryResults = new MediaQueriesResults();
const query = new EventMediaQuery();
const queryResults = new QueryResults();
const viewQueryExecutor = mock<ViewQueryExecutor>();
viewQueryExecutor.getNewQueryModifiers.mockResolvedValue([
@@ -507,7 +568,7 @@ describe('should apply async view modifications', () => {
}),
]);
const manager = new ViewManager(createCardAPI(), {
const manager = new ViewManager(createInitializedCardAPI(), {
viewFactory: viewFactory,
viewQueryExecutor: viewQueryExecutor,
});
@@ -523,12 +584,12 @@ describe('should apply async view modifications', () => {
const viewFactory = mock<ViewFactory>();
viewFactory.getViewDefault.mockReturnValueOnce(createView({ view: 'live' }));
const query = new EventMediaQueries();
const queryResults = new MediaQueriesResults();
const query = new EventMediaQuery();
const queryResults = new QueryResults();
const viewQueryExecutor = mock<ViewQueryExecutor>();
const manager = new ViewManager(createCardAPI(), {
const manager = new ViewManager(createInitializedCardAPI(), {
viewFactory: viewFactory,
viewQueryExecutor: viewQueryExecutor,
});
@@ -572,7 +633,7 @@ describe('should apply async view modifications', () => {
viewFactory.getViewDefault.mockReturnValueOnce(createView({ view: 'live' }));
const viewQueryExecutor = mock<ViewQueryExecutor>();
const manager = new ViewManager(createCardAPI(), {
const manager = new ViewManager(createInitializedCardAPI(), {
viewFactory: viewFactory,
viewQueryExecutor: viewQueryExecutor,
});
@@ -608,7 +669,7 @@ describe('should apply async view modifications', () => {
viewFactory.getViewDefault.mockReturnValueOnce(createView({ view: 'live' }));
const viewQueryExecutor = mock<ViewQueryExecutor>();
const manager = new ViewManager(createCardAPI(), {
const manager = new ViewManager(createInitializedCardAPI(), {
viewFactory: viewFactory,
viewQueryExecutor: viewQueryExecutor,
});
@@ -644,7 +705,7 @@ describe('should apply async view modifications', () => {
viewFactory.getViewDefault.mockReturnValueOnce(createView({ view: 'live' }));
const viewQueryExecutor = mock<ViewQueryExecutor>();
const manager = new ViewManager(createCardAPI(), {
const manager = new ViewManager(createInitializedCardAPI(), {
viewFactory: viewFactory,
viewQueryExecutor: viewQueryExecutor,
});
@@ -681,14 +742,14 @@ it('should adopt query and results when changing to gallery from viewer', async
const baseView = createView({
view: 'media',
camera: 'camera.office',
query: new EventMediaQueries([
query: new EventMediaQuery([
{
type: QueryType.Event,
cameraIDs: new Set(['camera.office']),
hasClip: true,
},
]),
queryResults: new MediaQueriesResults(),
queryResults: new QueryResults(),
});
const viewFactory = mock<ViewFactory>();
@@ -699,7 +760,7 @@ it('should adopt query and results when changing to gallery from viewer', async
const viewQueryExecutor = mock<ViewQueryExecutor>();
viewQueryExecutor.getNewQueryModifiers.mockResolvedValue([]);
const manager = new ViewManager(createCardAPI(), {
const manager = new ViewManager(createInitializedCardAPI(), {
viewFactory: viewFactory,
viewQueryExecutor: viewQueryExecutor,
});
@@ -5,10 +5,11 @@ import { QueryExecutor } from '../../../src/card-controller/view/query-executor'
import { ViewQueryExecutor } from '../../../src/card-controller/view/view-query-executor';
import { AdvancedCameraCardView } from '../../../src/config/schema/common/const';
import {
EventMediaQueries,
RecordingMediaQueries,
} from '../../../src/view/media-queries';
import { MediaQueriesResults } from '../../../src/view/media-queries-results';
EventMediaQuery,
FolderViewQuery,
RecordingMediaQuery,
} from '../../../src/view/query';
import { QueryResults } from '../../../src/view/query-results';
import { View } from '../../../src/view/view';
import { createCardAPI, createView } from '../../test-utils';
import { createPopulatedAPI } from './test-utils';
@@ -19,15 +20,15 @@ describe('ViewQueryExecutor', () => {
const executor = mock<QueryExecutor>();
const viewQueryExecutor = new ViewQueryExecutor(createCardAPI(), executor);
const query = new EventMediaQueries();
const queryResults = new MediaQueriesResults();
const query = new EventMediaQuery();
const queryResults = new QueryResults();
const view = createView({
query: query,
});
executor.execute.mockResolvedValue(queryResults);
executor.executeQuery.mockResolvedValue({ query, queryResults });
const queryExecutorOptions = {};
const queryExecutorOptions = { useCache: true };
const modifiers = await viewQueryExecutor.getExistingQueryModifiers(
view,
queryExecutorOptions,
@@ -37,7 +38,7 @@ describe('ViewQueryExecutor', () => {
expect(view.query).toBe(query);
expect(view.queryResults).toBe(queryResults);
expect(executor.execute).toBeCalledWith(query, queryExecutorOptions);
expect(executor.executeQuery).toBeCalledWith(query, queryExecutorOptions);
});
it('should not return modifier when query absent', async () => {
@@ -58,7 +59,7 @@ describe('ViewQueryExecutor', () => {
expect(view.query).toBeNull();
expect(view.queryResults).toBeNull();
expect(executor.execute).not.toBeCalled();
expect(executor.executeMediaQuery).not.toBeCalled();
});
});
@@ -79,8 +80,8 @@ describe('ViewQueryExecutor', () => {
});
it('should set query and queryResults for events', async () => {
const query = new EventMediaQueries();
const queryResults = new MediaQueriesResults();
const query = new EventMediaQuery();
const queryResults = new QueryResults();
const executor = mock<QueryExecutor>();
executor.executeDefaultEventQuery.mockResolvedValue({
@@ -108,11 +109,12 @@ describe('ViewQueryExecutor', () => {
},
});
expect(executor.executeDefaultRecordingQuery).not.toHaveBeenCalled();
expect(executor.executeDefaultFolderQuery).not.toHaveBeenCalled();
});
it('should set query and queryResults for recordings', async () => {
const query = new RecordingMediaQueries();
const queryResults = new MediaQueriesResults();
const query = new RecordingMediaQuery();
const queryResults = new QueryResults();
const executor = mock<QueryExecutor>();
executor.executeDefaultRecordingQuery.mockResolvedValue({
@@ -150,6 +152,7 @@ describe('ViewQueryExecutor', () => {
},
});
expect(executor.executeDefaultEventQuery).not.toBeCalled();
expect(executor.executeDefaultFolderQuery).not.toHaveBeenCalled();
});
describe('should set timeline window', async () => {
@@ -227,14 +230,15 @@ describe('ViewQueryExecutor', () => {
expect(view?.queryResults).toBeNull();
expect(executor.executeDefaultEventQuery).not.toHaveBeenCalled();
expect(executor.executeDefaultRecordingQuery).not.toHaveBeenCalled();
expect(executor.executeDefaultFolderQuery).not.toHaveBeenCalled();
});
});
describe('with a media view', () => {
it('should set query and queryResults for events', async () => {
const executor = mock<QueryExecutor>();
const query = new EventMediaQueries();
const queryResults = new MediaQueriesResults();
const query = new EventMediaQuery();
const queryResults = new QueryResults();
executor.executeDefaultEventQuery.mockResolvedValue({
query: query,
@@ -260,6 +264,7 @@ describe('ViewQueryExecutor', () => {
},
});
expect(executor.executeDefaultRecordingQuery).not.toHaveBeenCalled();
expect(executor.executeDefaultFolderQuery).not.toHaveBeenCalled();
});
});
@@ -276,8 +281,8 @@ describe('ViewQueryExecutor', () => {
eventsMediaType: 'clips' | 'snapshots',
) => {
const executor = mock<QueryExecutor>();
const query = new EventMediaQueries();
const queryResults = new MediaQueriesResults();
const query = new EventMediaQuery();
const queryResults = new QueryResults();
executor.executeDefaultEventQuery.mockResolvedValue({
query: query,
@@ -306,6 +311,7 @@ describe('ViewQueryExecutor', () => {
},
});
expect(executor.executeDefaultRecordingQuery).not.toHaveBeenCalled();
expect(executor.executeDefaultFolderQuery).not.toHaveBeenCalled();
},
);
});
@@ -315,8 +321,8 @@ describe('ViewQueryExecutor', () => {
'%s',
async (viewName: AdvancedCameraCardView) => {
const executor = mock<QueryExecutor>();
const query = new RecordingMediaQueries();
const queryResults = new MediaQueriesResults();
const query = new RecordingMediaQuery();
const queryResults = new QueryResults();
executor.executeDefaultRecordingQuery.mockResolvedValue({
query: query,
@@ -344,10 +350,52 @@ describe('ViewQueryExecutor', () => {
useCache: false,
},
});
expect(executor.executeDefaultFolderQuery).not.toHaveBeenCalled();
},
);
});
describe('with a folder view', () => {
it('should execute default folder query with folder view', async () => {
const executor = mock<QueryExecutor>();
const query = new FolderViewQuery();
const queryResults = new QueryResults();
executor.executeDefaultFolderQuery.mockResolvedValue({
query: query,
queryResults: queryResults,
});
const viewQueryExecutor = new ViewQueryExecutor(createPopulatedAPI(), executor);
const view = createView({ view: 'folder', camera: 'camera.office' });
const modifiers = await viewQueryExecutor.getNewQueryModifiers(view);
applyViewModifiers(view, modifiers);
expect(view?.query).toBe(query);
expect(view?.queryResults).toBe(queryResults);
expect(executor.executeDefaultEventQuery).not.toBeCalled();
expect(executor.executeDefaultRecordingQuery).not.toBeCalled();
expect(executor.executeDefaultFolderQuery).toBeCalledWith({
useCache: false,
});
});
it('should execute default folder query with folder view and handle null results', async () => {
const executor = mock<QueryExecutor>();
executor.executeDefaultFolderQuery.mockResolvedValue(null);
const viewQueryExecutor = new ViewQueryExecutor(createPopulatedAPI(), executor);
const view = createView({ view: 'folder', camera: 'camera.office' });
const modifiers = await viewQueryExecutor.getNewQueryModifiers(view);
applyViewModifiers(view, modifiers);
expect(view?.query).toBeNull();
expect(view?.queryResults).toBeNull();
});
});
describe('when setting or removing seek time', () => {
it('should set seek time when results are selected based on time', async () => {
const now = new Date();