feat: Make cameras optional (#2343)

This commit is contained in:
Dermot Duffy
2026-02-13 20:21:05 -08:00
committed by GitHub
parent 21f9469784
commit 93f1f08e51
61 changed files with 1312 additions and 557 deletions
@@ -1,4 +1,4 @@
import { describe, expect, it, vi } from 'vitest';
import { describe, expect, it } from 'vitest';
import { EffectAction } from '../../../../src/card-controller/actions/actions/effect';
import { createEffectAction } from '../../../../src/utils/action';
import { createCardAPI } from '../../../test-utils';
@@ -11,7 +11,7 @@ describe('EffectAction', () => {
await action.execute(api);
expect(api.getEffectsControllerAPI()?.startEffect).toHaveBeenCalledWith('snow');
expect(api.getEffectsManager().startEffect).toHaveBeenCalledWith('snow');
});
it('should call stopEffect when action is stop', async () => {
@@ -21,7 +21,7 @@ describe('EffectAction', () => {
await action.execute(api);
expect(api.getEffectsControllerAPI()?.stopEffect).toHaveBeenCalledWith('snow');
expect(api.getEffectsManager().stopEffect).toHaveBeenCalledWith('snow');
});
it('should call toggleEffect when action is toggle', async () => {
@@ -31,16 +31,7 @@ describe('EffectAction', () => {
await action.execute(api);
expect(api.getEffectsControllerAPI()?.toggleEffect).toHaveBeenCalledWith('snow');
});
it('should not throw when effectsControllerAPI is null', async () => {
const api = createCardAPI();
vi.mocked(api.getEffectsControllerAPI).mockReturnValue(null);
const actionConfig = createEffectAction('snow', 'start');
const action = new EffectAction({}, actionConfig);
await expect(action.execute(api)).resolves.toBeUndefined();
expect(api.getEffectsManager().toggleEffect).toHaveBeenCalledWith('snow');
});
it('should have a no-op stop method', async () => {
@@ -101,4 +101,28 @@ describe('should handle media_player action', () => {
expect(api.getMediaPlayerManager().playMedia).not.toBeCalled();
});
it('to not play live without camera', async () => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
camera: null,
view: 'live',
}),
);
const action = new MediaPlayerAction(
{},
{
action: 'fire-dom-event',
advanced_camera_card_action: 'media_player',
media_player_action: 'play',
media_player: 'this_is_a_media_player',
},
);
await action.execute(api);
expect(api.getMediaPlayerManager().playLive).not.toBeCalled();
});
});
@@ -1,12 +1,14 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ZodError } from 'zod';
import { AutomationsManager } from '../../../src/card-controller/automations-manager';
import { ConfigManager } from '../../../src/card-controller/config/config-manager';
import { InitializationAspect } from '../../../src/card-controller/initialization-manager';
import { ConditionStateManager } from '../../../src/conditions/state-manager';
import { AutomationsManager } from '../../../src/card-controller/automations-manager';
import { Automation } from '../../../src/config/schema/automations';
import { AdvancedCameraCardCondition } from '../../../src/config/schema/conditions/types';
import { advancedCameraCardConfigSchema } from '../../../src/config/schema/types';
import { createCardAPI, createConfig, flushPromises } from '../../test-utils';
import { createGeneralAction } from '../../../src/utils/action';
import { createCardAPI, createConfig, flushPromises } from '../../test-utils';
/**
* Create a ConfigManager test setup with real AutomationsManager and ConditionStateManager.
@@ -92,7 +94,7 @@ describe('ConfigManager', () => {
it('invalid configuration with hint', () => {
const manager = new ConfigManager(createCardAPI());
expect(() => manager.setConfig({})).toThrowError(
'Invalid configuration: [\n "cameras",\n "type"\n]',
'Invalid configuration: [\n "type"\n]',
);
});
@@ -290,7 +292,7 @@ describe('ConfigManager', () => {
overrides: [
{
conditions: [TEST_CONDITIONS.FULLSCREEN_ON],
delete: ['cameras'],
delete: ['type'],
},
],
};
@@ -472,7 +474,7 @@ describe('ConfigManager', () => {
],
});
manager.setConfig(config as any);
manager.setConfig(config);
await flushPromises();
// Verify keyboard shortcuts automations were added initially with ptz_home
@@ -501,9 +503,10 @@ describe('ConfigManager', () => {
// This confirms the override removed them (directly verified through add calls)
const addCalls = addAutomationsSpy.mock.calls;
const hasKeyboardShortcut = addCalls.some((call) =>
call[0].some((automation: any) =>
call[0].some((automation: Automation) =>
automation.conditions?.some(
(cond: any) => cond.condition === 'key' && cond.key === 'h',
(cond: AdvancedCameraCardCondition) =>
cond.condition === 'key' && cond.key === 'h',
),
),
);
@@ -524,7 +527,7 @@ describe('ConfigManager', () => {
],
});
manager.setConfig(config as any);
manager.setConfig(config);
await flushPromises();
// Verify initial payload (folders may be populated with defaults, so
@@ -594,7 +597,7 @@ describe('ConfigManager', () => {
],
});
manager.setConfig(config as any);
manager.setConfig(config);
await flushPromises();
// Verify automations were added initially
@@ -652,7 +655,7 @@ describe('ConfigManager', () => {
});
// Initial set should register remote control automations
manager.setConfig(config as any);
manager.setConfig(config);
await flushPromises();
// Verify remote-control automations were added initially with config condition
@@ -682,9 +685,9 @@ describe('ConfigManager', () => {
// Verify new automations don't contain remote-control config conditions
const addCalls = addAutomationsSpy.mock.calls;
const hasRemoteControl = addCalls.some((call) =>
call[0].some((automation: any) =>
call[0].some((automation: Automation) =>
automation.conditions?.some(
(cond: any) =>
(cond: AdvancedCameraCardCondition) =>
cond.condition === 'config' &&
cond.paths?.includes('remote_control.entities.camera'),
),
@@ -313,13 +313,16 @@ describe('setRemoteControlEntityFromConfig', () => {
});
describe('should select option on entity', () => {
it('should select option when camera differs from entity state', () => {
it('should select option when camera differs from entity state', async () => {
const hass = createHASS({
'input_select.camera': createStateEntity({
state: 'camera.one',
}),
});
const api = createCardAPI();
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([{ cameraID: 'camera.two' }]),
);
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
@@ -349,7 +352,7 @@ describe('setRemoteControlEntityFromConfig', () => {
INTERNAL_CALLBACK_ACTION,
);
cameraSyncAction.callback(api);
await cameraSyncAction.callback(api);
expect(hass.callService).toBeCalledWith(
'input_select',
'select_option',
@@ -369,6 +372,9 @@ describe('setRemoteControlEntityFromConfig', () => {
}),
});
const api = createCardAPI();
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([{ cameraID: 'camera.one' }]),
);
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
@@ -441,7 +447,47 @@ describe('setRemoteControlEntityFromConfig', () => {
expect(hass.callService).not.toBeCalled();
});
it('should not throw when hass is undefined', () => {
it('should not select option when view exists but has no camera', () => {
const hass = createHASS({
'input_select.camera': createStateEntity({
state: 'camera.one',
}),
});
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
remote_control: {
entities: {
camera: 'input_select.camera',
camera_priority: 'card',
},
},
}),
);
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
camera: null,
view: 'live',
}),
);
setRemoteControlEntityFromConfig(api);
// Test the 'camera' condition callback (automation index 1)
const cameraSyncAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
.calls[0][0][1].actions?.[0] as InternalCallbackActionConfig;
cameraSyncAction.callback(api);
expect(hass.callService).not.toBeCalled();
// Also test the 'initialized' condition callback (automation index 2)
const initializedSyncAction = vi.mocked(api.getAutomationsManager().addAutomations)
.mock.calls[0][0][2].actions?.[0] as InternalCallbackActionConfig;
initializedSyncAction.callback(api);
expect(hass.callService).not.toBeCalled();
});
it('should not throw when hass is undefined', async () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
@@ -472,12 +518,15 @@ describe('setRemoteControlEntityFromConfig', () => {
);
// Should not throw and obviously not call service (as hass is null)
cameraSyncAction.callback(api);
await cameraSyncAction.callback(api);
});
it('should select option when entity state is undefined', () => {
it('should select option when entity state is undefined', async () => {
const hass = createHASS({});
const api = createCardAPI();
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([{ cameraID: 'camera.two' }]),
);
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
@@ -506,7 +555,7 @@ describe('setRemoteControlEntityFromConfig', () => {
INTERNAL_CALLBACK_ACTION,
);
cameraSyncAction.callback(api);
await cameraSyncAction.callback(api);
expect(hass.callService).toBeCalledWith(
'input_select',
'select_option',
@@ -519,13 +568,16 @@ describe('setRemoteControlEntityFromConfig', () => {
);
});
it('should select option on initialization with card priority', () => {
it('should select option on initialization with card priority', async () => {
const hass = createHASS({
'input_select.camera': createStateEntity({
state: 'camera.one',
}),
});
const api = createCardAPI();
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([{ cameraID: 'camera.two' }]),
);
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
@@ -553,7 +605,7 @@ describe('setRemoteControlEntityFromConfig', () => {
expect(isAdvancedCameraCardCustomAction(initAction)).toBeTruthy();
expect(initAction.advanced_camera_card_action).toBe(INTERNAL_CALLBACK_ACTION);
initAction.callback(api);
await initAction.callback(api);
expect(hass.callService).toBeCalledWith(
'input_select',
'select_option',
+3 -12
View File
@@ -1,5 +1,4 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { CameraManager } from '../../src/camera-manager/manager';
import { ActionsManager } from '../../src/card-controller/actions/actions-manager';
import { AutomationsManager } from '../../src/card-controller/automations-manager';
@@ -34,7 +33,6 @@ import { AdvancedCameraCardEditor } from '../../src/editor';
import { DeviceRegistryManager } from '../../src/ha/registry/device';
import { EntityRegistryManagerLive } from '../../src/ha/registry/entity';
import { ResolvedMediaCache } from '../../src/ha/resolved-media';
import { EffectsControllerAPI } from '../../src/types';
vi.mock('../../src/camera-manager/manager');
vi.mock('../../src/card-controller/actions/actions-manager');
@@ -74,7 +72,7 @@ const createCardElement = (): CardHTMLElement => {
};
const createController = (): CardController => {
return new CardController(createCardElement(), vi.fn(), vi.fn(), vi.fn());
return new CardController(createCardElement(), vi.fn(), vi.fn());
};
// @vitest-environment jsdom
@@ -87,15 +85,8 @@ describe('CardController', () => {
const element = createCardElement();
const scrollCallback = vi.fn();
const menuToggleCallback = vi.fn();
const effectsControllerAPI = mock<EffectsControllerAPI>();
const effectsControllerAPICallback = vi.fn().mockReturnValue(effectsControllerAPI);
const controller = new CardController(
element,
scrollCallback,
menuToggleCallback,
effectsControllerAPICallback,
);
const controller = new CardController(element, scrollCallback, menuToggleCallback);
expect(CardElementManager).toBeCalledWith(
controller,
@@ -103,7 +94,7 @@ describe('CardController', () => {
scrollCallback,
menuToggleCallback,
);
expect(controller.getEffectsControllerAPI()).toBe(effectsControllerAPI);
expect(controller.getEffectsManager()).toBeTruthy();
});
describe('accessors', () => {
@@ -0,0 +1,281 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { EffectsManager } from '../../../src/card-controller/effects/effects-manager';
import { EffectComponent } from '../../../src/card-controller/effects/types';
import { EffectName } from '../../../src/types';
import { flushPromises } from '../../test-utils';
vi.mock('../../../src/components/effects/fireworks', () => ({
AdvancedCameraCardEffectFireworks: vi.fn(() => createMockEffectComponent()),
}));
vi.mock('../../../src/components/effects/check', () => ({
AdvancedCameraCardEffectCheck: vi.fn(() => createMockEffectComponent()),
}));
vi.mock('../../../src/components/effects/ghost', () => ({
AdvancedCameraCardEffectGhost: vi.fn(() => createMockEffectComponent()),
}));
vi.mock('../../../src/components/effects/hearts', () => ({
AdvancedCameraCardEffectHearts: vi.fn(() => createMockEffectComponent()),
}));
vi.mock('../../../src/components/effects/shamrocks', () => ({
AdvancedCameraCardEffectShamrocks: vi.fn(() => createMockEffectComponent()),
}));
vi.mock('../../../src/components/effects/snow', () => ({
AdvancedCameraCardEffectSnow: vi.fn(() => createMockEffectComponent()),
}));
const createMockEffectComponent = (): EffectComponent => {
const element = document.createElement('div') as unknown as EffectComponent;
element.fadeIn = true;
element.startFadeOut = vi.fn().mockResolvedValue(undefined);
return element;
};
// @vitest-environment jsdom
describe('EffectsManager', () => {
let container: HTMLElement;
let manager: EffectsManager;
beforeEach(() => {
container = document.createElement('div');
manager = new EffectsManager();
manager.setContainer(container);
vi.useFakeTimers();
});
afterEach(() => {
vi.clearAllMocks();
vi.useRealTimers();
});
describe('startEffect', () => {
it('should load and start snow effect', async () => {
expect(container.children.length).toBe(0);
await manager.startEffect('snow');
expect(container.children.length).toBe(1);
});
it('should load and start hearts effect', async () => {
expect(container.children.length).toBe(0);
await manager.startEffect('hearts');
expect(container.children.length).toBe(1);
});
it('should load and start shamrocks effect', async () => {
expect(container.children.length).toBe(0);
await manager.startEffect('shamrocks');
expect(container.children.length).toBe(1);
});
it('should load and start fireworks effect', async () => {
expect(container.children.length).toBe(0);
await manager.startEffect('fireworks');
expect(container.children.length).toBe(1);
});
it('should load and start ghost effect', async () => {
expect(container.children.length).toBe(0);
await manager.startEffect('ghost');
expect(container.children.length).toBe(1);
});
it('should load and start check effect', async () => {
expect(container.children.length).toBe(0);
await manager.startEffect('check');
expect(container.children.length).toBe(1);
});
it('should stop effect automatically after duration', async () => {
const effectPromise = manager.startEffect('snow', { duration: 1 });
// Wait for the effect to be added to the DOM.
await flushPromises();
expect(container.children.length).toBe(1);
vi.advanceTimersByTime(1000);
await effectPromise;
expect(container.children.length).toBe(0);
});
it('should clean up timer when stopped manually', async () => {
// Don't await startEffect as it waits for the duration
manager.startEffect('snow', { duration: 10 });
// Wait for the effect to be added to the DOM.
await flushPromises();
await manager.stopEffect('snow');
expect(container.children.length).toBe(0);
});
it('should set fadeIn to true by default', async () => {
await manager.startEffect('snow');
const effectElement = container.children[0] as EffectComponent;
expect(effectElement.fadeIn).toBe(true);
});
it('should set fadeIn from options', async () => {
await manager.startEffect('snow', { fadeIn: false });
const effectElement = container.children[0] as EffectComponent;
expect(effectElement.fadeIn).toBe(false);
});
it('should queue effects until a container is available', async () => {
manager.removeContainer();
await manager.startEffect('snow');
expect(container.children.length).toBe(0);
manager.setContainer(container);
await flushPromises();
expect(container.children.length).toBe(1);
});
it('should not start effect if already active', async () => {
await manager.startEffect('snow');
await manager.startEffect('snow');
// Should only have one child since the second call is ignored.
expect(container.children.length).toBe(1);
});
it('should allow starting different effects simultaneously', async () => {
await manager.startEffect('snow');
await manager.startEffect('hearts');
expect(container.children.length).toBe(2);
});
it('should return early if effect module cannot be loaded', async () => {
// Use an effect name that doesn't exist in the registry
await manager.startEffect('unknown' as EffectName);
expect(container.children.length).toBe(0);
});
});
describe('stopEffect', () => {
it('should stop an active effect', async () => {
await manager.startEffect('snow');
const effectElement = container.children[0] as EffectComponent;
await manager.stopEffect('snow');
expect(effectElement.startFadeOut).toHaveBeenCalled();
expect(container.children.length).toBe(0);
});
it('should do nothing if effect is not active', async () => {
expect(container.children.length).toBe(0);
await manager.stopEffect('snow');
expect(container.children.length).toBe(0);
});
it('should allow starting effect again after stopping', async () => {
await manager.startEffect('snow');
await manager.stopEffect('snow');
await manager.startEffect('snow');
expect(container.children.length).toBe(1);
});
it('should cancel effect when stopped during loading', async () => {
// Start the effect but don't await it - simulates the loading phase.
const startPromise = manager.startEffect('snow');
// Stop the effect immediately while it's still loading.
await manager.stopEffect('snow');
// Wait for start to complete.
await startPromise;
// The effect should not appear since it was stopped during loading.
expect(container.children.length).toBe(0);
});
it('should cancel queued effects', async () => {
manager.removeContainer();
await manager.startEffect('snow');
await manager.stopEffect('snow');
manager.setContainer(container);
await flushPromises();
expect(container.children.length).toBe(0);
});
it('should clear active effects and timers when container is removed', async () => {
manager.startEffect('snow', { duration: 10 });
await flushPromises();
expect(container.children.length).toBe(1);
manager.removeContainer();
expect(container.children.length).toBe(0);
});
});
describe('toggleEffect', () => {
it('should start effect when not active', async () => {
await manager.toggleEffect('snow');
expect(container.children.length).toBe(1);
});
it('should stop effect when active', async () => {
await manager.startEffect('snow');
const effectElement = container.children[0] as EffectComponent;
await manager.toggleEffect('snow');
expect(effectElement.startFadeOut).toHaveBeenCalled();
});
it('should pass options when starting effect', async () => {
await manager.toggleEffect('snow', { fadeIn: false });
const effectElement = container.children[0] as EffectComponent;
expect(effectElement.fadeIn).toBe(false);
});
it('should cancel effect when toggled during loading', async () => {
// Start the effect but don't await it - simulates the loading phase.
const startPromise = manager.startEffect('snow');
// Toggle the effect immediately while it's still loading.
await manager.toggleEffect('snow');
// Wait for start to complete.
await startPromise;
// The effect should not appear since it was toggled (stopped) during loading.
expect(container.children.length).toBe(0);
});
});
});
+89 -3
View File
@@ -1,4 +1,6 @@
import { describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { FoldersManager } from '../../../src/card-controller/folders/manager';
import { ViewFactory } from '../../../src/card-controller/view/factory';
import { ViewModifier } from '../../../src/card-controller/view/types';
import { AdvancedCameraCardView } from '../../../src/config/schema/common/const';
@@ -9,6 +11,7 @@ import {
createCapabilities,
createCardAPI,
createConfig,
createFolder,
createStore,
createView,
} from '../../test-utils';
@@ -20,7 +23,7 @@ describe('getViewDefault', () => {
expect(factory.getViewDefault()).toBeNull();
});
it('should throw if no cameras support view', () => {
it('should throw when no cameras support view', () => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
@@ -47,6 +50,23 @@ describe('getViewDefault', () => {
expect(() => factory.getViewDefault()).toThrowError(/No cameras support this view/);
});
it('should use folders view as default when folders exist without cameras', () => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getFoldersManager).mockReturnValue(mock<FoldersManager>());
vi.mocked(api.getFoldersManager().hasFolders).mockReturnValue(true);
vi.mocked(api.getFoldersManager().getFolder).mockReturnValue(createFolder());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(createStore([]));
const factory = new ViewFactory(api);
const view = factory.getViewDefault();
expect(view?.view).toBe('folders');
expect(view?.camera).toBeNull();
});
it('should create view', () => {
const factory = new ViewFactory(createPopulatedAPI());
const view = factory.getViewDefault();
@@ -217,8 +237,6 @@ describe('getViewByParameters', () => {
const factory = new ViewFactory(api);
expect(() =>
factory.getViewByParameters({
// Since no camera is specified, and no camera supports the capabilities
// necessary for this view, the view will be null.
params: {
view: 'snapshots',
},
@@ -226,6 +244,56 @@ describe('getViewByParameters', () => {
).toThrowError(/No cameras support this view/);
});
describe('should handle no camera for view with failsafe', () => {
it('should choose default view when it does not require a camera', () => {
const api = createCardAPI();
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(createStore([]));
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
view: {
default: 'diagnostics',
},
}),
);
const factory = new ViewFactory(api);
const view = factory.getViewByParameters({
params: {
view: 'live',
},
failSafe: true,
});
expect(view?.is('diagnostics')).toBeTruthy();
expect(view?.camera).toBeNull();
});
it('should choose default view and camera when it requires a camera', () => {
const api = createCardAPI();
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([{ cameraID: 'camera.one' }]),
);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
view: {
default: 'live',
},
}),
);
const factory = new ViewFactory(api);
const view = factory.getViewByParameters({
params: {
view: 'snapshots',
},
failSafe: true,
});
expect(view?.is('live')).toBeTruthy();
expect(view?.camera).toBe('camera.one');
});
});
describe('should handle unsupported view', () => {
it('should throw without failsafe', () => {
const api = createCardAPI();
@@ -285,6 +353,24 @@ describe('getViewByParameters', () => {
});
expect(view?.is('live')).toBeTruthy();
});
it('should throw and omit capabilities when camera object is missing from store', () => {
const api = createCardAPI();
const store = createStore([{ cameraID: 'camera.one' }]);
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager(store));
vi.spyOn(store, 'getCamera').mockReturnValue(null);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
const factory = new ViewFactory(api);
expect(() =>
factory.getViewByParameters({
params: {
camera: 'camera.one',
view: 'snapshots',
},
}),
).toThrowError(/The selected camera or media does not support this view/);
});
});
it('should call modifiers', () => {
@@ -21,29 +21,27 @@ const createAPIWithSubstreams = (
config?: RawAdvancedCameraCardConfig,
): CardController => {
const api = createCardAPI();
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
{
cameraID: 'camera.office',
capabilities: createCapabilities({
live: true,
substream: true,
}),
config: createCameraConfig({
dependencies: {
all_cameras: true,
},
}),
},
{
cameraID: 'camera.kitchen',
capabilities: createCapabilities({
substream: true,
}),
},
]),
);
const store = createStore([
{
cameraID: 'camera.office',
capabilities: createCapabilities({
live: true,
substream: true,
}),
config: createCameraConfig({
dependencies: {
all_cameras: true,
},
}),
},
{
cameraID: 'camera.kitchen',
capabilities: createCapabilities({
substream: true,
}),
},
]);
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager(store));
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig(config));
return api;
};
@@ -97,11 +95,25 @@ describe('should turn on substream', () => {
expect(hasSubstream(view)).toBe(false);
const api = createCardAPI();
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
const cameraManager = createCameraManager(createStore([]));
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
const modifier = new SubstreamOnViewModifier(api);
modifier.modify(view);
expect(hasSubstream(view)).toBe(false);
});
it('without camera', () => {
const view = createView({
camera: null,
view: 'live',
});
const api = createCardAPI();
const modifier = new SubstreamOnViewModifier(api);
modifier.modify(view);
expect(hasSubstream(view)).toBe(false);
});
});
@@ -257,7 +257,7 @@ describe('ViewQueryExecutor', () => {
const store = createStore([
{
cameraID: 'camera.office',
capabilities: createCapabilities({ [mode]: true }),
capabilities: createCapabilities({ live: true, [mode]: true }),
config: createCameraConfig({
media: {
type: mode === 'reviews' ? 'reviews' : 'events',
@@ -546,6 +546,34 @@ describe('ViewQueryExecutor', () => {
expect(view.query).toBeNull();
expect(view.queryResults).toBeNull();
});
it('should handle non-grid view without a camera', async () => {
const api = createPopulatedAPI();
const viewQueryExecutor = new ViewQueryExecutor(api);
const view = createView({
view: 'live',
camera: null,
});
const modifiers = await viewQueryExecutor.getNewQueryModifiers(view);
applyViewModifiers(view, modifiers);
expect(view.query).toBeNull();
});
it('should handle diagnostics view without a camera', async () => {
const api = createPopulatedAPI();
const viewQueryExecutor = new ViewQueryExecutor(api);
const view = createView({
view: 'diagnostics',
camera: null,
});
const modifiers = await viewQueryExecutor.getNewQueryModifiers(view);
applyViewModifiers(view, modifiers);
expect(view.query).toBeNull();
});
});
describe('with a timeline view', () => {
@@ -799,6 +827,7 @@ describe('ViewQueryExecutor', () => {
'should execute default folder query with %s view',
async (viewName: AdvancedCameraCardView) => {
const api = createPopulatedAPI();
vi.mocked(api.getFoldersManager().hasFolders).mockReturnValue(true);
vi.mocked(api.getFoldersManager().getFolder).mockReturnValue({
type: 'ha',
id: 'office',
@@ -827,6 +856,7 @@ describe('ViewQueryExecutor', () => {
it('should execute default folder query with folder view and handle null results', async () => {
const api = createPopulatedAPI();
vi.mocked(api.getFoldersManager().hasFolders).mockReturnValue(true);
vi.mocked(api.getFoldersManager().getFolder).mockReturnValue(null);
vi.mocked(api.getFoldersManager().getDefaultQueryParameters).mockReturnValue(
null,