feat: Make cameras optional (#2343)

This commit is contained in:
Dermot Duffy authored and GitHub committed 2026-02-13 20:21:05 -08:00
1 parent 21f9469784
commit 93f1f08e51
61 files changed
+1312 -557

No files matched your search

+16
View File
@@ -24,6 +24,7 @@ import {
import { CardController } from '../../src/card-controller/controller.js';
import { sortItems } from '../../src/card-controller/view/sort.js';
import { CameraConfig } from '../../src/config/schema/cameras.js';
import { advancedCameraCardConfigSchema } from '../../src/config/schema/types.js';
import { HomeAssistant } from '../../src/ha/types.js';
import { QuerySource } from '../../src/query-source.js';
import { Endpoint, PTZMovementType } from '../../src/types.js';
@@ -340,6 +341,21 @@ describe('CameraManager', () => {
expect(manager.getStore().getCameraCount()).toBe(0);
});
it('without cameras in config', async () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
advancedCameraCardConfigSchema.parse({
type: 'advanced-camera-card' as const,
}),
);
const manager = new CameraManager(api);
await manager.initializeCamerasFromConfig();
expect(manager.getStore().getCameraCount()).toBe(0);
});
it('without an id', async () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
@@ -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', () => {
@@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { EffectsController } from '../../../src/components-lib/effects/effects-controller';
import { EffectComponent } from '../../../src/components-lib/effects/types';
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';
@@ -36,14 +36,14 @@ const createMockEffectComponent = (): EffectComponent => {
};
// @vitest-environment jsdom
describe('EffectsController', () => {
describe('EffectsManager', () => {
let container: HTMLElement;
let controller: EffectsController;
let manager: EffectsManager;
beforeEach(() => {
container = document.createElement('div');
controller = new EffectsController();
controller.setContainer(container);
manager = new EffectsManager();
manager.setContainer(container);
vi.useFakeTimers();
});
@@ -56,7 +56,7 @@ describe('EffectsController', () => {
it('should load and start snow effect', async () => {
expect(container.children.length).toBe(0);
await controller.startEffect('snow');
await manager.startEffect('snow');
expect(container.children.length).toBe(1);
});
@@ -64,7 +64,7 @@ describe('EffectsController', () => {
it('should load and start hearts effect', async () => {
expect(container.children.length).toBe(0);
await controller.startEffect('hearts');
await manager.startEffect('hearts');
expect(container.children.length).toBe(1);
});
@@ -72,7 +72,7 @@ describe('EffectsController', () => {
it('should load and start shamrocks effect', async () => {
expect(container.children.length).toBe(0);
await controller.startEffect('shamrocks');
await manager.startEffect('shamrocks');
expect(container.children.length).toBe(1);
});
@@ -80,7 +80,7 @@ describe('EffectsController', () => {
it('should load and start fireworks effect', async () => {
expect(container.children.length).toBe(0);
await controller.startEffect('fireworks');
await manager.startEffect('fireworks');
expect(container.children.length).toBe(1);
});
@@ -88,7 +88,7 @@ describe('EffectsController', () => {
it('should load and start ghost effect', async () => {
expect(container.children.length).toBe(0);
await controller.startEffect('ghost');
await manager.startEffect('ghost');
expect(container.children.length).toBe(1);
});
@@ -96,13 +96,13 @@ describe('EffectsController', () => {
it('should load and start check effect', async () => {
expect(container.children.length).toBe(0);
await controller.startEffect('check');
await manager.startEffect('check');
expect(container.children.length).toBe(1);
});
it('should stop effect automatically after duration', async () => {
const effectPromise = controller.startEffect('snow', { duration: 1 });
const effectPromise = manager.startEffect('snow', { duration: 1 });
// Wait for the effect to be added to the DOM.
await flushPromises();
@@ -117,56 +117,61 @@ describe('EffectsController', () => {
it('should clean up timer when stopped manually', async () => {
// Don't await startEffect as it waits for the duration
controller.startEffect('snow', { duration: 10 });
manager.startEffect('snow', { duration: 10 });
// Wait for the effect to be added to the DOM.
await flushPromises();
await controller.stopEffect('snow');
await manager.stopEffect('snow');
expect(container.children.length).toBe(0);
});
it('should set fadeIn to true by default', async () => {
await controller.startEffect('snow');
await manager.startEffect('snow');
const effectElement = container.children[0] as EffectComponent;
expect(effectElement.fadeIn).toBe(true);
});
it('should set fadeIn from options', async () => {
await controller.startEffect('snow', { fadeIn: false });
await manager.startEffect('snow', { fadeIn: false });
const effectElement = container.children[0] as EffectComponent;
expect(effectElement.fadeIn).toBe(false);
});
it('should not start effect if container is null', async () => {
controller.setContainer(null);
it('should queue effects until a container is available', async () => {
manager.removeContainer();
await controller.startEffect('snow');
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 controller.startEffect('snow');
await controller.startEffect('snow');
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 controller.startEffect('snow');
await controller.startEffect('hearts');
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 controller.startEffect('unknown' as EffectName);
await manager.startEffect('unknown' as EffectName);
expect(container.children.length).toBe(0);
});
@@ -174,10 +179,10 @@ describe('EffectsController', () => {
describe('stopEffect', () => {
it('should stop an active effect', async () => {
await controller.startEffect('snow');
await manager.startEffect('snow');
const effectElement = container.children[0] as EffectComponent;
await controller.stopEffect('snow');
await manager.stopEffect('snow');
expect(effectElement.startFadeOut).toHaveBeenCalled();
expect(container.children.length).toBe(0);
@@ -186,25 +191,25 @@ describe('EffectsController', () => {
it('should do nothing if effect is not active', async () => {
expect(container.children.length).toBe(0);
await controller.stopEffect('snow');
await manager.stopEffect('snow');
expect(container.children.length).toBe(0);
});
it('should allow starting effect again after stopping', async () => {
await controller.startEffect('snow');
await controller.stopEffect('snow');
await controller.startEffect('snow');
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 = controller.startEffect('snow');
const startPromise = manager.startEffect('snow');
// Stop the effect immediately while it's still loading.
await controller.stopEffect('snow');
await manager.stopEffect('snow');
// Wait for start to complete.
await startPromise;
@@ -212,26 +217,48 @@ describe('EffectsController', () => {
// 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 controller.toggleEffect('snow');
await manager.toggleEffect('snow');
expect(container.children.length).toBe(1);
});
it('should stop effect when active', async () => {
await controller.startEffect('snow');
await manager.startEffect('snow');
const effectElement = container.children[0] as EffectComponent;
await controller.toggleEffect('snow');
await manager.toggleEffect('snow');
expect(effectElement.startFadeOut).toHaveBeenCalled();
});
it('should pass options when starting effect', async () => {
await controller.toggleEffect('snow', { fadeIn: false });
await manager.toggleEffect('snow', { fadeIn: false });
const effectElement = container.children[0] as EffectComponent;
expect(effectElement.fadeIn).toBe(false);
@@ -239,10 +266,10 @@ describe('EffectsController', () => {
it('should cancel effect when toggled during loading', async () => {
// Start the effect but don't await it - simulates the loading phase.
const startPromise = controller.startEffect('snow');
const startPromise = manager.startEffect('snow');
// Toggle the effect immediately while it's still loading.
await controller.toggleEffect('snow');
await manager.toggleEffect('snow');
// Wait for start to complete.
await startPromise;
+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,
@@ -26,8 +26,8 @@ import { ViewMedia, ViewMediaType } from '../../src/view/item.js';
import { QueryResults } from '../../src/view/query-results.js';
import { UnifiedQuery } from '../../src/view/unified-query.js';
import {
getCameraIDsForViewName,
isViewSupportedByCamera,
getCameraIDsWithCapabilityForView,
isViewSupported,
} from '../../src/view/view-support.js';
import { View } from '../../src/view/view.js';
import {
@@ -89,6 +89,7 @@ describe('MenuButtonController', () => {
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(isViewSupported).mockReturnValue(true);
controller = new MenuButtonController();
});
@@ -507,7 +508,7 @@ describe('MenuButtonController', () => {
describe('should have live menu button', () => {
it('when in live view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
view: createView({ view: 'live' }),
viewManager: viewManager,
@@ -526,7 +527,7 @@ describe('MenuButtonController', () => {
it('when not in live view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
view: createView({ view: 'clips' }),
viewManager: viewManager,
@@ -545,7 +546,7 @@ describe('MenuButtonController', () => {
it('when not supported', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockReturnValue(false);
vi.mocked(isViewSupported).mockReturnValue(false);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
@@ -561,7 +562,7 @@ describe('MenuButtonController', () => {
describe('should have clips menu button', () => {
it('when in clips view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
view: createView({ view: 'clips' }),
viewManager: viewManager,
@@ -581,7 +582,7 @@ describe('MenuButtonController', () => {
it('when not in clips view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
@@ -600,7 +601,7 @@ describe('MenuButtonController', () => {
it('when not supported', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockReturnValue(false);
vi.mocked(isViewSupported).mockReturnValue(false);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
@@ -614,9 +615,7 @@ describe('MenuButtonController', () => {
describe('should have snapshots menu button', () => {
it('when in snapshots view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockImplementation(
(view) => view !== 'reviews',
);
vi.mocked(isViewSupported).mockImplementation((view) => view !== 'reviews');
const buttons = calculateButtons(controller, {
view: createView({ view: 'snapshots' }),
viewManager: viewManager,
@@ -642,7 +641,7 @@ describe('MenuButtonController', () => {
it('when not in snapshots view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
@@ -667,7 +666,7 @@ describe('MenuButtonController', () => {
it('when not supported', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockReturnValue(false);
vi.mocked(isViewSupported).mockReturnValue(false);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
@@ -683,7 +682,7 @@ describe('MenuButtonController', () => {
describe('should have reviews menu button', () => {
it('when in reviews view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
view: createView({ view: 'reviews' }),
viewManager: viewManager,
@@ -709,7 +708,7 @@ describe('MenuButtonController', () => {
it('when not in reviews view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
@@ -734,7 +733,7 @@ describe('MenuButtonController', () => {
it('when not supported', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockReturnValue(false);
vi.mocked(isViewSupported).mockReturnValue(false);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
@@ -748,7 +747,7 @@ describe('MenuButtonController', () => {
describe('should have gallery menu button', () => {
it('when in gallery view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
view: createView({ view: 'gallery' }),
viewManager: viewManager,
@@ -774,7 +773,7 @@ describe('MenuButtonController', () => {
it('when not in gallery view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
@@ -799,7 +798,7 @@ describe('MenuButtonController', () => {
it('when not supported', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockReturnValue(false);
vi.mocked(isViewSupported).mockReturnValue(false);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
@@ -813,7 +812,7 @@ describe('MenuButtonController', () => {
describe('should have recordings menu button', () => {
it('when in recordings view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
view: createView({ view: 'recordings' }),
viewManager: viewManager,
@@ -839,7 +838,7 @@ describe('MenuButtonController', () => {
it('when not in recordings view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
@@ -864,7 +863,7 @@ describe('MenuButtonController', () => {
it('when not supported', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockReturnValue(false);
vi.mocked(isViewSupported).mockReturnValue(false);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
@@ -880,7 +879,7 @@ describe('MenuButtonController', () => {
describe('should have image menu button', () => {
it('when in image view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
view: createView({ view: 'image' }),
@@ -900,7 +899,7 @@ describe('MenuButtonController', () => {
it('when not in image view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
view: createView({ view: 'live' }),
@@ -920,7 +919,7 @@ describe('MenuButtonController', () => {
it('when not supported', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockReturnValue(false);
vi.mocked(isViewSupported).mockReturnValue(false);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
@@ -936,7 +935,7 @@ describe('MenuButtonController', () => {
describe('should have timeline button', () => {
it('when in timeline view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
view: createView({ view: 'timeline' }),
viewManager: viewManager,
@@ -958,7 +957,7 @@ describe('MenuButtonController', () => {
it('when not in timeline view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
view: createView({ view: 'live' }),
viewManager: viewManager,
@@ -980,7 +979,7 @@ describe('MenuButtonController', () => {
it('when not supported', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockReturnValue(false);
vi.mocked(isViewSupported).mockReturnValue(false);
const buttons = calculateButtons(controller, {
view: createView({ view: 'live' }),
viewManager: viewManager,
@@ -1640,7 +1639,7 @@ describe('MenuButtonController', () => {
]),
);
vi.mocked(getCameraIDsForViewName).mockReturnValue(
vi.mocked(getCameraIDsWithCapabilityForView).mockReturnValue(
new Set(['camera-1', 'camera-2']),
);
@@ -1928,6 +1927,7 @@ describe('MenuButtonController', () => {
describe('should have folders button', () => {
it('should have no folders button without folders', () => {
const foldersManager = mock<FoldersManager>();
foldersManager.hasFolders.mockReturnValue(true);
foldersManager.getFolders.mockReturnValue(new Map().entries());
const buttons = calculateButtons(controller, {
@@ -1945,6 +1945,7 @@ describe('MenuButtonController', () => {
it('should have a folders button for a single folder outside the folder view', () => {
const foldersManager = mock<FoldersManager>();
foldersManager.hasFolders.mockReturnValue(true);
foldersManager.getFolders.mockReturnValue(
new Map([['folder-0', createFolder({ id: 'folder-0' })]]).entries(),
);
@@ -1968,6 +1969,7 @@ describe('MenuButtonController', () => {
it('should have a folders button for a single folder inside the folder view', () => {
const folder = createFolder({ id: 'folder-0' });
const foldersManager = mock<FoldersManager>();
foldersManager.hasFolders.mockReturnValue(true);
foldersManager.getFolders.mockReturnValue(
new Map([['folder-0', folder]]).entries(),
);
@@ -2006,6 +2008,7 @@ describe('MenuButtonController', () => {
['folder-0', createFolder({ id: 'folder-0' })],
['folder-selected', selectedFolder],
]);
foldersManager.hasFolders.mockReturnValue(true);
foldersManager.getFolders.mockReturnValue(folders.entries());
const selectedFolderNode: FolderQuery = {
@@ -2079,6 +2082,7 @@ describe('MenuButtonController', () => {
['folder-0', createFolder({ id: 'folder-0' })],
['folder-1', createFolder({ id: 'folder-1' })],
]);
foldersManager.hasFolders.mockReturnValue(true);
foldersManager.getFolders.mockReturnValue(folders.entries());
const buttons = calculateButtons(controller, {
@@ -2202,9 +2206,10 @@ describe('MenuButtonController', () => {
...dynamicButton,
tap_action: { action: 'fire-dom-event', advanced_camera_card_action: 'default' },
};
const cameraManager = createCameraManager(createStore([{ cameraID: 'camera-1' }]));
controller.addDynamicMenuButton(button);
expect(calculateButtons(controller)).toContainEqual({
expect(calculateButtons(controller, { cameraManager })).toContainEqual({
...button,
style: { color: 'var(--advanced-camera-card-menu-button-active-color)' },
});
@@ -2346,4 +2351,12 @@ describe('MenuButtonController', () => {
);
});
});
it('should handle view without camera', () => {
const buttons = calculateButtons(controller, {
view: createView({ camera: null }),
});
expect(buttons).toBeDefined();
});
});
+1 -1
View File
@@ -378,7 +378,7 @@ describe('config defaults', () => {
type: 'advanced-camera-card',
view: {
camera_select: 'current',
default: 'live',
default: 'auto',
keyboard_shortcuts: {
enabled: true,
ptz_down: {
+3 -7
View File
@@ -28,6 +28,7 @@ import { CardElementManager } from '../src/card-controller/card-element-manager'
import { ConfigManager } from '../src/card-controller/config/config-manager';
import { CardController } from '../src/card-controller/controller';
import { DefaultManager } from '../src/card-controller/default-manager';
import { EffectsManager } from '../src/card-controller/effects/effects-manager';
import { ExpandManager } from '../src/card-controller/expand-manager';
import { FoldersManager } from '../src/card-controller/folders/manager';
import { FolderQuery } from '../src/card-controller/folders/types';
@@ -71,12 +72,7 @@ import { Entity, EntityRegistryManager } from '../src/ha/registry/entity/types';
import { CurrentUser, HassStateDifference, HomeAssistant } from '../src/ha/types';
import { QuerySource } from '../src/query-source';
import { Severity } from '../src/severity';
import {
CapabilitiesRaw,
EffectsControllerAPI,
Interaction,
MediaLoadedInfo,
} from '../src/types';
import { CapabilitiesRaw, Interaction, MediaLoadedInfo } from '../src/types';
import {
EventViewMedia,
ReviewViewMedia,
@@ -658,7 +654,7 @@ export const createCardAPI = (): CardController => {
api.getCardElementManager.mockReturnValue(mock<CardElementManager>());
api.getConditionStateManager.mockReturnValue(mock<ConditionStateManager>());
api.getConfigManager.mockReturnValue(mock<ConfigManager>());
api.getEffectsControllerAPI.mockReturnValue(mock<EffectsControllerAPI>());
api.getEffectsManager.mockReturnValue(mock<EffectsManager>());
api.getEntityRegistryManager.mockReturnValue(mock<EntityRegistryManager>());
api.getExpandManager.mockReturnValue(mock<ExpandManager>());
api.getFoldersManager.mockReturnValue(mock<FoldersManager>());
+8
View File
@@ -120,6 +120,14 @@ describe('getPTZTarget', () => {
type: 'digital',
});
});
it('without cameraID', () => {
const view = createView({
view: 'live',
camera: null,
});
expect(getPTZTarget(view, { cameraManager: createCameraManager() })).toBeNull();
});
});
describe('in non-media views', () => {
+12
View File
@@ -123,4 +123,16 @@ describe('generateScreenshotTitle', () => {
expect(generateScreenshotFilename(view)).toBe('media_camera-1.jpg');
});
it('should get title for live view without camera', () => {
expect(generateScreenshotFilename(createView({ view: 'live', camera: null }))).toBe(
'live_2023-06-13-21-54-01.jpg',
);
});
it('should get title for viewer view without camera', () => {
expect(generateScreenshotFilename(createView({ view: 'media', camera: null }))).toBe(
'media.jpg',
);
});
});
+28
View File
@@ -3,6 +3,7 @@ import {
getStreamCameraID,
hasSubstream,
removeSubstream,
setSubstream,
} from '../../src/utils/substream';
import { View } from '../../src/view/view';
import { createView } from '../test-utils';
@@ -62,6 +63,25 @@ describe('hasSubstream/getStreamCameraID', () => {
expect(getStreamCameraID(view, 'camera3')).toBe('camera3');
});
});
it('should correctly handle null cameras', () => {
expect(getStreamCameraID(createView({ camera: null }))).toBeNull();
expect(hasSubstream(createView({ camera: null }))).toBeFalsy();
});
});
describe('setSubstream', () => {
it('should set substream', () => {
const view = createView({ camera: 'camera1' });
setSubstream(view, 'substream1');
expect(view.context?.live?.overrides?.get('camera1')).toBe('substream1');
});
it('should return null without a camera', () => {
const view = createView({ camera: null });
setSubstream(view, 'foo');
expect(view.context).toBeNull();
});
});
describe('removeSubstream', () => {
@@ -100,4 +120,12 @@ describe('removeSubstream', () => {
},
});
});
it('should not remove substream without camera', () => {
const view = createView({
camera: null,
});
removeSubstream(view);
expect(view.context).toBeNull();
});
});
+42
View File
@@ -0,0 +1,42 @@
import { describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { CameraManager } from '../../../src/camera-manager/manager';
import { FoldersManager } from '../../../src/card-controller/folders/manager';
import { resolveViewName } from '../../../src/view/utils/resolve-default';
import { createStore } from '../../test-utils';
describe('resolveViewName', () => {
it('should return the view name directly if not auto', () => {
expect(resolveViewName('live', mock<CameraManager>(), mock<FoldersManager>())).toBe(
'live',
);
});
it('should return live if auto and cameras are present', () => {
const cameraManager = mock<CameraManager>();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([{ cameraID: 'camera-1' }]),
);
const foldersManager = mock<FoldersManager>();
expect(resolveViewName('auto', cameraManager, foldersManager)).toBe('live');
});
it('should return folders if auto, no cameras, but folders are present', () => {
const cameraManager = mock<CameraManager>();
vi.mocked(cameraManager.getStore).mockReturnValue(createStore([]));
const foldersManager = mock<FoldersManager>();
vi.mocked(foldersManager.hasFolders).mockReturnValue(true);
expect(resolveViewName('auto', cameraManager, foldersManager)).toBe('folders');
});
it('should return image if auto and nothing is present', () => {
const cameraManager = mock<CameraManager>();
vi.mocked(cameraManager.getStore).mockReturnValue(createStore([]));
const foldersManager = mock<FoldersManager>();
vi.mocked(foldersManager.hasFolders).mockReturnValue(false);
expect(resolveViewName('auto', cameraManager, foldersManager)).toBe('image');
});
});
+131 -37
View File
@@ -4,18 +4,19 @@ import { FoldersManager } from '../../src/card-controller/folders/manager';
import { AdvancedCameraCardView } from '../../src/config/schema/common/const';
import { CapabilityKey } from '../../src/types';
import {
getCameraIDsForViewName,
getCameraIDsWithCapabilityForView,
isViewAvailable,
isViewSupported,
isViewSupportedByCamera,
} from '../../src/view/view-support';
import {
createCameraConfig,
createCameraManager,
createCapabilities,
createFolder,
createStore,
} from '../test-utils';
describe('getCameraIDsForViewName', () => {
describe('getCameraIDsWithCapabilityForView', () => {
describe('views that are always supported', () => {
it.each([['diagnostics' as const], ['image' as const]])(
'%s',
@@ -32,14 +33,24 @@ describe('getCameraIDsForViewName', () => {
);
const foldersManager = mock<FoldersManager>();
expect(getCameraIDsForViewName(viewName, cameraManager, foldersManager)).toEqual(
new Set(['camera-1', 'camera-2']),
);
expect(
getCameraIDsForViewName(viewName, cameraManager, foldersManager, 'camera-1'),
getCameraIDsWithCapabilityForView(viewName, cameraManager, foldersManager),
).toEqual(new Set(['camera-1', 'camera-2']));
expect(
getCameraIDsForViewName(viewName, cameraManager, foldersManager, 'camera-2'),
getCameraIDsWithCapabilityForView(
viewName,
cameraManager,
foldersManager,
'camera-1',
),
).toEqual(new Set(['camera-1', 'camera-2']));
expect(
getCameraIDsWithCapabilityForView(
viewName,
cameraManager,
foldersManager,
'camera-2',
),
).toEqual(new Set(['camera-1', 'camera-2']));
},
);
@@ -76,9 +87,9 @@ describe('getCameraIDsForViewName', () => {
);
const foldersManager = mock<FoldersManager>();
expect(getCameraIDsForViewName(viewName, cameraManager, foldersManager)).toEqual(
new Set(['camera-2']),
);
expect(
getCameraIDsWithCapabilityForView(viewName, cameraManager, foldersManager),
).toEqual(new Set(['camera-2']));
});
it.each([
@@ -106,20 +117,14 @@ describe('getCameraIDsForViewName', () => {
);
const foldersManager = mock<FoldersManager>();
expect(getCameraIDsForViewName(viewName, cameraManager, foldersManager)).toEqual(
new Set(['camera-1', 'camera-2']),
);
expect(
getCameraIDsForViewName(viewName, cameraManager, foldersManager, 'camera-1'),
).toEqual(new Set(['camera-1', 'camera-2']));
expect(
getCameraIDsForViewName(viewName, cameraManager, foldersManager, 'camera-2'),
).toEqual(new Set(['camera-2']));
getCameraIDsWithCapabilityForView(viewName, cameraManager, foldersManager),
).toEqual(new Set(['camera-2', 'camera-1']));
});
});
describe('views that respect a folder', () => {
describe('should return cameras when a folder is present', () => {
describe('folder views', () => {
describe('should return all cameras when a folder is present', () => {
it.each([
['folder' as const],
['folders' as const],
@@ -140,16 +145,10 @@ describe('getCameraIDsForViewName', () => {
]),
);
const foldersManager = mock<FoldersManager>();
vi.mocked(foldersManager.getFolder).mockReturnValue(createFolder());
vi.mocked(foldersManager.hasFolders).mockReturnValue(true);
expect(getCameraIDsForViewName(viewName, cameraManager, foldersManager)).toEqual(
new Set(['camera-1', 'camera-2']),
);
expect(
getCameraIDsForViewName(viewName, cameraManager, foldersManager, 'camera-1'),
).toEqual(new Set(['camera-1', 'camera-2']));
expect(
getCameraIDsForViewName(viewName, cameraManager, foldersManager, 'camera-2'),
getCameraIDsWithCapabilityForView(viewName, cameraManager, foldersManager),
).toEqual(new Set(['camera-1', 'camera-2']));
});
});
@@ -175,22 +174,63 @@ describe('getCameraIDsForViewName', () => {
]),
);
const foldersManager = mock<FoldersManager>();
vi.mocked(foldersManager.getFolder).mockReturnValue(null);
vi.mocked(foldersManager.hasFolders).mockReturnValue(false);
expect(getCameraIDsForViewName(viewName, cameraManager, foldersManager)).toEqual(
new Set(),
);
expect(
getCameraIDsForViewName(viewName, cameraManager, foldersManager, 'camera-1'),
).toEqual(new Set());
expect(
getCameraIDsForViewName(viewName, cameraManager, foldersManager, 'camera-2'),
getCameraIDsWithCapabilityForView(viewName, cameraManager, foldersManager),
).toEqual(new Set());
});
});
});
});
describe('isViewAvailable', () => {
it('should return true when cameras are present for camera view', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([{ cameraID: 'camera-1' }]),
);
const foldersManager = mock<FoldersManager>();
expect(isViewAvailable('live', cameraManager, foldersManager)).toBe(true);
});
it('should return false when cameras are missing for camera view', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(createStore([]));
const foldersManager = mock<FoldersManager>();
expect(isViewAvailable('live', cameraManager, foldersManager)).toBe(false);
});
it('should return true when folders are present for folder view', () => {
const cameraManager = createCameraManager();
const foldersManager = mock<FoldersManager>();
vi.mocked(foldersManager.hasFolders).mockReturnValue(true);
expect(isViewAvailable('folder', cameraManager, foldersManager)).toBe(true);
});
it('should return false when folders are missing for folder view', () => {
const cameraManager = createCameraManager();
const foldersManager = mock<FoldersManager>();
vi.mocked(foldersManager.hasFolders).mockReturnValue(false);
expect(isViewAvailable('folder', cameraManager, foldersManager)).toBe(false);
});
it('should return true when either covers it', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(createStore([]));
const foldersManager = mock<FoldersManager>();
vi.mocked(foldersManager.hasFolders).mockReturnValue(true);
expect(isViewAvailable('gallery', cameraManager, foldersManager)).toBe(true);
});
it('should return true for diagnostics and image view', () => {
const cameraManager = createCameraManager();
const foldersManager = mock<FoldersManager>();
expect(isViewAvailable('diagnostics', cameraManager, foldersManager)).toBe(true);
expect(isViewAvailable('image', cameraManager, foldersManager)).toBe(true);
});
});
describe('isViewSupportedByCamera', () => {
it('should return true for supported view', () => {
const cameraManager = createCameraManager();
@@ -226,3 +266,57 @@ describe('isViewSupportedByCamera', () => {
).toBe(false);
});
});
describe('isViewSupported', () => {
it('should return true when view is available and camera is null', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([{ cameraID: 'camera-1' }]),
);
const foldersManager = mock<FoldersManager>();
expect(isViewSupported('live', cameraManager, foldersManager, null)).toBe(true);
});
it('should return true when view is available and camera is supported', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera-1',
capabilities: createCapabilities({ live: true }),
},
]),
);
const foldersManager = mock<FoldersManager>();
expect(isViewSupported('live', cameraManager, foldersManager, 'camera-1')).toBe(
true,
);
});
it('should return false when view is available but camera is not supported', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera-1',
capabilities: createCapabilities({ live: false }),
},
]),
);
const foldersManager = mock<FoldersManager>();
expect(isViewSupported('live', cameraManager, foldersManager, 'camera-1')).toBe(
false,
);
});
it('should return false when view is not available', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(createStore([]));
const foldersManager = mock<FoldersManager>();
expect(isViewSupported('live', cameraManager, foldersManager, null)).toBe(false);
});
});