Files
advanced-camera-card/tests/utils/basic.test.ts
T
Dermot Duffy c02c692f68 feat: Add experimental rewrite of go2rtc live provider (MSE/WebRTC/MP4/MJPEG) (#2580)
- Closes #2556
- Closes #2450

**Key intended features:**
 - go2rtc compatible
- 100% test coverage to significantly improve ability to test, maintain
and work around browser weirdnesses (e.g. Safari).
 - Written from the ground up in the style of the rest of the project.

**To use:**
 - Change `live_provider` from `go2rtc` to `go2rtc-experimental`.
2026-07-14 14:22:41 -07:00

575 lines
16 KiB
TypeScript

import { afterAll, describe, expect, it, vi } from 'vitest';
import { AdvancedCameraCardError } from '../../src/types.js';
import {
allPromises,
arefloatsApproximatelyEqual,
arrayify,
arrayifyWithFalsy,
arrayMove,
aspectRatioToStyle,
contentsChanged,
dayToDate,
desparsifyArrays,
errorToConsole,
forceReflow,
formatDate,
formatDateAndTime,
generateFloatApproximatelyEqualsCustomizer,
getChildrenFromElement,
getDurationString,
ignoreFunctionIdentity,
isHoverableDevice,
isHTMLElement,
isSuperset,
isTruthy,
isValidDate,
prettifyTitle,
recursivelyMergeObjectsConcatenatingArraysUniquely,
recursivelyMergeObjectsNotArrays,
runWhenIdleIfSupported,
setify,
setOrRemoveAttribute,
setOrRemoveStyleProperty,
} from '../../src/utils/basic.js';
import { createSlot, createSlotHost } from '../test-utils.js';
describe('prettifyTitle', () => {
it('should return undefined when passed undefined', () => {
expect(prettifyTitle(undefined)).toBe(undefined);
});
it('should prettify words', () => {
expect(prettifyTitle('this is_a string')).toBe('This Is A String');
});
it('should handle leading spaces', () => {
expect(prettifyTitle(' this is_a string')).toBe('This Is A String');
});
it('should handle empty string', () => {
expect(prettifyTitle('')).toBeUndefined();
});
});
describe('arrayMove', () => {
it('should move array item', () => {
const data = [1, 2, 3];
expect(arrayMove(data, 1, 0)).toEqual([2, 1, 3]);
});
});
describe('arrayify', () => {
it('should convert non array to array', () => {
expect(arrayify(1)).toEqual([1]);
});
it('should return array', () => {
const data = [1, 2, 3];
expect(arrayify(data)).toBe(data);
});
it('should handle undefined', () => {
expect(arrayify()).toEqual([]);
});
});
describe('arrayifyWithFalsy', () => {
it('should wrap a non-array in an array', () => {
expect(arrayifyWithFalsy(1)).toEqual([1]);
});
it('should return an existing array unchanged', () => {
const data = [1, 2, 3];
expect(arrayifyWithFalsy(data)).toBe(data);
});
it('should preserve falsy values instead of dropping them', () => {
expect(arrayifyWithFalsy(0)).toEqual([0]);
expect(arrayifyWithFalsy(false)).toEqual([false]);
expect(arrayifyWithFalsy('')).toEqual(['']);
expect(arrayifyWithFalsy(null)).toEqual([null]);
});
});
describe('setify', () => {
it('should convert non set to set', () => {
expect(setify(1)).toEqual(new Set([1]));
});
it('should return set', () => {
const data = new Set([1, 2, 3]);
expect(setify(data)).toBe(data);
});
});
describe('contentsChanged', () => {
it('should have changed contents', () => {
expect(contentsChanged([1, 2], [2, 1])).toBeTruthy();
});
it('should not have changed contents', () => {
expect(contentsChanged([1, 2], [1, 2])).toBeFalsy();
});
});
describe('errorToConsole', () => {
const spy = vi.spyOn(global.console, 'warn').mockImplementation(() => true);
afterAll(() => {
vi.restoreAllMocks();
});
it('should log given error', () => {
const error = new Error('ERROR');
errorToConsole(error);
expect(spy).toHaveBeenCalledWith('ERROR');
});
it('should log with context given advanced camera card error', () => {
const data = { foo: 2 };
const error = new AdvancedCameraCardError('ERROR', { foo: 2 });
errorToConsole(error);
expect(spy).toHaveBeenCalledWith(error, data);
});
it('should log with custom function', () => {
const func = vi.fn();
const error = new Error('ERROR');
errorToConsole(error, func);
expect(func).toHaveBeenCalledWith('ERROR');
});
it('should log string', () => {
errorToConsole('string message');
expect(spy).toHaveBeenCalledWith('string message');
});
it('should do nothing given a falsy value', () => {
spy.mockClear();
errorToConsole(null);
expect(spy).not.toHaveBeenCalled();
});
});
// @vitest-environment jsdom
describe('isHoverableDevice', () => {
afterAll(() => {
vi.restoreAllMocks();
});
it('should return hoverable', () => {
vi.spyOn(window, 'matchMedia').mockReturnValue(<MediaQueryList>{ matches: true });
expect(isHoverableDevice()).toBeTruthy();
});
it('should return not hoverable', () => {
vi.spyOn(window, 'matchMedia').mockReturnValue(<MediaQueryList>{ matches: false });
expect(isHoverableDevice()).toBeFalsy();
});
});
describe('formatDateAndTime', () => {
it('should format date and time', () => {
const date = new Date(2023, 3, 14, 13, 35, 0);
expect(formatDateAndTime(date)).toBe('2023-04-14 13:35');
});
it('should format date and time with seconds', () => {
const date = new Date(2023, 3, 14, 13, 35, 1);
expect(formatDateAndTime(date, true)).toBe('2023-04-14 13:35:01');
});
});
describe('formatDate', () => {
it('should format date', () => {
const date = new Date(2023, 3, 14, 13, 35, 0);
expect(formatDate(date)).toBe('2023-04-14');
});
});
// @vitest-environment jsdom
describe('runWhenIdleIfSupported', () => {
const originalRequestIdleCallback = window.requestIdleCallback;
afterAll(() => {
window.requestIdleCallback = originalRequestIdleCallback;
});
it('should run directly when not supported', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).requestIdleCallback = undefined;
const func = vi.fn();
runWhenIdleIfSupported(func);
expect(func).toHaveBeenCalled();
});
it('should run idle when supported', () => {
const requestIdle = vi.fn();
window.requestIdleCallback = requestIdle;
const func = vi.fn();
runWhenIdleIfSupported(func);
expect(requestIdle).toBeCalledWith(func, {});
});
it('should run idle with timeout when supported', () => {
const requestIdle = vi.fn();
window.requestIdleCallback = requestIdle;
const func = vi.fn();
runWhenIdleIfSupported(func, 10);
expect(requestIdle).toBeCalledWith(func, { timeout: 10 });
});
});
describe('getDurationString', () => {
it('should return duration', () => {
const start = new Date(2023, 3, 14, 13, 35, 0);
const end = new Date(2023, 3, 14, 15, 37, 20);
expect(getDurationString(start, end)).toBe('2h 2m 20s');
});
it('should return very short duration', () => {
const start = new Date(2023, 3, 14, 13, 35, 10);
const end = new Date(2023, 3, 14, 13, 35, 12);
expect(getDurationString(start, end)).toBe('2s');
});
it('should return 0s for no delta', () => {
const start = new Date(2023, 3, 14, 13, 35, 10);
expect(getDurationString(start, start)).toBe('0s');
});
});
describe('allPromises', () => {
it('should await all promises', async () => {
const results = await allPromises([1, 2, 3], async (n) => n * 2);
expect(results).toEqual([2, 4, 6]);
});
});
describe('dayToDate', () => {
it('should return correct date', () => {
expect(dayToDate('2023-04-14')).toEqual(new Date(2023, 3, 14));
});
});
describe('isSuperset', () => {
it('should return is a superset', () => {
expect(isSuperset(new Set([1, 2, 3, 4]), new Set([2, 3]))).toBeTruthy();
});
it('should return is not a superset', () => {
expect(isSuperset(new Set([1, 2, 3, 4]), new Set([2, 3, 5]))).toBeFalsy();
});
});
describe('isValidDate', () => {
it('should be valid date', () => {
expect(isValidDate(new Date(2023, 3, 28))).toBeTruthy();
});
it('should be invalid date', () => {
expect(isValidDate(new Date('moo'))).toBeFalsy();
});
});
describe('setOrRemoveAttribute', () => {
it('should set attribute without value', () => {
const element = document.createElement('div');
setOrRemoveAttribute(element, true, 'key');
expect(element.getAttribute('key')).toBe('');
});
it('should set attribute with value', () => {
const element = document.createElement('div');
setOrRemoveAttribute(element, true, 'key', 'value');
expect(element.getAttribute('key')).toBe('value');
});
it('should remove attribute date', () => {
const element = document.createElement('div');
element.setAttribute('key', 'value');
setOrRemoveAttribute(element, false, 'key');
expect(element.getAttribute('key')).toBeFalsy();
});
});
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();
});
it('should return false for false', () => {
expect(isTruthy(false)).toBeFalsy();
});
});
describe('isHTMLElement', () => {
it('should return true for HTMLElement', () => {
const htmlElement = document.createElement('div');
expect(isHTMLElement(htmlElement)).toBeTruthy();
});
it('should return false for Element', () => {
const svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
expect(isHTMLElement(svgElement)).toBeFalsy();
});
});
describe('getChildrenFromElement', () => {
it('should return children for simple parent', () => {
const children = [document.createElement('div'), document.createElement('div')];
const parent = document.createElement('div');
children.forEach((child) => parent.appendChild(child));
expect(getChildrenFromElement(parent)).toEqual(children);
});
it('should return children for slot', () => {
const children = [document.createElement('div'), document.createElement('div')];
const slot = createSlot();
createSlotHost({ slot: slot, children: children });
expect(getChildrenFromElement(slot)).toEqual(children);
});
});
describe('recursivelyMergeObjectsNotArrays', () => {
it('should recursively merge objects but replace arrays', () => {
expect(
recursivelyMergeObjectsNotArrays<Record<string, unknown>>(
{},
{
a: {
b: {
c: 3,
d: {
e: 4,
},
array: [1, 2, 3],
},
other: {
field: 7,
},
},
},
{
a: {
b: {
array: [4],
d: {
e: 5,
},
},
},
},
),
).toEqual({
a: {
b: {
c: 3,
array: [4],
d: {
e: 5,
},
},
other: {
field: 7,
},
},
});
});
});
describe('recursivelyMergeObjectsConcatenatingArraysUniquely', () => {
it('should recursively merge objects but uniquely concat arrays', () => {
expect(
recursivelyMergeObjectsConcatenatingArraysUniquely<Record<string, unknown>>(
{},
{
a: {
b: {
c: 3,
d: {
e: 4,
},
array: [5, 1, 2, 3, 4],
},
other: {
field: 7,
},
},
},
{
a: {
b: {
array: [4, 4, 5],
d: {
e: 5,
},
},
},
},
),
).toEqual({
a: {
b: {
c: 3,
array: [5, 1, 2, 3, 4],
d: {
e: 5,
},
},
other: {
field: 7,
},
},
});
});
});
describe('aspectRatioToStyle', () => {
it('default', () => {
expect(aspectRatioToStyle()).toEqual({ 'aspect-ratio': 'auto' });
});
it('default static', () => {
expect(aspectRatioToStyle({ defaultStatic: true })).toEqual({
'aspect-ratio': '16 / 9',
});
});
it('valid ratio', () => {
expect(aspectRatioToStyle({ ratio: [4, 3] })).toEqual({ 'aspect-ratio': '4 / 3' });
});
it('invalid ratio', () => {
expect(aspectRatioToStyle({ ratio: [4] })).toEqual({ 'aspect-ratio': 'auto' });
});
});
describe('desparsifyArrays', () => {
it('number', () => {
expect(desparsifyArrays(1)).toBe(1);
});
it('string', () => {
expect(desparsifyArrays('foo')).toBe('foo');
});
describe('array', () => {
it('simple', () => {
expect(desparsifyArrays([1, 2, undefined, 3])).toEqual([1, 2, 3]);
});
it('nested', () => {
expect(
desparsifyArrays([
1,
2,
undefined,
{
subArray: [undefined, 3],
},
4,
]),
).toEqual([1, 2, { subArray: [3] }, 4]);
});
});
describe('object', () => {
it('simple', () => {
expect(
desparsifyArrays({ foo: [1, 2, undefined, 3], bar: [undefined, 4] }),
).toEqual({
foo: [1, 2, 3],
bar: [4],
});
});
it('nested', () => {
expect(
desparsifyArrays({ foo: { bar: [1, undefined, 2], empty: [undefined] } }),
).toEqual({
foo: {
bar: [1, 2],
empty: [],
},
});
});
});
});
describe('arefloatsApproximatelyEqual', () => {
describe('without precision', () => {
it('equals', () => {
expect(arefloatsApproximatelyEqual(1.1, 1.2)).toBeTruthy();
});
it('not equals', () => {
expect(arefloatsApproximatelyEqual(0.5, 1.5)).toBeFalsy();
});
});
describe('with precision', () => {
it('equals', () => {
expect(arefloatsApproximatelyEqual(1.00001, 1.00002, 4)).toBeTruthy();
});
it('not equals', () => {
expect(arefloatsApproximatelyEqual(0.5, 1.5, 4)).toBeFalsy();
});
});
});
describe('generateFloatApproximatelyEqualsCustomizer', () => {
describe('with incorrect types', () => {
it('undefined a', () => {
expect(
generateFloatApproximatelyEqualsCustomizer(4)(undefined, 1.2),
).toBeUndefined();
});
it('undefined b', () => {
expect(
generateFloatApproximatelyEqualsCustomizer(4)(1.2, undefined),
).toBeUndefined();
});
it('strings', () => {
expect(
generateFloatApproximatelyEqualsCustomizer(4)('foo', 'bar'),
).toBeUndefined();
});
});
describe('with correct types', () => {
it('equals', () => {
expect(
generateFloatApproximatelyEqualsCustomizer(4)(1.00001, 1.00002),
).toBeTruthy();
});
it('not equals', () => {
expect(
generateFloatApproximatelyEqualsCustomizer(5)(1.00001, 1.00002),
).toBeFalsy();
});
});
});
describe('ignoreFunctionIdentity', () => {
it('should treat two functions as equal regardless of identity', () => {
expect(
ignoreFunctionIdentity(
() => 1,
() => 2,
),
).toBe(true);
});
it('should treat a function appearing as a real change', () => {
expect(ignoreFunctionIdentity(() => 1, undefined)).toBe(false);
});
it('should treat a function disappearing as a real change', () => {
expect(ignoreFunctionIdentity(undefined, () => 1)).toBe(false);
});
it('should defer to default equality for non-functions', () => {
expect(ignoreFunctionIdentity(1, 2)).toBeUndefined();
});
});
describe('forceReflow', () => {
it('should access offsetHeight to trigger reflow', () => {
const element = document.createElement('div');
const offsetHeightSpy = vi
.spyOn(element, 'offsetHeight', 'get')
.mockReturnValue(100);
forceReflow(element);
expect(offsetHeightSpy).toHaveBeenCalled();
});
});