feat: Add support for rotating camera streams (#2149)

* Closes #1307
This commit is contained in:
Dermot Duffy
2025-08-23 17:30:53 -07:00
committed by GitHub
parent 69ae80d6f1
commit 4d4b64232a
25 changed files with 1393 additions and 764 deletions
@@ -0,0 +1,733 @@
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { MediaDimensionsContainerController } from '../../src/components-lib/media-dimensions-container-controller';
import { CameraDimensionsConfig, Rotation } from '../../src/config/schema/cameras';
import { MediaLoadedInfo } from '../../src/types';
import {
callResizeHandler,
createLitElement,
getResizeObserver,
ResizeObserverMock,
} from '../test-utils';
vi.mock('lodash-es', () => ({
debounce: vi.fn((fn) => fn),
}));
// @vitest-environment jsdom
describe('MediaDimensionsContainerController', () => {
beforeAll(() => {
vi.stubGlobal('ResizeObserver', ResizeObserverMock);
});
afterAll(() => {
vi.unstubAllGlobals();
});
beforeEach(() => {
vi.clearAllMocks();
});
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.toBeCalled();
expect(eventListener).not.toBeCalled();
});
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).toBeCalledTimes(0);
controller.hostConnected();
expect(observer?.observe).toBeCalledWith(host);
expect(observer?.observe).toBeCalledTimes(1);
controller.hostDisconnected();
expect(observer?.disconnect).toBeCalled();
});
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.toBeCalled();
controller.hostDisconnected();
expect(observer?.disconnect).toBeCalled();
controller.hostConnected();
expect(observer?.observe).toBeCalledWith(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'));
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(
new CustomEvent<MediaLoadedInfo>('advanced-camera-card:media:loaded', {
detail: mediaLoadedInfo,
}),
);
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(
new CustomEvent<MediaLoadedInfo>('advanced-camera-card:media:loaded', {
detail: mediaLoadedInfo,
}),
);
expect(host.hasAttribute('rotated')).toBeTruthy();
host.removeAttribute('rotated');
innerContainer.dispatchEvent(
new CustomEvent<MediaLoadedInfo>('advanced-camera-card:media:loaded', {
detail: mediaLoadedInfo,
}),
);
expect(host.hasAttribute('rotated')).toBeFalsy();
});
});
});
@@ -1,419 +0,0 @@
import {
afterAll,
afterEach,
beforeAll,
beforeEach,
describe,
expect,
it,
vi,
} from 'vitest';
import { MediaProviderDimensionsController } from '../../src/components-lib/media-provider-dimensions-controller';
import { CameraDimensionsConfig } from '../../src/config/schema/cameras';
import { dispatchExistingMediaLoadedInfoAsEvent } from '../../src/utils/media-info';
import {
callResizeHandler,
createLitElement,
createMediaLoadedInfo,
getResizeObserver,
requestAnimationFrameMock,
ResizeObserverMock,
} from '../test-utils';
vi.mock('lodash-es', () => ({
throttle: vi.fn((fn) => fn),
}));
// @vitest-environment jsdom
describe('MediaProviderDimensionsController', () => {
beforeAll(() => {
vi.stubGlobal('ResizeObserver', ResizeObserverMock);
});
afterAll(() => {
vi.unstubAllGlobals();
});
beforeEach(() => {
vi.clearAllMocks();
});
const configWithAspectRatioLandscape: CameraDimensionsConfig = {
aspect_ratio: [16, 9],
};
const configWithAspectRatioPortrait: CameraDimensionsConfig = {
aspect_ratio: [9, 16],
};
it('should construct', () => {
const host = createLitElement();
const eventListener = vi.fn();
host.addEventListener = eventListener;
new MediaProviderDimensionsController(host);
const observer = getResizeObserver();
// No resize observer should be created.
expect(observer?.observe).not.toBeCalled();
expect(eventListener).not.toBeCalled();
});
describe('should connect and disconnect', () => {
it('should connect and disconnect without a container', () => {
const host = createLitElement();
const controller = new MediaProviderDimensionsController(host);
const observer = getResizeObserver();
expect(observer?.observe).toBeCalledTimes(0);
controller.hostConnected();
expect(observer?.observe).toBeCalledWith(host);
expect(observer?.observe).toBeCalledTimes(1);
controller.hostDisconnected();
expect(observer?.disconnect).toBeCalled();
});
it('should connect and disconnect with a container when host is connected', () => {
const host = createLitElement();
Object.defineProperty(host, 'isConnected', {
value: true,
});
const controller = new MediaProviderDimensionsController(host);
const observer = getResizeObserver(0);
const container = createLitElement();
controller.setContainer(container);
expect(observer?.observe).not.toBeCalled();
controller.hostDisconnected();
expect(observer?.disconnect).toBeCalled();
controller.hostConnected();
expect(observer?.observe).toBeCalledWith(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 MediaProviderDimensionsController(host);
controller.setCameraConfig(configWithAspectRatioLandscape);
controller.setContainer(container);
expect(container.style.aspectRatio).toBe('16 / 9');
});
describe('should set host attribute', () => {
it.each([
['max', {}],
['max-width', { aspect_ratio: [16, 9] }],
['max-height', { aspect_ratio: [9, 16] }],
])('%s', async (value: string, config: CameraDimensionsConfig) => {
const host = createLitElement();
const container = document.createElement('div');
const controller = new MediaProviderDimensionsController(host);
controller.setCameraConfig(config);
controller.setContainer(container);
expect(host.getAttribute('size')).toBe(value);
});
});
it('should set layout attributes', () => {
const host = createLitElement();
const container = document.createElement('div');
const controller = new MediaProviderDimensionsController(host);
const config = {
layout: {
fit: 'contain' as const,
position: { x: 1, y: 2 },
view_box: { top: 3, bottom: 4, left: 5, right: 6 },
},
};
controller.setCameraConfig(config);
controller.setContainer(container);
expect(
host.style.getPropertyValue('--advanced-camera-card-media-layout-fit'),
).toBe('contain');
expect(
host.style.getPropertyValue('--advanced-camera-card-media-layout-position-x'),
).toBe('1%');
expect(
host.style.getPropertyValue('--advanced-camera-card-media-layout-position-y'),
).toBe('2%');
expect(
host.style.getPropertyValue('--advanced-camera-card-media-layout-view-box-top'),
).toBe('3%');
expect(
host.style.getPropertyValue(
'--advanced-camera-card-media-layout-view-box-bottom',
),
).toBe('4%');
expect(
host.style.getPropertyValue('--advanced-camera-card-media-layout-view-box-left'),
).toBe('5%');
expect(
host.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 MediaProviderDimensionsController(host);
controller.setCameraConfig(configWithAspectRatioLandscape);
controller.setContainer(container);
expect(host.getAttribute('size')).toBe('max-width');
host.setAttribute('size', 'custom');
controller.setContainer(container);
expect(host.getAttribute('size')).toBe('custom');
});
it('should reset container', () => {
const host = createLitElement();
const container = document.createElement('div');
const controller = new MediaProviderDimensionsController(host);
controller.setCameraConfig(configWithAspectRatioLandscape);
controller.setContainer(container);
controller.setContainer();
expect(host.getAttribute('size')).toBe('max-width');
});
describe('should set size attribute correctly', () => {
it('should set max-width', () => {
const host = createLitElement();
const container = document.createElement('div');
const controller = new MediaProviderDimensionsController(host);
controller.setCameraConfig(configWithAspectRatioLandscape);
controller.setContainer(container);
expect(container.style.aspectRatio).toBe('16 / 9');
expect(host.getAttribute('size')).toBe('max-width');
});
it('should set max-height', () => {
const host = createLitElement();
const container = document.createElement('div');
const controller = new MediaProviderDimensionsController(host);
controller.setCameraConfig(configWithAspectRatioPortrait);
controller.setContainer(container);
expect(container.style.aspectRatio).toBe('9 / 16');
expect(host.getAttribute('size')).toBe('max-height');
});
it('should set max', () => {
const host = createLitElement();
const container = document.createElement('div');
const controller = new MediaProviderDimensionsController(host);
controller.setContainer(container);
expect(host.getAttribute('size')).toBe('max');
});
it('should set max without a config', () => {
const host = createLitElement();
const container = document.createElement('div');
const controller = new MediaProviderDimensionsController(host);
controller.setCameraConfig();
controller.setContainer(container);
expect(host.getAttribute('size')).toBe('max');
});
});
describe('should respond to size changes', () => {
it('should ignore without an aspect ratio', () => {
const host = createLitElement();
host.setAttribute('size', '__RANDOM__');
host.getBoundingClientRect = vi.fn().mockReturnValue({
height: 600,
width: 300,
});
new MediaProviderDimensionsController(host);
callResizeHandler();
expect(host.getAttribute('size')).toBe('__RANDOM__');
});
it('should set host to max if no container', () => {
const host = createLitElement();
host.setAttribute('size', 'custom');
host.getBoundingClientRect = vi.fn().mockReturnValue({
height: 600,
width: 300,
});
const controller = new MediaProviderDimensionsController(host);
controller.setCameraConfig(configWithAspectRatioLandscape);
callResizeHandler();
expect(host.getAttribute('size')).toBe('max');
});
it('should set host to max if container has no dimensions', () => {
const host = createLitElement();
host.setAttribute('size', 'custom');
host.getBoundingClientRect = vi.fn().mockReturnValue({
height: 100,
width: 200,
});
const container = document.createElement('div');
container.getBoundingClientRect = vi.fn().mockReturnValue({
height: 0,
width: 0,
});
const controller = new MediaProviderDimensionsController(host);
controller.setCameraConfig(configWithAspectRatioLandscape);
controller.setContainer(container);
callResizeHandler();
expect(host.getAttribute('size')).toBe('max');
});
it('should ignore resize calls where actual equals intended size', () => {
const host = createLitElement();
host.setAttribute('size', 'custom');
host.getBoundingClientRect = vi.fn().mockReturnValue({
height: 100,
width: 200,
});
const container = document.createElement('div');
container.getBoundingClientRect = vi.fn().mockReturnValue({
height: 100,
width: 200,
});
const controller = new MediaProviderDimensionsController(host);
controller.setCameraConfig(configWithAspectRatioLandscape);
controller.setContainer(container);
callResizeHandler();
host.setAttribute('size', '__RANDOM__');
// 2nd call should be ignored.
callResizeHandler();
expect(host.getAttribute('size')).toBe('__RANDOM__');
});
describe('should resize container to fit width-limited container', () => {
it('should resize container to fit width-limited container', () => {
const host = createLitElement();
host.getBoundingClientRect = vi.fn().mockReturnValue({
height: 200,
width: 200,
});
const container = document.createElement('div');
container.getBoundingClientRect = vi.fn().mockReturnValue({
height: 90,
width: 160,
});
const controller = new MediaProviderDimensionsController(host);
controller.setCameraConfig(configWithAspectRatioLandscape);
controller.setContainer(container);
callResizeHandler();
expect(container.style.width).toBe('100%');
expect(container.style.height).toBe('auto');
expect(host.getAttribute('size')).toBe('custom');
});
it('should resize container to fit height-limited container', () => {
const host = createLitElement();
host.getBoundingClientRect = vi.fn().mockReturnValue({
height: 100,
width: 400,
});
const container = document.createElement('div');
container.getBoundingClientRect = vi.fn().mockReturnValue({
height: 200,
width: 160,
});
const controller = new MediaProviderDimensionsController(host);
controller.setCameraConfig(configWithAspectRatioLandscape);
controller.setContainer(container);
callResizeHandler();
expect(container.style.width).toBe(`${100 * (160 / 200)}px`);
expect(container.style.height).toBe('100px');
expect(host.getAttribute('size')).toBe('custom');
});
});
});
describe('should respond to media loading', () => {
beforeEach(() => {
vi.spyOn(window, 'requestAnimationFrame').mockImplementation(
requestAnimationFrameMock,
);
});
afterEach(() => {
vi.mocked(window.requestAnimationFrame).mockRestore();
});
it('should resize container after a media load', () => {
const host = createLitElement();
host.getBoundingClientRect = vi.fn().mockReturnValue({
height: 100,
width: 400,
});
const container = document.createElement('div');
container.getBoundingClientRect = vi.fn().mockReturnValue({
height: 90,
width: 160,
});
const controller = new MediaProviderDimensionsController(host);
controller.setCameraConfig(configWithAspectRatioLandscape);
controller.setContainer(container);
controller.hostConnected();
dispatchExistingMediaLoadedInfoAsEvent(host, createMediaLoadedInfo());
expect(container.style.width).toBe(`100%`);
expect(container.style.height).toBe('auto');
expect(host.getAttribute('size')).toBe('custom');
});
});
});
+22
View File
@@ -26,6 +26,7 @@ import {
runWhenIdleIfSupported,
setify,
setOrRemoveAttribute,
setOrRemoveStyleProperty,
} from '../../src/utils/basic.js';
import { createSlot, createSlotHost } from '../test-utils.js';
@@ -248,6 +249,27 @@ describe('setOrRemoveAttribute', () => {
});
});
describe('setOrRemoveStyleProperty', () => {
it('should set style property without value', () => {
const element = document.createElement('div');
setOrRemoveStyleProperty(element, true, 'width');
expect(element.style.getPropertyValue('width')).toBe('');
});
it('should set style property with value', () => {
const element = document.createElement('div');
setOrRemoveStyleProperty(element, true, 'width', '100px');
expect(element.style.getPropertyValue('width')).toBe('100px');
});
it('should remove style property', () => {
const element = document.createElement('div');
element.style.setProperty('width', '100px');
setOrRemoveStyleProperty(element, false, 'width');
expect(element.style.getPropertyValue('width')).toBeFalsy();
});
});
describe('isTruthy', () => {
it('should return true for true', () => {
expect(isTruthy(true)).toBeTruthy();