Files
advanced-camera-card/tests/components-lib/media-dimensions-container-controller.test.ts

781 lines
24 KiB
TypeScript

import {
afterAll,
afterEach,
beforeAll,
beforeEach,
describe,
expect,
it,
vi,
} from 'vitest';
import {
MediaDimensionsContainerController,
RESIZE_DEBOUNCE_SECONDS,
} from '../../src/components-lib/media-dimensions-container-controller';
import type { CameraDimensionsConfig, Rotation } from '../../src/config/schema/cameras';
import type { MediaLoadedInfo } from '../../src/types';
import {
callResizeHandler,
createLitElement,
createMediaLoadedInfoEvent,
getResizeObserver,
ResizeObserverMock,
} from '../test-utils';
// @vitest-environment jsdom
describe('MediaDimensionsContainerController', () => {
beforeAll(() => {
vi.stubGlobal('ResizeObserver', ResizeObserverMock);
});
afterAll(() => {
vi.unstubAllGlobals();
});
beforeEach(() => {
vi.clearAllMocks();
// The controller debounces its resize. Without fake timers a trailing
// resize outlives the test that scheduled it and runs against mocks that
// have since been cleared, in whatever test file happens to be running.
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
const configWithAspectRatio: CameraDimensionsConfig = {
aspect_ratio: [16, 9],
};
const configWithRotation: CameraDimensionsConfig = {
rotation: 90,
};
it('should construct', () => {
const host = createLitElement();
const eventListener = vi.fn();
host.addEventListener = eventListener;
new MediaDimensionsContainerController(host);
const observer = getResizeObserver();
// No resize observer should be created.
expect(observer?.observe).not.toHaveBeenCalled();
expect(eventListener).not.toHaveBeenCalled();
});
describe('should connect and disconnect', () => {
it('should connect and disconnect without a container', () => {
const host = createLitElement();
const controller = new MediaDimensionsContainerController(host);
const observer = getResizeObserver();
expect(observer?.observe).toHaveBeenCalledTimes(0);
controller.hostConnected();
expect(observer?.observe).toHaveBeenCalledWith(host);
expect(observer?.observe).toHaveBeenCalledTimes(1);
controller.hostDisconnected();
expect(observer?.disconnect).toHaveBeenCalled();
});
it('should cancel a pending resize on disconnect', () => {
const host = createLitElement();
host.getBoundingClientRect = vi.fn().mockReturnValue({
height: 200,
width: 200,
});
const innerContainer = document.createElement('div');
innerContainer.getBoundingClientRect = vi.fn().mockReturnValue({
height: 90,
width: 160,
});
const outerContainer = document.createElement('div');
const controller = new MediaDimensionsContainerController(host);
Object.defineProperty(host, 'isConnected', {
value: true,
});
controller.hostConnected();
controller.setConfig(configWithRotation);
controller.setContainers(innerContainer, outerContainer);
host.removeAttribute('rotated');
innerContainer.dispatchEvent(new Event('slotchange'));
controller.hostDisconnected();
vi.advanceTimersByTime(RESIZE_DEBOUNCE_SECONDS * 1000);
expect(host.hasAttribute('rotated')).toBeFalsy();
});
it('should connect and disconnect with a container when host is connected', () => {
const host = createLitElement();
Object.defineProperty(host, 'isConnected', {
value: true,
});
const controller = new MediaDimensionsContainerController(host);
const observer = getResizeObserver(0);
const container = createLitElement();
controller.setContainers(container);
expect(observer?.observe).not.toHaveBeenCalled();
controller.hostDisconnected();
expect(observer?.disconnect).toHaveBeenCalled();
controller.hostConnected();
expect(observer?.observe).toHaveBeenCalledWith(host);
});
});
describe('should set container respecting config', () => {
it('should set aspect ratio on container', () => {
const host = createLitElement();
const container = document.createElement('div');
const controller = new MediaDimensionsContainerController(host);
controller.setConfig(configWithAspectRatio);
controller.setContainers(container);
expect(container.style.aspectRatio).toBe('16 / 9');
});
it('should set layout attributes', () => {
const host = createLitElement();
const container = document.createElement('div');
const controller = new MediaDimensionsContainerController(host);
const config = {
layout: {
fit: 'contain' as const,
position: { x: 1, y: 2 },
view_box: { top: 3, bottom: 4, left: 5, right: 6 },
},
};
controller.setConfig(config);
controller.setContainers(container);
expect(
container.style.getPropertyValue('--advanced-camera-card-media-layout-fit'),
).toBe('contain');
expect(
container.style.getPropertyValue(
'--advanced-camera-card-media-layout-position-x',
),
).toBe('1%');
expect(
container.style.getPropertyValue(
'--advanced-camera-card-media-layout-position-y',
),
).toBe('2%');
expect(
container.style.getPropertyValue(
'--advanced-camera-card-media-layout-view-box-top',
),
).toBe('3%');
expect(
container.style.getPropertyValue(
'--advanced-camera-card-media-layout-view-box-bottom',
),
).toBe('4%');
expect(
container.style.getPropertyValue(
'--advanced-camera-card-media-layout-view-box-left',
),
).toBe('5%');
expect(
container.style.getPropertyValue(
'--advanced-camera-card-media-layout-view-box-right',
),
).toBe('6%');
});
});
it('should ignore multiple calls to set same container', () => {
const host = createLitElement();
const container = document.createElement('div');
const controller = new MediaDimensionsContainerController(host);
controller.setConfig(configWithAspectRatio);
controller.setContainers(container);
expect(container.style.aspectRatio).toBe('16 / 9');
container.style.aspectRatio = '';
controller.setContainers(container);
expect(container.style.aspectRatio).toBe('');
});
it('should ignore multiple calls to set same config', () => {
const host = createLitElement();
const container = document.createElement('div');
const controller = new MediaDimensionsContainerController(host);
controller.setConfig(configWithAspectRatio);
controller.setContainers(container);
container.style.aspectRatio = '';
controller.setConfig(configWithAspectRatio);
expect(container.style.aspectRatio).toBe('');
});
it('should reset config', () => {
const host = createLitElement();
const container = document.createElement('div');
const controller = new MediaDimensionsContainerController(host);
controller.setConfig(configWithAspectRatio);
controller.setContainers(container);
expect(container.style.aspectRatio).toBe('16 / 9');
controller.setConfig();
expect(container.style.aspectRatio).toBe('auto');
});
describe('should respond to size changes', () => {
it('should ignore without an aspect ratio or rotation', () => {
const host = createLitElement();
host.getBoundingClientRect = vi.fn().mockReturnValue({
height: 600,
width: 300,
});
const controller = new MediaDimensionsContainerController(host);
const innerContainer = document.createElement('div');
const outerContainer = document.createElement('div');
innerContainer.style.width = '1px';
innerContainer.style.height = '2px';
outerContainer.style.width = '3px';
outerContainer.style.height = '4px';
controller.setContainers(innerContainer, outerContainer);
callResizeHandler();
expect(innerContainer.style.width).toBe('100%');
expect(innerContainer.style.height).toBe('100%');
expect(outerContainer.style.width).toBe('100%');
expect(outerContainer.style.height).toBe('100%');
});
describe('should ignore without containers', () => {
describe.each([
['with aspect ratio', configWithAspectRatio],
['with rotation', configWithRotation],
])('%s', async (_name: string, config: CameraDimensionsConfig) => {
it('should ignore without inner container', () => {
const host = createLitElement();
const controller = new MediaDimensionsContainerController(host);
controller.setConfig(config);
const outerContainer = document.createElement('div');
outerContainer.style.width = '1px';
outerContainer.style.height = '2px';
controller.setContainers(undefined, outerContainer);
callResizeHandler();
expect(outerContainer.style.width).toBe('1px');
expect(outerContainer.style.height).toBe('2px');
});
it('should ignore without outer container', () => {
const host = createLitElement();
const controller = new MediaDimensionsContainerController(host);
controller.setConfig(config);
const innerContainer = document.createElement('div');
innerContainer.style.width = '1px';
innerContainer.style.height = '2px';
controller.setContainers(innerContainer);
callResizeHandler();
expect(innerContainer.style.width).toBe('1px');
expect(innerContainer.style.height).toBe('2px');
});
});
});
describe('should ignore without container sizes', () => {
describe('should ignore without container sizes with aspect ratio', () => {
it('should ignore without inner container size', () => {
const host = createLitElement();
const controller = new MediaDimensionsContainerController(host);
controller.setConfig(configWithAspectRatio);
const outerContainer = document.createElement('div');
const innerContainer = document.createElement('div');
innerContainer.getBoundingClientRect = vi.fn().mockReturnValue({
height: 0,
width: 0,
});
innerContainer.style.width = '1px';
innerContainer.style.height = '2px';
controller.setContainers(innerContainer, outerContainer);
callResizeHandler();
expect(outerContainer.style.width).toBe('100%');
expect(outerContainer.style.height).toBe('100%');
});
it('should ignore without host size', () => {
const host = createLitElement();
host.getBoundingClientRect = vi.fn().mockReturnValue({
height: 0,
width: 0,
});
const controller = new MediaDimensionsContainerController(host);
controller.setConfig(configWithAspectRatio);
const innerContainer = document.createElement('div');
innerContainer.style.width = '100px';
innerContainer.style.height = '200px';
innerContainer.getBoundingClientRect = vi.fn().mockReturnValue({
height: 100,
width: 200,
});
controller.setContainers(innerContainer, document.createElement('div'));
callResizeHandler();
expect(innerContainer.style.width).toBe('100%');
expect(innerContainer.style.height).toBe('100%');
});
});
describe('should ignore without container sizes with rotation', () => {
it('should ignore without inner container size', () => {
const host = createLitElement();
const controller = new MediaDimensionsContainerController(host);
controller.setConfig(configWithRotation);
const outerContainer = document.createElement('div');
const innerContainer = document.createElement('div');
innerContainer.getBoundingClientRect = vi.fn().mockReturnValue({
height: 0,
width: 0,
});
innerContainer.style.width = '1px';
innerContainer.style.height = '2px';
controller.setContainers(innerContainer, outerContainer);
callResizeHandler();
expect(outerContainer.style.width).toBe('100%');
expect(outerContainer.style.height).toBe('100%');
});
it('should ignore without host size', () => {
const host = createLitElement();
host.getBoundingClientRect = vi.fn().mockReturnValue({
height: 0,
width: 0,
});
const controller = new MediaDimensionsContainerController(host);
controller.setConfig(configWithRotation);
const innerContainer = document.createElement('div');
innerContainer.style.width = '100px';
innerContainer.style.height = '200px';
innerContainer.getBoundingClientRect = vi.fn().mockReturnValue({
height: 100,
width: 200,
});
controller.setContainers(innerContainer, document.createElement('div'));
callResizeHandler();
expect(innerContainer.style.width).toBe('100%');
expect(innerContainer.style.height).toBe('100%');
});
it('should ignore without inner container size during resize', () => {
const host = createLitElement();
host.getBoundingClientRect = vi.fn().mockReturnValue({
height: 100,
width: 200,
});
const controller = new MediaDimensionsContainerController(host);
controller.setConfig(configWithRotation);
const innerContainer = document.createElement('div');
innerContainer.style.width = '100px';
innerContainer.style.height = '200px';
innerContainer.getBoundingClientRect = vi
.fn()
.mockReturnValueOnce({
height: 100,
width: 200,
})
.mockReturnValueOnce({
height: 0,
width: 0,
});
controller.setContainers(innerContainer, document.createElement('div'));
expect(innerContainer.style.width).toBe('100%');
expect(innerContainer.style.height).toBe('100%');
});
it('should ignore without host size during resize', () => {
const host = createLitElement();
host.getBoundingClientRect = vi
.fn()
.mockReturnValueOnce({
height: 100,
width: 200,
})
.mockReturnValueOnce({
height: 0,
width: 0,
});
const controller = new MediaDimensionsContainerController(host);
controller.setConfig(configWithRotation);
const innerContainer = document.createElement('div');
innerContainer.style.width = '100px';
innerContainer.style.height = '200px';
innerContainer.getBoundingClientRect = vi.fn().mockReturnValue({
height: 100,
width: 200,
});
controller.setContainers(innerContainer, document.createElement('div'));
expect(innerContainer.style.width).toBe('100%');
expect(innerContainer.style.height).toBe('100%');
});
});
});
describe('should set rotation attributes and properties on container', () => {
it.each([
[false, undefined],
[false, 0 as const],
[true, 90 as const],
[true, 180 as const],
[true, 270 as const],
])(
'should rotate %s with rotation degrees %s',
(shouldRotate: boolean, degrees?: Rotation) => {
const host = createLitElement();
host.getBoundingClientRect = vi.fn().mockReturnValue({
height: 100,
width: 200,
});
const controller = new MediaDimensionsContainerController(host);
controller.setConfig({
rotation: degrees,
});
const innerContainer = document.createElement('div');
innerContainer.getBoundingClientRect = vi.fn().mockReturnValue({
height: 100,
width: 200,
});
controller.setContainers(innerContainer, document.createElement('div'));
callResizeHandler();
expect(host.hasAttribute('rotated')).toBe(shouldRotate);
expect(
innerContainer.style.getPropertyValue(
'--advanced-camera-card-media-rotation',
),
).toBe(shouldRotate ? `${degrees}deg` : '');
},
);
});
describe('should resize container with aspect-ratio', () => {
it('should resize container to fit width-limited container', () => {
const host = createLitElement();
host.getBoundingClientRect = vi.fn().mockReturnValue({
height: 200,
width: 200,
});
const innerContainer = document.createElement('div');
innerContainer.getBoundingClientRect = vi.fn().mockReturnValue({
height: 90,
width: 160,
});
const outerContainer = document.createElement('div');
const controller = new MediaDimensionsContainerController(host);
controller.setConfig(configWithAspectRatio);
controller.setContainers(innerContainer, outerContainer);
callResizeHandler();
expect(innerContainer.style.width).toBe('100%');
expect(innerContainer.style.height).toBe('auto');
expect(outerContainer.style.width).toBe('100%');
expect(outerContainer.style.height).toBe('100%');
expect(host.hasAttribute('rotated')).toBeFalsy();
});
it('should resize container to fit height-limited container', () => {
const host = createLitElement();
host.getBoundingClientRect = vi.fn().mockReturnValue({
height: 100,
width: 400,
});
const innerContainer = document.createElement('div');
innerContainer.getBoundingClientRect = vi.fn().mockReturnValue({
height: 200,
width: 160,
});
const outerContainer = document.createElement('div');
const controller = new MediaDimensionsContainerController(host);
controller.setConfig(configWithAspectRatio);
controller.setContainers(innerContainer, outerContainer);
callResizeHandler();
expect(innerContainer.style.width).toBe(`auto`);
expect(innerContainer.style.height).toBe('100%');
expect(outerContainer.style.width).toBe('100%');
expect(outerContainer.style.height).toBe('100%');
expect(host.hasAttribute('rotated')).toBeFalsy();
});
});
describe('should resize container with rotation', () => {
it('should resize container to fit width-limited container', () => {
const host = createLitElement();
host.getBoundingClientRect = vi.fn().mockReturnValue({
width: 500,
height: 1000,
});
const innerContainer = document.createElement('div');
innerContainer.getBoundingClientRect = vi
.fn()
.mockReturnValue({
// These are the pre-rotation dimensions.
width: 1000,
height: 2000,
})
.mockReturnValue({
// These are the post-rotation dimensions.
width: 500,
height: 1000,
});
const outerContainer = document.createElement('div');
const controller = new MediaDimensionsContainerController(host);
controller.setConfig(configWithRotation);
controller.setContainers(innerContainer, outerContainer);
callResizeHandler();
// These are pre-rotation dimensions.
expect(innerContainer.style.width).toBe('250px');
expect(innerContainer.style.height).toBe('500px');
expect(outerContainer.style.width).toBe('100%');
expect(outerContainer.style.height).toBe('250px');
expect(host.hasAttribute('rotated')).toBeTruthy();
});
it('should resize container to fit height-limited container', () => {
const host = createLitElement();
host.getBoundingClientRect = vi.fn().mockReturnValue({
width: 500,
height: 1000,
});
const innerContainer = document.createElement('div');
innerContainer.getBoundingClientRect = vi
.fn()
.mockReturnValue({
// These are the pre-rotation dimensions.
width: 1000,
height: 2000,
})
.mockReturnValue({
// These are the post-rotation dimensions.
width: 500,
height: 2000,
});
const outerContainer = document.createElement('div');
const controller = new MediaDimensionsContainerController(host);
controller.setConfig(configWithRotation);
controller.setContainers(innerContainer, outerContainer);
callResizeHandler();
// These are pre-rotation dimensions.
expect(innerContainer.style.width).toBe('1000px');
expect(innerContainer.style.height).toBe('4000px');
expect(outerContainer.style.width).toBe('100%');
expect(outerContainer.style.height).toBe('1000px');
expect(host.hasAttribute('rotated')).toBeTruthy();
});
});
});
describe('should respond to slot changes', () => {
it('should resize container on slotchange event', () => {
const host = createLitElement();
host.getBoundingClientRect = vi.fn().mockReturnValue({
height: 200,
width: 200,
});
const innerContainer = document.createElement('div');
innerContainer.getBoundingClientRect = vi.fn().mockReturnValue({
height: 90,
width: 160,
});
const outerContainer = document.createElement('div');
const controller = new MediaDimensionsContainerController(host);
Object.defineProperty(host, 'isConnected', {
value: true,
});
controller.hostConnected();
controller.setConfig(configWithRotation);
controller.setContainers(innerContainer, outerContainer);
host.removeAttribute('rotated');
innerContainer.dispatchEvent(new Event('slotchange'));
vi.advanceTimersByTime(RESIZE_DEBOUNCE_SECONDS * 1000);
expect(host.hasAttribute('rotated')).toBeTruthy();
});
});
describe('should respond to media load', () => {
it('should resize container on media load', () => {
const host = createLitElement();
host.getBoundingClientRect = vi.fn().mockReturnValue({
height: 200,
width: 200,
});
const innerContainer = document.createElement('div');
innerContainer.getBoundingClientRect = vi.fn().mockReturnValue({
height: 90,
width: 160,
});
const outerContainer = document.createElement('div');
const controller = new MediaDimensionsContainerController(host);
Object.defineProperty(host, 'isConnected', {
value: true,
});
controller.hostConnected();
controller.setConfig(configWithRotation);
controller.setContainers(innerContainer, outerContainer);
host.removeAttribute('rotated');
const mediaLoadedInfo: MediaLoadedInfo = {
width: 90,
height: 160,
};
innerContainer.dispatchEvent(
createMediaLoadedInfoEvent({ info: mediaLoadedInfo }),
);
vi.advanceTimersByTime(RESIZE_DEBOUNCE_SECONDS * 1000);
expect(host.hasAttribute('rotated')).toBeTruthy();
});
it('should not resize container on media load to same size', () => {
const host = createLitElement();
host.getBoundingClientRect = vi.fn().mockReturnValue({
height: 200,
width: 200,
});
const innerContainer = document.createElement('div');
innerContainer.getBoundingClientRect = vi.fn().mockReturnValue({
height: 90,
width: 160,
});
const outerContainer = document.createElement('div');
const controller = new MediaDimensionsContainerController(host);
Object.defineProperty(host, 'isConnected', {
value: true,
});
controller.hostConnected();
controller.setConfig(configWithRotation);
controller.setContainers(innerContainer, outerContainer);
const mediaLoadedInfo: MediaLoadedInfo = {
width: 90,
height: 160,
};
innerContainer.dispatchEvent(
createMediaLoadedInfoEvent({ info: mediaLoadedInfo }),
);
expect(host.hasAttribute('rotated')).toBeTruthy();
host.removeAttribute('rotated');
innerContainer.dispatchEvent(
createMediaLoadedInfoEvent({ info: mediaLoadedInfo }),
);
expect(host.hasAttribute('rotated')).toBeFalsy();
});
});
});