chore: Upgrade to Vitest 4 and regroup tests for performance (#2618)

This commit is contained in:
Dermot Duffy
2026-07-26 11:28:58 -07:00
committed by GitHub
parent a0fd464d23
commit 0c3d46ad3d
36 changed files with 1041 additions and 1207 deletions
@@ -208,6 +208,7 @@ describe('FrigateQueryResultsClassifier', () => {
describe('FrigateCameraManagerEngine', () => {
afterEach(() => {
vi.restoreAllMocks();
vi.clearAllMocks();
});
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 { createLogAction } from '../../../../src/utils/action';
@@ -6,6 +6,10 @@ import { arrayify } from '../../../../src/utils/basic';
import { createCardAPI } from '../../../test-utils';
describe('ActionSet', () => {
afterEach(() => {
vi.restoreAllMocks();
});
const createAPI = () => {
const api = createCardAPI();
vi.mocked(api.getLockManager().getAllowedActions).mockImplementation((actions) =>
+6 -2
View File
@@ -29,9 +29,13 @@ import {
// Replace Ringtone with a fresh `mock<Ringtone>()` per construction so each
// 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', () => ({
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
+18 -6
View File
@@ -12,20 +12,32 @@ import type { RingtoneConfig } from '../../../src/config/schema/live';
// Each tone constructor returns a fresh `mock<>()` per `new` call; the mock
// implementation persists across `vi.clearAllMocks()` (which only clears call
// 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', () => ({
ChimeTone: vi.fn().mockImplementation(() => mock<ChimeTone>()),
ChimeTone: vi.fn().mockImplementation(function () {
return mock<ChimeTone>();
}),
}));
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', () => ({
ArpeggioTone: vi.fn().mockImplementation(() => mock<ArpeggioTone>()),
ArpeggioTone: vi.fn().mockImplementation(function () {
return mock<ArpeggioTone>();
}),
}));
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', () => ({
CustomTone: vi.fn().mockImplementation(() => mock<CustomTone>()),
CustomTone: vi.fn().mockImplementation(function () {
return mock<CustomTone>();
}),
}));
// Returns the most recently constructed instance of a mocked class.
@@ -187,7 +199,7 @@ describe('lock', () => {
describe('natural finish', () => {
it('should release the lock when the tone fires its finished handler', () => {
let finishedHandler: (() => void) | undefined;
vi.mocked(ChimeTone).mockImplementationOnce(() => {
vi.mocked(ChimeTone).mockImplementationOnce(function () {
const tone = mock<ChimeTone>();
vi.mocked(tone.start).mockImplementation((handler) => {
finishedHandler = handler;
@@ -8,7 +8,7 @@ interface AudioMocks {
// and pushed here in construction order, so tests can dispatch real events
// and read real properties (`loop`, `currentTime`, etc.) on the instances.
instances: HTMLAudioElement[];
ctor: Mock<[string?], HTMLAudioElement>;
ctor: Mock<(url?: string) => HTMLAudioElement>;
}
// Uses real jsdom HTMLAudioElement instances and only stubs the parts jsdom
@@ -33,7 +33,10 @@ const useAudioElementMocks = (): AudioMocks => {
vi.spyOn(HTMLMediaElement.prototype, 'pause').mockImplementation(() => {});
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);
handle.instances.push(audio);
return audio;
@@ -10,7 +10,7 @@ import { mock, type MockProxy } from 'vitest-mock-extended';
// over the same observable state.
interface AudioMocks {
audioContext: MockProxy<AudioContext>;
audioContextCtor: Mock<[], MockProxy<AudioContext>>;
audioContextCtor: Mock<() => MockProxy<AudioContext>>;
// Filled in the order `createOscillator()` / `createGain()` were called.
oscillators: MockProxy<OscillatorNode>[];
@@ -73,7 +73,11 @@ export const useAudioMocks = (): AudioMocks => {
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);
});
@@ -5,28 +5,43 @@ import type { EffectComponent } from '../../../src/card-controller/effects/types
import type { EffectName } from '../../../src/types';
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', () => ({
AdvancedCameraCardEffectFireworks: vi.fn(() => createMockEffectComponent()),
AdvancedCameraCardEffectFireworks: vi.fn(function () {
return createMockEffectComponent();
}),
}));
vi.mock('../../../src/components/effects/check', () => ({
AdvancedCameraCardEffectCheck: vi.fn(() => createMockEffectComponent()),
AdvancedCameraCardEffectCheck: vi.fn(function () {
return createMockEffectComponent();
}),
}));
vi.mock('../../../src/components/effects/ghost', () => ({
AdvancedCameraCardEffectGhost: vi.fn(() => createMockEffectComponent()),
AdvancedCameraCardEffectGhost: vi.fn(function () {
return createMockEffectComponent();
}),
}));
vi.mock('../../../src/components/effects/hearts', () => ({
AdvancedCameraCardEffectHearts: vi.fn(() => createMockEffectComponent()),
AdvancedCameraCardEffectHearts: vi.fn(function () {
return createMockEffectComponent();
}),
}));
vi.mock('../../../src/components/effects/shamrocks', () => ({
AdvancedCameraCardEffectShamrocks: vi.fn(() => createMockEffectComponent()),
AdvancedCameraCardEffectShamrocks: vi.fn(function () {
return createMockEffectComponent();
}),
}));
vi.mock('../../../src/components/effects/snow', () => ({
AdvancedCameraCardEffectSnow: vi.fn(() => createMockEffectComponent()),
AdvancedCameraCardEffectSnow: vi.fn(function () {
return createMockEffectComponent();
}),
}));
const createMockEffectComponent = (): EffectComponent => {
@@ -11,6 +11,7 @@ vi.mock('../../../src/card-controller/fullscreen/factory');
describe('FullscreenManager', () => {
afterEach(() => {
vi.restoreAllMocks();
vi.clearAllMocks();
});
it('should initialize', () => {
@@ -29,6 +29,7 @@ describe('ScreenfullFullScreenProvider', () => {
afterEach(() => {
vi.restoreAllMocks();
vi.clearAllMocks();
});
describe('should connect', () => {
@@ -35,11 +35,13 @@ describe('LazyLoadController', () => {
Object.defineProperty(document, 'visibilityState', {
value: 'visible',
writable: true,
configurable: true,
});
});
afterEach(() => {
vi.restoreAllMocks();
vi.clearAllMocks();
});
it('should be unloaded by default', () => {
@@ -281,6 +283,7 @@ describe('LazyLoadController', () => {
Object.defineProperty(document, 'visibilityState', {
value: 'hidden',
writable: true,
configurable: true,
});
const controller = new LazyLoadController(createLitElement());
@@ -7,7 +7,7 @@ class FakeMediaSource extends EventTarget {
public addSourceBuffer = vi.fn();
public setLiveSeekableRange = vi.fn();
public static isTypeSupported = vi.fn<[string], boolean>(() => true);
public static isTypeSupported = vi.fn<(mimeType: string) => boolean>(() => true);
constructor() {
super();
@@ -19,7 +19,7 @@ class FakeManagedMediaSource extends EventTarget {
public addSourceBuffer = vi.fn();
public setLiveSeekableRange = vi.fn();
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');
@@ -100,7 +100,7 @@ describe('Go2RTCSessionController', () => {
cardWideConfig?: CardWideConfig | null;
}) => {
const websockets: FakeWebSocket[] = [];
const createWebSocket = vi.fn<[string], WebSocket>(() => {
const createWebSocket = vi.fn<(url: string) => WebSocket>(() => {
const websocket = new FakeWebSocket();
websockets.push(websocket);
return websocket.asWebSocket();
@@ -1060,7 +1060,7 @@ describe('Go2RTCSessionController', () => {
it('should ignore callbacks fired while a binary source is constructed', () => {
const websockets: FakeWebSocket[] = [];
const createWebSocket = vi.fn<[string], WebSocket>(() => {
const createWebSocket = vi.fn<(url: string) => WebSocket>(() => {
const websocket = new FakeWebSocket();
websockets.push(websocket);
return websocket.asWebSocket();
@@ -1095,7 +1095,7 @@ describe('Go2RTCSessionController', () => {
it('should ignore callbacks fired while a WebRTC source is constructed', () => {
const websockets: FakeWebSocket[] = [];
const createWebSocket = vi.fn<[string], WebSocket>(() => {
const createWebSocket = vi.fn<(url: string) => WebSocket>(() => {
const websocket = new FakeWebSocket();
websockets.push(websocket);
return websocket.asWebSocket();
@@ -1129,7 +1129,7 @@ describe('Go2RTCSessionController', () => {
it('should use the default binary source factory when none is injected', () => {
const websockets: FakeWebSocket[] = [];
const createWebSocket = vi.fn<[string], WebSocket>(() => {
const createWebSocket = vi.fn<(url: string) => WebSocket>(() => {
const websocket = new FakeWebSocket();
websockets.push(websocket);
return websocket.asWebSocket();
@@ -1160,7 +1160,7 @@ describe('Go2RTCSessionController', () => {
it('should use the default WebRTC source factory when none is injected', () => {
vi.stubGlobal('RTCPeerConnection', FakeRTCPeerConnection);
const websockets: FakeWebSocket[] = [];
const createWebSocket = vi.fn<[string], WebSocket>(() => {
const createWebSocket = vi.fn<(url: string) => WebSocket>(() => {
const websocket = new FakeWebSocket();
websockets.push(websocket);
return websocket.asWebSocket();
@@ -1187,7 +1187,7 @@ describe('Go2RTCSessionController', () => {
it('should create an off-screen video with the default factory when none is injected', () => {
const websockets: FakeWebSocket[] = [];
const createWebSocket = vi.fn<[string], WebSocket>(() => {
const createWebSocket = vi.fn<(url: string) => WebSocket>(() => {
const websocket = new FakeWebSocket();
websockets.push(websocket);
return websocket.asWebSocket();
@@ -9,7 +9,7 @@ describe('SignalingChannel', () => {
disconnectCallback?: () => void;
}) => {
const websockets: FakeWebSocket[] = [];
const createWebSocket = vi.fn<[string], WebSocket>(() => {
const createWebSocket = vi.fn<(url: string) => WebSocket>(() => {
const websocket = new FakeWebSocket();
websockets.push(websocket);
return websocket.asWebSocket();
@@ -238,7 +238,11 @@ describe('SignalingChannel', () => {
});
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);
const channel = new SignalingChannel('ws://host/api/ws', {});
channel.connect();
@@ -14,7 +14,7 @@ describe('MJPEGStreamSource', () => {
const channel = new FakeStreamSourceChannel();
const loadedCallback = 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> = {
target: { kind: 'image', showFrame },
@@ -32,7 +32,7 @@ describe('MP4StreamSource', () => {
const channel = new FakeStreamSourceChannel();
const loadedCallback = 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> = {
target: { kind: 'image', showFrame },
channel,
@@ -65,6 +65,7 @@ describe('MP4StreamSource', () => {
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
it('should request the mp4 stream on start', () => {
@@ -217,6 +218,12 @@ describe('MP4StreamSource', () => {
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');
source.start();
channel.binaryCallback?.(frame());
@@ -125,7 +125,7 @@ class FakeRTCTransceiver {
public currentDirection: string;
public sender: {
track: FakeMediaStreamTrack | null;
replaceTrack: Mock<[MediaStreamTrack | null], Promise<void>>;
replaceTrack: Mock<(track: MediaStreamTrack | null) => Promise<void>>;
};
public receiver: { track: FakeMediaStreamTrack };
@@ -134,7 +134,7 @@ class FakeRTCTransceiver {
this.currentDirection = direction;
this.sender = {
track,
replaceTrack: vi.fn<[MediaStreamTrack | null], Promise<void>>(() =>
replaceTrack: vi.fn<(track: MediaStreamTrack | null) => Promise<void>>(() =>
Promise.resolve(),
),
};
@@ -241,9 +241,9 @@ export class FakeMediaSourceInstance implements MediaSourceInterface {
public attach = vi.fn();
public detach = vi.fn();
public setLiveSeekableRange = vi.fn();
public isOpen = vi.fn<[], boolean>(() => true);
public isTypeSupported = vi.fn<[string], boolean>(() => true);
public addSourceBuffer = vi.fn<[string], SourceBuffer>(() =>
public isOpen = vi.fn<() => boolean>(() => true);
public isTypeSupported = vi.fn<(mimeType: string) => boolean>(() => true);
public addSourceBuffer = vi.fn<(mimeType: string) => SourceBuffer>(() =>
this.sourceBuffer.asSourceBuffer(),
);
@@ -24,8 +24,10 @@ vi.mock('lodash-es', async () => ({
}));
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', () => ({
default: vi.fn().mockImplementation(() => {
default: vi.fn().mockImplementation(function () {
return masonry;
}),
}));
@@ -23,6 +23,7 @@ const createEndpoint = (url: string, sign?: boolean): Endpoint => ({
describe('SignedURLController', () => {
afterEach(() => {
vi.restoreAllMocks();
vi.clearAllMocks();
});
it('should initialize correctly', () => {
@@ -2,7 +2,7 @@ import Panzoom, {
type PanzoomEventDetail,
type PanzoomObject,
} 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 { ZoomController } from '../../../src/components-lib/zoom/zoom-controller';
@@ -11,6 +11,7 @@ import {
createTouchEvent,
requestAnimationFrameMock,
ResizeObserverMock,
stubMatchMedia,
} from '../../test-utils';
vi.mock('@dermotduffy/panzoom');
@@ -37,7 +38,7 @@ const setElementToDefaultCardSize = (element: HTMLElement, multiple?: number): v
// @vitest-environment jsdom
describe('ZoomController', () => {
const mediaSpy = vi.spyOn(window, 'matchMedia');
let mediaSpy: Mock;
const createMockPanZoom = (): PanzoomObject => {
const panzoom = mock<PanzoomObject>();
@@ -60,6 +61,7 @@ describe('ZoomController', () => {
beforeEach(() => {
vi.mocked(Panzoom).mockReset();
vi.mocked(global.ResizeObserver).mockClear();
mediaSpy = stubMatchMedia();
mediaSpy.mockReturnValue(<MediaQueryList>{ matches: true });
});
@@ -1,6 +1,7 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { MediaQueryWatcher } from '../../../src/condition-trigger/common/media-query-watcher';
import { stubMatchMedia } from '../../test-utils';
// @vitest-environment jsdom
describe('MediaQueryWatcher', () => {
@@ -9,7 +10,7 @@ describe('MediaQueryWatcher', () => {
});
it('should report whether the query matches', () => {
vi.spyOn(window, 'matchMedia').mockReturnValue({
stubMatchMedia().mockReturnValue({
matches: true,
} as unknown as MediaQueryList);
@@ -18,7 +19,7 @@ describe('MediaQueryWatcher', () => {
it('should invoke the callback on a media-query change', () => {
const addEventListener = vi.fn();
vi.spyOn(window, 'matchMedia').mockReturnValue({
stubMatchMedia().mockReturnValue({
addEventListener,
removeEventListener: vi.fn(),
} as unknown as MediaQueryList);
@@ -35,7 +36,7 @@ describe('MediaQueryWatcher', () => {
it('should stop listening and ignore changes after teardown', () => {
const addEventListener = vi.fn();
const removeEventListener = vi.fn();
vi.spyOn(window, 'matchMedia').mockReturnValue({
stubMatchMedia().mockReturnValue({
addEventListener,
removeEventListener,
} as unknown as MediaQueryList);
@@ -8,6 +8,7 @@ import {
createMockTemplateRenderer,
createStateEntity,
stubConnectedHomeAssistant,
stubMatchMedia,
} from '../../test-utils';
// 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', () => {
const addEventListener = vi.fn();
const removeEventListener = vi.fn();
vi.spyOn(window, 'matchMedia')
stubMatchMedia()
.mockReturnValueOnce({
addEventListener: addEventListener,
removeEventListener: removeEventListener,
@@ -1,6 +1,7 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { createConditionEvaluator } from '../../../../src/condition-trigger/conditions/factory';
import { stubMatchMedia } from '../../../test-utils';
import { createEvaluatorContext } from './test-utils';
// @vitest-environment jsdom
@@ -10,7 +11,7 @@ describe('screen condition', () => {
});
it('should evaluate the media query', () => {
vi.spyOn(window, 'matchMedia').mockReturnValue({
stubMatchMedia().mockReturnValue({
matches: true,
} as unknown as MediaQueryList);
@@ -30,7 +31,7 @@ describe('screen condition', () => {
});
it('should not match or expose a source without a media query', () => {
const matchMedia = vi.spyOn(window, 'matchMedia');
const matchMedia = stubMatchMedia();
const evaluator = createConditionEvaluator(
{ condition: 'screen' as const },
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 type { TriggerOfType } from '../../../../src/condition-trigger/triggers/triggers/types';
import { stubMatchMedia } from '../../../test-utils';
// @vitest-environment jsdom
describe('ScreenTrigger', () => {
@@ -10,7 +11,7 @@ describe('ScreenTrigger', () => {
const removeEventListener = vi.fn();
const mockMatchMedia = (): void => {
vi.spyOn(window, 'matchMedia').mockImplementation(
stubMatchMedia().mockImplementation(
() =>
({
get matches(): boolean {
+5 -1
View File
@@ -1,5 +1,6 @@
import { expect, it } from 'vitest';
import { copyConfig } from '../../../src/config/management';
import { CASTING_PROFILE } from '../../../src/config/profiles/casting';
import { setProfiles } from '../../../src/config/profiles/set-profiles';
import { advancedCameraCardConfigSchema } from '../../../src/config/schema/types';
@@ -23,7 +24,10 @@ it('should contain expected defaults', () => {
it('should be parseable after application', () => {
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']);
+5 -1
View File
@@ -1,5 +1,6 @@
import { expect, it } from 'vitest';
import { copyConfig } from '../../../src/config/management';
import { DOORBELL_PROFILE } from '../../../src/config/profiles/doorbell';
import { setProfiles } from '../../../src/config/profiles/set-profiles';
import { advancedCameraCardConfigSchema } from '../../../src/config/schema/types';
@@ -17,7 +18,10 @@ it('should contain expected defaults', () => {
it('should be parseable after application', () => {
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']);
@@ -1,5 +1,6 @@
import { expect, it } from 'vitest';
import { copyConfig } from '../../../src/config/management';
import { LOW_PERFORMANCE_PROFILE } from '../../../src/config/profiles/low-performance';
import { setProfiles } from '../../../src/config/profiles/set-profiles';
import { advancedCameraCardConfigSchema } from '../../../src/config/schema/types';
@@ -53,7 +54,10 @@ it('should contain expected defaults', () => {
it('should be parseable after application', () => {
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']);
+5 -1
View File
@@ -1,5 +1,6 @@
import { expect, it } from 'vitest';
import { copyConfig } from '../../../src/config/management';
import { SCRUBBING_PROFILE } from '../../../src/config/profiles/scrubbing';
import { setProfiles } from '../../../src/config/profiles/set-profiles';
import { advancedCameraCardConfigSchema } from '../../../src/config/schema/types';
@@ -18,7 +19,10 @@ it('should contain expected defaults', () => {
it('should be parseable after application', () => {
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']);
+29 -16
View File
@@ -6,7 +6,7 @@ import {
} from 'home-assistant-js-websocket';
import type { LitElement } from 'lit';
import screenfull from 'screenfull';
import { expect, vi } from 'vitest';
import { expect, onTestFinished, vi, type Mock } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { Camera } from '../src/camera-manager/camera';
@@ -577,23 +577,35 @@ export class TestViewMedia extends ViewMedia implements EventViewMedia, ReviewVi
}
}
export const ResizeObserverMock = vi.fn(() => ({
disconnect: vi.fn(),
observe: vi.fn(),
unobserve: vi.fn(),
}));
// jsdom does not implement `window.matchMedia`, so it has to be installed
// before a test can control what it returns. Must be called from inside a test
// or a test hook, as the stub is removed once the test finishes.
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(() => ({
disconnect: vi.fn(),
observe: vi.fn(),
unobserve: 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(),
observe: vi.fn(),
unobserve: vi.fn(),
};
});
export const MutationObserverMock = vi.fn(() => ({
disconnect: vi.fn(),
observe: vi.fn(),
unobserve: vi.fn(),
}));
export const ResizeObserverMock = createObserverMock();
export const IntersectionObserverMock = createObserverMock();
export const MutationObserverMock = createObserverMock();
export const requestAnimationFrameMock = (callback: FrameRequestCallback) => {
callback(new Date().getTime());
@@ -650,6 +662,7 @@ export const callVisibilityHandler = async (visible: boolean): Promise<void> =>
Object.defineProperty(document, 'visibilityState', {
value: visible ? 'visible' : 'hidden',
writable: true,
configurable: true,
});
const mock = vi.mocked(global.document.addEventListener).mock;
+3 -3
View File
@@ -32,7 +32,7 @@ import {
setOrRemoveAttribute,
setOrRemoveStyleProperty,
} from '../../src/utils/basic.js';
import { createSlot, createSlotHost } from '../test-utils.js';
import { createSlot, createSlotHost, stubMatchMedia } from '../test-utils.js';
describe('prettifyTitle', () => {
it('should return undefined when passed undefined', () => {
@@ -146,11 +146,11 @@ describe('isHoverableDevice', () => {
});
it('should return hoverable', () => {
vi.spyOn(window, 'matchMedia').mockReturnValue(<MediaQueryList>{ matches: true });
stubMatchMedia().mockReturnValue(<MediaQueryList>{ matches: true });
expect(isHoverableDevice()).toBeTruthy();
});
it('should return not hoverable', () => {
vi.spyOn(window, 'matchMedia').mockReturnValue(<MediaQueryList>{ matches: false });
stubMatchMedia().mockReturnValue(<MediaQueryList>{ matches: false });
expect(isHoverableDevice()).toBeFalsy();
});
});
+11 -3
View File
@@ -71,7 +71,11 @@ describe('thumbnail utilities', () => {
});
vi.stubGlobal(
'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(
@@ -129,7 +133,9 @@ describe('thumbnail utilities', () => {
const mockFileReader = mock<FileReader>();
vi.stubGlobal(
'FileReader',
vi.fn(() => mockFileReader),
vi.fn(function () {
return mockFileReader;
}),
);
createFetchThumbnailTask(
@@ -166,7 +172,9 @@ describe('thumbnail utilities', () => {
});
vi.stubGlobal(
'FileReader',
vi.fn(() => mockFileReader),
vi.fn(function () {
return mockFileReader;
}),
);
createFetchThumbnailTask(