refactor: Significantly refactor how conditions work internally (#1886)

- Much improved API cleanliness and testability to allow further
extensibility in future
 - Simplified code in `live` view

This technically contains a small change in how overrides work in the
`live` view. Since that change is _closer_ to the documentation, and
since this is likely to be rarely used, this is not considered a
breaking change. Previously, overrides for a given live camera would
always render _as if_ that camera was selected, vs was actually
selected. Now, overrides will only apply in the live view when the
camera is _actually_ selected. If this is an issue for you in practice,
lets discuss.
This commit is contained in:
Dermot Duffy
2025-02-11 20:07:55 -08:00
committed by GitHub
parent a92074bc8d
commit 8d3cf07b43
51 changed files with 2443 additions and 2118 deletions
+100 -59
View File
@@ -1,8 +1,9 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { AutomationsManager } from '../../src/card-controller/automations-manager.js';
import { createCardAPI } from '../test-utils.js';
import { ActionType } from '../../src/config/types.js';
import { AuxillaryActionConfig } from '../../src/card-controller/actions/types.js';
import { AutomationsManager } from '../../src/card-controller/automations-manager.js';
import { ConditionStateManager } from '../../src/conditions/state-manager.js';
import { ActionType } from '../../src/config/types.js';
import { createCardAPI } from '../test-utils.js';
describe('AutomationsManager', () => {
const actions = [
@@ -25,39 +26,55 @@ describe('AutomationsManager', () => {
vi.clearAllMocks();
});
it('should do nothing without hass', () => {
const api = createCardAPI();
describe('should not execute actions', () => {
it('should do nothing without hass', () => {
const api = createCardAPI();
const stateManager = new ConditionStateManager();
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
const automationsManager = new AutomationsManager(api);
automationsManager.execute();
const automationsManager = new AutomationsManager(api);
automationsManager.addAutomations([automation]);
expect(api.getActionsManager().executeActions).not.toBeCalled();
});
stateManager.setState({ fullscreen: true });
it('should do nothing without being initialized', () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().hasHASS).mockReturnValue(true);
vi.mocked(api.getInitializationManager().isInitializedMandatory).mockReturnValue(
false,
);
expect(api.getActionsManager().executeActions).not.toBeCalled();
});
const automationsManager = new AutomationsManager(api);
automationsManager.execute();
it('should do nothing without being initialized', () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().hasHASS).mockReturnValue(true);
vi.mocked(api.getInitializationManager().isInitializedMandatory).mockReturnValue(
false,
);
const stateManager = new ConditionStateManager();
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
expect(api.getActionsManager().executeActions).not.toBeCalled();
});
const automationsManager = new AutomationsManager(api);
automationsManager.addAutomations([automation]);
it('should do nothing without automations', () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().hasHASS).mockReturnValue(true);
vi.mocked(api.getInitializationManager().isInitializedMandatory).mockReturnValue(
true,
);
stateManager.setState({ fullscreen: true });
const automationsManager = new AutomationsManager(api);
automationsManager.execute();
expect(api.getActionsManager().executeActions).not.toBeCalled();
});
expect(api.getActionsManager().executeActions).not.toBeCalled();
it('should do nothing with an error message present', () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().hasHASS).mockReturnValue(true);
vi.mocked(api.getInitializationManager().isInitializedMandatory).mockReturnValue(
true,
);
vi.mocked(api.getMessageManager().hasErrorMessage).mockReturnValue(true);
const stateManager = new ConditionStateManager();
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
const automationsManager = new AutomationsManager(api);
automationsManager.addAutomations([automation]);
stateManager.setState({ fullscreen: true });
expect(api.getActionsManager().executeActions).not.toBeCalled();
});
});
it('should execute actions', () => {
@@ -66,31 +83,25 @@ describe('AutomationsManager', () => {
vi.mocked(api.getInitializationManager().isInitializedMandatory).mockReturnValue(
true,
);
const stateManager = new ConditionStateManager();
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
const automationsManager = new AutomationsManager(api);
automationsManager.addAutomations([automation]);
automationsManager.execute();
expect(api.getActionsManager().executeActions).not.toBeCalled();
stateManager.setState({ fullscreen: true });
vi.mocked(api.getConditionsManager().evaluateConditions).mockReturnValue(true);
automationsManager.execute();
expect(api.getActionsManager().executeActions).toBeCalledTimes(1);
// Automation will not re-fire when condition continues to evaluate the
// same.
automationsManager.execute();
stateManager.setState({ fullscreen: true });
expect(api.getActionsManager().executeActions).toBeCalledTimes(1);
vi.mocked(api.getConditionsManager().evaluateConditions).mockReturnValue(false);
automationsManager.execute();
stateManager.setState({ fullscreen: false });
expect(api.getActionsManager().executeActions).toBeCalledTimes(1);
vi.mocked(api.getConditionsManager().evaluateConditions).mockReturnValue(true);
automationsManager.execute();
stateManager.setState({ fullscreen: true });
expect(api.getActionsManager().executeActions).toBeCalledTimes(2);
});
@@ -100,14 +111,16 @@ describe('AutomationsManager', () => {
vi.mocked(api.getInitializationManager().isInitializedMandatory).mockReturnValue(
true,
);
vi.mocked(api.getConditionsManager().evaluateConditions).mockReturnValue(false);
const stateManager = new ConditionStateManager();
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
const automationsManager = new AutomationsManager(api);
automationsManager.addAutomations([not_automation]);
automationsManager.execute();
stateManager.setState({ fullscreen: true });
expect(api.getActionsManager().executeActions).not.toBeCalled();
stateManager.setState({ fullscreen: false });
expect(api.getActionsManager().executeActions).toBeCalled();
});
@@ -117,12 +130,23 @@ describe('AutomationsManager', () => {
vi.mocked(api.getInitializationManager().isInitializedMandatory).mockReturnValue(
true,
);
const stateManager = new ConditionStateManager();
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
const automationsManager = new AutomationsManager(api);
automationsManager.addAutomations([automation, not_automation]);
automationsManager.addAutomations([
{
conditions: [{ condition: 'fullscreen' as const, fullscreen: true }],
actions: actions,
},
{
conditions: [{ condition: 'fullscreen' as const, fullscreen: false }],
actions_not: actions,
},
]);
// Create a setup where one automation action causes another...
let evaluation = true;
let fullscreen = true;
vi.mocked(api.getActionsManager().executeActions).mockImplementation(
async (
@@ -131,17 +155,12 @@ describe('AutomationsManager', () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_config?: AuxillaryActionConfig,
): Promise<void> => {
evaluation = !evaluation;
vi.mocked(api.getConditionsManager().evaluateConditions).mockReturnValue(
evaluation,
);
automationsManager.execute();
fullscreen = !fullscreen;
stateManager.setState({ fullscreen: fullscreen });
},
);
vi.mocked(api.getConditionsManager().evaluateConditions).mockReturnValue(evaluation);
automationsManager.execute();
stateManager.setState({ fullscreen: fullscreen });
expect(api.getMessageManager().setMessageIfHigherPriority).toBeCalledWith(
expect.objectContaining({
@@ -160,18 +179,40 @@ describe('AutomationsManager', () => {
vi.mocked(api.getInitializationManager().isInitializedMandatory).mockReturnValue(
true,
);
const stateManager = new ConditionStateManager();
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
const automationsManager = new AutomationsManager(api);
automationsManager.addAutomations([automation]);
automationsManager.addAutomations([
{
conditions: [{ condition: 'expand' as const, expand: true }],
actions: actions,
},
{
conditions: [{ condition: 'fullscreen' as const, fullscreen: true }],
actions: actions,
tag: 'fullscreen',
},
]);
vi.mocked(api.getConditionsManager().evaluateConditions).mockReturnValue(true);
automationsManager.execute();
stateManager.setState({ fullscreen: true });
expect(api.getActionsManager().executeActions).toBeCalledTimes(1);
// Delete the fullscreen automation.
automationsManager.deleteAutomations('fullscreen');
stateManager.setState({ fullscreen: false });
stateManager.setState({ fullscreen: true });
expect(api.getActionsManager().executeActions).toBeCalledTimes(1);
stateManager.setState({ expand: true });
expect(api.getActionsManager().executeActions).toBeCalledTimes(2);
// Delete all automations.
automationsManager.deleteAutomations();
automationsManager.execute();
expect(api.getActionsManager().executeActions).toBeCalledTimes(1);
stateManager.setState({ fullscreen: false });
stateManager.setState({ fullscreen: true });
expect(api.getActionsManager().executeActions).toBeCalledTimes(2);
});
});
File diff suppressed because it is too large Load Diff
@@ -1,12 +1,10 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ZodError } from 'zod';
import { getOverriddenConfig } from '../../../src/card-controller/conditions-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 { advancedCameraCardConfigSchema } from '../../../src/config/types';
import { createCardAPI, createConfig, flushPromises } from '../../test-utils';
vi.mock('../../../src/card-controller/conditions-manager.js');
import { createCardAPI, flushPromises } from '../../test-utils';
describe('ConfigManager', () => {
beforeEach(() => {
@@ -84,8 +82,7 @@ describe('ConfigManager', () => {
expect(manager.getConfig()?.menu.alignment).toBe('left');
// Verify appropriate API calls are made.
expect(api.getConditionsManager().setConditionsFromConfig).toBeCalled();
expect(api.getConditionsManager().setState).toBeCalledWith({
expect(api.getConditionStateManager().setState).toBeCalledWith({
view: undefined,
displayMode: undefined,
camera: undefined,
@@ -165,179 +162,222 @@ describe('ConfigManager', () => {
});
});
it('should ignore overrides without a config', () => {
const api = createCardAPI();
const manager = new ConfigManager(api);
manager.computeOverrideConfig();
expect(manager.getConfig()).toBeNull();
expect(api.getStyleManager().updateFromConfig).not.toBeCalled();
});
it('should ignore overrides with same config', () => {
const api = createCardAPI();
const stateManager = new ConditionStateManager();
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
const manager = new ConfigManager(api);
const cameras = [{ camera_entity: 'camera.office' }];
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
cameras: cameras,
overrides: [
{
conditions: [{ condition: 'fullscreen', fullscreen: true }],
set: {
// Override with the same.
cameras: cameras,
},
},
],
};
vi.mocked(getOverriddenConfig).mockReturnValue(config);
manager.setConfig(config);
expect(api.getStyleManager().updateFromConfig).toBeCalled();
vi.mocked(api.getStyleManager().updateFromConfig).mockClear();
manager.computeOverrideConfig();
expect(api.getStyleManager().updateFromConfig).toBeCalledTimes(1);
expect(api.getStyleManager().updateFromConfig).not.toBeCalled();
stateManager.setState({ fullscreen: true });
expect(api.getStyleManager().updateFromConfig).toBeCalledTimes(1);
});
it('should override', () => {
const api = createCardAPI();
const stateManager = new ConditionStateManager();
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
const manager = new ConfigManager(api);
const config_1 = {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
menu: {
style: 'hidden',
},
overrides: [
{
conditions: [{ condition: 'fullscreen', fullscreen: true }],
set: { 'menu.style': 'none' },
},
],
};
manager.setConfig(config_1);
vi.mocked(api.getStyleManager().updateFromConfig).mockClear();
const config_2 = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.kitchen' }],
};
vi.mocked(getOverriddenConfig).mockReturnValue(config_2);
manager.computeOverrideConfig();
manager.setConfig(config);
expect(manager.getConfig()?.menu?.style).toBe('hidden');
expect(api.getStyleManager().updateFromConfig).toBeCalled();
expect(api.getCardElementManager().update).toBeCalled();
stateManager.setState({ fullscreen: true });
expect(manager.getConfig()?.menu?.style).toBe('none');
expect(manager.getConfig()).not.toEqual(manager.getNonOverriddenConfig());
});
it('should set error on invalid override', () => {
const api = createCardAPI();
const stateManager = new ConditionStateManager();
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
const manager = new ConfigManager(api);
manager.setConfig({
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
});
overrides: [
{
conditions: [{ condition: 'fullscreen', fullscreen: true }],
delete: ['cameras'],
},
],
};
const error = new Error('Invalid override configuration');
vi.mocked(getOverriddenConfig).mockImplementation(() => {
throw error;
});
manager.setConfig(config);
expect(manager.getConfig()).not.toBeNull();
manager.computeOverrideConfig();
expect(api.getMessageManager().setErrorIfHigherPriority).toBeCalledWith(error);
stateManager.setState({ fullscreen: true });
expect(manager.getConfig()).not.toBeNull();
expect(api.getMessageManager().setErrorIfHigherPriority).toBeCalledWith(
expect.objectContaining({ message: 'Invalid override configuration' }),
);
});
describe('should uninitialize on override', () => {
it('cameras', () => {
const api = createCardAPI();
const stateManager = new ConditionStateManager();
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
const manager = new ConfigManager(api);
const config_1 = {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
overrides: [
{
conditions: [{ condition: 'fullscreen', fullscreen: true }],
set: {
cameras: [{ camera_entity: 'camera.kitchen' }],
},
},
],
};
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_1));
manager.setConfig(config_1);
expect(api.getInitializationManager().uninitialize).toHaveBeenLastCalledWith(
InitializationAspect.VIEW,
manager.setConfig(config);
expect(api.getInitializationManager().uninitialize).not.toHaveBeenCalledWith(
InitializationAspect.CAMERAS,
);
const config_2 = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.kitchen' }],
};
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_2));
manager.computeOverrideConfig();
stateManager.setState({ fullscreen: true });
expect(api.getInitializationManager().uninitialize).toHaveBeenLastCalledWith(
expect(api.getInitializationManager().uninitialize).toHaveBeenCalledWith(
InitializationAspect.CAMERAS,
);
});
it('cameras_global', () => {
const api = createCardAPI();
const stateManager = new ConditionStateManager();
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
const manager = new ConfigManager(api);
const config_1 = {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
overrides: [
{
conditions: [{ condition: 'fullscreen', fullscreen: true }],
set: {
cameras_global: { live_provider: 'jsmpeg' },
},
},
],
};
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_1));
manager.setConfig(config_1);
expect(api.getInitializationManager().uninitialize).toHaveBeenLastCalledWith(
InitializationAspect.VIEW,
manager.setConfig(config);
expect(api.getInitializationManager().uninitialize).not.toHaveBeenCalledWith(
InitializationAspect.CAMERAS,
);
const config_2 = {
...config_1,
cameras_global: {
live_provider: 'jsmpeg',
},
};
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_2));
manager.computeOverrideConfig();
stateManager.setState({ fullscreen: true });
expect(api.getInitializationManager().uninitialize).toHaveBeenLastCalledWith(
expect(api.getInitializationManager().uninitialize).toHaveBeenCalledWith(
InitializationAspect.CAMERAS,
);
});
it('live.microphone.always_connected', () => {
const api = createCardAPI();
const stateManager = new ConditionStateManager();
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
const manager = new ConfigManager(api);
const config_1 = {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
live: {
microphone: {
always_connected: false,
overrides: [
{
conditions: [{ condition: 'fullscreen', fullscreen: true }],
set: {
'live.microphone.always_connected': true,
},
},
},
],
};
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_1));
manager.setConfig(config_1);
expect(api.getInitializationManager().uninitialize).toHaveBeenLastCalledWith(
InitializationAspect.VIEW,
manager.setConfig(config);
expect(api.getInitializationManager().uninitialize).not.toHaveBeenCalledWith(
InitializationAspect.MICROPHONE_CONNECT,
);
const config_2 = {
...config_1,
live: {
microphone: {
always_connected: true,
},
},
};
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_2));
manager.computeOverrideConfig();
stateManager.setState({ fullscreen: true });
expect(api.getInitializationManager().uninitialize).toHaveBeenLastCalledWith(
expect(api.getInitializationManager().uninitialize).toHaveBeenCalledWith(
InitializationAspect.MICROPHONE_CONNECT,
);
});
});
it('should initialize background items', async () => {
const api = createCardAPI();
const manager = new ConfigManager(api);
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
};
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config));
describe('should initialize on override', () => {
it('should initialize background items', async () => {
const api = createCardAPI();
const stateManager = new ConditionStateManager();
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
manager.setConfig(config);
const manager = new ConfigManager(api);
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
overrides: [
{
conditions: [{ condition: 'fullscreen', fullscreen: true }],
set: {
cameras: [{ camera_entity: 'camera.kitchen' }],
},
},
],
};
await flushPromises();
manager.setConfig(config);
expect(api.getDefaultManager().initializeIfNecessary).toBeCalledWith(null);
expect(api.getMediaPlayerManager().initializeIfNecessary).toBeCalledWith(null);
await flushPromises();
expect(api.getDefaultManager().initializeIfNecessary).toBeCalledTimes(1);
expect(api.getMediaPlayerManager().initializeIfNecessary).toBeCalledTimes(1);
stateManager.setState({ fullscreen: true });
await flushPromises();
expect(api.getDefaultManager().initializeIfNecessary).toBeCalledTimes(2);
expect(api.getMediaPlayerManager().initializeIfNecessary).toBeCalledTimes(2);
});
});
});
@@ -0,0 +1,305 @@
import { describe, expect, it } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { z } from 'zod';
import { getOverriddenConfig } from '../../../src/card-controller/config/get-overridden-config';
import { ConditionsManagerReadonlyInterface } from '../../../src/conditions/types';
describe('getOverriddenConfig', () => {
const config = {
menu: {
style: 'none',
},
};
it('should not override without overrides', () => {
const manager = mock<ConditionsManagerReadonlyInterface>();
manager.getEvaluation.mockReturnValue({ result: true });
expect(getOverriddenConfig(manager, config)).toBe(config);
});
it('should not override when conditions do not match', () => {
const manager = mock<ConditionsManagerReadonlyInterface>();
manager.getEvaluation.mockReturnValue({ result: false });
expect(
getOverriddenConfig(manager, config, {
configOverrides: [
{
merge: {
menu: {
style: 'hidden',
},
},
delete: ['menu.style'],
set: {
'menu.style': 'overlay',
},
conditions: [
{
condition: 'fullscreen' as const,
fullscreen: true,
},
],
},
],
}),
).toBe(config);
});
describe('should merge', () => {
it('with path', () => {
const manager = mock<ConditionsManagerReadonlyInterface>();
manager.getEvaluation.mockReturnValue({ result: true });
expect(
getOverriddenConfig(manager, config, {
configOverrides: [
{
merge: {
'live.controls.thumbnails': {
mode: 'none',
},
},
conditions: [
{
condition: 'fullscreen' as const,
fullscreen: true,
},
],
},
],
}),
).toEqual({
menu: {
style: 'none',
},
live: {
controls: {
thumbnails: {
mode: 'none',
},
},
},
});
});
it('without path', () => {
const manager = mock<ConditionsManagerReadonlyInterface>();
manager.getEvaluation.mockReturnValue({ result: true });
expect(
getOverriddenConfig(manager, config, {
configOverrides: [
{
merge: {
menu: {
style: 'hidden',
},
},
conditions: [
{
condition: 'fullscreen' as const,
fullscreen: true,
},
],
},
],
}),
).toEqual({
menu: {
style: 'hidden',
},
});
});
it('with invalid merge', () => {
const manager = mock<ConditionsManagerReadonlyInterface>();
manager.getEvaluation.mockReturnValue({ result: true });
expect(
getOverriddenConfig(manager, config, {
configOverrides: [
{
merge: 6 as unknown as Record<string, unknown>,
conditions: [
{
condition: 'fullscreen' as const,
fullscreen: true,
},
],
},
],
}),
).toEqual({
menu: {
style: 'none',
},
});
});
});
describe('should set', () => {
it('leaf node', () => {
const manager = mock<ConditionsManagerReadonlyInterface>();
manager.getEvaluation.mockReturnValue({ result: true });
expect(
getOverriddenConfig(manager, config, {
configOverrides: [
{
set: {
'menu.style': 'hidden',
},
conditions: [
{
condition: 'fullscreen' as const,
fullscreen: true,
},
],
},
],
}),
).toEqual({
menu: {
style: 'hidden',
},
});
});
it('root node', () => {
const manager = mock<ConditionsManagerReadonlyInterface>();
manager.getEvaluation.mockReturnValue({ result: true });
expect(
getOverriddenConfig(manager, config, {
configOverrides: [
{
set: {
menu: {
style: 'hidden',
},
},
conditions: [
{
condition: 'fullscreen' as const,
fullscreen: true,
},
],
},
],
}),
).toEqual({
menu: {
style: 'hidden',
},
});
});
});
describe('should delete', () => {
it('leaf node', () => {
const manager = mock<ConditionsManagerReadonlyInterface>();
manager.getEvaluation.mockReturnValue({ result: true });
expect(
getOverriddenConfig(manager, config, {
configOverrides: [
{
delete: ['menu.style' as const],
conditions: [
{
condition: 'fullscreen' as const,
fullscreen: true,
},
],
},
],
}),
).toEqual({
menu: {},
});
});
it('root node', () => {
const manager = mock<ConditionsManagerReadonlyInterface>();
manager.getEvaluation.mockReturnValue({ result: true });
expect(
getOverriddenConfig(manager, config, {
configOverrides: [
{
delete: ['menu' as const],
conditions: [
{
condition: 'fullscreen' as const,
fullscreen: true,
},
],
},
],
}),
).toEqual({});
});
});
describe('should validate schema', () => {
const testSchema = z.object({
menu: z.object({
style: z.enum(['none', 'hidden']),
}),
});
it('passing', () => {
const manager = mock<ConditionsManagerReadonlyInterface>();
manager.getEvaluation.mockReturnValue({ result: true });
expect(
getOverriddenConfig(manager, config, {
configOverrides: [
{
conditions: [
{
condition: 'fullscreen' as const,
fullscreen: true,
},
],
set: {
'menu.style': 'hidden',
},
},
],
schema: testSchema,
}),
).toEqual({
menu: {
style: 'hidden',
},
});
});
it('failing', () => {
const manager = mock<ConditionsManagerReadonlyInterface>();
manager.getEvaluation.mockReturnValue({ result: true });
expect(() =>
getOverriddenConfig(manager, config, {
configOverrides: [
{
conditions: [
{
condition: 'fullscreen' as const,
fullscreen: true,
},
],
set: {
'menu.style': 'NOT_A_STYLE',
},
},
],
schema: testSchema,
}),
).toThrowError(/Invalid override configuration/);
});
});
});
+7 -14
View File
@@ -7,7 +7,6 @@ import {
CardElementManager,
CardHTMLElement,
} from '../../src/card-controller/card-element-manager';
import { ConditionsManager } from '../../src/card-controller/conditions-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';
@@ -27,6 +26,7 @@ import { StatusBarItemManager } from '../../src/card-controller/status-bar-item-
import { StyleManager } from '../../src/card-controller/style-manager';
import { TriggersManager } from '../../src/card-controller/triggers-manager';
import { ViewManager } from '../../src/card-controller/view/view-manager';
import { ConditionStateManager } from '../../src/conditions/state-manager';
import { AdvancedCameraCardEditor } from '../../src/editor';
import { DeviceRegistryManager } from '../../src/utils/ha/registry/device';
import { EntityRegistryManager } from '../../src/utils/ha/registry/entity';
@@ -37,7 +37,6 @@ vi.mock('../../src/card-controller/actions/actions-manager');
vi.mock('../../src/card-controller/automations-manager');
vi.mock('../../src/card-controller/camera-url-manager');
vi.mock('../../src/card-controller/card-element-manager');
vi.mock('../../src/card-controller/conditions-manager');
vi.mock('../../src/card-controller/config/config-manager');
vi.mock('../../src/card-controller/default-manager');
vi.mock('../../src/card-controller/download-manager');
@@ -56,6 +55,7 @@ vi.mock('../../src/card-controller/status-bar-item-manager');
vi.mock('../../src/card-controller/style-manager');
vi.mock('../../src/card-controller/triggers-manager');
vi.mock('../../src/card-controller/view/view-manager');
vi.mock('../../src/conditions/state-manager');
vi.mock('../../src/utils/ha/registry/device');
vi.mock('../../src/utils/ha/registry/entity');
vi.mock('../../src/utils/ha/resolved-media');
@@ -67,7 +67,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
@@ -80,16 +80,9 @@ describe('CardController', () => {
const element = createCardElement();
const scrollCallback = vi.fn();
const menuToggleCallback = vi.fn();
const conditionListener = vi.fn();
const manager = new CardController(
element,
scrollCallback,
menuToggleCallback,
conditionListener,
);
const manager = new CardController(element, scrollCallback, menuToggleCallback);
expect(ConditionsManager).toBeCalledWith(manager, conditionListener);
expect(CardElementManager).toBeCalledWith(
manager,
element,
@@ -135,9 +128,9 @@ describe('CardController', () => {
);
});
it('getConditionsManager', () => {
expect(createController().getConditionsManager()).toBe(
vi.mocked(ConditionsManager).mock.instances[0],
it('ConditionStateManager', () => {
expect(createController().getConditionStateManager()).toBe(
vi.mocked(ConditionStateManager).mock.instances[0],
);
});
+2 -2
View File
@@ -14,7 +14,7 @@ describe('ExpandManager', () => {
const manager = new ExpandManager(api);
manager.initialize();
expect(api.getConditionsManager().setState).toBeCalledWith({ expand: false });
expect(api.getConditionStateManager().setState).toBeCalledWith({ expand: false });
});
it('should set expanded', () => {
@@ -26,7 +26,7 @@ describe('ExpandManager', () => {
expect(manager.isExpanded()).toBeTruthy();
expect(api.getFullscreenManager().setFullscreen).toBeCalledWith(false);
expect(api.getConditionsManager().setState).toBeCalledWith({ expand: true });
expect(api.getConditionStateManager().setState).toBeCalledWith({ expand: true });
expect(api.getCardElementManager().update).toBeCalled();
});
@@ -17,7 +17,9 @@ describe('FullscreenManager', () => {
const manager = new FullscreenManager(api, mock<FullscreenProvider>());
manager.initialize();
expect(api.getConditionsManager().setState).toBeCalledWith({ fullscreen: false });
expect(api.getConditionStateManager().setState).toBeCalledWith({
fullscreen: false,
});
});
it('should correctly determine whether in fullscreen', () => {
@@ -133,7 +135,7 @@ describe('FullscreenManager', () => {
handler();
expect(api.getConditionsManager().setState).toBeCalledWith({
expect(api.getConditionStateManager().setState).toBeCalledWith({
fullscreen: fullscreen,
});
});
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { CardController } from '../../../../src/card-controller/controller';
import { WebkitFullScreenProvider } from '../../../../src/card-controller/fullscreen/webkit';
import { ConditionStateManager } from '../../../../src/conditions/state-manager';
import {
AdvancedCameraCardMediaPlayer,
WebkitHTMLVideoElement,
@@ -37,7 +38,7 @@ describe('WebkitFullScreenProvider', () => {
provider.connect();
expect(api.getConditionsManager().addListener).toBeCalledWith(expect.anything());
expect(api.getConditionStateManager().addListener).toBeCalledWith(expect.anything());
});
it('should disconnect', () => {
@@ -46,7 +47,9 @@ describe('WebkitFullScreenProvider', () => {
provider.disconnect();
expect(api.getConditionsManager().removeListener).toBeCalledWith(expect.anything());
expect(api.getConditionStateManager().removeListener).toBeCalledWith(
expect.anything(),
);
});
describe('should return if in fullscreen', () => {
@@ -152,20 +155,19 @@ describe('WebkitFullScreenProvider', () => {
(event: string) => {
const handler = vi.fn();
const api = createCardAPI();
const stateManager = new ConditionStateManager();
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
const provider = new WebkitFullScreenProvider(api, handler);
provider.connect();
const conditionChangeHandler = vi.mocked(
api.getConditionsManager().addListener,
).mock.calls[0][0];
const element_1 = createWebkitVideoElement();
const player_1 = mock<AdvancedCameraCardMediaPlayer>();
player_1.getFullscreenElement.mockReturnValue(element_1);
const mediaLoadedInfo_1 = createMediaLoadedInfo({ player: player_1 });
conditionChangeHandler({ mediaLoadedInfo: mediaLoadedInfo_1 });
stateManager.setState({ mediaLoadedInfo: mediaLoadedInfo_1 });
element_1.dispatchEvent(new Event(event));
@@ -176,10 +178,7 @@ describe('WebkitFullScreenProvider', () => {
player_2.getFullscreenElement.mockReturnValue(element_2);
const mediaLoadedInfo_2 = createMediaLoadedInfo({ player: player_2 });
conditionChangeHandler(
{ mediaLoadedInfo: mediaLoadedInfo_2 },
{ mediaLoadedInfo: mediaLoadedInfo_1 },
);
stateManager.setState({ mediaLoadedInfo: mediaLoadedInfo_2 });
element_2.dispatchEvent(new Event(event));
@@ -190,11 +189,10 @@ describe('WebkitFullScreenProvider', () => {
expect(handler).toBeCalledTimes(2);
// Test the media loaded info not changing.
conditionChangeHandler(
{ mediaLoadedInfo: mediaLoadedInfo_2 },
{ mediaLoadedInfo: mediaLoadedInfo_2 },
);
// Test the media loaded info changing, but the player not changing.
stateManager.setState({
mediaLoadedInfo: { ...mediaLoadedInfo_2, width: 101 },
});
// Events on the new element should still be handled.
element_2.dispatchEvent(new Event(event));
@@ -210,13 +208,13 @@ describe('WebkitFullScreenProvider', () => {
const handler = vi.fn();
const api = createCardAPI();
const stateManager = new ConditionStateManager();
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
const provider = new WebkitFullScreenProvider(api, handler);
provider.connect();
const conditionChangeHandler = vi.mocked(api.getConditionsManager().addListener).mock
.calls[0][0];
const element = createWebkitVideoElement();
element.play = vi.fn();
@@ -226,7 +224,7 @@ describe('WebkitFullScreenProvider', () => {
player.getFullscreenElement.mockReturnValue(element);
const mediaLoadedInfo = createMediaLoadedInfo({ player });
conditionChangeHandler({ mediaLoadedInfo });
stateManager.setState({ mediaLoadedInfo });
element.dispatchEvent(new Event('webkitendfullscreen'));
+13 -26
View File
@@ -47,35 +47,22 @@ describe('HASSManager', () => {
expect(api.getStyleManager().applyTheme).toBeCalled();
});
describe('should set condition manager state', () => {
it('positively', () => {
const api = createCardAPI();
const manager = new HASSManager(api);
vi.mocked(api.getConditionsManager().hasHAStateConditions).mockReturnValue(true);
it('should set condition manager state', () => {
const api = createCardAPI();
const manager = new HASSManager(api);
const states = { 'switch.foo': createStateEntity() };
const user = createUser({ id: 'user_1' });
const hass = createHASS(states, user);
const states = { 'switch.foo': createStateEntity() };
const user = createUser({ id: 'user_1' });
const hass = createHASS(states, user);
manager.setHASS(hass);
manager.setHASS(hass);
expect(api.getConditionsManager().setState).toBeCalledWith(
expect.objectContaining({
state: states,
user: user,
}),
);
});
it('negatively', () => {
const api = createCardAPI();
const manager = new HASSManager(api);
vi.mocked(api.getConditionsManager().hasHAStateConditions).mockReturnValue(false);
manager.setHASS(createHASS());
expect(api.getConditionsManager().setState).not.toBeCalled();
});
expect(api.getConditionStateManager().setState).toBeCalledWith(
expect.objectContaining({
state: states,
user: user,
}),
);
});
describe('should handle connection state change when', () => {
@@ -23,7 +23,9 @@ describe('InteractionManager', () => {
const manager = new InteractionManager(api);
manager.initialize();
expect(api.getConditionsManager().setState).toBeCalledWith({ interaction: false });
expect(api.getConditionStateManager().setState).toBeCalledWith({
interaction: false,
});
expect(element.getAttribute('interaction')).toBeNull();
});
@@ -62,11 +64,11 @@ describe('InteractionManager', () => {
vi.useFakeTimers();
vi.setSystemTime(start);
expect(api.getConditionsManager().setState).not.toBeCalled();
expect(api.getConditionStateManager().setState).not.toBeCalled();
manager.reportInteraction();
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
expect(api.getConditionStateManager().setState).toHaveBeenLastCalledWith(
expect.objectContaining({
interaction: true,
}),
@@ -77,7 +79,7 @@ describe('InteractionManager', () => {
vi.setSystemTime(add(start, { seconds: 10 }));
vi.runOnlyPendingTimers();
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
expect(api.getConditionStateManager().setState).toHaveBeenLastCalledWith(
expect.objectContaining({
interaction: false,
}),
@@ -17,7 +17,7 @@ describe('KeyboardStateManager', () => {
element.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
expect(api.getConditionsManager().setState).toHaveBeenCalledWith({
expect(api.getConditionStateManager().setState).toHaveBeenCalledWith({
keys: {
a: { state: 'down', ctrl: false, alt: false, meta: false, shift: false },
},
@@ -26,7 +26,7 @@ describe('KeyboardStateManager', () => {
element.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
// Duplicate keydown should not re-set the state.
expect(api.getConditionsManager().setState).toBeCalledTimes(1);
expect(api.getConditionStateManager().setState).toBeCalledTimes(1);
});
it('should set state on keyup', () => {
@@ -39,13 +39,13 @@ describe('KeyboardStateManager', () => {
element.dispatchEvent(new KeyboardEvent('keyup', { key: 'a' }));
// Key not held down in the first place should not update the state.
expect(api.getConditionsManager().setState).not.toBeCalled();
expect(api.getConditionStateManager().setState).not.toBeCalled();
element.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
element.dispatchEvent(new KeyboardEvent('keyup', { key: 'a' }));
expect(api.getConditionsManager().setState).toBeCalledTimes(2);
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith({
expect(api.getConditionStateManager().setState).toBeCalledTimes(2);
expect(api.getConditionStateManager().setState).toHaveBeenLastCalledWith({
keys: {
a: { state: 'up', ctrl: false, alt: false, meta: false, shift: false },
},
@@ -60,13 +60,13 @@ describe('KeyboardStateManager', () => {
manager.initialize();
element.dispatchEvent(new FocusEvent('blur'));
expect(api.getConditionsManager().setState).not.toBeCalled();
expect(api.getConditionStateManager().setState).not.toBeCalled();
element.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
element.dispatchEvent(new FocusEvent('blur'));
expect(api.getConditionsManager().setState).toBeCalledTimes(2);
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith({
expect(api.getConditionStateManager().setState).toBeCalledTimes(2);
expect(api.getConditionStateManager().setState).toHaveBeenLastCalledWith({
keys: {},
});
});
@@ -81,6 +81,6 @@ describe('KeyboardStateManager', () => {
element.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
expect(api.getConditionsManager().setState).not.toBeCalled();
expect(api.getConditionStateManager().setState).not.toBeCalled();
});
});
@@ -8,7 +8,7 @@ describe('MediaLoadedInfoManager', () => {
const manager = new MediaLoadedInfoManager(api);
manager.initialize();
expect(api.getConditionsManager().setState).toBeCalledWith({
expect(api.getConditionStateManager().setState).toBeCalledWith({
mediaLoadedInfo: null,
});
});
@@ -22,7 +22,7 @@ describe('MediaLoadedInfoManager', () => {
expect(manager.has()).toBeTruthy();
expect(manager.get()).toBe(mediaInfo);
expect(api.getConditionsManager().setState).toBeCalledWith(
expect(api.getConditionStateManager().setState).toBeCalledWith(
expect.objectContaining({ mediaLoadedInfo: mediaInfo }),
);
expect(api.getStyleManager().setExpandedMode).toBeCalled();
@@ -38,7 +38,7 @@ describe('MediaLoadedInfoManager', () => {
expect(manager.has()).toBeFalsy();
expect(manager.get()).toBeNull();
expect(api.getConditionsManager().setState).not.toBeCalled();
expect(api.getConditionStateManager().setState).not.toBeCalled();
});
it('should get last known', () => {
@@ -54,7 +54,7 @@ describe('MediaLoadedInfoManager', () => {
expect(manager.has()).toBeFalsy();
expect(manager.getLastKnown()).toBe(mediaLoadedInfo);
expect(api.getConditionsManager().setState).toBeCalledWith(
expect(api.getConditionStateManager().setState).toBeCalledWith(
expect.objectContaining({ mediaLoadedInfo }),
);
});
@@ -302,7 +302,7 @@ describe('MicrophoneManager', () => {
const manager = new MicrophoneManager(api);
manager.initialize();
expect(api.getConditionsManager().setState).toBeCalledWith({
expect(api.getConditionStateManager().setState).toBeCalledWith({
microphone: { connected: false, muted: true, forbidden: false, stream: undefined },
});
});
@@ -313,7 +313,7 @@ describe('MicrophoneManager', () => {
const stream = createMockStream();
vi.mocked(navigatorMock.mediaDevices.getUserMedia).mockResolvedValue(stream);
expect(api.getConditionsManager().setState).not.toBeCalled();
expect(api.getConditionStateManager().setState).not.toBeCalled();
await manager.connect();
@@ -325,7 +325,7 @@ describe('MicrophoneManager', () => {
};
expect(manager.getState()).toEqual(expectedState);
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
expect(api.getConditionStateManager().setState).toHaveBeenLastCalledWith(
expect.objectContaining({
microphone: expectedState,
}),
@@ -340,7 +340,7 @@ describe('MicrophoneManager', () => {
muted: false,
};
expect(manager.getState()).toEqual(expectedState);
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
expect(api.getConditionStateManager().setState).toHaveBeenLastCalledWith(
expect.objectContaining({
microphone: expectedState,
}),
@@ -355,7 +355,7 @@ describe('MicrophoneManager', () => {
muted: true,
};
expect(manager.getState()).toEqual(expectedState);
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
expect(api.getConditionStateManager().setState).toHaveBeenLastCalledWith(
expect.objectContaining({
microphone: expectedState,
}),
@@ -370,7 +370,7 @@ describe('MicrophoneManager', () => {
muted: true,
};
expect(manager.getState()).toEqual(expectedState);
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
expect(api.getConditionStateManager().setState).toHaveBeenLastCalledWith(
expect.objectContaining({
microphone: expectedState,
}),
@@ -50,7 +50,7 @@ const createTriggerAPI = (options?: {
},
}),
);
vi.mocked(api.getConditionsManager().getState).mockReturnValue({});
vi.mocked(api.getConditionStateManager().getState).mockReturnValue({});
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
@@ -319,10 +319,10 @@ describe('TriggersManager', () => {
manager.handleCameraEvent({ cameraID: 'camera_1', type: 'new' });
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith({
expect(api.getConditionStateManager().setState).toHaveBeenLastCalledWith({
triggered: new Set(['camera_1']),
});
vi.mocked(api.getConditionsManager().getState).mockReturnValue({
vi.mocked(api.getConditionStateManager().getState).mockReturnValue({
triggered: new Set(['camera_1']),
});
@@ -331,7 +331,7 @@ describe('TriggersManager', () => {
vi.setSystemTime(add(start, { seconds: 10 }));
vi.runOnlyPendingTimers();
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith({
expect(api.getConditionStateManager().setState).toHaveBeenLastCalledWith({
triggered: undefined,
});
});
@@ -45,7 +45,7 @@ describe('should act correctly when view is set', () => {
expect(api.getCardElementManager().scrollReset).toBeCalled();
expect(api.getMessageManager().reset).toBeCalled();
expect(api.getStyleManager().setExpandedMode).toBeCalled();
expect(api.getConditionsManager()?.setState).toBeCalledWith({
expect(api.getConditionStateManager()?.setState).toBeCalledWith({
view: 'live',
camera: 'camera',
displayMode: 'grid',