chore: Upgrade to Vitest 4 and regroup tests for performance (#2618)
This commit is contained in:
+4
-4
@@ -68,7 +68,7 @@
|
|||||||
"@types/masonry-layout": "^4.2.8",
|
"@types/masonry-layout": "^4.2.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.30.1",
|
"@typescript-eslint/eslint-plugin": "^8.30.1",
|
||||||
"@typescript-eslint/parser": "^8.30.1",
|
"@typescript-eslint/parser": "^8.30.1",
|
||||||
"@vitest/coverage-istanbul": "^1.6.0",
|
"@vitest/coverage-istanbul": "^4.1.10",
|
||||||
"conventional-changelog-conventionalcommits": "^8.0.0",
|
"conventional-changelog-conventionalcommits": "^8.0.0",
|
||||||
"docsify-cli": "^4.4.4",
|
"docsify-cli": "^4.4.4",
|
||||||
"eslint": "^9.24.0",
|
"eslint": "^9.24.0",
|
||||||
@@ -86,8 +86,8 @@
|
|||||||
"semantic-release-export-data": "^1.1.0",
|
"semantic-release-export-data": "^1.1.0",
|
||||||
"type-fest": "^4.41.0",
|
"type-fest": "^4.41.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vitest": "^1.6.0",
|
"vitest": "^4.1.10",
|
||||||
"vitest-mock-extended": "^1.3.1"
|
"vitest-mock-extended": "^5.1.0"
|
||||||
},
|
},
|
||||||
"release": {
|
"release": {
|
||||||
"branches": [
|
"branches": [
|
||||||
@@ -188,7 +188,7 @@
|
|||||||
"format-check": "prettier --check .",
|
"format-check": "prettier --check .",
|
||||||
"rollup": "rollup -c",
|
"rollup": "rollup -c",
|
||||||
"prune": "knip",
|
"prune": "knip",
|
||||||
"test": "VITEST_GROUP=shared vitest run && VITEST_GROUP=isolated vitest run",
|
"test": "vitest run",
|
||||||
"coverage": "vitest run --coverage"
|
"coverage": "vitest run --coverage"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
|
|||||||
@@ -242,14 +242,14 @@ export class CameraTriggersManager {
|
|||||||
} else if (ev.fidelity === 'high' && triggerAction === 'media') {
|
} else if (ev.fidelity === 'high' && triggerAction === 'media') {
|
||||||
// Choose the most appropriate media view based on what's available.
|
// Choose the most appropriate media view based on what's available.
|
||||||
// Priority: review > clip > snapshot
|
// Priority: review > clip > snapshot
|
||||||
|
/* istanbul ignore next: the `null` case is unreachable due to `skipViewAction` above -- @preserve */
|
||||||
const view = ev.review
|
const view = ev.review
|
||||||
? 'review'
|
? 'review'
|
||||||
: ev.clip
|
: ev.clip
|
||||||
? 'clip'
|
? 'clip'
|
||||||
: ev.snapshot
|
: ev.snapshot
|
||||||
? 'snapshot'
|
? 'snapshot'
|
||||||
: /* istanbul ignore next: unreachable due to `skipViewAction` above -- @preserve */
|
: null;
|
||||||
null;
|
|
||||||
|
|
||||||
/* istanbul ignore next: unreachable due to `skipViewAction` above -- @preserve */
|
/* istanbul ignore next: unreachable due to `skipViewAction` above -- @preserve */
|
||||||
if (view) {
|
if (view) {
|
||||||
|
|||||||
@@ -101,11 +101,10 @@ export class MediaHeightController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _initializeRoot(): void {
|
private _initializeRoot(): void {
|
||||||
|
/* istanbul ignore next: the absent-root path cannot be reached as root will
|
||||||
|
always exist by the time the mutation observer is observing -- @preserve */
|
||||||
const children = [
|
const children = [
|
||||||
...(this._root?.querySelectorAll<HTMLElement>(this._selector) ??
|
...(this._root?.querySelectorAll<HTMLElement>(this._selector) ?? []),
|
||||||
/* istanbul ignore next: this path cannot be reached as root will always
|
|
||||||
exist by the time the mutation observer is observing -- @preserve */
|
|
||||||
[]),
|
|
||||||
];
|
];
|
||||||
if (isEqual(children, this._children)) {
|
if (isEqual(children, this._children)) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -123,6 +123,12 @@ export class ZoomController {
|
|||||||
config?.zoom ?? ZOOM_DEFAULT_SCALE,
|
config?.zoom ?? ZOOM_DEFAULT_SCALE,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// The ZOOM_DEFAULT_SCALE fallback is not reachable: without a zoom value a
|
||||||
|
// default of 1 is assumed in _convertPercentToXYPan, which returns null at
|
||||||
|
// the default zoom, leaving `converted` unset.
|
||||||
|
/* istanbul ignore next @preserve */
|
||||||
|
const startScale = config?.zoom ?? ZOOM_DEFAULT_SCALE;
|
||||||
|
|
||||||
this._panzoom = Panzoom(this._element, {
|
this._panzoom = Panzoom(this._element, {
|
||||||
contain: 'outside',
|
contain: 'outside',
|
||||||
maxScale: 10,
|
maxScale: 10,
|
||||||
@@ -140,16 +146,7 @@ export class ZoomController {
|
|||||||
// Set the initial pan/zoom values to avoid an initial unzoomed view.
|
// Set the initial pan/zoom values to avoid an initial unzoomed view.
|
||||||
...(config && converted && { startX: converted.x }),
|
...(config && converted && { startX: converted.x }),
|
||||||
...(config && converted && { startY: converted.y }),
|
...(config && converted && { startY: converted.y }),
|
||||||
...(config &&
|
...(config && converted && { startScale }),
|
||||||
converted && {
|
|
||||||
startScale:
|
|
||||||
config.zoom ??
|
|
||||||
// This is not reachable as without a zoom value, a default of 1 is
|
|
||||||
// assumed in _convertPercentToXYPan, which will return null @
|
|
||||||
// default zoom, and so this cannot be reached in practice.
|
|
||||||
/* istanbul ignore next @preserve */
|
|
||||||
ZOOM_DEFAULT_SCALE,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const registerListeners = (
|
const registerListeners = (
|
||||||
@@ -436,18 +433,16 @@ export class ZoomController {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The ZOOM_DEFAULT_SCALE fallback cannot be reached: when
|
||||||
|
// this._defaultSettings.zoom is undefined, convertedDefault ends up null
|
||||||
|
// above and this function has already returned.
|
||||||
|
/* istanbul ignore next @preserve */
|
||||||
|
const defaultScale = this._defaultSettings.zoom ?? ZOOM_DEFAULT_SCALE;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
arefloatsApproximatelyEqual(x, convertedDefault.x) &&
|
arefloatsApproximatelyEqual(x, convertedDefault.x) &&
|
||||||
arefloatsApproximatelyEqual(y, convertedDefault.y) &&
|
arefloatsApproximatelyEqual(y, convertedDefault.y) &&
|
||||||
arefloatsApproximatelyEqual(
|
arefloatsApproximatelyEqual(scale, defaultScale)
|
||||||
scale,
|
|
||||||
this._defaultSettings.zoom ??
|
|
||||||
// The ZOOM_DEFAULT_SCALE clause below cannot be reached since when
|
|
||||||
// this._defaultConfig.zoom is undefined, convertedDefault will end up
|
|
||||||
// null above and this function will have already returned.
|
|
||||||
/* istanbul ignore next @preserve */
|
|
||||||
ZOOM_DEFAULT_SCALE,
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -208,6 +208,7 @@ describe('FrigateQueryResultsClassifier', () => {
|
|||||||
describe('FrigateCameraManagerEngine', () => {
|
describe('FrigateCameraManagerEngine', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getEngineType', () => {
|
describe('getEngineType', () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest';
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { ActionSet } from '../../../../src/card-controller/actions/actions/set';
|
import { ActionSet } from '../../../../src/card-controller/actions/actions/set';
|
||||||
import { createLogAction } from '../../../../src/utils/action';
|
import { createLogAction } from '../../../../src/utils/action';
|
||||||
@@ -6,6 +6,10 @@ import { arrayify } from '../../../../src/utils/basic';
|
|||||||
import { createCardAPI } from '../../../test-utils';
|
import { createCardAPI } from '../../../test-utils';
|
||||||
|
|
||||||
describe('ActionSet', () => {
|
describe('ActionSet', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
const createAPI = () => {
|
const createAPI = () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getLockManager().getAllowedActions).mockImplementation((actions) =>
|
vi.mocked(api.getLockManager().getAllowedActions).mockImplementation((actions) =>
|
||||||
|
|||||||
@@ -29,9 +29,13 @@ import {
|
|||||||
|
|
||||||
// Replace Ringtone with a fresh `mock<Ringtone>()` per construction so each
|
// Replace Ringtone with a fresh `mock<Ringtone>()` per construction so each
|
||||||
// CallManager gets an isolated, type-safe ringtone we can assert on. The
|
// CallManager gets an isolated, type-safe ringtone we can assert on. The
|
||||||
// real Ringtone creates an AudioContext, which we never want in tests.
|
// real Ringtone creates an AudioContext, which we never want in tests. The
|
||||||
|
// implementation must be callable with `new`, so it cannot be an arrow
|
||||||
|
// function.
|
||||||
vi.mock('../../../src/card-controller/call/ringtone', () => ({
|
vi.mock('../../../src/card-controller/call/ringtone', () => ({
|
||||||
Ringtone: vi.fn().mockImplementation(() => mock<Ringtone>()),
|
Ringtone: vi.fn().mockImplementation(function () {
|
||||||
|
return mock<Ringtone>();
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Each test creates a new CallManager which constructs a new Ringtone, so the
|
// Each test creates a new CallManager which constructs a new Ringtone, so the
|
||||||
|
|||||||
@@ -12,20 +12,32 @@ import type { RingtoneConfig } from '../../../src/config/schema/live';
|
|||||||
// Each tone constructor returns a fresh `mock<>()` per `new` call; the mock
|
// Each tone constructor returns a fresh `mock<>()` per `new` call; the mock
|
||||||
// implementation persists across `vi.clearAllMocks()` (which only clears call
|
// implementation persists across `vi.clearAllMocks()` (which only clears call
|
||||||
// records, not implementations) so tests don't need per-test re-installation.
|
// records, not implementations) so tests don't need per-test re-installation.
|
||||||
|
// The implementations must be callable with `new`, so they cannot be arrow
|
||||||
|
// functions.
|
||||||
vi.mock('../../../src/card-controller/call/tones/chime', () => ({
|
vi.mock('../../../src/card-controller/call/tones/chime', () => ({
|
||||||
ChimeTone: vi.fn().mockImplementation(() => mock<ChimeTone>()),
|
ChimeTone: vi.fn().mockImplementation(function () {
|
||||||
|
return mock<ChimeTone>();
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
vi.mock('../../../src/card-controller/call/tones/westminster', () => ({
|
vi.mock('../../../src/card-controller/call/tones/westminster', () => ({
|
||||||
WestminsterTone: vi.fn().mockImplementation(() => mock<WestminsterTone>()),
|
WestminsterTone: vi.fn().mockImplementation(function () {
|
||||||
|
return mock<WestminsterTone>();
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
vi.mock('../../../src/card-controller/call/tones/arpeggio', () => ({
|
vi.mock('../../../src/card-controller/call/tones/arpeggio', () => ({
|
||||||
ArpeggioTone: vi.fn().mockImplementation(() => mock<ArpeggioTone>()),
|
ArpeggioTone: vi.fn().mockImplementation(function () {
|
||||||
|
return mock<ArpeggioTone>();
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
vi.mock('../../../src/card-controller/call/tones/melody', () => ({
|
vi.mock('../../../src/card-controller/call/tones/melody', () => ({
|
||||||
MelodyTone: vi.fn().mockImplementation(() => mock<MelodyTone>()),
|
MelodyTone: vi.fn().mockImplementation(function () {
|
||||||
|
return mock<MelodyTone>();
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
vi.mock('../../../src/card-controller/call/tones/custom', () => ({
|
vi.mock('../../../src/card-controller/call/tones/custom', () => ({
|
||||||
CustomTone: vi.fn().mockImplementation(() => mock<CustomTone>()),
|
CustomTone: vi.fn().mockImplementation(function () {
|
||||||
|
return mock<CustomTone>();
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Returns the most recently constructed instance of a mocked class.
|
// Returns the most recently constructed instance of a mocked class.
|
||||||
@@ -187,7 +199,7 @@ describe('lock', () => {
|
|||||||
describe('natural finish', () => {
|
describe('natural finish', () => {
|
||||||
it('should release the lock when the tone fires its finished handler', () => {
|
it('should release the lock when the tone fires its finished handler', () => {
|
||||||
let finishedHandler: (() => void) | undefined;
|
let finishedHandler: (() => void) | undefined;
|
||||||
vi.mocked(ChimeTone).mockImplementationOnce(() => {
|
vi.mocked(ChimeTone).mockImplementationOnce(function () {
|
||||||
const tone = mock<ChimeTone>();
|
const tone = mock<ChimeTone>();
|
||||||
vi.mocked(tone.start).mockImplementation((handler) => {
|
vi.mocked(tone.start).mockImplementation((handler) => {
|
||||||
finishedHandler = handler;
|
finishedHandler = handler;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ interface AudioMocks {
|
|||||||
// and pushed here in construction order, so tests can dispatch real events
|
// and pushed here in construction order, so tests can dispatch real events
|
||||||
// and read real properties (`loop`, `currentTime`, etc.) on the instances.
|
// and read real properties (`loop`, `currentTime`, etc.) on the instances.
|
||||||
instances: HTMLAudioElement[];
|
instances: HTMLAudioElement[];
|
||||||
ctor: Mock<[string?], HTMLAudioElement>;
|
ctor: Mock<(url?: string) => HTMLAudioElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Uses real jsdom HTMLAudioElement instances and only stubs the parts jsdom
|
// Uses real jsdom HTMLAudioElement instances and only stubs the parts jsdom
|
||||||
@@ -33,7 +33,10 @@ const useAudioElementMocks = (): AudioMocks => {
|
|||||||
vi.spyOn(HTMLMediaElement.prototype, 'pause').mockImplementation(() => {});
|
vi.spyOn(HTMLMediaElement.prototype, 'pause').mockImplementation(() => {});
|
||||||
|
|
||||||
const RealAudio = window.Audio;
|
const RealAudio = window.Audio;
|
||||||
handle.ctor = vi.fn((url?: string) => {
|
|
||||||
|
// The source calls `new Audio(...)`, and a mock implementation must be
|
||||||
|
// callable with `new`, so it cannot be an arrow function.
|
||||||
|
handle.ctor = vi.fn(function (url?: string) {
|
||||||
const audio = new RealAudio(url);
|
const audio = new RealAudio(url);
|
||||||
handle.instances.push(audio);
|
handle.instances.push(audio);
|
||||||
return audio;
|
return audio;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { mock, type MockProxy } from 'vitest-mock-extended';
|
|||||||
// over the same observable state.
|
// over the same observable state.
|
||||||
interface AudioMocks {
|
interface AudioMocks {
|
||||||
audioContext: MockProxy<AudioContext>;
|
audioContext: MockProxy<AudioContext>;
|
||||||
audioContextCtor: Mock<[], MockProxy<AudioContext>>;
|
audioContextCtor: Mock<() => MockProxy<AudioContext>>;
|
||||||
|
|
||||||
// Filled in the order `createOscillator()` / `createGain()` were called.
|
// Filled in the order `createOscillator()` / `createGain()` were called.
|
||||||
oscillators: MockProxy<OscillatorNode>[];
|
oscillators: MockProxy<OscillatorNode>[];
|
||||||
@@ -73,7 +73,11 @@ export const useAudioMocks = (): AudioMocks => {
|
|||||||
configurable: true,
|
configurable: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
audio.audioContextCtor = vi.fn(() => audio.audioContext);
|
// The source calls `new AudioContext()`, and a mock implementation must be
|
||||||
|
// callable with `new`, so it cannot be an arrow function.
|
||||||
|
audio.audioContextCtor = vi.fn(function () {
|
||||||
|
return audio.audioContext;
|
||||||
|
});
|
||||||
vi.stubGlobal('AudioContext', audio.audioContextCtor);
|
vi.stubGlobal('AudioContext', audio.audioContextCtor);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,28 +5,43 @@ import type { EffectComponent } from '../../../src/card-controller/effects/types
|
|||||||
import type { EffectName } from '../../../src/types';
|
import type { EffectName } from '../../../src/types';
|
||||||
import { flushPromises } from '../../test-utils';
|
import { flushPromises } from '../../test-utils';
|
||||||
|
|
||||||
|
// The source constructs each effect component with `new`, and a mock
|
||||||
|
// implementation must be callable with `new`, so it cannot be an arrow
|
||||||
|
// function.
|
||||||
vi.mock('../../../src/components/effects/fireworks', () => ({
|
vi.mock('../../../src/components/effects/fireworks', () => ({
|
||||||
AdvancedCameraCardEffectFireworks: vi.fn(() => createMockEffectComponent()),
|
AdvancedCameraCardEffectFireworks: vi.fn(function () {
|
||||||
|
return createMockEffectComponent();
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../../src/components/effects/check', () => ({
|
vi.mock('../../../src/components/effects/check', () => ({
|
||||||
AdvancedCameraCardEffectCheck: vi.fn(() => createMockEffectComponent()),
|
AdvancedCameraCardEffectCheck: vi.fn(function () {
|
||||||
|
return createMockEffectComponent();
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../../src/components/effects/ghost', () => ({
|
vi.mock('../../../src/components/effects/ghost', () => ({
|
||||||
AdvancedCameraCardEffectGhost: vi.fn(() => createMockEffectComponent()),
|
AdvancedCameraCardEffectGhost: vi.fn(function () {
|
||||||
|
return createMockEffectComponent();
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../../src/components/effects/hearts', () => ({
|
vi.mock('../../../src/components/effects/hearts', () => ({
|
||||||
AdvancedCameraCardEffectHearts: vi.fn(() => createMockEffectComponent()),
|
AdvancedCameraCardEffectHearts: vi.fn(function () {
|
||||||
|
return createMockEffectComponent();
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../../src/components/effects/shamrocks', () => ({
|
vi.mock('../../../src/components/effects/shamrocks', () => ({
|
||||||
AdvancedCameraCardEffectShamrocks: vi.fn(() => createMockEffectComponent()),
|
AdvancedCameraCardEffectShamrocks: vi.fn(function () {
|
||||||
|
return createMockEffectComponent();
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../../src/components/effects/snow', () => ({
|
vi.mock('../../../src/components/effects/snow', () => ({
|
||||||
AdvancedCameraCardEffectSnow: vi.fn(() => createMockEffectComponent()),
|
AdvancedCameraCardEffectSnow: vi.fn(function () {
|
||||||
|
return createMockEffectComponent();
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const createMockEffectComponent = (): EffectComponent => {
|
const createMockEffectComponent = (): EffectComponent => {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ vi.mock('../../../src/card-controller/fullscreen/factory');
|
|||||||
describe('FullscreenManager', () => {
|
describe('FullscreenManager', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should initialize', () => {
|
it('should initialize', () => {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ describe('ScreenfullFullScreenProvider', () => {
|
|||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should connect', () => {
|
describe('should connect', () => {
|
||||||
|
|||||||
@@ -35,11 +35,13 @@ describe('LazyLoadController', () => {
|
|||||||
Object.defineProperty(document, 'visibilityState', {
|
Object.defineProperty(document, 'visibilityState', {
|
||||||
value: 'visible',
|
value: 'visible',
|
||||||
writable: true,
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be unloaded by default', () => {
|
it('should be unloaded by default', () => {
|
||||||
@@ -281,6 +283,7 @@ describe('LazyLoadController', () => {
|
|||||||
Object.defineProperty(document, 'visibilityState', {
|
Object.defineProperty(document, 'visibilityState', {
|
||||||
value: 'hidden',
|
value: 'hidden',
|
||||||
writable: true,
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const controller = new LazyLoadController(createLitElement());
|
const controller = new LazyLoadController(createLitElement());
|
||||||
|
|||||||
+2
-2
@@ -7,7 +7,7 @@ class FakeMediaSource extends EventTarget {
|
|||||||
|
|
||||||
public addSourceBuffer = vi.fn();
|
public addSourceBuffer = vi.fn();
|
||||||
public setLiveSeekableRange = vi.fn();
|
public setLiveSeekableRange = vi.fn();
|
||||||
public static isTypeSupported = vi.fn<[string], boolean>(() => true);
|
public static isTypeSupported = vi.fn<(mimeType: string) => boolean>(() => true);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -19,7 +19,7 @@ class FakeManagedMediaSource extends EventTarget {
|
|||||||
public addSourceBuffer = vi.fn();
|
public addSourceBuffer = vi.fn();
|
||||||
public setLiveSeekableRange = vi.fn();
|
public setLiveSeekableRange = vi.fn();
|
||||||
public readyState: 'closed' | 'open' | 'ended' = 'closed';
|
public readyState: 'closed' | 'open' | 'ended' = 'closed';
|
||||||
public static isTypeSupported = vi.fn<[string], boolean>(() => true);
|
public static isTypeSupported = vi.fn<(mimeType: string) => boolean>(() => true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const createObjectURL = vi.fn(() => 'blob:fake-url');
|
const createObjectURL = vi.fn(() => 'blob:fake-url');
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ describe('Go2RTCSessionController', () => {
|
|||||||
cardWideConfig?: CardWideConfig | null;
|
cardWideConfig?: CardWideConfig | null;
|
||||||
}) => {
|
}) => {
|
||||||
const websockets: FakeWebSocket[] = [];
|
const websockets: FakeWebSocket[] = [];
|
||||||
const createWebSocket = vi.fn<[string], WebSocket>(() => {
|
const createWebSocket = vi.fn<(url: string) => WebSocket>(() => {
|
||||||
const websocket = new FakeWebSocket();
|
const websocket = new FakeWebSocket();
|
||||||
websockets.push(websocket);
|
websockets.push(websocket);
|
||||||
return websocket.asWebSocket();
|
return websocket.asWebSocket();
|
||||||
@@ -1060,7 +1060,7 @@ describe('Go2RTCSessionController', () => {
|
|||||||
|
|
||||||
it('should ignore callbacks fired while a binary source is constructed', () => {
|
it('should ignore callbacks fired while a binary source is constructed', () => {
|
||||||
const websockets: FakeWebSocket[] = [];
|
const websockets: FakeWebSocket[] = [];
|
||||||
const createWebSocket = vi.fn<[string], WebSocket>(() => {
|
const createWebSocket = vi.fn<(url: string) => WebSocket>(() => {
|
||||||
const websocket = new FakeWebSocket();
|
const websocket = new FakeWebSocket();
|
||||||
websockets.push(websocket);
|
websockets.push(websocket);
|
||||||
return websocket.asWebSocket();
|
return websocket.asWebSocket();
|
||||||
@@ -1095,7 +1095,7 @@ describe('Go2RTCSessionController', () => {
|
|||||||
|
|
||||||
it('should ignore callbacks fired while a WebRTC source is constructed', () => {
|
it('should ignore callbacks fired while a WebRTC source is constructed', () => {
|
||||||
const websockets: FakeWebSocket[] = [];
|
const websockets: FakeWebSocket[] = [];
|
||||||
const createWebSocket = vi.fn<[string], WebSocket>(() => {
|
const createWebSocket = vi.fn<(url: string) => WebSocket>(() => {
|
||||||
const websocket = new FakeWebSocket();
|
const websocket = new FakeWebSocket();
|
||||||
websockets.push(websocket);
|
websockets.push(websocket);
|
||||||
return websocket.asWebSocket();
|
return websocket.asWebSocket();
|
||||||
@@ -1129,7 +1129,7 @@ describe('Go2RTCSessionController', () => {
|
|||||||
|
|
||||||
it('should use the default binary source factory when none is injected', () => {
|
it('should use the default binary source factory when none is injected', () => {
|
||||||
const websockets: FakeWebSocket[] = [];
|
const websockets: FakeWebSocket[] = [];
|
||||||
const createWebSocket = vi.fn<[string], WebSocket>(() => {
|
const createWebSocket = vi.fn<(url: string) => WebSocket>(() => {
|
||||||
const websocket = new FakeWebSocket();
|
const websocket = new FakeWebSocket();
|
||||||
websockets.push(websocket);
|
websockets.push(websocket);
|
||||||
return websocket.asWebSocket();
|
return websocket.asWebSocket();
|
||||||
@@ -1160,7 +1160,7 @@ describe('Go2RTCSessionController', () => {
|
|||||||
it('should use the default WebRTC source factory when none is injected', () => {
|
it('should use the default WebRTC source factory when none is injected', () => {
|
||||||
vi.stubGlobal('RTCPeerConnection', FakeRTCPeerConnection);
|
vi.stubGlobal('RTCPeerConnection', FakeRTCPeerConnection);
|
||||||
const websockets: FakeWebSocket[] = [];
|
const websockets: FakeWebSocket[] = [];
|
||||||
const createWebSocket = vi.fn<[string], WebSocket>(() => {
|
const createWebSocket = vi.fn<(url: string) => WebSocket>(() => {
|
||||||
const websocket = new FakeWebSocket();
|
const websocket = new FakeWebSocket();
|
||||||
websockets.push(websocket);
|
websockets.push(websocket);
|
||||||
return websocket.asWebSocket();
|
return websocket.asWebSocket();
|
||||||
@@ -1187,7 +1187,7 @@ describe('Go2RTCSessionController', () => {
|
|||||||
|
|
||||||
it('should create an off-screen video with the default factory when none is injected', () => {
|
it('should create an off-screen video with the default factory when none is injected', () => {
|
||||||
const websockets: FakeWebSocket[] = [];
|
const websockets: FakeWebSocket[] = [];
|
||||||
const createWebSocket = vi.fn<[string], WebSocket>(() => {
|
const createWebSocket = vi.fn<(url: string) => WebSocket>(() => {
|
||||||
const websocket = new FakeWebSocket();
|
const websocket = new FakeWebSocket();
|
||||||
websockets.push(websocket);
|
websockets.push(websocket);
|
||||||
return websocket.asWebSocket();
|
return websocket.asWebSocket();
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ describe('SignalingChannel', () => {
|
|||||||
disconnectCallback?: () => void;
|
disconnectCallback?: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const websockets: FakeWebSocket[] = [];
|
const websockets: FakeWebSocket[] = [];
|
||||||
const createWebSocket = vi.fn<[string], WebSocket>(() => {
|
const createWebSocket = vi.fn<(url: string) => WebSocket>(() => {
|
||||||
const websocket = new FakeWebSocket();
|
const websocket = new FakeWebSocket();
|
||||||
websockets.push(websocket);
|
websockets.push(websocket);
|
||||||
return websocket.asWebSocket();
|
return websocket.asWebSocket();
|
||||||
@@ -238,7 +238,11 @@ describe('SignalingChannel', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should construct a real websocket by default', () => {
|
it('should construct a real websocket by default', () => {
|
||||||
const webSocketConstructor = vi.fn(() => new FakeWebSocket().asWebSocket());
|
// A mock implementation must be callable with `new`, so it cannot be an
|
||||||
|
// arrow function.
|
||||||
|
const webSocketConstructor = vi.fn(function () {
|
||||||
|
return new FakeWebSocket().asWebSocket();
|
||||||
|
});
|
||||||
vi.stubGlobal('WebSocket', webSocketConstructor);
|
vi.stubGlobal('WebSocket', webSocketConstructor);
|
||||||
const channel = new SignalingChannel('ws://host/api/ws', {});
|
const channel = new SignalingChannel('ws://host/api/ws', {});
|
||||||
channel.connect();
|
channel.connect();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ describe('MJPEGStreamSource', () => {
|
|||||||
const channel = new FakeStreamSourceChannel();
|
const channel = new FakeStreamSourceChannel();
|
||||||
const loadedCallback = vi.fn();
|
const loadedCallback = vi.fn();
|
||||||
const failedCallback = vi.fn();
|
const failedCallback = vi.fn();
|
||||||
const showFrame = vi.fn<[Blob], Promise<void>>(() => Promise.resolve());
|
const showFrame = vi.fn<(blob: Blob) => Promise<void>>(() => Promise.resolve());
|
||||||
|
|
||||||
const context: StreamSourceContext<ImageStreamTarget> = {
|
const context: StreamSourceContext<ImageStreamTarget> = {
|
||||||
target: { kind: 'image', showFrame },
|
target: { kind: 'image', showFrame },
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ describe('MP4StreamSource', () => {
|
|||||||
const channel = new FakeStreamSourceChannel();
|
const channel = new FakeStreamSourceChannel();
|
||||||
const loadedCallback = vi.fn();
|
const loadedCallback = vi.fn();
|
||||||
const failedCallback = vi.fn();
|
const failedCallback = vi.fn();
|
||||||
const showFrame = vi.fn<[Blob], Promise<void>>(() => Promise.resolve());
|
const showFrame = vi.fn<(blob: Blob) => Promise<void>>(() => Promise.resolve());
|
||||||
const context: StreamSourceContext<ImageStreamTarget> = {
|
const context: StreamSourceContext<ImageStreamTarget> = {
|
||||||
target: { kind: 'image', showFrame },
|
target: { kind: 'image', showFrame },
|
||||||
channel,
|
channel,
|
||||||
@@ -65,6 +65,7 @@ describe('MP4StreamSource', () => {
|
|||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should request the mp4 stream on start', () => {
|
it('should request the mp4 stream on start', () => {
|
||||||
@@ -217,6 +218,12 @@ describe('MP4StreamSource', () => {
|
|||||||
callbacks: { loadedCallback: vi.fn(), failedCallback: vi.fn() },
|
callbacks: { loadedCallback: vi.fn(), failedCallback: vi.fn() },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// This is the only test that reaches a real canvas element. jsdom does not
|
||||||
|
// implement `getContext` and writes an error to the console when it is
|
||||||
|
// called; the source treats a missing context as nothing to draw to avoid
|
||||||
|
// console spam.
|
||||||
|
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(null);
|
||||||
|
|
||||||
const createElement = vi.spyOn(document, 'createElement');
|
const createElement = vi.spyOn(document, 'createElement');
|
||||||
source.start();
|
source.start();
|
||||||
channel.binaryCallback?.(frame());
|
channel.binaryCallback?.(frame());
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ class FakeRTCTransceiver {
|
|||||||
public currentDirection: string;
|
public currentDirection: string;
|
||||||
public sender: {
|
public sender: {
|
||||||
track: FakeMediaStreamTrack | null;
|
track: FakeMediaStreamTrack | null;
|
||||||
replaceTrack: Mock<[MediaStreamTrack | null], Promise<void>>;
|
replaceTrack: Mock<(track: MediaStreamTrack | null) => Promise<void>>;
|
||||||
};
|
};
|
||||||
public receiver: { track: FakeMediaStreamTrack };
|
public receiver: { track: FakeMediaStreamTrack };
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ class FakeRTCTransceiver {
|
|||||||
this.currentDirection = direction;
|
this.currentDirection = direction;
|
||||||
this.sender = {
|
this.sender = {
|
||||||
track,
|
track,
|
||||||
replaceTrack: vi.fn<[MediaStreamTrack | null], Promise<void>>(() =>
|
replaceTrack: vi.fn<(track: MediaStreamTrack | null) => Promise<void>>(() =>
|
||||||
Promise.resolve(),
|
Promise.resolve(),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
@@ -241,9 +241,9 @@ export class FakeMediaSourceInstance implements MediaSourceInterface {
|
|||||||
public attach = vi.fn();
|
public attach = vi.fn();
|
||||||
public detach = vi.fn();
|
public detach = vi.fn();
|
||||||
public setLiveSeekableRange = vi.fn();
|
public setLiveSeekableRange = vi.fn();
|
||||||
public isOpen = vi.fn<[], boolean>(() => true);
|
public isOpen = vi.fn<() => boolean>(() => true);
|
||||||
public isTypeSupported = vi.fn<[string], boolean>(() => true);
|
public isTypeSupported = vi.fn<(mimeType: string) => boolean>(() => true);
|
||||||
public addSourceBuffer = vi.fn<[string], SourceBuffer>(() =>
|
public addSourceBuffer = vi.fn<(mimeType: string) => SourceBuffer>(() =>
|
||||||
this.sourceBuffer.asSourceBuffer(),
|
this.sourceBuffer.asSourceBuffer(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,10 @@ vi.mock('lodash-es', async () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const masonry = mock<ExtendedMasonry>();
|
const masonry = mock<ExtendedMasonry>();
|
||||||
|
// The source calls `new Masonry(...)`, and a mock implementation must be
|
||||||
|
// callable with `new`, so it cannot be an arrow function.
|
||||||
vi.mock('masonry-layout', () => ({
|
vi.mock('masonry-layout', () => ({
|
||||||
default: vi.fn().mockImplementation(() => {
|
default: vi.fn().mockImplementation(function () {
|
||||||
return masonry;
|
return masonry;
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const createEndpoint = (url: string, sign?: boolean): Endpoint => ({
|
|||||||
describe('SignedURLController', () => {
|
describe('SignedURLController', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should initialize correctly', () => {
|
it('should initialize correctly', () => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Panzoom, {
|
|||||||
type PanzoomEventDetail,
|
type PanzoomEventDetail,
|
||||||
type PanzoomObject,
|
type PanzoomObject,
|
||||||
} from '@dermotduffy/panzoom';
|
} from '@dermotduffy/panzoom';
|
||||||
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeAll, beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
|
||||||
import { mock, mockClear } from 'vitest-mock-extended';
|
import { mock, mockClear } from 'vitest-mock-extended';
|
||||||
|
|
||||||
import { ZoomController } from '../../../src/components-lib/zoom/zoom-controller';
|
import { ZoomController } from '../../../src/components-lib/zoom/zoom-controller';
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
createTouchEvent,
|
createTouchEvent,
|
||||||
requestAnimationFrameMock,
|
requestAnimationFrameMock,
|
||||||
ResizeObserverMock,
|
ResizeObserverMock,
|
||||||
|
stubMatchMedia,
|
||||||
} from '../../test-utils';
|
} from '../../test-utils';
|
||||||
|
|
||||||
vi.mock('@dermotduffy/panzoom');
|
vi.mock('@dermotduffy/panzoom');
|
||||||
@@ -37,7 +38,7 @@ const setElementToDefaultCardSize = (element: HTMLElement, multiple?: number): v
|
|||||||
|
|
||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
describe('ZoomController', () => {
|
describe('ZoomController', () => {
|
||||||
const mediaSpy = vi.spyOn(window, 'matchMedia');
|
let mediaSpy: Mock;
|
||||||
|
|
||||||
const createMockPanZoom = (): PanzoomObject => {
|
const createMockPanZoom = (): PanzoomObject => {
|
||||||
const panzoom = mock<PanzoomObject>();
|
const panzoom = mock<PanzoomObject>();
|
||||||
@@ -60,6 +61,7 @@ describe('ZoomController', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.mocked(Panzoom).mockReset();
|
vi.mocked(Panzoom).mockReset();
|
||||||
vi.mocked(global.ResizeObserver).mockClear();
|
vi.mocked(global.ResizeObserver).mockClear();
|
||||||
|
mediaSpy = stubMatchMedia();
|
||||||
mediaSpy.mockReturnValue(<MediaQueryList>{ matches: true });
|
mediaSpy.mockReturnValue(<MediaQueryList>{ matches: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { MediaQueryWatcher } from '../../../src/condition-trigger/common/media-query-watcher';
|
import { MediaQueryWatcher } from '../../../src/condition-trigger/common/media-query-watcher';
|
||||||
|
import { stubMatchMedia } from '../../test-utils';
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
describe('MediaQueryWatcher', () => {
|
describe('MediaQueryWatcher', () => {
|
||||||
@@ -9,7 +10,7 @@ describe('MediaQueryWatcher', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should report whether the query matches', () => {
|
it('should report whether the query matches', () => {
|
||||||
vi.spyOn(window, 'matchMedia').mockReturnValue({
|
stubMatchMedia().mockReturnValue({
|
||||||
matches: true,
|
matches: true,
|
||||||
} as unknown as MediaQueryList);
|
} as unknown as MediaQueryList);
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ describe('MediaQueryWatcher', () => {
|
|||||||
|
|
||||||
it('should invoke the callback on a media-query change', () => {
|
it('should invoke the callback on a media-query change', () => {
|
||||||
const addEventListener = vi.fn();
|
const addEventListener = vi.fn();
|
||||||
vi.spyOn(window, 'matchMedia').mockReturnValue({
|
stubMatchMedia().mockReturnValue({
|
||||||
addEventListener,
|
addEventListener,
|
||||||
removeEventListener: vi.fn(),
|
removeEventListener: vi.fn(),
|
||||||
} as unknown as MediaQueryList);
|
} as unknown as MediaQueryList);
|
||||||
@@ -35,7 +36,7 @@ describe('MediaQueryWatcher', () => {
|
|||||||
it('should stop listening and ignore changes after teardown', () => {
|
it('should stop listening and ignore changes after teardown', () => {
|
||||||
const addEventListener = vi.fn();
|
const addEventListener = vi.fn();
|
||||||
const removeEventListener = vi.fn();
|
const removeEventListener = vi.fn();
|
||||||
vi.spyOn(window, 'matchMedia').mockReturnValue({
|
stubMatchMedia().mockReturnValue({
|
||||||
addEventListener,
|
addEventListener,
|
||||||
removeEventListener,
|
removeEventListener,
|
||||||
} as unknown as MediaQueryList);
|
} as unknown as MediaQueryList);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
createMockTemplateRenderer,
|
createMockTemplateRenderer,
|
||||||
createStateEntity,
|
createStateEntity,
|
||||||
stubConnectedHomeAssistant,
|
stubConnectedHomeAssistant,
|
||||||
|
stubMatchMedia,
|
||||||
} from '../../test-utils';
|
} from '../../test-utils';
|
||||||
|
|
||||||
// A mock renderer for the orchestration tests, which never render templates.
|
// A mock renderer for the orchestration tests, which never render templates.
|
||||||
@@ -71,7 +72,7 @@ describe('ConditionsManager', () => {
|
|||||||
it('should re-evaluate and notify when a subscribed condition source changes', () => {
|
it('should re-evaluate and notify when a subscribed condition source changes', () => {
|
||||||
const addEventListener = vi.fn();
|
const addEventListener = vi.fn();
|
||||||
const removeEventListener = vi.fn();
|
const removeEventListener = vi.fn();
|
||||||
vi.spyOn(window, 'matchMedia')
|
stubMatchMedia()
|
||||||
.mockReturnValueOnce({
|
.mockReturnValueOnce({
|
||||||
addEventListener: addEventListener,
|
addEventListener: addEventListener,
|
||||||
removeEventListener: removeEventListener,
|
removeEventListener: removeEventListener,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { createConditionEvaluator } from '../../../../src/condition-trigger/conditions/factory';
|
import { createConditionEvaluator } from '../../../../src/condition-trigger/conditions/factory';
|
||||||
|
import { stubMatchMedia } from '../../../test-utils';
|
||||||
import { createEvaluatorContext } from './test-utils';
|
import { createEvaluatorContext } from './test-utils';
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
@@ -10,7 +11,7 @@ describe('screen condition', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should evaluate the media query', () => {
|
it('should evaluate the media query', () => {
|
||||||
vi.spyOn(window, 'matchMedia').mockReturnValue({
|
stubMatchMedia().mockReturnValue({
|
||||||
matches: true,
|
matches: true,
|
||||||
} as unknown as MediaQueryList);
|
} as unknown as MediaQueryList);
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ describe('screen condition', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not match or expose a source without a media query', () => {
|
it('should not match or expose a source without a media query', () => {
|
||||||
const matchMedia = vi.spyOn(window, 'matchMedia');
|
const matchMedia = stubMatchMedia();
|
||||||
const evaluator = createConditionEvaluator(
|
const evaluator = createConditionEvaluator(
|
||||||
{ condition: 'screen' as const },
|
{ condition: 'screen' as const },
|
||||||
createEvaluatorContext(),
|
createEvaluatorContext(),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, vi, type Mock } from 'vitest';
|
|||||||
|
|
||||||
import { ScreenTrigger } from '../../../../src/condition-trigger/triggers/triggers/screen';
|
import { ScreenTrigger } from '../../../../src/condition-trigger/triggers/triggers/screen';
|
||||||
import type { TriggerOfType } from '../../../../src/condition-trigger/triggers/triggers/types';
|
import type { TriggerOfType } from '../../../../src/condition-trigger/triggers/triggers/types';
|
||||||
|
import { stubMatchMedia } from '../../../test-utils';
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
describe('ScreenTrigger', () => {
|
describe('ScreenTrigger', () => {
|
||||||
@@ -10,7 +11,7 @@ describe('ScreenTrigger', () => {
|
|||||||
const removeEventListener = vi.fn();
|
const removeEventListener = vi.fn();
|
||||||
|
|
||||||
const mockMatchMedia = (): void => {
|
const mockMatchMedia = (): void => {
|
||||||
vi.spyOn(window, 'matchMedia').mockImplementation(
|
stubMatchMedia().mockImplementation(
|
||||||
() =>
|
() =>
|
||||||
({
|
({
|
||||||
get matches(): boolean {
|
get matches(): boolean {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { expect, it } from 'vitest';
|
import { expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { copyConfig } from '../../../src/config/management';
|
||||||
import { CASTING_PROFILE } from '../../../src/config/profiles/casting';
|
import { CASTING_PROFILE } from '../../../src/config/profiles/casting';
|
||||||
import { setProfiles } from '../../../src/config/profiles/set-profiles';
|
import { setProfiles } from '../../../src/config/profiles/set-profiles';
|
||||||
import { advancedCameraCardConfigSchema } from '../../../src/config/schema/types';
|
import { advancedCameraCardConfigSchema } from '../../../src/config/schema/types';
|
||||||
@@ -23,7 +24,10 @@ it('should contain expected defaults', () => {
|
|||||||
|
|
||||||
it('should be parseable after application', () => {
|
it('should be parseable after application', () => {
|
||||||
const rawInputConfig = createRawConfig();
|
const rawInputConfig = createRawConfig();
|
||||||
const parsedConfig = advancedCameraCardConfigSchema.parse(rawInputConfig);
|
// `setProfiles` writes into the config it is given, and Zod hands out a
|
||||||
|
// single shared instance of each default object, so the parse result must be
|
||||||
|
// cloned before it is mutated.
|
||||||
|
const parsedConfig = copyConfig(advancedCameraCardConfigSchema.parse(rawInputConfig));
|
||||||
|
|
||||||
setProfiles(rawInputConfig, parsedConfig, ['casting']);
|
setProfiles(rawInputConfig, parsedConfig, ['casting']);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { expect, it } from 'vitest';
|
import { expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { copyConfig } from '../../../src/config/management';
|
||||||
import { DOORBELL_PROFILE } from '../../../src/config/profiles/doorbell';
|
import { DOORBELL_PROFILE } from '../../../src/config/profiles/doorbell';
|
||||||
import { setProfiles } from '../../../src/config/profiles/set-profiles';
|
import { setProfiles } from '../../../src/config/profiles/set-profiles';
|
||||||
import { advancedCameraCardConfigSchema } from '../../../src/config/schema/types';
|
import { advancedCameraCardConfigSchema } from '../../../src/config/schema/types';
|
||||||
@@ -17,7 +18,10 @@ it('should contain expected defaults', () => {
|
|||||||
|
|
||||||
it('should be parseable after application', () => {
|
it('should be parseable after application', () => {
|
||||||
const rawInputConfig = createRawConfig();
|
const rawInputConfig = createRawConfig();
|
||||||
const parsedConfig = advancedCameraCardConfigSchema.parse(rawInputConfig);
|
// `setProfiles` writes into the config it is given, and Zod hands out a
|
||||||
|
// single shared instance of each default object, so the parse result must be
|
||||||
|
// cloned before it is mutated.
|
||||||
|
const parsedConfig = copyConfig(advancedCameraCardConfigSchema.parse(rawInputConfig));
|
||||||
|
|
||||||
setProfiles(rawInputConfig, parsedConfig, ['doorbell']);
|
setProfiles(rawInputConfig, parsedConfig, ['doorbell']);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { expect, it } from 'vitest';
|
import { expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { copyConfig } from '../../../src/config/management';
|
||||||
import { LOW_PERFORMANCE_PROFILE } from '../../../src/config/profiles/low-performance';
|
import { LOW_PERFORMANCE_PROFILE } from '../../../src/config/profiles/low-performance';
|
||||||
import { setProfiles } from '../../../src/config/profiles/set-profiles';
|
import { setProfiles } from '../../../src/config/profiles/set-profiles';
|
||||||
import { advancedCameraCardConfigSchema } from '../../../src/config/schema/types';
|
import { advancedCameraCardConfigSchema } from '../../../src/config/schema/types';
|
||||||
@@ -53,7 +54,10 @@ it('should contain expected defaults', () => {
|
|||||||
|
|
||||||
it('should be parseable after application', () => {
|
it('should be parseable after application', () => {
|
||||||
const rawInputConfig = createRawConfig();
|
const rawInputConfig = createRawConfig();
|
||||||
const parsedConfig = advancedCameraCardConfigSchema.parse(rawInputConfig);
|
// `setProfiles` writes into the config it is given, and Zod hands out a
|
||||||
|
// single shared instance of each default object, so the parse result must be
|
||||||
|
// cloned before it is mutated.
|
||||||
|
const parsedConfig = copyConfig(advancedCameraCardConfigSchema.parse(rawInputConfig));
|
||||||
|
|
||||||
setProfiles(rawInputConfig, parsedConfig, ['low-performance']);
|
setProfiles(rawInputConfig, parsedConfig, ['low-performance']);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { expect, it } from 'vitest';
|
import { expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { copyConfig } from '../../../src/config/management';
|
||||||
import { SCRUBBING_PROFILE } from '../../../src/config/profiles/scrubbing';
|
import { SCRUBBING_PROFILE } from '../../../src/config/profiles/scrubbing';
|
||||||
import { setProfiles } from '../../../src/config/profiles/set-profiles';
|
import { setProfiles } from '../../../src/config/profiles/set-profiles';
|
||||||
import { advancedCameraCardConfigSchema } from '../../../src/config/schema/types';
|
import { advancedCameraCardConfigSchema } from '../../../src/config/schema/types';
|
||||||
@@ -18,7 +19,10 @@ it('should contain expected defaults', () => {
|
|||||||
|
|
||||||
it('should be parseable after application', () => {
|
it('should be parseable after application', () => {
|
||||||
const rawInputConfig = createRawConfig();
|
const rawInputConfig = createRawConfig();
|
||||||
const parsedConfig = advancedCameraCardConfigSchema.parse(rawInputConfig);
|
// `setProfiles` writes into the config it is given, and Zod hands out a
|
||||||
|
// single shared instance of each default object, so the parse result must be
|
||||||
|
// cloned before it is mutated.
|
||||||
|
const parsedConfig = copyConfig(advancedCameraCardConfigSchema.parse(rawInputConfig));
|
||||||
|
|
||||||
setProfiles(rawInputConfig, parsedConfig, ['low-performance']);
|
setProfiles(rawInputConfig, parsedConfig, ['low-performance']);
|
||||||
|
|
||||||
|
|||||||
+26
-13
@@ -6,7 +6,7 @@ import {
|
|||||||
} from 'home-assistant-js-websocket';
|
} from 'home-assistant-js-websocket';
|
||||||
import type { LitElement } from 'lit';
|
import type { LitElement } from 'lit';
|
||||||
import screenfull from 'screenfull';
|
import screenfull from 'screenfull';
|
||||||
import { expect, vi } from 'vitest';
|
import { expect, onTestFinished, vi, type Mock } from 'vitest';
|
||||||
import { mock } from 'vitest-mock-extended';
|
import { mock } from 'vitest-mock-extended';
|
||||||
|
|
||||||
import { Camera } from '../src/camera-manager/camera';
|
import { Camera } from '../src/camera-manager/camera';
|
||||||
@@ -577,23 +577,35 @@ export class TestViewMedia extends ViewMedia implements EventViewMedia, ReviewVi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ResizeObserverMock = vi.fn(() => ({
|
// jsdom does not implement `window.matchMedia`, so it has to be installed
|
||||||
disconnect: vi.fn(),
|
// before a test can control what it returns. Must be called from inside a test
|
||||||
observe: vi.fn(),
|
// or a test hook, as the stub is removed once the test finishes.
|
||||||
unobserve: vi.fn(),
|
export const stubMatchMedia = (): Mock => {
|
||||||
}));
|
const matchMedia = vi.fn();
|
||||||
|
vi.stubGlobal('matchMedia', matchMedia);
|
||||||
|
onTestFinished(() => {
|
||||||
|
// There is no singular unstubGlobal.
|
||||||
|
Reflect.deleteProperty(globalThis, 'matchMedia');
|
||||||
|
});
|
||||||
|
return matchMedia;
|
||||||
|
};
|
||||||
|
|
||||||
export const IntersectionObserverMock = vi.fn(() => ({
|
// A mock implementation must be callable with `new`, so it cannot be an arrow
|
||||||
|
// function.
|
||||||
|
const createObserverMock = () =>
|
||||||
|
vi.fn(function () {
|
||||||
|
return {
|
||||||
disconnect: vi.fn(),
|
disconnect: vi.fn(),
|
||||||
observe: vi.fn(),
|
observe: vi.fn(),
|
||||||
unobserve: vi.fn(),
|
unobserve: vi.fn(),
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
|
|
||||||
export const MutationObserverMock = vi.fn(() => ({
|
export const ResizeObserverMock = createObserverMock();
|
||||||
disconnect: vi.fn(),
|
|
||||||
observe: vi.fn(),
|
export const IntersectionObserverMock = createObserverMock();
|
||||||
unobserve: vi.fn(),
|
|
||||||
}));
|
export const MutationObserverMock = createObserverMock();
|
||||||
|
|
||||||
export const requestAnimationFrameMock = (callback: FrameRequestCallback) => {
|
export const requestAnimationFrameMock = (callback: FrameRequestCallback) => {
|
||||||
callback(new Date().getTime());
|
callback(new Date().getTime());
|
||||||
@@ -650,6 +662,7 @@ export const callVisibilityHandler = async (visible: boolean): Promise<void> =>
|
|||||||
Object.defineProperty(document, 'visibilityState', {
|
Object.defineProperty(document, 'visibilityState', {
|
||||||
value: visible ? 'visible' : 'hidden',
|
value: visible ? 'visible' : 'hidden',
|
||||||
writable: true,
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mock = vi.mocked(global.document.addEventListener).mock;
|
const mock = vi.mocked(global.document.addEventListener).mock;
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import {
|
|||||||
setOrRemoveAttribute,
|
setOrRemoveAttribute,
|
||||||
setOrRemoveStyleProperty,
|
setOrRemoveStyleProperty,
|
||||||
} from '../../src/utils/basic.js';
|
} from '../../src/utils/basic.js';
|
||||||
import { createSlot, createSlotHost } from '../test-utils.js';
|
import { createSlot, createSlotHost, stubMatchMedia } from '../test-utils.js';
|
||||||
|
|
||||||
describe('prettifyTitle', () => {
|
describe('prettifyTitle', () => {
|
||||||
it('should return undefined when passed undefined', () => {
|
it('should return undefined when passed undefined', () => {
|
||||||
@@ -146,11 +146,11 @@ describe('isHoverableDevice', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return hoverable', () => {
|
it('should return hoverable', () => {
|
||||||
vi.spyOn(window, 'matchMedia').mockReturnValue(<MediaQueryList>{ matches: true });
|
stubMatchMedia().mockReturnValue(<MediaQueryList>{ matches: true });
|
||||||
expect(isHoverableDevice()).toBeTruthy();
|
expect(isHoverableDevice()).toBeTruthy();
|
||||||
});
|
});
|
||||||
it('should return not hoverable', () => {
|
it('should return not hoverable', () => {
|
||||||
vi.spyOn(window, 'matchMedia').mockReturnValue(<MediaQueryList>{ matches: false });
|
stubMatchMedia().mockReturnValue(<MediaQueryList>{ matches: false });
|
||||||
expect(isHoverableDevice()).toBeFalsy();
|
expect(isHoverableDevice()).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -71,7 +71,11 @@ describe('thumbnail utilities', () => {
|
|||||||
});
|
});
|
||||||
vi.stubGlobal(
|
vi.stubGlobal(
|
||||||
'FileReader',
|
'FileReader',
|
||||||
vi.fn(() => mockFileReader),
|
// The source calls `new FileReader()`, and a mock implementation must be
|
||||||
|
// callable with `new`, so it cannot be an arrow function.
|
||||||
|
vi.fn(function () {
|
||||||
|
return mockFileReader;
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
createFetchThumbnailTask(
|
createFetchThumbnailTask(
|
||||||
@@ -129,7 +133,9 @@ describe('thumbnail utilities', () => {
|
|||||||
const mockFileReader = mock<FileReader>();
|
const mockFileReader = mock<FileReader>();
|
||||||
vi.stubGlobal(
|
vi.stubGlobal(
|
||||||
'FileReader',
|
'FileReader',
|
||||||
vi.fn(() => mockFileReader),
|
vi.fn(function () {
|
||||||
|
return mockFileReader;
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
createFetchThumbnailTask(
|
createFetchThumbnailTask(
|
||||||
@@ -166,7 +172,9 @@ describe('thumbnail utilities', () => {
|
|||||||
});
|
});
|
||||||
vi.stubGlobal(
|
vi.stubGlobal(
|
||||||
'FileReader',
|
'FileReader',
|
||||||
vi.fn(() => mockFileReader),
|
vi.fn(function () {
|
||||||
|
return mockFileReader;
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
createFetchThumbnailTask(
|
createFetchThumbnailTask(
|
||||||
|
|||||||
+64
-27
@@ -23,23 +23,6 @@ const EXCLUSIONS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const TEST_DIRECTORY = 'tests';
|
const TEST_DIRECTORY = 'tests';
|
||||||
const INCLUSIONS = [`${TEST_DIRECTORY}/**/*.test.ts`];
|
|
||||||
|
|
||||||
// For test performance reasons, tests are split into two groups:
|
|
||||||
//
|
|
||||||
// - `shared`: run non-isolated, so they share one loaded copy of the source
|
|
||||||
// tree instead of each re-importing it.
|
|
||||||
// - `isolated`: given their own module registry per file, because sharing one
|
|
||||||
// would change their behaviour. A file calling `vi.mock()` cannot replace a
|
|
||||||
// module an earlier file already loaded unmocked; a file loading the template
|
|
||||||
// renderer reads browser globals as the renderer loads; and a file needing a
|
|
||||||
// DOM leaves modules holding a `window` that is torn down when it finishes,
|
|
||||||
// which breaks any later file that reaches one of those modules.
|
|
||||||
//
|
|
||||||
// Absent this variable every test file runs isolated, which is both the safe
|
|
||||||
// default and what coverage requires (istanbul only counts a module's top-level
|
|
||||||
// code the first time it runs).
|
|
||||||
const GROUP = process.env.VITEST_GROUP;
|
|
||||||
|
|
||||||
const findTestFiles = (directory: string): string[] => {
|
const findTestFiles = (directory: string): string[] => {
|
||||||
const files: string[] = [];
|
const files: string[] = [];
|
||||||
@@ -54,16 +37,21 @@ const findTestFiles = (directory: string): string[] => {
|
|||||||
return files;
|
return files;
|
||||||
};
|
};
|
||||||
|
|
||||||
const REQUIRE_ISOLATION_REGEXP =
|
// Each test file is read and sorted into one of the projects below, which
|
||||||
/\bvi\.(do)?mock\(|loadRenderer|stubConnectedHomeAssistant|@vitest-environment\s+jsdom/;
|
// explain what these two properties cost.
|
||||||
|
const REQUIRE_ISOLATION_REGEXP = /\bvi\.(do)?mock\(/;
|
||||||
|
const REQUIRE_DOM_REGEXP = /@vitest-environment\s+jsdom/;
|
||||||
|
|
||||||
const getGroup = (file: string): string =>
|
const getGroup = (file: string): string => {
|
||||||
REQUIRE_ISOLATION_REGEXP.test(readFileSync(file, 'utf-8')) ? 'isolated' : 'shared';
|
const contents = readFileSync(file, 'utf-8');
|
||||||
|
if (REQUIRE_ISOLATION_REGEXP.test(contents)) {
|
||||||
|
return 'isolated';
|
||||||
|
}
|
||||||
|
return REQUIRE_DOM_REGEXP.test(contents) ? 'shared-jsdom' : 'shared-node';
|
||||||
|
};
|
||||||
|
|
||||||
const getInclusions = (): string[] =>
|
const getInclusions = (group: string): string[] =>
|
||||||
GROUP
|
findTestFiles(TEST_DIRECTORY).filter((file) => getGroup(file) === group);
|
||||||
? findTestFiles(TEST_DIRECTORY).filter((file) => getGroup(file) === GROUP)
|
|
||||||
: INCLUSIONS;
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [svgPath()],
|
plugins: [svgPath()],
|
||||||
@@ -75,13 +63,62 @@ export default defineConfig({
|
|||||||
inline: ['ha-nunjucks', 'ts-py-datetime'],
|
inline: ['ha-nunjucks', 'ts-py-datetime'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: getInclusions(),
|
|
||||||
|
|
||||||
// Forked child processes start and tear down faster here than worker
|
// Forked child processes start and tear down faster here than worker
|
||||||
// threads, which matters when every test file needs a fresh one.
|
// threads, which matters when every test file needs a fresh one.
|
||||||
pool: 'forks',
|
pool: 'forks',
|
||||||
|
|
||||||
isolate: !GROUP || GROUP === 'isolated',
|
// Importing the source tree costs far more than running the tests in it, so
|
||||||
|
// files are grouped by whether they can share one loaded copy of it. Each
|
||||||
|
// project below is a group that can, or the one that cannot.
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
extends: true,
|
||||||
|
test: {
|
||||||
|
// Nothing stops these sharing, so they run against a single loaded
|
||||||
|
// copy of the source tree. Most of the suite is here, and anything
|
||||||
|
// moved out of here pays to import that tree again.
|
||||||
|
name: 'shared-node',
|
||||||
|
include: getInclusions('shared-node'),
|
||||||
|
isolate: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extends: true,
|
||||||
|
test: {
|
||||||
|
// These share too, but only with each other. A module loaded under
|
||||||
|
// jsdom holds a reference to that DOM, so a worker that moves from a
|
||||||
|
// DOM file to a non-DOM one drops its loaded copy of the source tree
|
||||||
|
// and imports it again. A project's files are spread across workers
|
||||||
|
// without regard to environment, so putting both kinds in one project
|
||||||
|
// leaves every worker alternating between them and reloading as it
|
||||||
|
// goes, and the more workers there are the more often that happens. A
|
||||||
|
// project per environment hands each worker a run of files that all
|
||||||
|
// want the same one. The file-level `@vitest-environment jsdom`
|
||||||
|
// comments are what sort files into here, and are redundant once they
|
||||||
|
// arrive.
|
||||||
|
//
|
||||||
|
// They share one `document` as well, so a file that redefines part of
|
||||||
|
// it must leave the property configurable for the files that follow.
|
||||||
|
name: 'shared-jsdom',
|
||||||
|
include: getInclusions('shared-jsdom'),
|
||||||
|
environment: 'jsdom',
|
||||||
|
isolate: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extends: true,
|
||||||
|
test: {
|
||||||
|
// `vi.mock()` cannot replace a module that an earlier file already
|
||||||
|
// loaded unmocked, so these cannot share a loaded source tree with
|
||||||
|
// anything, including each other. They get a registry per file and
|
||||||
|
// pay the full import cost.
|
||||||
|
name: 'isolated',
|
||||||
|
include: getInclusions('isolated'),
|
||||||
|
isolate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
// Hide console writing to keep output clean, usual sources of noise:
|
// Hide console writing to keep output clean, usual sources of noise:
|
||||||
// - Unnecessary Lit dev-mode warnings.
|
// - Unnecessary Lit dev-mode warnings.
|
||||||
|
|||||||
Reference in New Issue
Block a user