chore: Enforce a few house rules via eslint (#2539)
This commit is contained in:
committed by
dermotduffy
parent
5572ec728e
commit
921d45e577
@@ -46,7 +46,7 @@ describe('ActionHandler', () => {
|
||||
// A document-level mouseup should cancel the hold timer.
|
||||
document.dispatchEvent(new MouseEvent('mouseup'));
|
||||
|
||||
// Advance past hold time — hold should NOT have triggered.
|
||||
// Advance past hold time -- hold should NOT have triggered.
|
||||
vi.advanceTimersByTime(500);
|
||||
|
||||
element.dispatchEvent(new MouseEvent('click'));
|
||||
|
||||
@@ -501,7 +501,7 @@ describe('should handle ptz action', () => {
|
||||
|
||||
// Emulate the stop being called while the action is running, but before
|
||||
// the *next* timer is scheduled.
|
||||
let resolve: () => void;
|
||||
let resolve: () => void = () => {};
|
||||
const promise: Promise<void> = new Promise((_resolve) => {
|
||||
resolve = _resolve;
|
||||
});
|
||||
@@ -512,7 +512,7 @@ describe('should handle ptz action', () => {
|
||||
|
||||
action.stop();
|
||||
|
||||
resolve!();
|
||||
resolve();
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
|
||||
// There should be no additional calls.
|
||||
|
||||
@@ -10,13 +10,13 @@ interface AudioMocks {
|
||||
}
|
||||
|
||||
// Uses real jsdom HTMLAudioElement instances and only stubs the parts jsdom
|
||||
// can't fulfil (`play()` / `pause()` — no audio backend). Tests then exercise
|
||||
// observable behaviour: registered listeners fire via `dispatchEvent`,
|
||||
// property writes round-trip on the element, etc.
|
||||
// can't fulfil (`play()` / `pause()` -- no audio backend). Tests then exercise
|
||||
// observable behaviour: registered listeners fire via `dispatchEvent`, property
|
||||
// writes round-trip on the element, etc.
|
||||
//
|
||||
// Called once at module load. The `beforeEach`/`afterEach` calls inside this
|
||||
// helper register Vitest hooks at the file level — Vitest picks them up just
|
||||
// as if they had been written at the top of the file — so every test in the
|
||||
// helper register Vitest hooks at the file level -- Vitest picks them up just
|
||||
// as if they had been written at the top of the file -- so every test in the
|
||||
// file gets fresh mocks installed/torn down automatically.
|
||||
const useAudioElementMocks = (): AudioMocks => {
|
||||
const handle = { instances: [] as HTMLAudioElement[] } as AudioMocks;
|
||||
|
||||
@@ -5,8 +5,9 @@ import { mock, MockProxy } from 'vitest-mock-extended';
|
||||
// constructs an AudioContext directly; here we stub the global `AudioContext`
|
||||
// to return a deep-mocked instance whose `createOscillator()` / `createGain()`
|
||||
// factories return a *fresh* mock per call (not a shared one). This lets tests
|
||||
// assert against individual notes — e.g. `audio.oscillators[2].frequency.value`
|
||||
// — instead of having every call write over the same observable state.
|
||||
// assert against individual notes -- e.g.
|
||||
// `audio.oscillators[2].frequency.value` -- instead of having every call write
|
||||
// over the same observable state.
|
||||
interface AudioMocks {
|
||||
audioContext: MockProxy<AudioContext>;
|
||||
audioContextCtor: Mock<[], MockProxy<AudioContext>>;
|
||||
@@ -15,7 +16,7 @@ interface AudioMocks {
|
||||
oscillators: MockProxy<OscillatorNode>[];
|
||||
gains: MockProxy<GainNode>[];
|
||||
|
||||
// gainParams[i] is the AudioParam exposed by `gains[i].gain` — kept as a
|
||||
// gainParams[i] is the AudioParam exposed by `gains[i].gain` -- kept as a
|
||||
// parallel array because `mock<GainNode>()` doesn't auto-populate the
|
||||
// AudioParam interface as a callable deep mock (we wire it up by hand).
|
||||
gainParams: MockProxy<AudioParam>[];
|
||||
@@ -26,9 +27,9 @@ interface AudioMocks {
|
||||
// can read its fields after each `beforeEach` runs.
|
||||
//
|
||||
// Called once at module load. The `beforeEach`/`afterEach` calls inside this
|
||||
// helper register Vitest hooks at the file level — Vitest picks them up just as
|
||||
// if they had been written at the top of the file — so every test in the file
|
||||
// gets fresh mocks installed/torn down automatically.
|
||||
// helper register Vitest hooks at the file level -- Vitest picks them up just
|
||||
// as if they had been written at the top of the file -- so every test in the
|
||||
// file gets fresh mocks installed/torn down automatically.
|
||||
export const useAudioMocks = (): AudioMocks => {
|
||||
const audio = {} as AudioMocks;
|
||||
|
||||
@@ -64,7 +65,7 @@ export const useAudioMocks = (): AudioMocks => {
|
||||
|
||||
// The source chains `.catch(...)` on the close() Promise.
|
||||
vi.mocked(audio.audioContext.close).mockResolvedValue();
|
||||
// The base class reads `_currentTime` from this — left as a deep-mock spy
|
||||
// The base class reads `_currentTime` from this -- left as a deep-mock spy
|
||||
// by default it'd return a function, so anchor it at 0 for predictable
|
||||
// scheduling assertions.
|
||||
Object.defineProperty(audio.audioContext, 'currentTime', {
|
||||
|
||||
@@ -355,7 +355,7 @@ describe('OverridesManager', () => {
|
||||
it('with `invalid_type` surfacing the attempted value', () => {
|
||||
const error = runInvalidOverride((config) => {
|
||||
assert(config.overrides);
|
||||
// @ts-expect-error — intentionally invalid runtime value to trigger
|
||||
// @ts-expect-error -- intentionally invalid runtime value to trigger
|
||||
// Zod's `invalid_type` issue code.
|
||||
config.overrides[0].merge = 6;
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import screenfull from 'screenfull';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { ScreenfullFullScreenProvider } from '../../../../src/card-controller/fullscreen/screenfull';
|
||||
import { createCardAPI, setScreenfulEnabled } from '../../../test-utils';
|
||||
import { createCardAPI, flushPromises, setScreenfulEnabled } from '../../../test-utils';
|
||||
|
||||
vi.mock('screenfull', () => ({
|
||||
default: {
|
||||
@@ -21,6 +21,11 @@ const setScreenfulFullscreen = (fullscreen: boolean): void => {
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('ScreenfullFullScreenProvider', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(screenfull.request).mockResolvedValue();
|
||||
vi.mocked(screenfull.exit).mockResolvedValue();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
@@ -158,5 +163,30 @@ describe('ScreenfullFullScreenProvider', () => {
|
||||
expect(screenfull.request).not.toBeCalled();
|
||||
expect(screenfull.exit).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should swallow a rejected fullscreen request', async () => {
|
||||
setScreenfulEnabled(true);
|
||||
const api = createCardAPI();
|
||||
const element = document.createElement('div');
|
||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||
vi.mocked(screenfull.request).mockRejectedValue(new Error('denied'));
|
||||
const provider = new ScreenfullFullScreenProvider(api, vi.fn());
|
||||
|
||||
provider.setFullscreen(true);
|
||||
await flushPromises();
|
||||
|
||||
expect(screenfull.request).toBeCalledWith(element);
|
||||
});
|
||||
|
||||
it('should swallow a rejected fullscreen exit', async () => {
|
||||
setScreenfulEnabled(true);
|
||||
vi.mocked(screenfull.exit).mockRejectedValue(new Error('not in fullscreen'));
|
||||
const provider = new ScreenfullFullScreenProvider(createCardAPI(), vi.fn());
|
||||
|
||||
provider.setFullscreen(false);
|
||||
await flushPromises();
|
||||
|
||||
expect(screenfull.exit).toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,11 @@ import { mock } from 'vitest-mock-extended';
|
||||
import { WebkitFullScreenProvider } from '../../../../src/card-controller/fullscreen/webkit';
|
||||
import { ConditionStateManager } from '../../../../src/condition-trigger/conditions/state-manager';
|
||||
import { MediaPlayerController, WebkitHTMLVideoElement } from '../../../../src/types';
|
||||
import { createCardAPI, createMediaLoadedInfo } from '../../../test-utils';
|
||||
import {
|
||||
createCardAPI,
|
||||
createMediaLoadedInfo,
|
||||
flushPromises,
|
||||
} from '../../../test-utils';
|
||||
|
||||
const createWebkitVideoElement = (): HTMLVideoElement &
|
||||
Partial<WebkitHTMLVideoElement> => {
|
||||
@@ -242,7 +246,7 @@ describe('WebkitFullScreenProvider', () => {
|
||||
provider.connect();
|
||||
|
||||
const element = createWebkitVideoElement();
|
||||
element.play = vi.fn();
|
||||
element.play = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
const mediaPlayerController = createMediaPlayerController(element);
|
||||
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
||||
@@ -263,4 +267,30 @@ describe('WebkitFullScreenProvider', () => {
|
||||
|
||||
expect(element.play).toBeCalled();
|
||||
});
|
||||
|
||||
it('should swallow a rejected video replay after fullscreen ends', async () => {
|
||||
const api = createCardAPI();
|
||||
const stateManager = new ConditionStateManager();
|
||||
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
|
||||
|
||||
const provider = new WebkitFullScreenProvider(api, vi.fn());
|
||||
provider.connect();
|
||||
|
||||
const element = createWebkitVideoElement();
|
||||
element.play = vi.fn().mockRejectedValue(new Error('denied'));
|
||||
|
||||
const mediaPlayerController = createMediaPlayerController(element);
|
||||
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
||||
createMediaLoadedInfo({ mediaPlayerController }),
|
||||
);
|
||||
stateManager.setState({
|
||||
mediaLoadedInfo: createMediaLoadedInfo({ mediaPlayerController }),
|
||||
});
|
||||
|
||||
element.dispatchEvent(new Event('webkitendfullscreen'));
|
||||
vi.runOnlyPendingTimers();
|
||||
await flushPromises();
|
||||
|
||||
expect(element.play).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -145,7 +145,7 @@ describe('HASSManager', () => {
|
||||
startingHASS.config.state = STATE_STARTING;
|
||||
manager.setHASS(startingHASS);
|
||||
|
||||
// No reinit yet — HA isn't fully ready.
|
||||
// No reinit yet -- HA isn't fully ready.
|
||||
expect(api.getInitializationManager().uninitialize).not.toBeCalled();
|
||||
expect(api.getCameraManager().destroy).not.toBeCalled();
|
||||
|
||||
@@ -176,7 +176,7 @@ describe('HASSManager', () => {
|
||||
startingHASS.config.state = STATE_STARTING;
|
||||
manager.setHASS(startingHASS);
|
||||
|
||||
// WS came back but integrations still loading — wait for RUNNING.
|
||||
// WS came back but integrations still loading -- wait for RUNNING.
|
||||
expect(api.getInitializationManager().uninitialize).not.toBeCalled();
|
||||
expect(api.getCameraManager().destroy).not.toBeCalled();
|
||||
});
|
||||
@@ -190,9 +190,9 @@ describe('HASSManager', () => {
|
||||
readyHASS.config.state = STATE_RUNNING;
|
||||
manager.setHASS(readyHASS);
|
||||
|
||||
// First-ever hass set — there's no "previous not-ready state" to
|
||||
// transition from, so the normal first-load init flow applies and we
|
||||
// must not blow away cameras.
|
||||
// First-ever hass set -- there's no "previous not-ready state" to
|
||||
// transition from, so the normal first-load init flow applies and we must
|
||||
// not blow away cameras.
|
||||
expect(api.getInitializationManager().uninitialize).not.toBeCalled();
|
||||
expect(api.getCameraManager().destroy).not.toBeCalled();
|
||||
});
|
||||
|
||||
@@ -198,11 +198,11 @@ describe('IssueManager', () => {
|
||||
|
||||
const manager = new IssueManager(api);
|
||||
|
||||
// hasIssue returns true from the start — simulates trigger() having
|
||||
// already mutated state before detectDynamic snapshots. The
|
||||
// before/after check inside detectDynamic sees true→true (no
|
||||
// transition), but the presence comparison against ConditionState
|
||||
// must still detect the change.
|
||||
// hasIssue returns true from the start -- simulates trigger() having
|
||||
// already mutated state before detectDynamic snapshots. The before/after
|
||||
// check inside detectDynamic sees true→true (no transition), but the
|
||||
// presence comparison against ConditionState must still detect the
|
||||
// change.
|
||||
const description = createIssueDescription();
|
||||
const issue = createIssue('config_error', {
|
||||
hasIssue: vi.fn().mockReturnValue(true),
|
||||
@@ -219,7 +219,7 @@ describe('IssueManager', () => {
|
||||
expect(api.getCardElementManager().update).toBeCalled();
|
||||
});
|
||||
|
||||
it('should never auto-popup on trigger — non-full-card issues surface via the status-bar icon; user clicks to open', () => {
|
||||
it('should never auto-popup on trigger -- non-full-card issues surface via the status-bar icon; user clicks to open', () => {
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getConditionStateManager().getState).mockReturnValue({});
|
||||
|
||||
@@ -250,8 +250,8 @@ describe('IssueManager', () => {
|
||||
|
||||
expect(issue.retry).toBeCalled();
|
||||
|
||||
// Timer should have been reset — advancing less than retrySeconds
|
||||
// should not fire it again.
|
||||
// Timer should have been reset -- advancing less than retrySeconds should
|
||||
// not fire it again.
|
||||
assert(issue.retry);
|
||||
vi.mocked(issue.retry).mockClear();
|
||||
vi.advanceTimersByTime(500);
|
||||
@@ -312,11 +312,11 @@ describe('IssueManager', () => {
|
||||
});
|
||||
|
||||
it('should call update when an active issue swaps sub-states without changing the key set', () => {
|
||||
// Simulates ConnectionIssue going from 'lost' to 'starting': the
|
||||
// presence key set ({connection}) is identical, but the description
|
||||
// value differs. Because IssuePresence is a Map<key, description>,
|
||||
// the condition state diff sees the value-level change and fires
|
||||
// listeners — the IssueManager's own listener calls update().
|
||||
// Simulates ConnectionIssue going from 'lost' to 'starting': the presence
|
||||
// key set ({connection}) is identical, but the description value differs.
|
||||
// Because IssuePresence is a Map<key, description>, the condition state
|
||||
// diff sees the value-level change and fires listeners -- the
|
||||
// IssueManager's own listener calls update().
|
||||
const api = createCardAPI();
|
||||
|
||||
// Real ConditionStateManager so its isEqual-based diff actually runs.
|
||||
@@ -638,7 +638,7 @@ describe('IssueManager', () => {
|
||||
const { manager, issue } = createRetriableSetup({ retrySeconds: 'auto' });
|
||||
manager.evaluate();
|
||||
|
||||
// Run two retries — second delay should be 2x the first.
|
||||
// Run two retries -- second delay should be 2x the first.
|
||||
vi.advanceTimersByTime(RETRY_EXPONENTIAL_BASE_SECONDS * 0.75 * 1000);
|
||||
vi.advanceTimersByTime(RETRY_EXPONENTIAL_BASE_SECONDS * 2 * 0.75 * 1000);
|
||||
expect(issue.retry).toBeCalledTimes(2);
|
||||
@@ -671,16 +671,16 @@ describe('IssueManager', () => {
|
||||
});
|
||||
manager.evaluate();
|
||||
|
||||
// Three gated firings — each at the base delay (22.5s with 0.75 jitter).
|
||||
// If the counter were incrementing on gated fires, the second would be
|
||||
// at 45s and we'd never reach it after only 22.5s.
|
||||
// Three gated firings -- each at the base delay (22.5s with 0.75 jitter).
|
||||
// If the counter were incrementing on gated fires, the second would be at
|
||||
// 45s and we'd never reach it after only 22.5s.
|
||||
vi.advanceTimersByTime(RETRY_EXPONENTIAL_BASE_SECONDS * 0.75 * 1000);
|
||||
vi.advanceTimersByTime(RETRY_EXPONENTIAL_BASE_SECONDS * 0.75 * 1000);
|
||||
vi.advanceTimersByTime(RETRY_EXPONENTIAL_BASE_SECONDS * 0.75 * 1000);
|
||||
expect(issue.retry).not.toBeCalled();
|
||||
|
||||
// Clear the interaction. The next firing — still at the base delay —
|
||||
// is now allowed and the retry runs.
|
||||
// Clear the interaction. The next firing -- still at the base delay -- is
|
||||
// now allowed and the retry runs.
|
||||
vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(false);
|
||||
vi.advanceTimersByTime(RETRY_EXPONENTIAL_BASE_SECONDS * 0.75 * 1000);
|
||||
expect(issue.retry).toBeCalledTimes(1);
|
||||
@@ -854,7 +854,7 @@ describe('IssueManager', () => {
|
||||
vi.mocked(api.getConditionStateManager().getState).mockReturnValue({});
|
||||
|
||||
const manager = new IssueManager(api);
|
||||
// Plain Issue implementation — no optional methods installed.
|
||||
// Plain Issue implementation -- no optional methods installed.
|
||||
const issue: Issue = {
|
||||
key: 'config_error',
|
||||
hasIssue: () => false,
|
||||
|
||||
@@ -2,9 +2,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { MediaLoadIssue } from '../../../../src/card-controller/issues/issues/media-load';
|
||||
import { InternalCallbackActionConfig } from '../../../../src/config/schema/actions/custom/internal';
|
||||
import { IMAGE_VIEW_TARGET_ID_SENTINEL } from '../../../../src/view/target-id';
|
||||
import { View } from '../../../../src/view/view';
|
||||
import { createCardAPI, createMediaLoadedInfo } from '../../../test-utils';
|
||||
import { IMAGE_VIEW_TARGET_ID_SENTINEL } from '../../../../src/view/target-id';
|
||||
|
||||
const createAPI = () => createCardAPI();
|
||||
|
||||
@@ -133,7 +133,7 @@ describe('MediaLoadIssue', () => {
|
||||
vi.advanceTimersByTime(10000);
|
||||
expect(issue.hasIssue()).toBe(true);
|
||||
|
||||
// Same target, different media view — issue stays active.
|
||||
// Same target, different media view -- issue stays active.
|
||||
issue.detectDynamic({ targetID: 'camera-1', view: 'clip' });
|
||||
expect(issue.hasIssue()).toBe(true);
|
||||
});
|
||||
@@ -145,7 +145,7 @@ describe('MediaLoadIssue', () => {
|
||||
vi.advanceTimersByTime(10000);
|
||||
expect(issue.hasIssue()).toBe(true);
|
||||
|
||||
// Switch to camera-2 which has no error — should deactivate and start
|
||||
// Switch to camera-2 which has no error -- should deactivate and start
|
||||
// a fresh timer for the new target.
|
||||
issue.detectDynamic({ targetID: 'camera-2', view: 'live' });
|
||||
expect(issue.hasIssue()).toBe(false);
|
||||
@@ -164,7 +164,7 @@ describe('MediaLoadIssue', () => {
|
||||
issue.detectDynamic({ targetID: 'camera-1', view: 'live' });
|
||||
expect(issue.hasIssue()).toBe(true);
|
||||
|
||||
// Switch to camera-2 which also has an error — should stay active.
|
||||
// Switch to camera-2 which also has an error -- should stay active.
|
||||
issue.detectDynamic({ targetID: 'camera-2', view: 'live' });
|
||||
expect(issue.hasIssue()).toBe(true);
|
||||
});
|
||||
@@ -456,7 +456,7 @@ describe('MediaLoadIssue', () => {
|
||||
|
||||
issue.retry();
|
||||
|
||||
// Issue remains active — no new 10s grace period. The error stays
|
||||
// Issue remains active -- no new 10s grace period. The error stays
|
||||
// visible while the provider re-attempts loading underneath.
|
||||
expect(issue.hasIssue()).toBe(true);
|
||||
});
|
||||
@@ -542,9 +542,9 @@ describe('MediaLoadIssue', () => {
|
||||
|
||||
issue.retry();
|
||||
|
||||
// After retry, the issue stays active and the errored target is
|
||||
// preserved — no new 10s grace period. If media:loaded fires, the
|
||||
// existing _handleMediaLoaded path will clear everything.
|
||||
// After retry, the issue stays active and the errored target is preserved
|
||||
// -- no new 10s grace period. If media:loaded fires, the existing
|
||||
// _handleMediaLoaded path will clear everything.
|
||||
expect(issue.hasIssue()).toBe(true);
|
||||
issue.detectDynamic({ targetID: 'camera-1', view: 'live' });
|
||||
expect(issue.hasIssue()).toBe(true);
|
||||
@@ -579,8 +579,8 @@ describe('MediaLoadIssue', () => {
|
||||
// Card detaches: timer must stop.
|
||||
issue.suspend();
|
||||
|
||||
// Full 10s later (plus margin) the timer has NOT matured — the user
|
||||
// was offscreen and that time does not count against them.
|
||||
// Full 10s later (plus margin) the timer has NOT matured -- the user was
|
||||
// offscreen and that time does not count against them.
|
||||
vi.advanceTimersByTime(20000);
|
||||
expect(issue.hasIssue()).toBe(false);
|
||||
expect(onChange).not.toBeCalled();
|
||||
@@ -594,7 +594,7 @@ describe('MediaLoadIssue', () => {
|
||||
vi.advanceTimersByTime(10000);
|
||||
expect(issue.hasIssue()).toBe(true);
|
||||
|
||||
// Card detaches — issue must remain visible on reattach.
|
||||
// Card detaches -- issue must remain visible on reattach.
|
||||
issue.suspend();
|
||||
|
||||
expect(issue.hasIssue()).toBe(true);
|
||||
@@ -609,8 +609,8 @@ describe('MediaLoadIssue', () => {
|
||||
issue.suspend();
|
||||
|
||||
// Reattach: the manager's resume() triggers evaluate() → detectDynamic.
|
||||
// The target is still loading, so the timer arms with a fresh 10s
|
||||
// window — not whatever was left when we suspended.
|
||||
// The target is still loading, so the timer arms with a fresh 10s window
|
||||
// -- not whatever was left when we suspended.
|
||||
issue.detectDynamic({ targetID: 'camera-1', view: 'live' });
|
||||
|
||||
vi.advanceTimersByTime(9999);
|
||||
|
||||
@@ -68,6 +68,28 @@ describe('IssueStateManager', () => {
|
||||
expect(mockLegacyResource.detectStatic).toBeCalledWith(hass);
|
||||
expect(mockMediaLoad.detectStatic).toBeCalledWith(hass);
|
||||
});
|
||||
|
||||
it('should isolate a failing issue and continue detecting the rest', async () => {
|
||||
const spy = vi.spyOn(console, 'warn').mockReturnValue();
|
||||
assert(mockConfigUpgrade.detectStatic);
|
||||
assert(mockLegacyResource.detectStatic);
|
||||
assert(mockMediaLoad.detectStatic);
|
||||
|
||||
vi.mocked(mockConfigUpgrade.detectStatic).mockRejectedValue(new Error('boom'));
|
||||
|
||||
// A second failure, a non-Error rejection, is isolated and logged too.
|
||||
vi.mocked(mockLegacyResource.detectStatic).mockRejectedValue('bad');
|
||||
|
||||
const manager = createManager();
|
||||
const hass = createHASS();
|
||||
|
||||
await expect(manager.detectStatic(hass)).resolves.toBeUndefined();
|
||||
|
||||
// Detection continued to the final issue despite the two earlier failures.
|
||||
expect(mockMediaLoad.detectStatic).toBeCalledWith(hass);
|
||||
expect(spy).toBeCalledTimes(2);
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('trigger', () => {
|
||||
|
||||
@@ -451,9 +451,9 @@ describe('MediaPlayerManager', () => {
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||
|
||||
// Bypass schema validation — the code has a runtime guard (TypeScript
|
||||
// narrowing) for the case where dashboard config is present but
|
||||
// dashboard_path / view_path are missing.
|
||||
// Bypass schema validation -- the code has a runtime guard
|
||||
// (TypeScript narrowing) for the case where dashboard config is
|
||||
// present but dashboard_path / view_path are missing.
|
||||
const configWithNoDashboardPaths = createCameraConfig({
|
||||
camera_entity: 'camera.foo',
|
||||
});
|
||||
|
||||
@@ -509,7 +509,7 @@ describe('should handle exceptions', () => {
|
||||
expect.objectContaining({ error }),
|
||||
);
|
||||
|
||||
// The loading flag must be cleared on error — otherwise gallery/viewer
|
||||
// The loading flag must be cleared on error -- otherwise gallery/viewer
|
||||
// components render "Awaiting media" indefinitely on top of the error
|
||||
// notification.
|
||||
expect(manager.getView()?.context?.loading?.query).toBeUndefined();
|
||||
@@ -538,9 +538,9 @@ describe('should handle exceptions', () => {
|
||||
viewQueryExecutor: viewQueryExecutor,
|
||||
});
|
||||
|
||||
// Concurrent reset during the await — clears `_view` before the
|
||||
// rejection is processed. The error path must not crash on the null
|
||||
// view when attempting to clear the loading flag.
|
||||
// Concurrent reset during the await -- clears `_view` before the rejection
|
||||
// is processed. The error path must not crash on the null view when
|
||||
// attempting to clear the loading flag.
|
||||
viewQueryExecutor.getNewQueryModifiers.mockImplementation(async () => {
|
||||
manager.reset();
|
||||
throw error;
|
||||
|
||||
@@ -56,6 +56,19 @@ describe('MicrophoneActionsController', () => {
|
||||
expect(microphoneManager.mute).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should swallow a rejected auto-unmute so a denied microphone does not surface', async () => {
|
||||
const microphoneManager = createMicrophoneManager();
|
||||
vi.mocked(microphoneManager.unmute).mockRejectedValue(new Error('denied'));
|
||||
const controller = new MicrophoneActionsController();
|
||||
controller.setOptions({
|
||||
microphoneManager,
|
||||
autoUnmuteConditions: ['selected' as const],
|
||||
});
|
||||
|
||||
await expect(controller.setSelectedCamera('camera-1')).resolves.toBeUndefined();
|
||||
expect(microphoneManager.unmute).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should mute on unselected when transitioning to a new camera', async () => {
|
||||
const microphoneManager = createMicrophoneManager();
|
||||
const controller = new MicrophoneActionsController();
|
||||
|
||||
@@ -61,7 +61,7 @@ describe('MediaLoadedInfoSinkController', () => {
|
||||
// Both cached, but only the selected one is exposed.
|
||||
expect(controller.get()).toBe(infoA);
|
||||
|
||||
// Selecting the other target switches what `get()` returns — without a
|
||||
// Selecting the other target switches what `get()` returns -- without a
|
||||
// new event arriving for it.
|
||||
selected = 'target-B';
|
||||
controller.hostUpdated();
|
||||
@@ -102,7 +102,7 @@ describe('MediaLoadedInfoSinkController', () => {
|
||||
controller.hostConnected();
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Load for an unselected target — cached but inactive.
|
||||
// Load for an unselected target -- cached but inactive.
|
||||
host.dispatchEvent(
|
||||
createMediaLoadedInfoEvent({
|
||||
info: createMediaLoadedInfo({ targetID: 'target-B' }),
|
||||
@@ -162,7 +162,7 @@ describe('MediaLoadedInfoSinkController', () => {
|
||||
controller.hostUpdated();
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Switch to another target with no cached info — active stays null.
|
||||
// Switch to another target with no cached info -- active stays null.
|
||||
selected = 'target-B';
|
||||
controller.hostUpdated();
|
||||
|
||||
|
||||
@@ -126,9 +126,9 @@ describe('MediaLoadedInfoSourceController', () => {
|
||||
});
|
||||
|
||||
it('should abort the prior dispatch when targetID changes between calls', () => {
|
||||
// Without this, the manager would zombie an entry under the old
|
||||
// targetID — its `onAbort` cleanup never fires because we never aborted
|
||||
// the prior signal before overwriting `_abort`.
|
||||
// Without this, the manager would zombie an entry under the old targetID
|
||||
// -- its `onAbort` cleanup never fires because we never aborted the prior
|
||||
// signal before overwriting `_abort`.
|
||||
let targetID: string | null = 'target-1';
|
||||
const host = createLitElement();
|
||||
const controller = new MediaLoadedInfoSourceController(host, {
|
||||
@@ -164,7 +164,7 @@ describe('MediaLoadedInfoSourceController', () => {
|
||||
controller.set(createMediaLoadedInfo());
|
||||
const firstSignal = (handler.mock.calls[0][0] as CustomEvent).detail.signal;
|
||||
|
||||
// Disconnect and reconnect — without a fresh `set`.
|
||||
// Disconnect and reconnect -- without a fresh `set`.
|
||||
controller.hostDisconnected();
|
||||
controller.hostConnected();
|
||||
|
||||
@@ -201,7 +201,7 @@ describe('MediaLoadedInfoSourceController', () => {
|
||||
host.addEventListener('advanced-camera-card:media:loaded', handler);
|
||||
|
||||
controller.set(createMediaLoadedInfo());
|
||||
// Active registration, no disconnect — connect should be a no-op.
|
||||
// Active registration, no disconnect -- connect should be a no-op.
|
||||
controller.hostConnected();
|
||||
|
||||
expect(handler).toBeCalledTimes(1);
|
||||
@@ -228,7 +228,7 @@ describe('MediaLoadedInfoSourceController', () => {
|
||||
targetID = 'target-2';
|
||||
controller.hostConnected();
|
||||
|
||||
// No re-dispatch — the stale cache was discarded.
|
||||
// No re-dispatch -- the stale cache was discarded.
|
||||
expect(handler).toBeCalledTimes(1);
|
||||
|
||||
// A subsequent set() under the new target dispatches fresh.
|
||||
|
||||
@@ -771,7 +771,7 @@ describe('SignedURLController', () => {
|
||||
controller.hostUpdate();
|
||||
expect(createProxiedEndpointIfNecessary).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Resolve the original request — should still succeed.
|
||||
// Resolve the original request -- should still succeed.
|
||||
resolveProxy?.({ endpoint: 'http://proxied-url.com', sign: false });
|
||||
await flushPromises();
|
||||
|
||||
@@ -809,7 +809,7 @@ describe('SignedURLController', () => {
|
||||
expect(createProxiedEndpointIfNecessary).toHaveBeenCalledTimes(1);
|
||||
expect(controller.getValue()).toBe('http://signed.com');
|
||||
|
||||
// Change only the extraneous field — should hit the cache, not re-fetch.
|
||||
// Change only the extraneous field -- should hit the cache, not re-fetch.
|
||||
extraneous = false;
|
||||
await controller.hostUpdate();
|
||||
expect(createProxiedEndpointIfNecessary).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -314,12 +314,12 @@ describe('StatusBarController', () => {
|
||||
sufficient: true,
|
||||
};
|
||||
|
||||
// Start with permanent item — bar stays visible.
|
||||
// Start with permanent item -- bar stays visible.
|
||||
controller.setItems([permanentItem, nonPermanentItem]);
|
||||
vi.advanceTimersByTime(10000);
|
||||
expect(host.getAttribute('hide')).toBe(null);
|
||||
|
||||
// Remove permanent item — popup timer starts.
|
||||
// Remove permanent item -- popup timer starts.
|
||||
controller.setItems([nonPermanentItem]);
|
||||
expect(host.getAttribute('hide')).toBe(null);
|
||||
|
||||
@@ -342,7 +342,7 @@ describe('StatusBarController', () => {
|
||||
string: 'Title',
|
||||
sufficient: true,
|
||||
};
|
||||
// A permanent item that is NOT sufficient — removing it does not
|
||||
// A permanent item that is NOT sufficient -- removing it does not
|
||||
// change the sufficient-values set, so the popup timer takes the
|
||||
// dedicated permanent-removal branch.
|
||||
const permanentInsufficientItem = {
|
||||
@@ -356,7 +356,7 @@ describe('StatusBarController', () => {
|
||||
vi.advanceTimersByTime(10000);
|
||||
expect(host.getAttribute('hide')).toBe(null);
|
||||
|
||||
// Remove the permanent (insufficient) item — sufficient values are
|
||||
// Remove the permanent (insufficient) item -- sufficient values are
|
||||
// unchanged, but the popup timer must still start.
|
||||
controller.setItems([sufficientItem]);
|
||||
expect(host.getAttribute('hide')).toBe(null);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { format } from 'date-fns';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { assert, describe, expect, it } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { CameraManager } from '../../../../src/camera-manager/manager';
|
||||
import { ThumbnailFeatureController } from '../../../../src/components-lib/thumbnail/feature/controller';
|
||||
@@ -12,6 +12,8 @@ describe('ThumbnailFeatureController', () => {
|
||||
title: 'Test Event',
|
||||
cameraID: 'camera_1',
|
||||
});
|
||||
const itemStartTime = itemWithTime.getStartTime();
|
||||
assert(itemStartTime);
|
||||
|
||||
describe('should set title', () => {
|
||||
it('should set title with start time ', () => {
|
||||
@@ -20,7 +22,7 @@ describe('ThumbnailFeatureController', () => {
|
||||
controller.calculate(null, itemWithTime, false);
|
||||
|
||||
// Use format() to generate expected time in local timezone
|
||||
const expectedTime = format(itemWithTime.getStartTime()!, 'HH:mm');
|
||||
const expectedTime = format(itemStartTime, 'HH:mm');
|
||||
expect(controller.getTitle()).toBe(expectedTime);
|
||||
});
|
||||
|
||||
@@ -54,7 +56,7 @@ describe('ThumbnailFeatureController', () => {
|
||||
controller.calculate(null, itemWithTime, false);
|
||||
|
||||
// Use format() to generate expected date string (formats in local time)
|
||||
const expectedDate = format(itemWithTime.getStartTime()!, 'MMM do');
|
||||
const expectedDate = format(itemStartTime, 'MMM do');
|
||||
expect(controller.getSubtitles()).toContain(expectedDate);
|
||||
});
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ describe('renderNoMedia', () => {
|
||||
});
|
||||
|
||||
it('should not include metadata when no camera is resolvable', () => {
|
||||
// Empty store — no default camera.
|
||||
// Empty store -- no default camera.
|
||||
const cameraManager = createCameraManager(createStore());
|
||||
|
||||
const result = renderNoMedia({
|
||||
|
||||
@@ -78,36 +78,37 @@ describe('dispatchViewContextChangeEvent', () => {
|
||||
const results = new QueryResults({ results: testResults });
|
||||
const slice = results.getSlice('office');
|
||||
expect(slice).not.toBeNull();
|
||||
expect(slice!.getResults()).toEqual(
|
||||
assert(slice);
|
||||
expect(slice.getResults()).toEqual(
|
||||
testResults.filter((item) => item.getCameraID() === 'office'),
|
||||
);
|
||||
expect(slice!.getResultsCount()).toEqual(100);
|
||||
expect(slice!.hasResults()).toBeTruthy();
|
||||
expect(slice!.getResult(0)).not.toBeNull();
|
||||
expect(slice!.getResult()).toBeNull();
|
||||
expect(slice!.getSelectedIndex()).toBe(99);
|
||||
expect(slice!.getSelectedResult()?.getID()).toEqual('id-office-99');
|
||||
expect(slice!.hasSelectedResult()).toBeTruthy();
|
||||
expect(slice.getResultsCount()).toEqual(100);
|
||||
expect(slice.hasResults()).toBeTruthy();
|
||||
expect(slice.getResult(0)).not.toBeNull();
|
||||
expect(slice.getResult()).toBeNull();
|
||||
expect(slice.getSelectedIndex()).toBe(99);
|
||||
expect(slice.getSelectedResult()?.getID()).toEqual('id-office-99');
|
||||
expect(slice.hasSelectedResult()).toBeTruthy();
|
||||
|
||||
expect(slice!.resetSelectedResult());
|
||||
expect(slice!.getSelectedResult()).toBeNull();
|
||||
expect(slice.resetSelectedResult());
|
||||
expect(slice.getSelectedResult()).toBeNull();
|
||||
|
||||
expect(slice!.selectIndex(10));
|
||||
expect(slice!.getSelectedIndex()).toBe(10);
|
||||
expect(slice.selectIndex(10));
|
||||
expect(slice.getSelectedIndex()).toBe(10);
|
||||
|
||||
expect(slice!.selectIndex(10000));
|
||||
expect(slice!.getSelectedIndex()).toBe(10);
|
||||
expect(slice.selectIndex(10000));
|
||||
expect(slice.getSelectedIndex()).toBe(10);
|
||||
|
||||
expect(slice!.selectIndex(-10000));
|
||||
expect(slice!.getSelectedIndex()).toBe(10);
|
||||
expect(slice.selectIndex(-10000));
|
||||
expect(slice.getSelectedIndex()).toBe(10);
|
||||
|
||||
slice!.selectResultIfFound((item: ViewItem) => item.getID() === 'id-office-42');
|
||||
expect(slice!.getSelectedResult()?.getID()).toBe('id-office-42');
|
||||
slice.selectResultIfFound((item: ViewItem) => item.getID() === 'id-office-42');
|
||||
expect(slice.getSelectedResult()?.getID()).toBe('id-office-42');
|
||||
|
||||
slice!.selectBestResult((itemArray: ViewItem[]) =>
|
||||
slice.selectBestResult((itemArray: ViewItem[]) =>
|
||||
itemArray.findIndex((item) => item.getID() === 'id-office-43'),
|
||||
);
|
||||
expect(slice!.getSelectedResult()?.getID()).toBe('id-office-43');
|
||||
expect(slice.getSelectedResult()?.getID()).toBe('id-office-43');
|
||||
});
|
||||
|
||||
describe('should respect select approach during construction', () => {
|
||||
|
||||
Reference in New Issue
Block a user