feat: Add support for config and initialized conditions (#1894)
These conditions are probably not hugely useful for most card users, but allows the inbuilt automation systems to be used for card internal operations. [skip ci]
This commit is contained in:
@@ -30,6 +30,21 @@ conditions:
|
|||||||
| `condition` | Must be `camera`. |
|
| `condition` | Must be `camera`. |
|
||||||
| `cameras` | An optional list of camera IDs in which this condition is satisfied. If not specified, any camera change will satisy the condition. See the camera [id](cameras/README.md) parameter. |
|
| `cameras` | An optional list of camera IDs in which this condition is satisfied. If not specified, any camera change will satisy the condition. See the camera [id](cameras/README.md) parameter. |
|
||||||
|
|
||||||
|
## `config`
|
||||||
|
|
||||||
|
Matches when card configuration changes (e.g. on startup, or when [Configuration Overrides](./overrides.md) are applied).
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
conditions:
|
||||||
|
- condition: config
|
||||||
|
# [...]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `condition` | Must be `config`. |
|
||||||
|
| `paths` | An optional array of configuration paths (e.g. `menu.style`). If provided condition matches if _ANY_ of the provided configuration paths has changed. |
|
||||||
|
|
||||||
## `expand`
|
## `expand`
|
||||||
|
|
||||||
Matches based on whether the card is in "expanded" mode.
|
Matches based on whether the card is in "expanded" mode.
|
||||||
@@ -60,6 +75,21 @@ conditions:
|
|||||||
| `condition` | Must be `fullscreen`. |
|
| `condition` | Must be `fullscreen`. |
|
||||||
| `fullscreen` | If `true` the condition is satisfied if the card is in fullscreen mode. If `false` the condition is satisfied if the card is **NOT** in fullscreen mode. |
|
| `fullscreen` | If `true` the condition is satisfied if the card is in fullscreen mode. If `false` the condition is satisfied if the card is **NOT** in fullscreen mode. |
|
||||||
|
|
||||||
|
## `initialized`
|
||||||
|
|
||||||
|
Matches when the card is first initialized.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
conditions:
|
||||||
|
- condition: initialized
|
||||||
|
```
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
| ----------- | ---------------------- |
|
||||||
|
| `condition` | Must be `initialized`. |
|
||||||
|
|
||||||
|
?> This is exclusively useful for running [automations](./automations.md) on card start.
|
||||||
|
|
||||||
## `interaction`
|
## `interaction`
|
||||||
|
|
||||||
Matches based on whether the card has been interacted with.
|
Matches based on whether the card has been interacted with.
|
||||||
@@ -258,10 +288,14 @@ conditions:
|
|||||||
- condition: camera
|
- condition: camera
|
||||||
cameras:
|
cameras:
|
||||||
- camera.office
|
- camera.office
|
||||||
|
- condition: config
|
||||||
|
paths:
|
||||||
|
- "menu.style"
|
||||||
- condition: expand
|
- condition: expand
|
||||||
expand: true
|
expand: true
|
||||||
- condition: fullscreen
|
- condition: fullscreen
|
||||||
fullscreen: true
|
fullscreen: true
|
||||||
|
- condition: initialized
|
||||||
- condition: interaction
|
- condition: interaction
|
||||||
interaction: true
|
interaction: true
|
||||||
- condition: key
|
- condition: key
|
||||||
|
|||||||
@@ -184,6 +184,18 @@ export class ConfigManager {
|
|||||||
await this._api.getDefaultManager().initializeIfNecessary(previousConfig);
|
await this._api.getDefaultManager().initializeIfNecessary(previousConfig);
|
||||||
await this._api.getMediaPlayerManager().initializeIfNecessary(previousConfig);
|
await this._api.getMediaPlayerManager().initializeIfNecessary(previousConfig);
|
||||||
|
|
||||||
|
// The config is only set in the state if the card is already fully
|
||||||
|
// initialized. If not, the config will be set post initialization in the
|
||||||
|
// InitializationManager.
|
||||||
|
if (
|
||||||
|
this._overriddenConfig &&
|
||||||
|
this._api.getInitializationManager().isInitializedMandatory()
|
||||||
|
) {
|
||||||
|
this._api.getConditionStateManager().setState({
|
||||||
|
config: this._overriddenConfig,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this._api.getCardElementManager().update();
|
this._api.getCardElementManager().update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,6 +128,18 @@ export class InitializationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._everInitialized = true;
|
this._everInitialized = true;
|
||||||
|
|
||||||
|
// When the card is initialized, both the initialization state (will never
|
||||||
|
// change again), and the config are set in the condition state. The
|
||||||
|
// config is set here, rather than in the ConfigManager, in order to
|
||||||
|
// ensure actions (that trigger on config change) are not run before hass
|
||||||
|
// is available and the card is initialzied (the first config is set in
|
||||||
|
// the card *before* hass is set in the card).
|
||||||
|
this._api.getConditionStateManager().setState({
|
||||||
|
config: config,
|
||||||
|
initialized: this._everInitialized,
|
||||||
|
});
|
||||||
|
|
||||||
this._api.getCardElementManager().update();
|
this._api.getCardElementManager().update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ export interface CardInitializerAPI {
|
|||||||
getMicrophoneManager(): MicrophoneManager;
|
getMicrophoneManager(): MicrophoneManager;
|
||||||
|
|
||||||
getCardElementManager(): CardElementManager;
|
getCardElementManager(): CardElementManager;
|
||||||
|
getConditionStateManager(): ConditionStateManager;
|
||||||
getConfigManager(): ConfigManager;
|
getConfigManager(): ConfigManager;
|
||||||
getDefaultManager(): DefaultManager;
|
getDefaultManager(): DefaultManager;
|
||||||
getEntityRegistryManager(): EntityRegistryManager;
|
getEntityRegistryManager(): EntityRegistryManager;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
|
import { getConfigValue } from '../config/management';
|
||||||
import { AdvancedCameraCardCondition } from '../config/types';
|
import { AdvancedCameraCardCondition } from '../config/types';
|
||||||
import { isCompanionApp } from '../utils/companion';
|
import { isCompanionApp } from '../utils/companion';
|
||||||
import {
|
import {
|
||||||
@@ -146,8 +147,12 @@ export class ConditionsManager implements ConditionsManagerReadonlyInterface {
|
|||||||
return {
|
return {
|
||||||
result:
|
result:
|
||||||
!!newState?.state &&
|
!!newState?.state &&
|
||||||
((!condition.state && !condition.state_not) ||
|
((!condition.state &&
|
||||||
(condition.entity in newState.state &&
|
!condition.state_not &&
|
||||||
|
newState.state[condition.entity]?.state !==
|
||||||
|
oldState?.state?.[condition.entity]?.state) ||
|
||||||
|
((!!condition.state || !!condition.state_not) &&
|
||||||
|
condition.entity in newState.state &&
|
||||||
(!condition.state ||
|
(!condition.state ||
|
||||||
(Array.isArray(condition.state)
|
(Array.isArray(condition.state)
|
||||||
? condition.state.includes(newState.state[condition.entity].state)
|
? condition.state.includes(newState.state[condition.entity].state)
|
||||||
@@ -281,6 +286,29 @@ export class ConditionsManager implements ConditionsManagerReadonlyInterface {
|
|||||||
(condition.user_agent_re === undefined ||
|
(condition.user_agent_re === undefined ||
|
||||||
new RegExp(condition.user_agent_re).test(newState.userAgent)),
|
new RegExp(condition.user_agent_re).test(newState.userAgent)),
|
||||||
};
|
};
|
||||||
|
case 'config': {
|
||||||
|
return {
|
||||||
|
result:
|
||||||
|
!!newState?.config &&
|
||||||
|
newState.config !== oldState?.config &&
|
||||||
|
(!condition.paths?.length ||
|
||||||
|
condition.paths.some(
|
||||||
|
(key) =>
|
||||||
|
getConfigValue(newState.config!, key) !==
|
||||||
|
(oldState?.config ? getConfigValue(oldState?.config, key) : undefined),
|
||||||
|
)),
|
||||||
|
data: {
|
||||||
|
config: {
|
||||||
|
...((oldState?.config || newState?.config) && {
|
||||||
|
...(oldState?.config && { from: oldState?.config }),
|
||||||
|
...(newState?.config && { to: newState?.config }),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'initialized':
|
||||||
|
return { result: !!newState?.initialized };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { CurrentUser } from '@dermotduffy/custom-card-helpers';
|
import { CurrentUser } from '@dermotduffy/custom-card-helpers';
|
||||||
import { HassEntities } from 'home-assistant-js-websocket';
|
import { HassEntities } from 'home-assistant-js-websocket';
|
||||||
import { ViewDisplayMode } from '../config/types';
|
import { AdvancedCameraCardConfig, ViewDisplayMode } from '../config/types';
|
||||||
import { MediaLoadedInfo } from '../types';
|
import { MediaLoadedInfo } from '../types';
|
||||||
import { KeysState, MicrophoneState } from '../card-controller/types';
|
import { KeysState, MicrophoneState } from '../card-controller/types';
|
||||||
|
|
||||||
export interface ConditionState {
|
export interface ConditionState {
|
||||||
camera?: string;
|
camera?: string;
|
||||||
|
config?: AdvancedCameraCardConfig;
|
||||||
displayMode?: ViewDisplayMode;
|
displayMode?: ViewDisplayMode;
|
||||||
expand?: boolean;
|
expand?: boolean;
|
||||||
fullscreen?: boolean;
|
fullscreen?: boolean;
|
||||||
|
initialized?: boolean;
|
||||||
interaction?: boolean;
|
interaction?: boolean;
|
||||||
keys?: KeysState;
|
keys?: KeysState;
|
||||||
mediaLoadedInfo?: MediaLoadedInfo | null;
|
mediaLoadedInfo?: MediaLoadedInfo | null;
|
||||||
@@ -43,10 +45,16 @@ interface ConditionsEvaluationDataState extends ConditionsEvaluationDataFromTo {
|
|||||||
entity: string;
|
entity: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ConditionsEvaluationDataConfig {
|
||||||
|
from?: AdvancedCameraCardConfig;
|
||||||
|
to?: AdvancedCameraCardConfig;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ConditionsEvaluationData {
|
export interface ConditionsEvaluationData {
|
||||||
camera?: ConditionsEvaluationDataFromTo;
|
camera?: ConditionsEvaluationDataFromTo;
|
||||||
view?: ConditionsEvaluationDataFromTo;
|
view?: ConditionsEvaluationDataFromTo;
|
||||||
state?: ConditionsEvaluationDataState;
|
state?: ConditionsEvaluationDataState;
|
||||||
|
config?: ConditionsEvaluationDataConfig;
|
||||||
}
|
}
|
||||||
export interface ConditionsEvaluationResult {
|
export interface ConditionsEvaluationResult {
|
||||||
result: boolean;
|
result: boolean;
|
||||||
|
|||||||
+39
-30
@@ -683,43 +683,33 @@ export type StatusBarItem = z.infer<typeof statusBarItemSchema>;
|
|||||||
// Custom Element Configuration: Conditions
|
// Custom Element Configuration: Conditions
|
||||||
// *************************************************************************
|
// *************************************************************************
|
||||||
|
|
||||||
const viewConditionSchema = z.object({
|
|
||||||
condition: z.literal('view'),
|
|
||||||
views: z.string().array().optional(),
|
|
||||||
});
|
|
||||||
const fullscreenConditionSchema = z.object({
|
|
||||||
condition: z.literal('fullscreen'),
|
|
||||||
fullscreen: z.boolean(),
|
|
||||||
});
|
|
||||||
const expandConditionSchema = z.object({
|
|
||||||
condition: z.literal('expand'),
|
|
||||||
expand: z.boolean(),
|
|
||||||
});
|
|
||||||
const cameraConditionSchema = z.object({
|
const cameraConditionSchema = z.object({
|
||||||
condition: z.literal('camera'),
|
condition: z.literal('camera'),
|
||||||
cameras: z.string().array().optional(),
|
cameras: z.string().array().optional(),
|
||||||
});
|
});
|
||||||
const mediaLoadedConditionSchema = z.object({
|
const configConditionSchema = z.object({
|
||||||
condition: z.literal('media_loaded'),
|
condition: z.literal('config'),
|
||||||
media_loaded: z.boolean(),
|
paths: z.string().array().optional(),
|
||||||
});
|
});
|
||||||
const displayModeConditionSchema = z.object({
|
const displayModeConditionSchema = z.object({
|
||||||
condition: z.literal('display_mode'),
|
condition: z.literal('display_mode'),
|
||||||
display_mode: viewDisplayModeSchema,
|
display_mode: viewDisplayModeSchema,
|
||||||
});
|
});
|
||||||
const triggeredConditionSchema = z.object({
|
const expandConditionSchema = z.object({
|
||||||
condition: z.literal('triggered'),
|
condition: z.literal('expand'),
|
||||||
triggered: z.string().array(),
|
expand: z.boolean(),
|
||||||
|
});
|
||||||
|
const fullscreenConditionSchema = z.object({
|
||||||
|
condition: z.literal('fullscreen'),
|
||||||
|
fullscreen: z.boolean(),
|
||||||
|
});
|
||||||
|
const initializedConditionSchema = z.object({
|
||||||
|
condition: z.literal('initialized'),
|
||||||
});
|
});
|
||||||
const interactionConditionSchema = z.object({
|
const interactionConditionSchema = z.object({
|
||||||
condition: z.literal('interaction'),
|
condition: z.literal('interaction'),
|
||||||
interaction: z.boolean(),
|
interaction: z.boolean(),
|
||||||
});
|
});
|
||||||
const microphoneConditionSchema = z.object({
|
|
||||||
condition: z.literal('microphone'),
|
|
||||||
connected: z.boolean().optional(),
|
|
||||||
muted: z.boolean().optional(),
|
|
||||||
});
|
|
||||||
const keyConditionSchema = z.object({
|
const keyConditionSchema = z.object({
|
||||||
condition: z.literal('key'),
|
condition: z.literal('key'),
|
||||||
key: z.string(),
|
key: z.string(),
|
||||||
@@ -729,6 +719,19 @@ const keyConditionSchema = z.object({
|
|||||||
alt: z.boolean().optional(),
|
alt: z.boolean().optional(),
|
||||||
meta: z.boolean().optional(),
|
meta: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
const mediaLoadedConditionSchema = z.object({
|
||||||
|
condition: z.literal('media_loaded'),
|
||||||
|
media_loaded: z.boolean(),
|
||||||
|
});
|
||||||
|
const microphoneConditionSchema = z.object({
|
||||||
|
condition: z.literal('microphone'),
|
||||||
|
connected: z.boolean().optional(),
|
||||||
|
muted: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
const triggeredConditionSchema = z.object({
|
||||||
|
condition: z.literal('triggered'),
|
||||||
|
triggered: z.string().array(),
|
||||||
|
});
|
||||||
const userAgentConditionSchema = z.object({
|
const userAgentConditionSchema = z.object({
|
||||||
condition: z.literal('user_agent'),
|
condition: z.literal('user_agent'),
|
||||||
user_agent: z.string().optional(),
|
user_agent: z.string().optional(),
|
||||||
@@ -748,26 +751,32 @@ const userAgentConditionSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
companion: z.boolean().optional(),
|
companion: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
const viewConditionSchema = z.object({
|
||||||
|
condition: z.literal('view'),
|
||||||
|
views: z.string().array().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const advancedCameraCardConditionSchema = z.discriminatedUnion('condition', [
|
export const advancedCameraCardConditionSchema = z.discriminatedUnion('condition', [
|
||||||
// Stock conditions:
|
// Stock conditions:
|
||||||
stateConditionSchema,
|
|
||||||
numericStateConditionSchema,
|
numericStateConditionSchema,
|
||||||
screenConditionSchema,
|
screenConditionSchema,
|
||||||
|
stateConditionSchema,
|
||||||
usersConditionSchema,
|
usersConditionSchema,
|
||||||
|
|
||||||
// Custom conditions:
|
// Custom conditions:
|
||||||
viewConditionSchema,
|
|
||||||
fullscreenConditionSchema,
|
|
||||||
expandConditionSchema,
|
|
||||||
cameraConditionSchema,
|
cameraConditionSchema,
|
||||||
mediaLoadedConditionSchema,
|
configConditionSchema,
|
||||||
displayModeConditionSchema,
|
displayModeConditionSchema,
|
||||||
triggeredConditionSchema,
|
expandConditionSchema,
|
||||||
|
fullscreenConditionSchema,
|
||||||
|
initializedConditionSchema,
|
||||||
interactionConditionSchema,
|
interactionConditionSchema,
|
||||||
microphoneConditionSchema,
|
|
||||||
keyConditionSchema,
|
keyConditionSchema,
|
||||||
|
mediaLoadedConditionSchema,
|
||||||
|
microphoneConditionSchema,
|
||||||
|
triggeredConditionSchema,
|
||||||
userAgentConditionSchema,
|
userAgentConditionSchema,
|
||||||
|
viewConditionSchema,
|
||||||
]);
|
]);
|
||||||
export type AdvancedCameraCardCondition = z.infer<
|
export type AdvancedCameraCardCondition = z.infer<
|
||||||
typeof advancedCameraCardConditionSchema
|
typeof advancedCameraCardConditionSchema
|
||||||
|
|||||||
@@ -349,6 +349,8 @@ describe('ConfigManager', () => {
|
|||||||
it('should initialize background items', async () => {
|
it('should initialize background items', async () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const stateManager = new ConditionStateManager();
|
const stateManager = new ConditionStateManager();
|
||||||
|
const listener = vi.fn();
|
||||||
|
stateManager.addListener(listener);
|
||||||
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
|
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
|
||||||
|
|
||||||
const manager = new ConfigManager(api);
|
const manager = new ConfigManager(api);
|
||||||
@@ -371,13 +373,24 @@ describe('ConfigManager', () => {
|
|||||||
|
|
||||||
expect(api.getDefaultManager().initializeIfNecessary).toBeCalledTimes(1);
|
expect(api.getDefaultManager().initializeIfNecessary).toBeCalledTimes(1);
|
||||||
expect(api.getMediaPlayerManager().initializeIfNecessary).toBeCalledTimes(1);
|
expect(api.getMediaPlayerManager().initializeIfNecessary).toBeCalledTimes(1);
|
||||||
|
expect(listener).not.toBeCalledWith(
|
||||||
|
expect.objectContaining({ change: { config: expect.anything() } }),
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.mocked(api.getInitializationManager().isInitializedMandatory).mockReturnValue(
|
||||||
|
true,
|
||||||
|
);
|
||||||
stateManager.setState({ fullscreen: true });
|
stateManager.setState({ fullscreen: true });
|
||||||
|
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
expect(api.getDefaultManager().initializeIfNecessary).toBeCalledTimes(2);
|
expect(api.getDefaultManager().initializeIfNecessary).toBeCalledTimes(2);
|
||||||
expect(api.getMediaPlayerManager().initializeIfNecessary).toBeCalledTimes(2);
|
expect(api.getMediaPlayerManager().initializeIfNecessary).toBeCalledTimes(2);
|
||||||
|
|
||||||
|
// Should set the config condition state.
|
||||||
|
expect(listener).toBeCalledWith(
|
||||||
|
expect.objectContaining({ change: { config: expect.anything() } }),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
InitializationAspect,
|
InitializationAspect,
|
||||||
InitializationManager,
|
InitializationManager,
|
||||||
} from '../../src/card-controller/initialization-manager';
|
} from '../../src/card-controller/initialization-manager';
|
||||||
|
import { ConditionStateManager } from '../../src/conditions/state-manager';
|
||||||
import { loadLanguages } from '../../src/localize/localize';
|
import { loadLanguages } from '../../src/localize/localize';
|
||||||
import { sideLoadHomeAssistantElements } from '../../src/utils/ha';
|
import { sideLoadHomeAssistantElements } from '../../src/utils/ha';
|
||||||
import { Initializer } from '../../src/utils/initializer/initializer';
|
import { Initializer } from '../../src/utils/initializer/initializer';
|
||||||
@@ -67,9 +68,15 @@ describe('InitializationManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('successfully', async () => {
|
it('successfully', async () => {
|
||||||
|
const stateListener = vi.fn();
|
||||||
|
const stateMananger = new ConditionStateManager();
|
||||||
|
stateMananger.addListener(stateListener);
|
||||||
|
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getConditionStateManager).mockReturnValue(stateMananger);
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
const config = createConfig();
|
||||||
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(config);
|
||||||
vi.mocked(api.getMessageManager().hasMessage).mockReturnValue(false);
|
vi.mocked(api.getMessageManager().hasMessage).mockReturnValue(false);
|
||||||
vi.mocked(api.getQueryStringManager().hasViewRelatedActionsToRun).mockReturnValue(
|
vi.mocked(api.getQueryStringManager().hasViewRelatedActionsToRun).mockReturnValue(
|
||||||
false,
|
false,
|
||||||
@@ -93,6 +100,15 @@ describe('InitializationManager', () => {
|
|||||||
expect(api.getCardElementManager().update).toBeCalled();
|
expect(api.getCardElementManager().update).toBeCalled();
|
||||||
|
|
||||||
expect(manager.wasEverInitialized()).toBeTruthy();
|
expect(manager.wasEverInitialized()).toBeTruthy();
|
||||||
|
|
||||||
|
expect(stateListener).toBeCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
change: {
|
||||||
|
initialized: true,
|
||||||
|
config,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('successfully with microphone if configured', async () => {
|
it('successfully with microphone if configured', async () => {
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
|
|||||||
import { MicrophoneState } from '../../src/card-controller/types';
|
import { MicrophoneState } from '../../src/card-controller/types';
|
||||||
import { ConditionsManager } from '../../src/conditions/conditions-manager';
|
import { ConditionsManager } from '../../src/conditions/conditions-manager';
|
||||||
import { ConditionStateManager } from '../../src/conditions/state-manager';
|
import { ConditionStateManager } from '../../src/conditions/state-manager';
|
||||||
import { createMediaLoadedInfo, createStateEntity, createUser } from '../test-utils';
|
import {
|
||||||
|
createConfig,
|
||||||
|
createMediaLoadedInfo,
|
||||||
|
createStateEntity,
|
||||||
|
createUser,
|
||||||
|
} from '../test-utils';
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
describe('ConditionsManager', () => {
|
describe('ConditionsManager', () => {
|
||||||
@@ -841,6 +846,146 @@ describe('ConditionsManager', () => {
|
|||||||
expect(manager.getEvaluation().result).toBeFalsy();
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('with config condition', () => {
|
||||||
|
const config_1 = createConfig({
|
||||||
|
// Default is:
|
||||||
|
//
|
||||||
|
// view: {
|
||||||
|
// default: live,
|
||||||
|
// },
|
||||||
|
});
|
||||||
|
const config_2 = createConfig({
|
||||||
|
view: {
|
||||||
|
default: 'clips',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const config_3 = createConfig({
|
||||||
|
view: {
|
||||||
|
default: 'clips',
|
||||||
|
default_cycle_camera: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const config_4 = createConfig({
|
||||||
|
view: {
|
||||||
|
default: 'clips',
|
||||||
|
default_cycle_camera: true,
|
||||||
|
dim: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match any config change', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'config' as const }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
const listener = vi.fn();
|
||||||
|
manager.addListener(listener);
|
||||||
|
|
||||||
|
stateManager.setState({ config: config_1 });
|
||||||
|
expect(listener).toHaveBeenLastCalledWith({
|
||||||
|
result: true,
|
||||||
|
data: {
|
||||||
|
config: {
|
||||||
|
to: config_1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
stateManager.setState({ config: config_2 });
|
||||||
|
expect(listener).toHaveBeenLastCalledWith({
|
||||||
|
result: true,
|
||||||
|
data: {
|
||||||
|
config: {
|
||||||
|
from: config_1,
|
||||||
|
to: config_2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(listener).toBeCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match specific config change', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'config' as const, paths: ['view.default'] }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({ config: config_1 });
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({ config: config_2 });
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({ config: config_3 });
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match multiple config change', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
condition: 'config' as const,
|
||||||
|
paths: ['view.default', 'view.default_cycle_camera'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({ config: config_1 });
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({ config: config_2 });
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({ config: config_3 });
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({ config: config_4 });
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not match unrelated changes', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'config' as const }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
const listener = vi.fn();
|
||||||
|
manager.addListener(listener);
|
||||||
|
|
||||||
|
stateManager.setState({ config: config_1 });
|
||||||
|
expect(listener).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
// On the next state set, the condition won't match anymore (as the
|
||||||
|
// config is the same), the listener will still be called to indicate
|
||||||
|
// the condition evaluation has changed.
|
||||||
|
stateManager.setState({ expand: true });
|
||||||
|
expect(listener).toBeCalledTimes(2);
|
||||||
|
expect(listener).toHaveBeenLastCalledWith({ result: false });
|
||||||
|
|
||||||
|
// Future unrelated state changes won't call the listener.
|
||||||
|
stateManager.setState({ fullscreen: true });
|
||||||
|
expect(listener).toBeCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with initialized condition', () => {
|
||||||
|
const stateManager = new ConditionStateManager();
|
||||||
|
const manager = new ConditionsManager(
|
||||||
|
[{ condition: 'initialized' as const }],
|
||||||
|
stateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
stateManager.setState({ initialized: true });
|
||||||
|
expect(manager.getEvaluation().result).toBeTruthy();
|
||||||
|
stateManager.setState({ initialized: false });
|
||||||
|
expect(manager.getEvaluation().result).toBeFalsy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should handle listeners correctly', () => {
|
describe('should handle listeners correctly', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user