Add support for more advanced forms of overriding

This commit is contained in:
Dermot Duffy
2024-04-13 18:23:11 -07:00
parent 0eca5d780c
commit 323925101d
21 changed files with 855 additions and 285 deletions
+2 -2
View File
@@ -457,11 +457,11 @@ describe('CameraManager', async () => {
},
);
it('without cameras', async () => {
it('without camera', async () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = createCameraManager(api, mock<CameraManagerEngine>(), []);
const manager = createCameraManager(api, mock<CameraManagerEngine>());
expect(manager.generateDefaultEventQueries('not_a_camera')).toBeNull();
});
+336 -63
View File
@@ -1,10 +1,10 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { z } from 'zod';
import {
ConditionsEvaluateRequestEvent,
ConditionsManager,
evaluateConditionViaEvent,
getOverriddenConfig,
getOverridesByKey,
} from '../../src/card-controller/conditions-manager';
import { FrigateCardCondition } from '../../src/config/types';
import {
@@ -77,77 +77,350 @@ describe('getOverriddenConfig', () => {
style: 'none',
},
};
const overrides = [
{
overrides: {
menu: {
style: 'above',
},
},
conditions: [
{
condition: 'fullscreen' as const,
fullscreen: true,
},
],
},
];
it('should not override config', () => {
const manager = new ConditionsManager(createCardAPI());
expect(getOverriddenConfig(manager, config, overrides)).toBe(config);
});
it('should override config', () => {
const manager = new ConditionsManager(createCardAPI());
manager.setState({ fullscreen: true });
expect(getOverriddenConfig(manager, config, overrides)).toEqual({
menu: {
style: 'above',
},
});
});
it('should do nothing without overrides', () => {
it('should not override without overrides', () => {
const manager = new ConditionsManager(createCardAPI());
manager.setState({ fullscreen: true });
expect(getOverriddenConfig(manager, config)).toBe(config);
});
});
describe('getOverridesByKey', () => {
const conditions = [
{
condition: 'fullscreen' as const,
fullscreen: true,
},
];
const override = {
menu: {
style: 'above',
},
};
const overrides = [
{
overrides: override,
conditions: conditions,
},
];
it('should get overrides', () => {
expect(getOverridesByKey('menu', overrides)).toEqual([
{ conditions: conditions, overrides: { style: 'above' } },
]);
it('should not override when condition does not match', () => {
const manager = new ConditionsManager(createCardAPI());
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);
});
it('should get no overrides', () => {
expect(getOverridesByKey('live', overrides)).toEqual([]);
describe('should merge', () => {
it('with path', () => {
const manager = new ConditionsManager(createCardAPI());
manager.setState({ fullscreen: 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 = new ConditionsManager(createCardAPI());
manager.setState({ fullscreen: 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 = new ConditionsManager(createCardAPI());
manager.setState({ fullscreen: 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',
},
});
});
});
it('should get no overrides when undefined', () => {
expect(getOverridesByKey('live')).toEqual([]);
describe('should set', () => {
it('leaf node', () => {
const manager = new ConditionsManager(createCardAPI());
manager.setState({ fullscreen: 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 = new ConditionsManager(createCardAPI());
manager.setState({ fullscreen: 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 = new ConditionsManager(createCardAPI());
manager.setState({ fullscreen: 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 = new ConditionsManager(createCardAPI());
manager.setState({ fullscreen: true });
expect(
getOverriddenConfig(manager, config, {
configOverrides: [
{
delete: ['menu' as const],
conditions: [
{
condition: 'fullscreen' as const,
fullscreen: true,
},
],
},
],
}),
).toEqual({});
});
// it('with empty value and object', () => {
// const manager = new ConditionsManager(createCardAPI());
// manager.setState({ fullscreen: true });
// expect(
// getOverriddenConfig(manager, config, {
// configOverrides: [
// {
// delete: [''],
// conditions: [
// {
// condition: 'fullscreen' as const,
// fullscreen: true,
// },
// ],
// },
// ],
// emptyKeyReplaces: true,
// }),
// ).toEqual({});
// });
});
describe('should validate schema', () => {
const testSchema = z.object({
menu: z.object({
style: z.enum(['none', 'hidden']),
}),
});
it('passing', () => {
const manager = new ConditionsManager(createCardAPI());
manager.setState({ fullscreen: 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 = new ConditionsManager(createCardAPI());
manager.setState({ fullscreen: true });
expect(
getOverriddenConfig(manager, config, {
configOverrides: [
{
conditions: [
{
condition: 'fullscreen' as const,
fullscreen: true,
},
],
set: {
'menu.style': 'NOT_A_STYLE',
},
},
],
schema: testSchema,
}),
).toEqual(config);
});
it('failing and logging', () => {
const consoleSpy = vi.spyOn(global.console, 'warn').mockReturnValue(undefined);
const manager = new ConditionsManager(createCardAPI());
manager.setState({ fullscreen: true });
expect(
getOverriddenConfig(manager, config, {
configOverrides: [
{
conditions: [
{
condition: 'fullscreen' as const,
fullscreen: true,
},
],
set: {
'menu.style': 'NOT_A_STYLE',
},
},
],
schema: testSchema,
logOnParseError: true,
}),
).toEqual(config);
expect(consoleSpy).toBeCalledWith(
'Cannot parse overridden configuration',
expect.anything(),
expect.anything(),
);
});
});
});
@@ -194,7 +467,7 @@ describe('ConditionsManager', () => {
return createConfig({
overrides: [
{
overrides: {},
merge: {},
conditions: conditions,
},
],
@@ -455,7 +728,7 @@ describe('ConditionsManager', () => {
describe('with screen condition', () => {
const mediaQueryConfig = {
type: 'custom:frigate-card',
cameras: [],
cameras: [{}],
elements: [
{
type: 'custom:frigate-card-conditional',
+50 -7
View File
@@ -420,7 +420,7 @@ describe('should handle version specific upgrades', () => {
media_loaded: true,
},
],
overrides: {
merge: {
view: {
default: 'clips',
},
@@ -489,7 +489,7 @@ describe('should handle version specific upgrades', () => {
overrides: [
{
conditions: {},
overrides: {
merge: {
menu: {
buttons: {
camera_ui: {
@@ -1872,7 +1872,7 @@ describe('should handle version specific upgrades', () => {
views: ['clips', 'snapshots'],
},
],
overrides: {
merge: {
view: {
default: 'clips',
},
@@ -1987,7 +1987,7 @@ describe('should handle version specific upgrades', () => {
cameras: ['camera_1', 'camera_2'],
},
],
overrides: {
merge: {
view: {
default: 'clips',
},
@@ -2107,7 +2107,7 @@ describe('should handle version specific upgrades', () => {
[condition]: true,
},
],
overrides: {
merge: {
view: {
default: 'clips',
},
@@ -2255,7 +2255,7 @@ describe('should handle version specific upgrades', () => {
state_not: 'off',
},
],
overrides: {
merge: {
view: {
default: 'clips',
},
@@ -2387,7 +2387,7 @@ describe('should handle version specific upgrades', () => {
media_query: 'query',
},
],
overrides: {
merge: {
view: {
default: 'clips',
},
@@ -2497,5 +2497,48 @@ describe('should handle version specific upgrades', () => {
});
});
});
it('from overrides to merge', () => {
const config = {
type: 'custom:frigate-card',
cameras: [],
overrides: [
{
conditions: [
{
condition: 'view',
view: ['clips'],
},
],
overrides: {
menu: {
style: 'hidden',
},
},
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:frigate-card',
cameras: [],
overrides: [
{
conditions: [
{
condition: 'view',
view: ['clips'],
},
],
merge: {
menu: {
style: 'hidden',
},
},
},
],
});
});
});
});
+1 -1
View File
@@ -25,7 +25,7 @@ describe('setProfiles', () => {
it('should handle profiles', () => {
const input = {
type: 'frigate-hass-card',
cameras: [],
cameras: [{}],
live: {
controls: {
timeline: {
+2 -2
View File
@@ -12,7 +12,7 @@ import { createConfig } from '../test-utils';
describe('config defaults', () => {
it('should be as expected', () => {
expect(createConfig()).toEqual({
cameras: [],
cameras: [{}],
cameras_global: {
dependencies: {
all_cameras: false,
@@ -453,7 +453,7 @@ describe('should handle custom frigate elements', () => {
it('should not require title controls to specify all options', () => {
expect(
createConfig({
cameras: [],
cameras: [{}],
live: {
controls: {
title: {
+1 -1
View File
@@ -67,7 +67,7 @@ export const createCondition = (
export const createConfig = (config?: RawFrigateCardConfig): FrigateCardConfig => {
return frigateCardConfigSchema.parse({
type: 'frigate-hass-card',
cameras: [],
cameras: [{}],
...config,
});
};
+52 -2
View File
@@ -7,6 +7,7 @@ import {
aspectRatioToStyle,
contentsChanged,
dayToDate,
desparsifyArrays,
dispatchFrigateCardEvent,
errorToConsole,
formatDate,
@@ -158,6 +159,7 @@ describe('runWhenIdleIfSupported', () => {
});
it('should run directly when not supported', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).requestIdleCallback = undefined;
const func = vi.fn();
runWhenIdleIfSupported(func);
@@ -166,7 +168,7 @@ describe('runWhenIdleIfSupported', () => {
it('should run idle when supported', () => {
const requestIdle = vi.fn();
(window as any).requestIdleCallback = requestIdle;
window.requestIdleCallback = requestIdle;
const func = vi.fn();
runWhenIdleIfSupported(func);
expect(requestIdle).toBeCalledWith(func, {});
@@ -174,7 +176,7 @@ describe('runWhenIdleIfSupported', () => {
it('should run idle with timeout when supported', () => {
const requestIdle = vi.fn();
(window as any).requestIdleCallback = requestIdle;
window.requestIdleCallback = requestIdle;
const func = vi.fn();
runWhenIdleIfSupported(func, 10);
expect(requestIdle).toBeCalledWith(func, { timeout: 10 });
@@ -298,6 +300,7 @@ describe('recursivelyMergeObjectsNotArrays', () => {
it('should recursively merge objects but replace arrays', () => {
expect(
recursivelyMergeObjectsNotArrays(
{},
{
a: {
b: {
@@ -356,3 +359,50 @@ describe('aspectRatioToStyle', () => {
expect(aspectRatioToStyle({ ratio: [4] })).toEqual({ 'aspect-ratio': 'auto' });
});
});
describe('desparsifyArrays', () => {
it('number', () => {
expect(desparsifyArrays(1)).toBe(1);
});
it('string', () => {
expect(desparsifyArrays('foo')).toBe('foo');
});
describe('array', () => {
it('simple', () => {
expect(desparsifyArrays([1, 2, undefined, 3])).toEqual([1, 2, 3]);
});
it('nested', () => {
expect(
desparsifyArrays([
1,
2,
undefined,
{
subArray: [undefined, 3],
},
4,
]),
).toEqual([1, 2, { subArray: [3] }, 4]);
});
});
describe('object', () => {
it('simple', () => {
expect(
desparsifyArrays({ foo: [1, 2, undefined, 3], bar: [undefined, 4] }),
).toEqual({
foo: [1, 2, 3],
bar: [4],
});
});
it('nested', () => {
expect(
desparsifyArrays({ foo: { bar: [1, undefined, 2], empty: [undefined] } }),
).toEqual({
foo: {
bar: [1, 2],
empty: [],
},
});
});
});
});
+17
View File
@@ -49,6 +49,23 @@ describe('deepRemoveDefaults', () => {
const result = deepRemoveDefaults(schema).parse({ string: 'moo' });
expect(result.string).toBe('moo');
});
describe('should still enforce array length', () => {
it('min', () => {
const schema = z.number().array().min(1);
const result = deepRemoveDefaults(schema).safeParse([]);
expect(result.success).toBeFalsy();
});
it('max', () => {
const schema = z.number().array().max(1);
const result = deepRemoveDefaults(schema).safeParse([1, 2]);
expect(result.success).toBeFalsy();
});
it('exact', () => {
const schema = z.number().array().length(1);
const result = deepRemoveDefaults(schema).safeParse([]);
expect(result.success).toBeFalsy();
});
});
});
describe('getParseErrorKeys', () => {