feat: Make cameras optional (#2343)
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user