Files
advanced-camera-card/tests/components-lib/gallery/controller.test.ts
T
Dermot Duffy fc32727860 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`.
2026-01-19 13:32:50 -08:00

323 lines
11 KiB
TypeScript

import { assert, describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import {
ViewManagerEpoch,
ViewManagerInterface,
} from '../../../src/card-controller/view/types';
import { GalleryController } from '../../../src/components-lib/gallery/controller';
import { THUMBNAIL_WIDTH_DEFAULT } from '../../../src/config/schema/common/controls/thumbnails';
import { FolderConfig } from '../../../src/config/schema/folders';
import { MediaGalleryThumbnailsConfig } from '../../../src/config/schema/media-gallery';
import { ViewFolder, ViewItem, ViewMedia, ViewMediaType } from '../../../src/view/item';
import { QueryResults } from '../../../src/view/query-results';
import { UnifiedQuery } from '../../../src/view/unified-query';
import { UnifiedQueryRunner } from '../../../src/view/unified-query-runner';
import { View } from '../../../src/view/view';
// @vitest-environment jsdom
const createThumbnailConfig = (
config?: Partial<MediaGalleryThumbnailsConfig>,
): MediaGalleryThumbnailsConfig => ({
size: THUMBNAIL_WIDTH_DEFAULT,
show_details: true,
show_favorite_control: true,
show_timeline_control: true,
show_download_control: true,
show_review_control: true,
show_info_control: true,
...config,
});
describe('GalleryController', () => {
it('should construct', () => {
const host = document.createElement('div');
const controller = new GalleryController(host);
expect(controller).toBeTruthy();
expect(controller.getItems()).toBeNull();
});
describe('setItemsFromView', () => {
it('should set items from view', () => {
const host = document.createElement('div');
const controller = new GalleryController(host);
const view = mock<View>();
view.queryResults = mock<QueryResults>();
const item1 = new ViewMedia(ViewMediaType.Clip);
const item2 = new ViewMedia(ViewMediaType.Clip);
vi.mocked(view.queryResults.getResults).mockReturnValue([item1, item2]);
controller.setItemsFromView(view);
// Items should be reversed (newest first)
expect(controller.getItems()).toEqual([item2, item1]);
});
it('should handle null view or results', () => {
const host = document.createElement('div');
const controller = new GalleryController(host);
controller.setItemsFromView(null);
expect(controller.getItems()).toBeNull();
const view = mock<View>();
view.queryResults = null;
controller.setItemsFromView(view);
expect(controller.getItems()).toBeNull();
});
it('should not update items if results are the same', () => {
const host = document.createElement('div');
const controller = new GalleryController(host);
const view = mock<View>();
view.queryResults = mock<QueryResults>();
const item1 = new ViewMedia(ViewMediaType.Clip);
vi.mocked(view.queryResults.getResults).mockReturnValue([item1]);
controller.setItemsFromView(view);
const itemsFirst = controller.getItems();
// Second call with same results object
controller.setItemsFromView(view, view);
expect(controller.getItems()).toBe(itemsFirst);
});
it('should update items if results change', () => {
const host = document.createElement('div');
const controller = new GalleryController(host);
const view1 = mock<View>();
view1.queryResults = mock<QueryResults>();
const item1 = new ViewMedia(ViewMediaType.Clip);
vi.mocked(view1.queryResults.getResults).mockReturnValue([item1]);
controller.setItemsFromView(view1);
const view2 = mock<View>();
view2.queryResults = mock<QueryResults>();
const item2 = new ViewMedia(ViewMediaType.Clip);
vi.mocked(view2.queryResults.getResults).mockReturnValue([item2]);
controller.setItemsFromView(view2, view1);
expect(controller.getItems()).toEqual([item2]);
});
});
it('should set thumbnail size', () => {
const host = document.createElement('div');
const controller = new GalleryController(host);
controller.setThumbnailSize(150);
expect(host.style.getPropertyValue('--advanced-camera-card-thumbnail-size')).toBe(
'150px',
);
controller.setThumbnailSize();
expect(host.style.getPropertyValue('--advanced-camera-card-thumbnail-size')).toBe(
`${THUMBNAIL_WIDTH_DEFAULT}px`,
);
});
describe('getColumnWidth', () => {
it('should return default width if no config', () => {
const host = document.createElement('div');
const controller = new GalleryController(host);
expect(controller.getColumnWidth()).toBe(THUMBNAIL_WIDTH_DEFAULT);
});
it('should return size if details are hidden', () => {
const host = document.createElement('div');
const controller = new GalleryController(host);
expect(
controller.getColumnWidth(
createThumbnailConfig({ size: 123, show_details: false }),
),
).toBe(123);
});
it('should return gallery width if details are shown and items are not all folders', () => {
const host = document.createElement('div');
const controller = new GalleryController(host);
const view = mock<View>();
const item = new ViewMedia(ViewMediaType.Clip);
view.queryResults = mock<QueryResults>();
vi.mocked(view.queryResults.getResults).mockReturnValue([item]);
controller.setItemsFromView(view);
expect(
controller.getColumnWidth(createThumbnailConfig({ show_details: true })),
).toBe(300);
});
it('should return folder width if details are shown and items are all folders', () => {
const host = document.createElement('div');
const controller = new GalleryController(host);
const view = mock<View>();
const item = new ViewFolder(mock<FolderConfig>(), []);
view.queryResults = mock<QueryResults>();
vi.mocked(view.queryResults.getResults).mockReturnValue([item]);
controller.setItemsFromView(view);
expect(
controller.getColumnWidth(createThumbnailConfig({ show_details: true })),
).toBe(200);
});
});
it('should get column count round method', () => {
const host = document.createElement('div');
const controller = new GalleryController(host);
expect(
controller.getColumnCountRoundMethod(
createThumbnailConfig({ show_details: true }),
),
).toBe('floor');
expect(
controller.getColumnCountRoundMethod(
createThumbnailConfig({ show_details: false }),
),
).toBe('ceil');
expect(controller.getColumnCountRoundMethod()).toBe('ceil');
});
describe('extend', () => {
it('should handle missing view or query', async () => {
const host = document.createElement('div');
const controller = new GalleryController(host);
const runner = mock<UnifiedQueryRunner>();
const manager = mock<ViewManagerInterface>();
const epoch = mock<ViewManagerEpoch>({
manager: manager,
});
manager.getView.mockReturnValue(null);
await controller.extend(runner, epoch, 'earlier');
expect(runner.extend).not.toHaveBeenCalled();
const view = mock<View>();
view.query = null;
manager.getView.mockReturnValue(view);
await controller.extend(runner, epoch, 'earlier');
expect(runner.extend).not.toHaveBeenCalled();
});
it('should handle missing results', async () => {
const host = document.createElement('div');
const controller = new GalleryController(host);
const runner = mock<UnifiedQueryRunner>();
const manager = mock<ViewManagerInterface>();
const epoch = mock<ViewManagerEpoch>({
manager: manager,
});
const view = mock<View>();
view.query = mock<UnifiedQuery>();
view.queryResults = mock<QueryResults>();
vi.mocked(view.queryResults.getResults).mockReturnValue(null);
manager.getView.mockReturnValue(view);
await controller.extend(runner, epoch, 'earlier');
expect(runner.extend).not.toHaveBeenCalled();
});
it('should extend and update view', async () => {
const host = document.createElement('div');
const controller = new GalleryController(host);
const runner = mock<UnifiedQueryRunner>();
const manager = mock<ViewManagerInterface>();
const epoch = mock<ViewManagerEpoch>({
manager: manager,
});
const view = mock<View>();
const query = mock<UnifiedQuery>();
const selectedResult = mock<ViewItem>();
const results = [selectedResult];
view.query = query;
view.queryResults = mock<QueryResults>();
vi.mocked(view.queryResults.getResults).mockReturnValue(results);
vi.mocked(view.queryResults.getSelectedResult).mockReturnValue(selectedResult);
manager.getView.mockReturnValue(view);
const extendedQuery = mock<UnifiedQuery>();
const extendedResults = [selectedResult, new ViewMedia(ViewMediaType.Clip)];
runner.extend.mockResolvedValue({
query: extendedQuery,
results: extendedResults,
});
await controller.extend(runner, epoch, 'earlier');
expect(runner.extend).toHaveBeenCalledWith(query, results, 'earlier', {
useCache: true,
});
expect(manager.setViewByParameters).toHaveBeenCalled();
const setViewCalls = vi.mocked(manager.setViewByParameters).mock.calls;
const setViewParams = setViewCalls[0][0];
assert(setViewParams && setViewParams.params);
const newQueryResults = setViewParams.params.queryResults;
assert(newQueryResults);
expect(newQueryResults.getResults()).toEqual(extendedResults);
expect(newQueryResults.getSelectedResult()).toBe(selectedResult);
});
it('should handle extend failure', async () => {
const host = document.createElement('div');
const controller = new GalleryController(host);
const runner = mock<UnifiedQueryRunner>();
const manager = mock<ViewManagerInterface>();
const epoch = mock<ViewManagerEpoch>({
manager: manager,
});
const view = mock<View>();
view.query = mock<UnifiedQuery>();
view.queryResults = mock<QueryResults>();
vi.mocked(view.queryResults.getResults).mockReturnValue([
new ViewMedia(ViewMediaType.Clip),
]);
manager.getView.mockReturnValue(view);
const error = new Error('test error');
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
runner.extend.mockRejectedValue(error);
await controller.extend(runner, epoch, 'earlier');
expect(spy).toHaveBeenCalledWith(error.message);
spy.mockRestore();
expect(manager.setViewByParameters).not.toHaveBeenCalled();
});
it('should not update view if extend returns null', async () => {
const host = document.createElement('div');
const controller = new GalleryController(host);
const runner = mock<UnifiedQueryRunner>();
const manager = mock<ViewManagerInterface>();
const epoch = mock<ViewManagerEpoch>({
manager: manager,
});
const view = mock<View>();
view.query = mock<UnifiedQuery>();
view.queryResults = mock<QueryResults>();
vi.mocked(view.queryResults.getResults).mockReturnValue([
new ViewMedia(ViewMediaType.Clip),
]);
manager.getView.mockReturnValue(view);
runner.extend.mockResolvedValue(null);
await controller.extend(runner, epoch, 'earlier');
expect(manager.setViewByParameters).not.toHaveBeenCalled();
});
});
});