diff --git a/docs/configuration/overrides.md b/docs/configuration/overrides.md index 59873789..d0c70375 100644 --- a/docs/configuration/overrides.md +++ b/docs/configuration/overrides.md @@ -1,39 +1,166 @@ # `overrides` -Various parts of card configuration may [conditionally](conditions.md) be -overridden (e.g. to hide the menu in fullscreen mode). +The card configuration may [conditionally](conditions.md) be overridden (e.g. to +hide the menu in fullscreen mode). ```yaml overrides: - conditions: [condition] - overrides: - [override] + [...] ``` -Not all configuration parameters are overriddable, some because it doesn't make -sense for that parameter to vary, and many because of the extra complexity of -supporting overriding given the lack of compelling usecases ([please request new -overridable parameters -here!](https://github.com/dermotduffy/frigate-hass-card/issues/new/choose)). +!> Whilst all configuration parameters are theoretically overridable, in some instances a configuration variable may only be consulted on startup or changing its value may negatively impact behavior -- override results may vary! -Each entry under the top-level `overrides` configuration block should be a list -item, that has both of the following parameters set: +The top-level `overrides` configuration block expects a list, with each list +item containing `conditions` and at least one of `merge`, `delete` or `set` specified. | Option | Default | Description | | - | - | - | | `conditions` | | A list of [conditions](conditions.md) that must evaluate to `true` in order for the overrides to be applied. | -| `overrides` | | Configuration overrides to be applied. Any configuration parameter matching [Overrideable parameters](overrides.md?id=overrideable-parameters) can be overridden. | +| `delete` | | An array of configuration paths to delete. See below. | +| `merge` | | A dictionary of configuration paths to merge. See below. | +| `set` | | A dictionary of configuration paths to set. See below. | -## Overrideable parameters +## Configuration Paths -| Configuration Key | Overrideable | -| - | - | -| [`cameras.*`](cameras/README.md) | :white_check_mark: | -| [`cameras_global.*`](cameras/README.md) | :white_check_mark: | -| [`dimensions.*`](dimensions.md) | :white_check_mark: | -| [`image.*`](image.md) | :white_check_mark: | -| [`live.controls.*`](live.md?id=controls), [`live.display.*`](live.md?id=display), [`live.microphone.*`](live.md?id=microphone), [`live.show_image_during_load`](live.md), [`live.zoomable`](live.md) | :white_check_mark: | -| [`menu.*`](menu.md) | :white_check_mark: | -| [`view.*`](view.md) | :white_check_mark: | -| *(Everything else)* | :heavy_multiplication_x: | +The `delete`, `merge` and `set` parameters take configuration paths. Paths are dot-separated references to particular configuration parameters. To refer to list elements use `[n]` notation. + +For example the path `cameras[1].dimensions.aspect_ratio` refers to the `aspect_ratio` parameter below: + +```yaml +cameras: + - camera_entity: camera.other + - camera_entity: camera.relevant + dimensions: + aspect_ratio: '16:9' +``` + +## `delete` + +An array of configuration paths to delete. + +```yaml +overrides: + - conditions: + [condition] + delete: + - [path_1] + - [path_2] +``` + +### Examples + +Delete the 2nd camera: + +```yaml +overrides: + - conditions: + [condition] + delete: + 'cameras[2]' +``` + +Delete the menu style parameter, thus falling back to the default: + +```yaml +overrides: + - conditions: + [condition] + delete: + 'menu.style' +``` + +## `merge` + +Specifies an object to recursively merge into existing configuration. + +| Option | Default | Description | +| - | - | - | +| [configuration path] | | Arbitrary configuration object to merge. Must be an object (i.e. not a literal value). | + +### Examples + +Hide the menu when a given condition is met: + +```yaml +overrides: + - conditions: + [condition] + merge: + menu: { + style: 'hidden' + } +``` + +Enable thumbnails below the `live` feed: + +```yaml +overrides: + - conditions: + [condition] + merge: + 'live.controls.thumbnails': { + mode: 'below' + } +``` + +Also enables thumbnails below the `live` feed, but without using the dot-separated notation: + +```yaml +overrides: + - conditions: + [condition] + merge: + live: { + controls: { + thumbnails: { + mode: 'below' + } + } + } +``` + +## `set` + +Specifies a value to set in the configuration. This differs from `merge` in that the existing value is entirely replaced. + +| Option | Default | Description | +| - | - | - | +| [configuration path] | | Arbitrary configuration value / object / list to set. | + +### Examples + +Set the entire menu configuration to defaults with the exception of the `style` which is set to `overlay`. + +```yaml +overrides: + - conditions: + [condition] + set: + menu: { + style: 'overlay' + } +``` + +Set the menu style but without touching the other `menu` parameters: + +```yaml +overrides: + - conditions: + [condition] + set: + 'menu.style': 'overlay' +``` + +That is equivalent to merging the following: + +```yaml +overrides: + - conditions: + [condition] + merge: + menu: { + style: 'overlay' + } +``` \ No newline at end of file diff --git a/docs/examples.md b/docs/examples.md index d2859a8d..c9a47395 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -424,7 +424,7 @@ overrides: - condition: state entity: light.office_main_lights state: 'on' - overrides: + merge: menu: position: bottom ``` @@ -451,7 +451,7 @@ overrides: - condition: state entity: binary_sensor.alarm_armed state: 'off' - overrides: + merge: view: default: image ``` @@ -471,7 +471,7 @@ overrides: fullscreen: true - condition: display_mode display_mode: grid - overrides: + merge: live: display: grid_columns: 5 @@ -497,7 +497,7 @@ overrides: - conditions: - condition: expand expand: true - overrides: + merge: menu: style: overlay ``` @@ -520,11 +520,29 @@ overrides: - conditions: - condition: fullscreen fullscreen: true - overrides: + merge: menu: style: none ``` +### Remove a camera when an entity state changes + +This example removes a camera from the card when an entity is disabled (e.g. a switch controlling power to the camera). + +```yaml +type: custom:frigate-card +cameras: + - camera_entity: camera.office + - camera_entity: camera.kitchen +overrides: + - conditions: + - condition: state + entity: switch.kitchen_camera_power + state: off + delete: + - 'cameras[1]' +``` + ## PTZ control The card supports using PTZ controls to conveniently control pan, tilt and zoom for cameras. This example shows the PTZ controls on the `live` view. Note that if your camera engine supports it (e.g. `frigate`) this will just work out of the box with no configuration at all. @@ -568,7 +586,7 @@ overrides: - conditions: - condition: screen media_query: '(orientation: landscape)' - overrides: + merge: menu: position: left ``` @@ -584,7 +602,7 @@ overrides: - conditions: - condition: screen media_query: '(max-width: 300px)' - overrides: + merge: menu: style: none live: diff --git a/package.json b/package.json index d91d94d5..f72013e0 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "@types/masonry-layout": "^4.2.5", "@typescript-eslint/eslint-plugin": "^5.36.2", "@typescript-eslint/parser": "^5.36.2", - "@vitest/coverage-istanbul": "^1.3.1", + "@vitest/coverage-istanbul": "^1.5.0", "docsify-cli": "^4.4.4", "eslint": "^8.23.0", "eslint-config-airbnb-base": "^15.0.0", @@ -81,7 +81,7 @@ "sass": "^1.54.9", "ts-prune": "^0.10.3", "typescript": "^4.9.5", - "vitest": "^1.3.1", + "vitest": "^1.5.0", "vitest-mock-extended": "^1.3.1" }, "scripts": { diff --git a/src/camera-manager/manager.ts b/src/camera-manager/manager.ts index 7f3968a6..df7775fa 100644 --- a/src/camera-manager/manager.ts +++ b/src/camera-manager/manager.ts @@ -145,7 +145,7 @@ export class CameraManager { // order, to ensure that the defaults in the cameras global config do not // override the values specified in the per-camera config. const cameras = config.cameras.map((camera) => - recursivelyMergeObjectsNotArrays(cloneDeep(config?.cameras_global), camera), + recursivelyMergeObjectsNotArrays({}, cloneDeep(config?.cameras_global), camera), ); try { diff --git a/src/card-controller/conditions-manager.ts b/src/card-controller/conditions-manager.ts index 122a44b7..7cc0d7ec 100644 --- a/src/card-controller/conditions-manager.ts +++ b/src/card-controller/conditions-manager.ts @@ -1,14 +1,21 @@ import { CurrentUser } from '@dermotduffy/custom-card-helpers'; import { HassEntities } from 'home-assistant-js-websocket'; import merge from 'lodash-es/merge'; -import { copyConfig } from '../config/management'; +import { ZodSchema } from 'zod'; +import { + copyConfig, + deleteConfigValue, + getConfigValue, + setConfigValue, +} from '../config/management'; import { FrigateCardCondition, - frigateConditionalSchema, - OverrideConfigurationKey, RawFrigateCardConfig, ViewDisplayMode, + frigateConditionalSchema, + Overrides, } from '../config/types'; +import { desparsifyArrays } from '../utils/basic'; import { CardConditionAPI } from './types'; interface MicrophoneConditionState { @@ -73,44 +80,66 @@ export function evaluateConditionViaEvent( return evaluateEvent.evaluation ?? false; } -type RawOverrides = { - conditions: FrigateCardCondition[]; - overrides: RawFrigateCardConfig; -}[]; - export function getOverriddenConfig( manager: Readonly, config: Readonly, - configOverrides?: Readonly, - stateOverrides?: Partial, + options?: { + configOverrides?: Readonly; + stateOverrides?: Partial; + schema?: ZodSchema; + logOnParseError?: boolean; + }, ): RawFrigateCardConfig { - const output = copyConfig(config); + let output = copyConfig(config); let overridden = false; - if (configOverrides) { - for (const override of configOverrides) { - if (manager.evaluateConditions(override.conditions, stateOverrides)) { - merge(output, override.overrides); + if (options?.configOverrides) { + for (const override of options.configOverrides) { + if (manager.evaluateConditions(override.conditions, options?.stateOverrides)) { + override.delete?.forEach((deletionKey) => { + deleteConfigValue(output, deletionKey); + }); + + Object.keys(override.set ?? {}).forEach((setKey) => { + setConfigValue(output, setKey, override.set?.[setKey]); + }); + + Object.keys(override.merge ?? {}).forEach((mergeKey) => { + setConfigValue( + output, + mergeKey, + merge({}, getConfigValue(output, mergeKey), override.merge?.[mergeKey]), + ); + }); + overridden = true; } } } - // Attempt to return the same configuration object if it has not been - // overridden (to reduce re-renders for a configuration that has not changed). - return overridden ? output : config; -} -export function getOverridesByKey( - key: OverrideConfigurationKey, - overrides?: Readonly, -): RawOverrides { - return ( - overrides - ?.filter((o) => key in o.overrides) - .map((o) => ({ - conditions: o.conditions, - overrides: o.overrides[key] as RawFrigateCardConfig, - })) ?? [] - ); + if (!overridden) { + // Attempt to return the same configuration object if it has not been + // overridden (to reduce re-renders for a configuration that has not changed). + return config; + } + + if (options?.configOverrides?.some((override) => override.delete?.length)) { + // If anything was deleted during this override, empty undefined slots may + // be left in arrays where values were unset. Desparsify them. + output = desparsifyArrays(output); + } + + if (options?.schema) { + const parseResult = options.schema.safeParse(output); + if (options.logOnParseError && !parseResult.success) { + console.warn( + `Cannot parse overridden configuration`, + output, + parseResult.error.message, + ); + } + return parseResult.success ? parseResult.data : config; + } + return output; } // A tiny wrapper interface to allow the same manager to be passed around diff --git a/src/card-controller/config-manager.ts b/src/card-controller/config-manager.ts index a062944d..947b530b 100644 --- a/src/card-controller/config-manager.ts +++ b/src/card-controller/config-manager.ts @@ -4,7 +4,7 @@ import { CardWideConfig, FrigateCardConfig, frigateCardConfigSchema, - RawFrigateCardConfig + RawFrigateCardConfig, } from '../config/types'; import { localize } from '../localize/localize'; import { setProfiles } from '../config/profiles'; @@ -104,11 +104,11 @@ export class ConfigManager { if (!this._config) { return; } - const overriddenConfig = getOverriddenConfig( - conditionsManager, - this._config, - this._config.overrides, - ) as FrigateCardConfig; + const overriddenConfig = getOverriddenConfig(conditionsManager, this._config, { + configOverrides: this._config.overrides, + schema: frigateCardConfigSchema, + logOnParseError: !!this.getCardWideConfig()?.debug?.logging, + }) as FrigateCardConfig; // Save on Lit re-rendering costs by only updating the configuration if it // actually changes. diff --git a/src/components/live/live.ts b/src/components/live/live.ts index fe075223..59a0bc70 100644 --- a/src/components/live/live.ts +++ b/src/components/live/live.ts @@ -26,8 +26,9 @@ import { CardWideConfig, frigateCardConfigDefaults, LiveConfig, - LiveOverrides, + liveConfigAbsoluteRootSchema, LiveProvider, + Overrides, TransitionEffect, } from '../../config/types.js'; import { localize } from '../../localize/localize.js'; @@ -80,7 +81,7 @@ export class FrigateCardLive extends LitElement { public overriddenLiveConfig?: LiveConfig; @property({ attribute: false, hasChanged: contentsChanged }) - public liveOverrides?: LiveOverrides; + public overrides?: Overrides; @property({ attribute: false }) public cameraManager?: CameraManager; @@ -151,7 +152,7 @@ export class FrigateCardLive extends LitElement { .overriddenLiveConfig=${this.overriddenLiveConfig} .inBackground=${this._controller.isInBackground()} .conditionsManagerEpoch=${this.conditionsManagerEpoch} - .liveOverrides=${this.liveOverrides} + .overrides=${this.overrides} .cardWideConfig=${this.cardWideConfig} .cameraManager=${this.cameraManager} .microphoneManager=${this.microphoneManager} @@ -182,7 +183,7 @@ export class FrigateCardLiveGrid extends LitElement { public overriddenLiveConfig?: LiveConfig; @property({ attribute: false, hasChanged: contentsChanged }) - public liveOverrides?: LiveOverrides; + public overrides?: Overrides; @property({ attribute: false }) public conditionsManagerEpoch?: ConditionsManagerEpoch; @@ -211,7 +212,7 @@ export class FrigateCardLiveGrid extends LitElement { .nonOverriddenLiveConfig=${this.nonOverriddenLiveConfig} .overriddenLiveConfig=${this.overriddenLiveConfig} .conditionsManagerEpoch=${this.conditionsManagerEpoch} - .liveOverrides=${this.liveOverrides} + .overrides=${this.overrides} .cardWideConfig=${this.cardWideConfig} .cameraManager=${this.cameraManager} .microphoneManager=${this.microphoneManager} @@ -290,7 +291,7 @@ export class FrigateCardLiveCarousel extends LitElement { public overriddenLiveConfig?: LiveConfig; @property({ attribute: false, hasChanged: contentsChanged }) - public liveOverrides?: LiveOverrides; + public overrides?: Overrides; @property({ attribute: false }) public conditionsManagerEpoch?: ConditionsManagerEpoch; @@ -471,19 +472,23 @@ export class FrigateCardLiveCarousel extends LitElement { // (in the carousel for example) is not necessarily the live camera *this* // is rendering right now, so we provide a // stateOverride to evaluate the condition in that context. - const config = getOverriddenConfig( + const liveConfig = getOverriddenConfig( this.conditionsManagerEpoch.manager, - this.nonOverriddenLiveConfig, - this.liveOverrides, - { camera: cameraID }, - ) as LiveConfig; + { live: this.nonOverriddenLiveConfig }, + { + configOverrides: this.overrides, + stateOverrides: { camera: cameraID }, + schema: liveConfigAbsoluteRootSchema, + logOnParseError: !!this.cardWideConfig?.debug?.logging, + }, + ).live as LiveConfig; const cameraMetadata = this.cameraManager.getCameraMetadata(cameraID); return html`
this.cameraManager?.getCameraEndpoints(cameraID) ?? undefined, )} .label=${cameraMetadata?.title ?? ''} - .liveConfig=${config} + .liveConfig=${liveConfig} .hass=${this.hass} .cardWideConfig=${this.cardWideConfig} > diff --git a/src/components/views.ts b/src/components/views.ts index d1c3a2ef..f5d08c0e 100644 --- a/src/components/views.ts +++ b/src/components/views.ts @@ -9,10 +9,7 @@ import { import { customElement, property } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; import { CameraManager } from '../camera-manager/manager.js'; -import { - ConditionsManagerEpoch, - getOverridesByKey, -} from '../card-controller/conditions-manager.js'; +import { ConditionsManagerEpoch } from '../card-controller/conditions-manager.js'; import { ReadonlyMicrophoneManager } from '../card-controller/microphone-manager.js'; import { CardWideConfig, @@ -233,10 +230,7 @@ export class FrigateCardViews extends LitElement { .nonOverriddenLiveConfig=${this.nonOverriddenConfig.live} .overriddenLiveConfig=${this.overriddenConfig.live} .conditionsManagerEpoch=${this.conditionsManagerEpoch} - .liveOverrides=${getOverridesByKey( - 'live', - this.overriddenConfig.overrides, - )} + .overrides=${this.overriddenConfig.overrides} .cameraManager=${this.cameraManager} .cardWideConfig=${this.cardWideConfig} .microphoneManager=${this.microphoneManager} @@ -248,10 +242,6 @@ export class FrigateCardViews extends LitElement { : `` } `; - - // .fetchMediaType=${this.view?.is('live') ? this.overriddenConfig.live.controls.thumbnails.media_type : undefined} - // .fetchEventsMediaType=${this.view?.is('live') ? this.overriddenConfig.live.controls.thumbnails.events_media_type : undefined} - } static get styles(): CSSResultGroup { diff --git a/src/config/management.ts b/src/config/management.ts index 146531f2..454826ee 100644 --- a/src/config/management.ts +++ b/src/config/management.ts @@ -42,7 +42,6 @@ import { arrayify } from '../utils/basic'; * @param keys The key to the property to set. * @param value The value to set. */ - export const setConfigValue = ( obj: RawFrigateCardConfig, keys: string | (string | number)[], @@ -689,4 +688,5 @@ const UPGRADES = [ // Delete the value if it's set to the default. transform: (val) => (val === 'low' ? ['low-performance'] : null), }), + upgradeArrayOfObjects(CONF_OVERRIDES, upgradeMoveTo('overrides', 'merge')), ]; diff --git a/src/config/types.ts b/src/config/types.ts index d3ca5191..7692d2ec 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -1008,8 +1008,24 @@ const livethumbnailsControlSchema = thumbnailsControlSchema.extend({ ), }); -const liveOverridableConfigSchema = z +const liveConfigSchema = z .object({ + auto_pause: z + .enum(MEDIA_ACTION_NEGATIVE_CONDITIONS) + .array() + .default(liveConfigDefault.auto_pause), + auto_play: z + .enum(MEDIA_ACTION_POSITIVE_CONDITIONS) + .array() + .default(liveConfigDefault.auto_play), + auto_mute: z + .enum(MEDIA_MUTE_CONDITIONS) + .array() + .default(liveConfigDefault.auto_mute), + auto_unmute: z + .enum(MEDIA_UNMUTE_CONDITIONS) + .array() + .default(liveConfigDefault.auto_unmute), controls: z .object({ builtin: z.boolean().default(liveConfigDefault.controls.builtin), @@ -1032,55 +1048,36 @@ const liveOverridableConfigSchema = z title: titleControlConfigSchema.optional(), }) .default(liveConfigDefault.controls), - show_image_during_load: z - .boolean() - .default(liveConfigDefault.show_image_during_load), - microphone: microphoneConfigSchema.default(liveConfigDefault.microphone), - zoomable: z.boolean().default(liveConfigDefault.zoomable), display: viewDisplaySchema, - }) - .merge(actionsSchema); - -const liveConfigSchema = liveOverridableConfigSchema - .extend({ - auto_play: z - .enum(MEDIA_ACTION_POSITIVE_CONDITIONS) - .array() - .default(liveConfigDefault.auto_play), - auto_pause: z - .enum(MEDIA_ACTION_NEGATIVE_CONDITIONS) - .array() - .default(liveConfigDefault.auto_pause), - auto_mute: z - .enum(MEDIA_MUTE_CONDITIONS) - .array() - .default(liveConfigDefault.auto_mute), - auto_unmute: z - .enum(MEDIA_UNMUTE_CONDITIONS) - .array() - .default(liveConfigDefault.auto_unmute), - preload: z.boolean().default(liveConfigDefault.preload), + draggable: z.boolean().default(liveConfigDefault.draggable), lazy_load: z.boolean().default(liveConfigDefault.lazy_load), lazy_unload: z .enum(MEDIA_ACTION_NEGATIVE_CONDITIONS) .array() .default(liveConfigDefault.lazy_unload), - draggable: z.boolean().default(liveConfigDefault.draggable), + microphone: microphoneConfigSchema.default(liveConfigDefault.microphone), + preload: z.boolean().default(liveConfigDefault.preload), + show_image_during_load: z + .boolean() + .default(liveConfigDefault.show_image_during_load), transition_effect: transitionEffectConfigSchema.default( liveConfigDefault.transition_effect, ), + zoomable: z.boolean().default(liveConfigDefault.zoomable), }) + .merge(actionsSchema) .default(liveConfigDefault); export type LiveConfig = z.infer; -const liveOverridesSchema = z - .object({ - conditions: frigateCardConditionSchema.array(), - overrides: liveOverridableConfigSchema, - }) - .array() - .optional(); -export type LiveOverrides = z.infer; +// This schema is used when the live config needs to be overridden (see +// `live.ts`). Overrides will always be "relative" to the config root, so this +// schema maintains that 'depth' from the root but without the other +// requirements that frigateCardConfigSchema has. Without this, overrides +// calculated in `live.ts` would fail since cameras/type are not provided (as +// these are mandatory parameters in the full config). +export const liveConfigAbsoluteRootSchema = z.object({ + live: liveConfigSchema, +}); // ************************************************************************* // Cast Configuration @@ -1580,26 +1577,16 @@ export const dimensionsConfigSchema = z // Override Configuration // ************************************************************************* -// Strip all defaults from the override schemas, to ensure values are only what -// the user has specified. -const overrideConfigurationSchema = z.object({ - cameras: deepRemoveDefaults(camerasConfigSchema).optional(), - cameras_global: deepRemoveDefaults(cameraConfigSchema).optional(), - live: deepRemoveDefaults(liveOverridableConfigSchema).optional(), - menu: deepRemoveDefaults(menuConfigSchema).optional(), - image: deepRemoveDefaults(imageConfigSchema).optional(), - view: deepRemoveDefaults(viewConfigSchema).optional(), - dimensions: deepRemoveDefaults(dimensionsConfigSchema).optional(), -}); -export type OverrideConfigurationKey = keyof z.infer; - const overridesSchema = z .object({ conditions: frigateCardConditionSchema.array(), - overrides: overrideConfigurationSchema, + merge: z.object({}).passthrough().optional(), + set: z.object({}).passthrough().optional(), + delete: z.string().array().optional(), }) .array() .optional(); +export type Overrides = z.infer; // ************************************************************************* // Automation Configuration diff --git a/src/utils/basic.ts b/src/utils/basic.ts index 9c074f54..a31f1223 100644 --- a/src/utils/basic.ts +++ b/src/utils/basic.ts @@ -243,8 +243,8 @@ export const getChildrenFromElement = (parent: HTMLElement): HTMLElement[] => { return children.filter(isHTMLElement); }; -export const recursivelyMergeObjectsNotArrays = (src1: T, src2: T): T => { - return mergeWith({}, src1, src2, (_a, b) => (Array.isArray(b) ? b : undefined)); +export const recursivelyMergeObjectsNotArrays = (target: T, src1: T, src2: T): T => { + return mergeWith(target, src1, src2, (_a, b) => (Array.isArray(b) ? b : undefined)); }; export const aspectRatioToString = (options?: { @@ -268,3 +268,21 @@ export const aspectRatioToStyle = (options?: { 'aspect-ratio': aspectRatioToString(options), }; }; + +/** + * Remove empty slots from nested arrays. + */ +export const desparsifyArrays = (data: T): T => { + if (Array.isArray(data)) { + return ( + data.filter((item) => item !== undefined).map((item) => desparsifyArrays(item)) + ); + } else if (typeof data === 'object' && data !== null) { + const result: Record = {}; + for (const key in data) { + result[key] = desparsifyArrays(data[key]); + } + return result; + } + return data; +}; diff --git a/src/utils/zod.ts b/src/utils/zod.ts index 03de3a50..2afdc90c 100644 --- a/src/utils/zod.ts +++ b/src/utils/zod.ts @@ -28,7 +28,10 @@ export function deepRemoveDefaults(schema: T): any { } if (schema instanceof z.ZodArray) { - return z.ZodArray.create(deepRemoveDefaults(schema.element)); + return z.ZodArray.create(deepRemoveDefaults(schema.element)) + .min(schema._def.minLength?.value, schema._def.minLength?.message) + .max(schema._def.maxLength?.value, schema._def.maxLength?.message) + .length(schema._def.exactLength?.value, schema._def.exactLength?.message); } if (schema instanceof z.ZodOptional) { diff --git a/tests/camera-manager/manager.test.ts b/tests/camera-manager/manager.test.ts index 282cf68e..6a0a2258 100644 --- a/tests/camera-manager/manager.test.ts +++ b/tests/camera-manager/manager.test.ts @@ -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(), []); + const manager = createCameraManager(api, mock()); expect(manager.generateDefaultEventQueries('not_a_camera')).toBeNull(); }); diff --git a/tests/card-controller/conditions-manager.test.ts b/tests/card-controller/conditions-manager.test.ts index eea42258..b072c0c0 100644 --- a/tests/card-controller/conditions-manager.test.ts +++ b/tests/card-controller/conditions-manager.test.ts @@ -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, + 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', diff --git a/tests/config/management.test.ts b/tests/config/management.test.ts index e12e60e4..9bfb248e 100644 --- a/tests/config/management.test.ts +++ b/tests/config/management.test.ts @@ -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', + }, + }, + }, + ], + }); + }); }); }); diff --git a/tests/config/profiles/index.test.ts b/tests/config/profiles/index.test.ts index 20fd2ad5..c34d1c08 100644 --- a/tests/config/profiles/index.test.ts +++ b/tests/config/profiles/index.test.ts @@ -25,7 +25,7 @@ describe('setProfiles', () => { it('should handle profiles', () => { const input = { type: 'frigate-hass-card', - cameras: [], + cameras: [{}], live: { controls: { timeline: { diff --git a/tests/config/types.test.ts b/tests/config/types.test.ts index 76ec3350..e94ced6c 100644 --- a/tests/config/types.test.ts +++ b/tests/config/types.test.ts @@ -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: { diff --git a/tests/test-utils.ts b/tests/test-utils.ts index 2009ceaf..3b1cf6e2 100644 --- a/tests/test-utils.ts +++ b/tests/test-utils.ts @@ -67,7 +67,7 @@ export const createCondition = ( export const createConfig = (config?: RawFrigateCardConfig): FrigateCardConfig => { return frigateCardConfigSchema.parse({ type: 'frigate-hass-card', - cameras: [], + cameras: [{}], ...config, }); }; diff --git a/tests/utils/basic.test.ts b/tests/utils/basic.test.ts index 4bbadfe6..38c0add6 100644 --- a/tests/utils/basic.test.ts +++ b/tests/utils/basic.test.ts @@ -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: [], + }, + }); + }); + }); +}); diff --git a/tests/utils/zod.test.ts b/tests/utils/zod.test.ts index b2d1b837..ef69193f 100644 --- a/tests/utils/zod.test.ts +++ b/tests/utils/zod.test.ts @@ -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', () => { diff --git a/yarn.lock b/yarn.lock index f11c2b5c..4cbbfb1b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -800,6 +800,16 @@ __metadata: languageName: node linkType: hard +"@jridgewell/trace-mapping@npm:^0.3.23": + version: 0.3.25 + resolution: "@jridgewell/trace-mapping@npm:0.3.25" + dependencies: + "@jridgewell/resolve-uri": "npm:^3.1.0" + "@jridgewell/sourcemap-codec": "npm:^1.4.14" + checksum: 10c0/3d1ce6ebc69df9682a5a8896b414c6537e428a1d68b02fcc8363b04284a8ca0df04d0ee3013132252ab14f2527bc13bea6526a912ecb5658f0e39fd2860b4df4 + languageName: node + linkType: hard + "@lit-labs/scoped-registry-mixin@npm:^1.0.3": version: 1.0.3 resolution: "@lit-labs/scoped-registry-mixin@npm:1.0.3" @@ -1467,76 +1477,76 @@ __metadata: languageName: node linkType: hard -"@vitest/coverage-istanbul@npm:^1.3.1": - version: 1.3.1 - resolution: "@vitest/coverage-istanbul@npm:1.3.1" +"@vitest/coverage-istanbul@npm:^1.5.0": + version: 1.5.0 + resolution: "@vitest/coverage-istanbul@npm:1.5.0" dependencies: debug: "npm:^4.3.4" istanbul-lib-coverage: "npm:^3.2.2" istanbul-lib-instrument: "npm:^6.0.1" istanbul-lib-report: "npm:^3.0.1" - istanbul-lib-source-maps: "npm:^4.0.1" + istanbul-lib-source-maps: "npm:^5.0.4" istanbul-reports: "npm:^3.1.6" magicast: "npm:^0.3.3" picocolors: "npm:^1.0.0" test-exclude: "npm:^6.0.0" peerDependencies: - vitest: 1.3.1 - checksum: 10c0/40efacde51e5998bde8032dd45d64fe214674f1efb97c094c4ae9ef1893afc4e0ef97f3fa4cc4923561b67b1196c27a877a0fa5bc71a049b8e48e3ef1f5c48db + vitest: 1.5.0 + checksum: 10c0/12abbaf8925c797e1fafef05c8b7d554c9fc61d13eb70c3f8532ade72db305a3cb1fec9bb7642763de27471e3f3da3d684d6533aa954756c5edf2707d2fc25cd languageName: node linkType: hard -"@vitest/expect@npm:1.3.1": - version: 1.3.1 - resolution: "@vitest/expect@npm:1.3.1" +"@vitest/expect@npm:1.5.0": + version: 1.5.0 + resolution: "@vitest/expect@npm:1.5.0" dependencies: - "@vitest/spy": "npm:1.3.1" - "@vitest/utils": "npm:1.3.1" + "@vitest/spy": "npm:1.5.0" + "@vitest/utils": "npm:1.5.0" chai: "npm:^4.3.10" - checksum: 10c0/ea66a1e912d896a481a27631b68089b885af7e8ed62ba8aaa119c37a9beafe6c094fd672775a20e6e23460af66e294f9ca259e6e0562708d1b7724eaaf53c7bb + checksum: 10c0/12138caf0831a9bcdf475750fdff27588f03f11ee5101124f9d39951bbcdec39aa6a19a1a30f3ab0ca17374ddfc7399b861e2fcb1548bba302943e8e88512a9c languageName: node linkType: hard -"@vitest/runner@npm:1.3.1": - version: 1.3.1 - resolution: "@vitest/runner@npm:1.3.1" +"@vitest/runner@npm:1.5.0": + version: 1.5.0 + resolution: "@vitest/runner@npm:1.5.0" dependencies: - "@vitest/utils": "npm:1.3.1" + "@vitest/utils": "npm:1.5.0" p-limit: "npm:^5.0.0" pathe: "npm:^1.1.1" - checksum: 10c0/d732de2368d2bc32cbc27f0bbc5477f6e36088ddfb873c036935a45b1b252ebc529b932cf5cd944eed9b692243acebef828f6d3218583cb8a6817a8270712050 + checksum: 10c0/a7b693e4121b6159a77f6cbffc03c228cd18c3734e27a968fe26579777f6d7a4d55a02fba396d564df996a91881ee76de80ee5a526d27a4b508f44a9af62d95a languageName: node linkType: hard -"@vitest/snapshot@npm:1.3.1": - version: 1.3.1 - resolution: "@vitest/snapshot@npm:1.3.1" +"@vitest/snapshot@npm:1.5.0": + version: 1.5.0 + resolution: "@vitest/snapshot@npm:1.5.0" dependencies: magic-string: "npm:^0.30.5" pathe: "npm:^1.1.1" pretty-format: "npm:^29.7.0" - checksum: 10c0/cad0844270852c6d53c1ca6b7ca279034880d2140837ff245d5bd2376f4356cc924929c58dc69bcf9fad83ba934d4a06000c908971cc24b5d7a9ec2656b72d29 + checksum: 10c0/d897070b3a7c008eb58d44ca0dc324c69dfc1f787335e59fb930ae2e8d02a148e6b3c13497a588083a0654bdc430281fdd84276ead4888230314a2984dd9f93d languageName: node linkType: hard -"@vitest/spy@npm:1.3.1": - version: 1.3.1 - resolution: "@vitest/spy@npm:1.3.1" +"@vitest/spy@npm:1.5.0": + version: 1.5.0 + resolution: "@vitest/spy@npm:1.5.0" dependencies: tinyspy: "npm:^2.2.0" - checksum: 10c0/efc42f679d2a51fc6583ca3136ccd47581cb27c923ed3cb0500f5dee9aac99b681bfdd400c16ef108f2e0761daa642bc190816a6411931a2aba99ebf8b213dd4 + checksum: 10c0/0206f1e8431f543474dc6d252b553227d0f286b27226b987b63babb18865e8320e6c3d822c67782aae68555728a57ecdeb7c4e16283dbad49791608a691f26d1 languageName: node linkType: hard -"@vitest/utils@npm:1.3.1": - version: 1.3.1 - resolution: "@vitest/utils@npm:1.3.1" +"@vitest/utils@npm:1.5.0": + version: 1.5.0 + resolution: "@vitest/utils@npm:1.5.0" dependencies: diff-sequences: "npm:^29.6.3" estree-walker: "npm:^3.0.3" loupe: "npm:^2.3.7" pretty-format: "npm:^29.7.0" - checksum: 10c0/d604c8ad3b1aee30d4dcd889098f591407bfe18547ff96485b1d1ed54eff58219c756a9544a7fbd4e37886863abacd7a89a76334cb3ea7f84c3d496bb757db23 + checksum: 10c0/b9d779ea5c1a5759df4f59e3ba3f3a94816d6c600afe7a2d14b963ea114ce1acedbfe678cbfacb0a20d33cabcce890a08b2ce3fd52e3465f2e0969cc39f7686b languageName: node linkType: hard @@ -3855,7 +3865,7 @@ __metadata: "@types/masonry-layout": "npm:^4.2.5" "@typescript-eslint/eslint-plugin": "npm:^5.36.2" "@typescript-eslint/parser": "npm:^5.36.2" - "@vitest/coverage-istanbul": "npm:^1.3.1" + "@vitest/coverage-istanbul": "npm:^1.5.0" component-emitter: "npm:^1.3.0" crypto: "npm:^1.0.1" date-fns: "npm:^2.29.2" @@ -3895,7 +3905,7 @@ __metadata: vis-data: "npm:^7.1.9" vis-timeline: "npm:^7.7.3" vis-util: "npm:^5.0.2" - vitest: "npm:^1.3.1" + vitest: "npm:^1.5.0" vitest-mock-extended: "npm:^1.3.1" web-dialog: "npm:^0.0.11" xss: "npm:^1.0.14" @@ -4938,14 +4948,14 @@ __metadata: languageName: node linkType: hard -"istanbul-lib-source-maps@npm:^4.0.1": - version: 4.0.1 - resolution: "istanbul-lib-source-maps@npm:4.0.1" +"istanbul-lib-source-maps@npm:^5.0.4": + version: 5.0.4 + resolution: "istanbul-lib-source-maps@npm:5.0.4" dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.23" debug: "npm:^4.1.1" istanbul-lib-coverage: "npm:^3.0.0" - source-map: "npm:^0.6.1" - checksum: 10c0/19e4cc405016f2c906dff271a76715b3e881fa9faeb3f09a86cb99b8512b3a5ed19cadfe0b54c17ca0e54c1142c9c6de9330d65506e35873994e06634eebeb66 + checksum: 10c0/48b48294590675005ba439888a53157fc71a99d78321428f3ce5f64e28cdfb6bc6eb45871333f448437118ef56a0ef371f4958163e2c2d066d3a703415a71b2e languageName: node linkType: hard @@ -8011,10 +8021,10 @@ __metadata: languageName: node linkType: hard -"tinypool@npm:^0.8.2": - version: 0.8.2 - resolution: "tinypool@npm:0.8.2" - checksum: 10c0/8998626614172fc37c394e9a14e701dc437727fc6525488a4d4fd42044a4b2b59d6f076d750cbf5c699f79c58dd4e40599ab09e2f1ae0df4b23516b98c9c3055 +"tinypool@npm:^0.8.3": + version: 0.8.3 + resolution: "tinypool@npm:0.8.3" + checksum: 10c0/c219d0cfb69de8e3cf17403034a508d773f2fccaad79a13cdbad68600c4fb10186ad814d2320bcaa8f6e774fff5666d2a3d3b241dc8a7ad9d970ee63fe620a32 languageName: node linkType: hard @@ -8487,9 +8497,9 @@ __metadata: languageName: node linkType: hard -"vite-node@npm:1.3.1": - version: 1.3.1 - resolution: "vite-node@npm:1.3.1" +"vite-node@npm:1.5.0": + version: 1.5.0 + resolution: "vite-node@npm:1.5.0" dependencies: cac: "npm:^6.7.14" debug: "npm:^4.3.4" @@ -8498,7 +8508,7 @@ __metadata: vite: "npm:^5.0.0" bin: vite-node: vite-node.mjs - checksum: 10c0/b50665ef224f3527f856ab88a0cfabab36dd6e2dd1e3edca8f8f25d5d33754e1050495472c2c82147d0dcf7c5280971dae2f37a531c10f3941d8d3344e34ce0b + checksum: 10c0/85f9e8616f3612c71193a2bbdfb00f610685108a65eaa7ac535655ca1bd65dda5436ee577465436f715966f34e4b25b54140bbd584fe2c95ea0726acf4ecd351 languageName: node linkType: hard @@ -8554,15 +8564,15 @@ __metadata: languageName: node linkType: hard -"vitest@npm:^1.3.1": - version: 1.3.1 - resolution: "vitest@npm:1.3.1" +"vitest@npm:^1.5.0": + version: 1.5.0 + resolution: "vitest@npm:1.5.0" dependencies: - "@vitest/expect": "npm:1.3.1" - "@vitest/runner": "npm:1.3.1" - "@vitest/snapshot": "npm:1.3.1" - "@vitest/spy": "npm:1.3.1" - "@vitest/utils": "npm:1.3.1" + "@vitest/expect": "npm:1.5.0" + "@vitest/runner": "npm:1.5.0" + "@vitest/snapshot": "npm:1.5.0" + "@vitest/spy": "npm:1.5.0" + "@vitest/utils": "npm:1.5.0" acorn-walk: "npm:^8.3.2" chai: "npm:^4.3.10" debug: "npm:^4.3.4" @@ -8574,15 +8584,15 @@ __metadata: std-env: "npm:^3.5.0" strip-literal: "npm:^2.0.0" tinybench: "npm:^2.5.1" - tinypool: "npm:^0.8.2" + tinypool: "npm:^0.8.3" vite: "npm:^5.0.0" - vite-node: "npm:1.3.1" + vite-node: "npm:1.5.0" why-is-node-running: "npm:^2.2.2" peerDependencies: "@edge-runtime/vm": "*" "@types/node": ^18.0.0 || >=20.0.0 - "@vitest/browser": 1.3.1 - "@vitest/ui": 1.3.1 + "@vitest/browser": 1.5.0 + "@vitest/ui": 1.5.0 happy-dom: "*" jsdom: "*" peerDependenciesMeta: @@ -8600,7 +8610,7 @@ __metadata: optional: true bin: vitest: vitest.mjs - checksum: 10c0/66d312a3dc12e67bba22d31332d939e89cd17d38531893c7b13b8826704564031c1dde795df2799b855660572c19a595301e920710c7775d072ee6332502efc5 + checksum: 10c0/87f6666cccd52678a0e1dbf32e7492e4003580a168114221e4c2d404d53097d886a8be4c5c379e347d0e749affa0ea5416e4e3a0356aeca228a77ee54891ca3c languageName: node linkType: hard