@@ -0,0 +1,134 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { ViewManagerEpoch } from '../../../src/card-controller/view/types';
|
||||
import {
|
||||
getUpFolderMediaItem,
|
||||
upFolderClickHandler,
|
||||
} from '../../../src/components-lib/folder/up-folder';
|
||||
import { ViewFolder } from '../../../src/view/item';
|
||||
import { EventMediaQuery, FolderViewQuery } from '../../../src/view/query';
|
||||
import {
|
||||
createCardAPI,
|
||||
createFolder,
|
||||
createView,
|
||||
TestViewMedia,
|
||||
} from '../../test-utils';
|
||||
|
||||
describe('upFolderClickHandler', () => {
|
||||
const item = new TestViewMedia();
|
||||
|
||||
it('should ignore non-folder query', () => {
|
||||
const api = createCardAPI();
|
||||
const view = createView({
|
||||
query: new EventMediaQuery(),
|
||||
});
|
||||
vi.mocked(api.getViewManager().getView).mockReturnValue(view);
|
||||
|
||||
const epoch: ViewManagerEpoch = {
|
||||
manager: api.getViewManager(),
|
||||
};
|
||||
|
||||
upFolderClickHandler(item, new Event('click'), epoch);
|
||||
|
||||
expect(api.getViewManager().setViewByParametersWithExistingQuery).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should ignore folder query without raw query', () => {
|
||||
const api = createCardAPI();
|
||||
const view = createView({
|
||||
query: new FolderViewQuery(),
|
||||
});
|
||||
vi.mocked(api.getViewManager().getView).mockReturnValue(view);
|
||||
|
||||
const epoch: ViewManagerEpoch = {
|
||||
manager: api.getViewManager(),
|
||||
};
|
||||
|
||||
upFolderClickHandler(item, new Event('click'), epoch);
|
||||
|
||||
expect(api.getViewManager().setViewByParametersWithExistingQuery).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should ignore folder query wihout parent to go up to', () => {
|
||||
const api = createCardAPI();
|
||||
const view = createView({
|
||||
query: new FolderViewQuery({ folder: createFolder(), path: ['path'] }),
|
||||
});
|
||||
vi.mocked(api.getViewManager().getView).mockReturnValue(view);
|
||||
|
||||
const epoch: ViewManagerEpoch = {
|
||||
manager: api.getViewManager(),
|
||||
};
|
||||
|
||||
upFolderClickHandler(item, new Event('click'), epoch);
|
||||
|
||||
expect(api.getViewManager().setViewByParametersWithExistingQuery).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should go up in the folder hierarchy', () => {
|
||||
const api = createCardAPI();
|
||||
const folder = createFolder();
|
||||
const view = createView({
|
||||
query: new FolderViewQuery({
|
||||
folder,
|
||||
path: ['one', 'two', 'three'],
|
||||
}),
|
||||
});
|
||||
vi.mocked(api.getViewManager().getView).mockReturnValue(view);
|
||||
|
||||
const epoch: ViewManagerEpoch = {
|
||||
manager: api.getViewManager(),
|
||||
};
|
||||
|
||||
upFolderClickHandler(item, new Event('click'), epoch);
|
||||
|
||||
expect(api.getViewManager().setViewByParametersWithExistingQuery).toBeCalledWith({
|
||||
params: {
|
||||
query: expect.any(FolderViewQuery),
|
||||
},
|
||||
});
|
||||
|
||||
const query = vi.mocked(api.getViewManager().setViewByParametersWithExistingQuery)
|
||||
.mock.calls[0][0]?.params?.query;
|
||||
expect(query?.getQuery()).toEqual({
|
||||
folder,
|
||||
path: ['one', 'two'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUpFolderMediaItem', () => {
|
||||
it('should ignore non-folder query', () => {
|
||||
const view = createView({
|
||||
query: new EventMediaQuery(),
|
||||
});
|
||||
expect(getUpFolderMediaItem(view)).toBeNull();
|
||||
});
|
||||
|
||||
it('should ignore folder query without raw query', () => {
|
||||
const view = createView({
|
||||
query: new FolderViewQuery(),
|
||||
});
|
||||
expect(getUpFolderMediaItem(view)).toBeNull();
|
||||
});
|
||||
|
||||
it('should ignore folder query without parents', () => {
|
||||
const view = createView({
|
||||
query: new FolderViewQuery({ folder: createFolder(), path: ['one'] }),
|
||||
});
|
||||
expect(getUpFolderMediaItem(view)).toBeNull();
|
||||
});
|
||||
|
||||
it('should get up folder media', () => {
|
||||
const view = createView({
|
||||
query: new FolderViewQuery({
|
||||
folder: createFolder(),
|
||||
path: ['one', 'two', 'three'],
|
||||
}),
|
||||
});
|
||||
|
||||
const folderMedia = getUpFolderMediaItem(view);
|
||||
|
||||
expect(folderMedia).toBeInstanceOf(ViewFolder);
|
||||
expect(folderMedia?.getIcon()).toBe('mdi:arrow-up-left');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,234 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { ViewManagerInterface } from '../../../src/card-controller/view/types';
|
||||
import {
|
||||
FOLDER_GALLERY_THUMBNAIL_DETAILS_WIDTH_MIN,
|
||||
FolderGalleryController,
|
||||
} from '../../../src/components-lib/gallery/folder-gallery-controller';
|
||||
import { THUMBNAIL_WIDTH_DEFAULT } from '../../../src/config/schema/common/controls/thumbnails';
|
||||
import { ViewFolder } from '../../../src/view/item';
|
||||
import { FolderViewQuery } from '../../../src/view/query';
|
||||
import { QueryResults } from '../../../src/view/query-results';
|
||||
import { createFolder, createView, TestViewMedia } from '../../test-utils';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('FolderGalleryController', () => {
|
||||
describe('should set thumbnail size', () => {
|
||||
it('should set thumbnail size explicitly', () => {
|
||||
const host = document.createElement('div');
|
||||
const controller = new FolderGalleryController(host);
|
||||
|
||||
controller.setThumbnailSize(100);
|
||||
|
||||
expect(host.style.getPropertyValue('--advanced-camera-card-thumbnail-size')).toBe(
|
||||
'100px',
|
||||
);
|
||||
});
|
||||
|
||||
it('should set thumbnail size implicitly', () => {
|
||||
const host = document.createElement('div');
|
||||
const controller = new FolderGalleryController(host);
|
||||
|
||||
controller.setThumbnailSize();
|
||||
|
||||
expect(host.style.getPropertyValue('--advanced-camera-card-thumbnail-size')).toBe(
|
||||
`${THUMBNAIL_WIDTH_DEFAULT}px`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should get column width', () => {
|
||||
it('should get default column width', () => {
|
||||
const host = document.createElement('div');
|
||||
const controller = new FolderGalleryController(host);
|
||||
|
||||
expect(controller.getColumnWidth()).toBe(THUMBNAIL_WIDTH_DEFAULT);
|
||||
});
|
||||
|
||||
it('should get column width with defailts', () => {
|
||||
const host = document.createElement('div');
|
||||
const controller = new FolderGalleryController(host);
|
||||
|
||||
expect(
|
||||
controller.getColumnWidth({
|
||||
size: 100,
|
||||
show_details: true,
|
||||
show_favorite_control: true,
|
||||
show_timeline_control: true,
|
||||
show_download_control: true,
|
||||
}),
|
||||
).toBe(FOLDER_GALLERY_THUMBNAIL_DETAILS_WIDTH_MIN);
|
||||
});
|
||||
|
||||
it('should get column width with explicit size', () => {
|
||||
const host = document.createElement('div');
|
||||
const controller = new FolderGalleryController(host);
|
||||
|
||||
expect(
|
||||
controller.getColumnWidth({
|
||||
size: 142,
|
||||
show_details: false,
|
||||
show_favorite_control: true,
|
||||
show_timeline_control: true,
|
||||
show_download_control: true,
|
||||
}),
|
||||
).toBe(142);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should get column count round method', () => {
|
||||
it('should get default column count round method', () => {
|
||||
const host = document.createElement('div');
|
||||
const controller = new FolderGalleryController(host);
|
||||
|
||||
expect(controller.getColumnCountRoundMethod()).toBe('ceil');
|
||||
});
|
||||
|
||||
it('should get column count round method when details being shown', () => {
|
||||
const host = document.createElement('div');
|
||||
const controller = new FolderGalleryController(host);
|
||||
|
||||
expect(
|
||||
controller.getColumnCountRoundMethod({
|
||||
size: 100,
|
||||
show_details: true,
|
||||
show_favorite_control: true,
|
||||
show_timeline_control: true,
|
||||
show_download_control: true,
|
||||
}),
|
||||
).toBe('floor');
|
||||
});
|
||||
});
|
||||
|
||||
describe('should handle item clicks', () => {
|
||||
it('should ignore calls without view', () => {
|
||||
const host = document.createElement('div');
|
||||
const controller = new FolderGalleryController(host);
|
||||
|
||||
const viewManager = mock<ViewManagerInterface>();
|
||||
|
||||
const item = new TestViewMedia();
|
||||
controller.itemClickHandler(viewManager, item, new Event('click'));
|
||||
|
||||
expect(viewManager.setViewByParameters).not.toHaveBeenCalled();
|
||||
expect(viewManager.setViewByParametersWithExistingQuery).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle media item click', () => {
|
||||
const host = document.createElement('div');
|
||||
const item_1 = new TestViewMedia();
|
||||
const item_2 = new TestViewMedia();
|
||||
|
||||
const controller = new FolderGalleryController(host);
|
||||
const event = new Event('click');
|
||||
|
||||
const view = createView({
|
||||
queryResults: new QueryResults({ results: [item_1, item_2], selectedIndex: 0 }),
|
||||
});
|
||||
|
||||
const viewManager = mock<ViewManagerInterface>();
|
||||
viewManager.getView.mockReturnValue(view);
|
||||
|
||||
controller.itemClickHandler(viewManager, item_2, event);
|
||||
|
||||
expect(viewManager.setViewByParameters).toHaveBeenCalledWith({
|
||||
params: {
|
||||
view: 'media',
|
||||
queryResults: expect.any(QueryResults),
|
||||
},
|
||||
});
|
||||
|
||||
const newQueryResults =
|
||||
viewManager.setViewByParameters.mock.calls[0][0]?.params?.queryResults;
|
||||
expect(newQueryResults).toBeInstanceOf(QueryResults);
|
||||
expect(newQueryResults?.getSelectedResult()).toBe(item_2);
|
||||
});
|
||||
|
||||
describe('should handle folder click', () => {
|
||||
it('should handle normal folder click', () => {
|
||||
const folder = createFolder();
|
||||
const folderItem = new ViewFolder(folder, {
|
||||
id: 'parent',
|
||||
});
|
||||
|
||||
const controller = new FolderGalleryController(document.createElement('div'));
|
||||
const event = new Event('click');
|
||||
|
||||
const view = createView({
|
||||
queryResults: new QueryResults({
|
||||
results: [new TestViewMedia(), folderItem],
|
||||
selectedIndex: 0,
|
||||
}),
|
||||
query: new FolderViewQuery({
|
||||
folder,
|
||||
path: [{ id: 'grandparent' }],
|
||||
}),
|
||||
});
|
||||
|
||||
const viewManager = mock<ViewManagerInterface>();
|
||||
viewManager.getView.mockReturnValue(view);
|
||||
|
||||
controller.itemClickHandler(viewManager, folderItem, event);
|
||||
|
||||
expect(viewManager.setViewByParametersWithExistingQuery).toHaveBeenCalledWith({
|
||||
params: {
|
||||
query: expect.any(FolderViewQuery),
|
||||
},
|
||||
});
|
||||
|
||||
const newQuery =
|
||||
viewManager.setViewByParametersWithExistingQuery.mock.calls[0][0]?.params
|
||||
?.query;
|
||||
expect(newQuery).toBeInstanceOf(FolderViewQuery);
|
||||
expect(newQuery?.getQuery()).toEqual({
|
||||
folder,
|
||||
path: [{ id: 'grandparent' }, { id: folderItem.getID() }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle folder click without query', () => {
|
||||
const controller = new FolderGalleryController(document.createElement('div'));
|
||||
|
||||
const view = createView({
|
||||
query: null,
|
||||
});
|
||||
const viewManager = mock<ViewManagerInterface>();
|
||||
viewManager.getView.mockReturnValue(view);
|
||||
|
||||
controller.itemClickHandler(
|
||||
viewManager,
|
||||
new ViewFolder(createFolder(), {
|
||||
id: 'parent',
|
||||
}),
|
||||
new Event('click'),
|
||||
);
|
||||
|
||||
expect(viewManager.setViewByParametersWithExistingQuery).not.toBeCalled();
|
||||
expect(viewManager.setViewByParameters).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should handle folder click without path', () => {
|
||||
const controller = new FolderGalleryController(document.createElement('div'));
|
||||
const folder = createFolder();
|
||||
|
||||
const view = createView({
|
||||
query: new FolderViewQuery({
|
||||
folder,
|
||||
path: [{ id: 'id' }],
|
||||
}),
|
||||
});
|
||||
const viewManager = mock<ViewManagerInterface>();
|
||||
viewManager.getView.mockReturnValue(view);
|
||||
|
||||
controller.itemClickHandler(
|
||||
viewManager,
|
||||
new ViewFolder(folder),
|
||||
new Event('click'),
|
||||
);
|
||||
|
||||
expect(viewManager.setViewByParametersWithExistingQuery).not.toBeCalled();
|
||||
expect(viewManager.setViewByParameters).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,650 @@
|
||||
import { LitElement } from 'lit';
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
GalleryColumnCountRoundMethod,
|
||||
GalleryCoreController,
|
||||
} from '../../../src/components-lib/gallery/gallery-core-controller';
|
||||
import { GalleryExtendEvent } from '../../../src/components/gallery/types';
|
||||
import { scrollIntoView } from '../../../src/utils/scroll';
|
||||
import { sleep } from '../../../src/utils/sleep';
|
||||
import {
|
||||
callIntersectionHandler,
|
||||
createLitElement,
|
||||
createSlot,
|
||||
createSlotHost,
|
||||
createTouch,
|
||||
createTouchEvent,
|
||||
flushPromises,
|
||||
IntersectionObserverMock,
|
||||
ResizeObserverMock,
|
||||
} from '../../test-utils';
|
||||
import { callResizeHandler } from '../../utils/embla/test-utils';
|
||||
|
||||
vi.mock('lodash-es', async () => {
|
||||
return {
|
||||
...(await vi.importActual('lodash-es')),
|
||||
throttle: vi.fn((fn) => fn),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../../src/utils/sleep');
|
||||
vi.mock('../../../src/utils/scroll');
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('GalleryCoreController', () => {
|
||||
const createController = (options?: {
|
||||
host?: LitElement;
|
||||
getSlot?: () => HTMLSlotElement | null;
|
||||
getSentinelBottom?: () => HTMLElement | null;
|
||||
showLoaderTopCallback?: (show: boolean) => void;
|
||||
showSentinelBottomCallback?: (show: boolean) => void;
|
||||
}) => {
|
||||
return new GalleryCoreController(
|
||||
options?.host ?? createLitElement(),
|
||||
options?.getSlot ?? (() => null),
|
||||
options?.getSentinelBottom ?? (() => null),
|
||||
options?.showLoaderTopCallback ?? (() => null),
|
||||
options?.showSentinelBottomCallback ?? (() => null),
|
||||
);
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
vi.stubGlobal('ResizeObserver', ResizeObserverMock);
|
||||
vi.stubGlobal('IntersectionObserver', IntersectionObserverMock);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('should add controller', () => {
|
||||
const host = createLitElement();
|
||||
const controller = createController({ host });
|
||||
expect(controller).toBeInstanceOf(GalleryCoreController);
|
||||
expect(host.addController).toBeCalledWith(controller);
|
||||
});
|
||||
|
||||
it('should remove controller', () => {
|
||||
const host = createLitElement();
|
||||
const controller = createController({ host });
|
||||
expect(controller).toBeInstanceOf(GalleryCoreController);
|
||||
controller.removeController();
|
||||
expect(host.removeController).toBeCalledWith(controller);
|
||||
});
|
||||
|
||||
it('should remove controller', () => {
|
||||
const host = createLitElement();
|
||||
const controller = createController({ host });
|
||||
expect(controller).toBeInstanceOf(GalleryCoreController);
|
||||
controller.removeController();
|
||||
expect(host.removeController).toBeCalledWith(controller);
|
||||
});
|
||||
|
||||
describe('should observe sentintel when host updated', () => {
|
||||
it('should disconnect and observe sentinel when valid sentinel exists', () => {
|
||||
const sentinel = document.createElement('div');
|
||||
const getSentinelBottom = vi.fn(() => sentinel);
|
||||
const host = createLitElement();
|
||||
|
||||
const controller = createController({
|
||||
host,
|
||||
getSentinelBottom,
|
||||
});
|
||||
|
||||
controller.hostUpdated();
|
||||
|
||||
expect(
|
||||
vi.mocked(IntersectionObserver).mock.results[0].value.disconnect,
|
||||
).toBeCalled();
|
||||
expect(
|
||||
vi.mocked(IntersectionObserver).mock.results[0].value.observe,
|
||||
).toBeCalledWith(sentinel);
|
||||
});
|
||||
|
||||
it('should not observe when sentinel is null', () => {
|
||||
const getSentinelBottom = vi.fn(() => null);
|
||||
const host = createLitElement();
|
||||
|
||||
const controller = createController({
|
||||
host,
|
||||
getSentinelBottom,
|
||||
});
|
||||
|
||||
controller.hostUpdated();
|
||||
|
||||
expect(
|
||||
vi.mocked(IntersectionObserver).mock.results[0].value.disconnect,
|
||||
).toBeCalled();
|
||||
expect(
|
||||
vi.mocked(IntersectionObserver).mock.results[0].value.observe,
|
||||
).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should attach listeners on connect', () => {
|
||||
const host = createLitElement();
|
||||
host.addEventListener = vi.fn();
|
||||
const controller = createController({ host });
|
||||
|
||||
controller.hostConnected();
|
||||
|
||||
expect(vi.mocked(ResizeObserver).mock.results[0].value.observe).toBeCalledWith(host);
|
||||
expect(host.addEventListener).toBeCalledWith('wheel', expect.anything(), {
|
||||
passive: true,
|
||||
});
|
||||
expect(host.addEventListener).toBeCalledWith('touchstart', expect.anything(), {
|
||||
passive: true,
|
||||
});
|
||||
expect(host.addEventListener).toBeCalledWith('touchend', expect.anything());
|
||||
expect(host.requestUpdate).toBeCalled();
|
||||
});
|
||||
|
||||
it('should detach listeners on disconnect', () => {
|
||||
const host = createLitElement();
|
||||
host.removeEventListener = vi.fn();
|
||||
const controller = createController({ host });
|
||||
|
||||
controller.hostDisconnected();
|
||||
|
||||
expect(vi.mocked(ResizeObserver).mock.results[0].value.disconnect).toBeCalled();
|
||||
expect(
|
||||
vi.mocked(IntersectionObserver).mock.results[0].value.disconnect,
|
||||
).toBeCalled();
|
||||
expect(host.removeEventListener).toBeCalledWith('wheel', expect.anything());
|
||||
expect(host.removeEventListener).toBeCalledWith('touchstart', expect.anything());
|
||||
expect(host.removeEventListener).toBeCalledWith('touchend', expect.anything());
|
||||
});
|
||||
|
||||
describe('should set the number of columns', () => {
|
||||
it('should update columns based on columnWidth and host width', () => {
|
||||
const host = createLitElement();
|
||||
Object.defineProperty(host, 'clientWidth', { value: 492 });
|
||||
|
||||
const controller = createController({ host });
|
||||
|
||||
controller.setOptions({ columnWidth: 250 });
|
||||
|
||||
expect(host.style.getPropertyValue('--advanced-camera-card-gallery-columns')).toBe(
|
||||
'1',
|
||||
);
|
||||
});
|
||||
|
||||
it('should calculate at least 1 column', () => {
|
||||
const host = createLitElement();
|
||||
Object.defineProperty(host, 'clientWidth', { value: 100 });
|
||||
|
||||
const controller = createController({ host });
|
||||
|
||||
controller.setOptions({ columnWidth: 200 });
|
||||
|
||||
expect(host.style.getPropertyValue('--advanced-camera-card-gallery-columns')).toBe(
|
||||
'1',
|
||||
);
|
||||
});
|
||||
|
||||
describe('should respect column count round method', () => {
|
||||
it.each([
|
||||
['ceil' as const, 2],
|
||||
['floor' as const, 1],
|
||||
])(
|
||||
'%s',
|
||||
async (
|
||||
columnCountRoundMethod: GalleryColumnCountRoundMethod,
|
||||
expectedColumns: number,
|
||||
) => {
|
||||
const host = createLitElement();
|
||||
Object.defineProperty(host, 'clientWidth', { value: 100 });
|
||||
|
||||
const controller = createController({ host });
|
||||
|
||||
controller.setOptions({ columnWidth: 51, columnCountRoundMethod });
|
||||
|
||||
expect(
|
||||
host.style.getPropertyValue('--advanced-camera-card-gallery-columns'),
|
||||
).toBe(String(expectedColumns));
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should not update columns when columnWidth is not set', () => {
|
||||
const host = createLitElement();
|
||||
Object.defineProperty(host, 'clientWidth', { value: 500 });
|
||||
|
||||
const controller = createController({ host });
|
||||
|
||||
controller.setOptions({});
|
||||
|
||||
expect(host.style.getPropertyValue('--advanced-camera-card-gallery-columns')).toBe(
|
||||
'',
|
||||
);
|
||||
});
|
||||
|
||||
it('should update columns on resize', () => {
|
||||
const host = createLitElement();
|
||||
const controller = createController({ host });
|
||||
controller.setOptions({ columnWidth: 250 });
|
||||
|
||||
const width = 1000;
|
||||
Object.defineProperty(host, 'clientWidth', { value: width });
|
||||
callResizeHandler([{ target: host, width, height: 200 }]);
|
||||
|
||||
expect(host.style.getPropertyValue('--advanced-camera-card-gallery-columns')).toBe(
|
||||
'4',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should extend gallery', () => {
|
||||
describe('should extend gallery up', () => {
|
||||
describe('should extend gallery up with wheel events', () => {
|
||||
it('should extend up with wheel event upwards', async () => {
|
||||
const host = createLitElement();
|
||||
host.scrollTop = 0;
|
||||
const showLoaderTop = vi.fn();
|
||||
const controller = createController({
|
||||
host,
|
||||
showLoaderTopCallback: showLoaderTop,
|
||||
});
|
||||
|
||||
controller.setOptions({ extendUp: true });
|
||||
controller.hostConnected();
|
||||
|
||||
const extendUpCallback = vi
|
||||
.fn()
|
||||
.mockImplementation((ev: CustomEvent<GalleryExtendEvent>) => {
|
||||
expect(showLoaderTop).toHaveBeenLastCalledWith(true);
|
||||
|
||||
ev.detail.resolve();
|
||||
});
|
||||
host.addEventListener(
|
||||
'advanced-camera-card:gallery:extend:up',
|
||||
extendUpCallback,
|
||||
);
|
||||
|
||||
host.dispatchEvent(new WheelEvent('wheel', { deltaY: -100 }));
|
||||
|
||||
await flushPromises();
|
||||
expect(showLoaderTop).toHaveBeenLastCalledWith(false);
|
||||
});
|
||||
|
||||
it('should not extend up with wheel event when not at top of component', async () => {
|
||||
const host = createLitElement();
|
||||
host.scrollTop = 100;
|
||||
const showLoaderTop = vi.fn();
|
||||
const controller = createController({
|
||||
host,
|
||||
showLoaderTopCallback: showLoaderTop,
|
||||
});
|
||||
|
||||
controller.setOptions({ extendUp: true });
|
||||
controller.hostConnected();
|
||||
|
||||
host.dispatchEvent(new WheelEvent('wheel', { deltaY: -100 }));
|
||||
|
||||
expect(showLoaderTop).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not extend up with wheel event downwards', async () => {
|
||||
const host = createLitElement();
|
||||
host.scrollTop = 0;
|
||||
const showLoaderTop = vi.fn();
|
||||
const controller = createController({
|
||||
host,
|
||||
showLoaderTopCallback: showLoaderTop,
|
||||
});
|
||||
|
||||
controller.setOptions({ extendUp: true });
|
||||
controller.hostConnected();
|
||||
|
||||
host.dispatchEvent(new WheelEvent('wheel', { deltaY: 100 }));
|
||||
|
||||
expect(showLoaderTop).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should extend gallery up with touch events', () => {
|
||||
it('should extend up with touch event upwards', async () => {
|
||||
const host = createLitElement();
|
||||
const showLoaderTop = vi.fn();
|
||||
const controller = createController({
|
||||
host,
|
||||
showLoaderTopCallback: showLoaderTop,
|
||||
});
|
||||
|
||||
controller.setOptions({ extendUp: true });
|
||||
controller.hostConnected();
|
||||
|
||||
const extendUpCallback = vi
|
||||
.fn()
|
||||
.mockImplementation((ev: CustomEvent<GalleryExtendEvent>) => {
|
||||
expect(showLoaderTop).toHaveBeenLastCalledWith(true);
|
||||
|
||||
ev.detail.resolve();
|
||||
});
|
||||
|
||||
host.addEventListener(
|
||||
'advanced-camera-card:gallery:extend:up',
|
||||
extendUpCallback,
|
||||
);
|
||||
|
||||
host.dispatchEvent(
|
||||
createTouchEvent('touchstart', { touches: [createTouch({ screenY: 0 })] }),
|
||||
);
|
||||
host.dispatchEvent(
|
||||
createTouchEvent('touchend', {
|
||||
changedTouches: [createTouch({ screenY: 100 })],
|
||||
}),
|
||||
);
|
||||
|
||||
await flushPromises();
|
||||
expect(showLoaderTop).toHaveBeenLastCalledWith(false);
|
||||
});
|
||||
|
||||
it('should not extend up with multiple touches', async () => {
|
||||
const host = createLitElement();
|
||||
const showLoaderTop = vi.fn();
|
||||
const controller = createController({
|
||||
host,
|
||||
showLoaderTopCallback: showLoaderTop,
|
||||
});
|
||||
|
||||
controller.setOptions({ extendUp: true });
|
||||
controller.hostConnected();
|
||||
|
||||
host.dispatchEvent(
|
||||
createTouchEvent('touchstart', {
|
||||
touches: [createTouch({ screenY: 0 }), createTouch({ screenY: 0 })],
|
||||
}),
|
||||
);
|
||||
host.dispatchEvent(
|
||||
createTouchEvent('touchend', {
|
||||
changedTouches: [createTouch({ screenY: 100 })],
|
||||
}),
|
||||
);
|
||||
|
||||
await flushPromises();
|
||||
expect(showLoaderTop).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should not extend up with touch when when not at top of component', async () => {
|
||||
const host = createLitElement();
|
||||
host.scrollTop = 100;
|
||||
const showLoaderTop = vi.fn();
|
||||
const controller = createController({
|
||||
host,
|
||||
showLoaderTopCallback: showLoaderTop,
|
||||
});
|
||||
|
||||
controller.setOptions({ extendUp: true });
|
||||
controller.hostConnected();
|
||||
|
||||
host.dispatchEvent(
|
||||
createTouchEvent('touchstart', {
|
||||
touches: [createTouch({ screenY: 0 }), createTouch({ screenY: 0 })],
|
||||
}),
|
||||
);
|
||||
host.dispatchEvent(
|
||||
createTouchEvent('touchend', {
|
||||
changedTouches: [createTouch({ screenY: 100 })],
|
||||
}),
|
||||
);
|
||||
|
||||
await flushPromises();
|
||||
expect(showLoaderTop).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should not extend up with touch when touches moved downwards', async () => {
|
||||
const host = createLitElement();
|
||||
const showLoaderTop = vi.fn();
|
||||
const controller = createController({
|
||||
host,
|
||||
showLoaderTopCallback: showLoaderTop,
|
||||
});
|
||||
|
||||
controller.setOptions({ extendUp: true });
|
||||
controller.hostConnected();
|
||||
|
||||
host.dispatchEvent(
|
||||
createTouchEvent('touchstart', {
|
||||
touches: [createTouch({ screenY: 100 })],
|
||||
}),
|
||||
);
|
||||
host.dispatchEvent(
|
||||
createTouchEvent('touchend', {
|
||||
changedTouches: [createTouch({ screenY: 0 })],
|
||||
}),
|
||||
);
|
||||
|
||||
await flushPromises();
|
||||
expect(showLoaderTop).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not extend when extending up is disabled', async () => {
|
||||
const host = createLitElement();
|
||||
const showLoaderTop = vi.fn();
|
||||
const controller = createController({
|
||||
host,
|
||||
showLoaderTopCallback: showLoaderTop,
|
||||
});
|
||||
|
||||
controller.setOptions({ extendUp: false });
|
||||
controller.hostConnected();
|
||||
|
||||
host.dispatchEvent(new WheelEvent('wheel', { deltaY: -100 }));
|
||||
|
||||
expect(showLoaderTop).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not artifically sleep if extend up call takes too long', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const host = createLitElement();
|
||||
host.scrollTop = 0;
|
||||
const showLoaderTop = vi.fn();
|
||||
const controller = createController({
|
||||
host,
|
||||
showLoaderTopCallback: showLoaderTop,
|
||||
});
|
||||
|
||||
controller.setOptions({ extendUp: true });
|
||||
controller.hostConnected();
|
||||
|
||||
const extendUpCallback = vi
|
||||
.fn()
|
||||
.mockImplementation((ev: CustomEvent<GalleryExtendEvent>) => {
|
||||
vi.advanceTimersByTime(10000);
|
||||
ev.detail.resolve();
|
||||
});
|
||||
|
||||
host.addEventListener(
|
||||
'advanced-camera-card:gallery:extend:up',
|
||||
extendUpCallback,
|
||||
);
|
||||
|
||||
host.dispatchEvent(new WheelEvent('wheel', { deltaY: -100 }));
|
||||
|
||||
await flushPromises();
|
||||
expect(sleep).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should extend gallery down', () => {
|
||||
it('should extend down when sentinel intersects', async () => {
|
||||
const host = createLitElement();
|
||||
const showSentinelBottom = vi.fn();
|
||||
const controller = createController({
|
||||
host,
|
||||
showSentinelBottomCallback: showSentinelBottom,
|
||||
});
|
||||
|
||||
controller.setOptions({ extendDown: true });
|
||||
controller.hostConnected();
|
||||
|
||||
const extendDownCallback = vi
|
||||
.fn()
|
||||
.mockImplementation((ev: CustomEvent<GalleryExtendEvent>) => {
|
||||
expect(showSentinelBottom).toHaveBeenLastCalledWith(false);
|
||||
|
||||
ev.detail.resolve();
|
||||
});
|
||||
host.addEventListener(
|
||||
'advanced-camera-card:gallery:extend:down',
|
||||
extendDownCallback,
|
||||
);
|
||||
|
||||
callIntersectionHandler(true);
|
||||
|
||||
await flushPromises();
|
||||
expect(showSentinelBottom).toHaveBeenLastCalledWith(false);
|
||||
});
|
||||
|
||||
it('should not extend down when sentinel does not intersect', async () => {
|
||||
const host = createLitElement();
|
||||
const showSentinelBottom = vi.fn();
|
||||
const controller = createController({
|
||||
host,
|
||||
showSentinelBottomCallback: showSentinelBottom,
|
||||
});
|
||||
|
||||
controller.setOptions({ extendUp: true });
|
||||
controller.hostConnected();
|
||||
|
||||
callIntersectionHandler(false);
|
||||
|
||||
expect(showSentinelBottom).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not extend when extending down is disabled', async () => {
|
||||
const host = createLitElement();
|
||||
const showSentinelBottom = vi.fn();
|
||||
const controller = createController({
|
||||
host,
|
||||
showSentinelBottomCallback: showSentinelBottom,
|
||||
});
|
||||
|
||||
controller.setOptions({ extendUp: false });
|
||||
controller.hostConnected();
|
||||
|
||||
callIntersectionHandler(true);
|
||||
|
||||
expect(showSentinelBottom).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('should handle content change', () => {
|
||||
describe('should scroll selected into view', () => {
|
||||
it('should scroll selected into view on first load', async () => {
|
||||
const selectedChild = document.createElement('div');
|
||||
selectedChild.setAttribute('selected', '');
|
||||
|
||||
const slot = createSlot();
|
||||
const host = createSlotHost({
|
||||
slot,
|
||||
children: [
|
||||
document.createElement('div'),
|
||||
document.createElement('div'),
|
||||
selectedChild,
|
||||
],
|
||||
});
|
||||
|
||||
const controller = createController({
|
||||
host,
|
||||
getSlot: () => slot,
|
||||
});
|
||||
controller.updateContents();
|
||||
|
||||
expect(scrollIntoView).toBeCalledWith(selectedChild, {
|
||||
boundary: host,
|
||||
block: 'center',
|
||||
});
|
||||
});
|
||||
|
||||
it('should do nothing on subsequent contents changes', async () => {
|
||||
const selectedChild = document.createElement('div');
|
||||
selectedChild.setAttribute('selected', '');
|
||||
|
||||
const slot = createSlot();
|
||||
const host = createSlotHost({
|
||||
slot,
|
||||
children: [
|
||||
document.createElement('div'),
|
||||
document.createElement('div'),
|
||||
selectedChild,
|
||||
],
|
||||
});
|
||||
|
||||
const controller = createController({
|
||||
host,
|
||||
getSlot: () => slot,
|
||||
});
|
||||
controller.updateContents();
|
||||
controller.updateContents();
|
||||
controller.updateContents();
|
||||
|
||||
expect(scrollIntoView).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should do nothing without a selected element', async () => {
|
||||
const slot = createSlot();
|
||||
const host = createSlotHost({
|
||||
slot,
|
||||
children: [
|
||||
document.createElement('div'),
|
||||
document.createElement('div'),
|
||||
document.createElement('div'),
|
||||
],
|
||||
});
|
||||
|
||||
const controller = createController({
|
||||
host,
|
||||
getSlot: () => slot,
|
||||
});
|
||||
controller.updateContents();
|
||||
|
||||
expect(scrollIntoView).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show bottom sentintel if contents change', async () => {
|
||||
const slot = createSlot();
|
||||
const host = createSlotHost({
|
||||
slot,
|
||||
});
|
||||
|
||||
const showSentinelBottom = vi.fn();
|
||||
const controller = createController({
|
||||
host,
|
||||
getSlot: () => slot,
|
||||
showSentinelBottomCallback: showSentinelBottom,
|
||||
});
|
||||
|
||||
controller.updateContents();
|
||||
|
||||
expect(showSentinelBottom).toBeCalledWith(true);
|
||||
});
|
||||
|
||||
it('should do nothing without slot', async () => {
|
||||
const slot = createSlot();
|
||||
const host = createSlotHost({
|
||||
slot,
|
||||
});
|
||||
|
||||
const showSentinelBottom = vi.fn();
|
||||
const controller = createController({
|
||||
host,
|
||||
showSentinelBottomCallback: showSentinelBottom,
|
||||
});
|
||||
|
||||
controller.updateContents();
|
||||
|
||||
expect(showSentinelBottom).not.toBeCalled();
|
||||
expect(scrollIntoView).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,427 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { ExtendedMediaQueryResult } from '../../../src/camera-manager/manager';
|
||||
import {
|
||||
EventQuery,
|
||||
QueryType,
|
||||
RecordingQuery,
|
||||
} from '../../../src/camera-manager/types';
|
||||
import { ViewManagerEpoch } from '../../../src/card-controller/view/types';
|
||||
import { ViewManager } from '../../../src/card-controller/view/view-manager';
|
||||
import {
|
||||
MEDIA_GALLERY_THUMBNAIL_DETAILS_WIDTH_MIN,
|
||||
MediaGalleryController,
|
||||
} from '../../../src/components-lib/gallery/media-gallery-controller';
|
||||
import { THUMBNAIL_WIDTH_DEFAULT } from '../../../src/config/schema/common/controls/thumbnails';
|
||||
import {
|
||||
EventMediaQuery,
|
||||
FolderViewQuery,
|
||||
RecordingMediaQuery,
|
||||
} from '../../../src/view/query';
|
||||
import { QueryResults } from '../../../src/view/query-results';
|
||||
import {
|
||||
createCameraManager,
|
||||
createLitElement,
|
||||
createView,
|
||||
TestViewMedia,
|
||||
} from '../../test-utils';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('MediaGalleryController', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('getMedia', () => {
|
||||
it('should return null initially', () => {
|
||||
expect(new MediaGalleryController(createLitElement()).getMedia()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setMediaFromView', () => {
|
||||
it('should set media from view if media is null', () => {
|
||||
const controller = new MediaGalleryController(createLitElement());
|
||||
controller.setMediaFromView(null);
|
||||
expect(controller.getMedia()).toBeNull();
|
||||
});
|
||||
|
||||
it('should set media from view if results are null', () => {
|
||||
const controller = new MediaGalleryController(createLitElement());
|
||||
controller.setMediaFromView(createView({ queryResults: new QueryResults() }));
|
||||
expect(controller.getMedia()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should update media when query results first set', () => {
|
||||
const media_1 = new TestViewMedia({ id: 'one' });
|
||||
const media_2 = new TestViewMedia({ id: 'two' });
|
||||
const newView = createView({
|
||||
queryResults: new QueryResults({ results: [media_1, media_2] }),
|
||||
});
|
||||
const controller = new MediaGalleryController(createLitElement());
|
||||
controller.setMediaFromView(newView);
|
||||
expect(controller.getMedia()).toEqual([media_2, media_1]);
|
||||
});
|
||||
|
||||
it('should update media when query results change', () => {
|
||||
const media_1 = new TestViewMedia({ id: 'one' });
|
||||
const media_2 = new TestViewMedia({ id: 'two' });
|
||||
const media_3 = new TestViewMedia({ id: 'three' });
|
||||
|
||||
const oldView = createView({
|
||||
queryResults: new QueryResults({ results: [media_1, media_2] }),
|
||||
});
|
||||
const newView = createView({
|
||||
queryResults: new QueryResults({ results: [media_2, media_3] }),
|
||||
});
|
||||
|
||||
const controller = new MediaGalleryController(createLitElement());
|
||||
controller.setMediaFromView(newView, oldView);
|
||||
expect(controller.getMedia()).toEqual([media_3, media_2]);
|
||||
});
|
||||
|
||||
it('should not update media if query results are the same', () => {
|
||||
const media_1 = new TestViewMedia({ id: 'one' });
|
||||
const media_2 = new TestViewMedia({ id: 'two' });
|
||||
const results = [media_1, media_2];
|
||||
const oldView = createView({
|
||||
queryResults: new QueryResults({ results }),
|
||||
});
|
||||
const newView = createView({
|
||||
queryResults: new QueryResults({ results }),
|
||||
});
|
||||
|
||||
const controller = new MediaGalleryController(createLitElement());
|
||||
controller.setMediaFromView(oldView);
|
||||
const media = controller.getMedia();
|
||||
|
||||
controller.setMediaFromView(newView, oldView);
|
||||
|
||||
expect(controller.getMedia()).toBe(media);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should set thumbnail size', () => {
|
||||
it('should set thumbnail size explicitly', () => {
|
||||
const host = document.createElement('div');
|
||||
const controller = new MediaGalleryController(host);
|
||||
|
||||
controller.setThumbnailSize(100);
|
||||
|
||||
expect(host.style.getPropertyValue('--advanced-camera-card-thumbnail-size')).toBe(
|
||||
'100px',
|
||||
);
|
||||
});
|
||||
|
||||
it('should set thumbnail size implicitly', () => {
|
||||
const host = document.createElement('div');
|
||||
const controller = new MediaGalleryController(host);
|
||||
|
||||
controller.setThumbnailSize();
|
||||
|
||||
expect(host.style.getPropertyValue('--advanced-camera-card-thumbnail-size')).toBe(
|
||||
`${THUMBNAIL_WIDTH_DEFAULT}px`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should get column width', () => {
|
||||
it('should get default column width', () => {
|
||||
const host = document.createElement('div');
|
||||
const controller = new MediaGalleryController(host);
|
||||
|
||||
expect(controller.getColumnWidth()).toBe(THUMBNAIL_WIDTH_DEFAULT);
|
||||
});
|
||||
|
||||
it('should get column width with defailts', () => {
|
||||
const host = document.createElement('div');
|
||||
const controller = new MediaGalleryController(host);
|
||||
|
||||
expect(
|
||||
controller.getColumnWidth({
|
||||
size: 100,
|
||||
show_details: true,
|
||||
show_favorite_control: true,
|
||||
show_timeline_control: true,
|
||||
show_download_control: true,
|
||||
}),
|
||||
).toBe(MEDIA_GALLERY_THUMBNAIL_DETAILS_WIDTH_MIN);
|
||||
});
|
||||
|
||||
it('should get column width with explicit size', () => {
|
||||
const host = document.createElement('div');
|
||||
const controller = new MediaGalleryController(host);
|
||||
|
||||
expect(
|
||||
controller.getColumnWidth({
|
||||
size: 142,
|
||||
show_details: false,
|
||||
show_favorite_control: true,
|
||||
show_timeline_control: true,
|
||||
show_download_control: true,
|
||||
}),
|
||||
).toBe(142);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should get column count round method', () => {
|
||||
it('should get default column count round method', () => {
|
||||
const host = document.createElement('div');
|
||||
const controller = new MediaGalleryController(host);
|
||||
|
||||
expect(controller.getColumnCountRoundMethod()).toBe('ceil');
|
||||
});
|
||||
|
||||
it('should get column count round method when details being shown', () => {
|
||||
const host = document.createElement('div');
|
||||
const controller = new MediaGalleryController(host);
|
||||
|
||||
expect(
|
||||
controller.getColumnCountRoundMethod({
|
||||
size: 100,
|
||||
show_details: true,
|
||||
show_favorite_control: true,
|
||||
show_timeline_control: true,
|
||||
show_download_control: true,
|
||||
}),
|
||||
).toBe('floor');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extendMediaGallery', () => {
|
||||
it('should do nothing if no view is available', async () => {
|
||||
const controller = new MediaGalleryController(createLitElement());
|
||||
const cameraManager = createCameraManager();
|
||||
const viewManagerEpoch = mock<ViewManagerEpoch>();
|
||||
viewManagerEpoch.manager.getView = vi.fn().mockReturnValue(null);
|
||||
|
||||
await controller.extendMediaGallery(cameraManager, viewManagerEpoch, 'earlier');
|
||||
expect(cameraManager.extendMediaQueries).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should do nothing if no queries are available', async () => {
|
||||
const controller = new MediaGalleryController(createLitElement());
|
||||
const cameraManager = createCameraManager();
|
||||
const viewManagerEpoch = mock<ViewManagerEpoch>();
|
||||
viewManagerEpoch.manager.getView = vi.fn().mockReturnValue(
|
||||
createView({
|
||||
query: new EventMediaQuery(),
|
||||
queryResults: new QueryResults({ results: [new TestViewMedia()] }),
|
||||
}),
|
||||
);
|
||||
|
||||
await controller.extendMediaGallery(cameraManager, viewManagerEpoch, 'earlier');
|
||||
expect(cameraManager.extendMediaQueries).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should do nothing if no query results are available', async () => {
|
||||
const controller = new MediaGalleryController(createLitElement());
|
||||
const cameraManager = createCameraManager();
|
||||
const viewManagerEpoch = mock<ViewManagerEpoch>();
|
||||
viewManagerEpoch.manager.getView = vi.fn().mockReturnValue(createView());
|
||||
|
||||
await controller.extendMediaGallery(cameraManager, viewManagerEpoch, 'earlier');
|
||||
expect(cameraManager.extendMediaQueries).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should do nothing if non-media query is present', async () => {
|
||||
const controller = new MediaGalleryController(createLitElement());
|
||||
const cameraManager = createCameraManager();
|
||||
const viewManagerEpoch = mock<ViewManagerEpoch>();
|
||||
viewManagerEpoch.manager.getView = vi
|
||||
.fn()
|
||||
.mockReturnValue(createView({ query: new FolderViewQuery() }));
|
||||
|
||||
await controller.extendMediaGallery(cameraManager, viewManagerEpoch, 'earlier');
|
||||
expect(cameraManager.extendMediaQueries).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should successfully extend event media queries', async () => {
|
||||
const controller = new MediaGalleryController(createLitElement());
|
||||
const cameraManager = createCameraManager();
|
||||
const viewManagerEpoch = mock<ViewManagerEpoch>({ manager: mock<ViewManager>() });
|
||||
|
||||
const existingRawQueries: EventQuery[] = [
|
||||
{ type: QueryType.Event, cameraIDs: new Set(['camera.office']) },
|
||||
];
|
||||
const existingMedia = [new TestViewMedia()];
|
||||
const baseView = createView({
|
||||
query: new EventMediaQuery(existingRawQueries),
|
||||
queryResults: new QueryResults({ results: existingMedia }),
|
||||
});
|
||||
viewManagerEpoch.manager.getView = vi.fn().mockReturnValue(baseView);
|
||||
|
||||
const newQueries: EventQuery[] = [
|
||||
{ type: QueryType.Event, cameraIDs: new Set(['camera.office']) },
|
||||
];
|
||||
const newResults = [new TestViewMedia()];
|
||||
|
||||
const extension: ExtendedMediaQueryResult<EventQuery> = {
|
||||
queries: newQueries,
|
||||
results: newResults,
|
||||
};
|
||||
vi.mocked(cameraManager.extendMediaQueries).mockResolvedValue(extension);
|
||||
|
||||
await controller.extendMediaGallery(cameraManager, viewManagerEpoch, 'earlier');
|
||||
expect(cameraManager.extendMediaQueries).toBeCalledWith(
|
||||
existingRawQueries,
|
||||
existingMedia,
|
||||
'earlier',
|
||||
{ useCache: true },
|
||||
);
|
||||
expect(viewManagerEpoch.manager.setViewByParameters).toBeCalledWith({
|
||||
baseView,
|
||||
params: {
|
||||
query: expect.any(EventMediaQuery),
|
||||
queryResults: expect.any(QueryResults),
|
||||
},
|
||||
});
|
||||
|
||||
const callArguments = vi.mocked(viewManagerEpoch.manager.setViewByParameters).mock
|
||||
.lastCall?.[0];
|
||||
|
||||
expect(callArguments?.params?.query?.getQuery()).toEqual(newQueries);
|
||||
expect(callArguments?.params?.queryResults?.getResults()).toEqual(newResults);
|
||||
});
|
||||
});
|
||||
|
||||
it('should successfully extend recording media queries', async () => {
|
||||
const controller = new MediaGalleryController(createLitElement());
|
||||
const cameraManager = createCameraManager();
|
||||
const viewManagerEpoch = mock<ViewManagerEpoch>({ manager: mock<ViewManager>() });
|
||||
|
||||
const existingRawQueries: RecordingQuery[] = [
|
||||
{ type: QueryType.Recording, cameraIDs: new Set(['camera.office']) },
|
||||
];
|
||||
const existingMedia = [new TestViewMedia()];
|
||||
const baseView = createView({
|
||||
query: new RecordingMediaQuery(existingRawQueries),
|
||||
queryResults: new QueryResults({ results: existingMedia }),
|
||||
});
|
||||
viewManagerEpoch.manager.getView = vi.fn().mockReturnValue(baseView);
|
||||
|
||||
const newQueries: RecordingQuery[] = [
|
||||
{ type: QueryType.Recording, cameraIDs: new Set(['camera.office']) },
|
||||
];
|
||||
const newResults = [new TestViewMedia()];
|
||||
|
||||
const extension: ExtendedMediaQueryResult<RecordingQuery> = {
|
||||
queries: newQueries,
|
||||
results: newResults,
|
||||
};
|
||||
vi.mocked(cameraManager.extendMediaQueries).mockResolvedValue(extension);
|
||||
|
||||
await controller.extendMediaGallery(cameraManager, viewManagerEpoch, 'earlier');
|
||||
expect(cameraManager.extendMediaQueries).toBeCalledWith(
|
||||
existingRawQueries,
|
||||
existingMedia,
|
||||
'earlier',
|
||||
{ useCache: true },
|
||||
);
|
||||
expect(viewManagerEpoch.manager.setViewByParameters).toBeCalledWith({
|
||||
baseView,
|
||||
params: {
|
||||
query: expect.any(RecordingMediaQuery),
|
||||
queryResults: expect.any(QueryResults),
|
||||
},
|
||||
});
|
||||
|
||||
const callArguments = vi.mocked(viewManagerEpoch.manager.setViewByParameters).mock
|
||||
.lastCall?.[0];
|
||||
|
||||
expect(callArguments?.params?.query?.getQuery()).toEqual(newQueries);
|
||||
expect(callArguments?.params?.queryResults?.getResults()).toEqual(newResults);
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
const controller = new MediaGalleryController(createLitElement());
|
||||
const cameraManager = createCameraManager();
|
||||
const viewManagerEpoch = mock<ViewManagerEpoch>({ manager: mock<ViewManager>() });
|
||||
|
||||
viewManagerEpoch.manager.getView = vi.fn().mockReturnValue(
|
||||
createView({
|
||||
query: new EventMediaQuery([
|
||||
{ type: QueryType.Event, cameraIDs: new Set(['camera.office']) },
|
||||
]),
|
||||
queryResults: new QueryResults({ results: [new TestViewMedia()] }),
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mocked(cameraManager.extendMediaQueries).mockRejectedValue(
|
||||
new Error('Test error'),
|
||||
);
|
||||
|
||||
const consoleSpy = vi.spyOn(global.console, 'warn').mockReturnValue(undefined);
|
||||
await controller.extendMediaGallery(cameraManager, viewManagerEpoch, 'earlier');
|
||||
|
||||
expect(viewManagerEpoch.manager.setViewByParameters).not.toHaveBeenCalled();
|
||||
|
||||
expect(consoleSpy).toBeCalledWith('Test error');
|
||||
});
|
||||
|
||||
it('should handle null extension gracefully', async () => {
|
||||
const controller = new MediaGalleryController(createLitElement());
|
||||
const cameraManager = createCameraManager();
|
||||
const viewManagerEpoch = mock<ViewManagerEpoch>({ manager: mock<ViewManager>() });
|
||||
|
||||
viewManagerEpoch.manager.getView = vi.fn().mockReturnValue(
|
||||
createView({
|
||||
query: new EventMediaQuery([
|
||||
{ type: QueryType.Event, cameraIDs: new Set(['camera.office']) },
|
||||
]),
|
||||
queryResults: new QueryResults({ results: [new TestViewMedia()] }),
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mocked(cameraManager.extendMediaQueries).mockResolvedValue(null);
|
||||
|
||||
await controller.extendMediaGallery(cameraManager, viewManagerEpoch, 'earlier');
|
||||
expect(viewManagerEpoch.manager.setViewByParameters).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('should handle item click', () => {
|
||||
it('should do nothing without a view', () => {
|
||||
const controller = new MediaGalleryController(createLitElement());
|
||||
const viewManager = mock<ViewManager>();
|
||||
|
||||
controller.itemClickHandler(viewManager, 0, new Event('click'));
|
||||
|
||||
expect(viewManager.setViewByParameters).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should change view to selected item', () => {
|
||||
const media = [
|
||||
new TestViewMedia({ id: 'zero' }),
|
||||
new TestViewMedia({ id: 'one' }),
|
||||
];
|
||||
const view = createView({
|
||||
queryResults: new QueryResults({ results: media, selectedIndex: 0 }),
|
||||
});
|
||||
|
||||
const controller = new MediaGalleryController(createLitElement());
|
||||
controller.setMediaFromView(view);
|
||||
|
||||
const viewManager = mock<ViewManager>();
|
||||
viewManager.getView.mockReturnValue(view);
|
||||
|
||||
controller.itemClickHandler(
|
||||
viewManager,
|
||||
// As the media in the gallery is reversed, passing in 0 as a
|
||||
// reversedIndex argument is requesting the selection of the media item
|
||||
// with index 1 (from an array of 2 media items).
|
||||
0,
|
||||
new Event('click'),
|
||||
);
|
||||
|
||||
expect(viewManager.setViewByParameters).toBeCalledWith({
|
||||
params: {
|
||||
view: 'media',
|
||||
queryResults: expect.any(QueryResults),
|
||||
},
|
||||
});
|
||||
|
||||
const newQueryResults = vi.mocked(viewManager.setViewByParameters).mock
|
||||
.lastCall?.[0]?.params?.queryResults;
|
||||
expect(newQueryResults?.getSelectedResult()).toEqual(media[1]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -12,11 +12,7 @@ import {
|
||||
MediaFilterCoreWhen,
|
||||
MediaFilterMediaType,
|
||||
} from '../../src/components-lib/media-filter-controller';
|
||||
import {
|
||||
EventMediaQueries,
|
||||
MediaQueries,
|
||||
RecordingMediaQueries,
|
||||
} from '../../src/view/media-queries';
|
||||
import { EventMediaQuery, Query, RecordingMediaQuery } from '../../src/view/query';
|
||||
import {
|
||||
createCameraConfig,
|
||||
createCameraManager,
|
||||
@@ -139,7 +135,7 @@ describe('MediaFilterController', () => {
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getCameraMetadata).mockReturnValue({
|
||||
title: 'Kitchen Camera',
|
||||
icon: 'mdi:camera',
|
||||
icon: { icon: 'mdi:camera' },
|
||||
});
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(createCameraStore());
|
||||
|
||||
@@ -157,7 +153,7 @@ describe('MediaFilterController', () => {
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getCameraMetadata).mockReturnValue({
|
||||
title: 'Kitchen Camera',
|
||||
icon: 'mdi:camera',
|
||||
icon: { icon: 'mdi:camera' },
|
||||
});
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||
createCameraStore({
|
||||
@@ -295,9 +291,7 @@ describe('MediaFilterController', () => {
|
||||
describe('should get correct controls to show', () => {
|
||||
it('view with events', () => {
|
||||
const viewManager = mock<ViewManager>();
|
||||
viewManager.getView.mockReturnValue(
|
||||
createView({ query: new EventMediaQueries() }),
|
||||
);
|
||||
viewManager.getView.mockReturnValue(createView({ query: new EventMediaQuery() }));
|
||||
const cameraManager = createCameraManager();
|
||||
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
@@ -311,7 +305,7 @@ describe('MediaFilterController', () => {
|
||||
it('view with recordings', () => {
|
||||
const viewManager = mock<ViewManager>();
|
||||
viewManager.getView.mockReturnValue(
|
||||
createView({ query: new RecordingMediaQueries() }),
|
||||
createView({ query: new RecordingMediaQuery() }),
|
||||
);
|
||||
const cameraManager = createCameraManager();
|
||||
|
||||
@@ -325,9 +319,7 @@ describe('MediaFilterController', () => {
|
||||
|
||||
it('can favorite events', () => {
|
||||
const viewManager = mock<ViewManager>();
|
||||
viewManager.getView.mockReturnValue(
|
||||
createView({ query: new EventMediaQueries() }),
|
||||
);
|
||||
viewManager.getView.mockReturnValue(createView({ query: new EventMediaQuery() }));
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue(
|
||||
createCapabilities({ 'favorite-events': true }),
|
||||
@@ -344,7 +336,7 @@ describe('MediaFilterController', () => {
|
||||
it('can favorite recordings', () => {
|
||||
const viewManager = mock<ViewManager>();
|
||||
viewManager.getView.mockReturnValue(
|
||||
createView({ query: new RecordingMediaQueries() }),
|
||||
createView({ query: new RecordingMediaQuery() }),
|
||||
);
|
||||
|
||||
const cameraManager = createCameraManager();
|
||||
@@ -436,7 +428,7 @@ describe('MediaFilterController', () => {
|
||||
}),
|
||||
});
|
||||
expect(
|
||||
viewManager.setViewByParametersWithExistingQuery.mock.calls[0][0]?.params?.query?.getQueries(),
|
||||
viewManager.setViewByParametersWithExistingQuery.mock.calls[0][0]?.params?.query?.getQuery(),
|
||||
).toEqual([
|
||||
{
|
||||
cameraIDs: new Set(['camera.kitchen']),
|
||||
@@ -501,7 +493,7 @@ describe('MediaFilterController', () => {
|
||||
});
|
||||
|
||||
expect(
|
||||
viewManager.setViewByParametersWithExistingQuery.mock.calls[0][0]?.params?.query?.getQueries(),
|
||||
viewManager.setViewByParametersWithExistingQuery.mock.calls[0][0]?.params?.query?.getQuery(),
|
||||
).toEqual([
|
||||
{
|
||||
cameraIDs: new Set(['camera.kitchen']),
|
||||
@@ -543,7 +535,7 @@ describe('MediaFilterController', () => {
|
||||
});
|
||||
|
||||
expect(
|
||||
viewManager.setViewByParametersWithExistingQuery.mock.calls[0][0]?.params?.query?.getQueries(),
|
||||
viewManager.setViewByParametersWithExistingQuery.mock.calls[0][0]?.params?.query?.getQuery(),
|
||||
).toEqual([
|
||||
{
|
||||
cameraIDs: new Set(['camera.kitchen']),
|
||||
@@ -607,7 +599,7 @@ describe('MediaFilterController', () => {
|
||||
},
|
||||
);
|
||||
expect(
|
||||
viewManager.setViewByParametersWithExistingQuery.mock.calls[0][0]?.params?.query?.getQueries(),
|
||||
viewManager.setViewByParametersWithExistingQuery.mock.calls[0][0]?.params?.query?.getQuery(),
|
||||
).toEqual([
|
||||
{
|
||||
cameraIDs: new Set(['camera.kitchen']),
|
||||
@@ -640,7 +632,7 @@ describe('MediaFilterController', () => {
|
||||
);
|
||||
|
||||
expect(
|
||||
viewManager.setViewByParametersWithExistingQuery.mock.calls[0][0]?.params?.query?.getQueries(),
|
||||
viewManager.setViewByParametersWithExistingQuery.mock.calls[0][0]?.params?.query?.getQuery(),
|
||||
).toEqual([
|
||||
{
|
||||
cameraIDs: new Set(['camera.kitchen']),
|
||||
@@ -652,14 +644,36 @@ describe('MediaFilterController', () => {
|
||||
});
|
||||
|
||||
describe('should calculate correct defaults', () => {
|
||||
it('with no queries', () => {
|
||||
it('with no query', () => {
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(createCameraStore());
|
||||
|
||||
const viewManager = mock<ViewManager>();
|
||||
viewManager.getView.mockReturnValue(createView());
|
||||
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
controller.setViewManager(viewManager);
|
||||
|
||||
controller.computeInitialDefaultsFromView(createCameraManager());
|
||||
controller.computeInitialDefaultsFromView(cameraManager);
|
||||
|
||||
expect(controller.getDefaults()).toBeNull();
|
||||
});
|
||||
|
||||
it('with no raw queries', () => {
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(createCameraStore());
|
||||
|
||||
const viewManager = mock<ViewManager>();
|
||||
viewManager.getView.mockReturnValue(
|
||||
createView({
|
||||
query: new EventMediaQuery(),
|
||||
}),
|
||||
);
|
||||
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
controller.setViewManager(viewManager);
|
||||
|
||||
controller.computeInitialDefaultsFromView(cameraManager);
|
||||
|
||||
expect(controller.getDefaults()).toBeNull();
|
||||
});
|
||||
@@ -680,7 +694,7 @@ describe('MediaFilterController', () => {
|
||||
it.each([
|
||||
[
|
||||
'same cameras' as const,
|
||||
new EventMediaQueries([
|
||||
new EventMediaQuery([
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set(['camera.kitchen', 'camera.living_room']),
|
||||
@@ -696,7 +710,7 @@ describe('MediaFilterController', () => {
|
||||
],
|
||||
[
|
||||
'different cameras' as const,
|
||||
new EventMediaQueries([
|
||||
new EventMediaQuery([
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set(['camera.kitchen']),
|
||||
@@ -710,7 +724,7 @@ describe('MediaFilterController', () => {
|
||||
],
|
||||
[
|
||||
'all cameras' as const,
|
||||
new EventMediaQueries([
|
||||
new EventMediaQuery([
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set(['camera.kitchen']),
|
||||
@@ -720,7 +734,7 @@ describe('MediaFilterController', () => {
|
||||
],
|
||||
[
|
||||
'different favorites ' as const,
|
||||
new EventMediaQueries([
|
||||
new EventMediaQuery([
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set(['camera.kitchen']),
|
||||
@@ -741,7 +755,7 @@ describe('MediaFilterController', () => {
|
||||
],
|
||||
[
|
||||
'all favorites' as const,
|
||||
new EventMediaQueries([
|
||||
new EventMediaQuery([
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set(['camera.kitchen']),
|
||||
@@ -759,7 +773,7 @@ describe('MediaFilterController', () => {
|
||||
],
|
||||
[
|
||||
'all not favorites' as const,
|
||||
new EventMediaQueries([
|
||||
new EventMediaQuery([
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set(['camera.kitchen']),
|
||||
@@ -777,7 +791,7 @@ describe('MediaFilterController', () => {
|
||||
],
|
||||
[
|
||||
'same hasClip' as const,
|
||||
new EventMediaQueries([
|
||||
new EventMediaQuery([
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set(['camera.kitchen']),
|
||||
@@ -795,7 +809,7 @@ describe('MediaFilterController', () => {
|
||||
],
|
||||
[
|
||||
'different hasClip' as const,
|
||||
new EventMediaQueries([
|
||||
new EventMediaQuery([
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set(['camera.kitchen']),
|
||||
@@ -811,7 +825,7 @@ describe('MediaFilterController', () => {
|
||||
],
|
||||
[
|
||||
'same hasSnapshot' as const,
|
||||
new EventMediaQueries([
|
||||
new EventMediaQuery([
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set(['camera.kitchen']),
|
||||
@@ -829,7 +843,7 @@ describe('MediaFilterController', () => {
|
||||
],
|
||||
[
|
||||
'different hasSnapshot' as const,
|
||||
new EventMediaQueries([
|
||||
new EventMediaQuery([
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set(['camera.kitchen']),
|
||||
@@ -845,7 +859,7 @@ describe('MediaFilterController', () => {
|
||||
],
|
||||
[
|
||||
'same what' as const,
|
||||
new EventMediaQueries([
|
||||
new EventMediaQuery([
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set(['camera.kitchen']),
|
||||
@@ -863,7 +877,7 @@ describe('MediaFilterController', () => {
|
||||
],
|
||||
[
|
||||
'different what' as const,
|
||||
new EventMediaQueries([
|
||||
new EventMediaQuery([
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set(['camera.kitchen']),
|
||||
@@ -879,7 +893,7 @@ describe('MediaFilterController', () => {
|
||||
],
|
||||
[
|
||||
'same where' as const,
|
||||
new EventMediaQueries([
|
||||
new EventMediaQuery([
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set(['camera.kitchen']),
|
||||
@@ -897,7 +911,7 @@ describe('MediaFilterController', () => {
|
||||
],
|
||||
[
|
||||
'different where' as const,
|
||||
new EventMediaQueries([
|
||||
new EventMediaQuery([
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set(['camera.kitchen']),
|
||||
@@ -913,7 +927,7 @@ describe('MediaFilterController', () => {
|
||||
],
|
||||
[
|
||||
'same tags' as const,
|
||||
new EventMediaQueries([
|
||||
new EventMediaQuery([
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set(['camera.kitchen']),
|
||||
@@ -931,7 +945,7 @@ describe('MediaFilterController', () => {
|
||||
],
|
||||
[
|
||||
'different tags' as const,
|
||||
new EventMediaQueries([
|
||||
new EventMediaQuery([
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set(['camera.kitchen']),
|
||||
@@ -947,7 +961,7 @@ describe('MediaFilterController', () => {
|
||||
],
|
||||
[
|
||||
'recordings' as const,
|
||||
new RecordingMediaQueries([
|
||||
new RecordingMediaQuery([
|
||||
{
|
||||
type: QueryType.Recording,
|
||||
cameraIDs: new Set(['camera.kitchen']),
|
||||
@@ -961,7 +975,7 @@ describe('MediaFilterController', () => {
|
||||
'%s',
|
||||
(
|
||||
_name: string,
|
||||
mediaQueries: MediaQueries,
|
||||
mediaQueries: Query,
|
||||
defaults: MediaFilterCoreDefaults | null,
|
||||
) => {
|
||||
const viewManager = mock<ViewManager>();
|
||||
|
||||
@@ -15,8 +15,9 @@ import {
|
||||
createSlotHost,
|
||||
} from '../test-utils';
|
||||
|
||||
vi.mock('lodash-es/throttle', () => ({
|
||||
default: vi.fn((fn) => fn),
|
||||
vi.mock('lodash-es', async () => ({
|
||||
...(await vi.importActual('lodash-es')),
|
||||
throttle: vi.fn((fn) => fn),
|
||||
}));
|
||||
|
||||
const masonry = mock<ExtendedMasonry>();
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { Capabilities } from '../../src/camera-manager/capabilities.js';
|
||||
import { CameraManager } from '../../src/camera-manager/manager.js';
|
||||
import { CameraManagerCameraMetadata } from '../../src/camera-manager/types.js';
|
||||
import { FoldersManager } from '../../src/card-controller/folders/manager.js';
|
||||
import { FullscreenManager } from '../../src/card-controller/fullscreen/fullscreen-manager.js';
|
||||
import { MediaPlayerManager } from '../../src/card-controller/media-player-manager.js';
|
||||
import { MicrophoneManager } from '../../src/card-controller/microphone-manager.js';
|
||||
@@ -17,16 +18,18 @@ import { ViewDisplayMode } from '../../src/config/schema/common/display.js';
|
||||
import { MenuItem } from '../../src/config/schema/elements/custom/menu/types.js';
|
||||
import { AdvancedCameraCardConfig } from '../../src/config/schema/types.js';
|
||||
import { HomeAssistant } from '../../src/ha/types.js';
|
||||
import { MediaPlayerController } from '../../src/types.js';
|
||||
import { MediaPlayerController, PTZMovementType } from '../../src/types.js';
|
||||
import { createGeneralAction, createViewAction } from '../../src/utils/action.js';
|
||||
import { MediaQueriesResults } from '../../src/view/media-queries-results.js';
|
||||
import { ViewMedia } from '../../src/view/media.js';
|
||||
import { ViewMedia, ViewMediaType } from '../../src/view/item.js';
|
||||
import { QueryResults } from '../../src/view/query-results.js';
|
||||
import { FolderViewQuery } from '../../src/view/query.js';
|
||||
import { View } from '../../src/view/view.js';
|
||||
import {
|
||||
createCameraConfig,
|
||||
createCameraManager,
|
||||
createCapabilities,
|
||||
createConfig,
|
||||
createFolder,
|
||||
createHASS,
|
||||
createMediaCapabilities,
|
||||
createMediaLoadedInfo,
|
||||
@@ -858,8 +861,12 @@ describe('MenuButtonController', () => {
|
||||
);
|
||||
const view = createView({
|
||||
view: 'media',
|
||||
queryResults: new MediaQueriesResults({
|
||||
results: [new ViewMedia('clip', 'camera-1')],
|
||||
queryResults: new QueryResults({
|
||||
results: [
|
||||
new ViewMedia(ViewMediaType.Clip, {
|
||||
cameraID: 'camera-1',
|
||||
}),
|
||||
],
|
||||
selectedIndex: 0,
|
||||
}),
|
||||
});
|
||||
@@ -893,8 +900,8 @@ describe('MenuButtonController', () => {
|
||||
createMediaCapabilities({ canDownload: true }),
|
||||
);
|
||||
const view = createView({
|
||||
queryResults: new MediaQueriesResults({
|
||||
results: [new ViewMedia('clip', 'camera-1')],
|
||||
queryResults: new QueryResults({
|
||||
results: [new ViewMedia(ViewMediaType.Clip, { cameraID: 'camera-1' })],
|
||||
selectedIndex: 0,
|
||||
}),
|
||||
});
|
||||
@@ -915,8 +922,8 @@ describe('MenuButtonController', () => {
|
||||
);
|
||||
const view = createView({
|
||||
view: 'live',
|
||||
queryResults: new MediaQueriesResults({
|
||||
results: [new ViewMedia('clip', 'camera-1')],
|
||||
queryResults: new QueryResults({
|
||||
results: [new ViewMedia(ViewMediaType.Clip, { cameraID: 'camera-1' })],
|
||||
selectedIndex: 0,
|
||||
}),
|
||||
});
|
||||
@@ -1499,7 +1506,7 @@ describe('MenuButtonController', () => {
|
||||
const store = createStore([
|
||||
{
|
||||
cameraID: 'camera-1',
|
||||
capabilities: new Capabilities({ ptz: { left: ['relative'] } }),
|
||||
capabilities: new Capabilities({ ptz: { left: [PTZMovementType.Relative] } }),
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -1519,7 +1526,7 @@ describe('MenuButtonController', () => {
|
||||
const store = createStore([
|
||||
{
|
||||
cameraID: 'camera-1',
|
||||
capabilities: new Capabilities({ ptz: { left: ['relative'] } }),
|
||||
capabilities: new Capabilities({ ptz: { left: [PTZMovementType.Relative] } }),
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -1539,7 +1546,7 @@ describe('MenuButtonController', () => {
|
||||
const store = createStore([
|
||||
{
|
||||
cameraID: 'camera-1',
|
||||
capabilities: new Capabilities({ ptz: { left: ['relative'] } }),
|
||||
capabilities: new Capabilities({ ptz: { left: [PTZMovementType.Relative] } }),
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -1573,7 +1580,7 @@ describe('MenuButtonController', () => {
|
||||
const store = createStore([
|
||||
{
|
||||
cameraID: 'camera-1',
|
||||
capabilities: new Capabilities({ ptz: { left: ['relative'] } }),
|
||||
capabilities: new Capabilities({ ptz: { left: [PTZMovementType.Relative] } }),
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -1605,7 +1612,7 @@ describe('MenuButtonController', () => {
|
||||
const store = createStore([
|
||||
{
|
||||
cameraID: 'camera-1',
|
||||
capabilities: new Capabilities({ ptz: { left: ['relative'] } }),
|
||||
capabilities: new Capabilities({ ptz: { left: [PTZMovementType.Relative] } }),
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -1643,7 +1650,7 @@ describe('MenuButtonController', () => {
|
||||
},
|
||||
{
|
||||
cameraID: 'camera-2',
|
||||
capabilities: new Capabilities({ ptz: { left: ['relative'] } }),
|
||||
capabilities: new Capabilities({ ptz: { left: [PTZMovementType.Relative] } }),
|
||||
},
|
||||
]);
|
||||
const view = createView({
|
||||
@@ -1701,7 +1708,7 @@ describe('MenuButtonController', () => {
|
||||
const view = createView({
|
||||
view: viewName,
|
||||
camera: 'camera-1',
|
||||
queryResults: new MediaQueriesResults({
|
||||
queryResults: new QueryResults({
|
||||
results: [new TestViewMedia({ id: 'media-1' })],
|
||||
selectedIndex: 0,
|
||||
}),
|
||||
@@ -1750,6 +1757,184 @@ describe('MenuButtonController', () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe('should have folders button', () => {
|
||||
it('should have no folders button without folders', () => {
|
||||
const foldersManager = mock<FoldersManager>();
|
||||
foldersManager.getFolders.mockReturnValue(new Map().entries());
|
||||
|
||||
const buttons = calculateButtons(controller, {
|
||||
foldersManager,
|
||||
});
|
||||
|
||||
expect(buttons).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
icon: 'mdi:folder',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should have a folders button for a single folder outside the folder view', () => {
|
||||
const foldersManager = mock<FoldersManager>();
|
||||
foldersManager.getFolders.mockReturnValue(
|
||||
new Map([['folder-0', createFolder({ id: 'folder-0' })]]).entries(),
|
||||
);
|
||||
|
||||
const buttons = calculateButtons(controller, {
|
||||
foldersManager,
|
||||
});
|
||||
|
||||
expect(buttons).toContainEqual({
|
||||
icon: 'mdi:folder',
|
||||
enabled: true,
|
||||
priority: 50,
|
||||
style: {},
|
||||
type: 'custom:advanced-camera-card-menu-icon',
|
||||
title: 'Folders',
|
||||
tap_action: { action: 'fire-dom-event', advanced_camera_card_action: 'folder' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should have a folders button for a single folder inside the folder view', () => {
|
||||
const folder = createFolder({ id: 'folder-0' });
|
||||
const foldersManager = mock<FoldersManager>();
|
||||
foldersManager.getFolders.mockReturnValue(
|
||||
new Map([['folder-0', folder]]).entries(),
|
||||
);
|
||||
|
||||
const buttons = calculateButtons(controller, {
|
||||
foldersManager,
|
||||
view: createView({
|
||||
view: 'folder',
|
||||
query: new FolderViewQuery({ folder, path: [{ id: 'one' }] }),
|
||||
}),
|
||||
});
|
||||
|
||||
expect(buttons).toContainEqual({
|
||||
icon: 'mdi:folder',
|
||||
enabled: true,
|
||||
priority: 50,
|
||||
style: {
|
||||
color: 'var(--advanced-camera-card-menu-button-active-color)',
|
||||
},
|
||||
type: 'custom:advanced-camera-card-menu-icon',
|
||||
title: 'Folders',
|
||||
tap_action: { action: 'fire-dom-event', advanced_camera_card_action: 'folder' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should have a folders submenu for multiple folder with a selected folder', () => {
|
||||
const foldersManager = mock<FoldersManager>();
|
||||
const selectedFolder = createFolder({ id: 'folder-selected' });
|
||||
const folders = new Map([
|
||||
['folder-0', createFolder({ id: 'folder-0' })],
|
||||
['folder-selected', selectedFolder],
|
||||
]);
|
||||
foldersManager.getFolders.mockReturnValue(folders.entries());
|
||||
|
||||
const view = createView({
|
||||
view: 'folder',
|
||||
query: new FolderViewQuery({
|
||||
folder: selectedFolder,
|
||||
path: [{ id: 'id' }],
|
||||
}),
|
||||
});
|
||||
|
||||
const buttons = calculateButtons(controller, {
|
||||
foldersManager,
|
||||
view,
|
||||
});
|
||||
|
||||
expect(buttons).toContainEqual({
|
||||
icon: 'mdi:folder-multiple',
|
||||
enabled: true,
|
||||
priority: 50,
|
||||
type: 'custom:advanced-camera-card-menu-submenu',
|
||||
title: 'Folders',
|
||||
style: {
|
||||
color: 'var(--advanced-camera-card-menu-button-active-color)',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
enabled: true,
|
||||
icon: 'mdi:folder',
|
||||
selected: false,
|
||||
style: {},
|
||||
tap_action: {
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'folder',
|
||||
folder: 'folder-0',
|
||||
},
|
||||
title: 'folder-0',
|
||||
},
|
||||
{
|
||||
enabled: true,
|
||||
icon: 'mdi:folder',
|
||||
selected: true,
|
||||
style: {
|
||||
color: 'var(--advanced-camera-card-menu-button-active-color)',
|
||||
},
|
||||
tap_action: {
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'folder',
|
||||
folder: 'folder-selected',
|
||||
},
|
||||
title: 'folder-selected',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should have a folders submenu for multiple folder without a selected folder', () => {
|
||||
const foldersManager = mock<FoldersManager>();
|
||||
const folders = new Map([
|
||||
['folder-0', createFolder({ id: 'folder-0' })],
|
||||
['folder-1', createFolder({ id: 'folder-1' })],
|
||||
]);
|
||||
foldersManager.getFolders.mockReturnValue(folders.entries());
|
||||
|
||||
const buttons = calculateButtons(controller, {
|
||||
foldersManager,
|
||||
});
|
||||
|
||||
expect(buttons).toContainEqual({
|
||||
icon: 'mdi:folder-multiple',
|
||||
enabled: true,
|
||||
priority: 50,
|
||||
type: 'custom:advanced-camera-card-menu-submenu',
|
||||
title: 'Folders',
|
||||
style: {},
|
||||
items: [
|
||||
{
|
||||
enabled: true,
|
||||
icon: 'mdi:folder',
|
||||
selected: false,
|
||||
style: {},
|
||||
tap_action: {
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'folder',
|
||||
folder: 'folder-0',
|
||||
},
|
||||
title: 'folder-0',
|
||||
},
|
||||
{
|
||||
enabled: true,
|
||||
icon: 'mdi:folder',
|
||||
selected: false,
|
||||
style: {},
|
||||
tap_action: {
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'folder',
|
||||
folder: 'folder-1',
|
||||
},
|
||||
title: 'folder-1',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('should handle dynamic buttons', () => {
|
||||
it('successfully', () => {
|
||||
const button: MenuItem = {
|
||||
|
||||
@@ -2,7 +2,8 @@ import yaml from 'js-yaml';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { MessageController } from '../../../src/components-lib/message/controller';
|
||||
import { TROUBLESHOOTING_URL } from '../../../src/const';
|
||||
import { Message, MessageType } from '../../../src/types';
|
||||
import { localize } from '../../../src/localize/localize';
|
||||
import { Message, MessageType, MessageURL } from '../../../src/types';
|
||||
|
||||
describe('MessageController', () => {
|
||||
describe('should return the correct message string', () => {
|
||||
@@ -64,7 +65,10 @@ describe('MessageController', () => {
|
||||
it('should show for errors', () => {
|
||||
const controller = new MessageController();
|
||||
const message: Message = { message: 'Error message', type: 'error' };
|
||||
expect(controller.shouldShowTroubleshootingURL(message)).toBe(true);
|
||||
expect(controller.getURL(message)).toEqual({
|
||||
link: TROUBLESHOOTING_URL,
|
||||
title: localize('error.troubleshooting'),
|
||||
});
|
||||
});
|
||||
|
||||
describe('should not show for other types', () => {
|
||||
@@ -77,7 +81,7 @@ describe('MessageController', () => {
|
||||
icon: 'mdi:car',
|
||||
type,
|
||||
};
|
||||
expect(controller.shouldShowTroubleshootingURL(message)).toBe(false);
|
||||
expect(controller.getURL(message)).toBeNull();
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -86,18 +90,21 @@ describe('MessageController', () => {
|
||||
it('by default', () => {
|
||||
const controller = new MessageController();
|
||||
const message: Message = { message: 'Error message', type: 'error' };
|
||||
expect(controller.getTroubleshootingURL(message)).toBe(TROUBLESHOOTING_URL);
|
||||
expect(controller.getURL(message)?.link).toBe(TROUBLESHOOTING_URL);
|
||||
});
|
||||
|
||||
it('when specified', () => {
|
||||
const controller = new MessageController();
|
||||
const troubleshootingURL = 'http://localhost/troubleshooting.md';
|
||||
const url: MessageURL = {
|
||||
link: 'link',
|
||||
title: 'title',
|
||||
};
|
||||
const message: Message = {
|
||||
message: 'Error message',
|
||||
type: 'error',
|
||||
troubleshootingURL: troubleshootingURL,
|
||||
url,
|
||||
};
|
||||
expect(controller.getTroubleshootingURL(message)).toBe(troubleshootingURL);
|
||||
expect(controller.getURL(message)).toBe(url);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { CameraManager } from '../../../../src/camera-manager/manager';
|
||||
import { ThumbnailDetailsController } from '../../../../src/components-lib/thumbnail/details-controller';
|
||||
import { ViewFolder, ViewMediaType } from '../../../../src/view/item';
|
||||
import { createFolder, TestViewMedia } from '../../../test-utils';
|
||||
|
||||
describe('ThumbnailDetailsController', () => {
|
||||
describe('should set heading', () => {
|
||||
it('should set heading on event with what, tags and score', () => {
|
||||
const item = new TestViewMedia({
|
||||
what: ['person', 'car'],
|
||||
tags: ['tag1', 'tag2'],
|
||||
score: 0.5,
|
||||
});
|
||||
|
||||
const controller = new ThumbnailDetailsController();
|
||||
controller.calculate(null, item);
|
||||
expect(controller.getHeading()).toBe('Person, Car: Tag1, Tag2 50.00%');
|
||||
});
|
||||
|
||||
it('should set heading on event with tags', () => {
|
||||
const item = new TestViewMedia({
|
||||
tags: ['tag1', 'tag2'],
|
||||
});
|
||||
|
||||
const controller = new ThumbnailDetailsController();
|
||||
controller.calculate(null, item);
|
||||
expect(controller.getHeading()).toBe('Tag1, Tag2');
|
||||
});
|
||||
|
||||
it('should set heading on event with what', () => {
|
||||
const item = new TestViewMedia({
|
||||
what: ['person', 'car'],
|
||||
});
|
||||
|
||||
const controller = new ThumbnailDetailsController();
|
||||
controller.calculate(null, item);
|
||||
expect(controller.getHeading()).toBe('Person, Car');
|
||||
});
|
||||
|
||||
it('should set null heading on event with no other information', () => {
|
||||
const item = new TestViewMedia({
|
||||
mediaType: ViewMediaType.Snapshot,
|
||||
what: null,
|
||||
tags: null,
|
||||
score: null,
|
||||
});
|
||||
|
||||
const controller = new ThumbnailDetailsController();
|
||||
controller.calculate(null, item);
|
||||
expect(controller.getHeading()).toBeNull();
|
||||
});
|
||||
|
||||
it('should set heading on recording with camera metadata', () => {
|
||||
const cameraManager = mock<CameraManager>();
|
||||
cameraManager.getCameraMetadata.mockReturnValue({
|
||||
title: 'Camera Title',
|
||||
icon: { icon: 'mdi:cow' },
|
||||
});
|
||||
|
||||
const item = new TestViewMedia({
|
||||
mediaType: ViewMediaType.Recording,
|
||||
});
|
||||
|
||||
const controller = new ThumbnailDetailsController();
|
||||
controller.calculate(cameraManager, item);
|
||||
expect(controller.getHeading()).toBe('Camera Title');
|
||||
});
|
||||
|
||||
it('should set heading on recording without camera metadata', () => {
|
||||
const item = new TestViewMedia({
|
||||
mediaType: ViewMediaType.Recording,
|
||||
});
|
||||
|
||||
const controller = new ThumbnailDetailsController();
|
||||
controller.calculate(null, item);
|
||||
expect(controller.getHeading()).toBeNull();
|
||||
});
|
||||
|
||||
it('should set no heading on folder', () => {
|
||||
const item = new ViewFolder(createFolder());
|
||||
|
||||
const controller = new ThumbnailDetailsController();
|
||||
controller.calculate(null, item);
|
||||
expect(controller.getHeading()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should set details', () => {
|
||||
describe('should have title in details', () => {
|
||||
it('should have icon with title when there are other details', () => {
|
||||
const item = new TestViewMedia({
|
||||
title: 'Test Event',
|
||||
where: ['where1', 'where2'],
|
||||
});
|
||||
|
||||
const controller = new ThumbnailDetailsController();
|
||||
controller.calculate(null, item);
|
||||
expect(controller.getDetails()).toContainEqual({
|
||||
title: 'Test Event',
|
||||
icon: { icon: 'mdi:rename' },
|
||||
hint: 'Title',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not have icon with title when there are no other details', () => {
|
||||
const item = new TestViewMedia({
|
||||
title: 'Test Event',
|
||||
});
|
||||
|
||||
const controller = new ThumbnailDetailsController();
|
||||
controller.calculate(null, item);
|
||||
expect(controller.getDetails()).toEqual([
|
||||
{
|
||||
title: 'Test Event',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have start time in details', () => {
|
||||
const item = new TestViewMedia({
|
||||
startTime: new Date('2025-05-18T17:03:00Z'),
|
||||
});
|
||||
|
||||
const controller = new ThumbnailDetailsController();
|
||||
controller.calculate(null, item);
|
||||
expect(controller.getDetails()).toContainEqual({
|
||||
title: '2025-05-18 17:03:00',
|
||||
hint: 'Start',
|
||||
icon: { icon: 'mdi:calendar-clock-outline' },
|
||||
});
|
||||
});
|
||||
|
||||
describe('should have duration in details', () => {
|
||||
it('should have duration in details', () => {
|
||||
const item = new TestViewMedia({
|
||||
startTime: new Date('2025-05-18T17:03:00Z'),
|
||||
endTime: new Date('2025-05-18T17:04:00Z'),
|
||||
});
|
||||
|
||||
const controller = new ThumbnailDetailsController();
|
||||
controller.calculate(null, item);
|
||||
expect(controller.getDetails()).toContainEqual({
|
||||
title: '1m 0s',
|
||||
hint: 'Duration',
|
||||
icon: { icon: 'mdi:clock-outline' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should have in-progress in details', () => {
|
||||
const item = new TestViewMedia({
|
||||
startTime: new Date('2025-05-18T17:03:00Z'),
|
||||
endTime: null,
|
||||
inProgress: true,
|
||||
});
|
||||
|
||||
const controller = new ThumbnailDetailsController();
|
||||
controller.calculate(null, item);
|
||||
expect(controller.getDetails()).toContainEqual({
|
||||
title: 'In Progress',
|
||||
hint: 'Duration',
|
||||
icon: { icon: 'mdi:clock-outline' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should have duration and in-progress in details', () => {
|
||||
const item = new TestViewMedia({
|
||||
startTime: new Date('2025-05-18T17:03:00Z'),
|
||||
endTime: new Date('2025-05-18T17:04:00Z'),
|
||||
inProgress: true,
|
||||
});
|
||||
|
||||
const controller = new ThumbnailDetailsController();
|
||||
controller.calculate(null, item);
|
||||
expect(controller.getDetails()).toContainEqual({
|
||||
title: '1m 0s In Progress',
|
||||
hint: 'Duration',
|
||||
icon: { icon: 'mdi:clock-outline' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should have camera title in details', () => {
|
||||
const cameraManager = mock<CameraManager>();
|
||||
cameraManager.getCameraMetadata.mockReturnValue({
|
||||
title: 'Camera Title',
|
||||
icon: { icon: 'mdi:cow' },
|
||||
});
|
||||
|
||||
const item = new TestViewMedia({
|
||||
cameraID: 'camera_1',
|
||||
});
|
||||
|
||||
const controller = new ThumbnailDetailsController();
|
||||
controller.calculate(cameraManager, item);
|
||||
expect(controller.getDetails()).toContainEqual({
|
||||
title: 'Camera Title',
|
||||
hint: 'Camera',
|
||||
icon: { icon: 'mdi:cctv' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should have where in details', () => {
|
||||
const item = new TestViewMedia({
|
||||
cameraID: 'camera_1',
|
||||
where: ['where1', 'where2'],
|
||||
});
|
||||
|
||||
const controller = new ThumbnailDetailsController();
|
||||
controller.calculate(null, item);
|
||||
expect(controller.getDetails()).toContainEqual({
|
||||
title: 'Where1, Where2',
|
||||
hint: 'Where',
|
||||
icon: { icon: 'mdi:map-marker-outline' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should have tags in details', () => {
|
||||
const item = new TestViewMedia({
|
||||
cameraID: 'camera_1',
|
||||
tags: ['tag1', 'tag2'],
|
||||
});
|
||||
|
||||
const controller = new ThumbnailDetailsController();
|
||||
controller.calculate(null, item);
|
||||
expect(controller.getDetails()).toContainEqual({
|
||||
title: 'Tag1, Tag2',
|
||||
hint: 'Tag',
|
||||
icon: { icon: 'mdi:tag' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should have seek in details', () => {
|
||||
const item = new TestViewMedia();
|
||||
|
||||
const controller = new ThumbnailDetailsController();
|
||||
controller.calculate(null, item, new Date('2025-05-20T07:14:57Z'));
|
||||
expect(controller.getDetails()).toContainEqual({
|
||||
title: '07:14:57',
|
||||
hint: 'Seek',
|
||||
icon: { icon: 'mdi:clock-fast' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,176 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { CameraManager } from '../../../../src/camera-manager/manager';
|
||||
import { ThumbnailFeatureController } from '../../../../src/components-lib/thumbnail/feature/controller';
|
||||
import { ViewFolder } from '../../../../src/view/item';
|
||||
import { createFolder, TestViewMedia } from '../../../test-utils';
|
||||
|
||||
describe('ThumbnailFeatureController', () => {
|
||||
const itemWithTime = new TestViewMedia({
|
||||
startTime: new Date('2025-05-18T17:03:00Z'),
|
||||
title: 'Test Event',
|
||||
cameraID: 'camera_1',
|
||||
});
|
||||
|
||||
describe('should set title', () => {
|
||||
it('should set title with start time ', () => {
|
||||
const controller = new ThumbnailFeatureController();
|
||||
|
||||
controller.calculate(null, itemWithTime, false);
|
||||
|
||||
expect(controller.getTitle()).toBe('17:03');
|
||||
});
|
||||
|
||||
it('should not set title when details are shown ', () => {
|
||||
const controller = new ThumbnailFeatureController();
|
||||
|
||||
controller.calculate(null, itemWithTime, true);
|
||||
|
||||
expect(controller.getTitle()).toBeNull();
|
||||
});
|
||||
|
||||
it('should not set title on media with a thumbnail', () => {
|
||||
const controller = new ThumbnailFeatureController();
|
||||
const itemWithThumbnail = new TestViewMedia({
|
||||
startTime: new Date('2025-05-18T17:03:00Z'),
|
||||
title: 'Test Event',
|
||||
cameraID: 'camera_1',
|
||||
thumbnail: 'thumbnail',
|
||||
});
|
||||
|
||||
controller.calculate(null, itemWithThumbnail, true);
|
||||
|
||||
expect(controller.getTitle()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should set subtitles', () => {
|
||||
it('should set subtitle with start date ', () => {
|
||||
const controller = new ThumbnailFeatureController();
|
||||
|
||||
controller.calculate(null, itemWithTime, false);
|
||||
|
||||
expect(controller.getSubtitles()).toContain('May 18th');
|
||||
});
|
||||
|
||||
it('should set subtitle with source from item title ', () => {
|
||||
const controller = new ThumbnailFeatureController();
|
||||
|
||||
controller.calculate(null, itemWithTime, false);
|
||||
|
||||
expect(controller.getSubtitles()).toContain('Test Event');
|
||||
});
|
||||
|
||||
it('should set subtitle with source from camera title ', () => {
|
||||
const controller = new ThumbnailFeatureController();
|
||||
|
||||
const cameraManager = mock<CameraManager>();
|
||||
cameraManager.getCameraMetadata.mockReturnValue({
|
||||
title: 'Camera 1',
|
||||
icon: { icon: 'mdi:camera' },
|
||||
});
|
||||
|
||||
const itemWithoutTitle = new TestViewMedia({
|
||||
startTime: new Date('2025-05-18T17:03:00Z'),
|
||||
title: null,
|
||||
cameraID: 'camera_1',
|
||||
});
|
||||
|
||||
controller.calculate(cameraManager, itemWithoutTitle, false);
|
||||
|
||||
expect(controller.getSubtitles()).toContain('Camera 1');
|
||||
});
|
||||
|
||||
it('should set subtitle falling back to item title when cameraID not found', () => {
|
||||
const controller = new ThumbnailFeatureController();
|
||||
|
||||
const item = new TestViewMedia({
|
||||
title: 'Title',
|
||||
cameraID: null,
|
||||
});
|
||||
|
||||
controller.calculate(null, item, false);
|
||||
|
||||
expect(controller.getSubtitles()).toContain('Title');
|
||||
});
|
||||
|
||||
it('should not set subtitle on media with a thumbnail', () => {
|
||||
const controller = new ThumbnailFeatureController();
|
||||
const itemWithThumbnail = new TestViewMedia({
|
||||
startTime: new Date('2025-05-18T17:03:00Z'),
|
||||
title: 'Test Event',
|
||||
cameraID: 'camera_1',
|
||||
thumbnail: 'thumbnail',
|
||||
});
|
||||
|
||||
controller.calculate(null, itemWithThumbnail, false);
|
||||
|
||||
expect(controller.getSubtitles()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not set subtitle on folder media', () => {
|
||||
const controller = new ThumbnailFeatureController();
|
||||
const itemWithThumbnail = new ViewFolder(createFolder(), {
|
||||
title: 'Test Folder',
|
||||
});
|
||||
|
||||
controller.calculate(null, itemWithThumbnail, false);
|
||||
|
||||
expect(controller.getSubtitles()).toContain('Test Folder');
|
||||
});
|
||||
});
|
||||
|
||||
describe('should set icon', () => {
|
||||
it('should set icon without a thumbnail', () => {
|
||||
const controller = new ThumbnailFeatureController();
|
||||
const itemWithThumbnail = new TestViewMedia({
|
||||
thumbnail: null,
|
||||
icon: 'mdi:cow',
|
||||
});
|
||||
|
||||
controller.calculate(null, itemWithThumbnail, false);
|
||||
|
||||
expect(controller.getIcon()).toBe('mdi:cow');
|
||||
});
|
||||
|
||||
it('should not set icon when there is a thumbnail', () => {
|
||||
const controller = new ThumbnailFeatureController();
|
||||
const itemWithThumbnail = new TestViewMedia({
|
||||
thumbnail: 'thumbnail',
|
||||
icon: 'mdi:cow',
|
||||
});
|
||||
|
||||
controller.calculate(null, itemWithThumbnail, false);
|
||||
|
||||
expect(controller.getIcon()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should set thumbnail', () => {
|
||||
it('should set brand thumbnail', () => {
|
||||
const controller = new ThumbnailFeatureController();
|
||||
const itemWithThumbnail = new TestViewMedia({
|
||||
thumbnail: 'https://brands.home-assistant.io//amcrest/icon.png',
|
||||
});
|
||||
|
||||
controller.calculate(null, itemWithThumbnail, false);
|
||||
|
||||
expect(controller.getThumbnail()).toBe(
|
||||
'https://brands.home-assistant.io/brands/_/amcrest/icon.png',
|
||||
);
|
||||
expect(controller.getThumbnailClass()).toBe('brand');
|
||||
});
|
||||
|
||||
it('should set other thumbnail', () => {
|
||||
const controller = new ThumbnailFeatureController();
|
||||
const itemWithThumbnail = new TestViewMedia({
|
||||
thumbnail: 'https://card.camera/thumbnail.jpg',
|
||||
});
|
||||
|
||||
controller.calculate(null, itemWithThumbnail, false);
|
||||
|
||||
expect(controller.getThumbnail()).toBe('https://card.camera/thumbnail.jpg');
|
||||
expect(controller.getThumbnailClass()).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,11 +2,17 @@ import Panzoom, { PanzoomEventDetail, PanzoomObject } from '@dermotduffy/panzoom
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mock, mockClear } from 'vitest-mock-extended';
|
||||
import { ZoomController } from '../../../src/components-lib/zoom/zoom-controller';
|
||||
import { ResizeObserverMock, requestAnimationFrameMock } from '../../test-utils';
|
||||
import {
|
||||
ResizeObserverMock,
|
||||
createTouch,
|
||||
createTouchEvent,
|
||||
requestAnimationFrameMock,
|
||||
} from '../../test-utils';
|
||||
|
||||
vi.mock('@dermotduffy/panzoom');
|
||||
vi.mock('lodash-es/throttle', () => ({
|
||||
default: vi.fn((fn) => fn),
|
||||
vi.mock('lodash-es', () => ({
|
||||
round: vi.fn((fn) => fn),
|
||||
throttle: vi.fn((fn) => fn),
|
||||
}));
|
||||
|
||||
// https://github.com/jsdom/jsdom/issues/2527
|
||||
@@ -42,23 +48,6 @@ describe('ZoomController', () => {
|
||||
return zoom;
|
||||
};
|
||||
|
||||
const createTouch = (target: HTMLElement): Touch => {
|
||||
return {
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
force: 0,
|
||||
identifier: 0,
|
||||
pageX: 0,
|
||||
pageY: 0,
|
||||
radiusX: 0,
|
||||
radiusY: 0,
|
||||
rotationAngle: 0,
|
||||
screenX: 0,
|
||||
screenY: 0,
|
||||
target: target,
|
||||
};
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
vi.stubGlobal('ResizeObserver', ResizeObserverMock);
|
||||
window.requestAnimationFrame = requestAnimationFrameMock;
|
||||
@@ -144,24 +133,23 @@ describe('ZoomController', () => {
|
||||
|
||||
createAndRegisterZoom(element);
|
||||
|
||||
const ev_1 = new TouchEvent('touchstart', {
|
||||
bubbles: false,
|
||||
touches: [createTouch(element), createTouch(element)],
|
||||
const ev_1 = createTouchEvent('touchstart', {
|
||||
touches: [createTouch({ target: element }), createTouch({ target: element })],
|
||||
});
|
||||
element.dispatchEvent(ev_1);
|
||||
expect(panzoom.handleDown).toBeCalledWith(ev_1);
|
||||
|
||||
panzoom.getScale = vi.fn().mockReturnValue(1.2);
|
||||
|
||||
const ev_3 = new TouchEvent('touchstart');
|
||||
const ev_3 = createTouchEvent('touchstart');
|
||||
element.dispatchEvent(ev_3);
|
||||
expect(panzoom.handleDown).toBeCalledWith(ev_3);
|
||||
|
||||
const ev_4 = new TouchEvent('touchmove');
|
||||
const ev_4 = createTouchEvent('touchmove');
|
||||
element.dispatchEvent(ev_4);
|
||||
expect(panzoom.handleMove).toBeCalledWith(ev_4);
|
||||
|
||||
const ev_5 = new TouchEvent('touchend');
|
||||
const ev_5 = createTouchEvent('touchend');
|
||||
element.dispatchEvent(ev_5);
|
||||
expect(panzoom.handleUp).toBeCalledWith(ev_5);
|
||||
});
|
||||
|
||||
@@ -57,33 +57,68 @@ describe('generateViewContextForZoom', () => {
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
it('handleZoomSettingsObservedEvent', () => {
|
||||
const viewManager = mock<ViewManager>();
|
||||
describe('handleZoomSettingsObservedEvent', () => {
|
||||
it('should reject observed zoom settings without viewManager ', () => {
|
||||
handleZoomSettingsObservedEvent(
|
||||
new CustomEvent('advanced-camera-card:zoom:change', {
|
||||
detail: {
|
||||
pan: { x: 1, y: 2 },
|
||||
zoom: 3,
|
||||
isDefault: true,
|
||||
unzoomed: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
handleZoomSettingsObservedEvent(
|
||||
new CustomEvent('advanced-camera-card:zoom:change', {
|
||||
detail: {
|
||||
pan: { x: 1, y: 2 },
|
||||
zoom: 3,
|
||||
isDefault: true,
|
||||
unzoomed: true,
|
||||
},
|
||||
}),
|
||||
viewManager,
|
||||
'target',
|
||||
);
|
||||
expect(viewManager.setViewByParameters).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
modifiers: [expect.any(MergeContextViewModifier)],
|
||||
}),
|
||||
);
|
||||
// No observable effect.
|
||||
});
|
||||
|
||||
expect(MergeContextViewModifier).toBeCalledWith({
|
||||
zoom: {
|
||||
target: {
|
||||
observed: { pan: { x: 1, y: 2 }, zoom: 3, isDefault: true, unzoomed: true },
|
||||
requested: null,
|
||||
it('should reject observed zoom settings without target ', () => {
|
||||
const viewManager = mock<ViewManager>();
|
||||
|
||||
handleZoomSettingsObservedEvent(
|
||||
new CustomEvent('advanced-camera-card:zoom:change', {
|
||||
detail: {
|
||||
pan: { x: 1, y: 2 },
|
||||
zoom: 3,
|
||||
isDefault: true,
|
||||
unzoomed: true,
|
||||
},
|
||||
}),
|
||||
viewManager,
|
||||
);
|
||||
|
||||
expect(viewManager.setViewByParameters).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should handle observed zoom settings ', () => {
|
||||
const viewManager = mock<ViewManager>();
|
||||
|
||||
handleZoomSettingsObservedEvent(
|
||||
new CustomEvent('advanced-camera-card:zoom:change', {
|
||||
detail: {
|
||||
pan: { x: 1, y: 2 },
|
||||
zoom: 3,
|
||||
isDefault: true,
|
||||
unzoomed: true,
|
||||
},
|
||||
}),
|
||||
viewManager,
|
||||
'target',
|
||||
);
|
||||
expect(viewManager.setViewByParameters).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
modifiers: [expect.any(MergeContextViewModifier)],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(MergeContextViewModifier).toBeCalledWith({
|
||||
zoom: {
|
||||
target: {
|
||||
observed: { pan: { x: 1, y: 2 }, zoom: 3, isDefault: true, unzoomed: true },
|
||||
requested: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user