Treat camera unavailability in a more friendly way.
Also creates a `components-lib` directory for major webcomponent support code.
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { createCameraConfig, createHASS, createStateEntity } from '../test-utils';
|
||||
import { getStateObjOrDispatchError } from '../../src/utils/get-state-obj';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('getStateObjOrDispatchError', () => {
|
||||
it('should retrieve valid state object', () => {
|
||||
const messageHandler = vi.fn();
|
||||
const element = document.createElement('div');
|
||||
element.addEventListener('frigate-card:message', messageHandler);
|
||||
const state = createStateEntity();
|
||||
|
||||
expect(
|
||||
getStateObjOrDispatchError(
|
||||
element,
|
||||
createHASS({
|
||||
'camera.test': state,
|
||||
}),
|
||||
createCameraConfig({
|
||||
camera_entity: 'camera.test',
|
||||
}),
|
||||
),
|
||||
).toBe(state);
|
||||
|
||||
expect(messageHandler).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should dispatch unspecified entity', () => {
|
||||
const messageHandler = vi.fn();
|
||||
const element = document.createElement('div');
|
||||
element.addEventListener('frigate-card:message', messageHandler);
|
||||
|
||||
expect(
|
||||
getStateObjOrDispatchError(element, createHASS(), createCameraConfig()),
|
||||
).toBeNull();
|
||||
|
||||
expect(messageHandler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: expect.objectContaining({
|
||||
message:
|
||||
'The camera_entity parameter must be set and valid for this live provider',
|
||||
type: 'error',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should dispatch not found state', () => {
|
||||
const messageHandler = vi.fn();
|
||||
const element = document.createElement('div');
|
||||
element.addEventListener('frigate-card:message', messageHandler);
|
||||
|
||||
expect(
|
||||
getStateObjOrDispatchError(
|
||||
element,
|
||||
createHASS(),
|
||||
createCameraConfig({
|
||||
camera_entity: 'camera.will-not-be-found',
|
||||
}),
|
||||
),
|
||||
).toBeNull();
|
||||
|
||||
expect(messageHandler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: expect.objectContaining({
|
||||
message: 'The configured camera_entity was not found',
|
||||
type: 'error',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,526 +0,0 @@
|
||||
import Masonry from 'masonry-layout';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { MediaLoadedInfo } from '../../src/types';
|
||||
import {
|
||||
MediaGridConstructorOptions,
|
||||
MediaGridController,
|
||||
} from '../../src/utils/media-grid-controller';
|
||||
import { dispatchExistingMediaLoadedInfoAsEvent } from '../../src/utils/media-info';
|
||||
import {
|
||||
MutationObserverMock,
|
||||
ResizeObserverMock,
|
||||
createSlot,
|
||||
createSlotHost,
|
||||
} from '../test-utils';
|
||||
|
||||
vi.mock('lodash-es/throttle', () => ({
|
||||
default: vi.fn((fn) => fn),
|
||||
}));
|
||||
|
||||
const masonry = mock<Masonry>();
|
||||
vi.mock('masonry-layout', () => ({
|
||||
default: vi.fn().mockImplementation(() => {
|
||||
return masonry;
|
||||
}),
|
||||
}));
|
||||
|
||||
const createChildren = (childIDs?: string[], idAttribute?: string): HTMLElement[] => {
|
||||
const children: HTMLElement[] = [];
|
||||
for (let i = 0; i < (childIDs?.length ?? 3); ++i) {
|
||||
const child = document.createElement('div');
|
||||
if (childIDs) {
|
||||
child.setAttribute(idAttribute ?? 'grid-id', childIDs[i]);
|
||||
}
|
||||
children.push(child);
|
||||
}
|
||||
return children;
|
||||
};
|
||||
|
||||
const setElementWidth = (element: HTMLElement, width: number): void => {
|
||||
element.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||
width: width,
|
||||
});
|
||||
};
|
||||
|
||||
const createParent = (options?: {
|
||||
children?: HTMLElement[];
|
||||
width?: number;
|
||||
}): HTMLElement => {
|
||||
const host = document.createElement('div');
|
||||
if (options?.children) {
|
||||
host.append(...options.children);
|
||||
}
|
||||
// Default Lovelace card width is 492.
|
||||
setElementWidth(host, options?.width ?? 492);
|
||||
return host;
|
||||
};
|
||||
|
||||
const createController = (host: HTMLElement, options?: MediaGridConstructorOptions) => {
|
||||
return new MediaGridController(host, options);
|
||||
};
|
||||
|
||||
const triggerMutationObserver = (hostOrCell: 'cell' | 'host'): void => {
|
||||
const mutationObserverTrigger = vi.mocked(global.MutationObserver).mock.calls[
|
||||
hostOrCell === 'host' ? 0 : 1
|
||||
][0];
|
||||
mutationObserverTrigger([], mock<MutationObserver>());
|
||||
};
|
||||
|
||||
const triggerResizeObserver = (hostOrCell: 'cell' | 'host'): void => {
|
||||
const resizeObserverTrigger = vi.mocked(global.ResizeObserver).mock.calls[
|
||||
hostOrCell === 'host' ? 0 : 1
|
||||
][0];
|
||||
resizeObserverTrigger([], mock<ResizeObserver>());
|
||||
};
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('MediaGridController', () => {
|
||||
const mediaLoadedInfo: MediaLoadedInfo = {
|
||||
width: 10,
|
||||
height: 20,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.stubGlobal('MutationObserver', MutationObserverMock);
|
||||
vi.stubGlobal('ResizeObserver', ResizeObserverMock);
|
||||
});
|
||||
|
||||
it('should be constructable', () => {
|
||||
const controller = createController(createParent());
|
||||
expect(controller).toBeTruthy();
|
||||
expect(masonry.layout).toBeCalled();
|
||||
});
|
||||
|
||||
it('should set grid contents correctly from regular elements', () => {
|
||||
const children = createChildren();
|
||||
const parent = createParent({ children: children });
|
||||
const controller = createController(parent);
|
||||
expect(controller.getGridContents()).toEqual(
|
||||
new Map([
|
||||
['0', children[0]],
|
||||
['1', children[1]],
|
||||
['2', children[2]],
|
||||
]),
|
||||
);
|
||||
expect(controller.getGridSize()).toBe(3);
|
||||
expect(masonry.layout).toBeCalled();
|
||||
});
|
||||
|
||||
it('should set grid contents correctly from slotted elements', () => {
|
||||
const children = createChildren();
|
||||
const slot = createSlot();
|
||||
const host = createSlotHost({ slot: slot, children: children });
|
||||
const controller = createController(host);
|
||||
expect(controller.getGridContents()).toEqual(
|
||||
new Map([
|
||||
['0', children[0]],
|
||||
['1', children[1]],
|
||||
['2', children[2]],
|
||||
]),
|
||||
);
|
||||
expect(controller.getGridSize()).toBe(3);
|
||||
});
|
||||
|
||||
it('should select element', () => {
|
||||
const children = createChildren();
|
||||
const slot = createSlot();
|
||||
createSlotHost({ slot: slot, children: children });
|
||||
|
||||
const controller = createController(slot);
|
||||
|
||||
// All children should be unselected.
|
||||
expect(controller.getSelected()).toBeNull();
|
||||
for (const child of children) {
|
||||
expect(child.getAttribute('selected')).toBeNull();
|
||||
expect(child.getAttribute('unselected')).toEqual('');
|
||||
}
|
||||
|
||||
controller.selectCell('0');
|
||||
expect(controller.getSelected()).toBe('0');
|
||||
|
||||
// 1st child should now be selected.
|
||||
expect(children[0].getAttribute('selected')).toEqual('');
|
||||
expect(children[0].getAttribute('unselected')).toBeNull();
|
||||
|
||||
// 2nd and 3rd should be unselected.
|
||||
for (const child of children.slice(1)) {
|
||||
expect(child.getAttribute('selected')).toBeNull();
|
||||
expect(child.getAttribute('unselected')).toEqual('');
|
||||
}
|
||||
});
|
||||
|
||||
it('should re-select element', () => {
|
||||
const children = createChildren();
|
||||
const slot = createSlot();
|
||||
createSlotHost({ slot: slot, children: children });
|
||||
const controller = createController(slot);
|
||||
|
||||
// All children should be unselected.
|
||||
expect(controller.getSelected()).toBeNull();
|
||||
|
||||
controller.selectCell('0');
|
||||
expect(controller.getSelected()).toBe('0');
|
||||
|
||||
controller.selectCell('0');
|
||||
expect(controller.getSelected()).toBe('0');
|
||||
});
|
||||
|
||||
it('should dispatch media loaded info on selection', () => {
|
||||
const children = createChildren();
|
||||
const slot = createSlot();
|
||||
const host = createSlotHost({ slot: slot, children: children });
|
||||
const controller = createController(slot);
|
||||
|
||||
const mediaLoadedInfoHandler = vi.fn();
|
||||
host.addEventListener('frigate-card:media:loaded', mediaLoadedInfoHandler);
|
||||
dispatchExistingMediaLoadedInfoAsEvent(children[0], mediaLoadedInfo);
|
||||
|
||||
controller.selectCell('0');
|
||||
expect(mediaLoadedInfoHandler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: mediaLoadedInfo,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should dispatch media loaded info when cell is selected', () => {
|
||||
const children = createChildren();
|
||||
const slot = createSlot();
|
||||
const host = createSlotHost({ slot: slot, children: children });
|
||||
const controller = createController(slot);
|
||||
|
||||
controller.selectCell('0');
|
||||
|
||||
const mediaLoadedInfoHandler = vi.fn();
|
||||
host.addEventListener('frigate-card:media:loaded', mediaLoadedInfoHandler);
|
||||
dispatchExistingMediaLoadedInfoAsEvent(children[0], mediaLoadedInfo);
|
||||
|
||||
expect(mediaLoadedInfoHandler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: mediaLoadedInfo,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not dispatch media loaded info when cell is not selected', () => {
|
||||
const children = createChildren();
|
||||
const slot = createSlot();
|
||||
const host = createSlotHost({ slot: slot, children: children });
|
||||
const controller = createController(host);
|
||||
|
||||
controller.selectCell('1');
|
||||
|
||||
const mediaLoadedInfoHandler = vi.fn();
|
||||
host.addEventListener('frigate-card:media:loaded', mediaLoadedInfoHandler);
|
||||
dispatchExistingMediaLoadedInfoAsEvent(children[0], mediaLoadedInfo);
|
||||
|
||||
// Another element is selected, so the event should not have propagated.
|
||||
expect(mediaLoadedInfoHandler).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should unselect', () => {
|
||||
const children = createChildren();
|
||||
const slot = createSlot();
|
||||
const host = createSlotHost({ slot: slot, children: children });
|
||||
const controller = createController(host);
|
||||
|
||||
const unselectedHandler = vi.fn();
|
||||
const unloadMediaHandler = vi.fn();
|
||||
host.addEventListener('frigate-card:media-grid:unselected', unselectedHandler);
|
||||
host.addEventListener('frigate-card:media:unloaded', unloadMediaHandler);
|
||||
|
||||
controller.selectCell('0');
|
||||
expect(controller.getSelected()).toBe('0');
|
||||
|
||||
// Unselect all elements.
|
||||
controller.unselectAll();
|
||||
|
||||
// Expect selected to now be null.
|
||||
expect(controller.getSelected()).toBeNull();
|
||||
|
||||
// Expect styles to have been updated.
|
||||
for (const child of children) {
|
||||
expect(child.getAttribute('selected')).toBeNull();
|
||||
expect(child.getAttribute('unselected')).toEqual('');
|
||||
}
|
||||
|
||||
// Expect handlers to have been called.
|
||||
expect(unselectedHandler).toBeCalledTimes(1);
|
||||
expect(unloadMediaHandler).toBeCalledTimes(1);
|
||||
|
||||
// Unselecting a second time should do nothing.
|
||||
controller.unselectAll();
|
||||
|
||||
expect(unselectedHandler).toBeCalledTimes(1);
|
||||
expect(unloadMediaHandler).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should select in constructor', () => {
|
||||
const children = createChildren();
|
||||
const slot = createSlot();
|
||||
const host = createSlotHost({ slot: slot, children: children });
|
||||
const controller = createController(host, { selected: '2' });
|
||||
|
||||
expect(controller.getSelected()).toBe('2');
|
||||
});
|
||||
|
||||
it('should respect grid attribute option', () => {
|
||||
const children = createChildren(['one', 'two', 'three'], 'test-id');
|
||||
const slot = createSlot();
|
||||
const host = createSlotHost({ slot: slot, children: children });
|
||||
const controller = createController(host, { idAttribute: 'test-id' });
|
||||
expect(controller.getGridContents()).toEqual(
|
||||
new Map([
|
||||
['one', children[0]],
|
||||
['two', children[1]],
|
||||
['three', children[2]],
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should destroy with regular elements', () => {
|
||||
const children = createChildren();
|
||||
const parent = createParent({ children: children });
|
||||
const controller = createController(parent);
|
||||
expect(controller.getGridSize()).toBe(3);
|
||||
controller.destroy();
|
||||
expect(controller.getGridSize()).toBe(0);
|
||||
});
|
||||
|
||||
it('should destroy with slotted elements', () => {
|
||||
const children = createChildren();
|
||||
const slot = createSlot();
|
||||
createSlotHost({ slot: slot, children: children });
|
||||
const controller = createController(slot);
|
||||
|
||||
expect(controller.getGridSize()).toBe(3);
|
||||
controller.destroy();
|
||||
expect(controller.getGridSize()).toBe(0);
|
||||
});
|
||||
|
||||
it('should replace children when they change', () => {
|
||||
const children = createChildren();
|
||||
const parent = createParent({ children: children });
|
||||
const controller = createController(parent, { selected: '1' });
|
||||
dispatchExistingMediaLoadedInfoAsEvent(children[0], mediaLoadedInfo);
|
||||
|
||||
expect(controller.getSelected()).toBe('1');
|
||||
expect(controller.getGridSize()).toBe(3);
|
||||
|
||||
children.forEach((child) => parent.removeChild(child));
|
||||
const newChildren = createChildren(['one', 'two', 'three']);
|
||||
newChildren.forEach((child) => parent.appendChild(child));
|
||||
|
||||
triggerMutationObserver('host');
|
||||
|
||||
expect(controller.getGridContents()).toEqual(
|
||||
new Map([
|
||||
['one', newChildren[0]],
|
||||
['two', newChildren[1]],
|
||||
['three', newChildren[2]],
|
||||
]),
|
||||
);
|
||||
expect(controller.getSelected()).toBeNull();
|
||||
});
|
||||
|
||||
it('should re-calculate children when id attribute changes', () => {
|
||||
const children = createChildren(['one', 'two', 'three'], 'test-id');
|
||||
const parent = createParent({ children: children });
|
||||
const controller = createController(parent, {
|
||||
selected: 'one',
|
||||
idAttribute: 'test-id',
|
||||
});
|
||||
|
||||
expect(controller.getSelected()).toBe('one');
|
||||
expect(controller.getGridSize()).toBe(3);
|
||||
|
||||
children[0].setAttribute('test-id', 'alpha');
|
||||
children[1].setAttribute('test-id', 'beta');
|
||||
children[2].setAttribute('test-id', 'gamma');
|
||||
|
||||
triggerMutationObserver('cell');
|
||||
|
||||
expect(controller.getGridContents()).toEqual(
|
||||
new Map([
|
||||
['alpha', children[0]],
|
||||
['beta', children[1]],
|
||||
['gamma', children[2]],
|
||||
]),
|
||||
);
|
||||
expect(controller.getSelected()).toBeNull();
|
||||
});
|
||||
|
||||
it('should replace children of a slot when they change', () => {
|
||||
const children = createChildren();
|
||||
const slot = createSlot();
|
||||
const host = createSlotHost({ slot: slot, children: children });
|
||||
|
||||
const controller = createController(slot, { selected: '1' });
|
||||
|
||||
expect(controller.getSelected()).toBe('1');
|
||||
expect(controller.getGridSize()).toBe(3);
|
||||
|
||||
children.forEach((child) => host.removeChild(child));
|
||||
const newChildren = createChildren(['one', 'two', 'three']);
|
||||
newChildren.forEach((child) => host.append(child));
|
||||
|
||||
slot.dispatchEvent(new Event('slotchange'));
|
||||
|
||||
expect(controller.getGridContents()).toEqual(
|
||||
new Map([
|
||||
['one', newChildren[0]],
|
||||
['two', newChildren[1]],
|
||||
['three', newChildren[2]],
|
||||
]),
|
||||
);
|
||||
expect(controller.getSelected()).toBeNull();
|
||||
});
|
||||
|
||||
it('should construct masonry correctly', () => {
|
||||
const children = createChildren();
|
||||
const parent = createParent({ children: children });
|
||||
createController(parent);
|
||||
expect(Masonry).toBeCalledWith(
|
||||
parent,
|
||||
expect.objectContaining({
|
||||
initLayout: false,
|
||||
percentPosition: true,
|
||||
transitionDuration: '0.2s',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should set default column size correctly', () => {
|
||||
const parent = createParent({ children: createChildren() });
|
||||
createController(parent);
|
||||
expect(Masonry).toBeCalledWith(
|
||||
parent,
|
||||
expect.objectContaining({
|
||||
columnWidth: 246,
|
||||
}),
|
||||
);
|
||||
expect(parent.style.getPropertyValue('--frigate-card-grid-column-size')).toBe(
|
||||
'246px',
|
||||
);
|
||||
});
|
||||
|
||||
it('should respect exact columns', () => {
|
||||
const parent = createParent({ children: createChildren(), width: 2000 });
|
||||
const controller = createController(parent);
|
||||
controller.setDisplayConfig({ mode: 'grid', grid_columns: 2 });
|
||||
|
||||
// Will have been called once on construction, and then again when the
|
||||
// number of columns changes.
|
||||
expect(Masonry).toBeCalledTimes(2);
|
||||
expect(Masonry).toBeCalledWith(
|
||||
parent,
|
||||
expect.objectContaining({
|
||||
columnWidth: 1000,
|
||||
}),
|
||||
);
|
||||
expect(parent.style.getPropertyValue('--frigate-card-grid-column-size')).toBe(
|
||||
'1000px',
|
||||
);
|
||||
});
|
||||
|
||||
it('should respect selected width factor', () => {
|
||||
const parent = createParent({ children: createChildren(), width: 2000 });
|
||||
const controller = createController(parent);
|
||||
controller.setDisplayConfig({ mode: 'grid', grid_selected_width_factor: 3 });
|
||||
expect(
|
||||
parent.style.getPropertyValue('--frigate-card-grid-selected-width-factor'),
|
||||
).toBe('3');
|
||||
|
||||
// Setting the same config again should do nothing.
|
||||
controller.setDisplayConfig({ mode: 'grid', grid_selected_width_factor: 3 });
|
||||
expect(
|
||||
parent.style.getPropertyValue('--frigate-card-grid-selected-width-factor'),
|
||||
).toBe('3');
|
||||
});
|
||||
|
||||
it('should select cell when interacted with', () => {
|
||||
const children = createChildren();
|
||||
const parent = createParent({ children: children, width: 2000 });
|
||||
const controller = createController(parent);
|
||||
|
||||
expect(controller.getSelected()).toBeNull();
|
||||
|
||||
const clickHandler = vi.fn();
|
||||
parent.addEventListener('click', clickHandler);
|
||||
|
||||
children[1].click();
|
||||
|
||||
// Click will not be allowed through.
|
||||
expect(clickHandler).not.toBeCalled();
|
||||
expect(controller.getSelected()).toBe('1');
|
||||
});
|
||||
|
||||
it('should ignore interaction events on already selected cell', () => {
|
||||
const children = createChildren();
|
||||
const parent = createParent({ children: children, width: 2000 });
|
||||
const controller = createController(parent);
|
||||
controller.selectCell('1');
|
||||
|
||||
const clickHandler = vi.fn();
|
||||
parent.addEventListener('click', clickHandler);
|
||||
children[1].click();
|
||||
|
||||
// Click will be allowed through.
|
||||
expect(clickHandler).toBeCalled();
|
||||
expect(controller.getSelected()).toBe('1');
|
||||
});
|
||||
|
||||
it('should re-layout when child size changes', () => {
|
||||
createController(createParent({ children: createChildren() }));
|
||||
|
||||
vi.mocked(masonry.layout)?.mockClear();
|
||||
triggerResizeObserver('cell');
|
||||
expect(masonry.layout).toBeCalled();
|
||||
});
|
||||
|
||||
it('should re-create masonry when host size changes', () => {
|
||||
const children = createChildren();
|
||||
const parent = createParent({ children: children });
|
||||
createController(parent);
|
||||
expect(Masonry).toBeCalledWith(
|
||||
parent,
|
||||
expect.objectContaining({
|
||||
columnWidth: 246,
|
||||
}),
|
||||
);
|
||||
expect(parent.style.getPropertyValue('--frigate-card-grid-column-size')).toBe(
|
||||
'246px',
|
||||
);
|
||||
|
||||
// Clear mock state.
|
||||
vi.mocked(Masonry).mockClear();
|
||||
vi.mocked(masonry.layout)?.mockClear();
|
||||
|
||||
// Resize the host.
|
||||
setElementWidth(parent, 2000);
|
||||
triggerResizeObserver('host');
|
||||
|
||||
// Masonry should be reconstructed, styles set and layout called.
|
||||
expect(Masonry).toBeCalledWith(
|
||||
parent,
|
||||
expect.objectContaining({
|
||||
columnWidth: 667,
|
||||
}),
|
||||
);
|
||||
expect(parent.style.getPropertyValue('--frigate-card-grid-column-size')).toBe(
|
||||
'667px',
|
||||
);
|
||||
expect(masonry.layout).toBeCalled();
|
||||
|
||||
// Clear mock state.
|
||||
vi.mocked(Masonry).mockClear();
|
||||
vi.mocked(masonry.layout)?.mockClear();
|
||||
|
||||
// Triger with the same sizes.
|
||||
triggerResizeObserver('host');
|
||||
expect(Masonry).not.toBeCalled();
|
||||
expect(masonry.layout).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,322 +0,0 @@
|
||||
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
|
||||
import { Zoom } from '../../src/utils/zoom/zoom';
|
||||
import { PanzoomObject, PanzoomEventDetail } from '@dermotduffy/panzoom';
|
||||
import Panzoom from '@dermotduffy/panzoom';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
vi.mock('@dermotduffy/panzoom');
|
||||
|
||||
// https://github.com/jsdom/jsdom/issues/2527
|
||||
(window as any).PointerEvent = MouseEvent;
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('Zoom', () => {
|
||||
const mediaMediSpy = vi.spyOn(window, 'matchMedia');
|
||||
|
||||
const createMockPanZoom = (): PanzoomObject => {
|
||||
const panzoom = mock<PanzoomObject>();
|
||||
panzoom.getScale.mockReturnValue(1.0);
|
||||
return panzoom;
|
||||
};
|
||||
|
||||
const createAndRegisterZoom = (element: HTMLElement): Zoom => {
|
||||
const zoom = new Zoom(element);
|
||||
zoom.activate();
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mediaMediSpy.mockReturnValue(<MediaQueryList>{ matches: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be creatable', () => {
|
||||
const element = document.createElement('div');
|
||||
const zoom = new Zoom(element);
|
||||
expect(zoom).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should respond with pointer', () => {
|
||||
const element = document.createElement('div');
|
||||
|
||||
const panzoom = createMockPanZoom();
|
||||
vi.mocked(Panzoom).mockReturnValueOnce(panzoom);
|
||||
|
||||
createAndRegisterZoom(element);
|
||||
|
||||
// Won't zoom without control key.
|
||||
const ev_1 = new WheelEvent('wheel', { bubbles: false, deltaY: -120 });
|
||||
element.dispatchEvent(ev_1);
|
||||
expect(panzoom.zoomWithWheel).not.toBeCalled();
|
||||
|
||||
const ev_2 = new WheelEvent('wheel', {
|
||||
bubbles: false,
|
||||
deltaY: -120,
|
||||
ctrlKey: true,
|
||||
});
|
||||
element.dispatchEvent(ev_2);
|
||||
expect(panzoom.zoomWithWheel).toBeCalledWith(ev_2);
|
||||
|
||||
panzoom.getScale = vi.fn().mockReturnValue(1.2);
|
||||
|
||||
const ev_3 = new PointerEvent('pointerdown');
|
||||
element.dispatchEvent(ev_3);
|
||||
expect(panzoom.handleDown).toBeCalledWith(ev_3);
|
||||
|
||||
const ev_4 = new PointerEvent('pointermove');
|
||||
element.dispatchEvent(ev_4);
|
||||
expect(panzoom.handleMove).toBeCalledWith(ev_4);
|
||||
|
||||
const ev_5 = new PointerEvent('pointerup');
|
||||
element.dispatchEvent(ev_5);
|
||||
expect(panzoom.handleUp).toBeCalledWith(ev_5);
|
||||
});
|
||||
|
||||
it('should not respond to pointer when not zoomed', () => {
|
||||
const element = document.createElement('div');
|
||||
|
||||
const panzoom = createMockPanZoom();
|
||||
vi.mocked(Panzoom).mockReturnValueOnce(panzoom);
|
||||
|
||||
createAndRegisterZoom(element);
|
||||
|
||||
const ev_1 = new PointerEvent('pointerdown');
|
||||
element.dispatchEvent(ev_1);
|
||||
expect(panzoom.handleDown).not.toBeCalledWith(ev_1);
|
||||
|
||||
const ev_2 = new PointerEvent('pointermove');
|
||||
element.dispatchEvent(ev_2);
|
||||
expect(panzoom.handleDown).not.toBeCalledWith(ev_2);
|
||||
|
||||
const ev_3 = new PointerEvent('pointerup');
|
||||
element.dispatchEvent(ev_3);
|
||||
expect(panzoom.handleDown).not.toBeCalledWith(ev_3);
|
||||
});
|
||||
|
||||
it('should respond with touch', () => {
|
||||
mediaMediSpy.mockReturnValue(<MediaQueryList>{ matches: false });
|
||||
|
||||
const element = document.createElement('div');
|
||||
|
||||
const panzoom = createMockPanZoom();
|
||||
vi.mocked(Panzoom).mockReturnValueOnce(panzoom);
|
||||
|
||||
createAndRegisterZoom(element);
|
||||
|
||||
const ev_1 = new TouchEvent('touchstart', {
|
||||
bubbles: false,
|
||||
touches: [createTouch(element), createTouch(element)],
|
||||
});
|
||||
element.dispatchEvent(ev_1);
|
||||
expect(panzoom.handleDown).toBeCalledWith(ev_1);
|
||||
|
||||
panzoom.getScale = vi.fn().mockReturnValue(1.2);
|
||||
|
||||
const ev_3 = new TouchEvent('touchstart');
|
||||
element.dispatchEvent(ev_3);
|
||||
expect(panzoom.handleDown).toBeCalledWith(ev_3);
|
||||
|
||||
const ev_4 = new TouchEvent('touchmove');
|
||||
element.dispatchEvent(ev_4);
|
||||
expect(panzoom.handleMove).toBeCalledWith(ev_4);
|
||||
|
||||
const ev_5 = new TouchEvent('touchend');
|
||||
element.dispatchEvent(ev_5);
|
||||
expect(panzoom.handleUp).toBeCalledWith(ev_5);
|
||||
});
|
||||
|
||||
it('should ignore click after pointerdown', () => {
|
||||
const outer = document.createElement('div');
|
||||
const inner = document.createElement('div');
|
||||
outer.appendChild(inner);
|
||||
const clickHandler = vi.fn();
|
||||
outer.addEventListener('click', clickHandler);
|
||||
|
||||
const panzoom = createMockPanZoom();
|
||||
vi.mocked(Panzoom).mockReturnValueOnce(panzoom);
|
||||
|
||||
createAndRegisterZoom(inner);
|
||||
|
||||
// Simulate being zoomed in.
|
||||
panzoom.getScale = vi.fn().mockReturnValue(1.2);
|
||||
|
||||
// A click on its own will be fine.
|
||||
const click_1 = new MouseEvent('click', { bubbles: true });
|
||||
inner.dispatchEvent(click_1);
|
||||
expect(clickHandler).toBeCalledTimes(1);
|
||||
|
||||
// A click after a pointerdown will be ignored.
|
||||
const pointerdown_1 = new PointerEvent('pointerdown');
|
||||
inner.dispatchEvent(pointerdown_1);
|
||||
|
||||
const click_2 = new MouseEvent('click', { bubbles: true });
|
||||
inner.dispatchEvent(click_2);
|
||||
|
||||
// Click will have been ignored.
|
||||
expect(clickHandler).toBeCalledTimes(1);
|
||||
|
||||
// Simulate being zoomed out.
|
||||
panzoom.getScale = vi.fn().mockReturnValue(1.0);
|
||||
const pointerdown_2 = new PointerEvent('pointerdown');
|
||||
inner.dispatchEvent(pointerdown_2);
|
||||
|
||||
const click_3 = new MouseEvent('click', { bubbles: true });
|
||||
inner.dispatchEvent(click_3);
|
||||
|
||||
// Click will have been processed.
|
||||
expect(clickHandler).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('deactivate should remove event handlers', () => {
|
||||
const element = document.createElement('div');
|
||||
|
||||
const panzoom = createMockPanZoom();
|
||||
vi.mocked(Panzoom).mockReturnValueOnce(panzoom);
|
||||
|
||||
createAndRegisterZoom(element).deactivate();
|
||||
|
||||
const ev_1 = new WheelEvent('wheel', {
|
||||
bubbles: false,
|
||||
deltaY: -120,
|
||||
ctrlKey: true,
|
||||
});
|
||||
element.dispatchEvent(ev_1);
|
||||
expect(panzoom.zoomWithWheel).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should fire frigate cards on zoom/unzoom', () => {
|
||||
const element = document.createElement('div');
|
||||
|
||||
const zoomedFunc = vi.fn();
|
||||
const unzoomedFunc = vi.fn();
|
||||
|
||||
element.addEventListener('frigate-card:zoom:zoomed', zoomedFunc);
|
||||
element.addEventListener('frigate-card:zoom:unzoomed', unzoomedFunc);
|
||||
|
||||
vi.mocked(Panzoom).mockReturnValueOnce(createMockPanZoom());
|
||||
|
||||
createAndRegisterZoom(element);
|
||||
|
||||
const ev_1 = new CustomEvent<PanzoomEventDetail>('panzoomzoom', {
|
||||
detail: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
scale: 1.2,
|
||||
isSVG: false,
|
||||
originalEvent: new PointerEvent('pointermove'),
|
||||
},
|
||||
});
|
||||
element.dispatchEvent(ev_1);
|
||||
expect(zoomedFunc).toBeCalled();
|
||||
expect(unzoomedFunc).not.toBeCalled();
|
||||
|
||||
const ev_2 = new CustomEvent<PanzoomEventDetail>('panzoomzoom', {
|
||||
detail: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
isSVG: false,
|
||||
originalEvent: new PointerEvent('pointermove'),
|
||||
},
|
||||
});
|
||||
element.dispatchEvent(ev_2);
|
||||
expect(unzoomedFunc).toBeCalled();
|
||||
});
|
||||
|
||||
it('should set touch action on zoom/unzoom', () => {
|
||||
const element = document.createElement('div');
|
||||
vi.mocked(Panzoom).mockReturnValueOnce(createMockPanZoom());
|
||||
|
||||
createAndRegisterZoom(element);
|
||||
|
||||
const ev_1 = new CustomEvent<PanzoomEventDetail>('panzoomzoom', {
|
||||
detail: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
scale: 1.2,
|
||||
isSVG: false,
|
||||
originalEvent: new PointerEvent('pointermove'),
|
||||
},
|
||||
});
|
||||
element.dispatchEvent(ev_1);
|
||||
expect(element.style.touchAction).toBe('none');
|
||||
|
||||
const ev_2 = new CustomEvent<PanzoomEventDetail>('panzoomzoom', {
|
||||
detail: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
isSVG: false,
|
||||
originalEvent: new PointerEvent('pointermove'),
|
||||
},
|
||||
});
|
||||
element.dispatchEvent(ev_2);
|
||||
expect(element.style.touchAction).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should not fire frigate cards when state has not changed or spurious events received', () => {
|
||||
const element = document.createElement('div');
|
||||
|
||||
const zoomedFunc = vi.fn();
|
||||
const unzoomedFunc = vi.fn();
|
||||
|
||||
element.addEventListener('frigate-card:zoom:zoomed', zoomedFunc);
|
||||
element.addEventListener('frigate-card:zoom:unzoomed', unzoomedFunc);
|
||||
|
||||
vi.mocked(Panzoom).mockReturnValueOnce(createMockPanZoom());
|
||||
|
||||
createAndRegisterZoom(element);
|
||||
|
||||
const ev_1 = new CustomEvent<PanzoomEventDetail>('panzoomzoom', {
|
||||
detail: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
isSVG: false,
|
||||
originalEvent: new PointerEvent('pointermove'),
|
||||
},
|
||||
});
|
||||
element.dispatchEvent(ev_1);
|
||||
|
||||
// Unzoomed event with scale === 1, this._zoomed will already be false.
|
||||
expect(unzoomedFunc).not.toBeCalled();
|
||||
expect(zoomedFunc).not.toBeCalled();
|
||||
|
||||
const ev_2 = new CustomEvent<PanzoomEventDetail>('panzoomzoom', {
|
||||
detail: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
scale: 1.2,
|
||||
isSVG: false,
|
||||
originalEvent: new PointerEvent('pointermove'),
|
||||
},
|
||||
});
|
||||
element.dispatchEvent(ev_2);
|
||||
expect(zoomedFunc).toBeCalledTimes(1);
|
||||
expect(unzoomedFunc).not.toBeCalled();
|
||||
|
||||
// Another call when already zoomed will be ignored.
|
||||
element.dispatchEvent(ev_2);
|
||||
expect(zoomedFunc).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user