Initial support for grid for live and media viewer.
This commit is contained in:
@@ -76,30 +76,26 @@ describe('capEndDate', () => {
|
||||
});
|
||||
|
||||
describe('sortMedia', () => {
|
||||
const media_1 = new TestViewMedia(
|
||||
'id-1',
|
||||
new Date('2023-04-29T14:25'),
|
||||
'clip',
|
||||
'camera-1',
|
||||
);
|
||||
const media_2 = new TestViewMedia(
|
||||
'id-2',
|
||||
new Date('2023-04-29T14:26'),
|
||||
'clip',
|
||||
'camera-1',
|
||||
);
|
||||
const media_3_dup_id = new TestViewMedia(
|
||||
'id-2',
|
||||
new Date('2023-04-29T14:26'),
|
||||
'clip',
|
||||
'camera-1',
|
||||
);
|
||||
const media_4_no_id = new TestViewMedia(
|
||||
null,
|
||||
new Date('2023-04-29T14:27'),
|
||||
'clip',
|
||||
'camera-1',
|
||||
);
|
||||
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',
|
||||
});
|
||||
|
||||
it('should sort sorted media', () => {
|
||||
const media = [media_1, media_2];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, describe, it, expect, vi } from 'vitest';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
ConditionController,
|
||||
ConditionEvaluateRequestEvent,
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
getOverriddenConfig,
|
||||
getOverridesByKey,
|
||||
} from '../src/conditions';
|
||||
import { FrigateCardCondition } from '../src/types';
|
||||
import { createCondition, createConfig, createStateEntity } from './test-utils';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
@@ -347,4 +348,14 @@ describe('ConditionController', () => {
|
||||
controller.destroy();
|
||||
expect(removeEventListener).toBeCalled();
|
||||
});
|
||||
|
||||
it('should evaluate conditions with display mode', () => {
|
||||
const controller = new ConditionController();
|
||||
const condition: FrigateCardCondition = { display_mode: 'grid' };
|
||||
expect(controller.evaluateCondition(condition)).toBeFalsy();
|
||||
controller.setState({ displayMode: 'grid' });
|
||||
expect(controller.evaluateCondition(condition)).toBeTruthy();
|
||||
controller.setState({ displayMode: 'single' });
|
||||
expect(controller.evaluateCondition(condition)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
+71
-10
@@ -9,6 +9,7 @@ import {
|
||||
CameraConfigs,
|
||||
CameraManagerCameraCapabilities,
|
||||
CameraManagerMediaCapabilities,
|
||||
QueryType,
|
||||
} from '../src/camera-manager/types';
|
||||
import {
|
||||
CameraConfig,
|
||||
@@ -16,10 +17,12 @@ import {
|
||||
FrigateCardCondition,
|
||||
FrigateCardConfig,
|
||||
MediaLoadedInfo,
|
||||
PerformanceConfig,
|
||||
RawFrigateCardConfig,
|
||||
cameraConfigSchema,
|
||||
frigateCardConditionSchema,
|
||||
frigateCardConfigSchema,
|
||||
performanceConfigSchema,
|
||||
} from '../src/types';
|
||||
import { Entity } from '../src/utils/ha/entity-registry/types';
|
||||
import { ViewMedia, ViewMediaType } from '../src/view/media';
|
||||
@@ -126,11 +129,25 @@ export const createCameraManager = (options?: {
|
||||
const configs = options?.configs ?? new Map([['camera', createCameraConfig()]]);
|
||||
vi.mocked(store.getCameras).mockReturnValue(configs);
|
||||
vi.mocked(store.getVisibleCameras).mockReturnValue(configs);
|
||||
vi.mocked(store.getVisibleCameraIDs).mockReturnValue(new Set(configs.keys()));
|
||||
vi.mocked(store.getCameraConfig).mockImplementation((cameraID): CameraConfig => {
|
||||
return configs.get(cameraID) ?? createCameraConfig();
|
||||
});
|
||||
}
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(store);
|
||||
vi.mocked(cameraManager.generateDefaultEventQueries).mockReturnValue([
|
||||
{
|
||||
cameraIDs: new Set(['camera']),
|
||||
type: QueryType.Event,
|
||||
},
|
||||
]);
|
||||
vi.mocked(cameraManager.generateDefaultRecordingQueries).mockReturnValue([
|
||||
{
|
||||
cameraIDs: new Set(['camera']),
|
||||
type: QueryType.Recording,
|
||||
},
|
||||
]);
|
||||
|
||||
return cameraManager;
|
||||
};
|
||||
|
||||
@@ -169,21 +186,44 @@ export const createMediaLoadedInfo = (
|
||||
};
|
||||
};
|
||||
|
||||
export const createPerformanceConfig = (config: unknown): PerformanceConfig => {
|
||||
return performanceConfigSchema.parse(config);
|
||||
};
|
||||
|
||||
export const generateViewMediaArray = (options?: {
|
||||
cameraIDs?: string[];
|
||||
count?: number;
|
||||
}): ViewMedia[] => {
|
||||
const media: ViewMedia[] = [];
|
||||
for (let i = 0; i < (options?.count ?? 100); ++i) {
|
||||
for (const cameraID of options?.cameraIDs ?? ['kitchen', 'office']) {
|
||||
media.push(new TestViewMedia({ cameraID: cameraID, id: `id-${cameraID}-${i}` }));
|
||||
}
|
||||
}
|
||||
return media;
|
||||
};
|
||||
|
||||
// ViewMedia itself has no native way to set startTime and ID that aren't linked
|
||||
// to an engine.
|
||||
export class TestViewMedia extends ViewMedia {
|
||||
protected _id: string | null;
|
||||
protected _startTime: Date;
|
||||
protected _startTime: Date | null;
|
||||
protected _endTime: Date | null;
|
||||
protected _inProgress: boolean | null;
|
||||
|
||||
constructor(
|
||||
id: string | null,
|
||||
startTime: Date,
|
||||
mediaType: ViewMediaType,
|
||||
cameraID: string,
|
||||
) {
|
||||
super(mediaType, cameraID);
|
||||
this._id = id;
|
||||
this._startTime = startTime;
|
||||
constructor(options?: {
|
||||
id?: string | null;
|
||||
startTime?: Date;
|
||||
mediaType?: ViewMediaType;
|
||||
cameraID?: string;
|
||||
endTime?: Date;
|
||||
inProgress?: boolean;
|
||||
}) {
|
||||
super(options?.mediaType ?? 'clip', options?.cameraID ?? 'camera');
|
||||
this._id = options?.id !== undefined ? options.id : 'id';
|
||||
this._startTime = options?.startTime ?? null;
|
||||
this._endTime = options?.endTime ?? null;
|
||||
this._inProgress = options?.inProgress !== undefined ? options.inProgress : false;
|
||||
}
|
||||
public getID(): string | null {
|
||||
return this._id;
|
||||
@@ -191,4 +231,25 @@ export class TestViewMedia extends ViewMedia {
|
||||
public getStartTime(): Date | null {
|
||||
return this._startTime;
|
||||
}
|
||||
public getEndTime(): Date | null {
|
||||
return this._endTime;
|
||||
}
|
||||
public inProgress(): boolean | null {
|
||||
return this._inProgress;
|
||||
}
|
||||
}
|
||||
|
||||
export const createResizeObserverImplementation = (): (() => void) => {
|
||||
return () => ({
|
||||
observe: vi.fn(),
|
||||
unobserve: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
});
|
||||
};
|
||||
|
||||
export const createMutationObserverImplementation = (): (() => void) => {
|
||||
return () => ({
|
||||
observe: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -103,6 +103,24 @@ describe('createFrigateCardCustomAction', () => {
|
||||
card_id: 'card_id',
|
||||
});
|
||||
});
|
||||
|
||||
it('should create display mode action', () => {
|
||||
expect(
|
||||
createFrigateCardCustomAction('display_mode_select', {
|
||||
display_mode: 'grid',
|
||||
cardID: 'card_id',
|
||||
}),
|
||||
).toEqual({
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'display_mode_select',
|
||||
display_mode: 'grid',
|
||||
card_id: 'card_id',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not create display mode action without display mode', () => {
|
||||
expect(createFrigateCardCustomAction('display_mode_select')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getActionConfigGivenAction', () => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
dayToDate,
|
||||
dispatchFrigateCardEvent,
|
||||
errorToConsole,
|
||||
filterTruthy,
|
||||
formatDate,
|
||||
formatDateAndTime,
|
||||
getDurationString,
|
||||
@@ -227,7 +228,13 @@ describe('isValidDate', () => {
|
||||
});
|
||||
|
||||
describe('setOrRemoveAttribute', () => {
|
||||
it('should set attribute', () => {
|
||||
it('should set attribute without value', () => {
|
||||
const element = document.createElement('div');
|
||||
setOrRemoveAttribute(element, true, 'key');
|
||||
expect(element.getAttribute('key')).toBe('');
|
||||
});
|
||||
|
||||
it('should set attribute with value', () => {
|
||||
const element = document.createElement('div');
|
||||
setOrRemoveAttribute(element, true, 'key', 'value');
|
||||
expect(element.getAttribute('key')).toBe('value');
|
||||
@@ -240,3 +247,12 @@ describe('setOrRemoveAttribute', () => {
|
||||
expect(element.getAttribute('key')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterTruthy', () => {
|
||||
it('should return true for true', () => {
|
||||
expect(filterTruthy(true)).toBeTruthy();
|
||||
});
|
||||
it('should return false for false', () => {
|
||||
expect(filterTruthy(false)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
import Masonry from 'masonry-layout';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { MediaLoadedInfo } from '../../src/types';
|
||||
import {
|
||||
MediaGridConstructorOptions,
|
||||
MediaGridController,
|
||||
} from '../../src/utils/media-grid-controller';
|
||||
import { dispatchExistingMediaLoadedInfoAsEvent } from '../../src/utils/media-info';
|
||||
import {
|
||||
createMutationObserverImplementation,
|
||||
createResizeObserverImplementation,
|
||||
} from '../test-utils';
|
||||
|
||||
vi.mock('lodash-es/throttle', () => ({
|
||||
default: vi.fn((fn) => fn),
|
||||
}));
|
||||
|
||||
const masonry = mock<Masonry>();
|
||||
vi.mock('masonry-layout', () => ({
|
||||
default: vi.fn().mockImplementation(() => {
|
||||
return masonry;
|
||||
}),
|
||||
}));
|
||||
|
||||
const createChildren = (childIDs?: string[], idAttribute?: string): HTMLElement[] => {
|
||||
const children: HTMLElement[] = [];
|
||||
for (let i = 0; i < (childIDs?.length ?? 3); ++i) {
|
||||
const child = document.createElement('div');
|
||||
if (childIDs) {
|
||||
child.setAttribute(idAttribute ?? 'grid-id', childIDs[i]);
|
||||
}
|
||||
children.push(child);
|
||||
}
|
||||
return children;
|
||||
};
|
||||
|
||||
const setElementWidth = (element: HTMLElement, width: number): void => {
|
||||
element.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||
width: width,
|
||||
});
|
||||
};
|
||||
|
||||
const createHost = (options?: {
|
||||
children?: HTMLElement[];
|
||||
width?: number;
|
||||
}): HTMLElement => {
|
||||
const host = document.createElement('div');
|
||||
if (options?.children) {
|
||||
host.append(...options.children);
|
||||
}
|
||||
// Default Lovelace card width is 492.
|
||||
setElementWidth(host, options?.width ?? 492);
|
||||
return host;
|
||||
};
|
||||
|
||||
const createSlotParent = (): HTMLElement => {
|
||||
const parent = document.createElement('div');
|
||||
parent.attachShadow({ mode: 'open' });
|
||||
return parent;
|
||||
};
|
||||
|
||||
const createSlotHost = (options?: {
|
||||
children?: HTMLElement[];
|
||||
parent?: HTMLElement;
|
||||
}): HTMLElement => {
|
||||
const parent = options?.parent ?? createSlotParent();
|
||||
const slot = document.createElement('slot');
|
||||
parent.shadowRoot?.append(slot);
|
||||
|
||||
if (options?.children) {
|
||||
// Children will automatically be slotted into the default slot.
|
||||
parent.append(...options.children);
|
||||
}
|
||||
|
||||
return slot;
|
||||
};
|
||||
|
||||
const createController = (host: HTMLElement, options?: MediaGridConstructorOptions) => {
|
||||
return new MediaGridController(host, options);
|
||||
};
|
||||
|
||||
const triggerMutationObserver = (): void => {
|
||||
const mutationObserverTrigger = vi.mocked(global.MutationObserver).mock.calls[0][0];
|
||||
mutationObserverTrigger([], mock<MutationObserver>());
|
||||
};
|
||||
|
||||
const triggerResizeObserver = (cellOrHost: 'cell' | 'host'): void => {
|
||||
const resizeObserverTrigger = vi.mocked(global.ResizeObserver).mock.calls[
|
||||
cellOrHost === 'cell' ? 0 : 1
|
||||
][0];
|
||||
resizeObserverTrigger([], mock<ResizeObserver>());
|
||||
};
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('MediaGridController', () => {
|
||||
const mediaLoadedInfo: MediaLoadedInfo = {
|
||||
width: 10,
|
||||
height: 20,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
global.ResizeObserver = vi
|
||||
.fn()
|
||||
// Caution: Order must match the order of initialization in
|
||||
// media-grid-controller.ts .
|
||||
.mockImplementationOnce(createResizeObserverImplementation())
|
||||
.mockImplementationOnce(createResizeObserverImplementation());
|
||||
|
||||
global.MutationObserver = vi
|
||||
.fn()
|
||||
.mockImplementation(createMutationObserverImplementation());
|
||||
//global.MutationObserver = mock<MutationObserver>();
|
||||
});
|
||||
|
||||
it('should be constructable', () => {
|
||||
const controller = createController(createHost());
|
||||
expect(controller).toBeTruthy();
|
||||
expect(masonry.layout).toBeCalled();
|
||||
});
|
||||
|
||||
it('should set grid contents correctly from regular elements', () => {
|
||||
const children = createChildren();
|
||||
const host = createHost({ children: children });
|
||||
const controller = createController(host);
|
||||
expect(controller.getGridContents()).toEqual(
|
||||
new Map([
|
||||
['0', children[0]],
|
||||
['1', children[1]],
|
||||
['2', children[2]],
|
||||
]),
|
||||
);
|
||||
expect(controller.getGridSize()).toBe(3);
|
||||
expect(masonry.layout).toBeCalled();
|
||||
});
|
||||
|
||||
it('should set grid contents correctly from slotted elements', () => {
|
||||
const children = createChildren();
|
||||
const host = createSlotHost({ children: children });
|
||||
const controller = createController(host);
|
||||
expect(controller.getGridContents()).toEqual(
|
||||
new Map([
|
||||
['0', children[0]],
|
||||
['1', children[1]],
|
||||
['2', children[2]],
|
||||
]),
|
||||
);
|
||||
expect(controller.getGridSize()).toBe(3);
|
||||
});
|
||||
|
||||
it('should select element', () => {
|
||||
const children = createChildren();
|
||||
const controller = createController(createSlotHost({ children: children }));
|
||||
|
||||
// All children should be unselected.
|
||||
expect(controller.getSelected()).toBeNull();
|
||||
for (const child of children) {
|
||||
expect(child.getAttribute('selected')).toBeNull();
|
||||
expect(child.getAttribute('unselected')).toEqual('');
|
||||
}
|
||||
|
||||
controller.selectCell('0');
|
||||
expect(controller.getSelected()).toBe('0');
|
||||
|
||||
// 1st child should now be selected.
|
||||
expect(children[0].getAttribute('selected')).toEqual('');
|
||||
expect(children[0].getAttribute('unselected')).toBeNull();
|
||||
|
||||
// 2nd and 3rd should be unselected.
|
||||
for (const child of children.slice(1)) {
|
||||
expect(child.getAttribute('selected')).toBeNull();
|
||||
expect(child.getAttribute('unselected')).toEqual('');
|
||||
}
|
||||
});
|
||||
|
||||
it('should re-select element', () => {
|
||||
const controller = createController(createSlotHost({ children: createChildren() }));
|
||||
|
||||
// All children should be unselected.
|
||||
expect(controller.getSelected()).toBeNull();
|
||||
|
||||
controller.selectCell('0');
|
||||
expect(controller.getSelected()).toBe('0');
|
||||
|
||||
controller.selectCell('0');
|
||||
expect(controller.getSelected()).toBe('0');
|
||||
});
|
||||
|
||||
it('should dispatch media loaded info on selection', () => {
|
||||
const children = createChildren();
|
||||
const host = createSlotHost({ children: children });
|
||||
const controller = createController(host);
|
||||
|
||||
const mediaLoadedInfoHandler = vi.fn();
|
||||
host.addEventListener('frigate-card:media:loaded', mediaLoadedInfoHandler);
|
||||
dispatchExistingMediaLoadedInfoAsEvent(children[0], mediaLoadedInfo);
|
||||
|
||||
// Nothing is selected, so the event should not have propagated.
|
||||
expect(mediaLoadedInfoHandler).not.toBeCalled();
|
||||
|
||||
controller.selectCell('0');
|
||||
expect(mediaLoadedInfoHandler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: mediaLoadedInfo,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should unselect', () => {
|
||||
const children = createChildren();
|
||||
const host = createSlotHost({ children: children });
|
||||
const controller = createController(host);
|
||||
|
||||
const unselectedHandler = vi.fn();
|
||||
const unloadMediaHandler = vi.fn();
|
||||
host.addEventListener('frigate-card:media-grid:unselected', unselectedHandler);
|
||||
host.addEventListener('frigate-card:media:unloaded', unloadMediaHandler);
|
||||
|
||||
controller.selectCell('0');
|
||||
expect(controller.getSelected()).toBe('0');
|
||||
|
||||
// Unselect all elements.
|
||||
controller.unselectAll();
|
||||
|
||||
// Expect selected to now be null.
|
||||
expect(controller.getSelected()).toBeNull();
|
||||
|
||||
// Expect styles to have been updated.
|
||||
for (const child of children) {
|
||||
expect(child.getAttribute('selected')).toBeNull();
|
||||
expect(child.getAttribute('unselected')).toEqual('');
|
||||
}
|
||||
|
||||
// Expect handlers to have been called.
|
||||
expect(unselectedHandler).toBeCalled();
|
||||
expect(unloadMediaHandler).toBeCalled();
|
||||
});
|
||||
|
||||
it('should select in constructor', () => {
|
||||
const children = createChildren();
|
||||
const host = createSlotHost({ children: children });
|
||||
const controller = createController(host, { selected: '2' });
|
||||
expect(controller.getSelected()).toBe('2');
|
||||
});
|
||||
|
||||
it('should respect grid attribute option', () => {
|
||||
const children = createChildren(['one', 'two', 'three'], 'test-id');
|
||||
const host = createSlotHost({ children: children });
|
||||
const controller = createController(host, { idAttribute: 'test-id' });
|
||||
expect(controller.getGridContents()).toEqual(
|
||||
new Map([
|
||||
['one', children[0]],
|
||||
['two', children[1]],
|
||||
['three', children[2]],
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should destroy', () => {
|
||||
const children = createChildren();
|
||||
const host = createSlotHost({ children: children });
|
||||
const controller = createController(host);
|
||||
expect(controller.getGridSize()).toBe(3);
|
||||
controller.destroy();
|
||||
expect(controller.getGridSize()).toBe(0);
|
||||
});
|
||||
|
||||
it('should replace children when they change', () => {
|
||||
const children = createChildren();
|
||||
const host = createHost({ children: children });
|
||||
const controller = createController(host, { selected: '1' });
|
||||
dispatchExistingMediaLoadedInfoAsEvent(children[0], mediaLoadedInfo);
|
||||
|
||||
expect(controller.getSelected()).toBe('1');
|
||||
expect(controller.getGridSize()).toBe(3);
|
||||
|
||||
children.forEach((child) => host.removeChild(child));
|
||||
const newChildren = createChildren(['one', 'two', 'three']);
|
||||
newChildren.forEach((child) => host.appendChild(child));
|
||||
|
||||
triggerMutationObserver();
|
||||
|
||||
expect(controller.getGridContents()).toEqual(
|
||||
new Map([
|
||||
['one', newChildren[0]],
|
||||
['two', newChildren[1]],
|
||||
['three', newChildren[2]],
|
||||
]),
|
||||
);
|
||||
expect(controller.getSelected()).toBeNull();
|
||||
});
|
||||
|
||||
it('should construct masonry correctly', () => {
|
||||
const children = createChildren();
|
||||
const host = createHost({ children: children });
|
||||
createController(host);
|
||||
expect(Masonry).toBeCalledWith(
|
||||
host,
|
||||
expect.objectContaining({
|
||||
initLayout: false,
|
||||
percentPosition: true,
|
||||
transitionDuration: '0.3s',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should set default column size correctly', () => {
|
||||
const host = createHost({ children: createChildren() });
|
||||
createController(host);
|
||||
expect(Masonry).toBeCalledWith(
|
||||
host,
|
||||
expect.objectContaining({
|
||||
columnWidth: 246,
|
||||
}),
|
||||
);
|
||||
expect(host.style.getPropertyValue('--frigate-card-grid-column-size')).toBe('246px');
|
||||
});
|
||||
|
||||
it('should respect exact columns', () => {
|
||||
const host = createHost({ children: createChildren(), width: 2000 });
|
||||
const controller = createController(host);
|
||||
controller.setDisplayConfig({ mode: 'grid', grid_columns: 2 });
|
||||
|
||||
// Will have been called once on construction, and then again when the
|
||||
// number of columns changes.
|
||||
expect(Masonry).toBeCalledTimes(2);
|
||||
expect(Masonry).toBeCalledWith(
|
||||
host,
|
||||
expect.objectContaining({
|
||||
columnWidth: 1000,
|
||||
}),
|
||||
);
|
||||
expect(host.style.getPropertyValue('--frigate-card-grid-column-size')).toBe(
|
||||
'1000px',
|
||||
);
|
||||
});
|
||||
|
||||
it('should respect selected width factor', () => {
|
||||
const host = createHost({ children: createChildren(), width: 2000 });
|
||||
const controller = createController(host);
|
||||
controller.setDisplayConfig({ mode: 'grid', grid_selected_width_factor: 3 });
|
||||
expect(
|
||||
host.style.getPropertyValue('--frigate-card-grid-selected-width-factor'),
|
||||
).toBe('3');
|
||||
});
|
||||
|
||||
it('should select cell with interacted with', () => {
|
||||
const children = createChildren();
|
||||
const host = createHost({ children: children, width: 2000 });
|
||||
const controller = createController(host);
|
||||
|
||||
expect(controller.getSelected()).toBeNull();
|
||||
|
||||
const touchEvent = new TouchEvent('touchend');
|
||||
children[1].dispatchEvent(touchEvent);
|
||||
|
||||
expect(controller.getSelected()).toBe('1');
|
||||
});
|
||||
|
||||
it('should re-layout when child size changes', () => {
|
||||
createController(createHost({ children: createChildren() }));
|
||||
|
||||
vi.mocked(masonry.layout)?.mockClear();
|
||||
triggerResizeObserver('cell');
|
||||
expect(masonry.layout).toBeCalled();
|
||||
});
|
||||
|
||||
it('should re-create masonry when host size changes', () => {
|
||||
const children = createChildren();
|
||||
const host = createHost({ children: children });
|
||||
const controller = createController(host);
|
||||
expect(Masonry).toBeCalledWith(
|
||||
host,
|
||||
expect.objectContaining({
|
||||
columnWidth: 246,
|
||||
}),
|
||||
);
|
||||
expect(host.style.getPropertyValue('--frigate-card-grid-column-size')).toBe('246px');
|
||||
|
||||
// Clear mock state.
|
||||
vi.mocked(Masonry).mockClear();
|
||||
vi.mocked(masonry.layout)?.mockClear();
|
||||
|
||||
// Resize the host.
|
||||
setElementWidth(host, 2000);
|
||||
triggerResizeObserver('host');
|
||||
|
||||
// Masonry should be reconstructed, styles set and layout called.
|
||||
expect(Masonry).toBeCalledWith(
|
||||
host,
|
||||
expect.objectContaining({
|
||||
columnWidth: 667,
|
||||
}),
|
||||
);
|
||||
expect(host.style.getPropertyValue('--frigate-card-grid-column-size')).toBe('667px');
|
||||
expect(masonry.layout).toBeCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,452 @@
|
||||
import add from 'date-fns/add';
|
||||
import sub from 'date-fns/sub';
|
||||
import { beforeEach, describe, expect, it, Mock, vi } from 'vitest';
|
||||
import {
|
||||
CameraConfigs,
|
||||
PartialRecordingQuery,
|
||||
QueryType,
|
||||
} from '../../src/camera-manager/types';
|
||||
import { setify } from '../../src/utils/basic';
|
||||
import {
|
||||
changeViewToRecentEventsForCameraAndDependents,
|
||||
changeViewToRecentRecordingForCameraAndDependents,
|
||||
createQueriesForRecordingsView,
|
||||
executeMediaQueryForView,
|
||||
findBestMediaIndex,
|
||||
} from '../../src/utils/media-to-view';
|
||||
import { ViewMedia } from '../../src/view/media';
|
||||
import { EventMediaQueries } from '../../src/view/media-queries';
|
||||
import {
|
||||
createCameraManager,
|
||||
createHASS,
|
||||
createPerformanceConfig,
|
||||
createView,
|
||||
TestViewMedia,
|
||||
} from '../test-utils';
|
||||
|
||||
vi.mock('../../src/camera-manager/manager.js');
|
||||
|
||||
const createElementListenForView = (): {
|
||||
element: HTMLElement;
|
||||
viewHandler: Mock<any, any>;
|
||||
messageHandler: Mock<any, any>;
|
||||
} => {
|
||||
const element = document.createElement('div');
|
||||
|
||||
const viewHandler = vi.fn();
|
||||
element.addEventListener('frigate-card:view:change', viewHandler);
|
||||
|
||||
const messageHandler = vi.fn();
|
||||
element.addEventListener('frigate-card:message', messageHandler);
|
||||
|
||||
return {
|
||||
element: element,
|
||||
viewHandler: viewHandler,
|
||||
messageHandler: messageHandler,
|
||||
};
|
||||
};
|
||||
|
||||
const getMediaFromHandlerCall = (handler: Mock<any, any>): ViewMedia[] | null => {
|
||||
return handler.mock.calls[0][0].detail.queryResults.getResults();
|
||||
};
|
||||
|
||||
const generateViewMedia = (
|
||||
index: number,
|
||||
base: Date,
|
||||
durationSeconds: number,
|
||||
): ViewMedia => {
|
||||
return new TestViewMedia({
|
||||
id: `id-${index}`,
|
||||
startTime: base,
|
||||
endTime: add(base, { seconds: durationSeconds }),
|
||||
});
|
||||
};
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('changeViewToRecentEventsForCameraAndDependents', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should do nothing without camera config for selected camera', async () => {
|
||||
const elementHandler = createElementListenForView();
|
||||
const cameraManager = createCameraManager({ configs: new Map() });
|
||||
|
||||
await changeViewToRecentEventsForCameraAndDependents(
|
||||
elementHandler.element,
|
||||
createHASS(),
|
||||
cameraManager,
|
||||
{},
|
||||
createView(),
|
||||
);
|
||||
expect(elementHandler.viewHandler).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should do nothing without camera configs for all cameras', async () => {
|
||||
const elementHandler = createElementListenForView();
|
||||
const cameraManager = createCameraManager({ configs: new Map() });
|
||||
|
||||
await changeViewToRecentEventsForCameraAndDependents(
|
||||
elementHandler.element,
|
||||
createHASS(),
|
||||
cameraManager,
|
||||
{},
|
||||
createView(),
|
||||
{
|
||||
allCameras: true,
|
||||
},
|
||||
);
|
||||
expect(elementHandler.viewHandler).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should do nothing unless queries can be created', async () => {
|
||||
const elementHandler = createElementListenForView();
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.generateDefaultEventQueries).mockReturnValue(null);
|
||||
|
||||
await changeViewToRecentEventsForCameraAndDependents(
|
||||
elementHandler.element,
|
||||
createHASS(),
|
||||
cameraManager,
|
||||
{},
|
||||
createView(),
|
||||
{
|
||||
mediaType: 'clips',
|
||||
},
|
||||
);
|
||||
expect(elementHandler.viewHandler).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should dispatch new view on success', async () => {
|
||||
const elementHandler = createElementListenForView();
|
||||
const cameraManager = createCameraManager();
|
||||
|
||||
const mediaArray = [new ViewMedia('clip', 'camera')];
|
||||
vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(mediaArray);
|
||||
|
||||
await changeViewToRecentEventsForCameraAndDependents(
|
||||
elementHandler.element,
|
||||
createHASS(),
|
||||
cameraManager,
|
||||
{},
|
||||
createView(),
|
||||
{
|
||||
targetView: 'clips',
|
||||
select: 'latest',
|
||||
},
|
||||
);
|
||||
expect(elementHandler.viewHandler).toBeCalled();
|
||||
expect(getMediaFromHandlerCall(elementHandler.viewHandler)).toBe(mediaArray);
|
||||
});
|
||||
|
||||
it('should dispatch error message on fail', async () => {
|
||||
vi.spyOn(global.console, 'warn').mockImplementation(() => true);
|
||||
|
||||
const elementHandler = createElementListenForView();
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.executeMediaQueries).mockRejectedValue(new Error());
|
||||
|
||||
await changeViewToRecentEventsForCameraAndDependents(
|
||||
elementHandler.element,
|
||||
createHASS(),
|
||||
cameraManager,
|
||||
{},
|
||||
createView(),
|
||||
);
|
||||
expect(elementHandler.viewHandler).not.toBeCalled();
|
||||
expect(elementHandler.messageHandler).toBeCalled();
|
||||
});
|
||||
|
||||
it('should respect media chunk size', async () => {
|
||||
const cameraManager = createCameraManager();
|
||||
|
||||
await changeViewToRecentEventsForCameraAndDependents(
|
||||
createElementListenForView().element,
|
||||
createHASS(),
|
||||
cameraManager,
|
||||
{
|
||||
performance: createPerformanceConfig({
|
||||
features: {
|
||||
media_chunk_size: 1000,
|
||||
},
|
||||
}),
|
||||
},
|
||||
createView(),
|
||||
);
|
||||
|
||||
expect(cameraManager.generateDefaultEventQueries).toBeCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
limit: 1000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe('should respect request for media type', () => {
|
||||
it.each([
|
||||
['snapshots' as const, 'hasSnapshot'],
|
||||
['clips' as const, 'hasClip'],
|
||||
])('%s', async (mediaType, queryParameter) => {
|
||||
const cameraManager = createCameraManager();
|
||||
|
||||
await changeViewToRecentEventsForCameraAndDependents(
|
||||
createElementListenForView().element,
|
||||
createHASS(),
|
||||
cameraManager,
|
||||
{},
|
||||
createView(),
|
||||
{
|
||||
mediaType: mediaType,
|
||||
},
|
||||
);
|
||||
|
||||
expect(cameraManager.generateDefaultEventQueries).toBeCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
[queryParameter]: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('executeMediaQueryForView', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should not execute empty queries', async () => {
|
||||
const elementHandler = createElementListenForView();
|
||||
const cameraConfigs: CameraConfigs = new Map();
|
||||
const cameraManager = createCameraManager({ configs: cameraConfigs });
|
||||
|
||||
expect(
|
||||
await executeMediaQueryForView(
|
||||
elementHandler.element,
|
||||
createHASS(),
|
||||
cameraManager,
|
||||
createView(),
|
||||
new EventMediaQueries(),
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should select time-based result', async () => {
|
||||
const elementHandler = createElementListenForView();
|
||||
const cameraConfigs: CameraConfigs = new Map();
|
||||
const cameraManager = createCameraManager({ configs: cameraConfigs });
|
||||
|
||||
const now = new Date();
|
||||
const mediaArray = [
|
||||
generateViewMedia(0, now, 60),
|
||||
generateViewMedia(1, now, 120),
|
||||
generateViewMedia(2, now, 10),
|
||||
];
|
||||
vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(mediaArray);
|
||||
|
||||
const view = await executeMediaQueryForView(
|
||||
elementHandler.element,
|
||||
createHASS(),
|
||||
cameraManager,
|
||||
createView(),
|
||||
new EventMediaQueries(
|
||||
cameraManager.generateDefaultEventQueries('camera') ?? undefined,
|
||||
),
|
||||
{
|
||||
select: 'time',
|
||||
targetTime: add(now, { seconds: 30 }),
|
||||
},
|
||||
);
|
||||
|
||||
// Should select the longest event.
|
||||
expect(view?.queryResults?.getSelectedIndex()).toBe(1);
|
||||
expect(view?.queryResults?.getResults()).toBe(mediaArray);
|
||||
});
|
||||
|
||||
it('should select nothing when time-based selection does not match', async () => {
|
||||
const elementHandler = createElementListenForView();
|
||||
const cameraConfigs: CameraConfigs = new Map();
|
||||
const cameraManager = createCameraManager({ configs: cameraConfigs });
|
||||
|
||||
const now = new Date();
|
||||
const mediaArray = [
|
||||
generateViewMedia(0, now, 60),
|
||||
generateViewMedia(1, now, 120),
|
||||
generateViewMedia(2, now, 10),
|
||||
];
|
||||
|
||||
vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(mediaArray);
|
||||
|
||||
const view = await executeMediaQueryForView(
|
||||
elementHandler.element,
|
||||
createHASS(),
|
||||
cameraManager,
|
||||
createView(),
|
||||
new EventMediaQueries(
|
||||
cameraManager.generateDefaultEventQueries('camera') ?? undefined,
|
||||
),
|
||||
{
|
||||
select: 'time',
|
||||
targetTime: sub(now, { seconds: 30 }),
|
||||
},
|
||||
);
|
||||
|
||||
// Should leave selection untouched (last item will remain selected).
|
||||
expect(view?.queryResults?.getSelectedIndex()).toBe(2);
|
||||
expect(view?.queryResults?.getResults()).toBe(mediaArray);
|
||||
});
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('changeViewToRecentRecordingForCameraAndDependents', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should do nothing without camera config for selected camera', async () => {
|
||||
const elementHandler = createElementListenForView();
|
||||
const cameraManager = createCameraManager({ configs: new Map() });
|
||||
|
||||
await changeViewToRecentRecordingForCameraAndDependents(
|
||||
elementHandler.element,
|
||||
createHASS(),
|
||||
cameraManager,
|
||||
{},
|
||||
createView(),
|
||||
);
|
||||
expect(elementHandler.viewHandler).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should do nothing without camera configs for all cameras', async () => {
|
||||
const elementHandler = createElementListenForView();
|
||||
const cameraManager = createCameraManager({ configs: new Map() });
|
||||
|
||||
await changeViewToRecentRecordingForCameraAndDependents(
|
||||
elementHandler.element,
|
||||
createHASS(),
|
||||
cameraManager,
|
||||
{},
|
||||
createView(),
|
||||
{
|
||||
allCameras: true,
|
||||
},
|
||||
);
|
||||
expect(elementHandler.viewHandler).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should do nothing unless queries can be created', async () => {
|
||||
const elementHandler = createElementListenForView();
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.generateDefaultRecordingQueries).mockReturnValue(null);
|
||||
|
||||
await changeViewToRecentRecordingForCameraAndDependents(
|
||||
elementHandler.element,
|
||||
createHASS(),
|
||||
cameraManager,
|
||||
{},
|
||||
createView(),
|
||||
);
|
||||
expect(elementHandler.viewHandler).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should dispatch new view on success', async () => {
|
||||
const elementHandler = createElementListenForView();
|
||||
const cameraManager = createCameraManager();
|
||||
|
||||
const mediaArray = [new ViewMedia('recording', 'camera')];
|
||||
vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(mediaArray);
|
||||
|
||||
await changeViewToRecentRecordingForCameraAndDependents(
|
||||
elementHandler.element,
|
||||
createHASS(),
|
||||
cameraManager,
|
||||
{},
|
||||
createView(),
|
||||
{
|
||||
targetView: 'recordings',
|
||||
select: 'latest',
|
||||
},
|
||||
);
|
||||
expect(elementHandler.viewHandler).toBeCalled();
|
||||
expect(getMediaFromHandlerCall(elementHandler.viewHandler)).toBe(mediaArray);
|
||||
});
|
||||
|
||||
it('should respect media chunk size', async () => {
|
||||
const cameraManager = createCameraManager();
|
||||
|
||||
await changeViewToRecentRecordingForCameraAndDependents(
|
||||
createElementListenForView().element,
|
||||
createHASS(),
|
||||
cameraManager,
|
||||
{
|
||||
performance: createPerformanceConfig({
|
||||
features: {
|
||||
media_chunk_size: 1000,
|
||||
},
|
||||
}),
|
||||
},
|
||||
createView(),
|
||||
);
|
||||
|
||||
expect(cameraManager.generateDefaultRecordingQueries).toBeCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
limit: 1000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('createQueriesForRecordingsView', () => {
|
||||
it('should respect start and end date in recording query', async () => {
|
||||
const cameraManager = createCameraManager({ configs: new Map() });
|
||||
|
||||
vi.mocked(cameraManager.generateDefaultRecordingQueries).mockImplementation(
|
||||
(cameraIDs: string | Set<string>, partialQuery?: PartialRecordingQuery) => [
|
||||
{
|
||||
cameraIDs: setify(cameraIDs),
|
||||
type: QueryType.Recording,
|
||||
...partialQuery,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
const start = new Date('2023-04-29T14:00:00');
|
||||
const end = new Date('2023-04-29T14:59:59');
|
||||
|
||||
const queries = createQueriesForRecordingsView(
|
||||
cameraManager,
|
||||
{},
|
||||
new Set(['camera']),
|
||||
{
|
||||
start: start,
|
||||
end: end,
|
||||
},
|
||||
);
|
||||
|
||||
expect(queries?.getQueries()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
start: start,
|
||||
end: end,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('findBestMediaIndex', () => {
|
||||
it('should find best media index', async () => {
|
||||
const now = new Date();
|
||||
const mediaArray = [
|
||||
generateViewMedia(0, now, 60),
|
||||
generateViewMedia(1, now, 120),
|
||||
generateViewMedia(2, now, 10),
|
||||
];
|
||||
|
||||
expect(findBestMediaIndex(mediaArray, add(now, { seconds: 30 }))).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import { mock } from 'vitest-mock-extended';
|
||||
import { FrigateCardMediaPlayer } from '../../src/types.js';
|
||||
import {
|
||||
FrigateCardHTMLVideoElement,
|
||||
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
||||
hideMediaControlsTemporarily,
|
||||
playMediaMutingIfNecessary,
|
||||
setControlsOnVideo,
|
||||
@@ -108,3 +109,9 @@ describe('playMediaMutingIfNecessary', () => {
|
||||
expect(player.mute).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('constants', () => {
|
||||
it('MEDIA_LOAD_CONTROLS_HIDE_SECONDS', () => {
|
||||
expect(MEDIA_LOAD_CONTROLS_HIDE_SECONDS).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
FrigateCardMediaPlayer,
|
||||
MediaLoadedInfo,
|
||||
MenuButton,
|
||||
ViewDisplayMode,
|
||||
} from '../../src/types';
|
||||
import { createFrigateCardCustomAction } from '../../src/utils/action';
|
||||
import { MenuButtonController } from '../../src/utils/menu-controller';
|
||||
@@ -67,6 +68,7 @@ const calculateButtons = (
|
||||
);
|
||||
};
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('MenuButtonController', () => {
|
||||
let controller: MenuButtonController;
|
||||
const dynamicButton: MenuButton = {
|
||||
@@ -612,7 +614,10 @@ describe('MenuButtonController', () => {
|
||||
|
||||
const cameraManager = createCameraManager();
|
||||
const view = createView({
|
||||
queryResults: new MediaQueriesResults([new ViewMedia('clip', 'camera-1')], 0),
|
||||
queryResults: new MediaQueriesResults({
|
||||
results: [new ViewMedia('clip', 'camera-1')],
|
||||
selectedIndex: 0,
|
||||
}),
|
||||
});
|
||||
mock<CameraManager>(cameraManager).getMediaCapabilities.mockReturnValue(
|
||||
createMediaCapabilities({ canDownload: true }),
|
||||
@@ -640,7 +645,10 @@ describe('MenuButtonController', () => {
|
||||
|
||||
const cameraManager = createCameraManager();
|
||||
const view = createView({
|
||||
queryResults: new MediaQueriesResults([new ViewMedia('clip', 'camera-1')], 0),
|
||||
queryResults: new MediaQueriesResults({
|
||||
results: [new ViewMedia('clip', 'camera-1')],
|
||||
selectedIndex: 0,
|
||||
}),
|
||||
});
|
||||
mock<CameraManager>(cameraManager).getMediaCapabilities.mockReturnValue(
|
||||
createMediaCapabilities({ canDownload: true }),
|
||||
@@ -1084,6 +1092,27 @@ describe('MenuButtonController', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('should have grid button when display mode is', () => {
|
||||
it.each([['single' as const], ['grid' as const]])(
|
||||
'%s',
|
||||
(displayMode: ViewDisplayMode) => {
|
||||
const view = createView({ view: 'live', displayMode: displayMode });
|
||||
expect(calculateButtons(controller, { view: view })).toContainEqual({
|
||||
icon: displayMode === 'single' ? 'mdi:grid' : 'mdi:grid-off',
|
||||
enabled: true,
|
||||
priority: 50,
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
title: 'Display mode',
|
||||
tap_action: {
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'display_mode_select',
|
||||
display_mode: displayMode === 'single' ? 'grid' : 'single',
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle dynamic buttons', () => {
|
||||
const button: MenuButton = {
|
||||
...dynamicButton,
|
||||
|
||||
@@ -62,32 +62,30 @@ describe('generateScreenshotTitle', () => {
|
||||
});
|
||||
|
||||
it('should get title for media viewer view with id', () => {
|
||||
const media = new TestViewMedia(
|
||||
'id1',
|
||||
new Date('2023-06-16T18:52'),
|
||||
'clip',
|
||||
'camera-1',
|
||||
);
|
||||
const media = new TestViewMedia({
|
||||
id: 'id1',
|
||||
startTime: new Date('2023-06-16T18:52'),
|
||||
cameraID: 'camera-1',
|
||||
});
|
||||
const view = createView({
|
||||
view: 'media',
|
||||
camera: 'camera-1',
|
||||
queryResults: new MediaQueriesResults([media], 0),
|
||||
queryResults: new MediaQueriesResults({ results: [media], selectedIndex: 0 }),
|
||||
});
|
||||
|
||||
expect(generateScreenshotTitle(view)).toBe('media-camera-1-id1.jpg');
|
||||
});
|
||||
|
||||
it('should get title for media viewer view without id', () => {
|
||||
const media = new TestViewMedia(
|
||||
null,
|
||||
new Date('2023-06-16T18:52'),
|
||||
'clip',
|
||||
'camera-1',
|
||||
);
|
||||
const media = new TestViewMedia({
|
||||
id: null,
|
||||
startTime: new Date('2023-06-16T18:52'),
|
||||
cameraID: 'camera-1',
|
||||
});
|
||||
const view = createView({
|
||||
view: 'media',
|
||||
camera: 'camera-1',
|
||||
queryResults: new MediaQueriesResults([media], 0),
|
||||
queryResults: new MediaQueriesResults({ results: [media], selectedIndex: 0 }),
|
||||
});
|
||||
|
||||
expect(generateScreenshotTitle(view)).toBe('media-camera-1.jpg');
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { ViewMediaType } from '../../src/view/media';
|
||||
import { ViewMediaClassifier } from '../../src/view/media-classifier';
|
||||
import { TestViewMedia } from '../test-utils';
|
||||
|
||||
describe('ViewMediaClassifier', () => {
|
||||
describe('isEvent', () => {
|
||||
it.each([
|
||||
['clip' as const, true],
|
||||
['snapshot' as const, true],
|
||||
['recording' as const, false],
|
||||
])('%s', (mediaType: ViewMediaType, expectedResult: boolean) => {
|
||||
expect(
|
||||
ViewMediaClassifier.isEvent(new TestViewMedia({ mediaType: mediaType })),
|
||||
).toBe(expectedResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isRecording', () => {
|
||||
it.each([
|
||||
['clip' as const, false],
|
||||
['snapshot' as const, false],
|
||||
['recording' as const, true],
|
||||
])('%s', (mediaType: ViewMediaType, expectedResult: boolean) => {
|
||||
expect(
|
||||
ViewMediaClassifier.isRecording(new TestViewMedia({ mediaType: mediaType })),
|
||||
).toBe(expectedResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isClip', () => {
|
||||
it.each([
|
||||
['clip' as const, true],
|
||||
['snapshot' as const, false],
|
||||
['recording' as const, false],
|
||||
])('%s', (mediaType: ViewMediaType, expectedResult: boolean) => {
|
||||
expect(
|
||||
ViewMediaClassifier.isClip(new TestViewMedia({ mediaType: mediaType })),
|
||||
).toBe(expectedResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSnapshot', () => {
|
||||
it.each([
|
||||
['clip' as const, false],
|
||||
['snapshot' as const, true],
|
||||
['recording' as const, false],
|
||||
])('%s', (mediaType: ViewMediaType, expectedResult: boolean) => {
|
||||
expect(
|
||||
ViewMediaClassifier.isSnapshot(new TestViewMedia({ mediaType: mediaType })),
|
||||
).toBe(expectedResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isVideo', () => {
|
||||
it.each([
|
||||
['clip' as const, true],
|
||||
['snapshot' as const, false],
|
||||
['recording' as const, true],
|
||||
])('%s', (mediaType: ViewMediaType, expectedResult: boolean) => {
|
||||
expect(
|
||||
ViewMediaClassifier.isVideo(new TestViewMedia({ mediaType: mediaType })),
|
||||
).toBe(expectedResult);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,233 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { ViewMedia } from '../../src/view/media';
|
||||
import { MediaQueriesResults } from '../../src/view/media-queries-results';
|
||||
import { generateViewMediaArray } from '../test-utils';
|
||||
|
||||
describe('dispatchViewContextChangeEvent', () => {
|
||||
beforeEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should function with empty results', () => {
|
||||
const fakeNow = new Date('2023-08-07T20:44');
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(fakeNow);
|
||||
|
||||
const results = new MediaQueriesResults();
|
||||
expect(results.isSupersetOf(results)).toBeFalsy();
|
||||
expect(results.getCameraIDs()).toEqual(new Set());
|
||||
expect(results.getResults()).toEqual([]);
|
||||
expect(results.getResultsCount()).toEqual(0);
|
||||
expect(results.hasResults()).toBeFalsy();
|
||||
expect(results.getResult(0)).toBeNull();
|
||||
expect(results.getSelectedIndex()).toBeNull();
|
||||
expect(results.getSelectedResult()).toBeNull();
|
||||
expect(results.hasSelectedResult()).toBeFalsy();
|
||||
|
||||
expect(results.resetSelectedResult()).toBe(results);
|
||||
expect(results.getResultsTimestamp()).toEqual(fakeNow);
|
||||
|
||||
expect(results.selectIndex(0)).toEqual(results);
|
||||
expect(results.getSelectedResult()).toBeNull();
|
||||
|
||||
expect(results.selectResultIfFound((_media: ViewMedia) => true)).toEqual(results);
|
||||
expect(results.getSelectedResult()).toBeNull();
|
||||
|
||||
expect(results.selectBestResult((_media: ViewMedia[]) => null)).toEqual(results);
|
||||
expect(results.getSelectedResult()).toBeNull();
|
||||
});
|
||||
|
||||
it('should function with basic results', () => {
|
||||
const testResults = generateViewMediaArray();
|
||||
const results = new MediaQueriesResults({ results: testResults });
|
||||
|
||||
expect(results.isSupersetOf(results)).toBeTruthy();
|
||||
expect(results.getCameraIDs()).toEqual(new Set(['kitchen', 'office']));
|
||||
expect(results.getResults()).toEqual(testResults);
|
||||
expect(results.getResultsCount()).toEqual(200);
|
||||
expect(results.hasResults()).toBeTruthy();
|
||||
expect(results.getResult(0)).not.toBeNull();
|
||||
expect(results.getSelectedIndex()).toBe(199);
|
||||
expect(results.getSelectedResult()).not.toBeNull();
|
||||
expect(results.hasSelectedResult()).toBeTruthy();
|
||||
|
||||
expect(results.resetSelectedResult()).toBe(results);
|
||||
expect(results.getSelectedResult()).toBeNull();
|
||||
|
||||
expect(results.selectIndex(100)).toEqual(results);
|
||||
expect(results.getSelectedIndex()).toBe(100);
|
||||
|
||||
expect(
|
||||
results.selectResultIfFound(
|
||||
(media: ViewMedia) => media.getID() === 'id-kitchen-42',
|
||||
),
|
||||
).toEqual(results);
|
||||
expect(results.getSelectedResult()?.getID()).toBe('id-kitchen-42');
|
||||
|
||||
expect(
|
||||
results.selectBestResult((mediaArray: ViewMedia[]) =>
|
||||
mediaArray.findIndex((media) => media.getID() === 'id-kitchen-43'),
|
||||
),
|
||||
).toEqual(results);
|
||||
expect(results.getSelectedResult()?.getID()).toBe('id-kitchen-43');
|
||||
});
|
||||
|
||||
it('should function with camera slice', () => {
|
||||
const testResults = generateViewMediaArray();
|
||||
const results = new MediaQueriesResults({ results: testResults });
|
||||
const slice = results.getSlice('office');
|
||||
expect(slice).not.toBeNull();
|
||||
expect(slice!.getResults()).toEqual(
|
||||
testResults.filter((media) => media.getCameraID() === 'office'),
|
||||
);
|
||||
expect(slice!.getResultsCount()).toEqual(100);
|
||||
expect(slice!.hasResults()).toBeTruthy();
|
||||
expect(slice!.getResult(0)).not.toBeNull();
|
||||
expect(slice!.getResult()).toBeNull();
|
||||
expect(slice!.getSelectedIndex()).toBe(99);
|
||||
expect(slice!.getSelectedResult()?.getID()).toEqual('id-office-99');
|
||||
expect(slice!.hasSelectedResult()).toBeTruthy();
|
||||
|
||||
expect(slice!.resetSelectedResult());
|
||||
expect(slice!.getSelectedResult()).toBeNull();
|
||||
|
||||
expect(slice!.selectIndex(10));
|
||||
expect(slice!.getSelectedIndex()).toBe(10);
|
||||
|
||||
expect(slice!.selectIndex(10000));
|
||||
expect(slice!.getSelectedIndex()).toBe(10);
|
||||
|
||||
expect(slice!.selectIndex(-10000));
|
||||
expect(slice!.getSelectedIndex()).toBe(10);
|
||||
|
||||
slice!.selectResultIfFound((media: ViewMedia) => media.getID() === 'id-office-42');
|
||||
expect(slice!.getSelectedResult()?.getID()).toBe('id-office-42');
|
||||
|
||||
slice!.selectBestResult((mediaArray: ViewMedia[]) =>
|
||||
mediaArray.findIndex((media) => media.getID() === 'id-office-43'),
|
||||
);
|
||||
expect(slice!.getSelectedResult()?.getID()).toBe('id-office-43');
|
||||
});
|
||||
|
||||
describe('should respect select approach during construction', () => {
|
||||
it.each([
|
||||
['first' as const, 0],
|
||||
['last' as const, 199],
|
||||
])('%s', async (selectApproach, expectedIndex) => {
|
||||
const results = new MediaQueriesResults({
|
||||
results: generateViewMediaArray(),
|
||||
selectApproach: selectApproach,
|
||||
});
|
||||
expect(results.getSelectedIndex()).toBe(expectedIndex);
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect selectIndex during construction', () => {
|
||||
const results = new MediaQueriesResults({
|
||||
results: generateViewMediaArray(),
|
||||
selectedIndex: 42,
|
||||
});
|
||||
expect(results.getSelectedIndex()).toBe(42);
|
||||
});
|
||||
|
||||
it('should correctly clone a slice', () => {
|
||||
const results = new MediaQueriesResults({
|
||||
results: generateViewMediaArray(),
|
||||
});
|
||||
const slice = results.getSlice('office');
|
||||
const clone = slice?.clone();
|
||||
expect(clone?.getResults()).toBe(slice?.getResults());
|
||||
expect(clone?.getSelectedIndex()).toBe(slice?.getSelectedIndex());
|
||||
});
|
||||
|
||||
it('should not get slice for non-existent camera', () => {
|
||||
const results = new MediaQueriesResults({
|
||||
results: generateViewMediaArray(),
|
||||
});
|
||||
expect(results.getSlice('not-a-camera')).toBeNull();
|
||||
});
|
||||
|
||||
it('should get main slice', () => {
|
||||
const results = new MediaQueriesResults({
|
||||
results: generateViewMediaArray(),
|
||||
});
|
||||
expect(results.getSlice()?.getResults()).toBe(results.getResults());
|
||||
});
|
||||
|
||||
it('should correctly clone', () => {
|
||||
const results = new MediaQueriesResults({
|
||||
results: generateViewMediaArray(),
|
||||
});
|
||||
const clone = results.clone();
|
||||
expect(results.getResultsTimestamp()).toBe(clone.getResultsTimestamp());
|
||||
expect(results.getResults()).toBe(clone.getResults());
|
||||
for (const cameraID of results.getCameraIDs()) {
|
||||
expect(results.getSlice(cameraID)?.getResults()).toBe(
|
||||
clone.getSlice(cameraID)?.getResults(),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not getResults on invalid slice', () => {
|
||||
const results = new MediaQueriesResults({
|
||||
results: generateViewMediaArray(),
|
||||
});
|
||||
expect(results.getResults('not-a-camera')).toBeNull();
|
||||
expect(results.getResultsCount('not-a-camera')).toBe(0);
|
||||
expect(results.hasSelectedResult('not-a-camera')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should always demote main selection', () => {
|
||||
const results = new MediaQueriesResults({
|
||||
results: generateViewMediaArray(),
|
||||
});
|
||||
|
||||
results
|
||||
.getSlice('office')
|
||||
?.selectResultIfFound((media) => media.getID() === 'id-office-42');
|
||||
|
||||
// Verify main and office selections are as expected.
|
||||
expect(results.getSelectedIndex()).toBe(199);
|
||||
expect(results.getSelectedResult('office')?.getID()).toBe('id-office-42');
|
||||
|
||||
// Select a different main result...
|
||||
results?.selectResultIfFound((media) => media.getID() === 'id-office-80');
|
||||
|
||||
// ... and ensure that selection has been demoted into the camera slice.
|
||||
expect(results.getSelectedResult('office')?.getID()).toBe('id-office-80');
|
||||
});
|
||||
|
||||
it('should promote camera selection', () => {
|
||||
const results = new MediaQueriesResults({
|
||||
results: generateViewMediaArray(),
|
||||
});
|
||||
|
||||
results
|
||||
.getSlice('office')
|
||||
?.selectResultIfFound((media) => media.getID() === 'id-office-42');
|
||||
|
||||
expect(results.getSelectedIndex()).toBe(199);
|
||||
|
||||
results.promoteCameraSelectionToMainSelection('office');
|
||||
|
||||
expect(results.getSelectedIndex()).not.toBe(199);
|
||||
expect(results.getSelectedResult()?.getID()).toBe('id-office-42');
|
||||
});
|
||||
|
||||
it('should selectBestResult via advanced selection criteria', () => {
|
||||
const results = new MediaQueriesResults({
|
||||
results: generateViewMediaArray(),
|
||||
});
|
||||
|
||||
results.selectBestResult(
|
||||
(mediaArray: ViewMedia[]) => {
|
||||
const index = mediaArray.findIndex((media) => media.getID()?.endsWith('-42'));
|
||||
return index < 0 ? null : index;
|
||||
},
|
||||
{ allCameras: true },
|
||||
);
|
||||
|
||||
expect(results.getSelectedResult('office')?.getID()).toBe('id-office-42');
|
||||
expect(results.getSelectedResult('kitchen')?.getID()).toBe('id-kitchen-42');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
EventQuery,
|
||||
PartialEventQuery,
|
||||
PartialRecordingQuery,
|
||||
QueryType,
|
||||
RecordingQuery,
|
||||
} from '../../src/camera-manager/types';
|
||||
import { setify } from '../../src/utils/basic';
|
||||
import { EventMediaQueries, RecordingMediaQueries } from '../../src/view/media-queries';
|
||||
|
||||
describe('EventMediaQueries', () => {
|
||||
const createRawEventQueries = (
|
||||
cameraIDs: string | Set<string>,
|
||||
query?: PartialEventQuery,
|
||||
): EventQuery[] => {
|
||||
return [
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: setify(cameraIDs),
|
||||
...query,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
it('should construct', () => {
|
||||
const rawQueries = createRawEventQueries('office');
|
||||
const queries = new EventMediaQueries(rawQueries);
|
||||
expect(queries.getQueries()).toBe(rawQueries);
|
||||
});
|
||||
|
||||
it('should set', () => {
|
||||
const rawQueries = createRawEventQueries('office');
|
||||
const queries = new EventMediaQueries(rawQueries);
|
||||
|
||||
const newRawQueries = createRawEventQueries('kitchen');
|
||||
queries.setQueries(newRawQueries);
|
||||
expect(queries.getQueries()).toBe(newRawQueries);
|
||||
});
|
||||
|
||||
it('should determine if queries exist for CameraIDs', () => {
|
||||
const rawQueries = createRawEventQueries(new Set(['office', 'kitchen']));
|
||||
const queries = new EventMediaQueries(rawQueries);
|
||||
|
||||
expect(queries.hasQueriesForCameraIDs(new Set(['office']))).toBeTruthy();
|
||||
expect(queries.hasQueriesForCameraIDs(new Set(['office', 'kitchen']))).toBeTruthy();
|
||||
expect(
|
||||
queries.hasQueriesForCameraIDs(new Set(['office', 'front_door'])),
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should convert to clips querys', () => {
|
||||
const rawQueries = createRawEventQueries('office', { hasSnapshot: true });
|
||||
const queries = new EventMediaQueries(rawQueries);
|
||||
|
||||
expect(queries.convertToClipsQueries().getQueries()).toEqual([
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set(['office']),
|
||||
hasClip: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should convert when queries are null', () => {
|
||||
const queries = new EventMediaQueries();
|
||||
expect(queries.convertToClipsQueries().getQueries()).toBeNull();
|
||||
});
|
||||
|
||||
it('should clone', () => {
|
||||
const rawQueries = createRawEventQueries('office', { hasSnapshot: true });
|
||||
const queries = new EventMediaQueries(rawQueries);
|
||||
expect(queries.clone().getQueries()).toEqual(queries.getQueries());
|
||||
});
|
||||
});
|
||||
|
||||
describe('RecordingMediaQueries', () => {
|
||||
const createRawRecordingQueries = (
|
||||
cameraIDs: string | Set<string>,
|
||||
query?: PartialRecordingQuery,
|
||||
): RecordingQuery[] => {
|
||||
return [
|
||||
{
|
||||
type: QueryType.Recording,
|
||||
cameraIDs: setify(cameraIDs),
|
||||
...query,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
it('should construct', () => {
|
||||
const rawQueries = createRawRecordingQueries('office');
|
||||
const queries = new RecordingMediaQueries(rawQueries);
|
||||
expect(queries.getQueries()).toBe(rawQueries);
|
||||
});
|
||||
|
||||
it('should clone', () => {
|
||||
const rawQueries = createRawRecordingQueries('office');
|
||||
const queries = new RecordingMediaQueries(rawQueries);
|
||||
expect(queries.clone().getQueries()).toEqual(queries.getQueries());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { VideoContentType, ViewMedia } from '../../src/view/media';
|
||||
import { TestViewMedia } from '../test-utils';
|
||||
|
||||
describe('ViewMedia', () => {
|
||||
beforeEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should construct', () => {
|
||||
const media = new ViewMedia('clip', 'camera');
|
||||
expect(media.getCameraID()).toBe('camera');
|
||||
expect(media.getMediaType()).toBe('clip');
|
||||
expect(media.getVideoContentType()).toBeNull();
|
||||
expect(media.getID()).toBeNull();
|
||||
expect(media.getStartTime()).toBeNull();
|
||||
expect(media.getEndTime()).toBeNull();
|
||||
expect(media.getUsableEndTime()).toBeNull();
|
||||
expect(media.inProgress()).toBeNull();
|
||||
expect(media.getContentID()).toBeNull();
|
||||
expect(media.getTitle()).toBeNull();
|
||||
expect(media.getThumbnail()).toBeNull();
|
||||
expect(media.getTitle()).toBeNull();
|
||||
expect(media.includesTime(new Date())).toBeFalsy();
|
||||
expect(media.getWhere()).toBeNull();
|
||||
expect(media.setFavorite(true)).toBeUndefined();
|
||||
expect(media.isFavorite()).toBeNull();
|
||||
});
|
||||
|
||||
it('should correctly determine if a media item includes a time', () => {
|
||||
const media = new TestViewMedia({
|
||||
startTime: new Date('2023-08-08T17:00:00'),
|
||||
endTime: new Date('2023-08-08T17:59:59'),
|
||||
});
|
||||
expect(media.includesTime(new Date('2023-08-08T17:30:30'))).toBeTruthy();
|
||||
expect(media.includesTime(new Date('2023-08-08T18:00:00'))).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should correctly get usable end time for in-progress event', () => {
|
||||
const media = new TestViewMedia({
|
||||
startTime: new Date('2023-08-08T17:00:00'),
|
||||
inProgress: true,
|
||||
});
|
||||
|
||||
vi.useFakeTimers();
|
||||
const fakeNow = new Date('2023-08-08T17:15:00');
|
||||
vi.setSystemTime(fakeNow);
|
||||
|
||||
expect(media.getUsableEndTime()).toEqual(fakeNow)
|
||||
});
|
||||
});
|
||||
|
||||
describe('VideoContentType', () => {
|
||||
it('MP4', () => {
|
||||
expect(VideoContentType.MP4).toBe('mp4');
|
||||
});
|
||||
it('HLS', () => {
|
||||
expect(VideoContentType.HLS).toBe('hls');
|
||||
});
|
||||
});
|
||||
+87
-7
@@ -1,10 +1,10 @@
|
||||
import { describe, expect, it, test, vi } from 'vitest';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { QueryType } from '../../src/camera-manager/types';
|
||||
import { ViewMedia } from '../../src/view/media';
|
||||
import { EventMediaQueries, RecordingMediaQueries } from '../../src/view/media-queries';
|
||||
import { MediaQueriesResults } from '../../src/view/media-queries-results';
|
||||
import { View, dispatchViewContextChangeEvent } from '../../src/view/view';
|
||||
import { createView } from '../test-utils';
|
||||
import { createView, generateViewMediaArray } from '../test-utils';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('View Basics', () => {
|
||||
@@ -52,6 +52,7 @@ describe('View Basics', () => {
|
||||
query: new EventMediaQueries(),
|
||||
queryResults: new MediaQueriesResults(),
|
||||
context: {},
|
||||
displayMode: 'single',
|
||||
});
|
||||
|
||||
const evolved = view.evolve({
|
||||
@@ -60,12 +61,14 @@ describe('View Basics', () => {
|
||||
query: new EventMediaQueries(),
|
||||
queryResults: new MediaQueriesResults(),
|
||||
context: {},
|
||||
displayMode: 'grid',
|
||||
});
|
||||
expect(evolved.view).not.toBe(view.view);
|
||||
expect(evolved.camera).not.toBe(view.camera);
|
||||
expect(evolved.query).not.toBe(view.query);
|
||||
expect(evolved.queryResults).not.toBe(view.queryResults);
|
||||
expect(evolved.context).not.toBe(view.context);
|
||||
expect(evolved.displayMode).not.toBe(view.displayMode);
|
||||
});
|
||||
|
||||
it('should evolve with nothing set', () => {
|
||||
@@ -119,6 +122,13 @@ describe('View Basics', () => {
|
||||
expect(view.context).toEqual({});
|
||||
});
|
||||
|
||||
it('should remove context property', () => {
|
||||
const view = createView({ context: { live: { overrides: new Map() } } });
|
||||
|
||||
view.removeContextProperty('live', 'overrides');
|
||||
expect(view.context).toEqual({ live: {} });
|
||||
});
|
||||
|
||||
it('should detect gallery views', () => {
|
||||
expect(createView({ view: 'clips' }).isGalleryView()).toBeTruthy();
|
||||
expect(createView({ view: 'snapshots' }).isGalleryView()).toBeTruthy();
|
||||
@@ -240,8 +250,8 @@ describe('View.isMajorMediaChange', () => {
|
||||
|
||||
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(media, 0);
|
||||
const queryResults_2 = new MediaQueriesResults(media, 1);
|
||||
const queryResults_1 = new MediaQueriesResults({ results: media, selectedIndex: 0 });
|
||||
const queryResults_2 = new MediaQueriesResults({ results: media, selectedIndex: 1 });
|
||||
expect(
|
||||
View.isMajorMediaChange(
|
||||
createView({ view: 'media', queryResults: queryResults_1 }),
|
||||
@@ -252,8 +262,8 @@ describe('View.isMajorMediaChange', () => {
|
||||
|
||||
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(media, 0);
|
||||
const queryResults_2 = new MediaQueriesResults(media, 1);
|
||||
const queryResults_1 = new MediaQueriesResults({ results: media, selectedIndex: 0 });
|
||||
const queryResults_2 = new MediaQueriesResults({ results: media, selectedIndex: 1 });
|
||||
expect(
|
||||
View.isMajorMediaChange(
|
||||
createView({ queryResults: queryResults_1 }),
|
||||
@@ -282,7 +292,7 @@ describe('View.adoptFromViewIfAppropriate', () => {
|
||||
expect(next.queryResults).toBe(queryResults);
|
||||
});
|
||||
|
||||
test.each([
|
||||
it.each([
|
||||
[
|
||||
new EventMediaQueries([
|
||||
{ type: QueryType.Event, cameraIDs: new Set(['camera']), hasClip: true },
|
||||
@@ -409,6 +419,76 @@ describe('View.adoptFromViewIfAppropriate', () => {
|
||||
View.adoptFromViewIfAppropriate(next, current);
|
||||
expect(next.context?.live).toEqual(current.context?.live);
|
||||
});
|
||||
|
||||
it('should determine if display mode is grid', () => {
|
||||
expect(createView({ displayMode: 'grid' }).isGrid()).toBeTruthy();
|
||||
expect(createView({ displayMode: 'single' }).isGrid()).toBeFalsy();
|
||||
expect(createView().isGrid()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should determine if view supports multiple display modes', () => {
|
||||
const resultsOne = new MediaQueriesResults({
|
||||
results: generateViewMediaArray({ cameraIDs: ['office'] }),
|
||||
});
|
||||
const resultsTwo = new MediaQueriesResults({
|
||||
results: generateViewMediaArray({ cameraIDs: ['office', 'kitchen'] }),
|
||||
});
|
||||
|
||||
expect(createView({ view: 'live' }).hasMultipleDisplayModes()).toBeFalsy();
|
||||
expect(createView({ view: 'live' }).hasMultipleDisplayModes(0)).toBeFalsy();
|
||||
expect(createView({ view: 'live' }).hasMultipleDisplayModes(1)).toBeFalsy();
|
||||
expect(createView({ view: 'live' }).hasMultipleDisplayModes(2)).toBeTruthy();
|
||||
|
||||
expect(createView({ view: 'media' }).hasMultipleDisplayModes()).toBeFalsy();
|
||||
expect(
|
||||
createView({ view: 'media', queryResults: resultsOne }).hasMultipleDisplayModes(),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
createView({ view: 'media', queryResults: resultsTwo }).hasMultipleDisplayModes(),
|
||||
).toBeTruthy();
|
||||
|
||||
expect(createView({ view: 'clip' }).hasMultipleDisplayModes()).toBeFalsy();
|
||||
expect(
|
||||
createView({ view: 'clip', queryResults: resultsOne }).hasMultipleDisplayModes(),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
createView({ view: 'clip', queryResults: resultsTwo }).hasMultipleDisplayModes(),
|
||||
).toBeTruthy();
|
||||
|
||||
expect(createView({ view: 'snapshot' }).hasMultipleDisplayModes()).toBeFalsy();
|
||||
expect(
|
||||
createView({
|
||||
view: 'snapshot',
|
||||
queryResults: resultsOne,
|
||||
}).hasMultipleDisplayModes(),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
createView({
|
||||
view: 'snapshot',
|
||||
queryResults: resultsTwo,
|
||||
}).hasMultipleDisplayModes(),
|
||||
).toBeTruthy();
|
||||
|
||||
expect(createView({ view: 'recording' }).hasMultipleDisplayModes()).toBeFalsy();
|
||||
expect(
|
||||
createView({
|
||||
view: 'recording',
|
||||
queryResults: resultsOne,
|
||||
}).hasMultipleDisplayModes(),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
createView({
|
||||
view: 'recording',
|
||||
queryResults: resultsTwo,
|
||||
}).hasMultipleDisplayModes(),
|
||||
).toBeTruthy();
|
||||
|
||||
expect(createView({ view: 'clips' }).hasMultipleDisplayModes()).toBeFalsy();
|
||||
expect(createView({ view: 'snapshots' }).hasMultipleDisplayModes()).toBeFalsy();
|
||||
expect(createView({ view: 'recordings' }).hasMultipleDisplayModes()).toBeFalsy();
|
||||
expect(createView({ view: 'image' }).hasMultipleDisplayModes()).toBeFalsy();
|
||||
expect(createView({ view: 'timeline' }).hasMultipleDisplayModes()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
|
||||
Reference in New Issue
Block a user