diff --git a/src/camera-manager/manager.ts b/src/camera-manager/manager.ts index db2b38d0..3a6f3719 100644 --- a/src/camera-manager/manager.ts +++ b/src/camera-manager/manager.ts @@ -8,7 +8,7 @@ import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const.js'; import { localize } from '../localize/localize.js'; import { allPromises, arrayify, setify } from '../utils/basic.js'; import { getCameraID } from '../utils/camera.js'; -import { CardCameraAPI } from '../utils/card-controller/types.js'; +import { CardCameraAPI } from '../card-controller/types.js'; import { log } from '../utils/debug.js'; import { EntityRegistryManager } from '../utils/ha/entity-registry/index.js'; import { ViewMedia } from '../view/media.js'; diff --git a/src/card-controller/config-manager.ts b/src/card-controller/config-manager.ts index 8684037b..e597ad3a 100644 --- a/src/card-controller/config-manager.ts +++ b/src/card-controller/config-manager.ts @@ -25,7 +25,7 @@ export class ConfigManager { protected _rawConfig: RawFrigateCardConfig | null = null; protected _cardWideConfig: CardWideConfig | null = null; - constructor(api) { + constructor(api: CardConfigAPI) { this._api = api; } diff --git a/src/card.ts b/src/card.ts index 5579fa1b..f7eca6c1 100644 --- a/src/card.ts +++ b/src/card.ts @@ -27,8 +27,8 @@ import { Message, } from './types.js'; import { frigateCardHasAction } from './utils/action.js'; -import { ConditionEvaluateRequestEvent } from './utils/card-controller/conditions-manager.js'; -import { CardController } from './utils/card-controller/controller'; +import { ConditionEvaluateRequestEvent } from './card-controller/conditions-manager.js'; +import { CardController } from './card-controller/controller'; import { MenuButtonController } from './utils/menu-controller'; import { View } from './view/view.js'; diff --git a/src/components/elements.ts b/src/components/elements.ts index 1cf93617..e2c50e72 100644 --- a/src/components/elements.ts +++ b/src/components/elements.ts @@ -35,7 +35,7 @@ import { dispatchFrigateCardEvent } from '../utils/basic.js'; import { ConditionsManagerEpoch, evaluateConditionViaEvent, -} from '../utils/card-controller/conditions-manager.js'; +} from '../card-controller/conditions-manager.js'; import { dispatchFrigateCardErrorEvent } from './message.js'; /* A note on picture element rendering: diff --git a/src/components/live/live.ts b/src/components/live/live.ts index 43c12c66..a8251e8c 100644 --- a/src/components/live/live.ts +++ b/src/components/live/live.ts @@ -40,7 +40,7 @@ import { contentsChanged } from '../../utils/basic.js'; import { ConditionsManagerEpoch, getOverriddenConfig, -} from '../../utils/card-controller/conditions-manager.js'; +} from '../../card-controller/conditions-manager.js'; import { CarouselSelected } from '../../utils/embla/carousel-controller.js'; import { AutoLazyLoad } from '../../utils/embla/plugins/auto-lazy-load/auto-lazy-load.js'; import { AutoMediaActions } from '../../utils/embla/plugins/auto-media-actions/auto-media-actions.js'; diff --git a/src/components/views.ts b/src/components/views.ts index 949acc95..da524511 100644 --- a/src/components/views.ts +++ b/src/components/views.ts @@ -9,10 +9,10 @@ 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 '../utils/card-controller/conditions-manager'; +import { ConditionsManagerEpoch, getOverridesByKey } from '../card-controller/conditions-manager.js'; import viewsStyle from '../scss/views.scss'; import { ExtendedHomeAssistant } from '../types.js'; -import { ConfigManager } from '../utils/card-controller/config-manager.js'; +import { ConfigManager } from '../card-controller/config-manager.js'; import { ResolvedMediaCache } from '../utils/ha/resolved-media.js'; import { View } from '../view/view.js'; import './surround.js'; diff --git a/src/utils/card-controller/actions-manager.ts b/src/utils/card-controller/actions-manager.ts deleted file mode 100644 index a8c8745c..00000000 --- a/src/utils/card-controller/actions-manager.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { - Actions, - ActionsConfig, - FrigateCardCustomAction, - FRIGATE_CARD_VIEW_DEFAULT, -} from '../../config/types.js'; -import { - convertActionToFrigateCardCustomAction, - frigateCardHandleActionConfig, - getActionConfigGivenAction, -} from '../action.js'; -import { getStreamCameraID } from '../substream.js'; -import { CardActionsManagerAPI } from './types.js'; - -export class ActionsManager { - protected _api: CardActionsManagerAPI; - - constructor(api: CardActionsManagerAPI) { - this._api = api; - } - - /** - * Merge card-wide and view-specific actions. - * @returns A combined set of action. - */ - public getMergedActions(): ActionsConfig { - const view = this._api.getViewManager().getView(); - if (this._api.getMessageManager().hasMessage()) { - return {}; - } - - const config = this._api.getConfigManager().getConfig(); - let specificActions: Actions | undefined = undefined; - if (view?.is('live')) { - specificActions = config?.live.actions; - } else if (view?.isGalleryView()) { - specificActions = config?.media_gallery?.actions; - } else if (view?.isViewerView()) { - specificActions = config?.media_viewer.actions; - } else if (view?.is('image')) { - specificActions = config?.image?.actions; - } else { - return {}; - } - return { ...config?.view.actions, ...specificActions }; - } - - /** - * Handle an human interaction called on an element (e.g. 'tap'). - */ - public handleInteraction(interaction: string): void { - const hass = this._api.getHASSManager().getHASS(); - const config = this.getMergedActions(); - const actionConfig = getActionConfigGivenAction(interaction, config); - if ( - hass && - config && - interaction && - // Don't call frigateCardHandleActionConfig() unless there is explicitly an - // action defined (as it uses a default that is unhelpful for views that - // have default tap/click actions). - actionConfig - ) { - frigateCardHandleActionConfig( - this._api.getCardElementManager().getElement(), - hass, - config, - interaction, - actionConfig, - ); - } - } - - public handleActionEvent = (ev: Event): void => { - if (!('detail' in ev)) { - // The event may not actually be a CustomEvent object, but may still have a - // detail field. See: - // https://github.com/custom-cards/custom-card-helpers/blob/master/src/fire-event.ts#L70 - return; - } - - const frigateCardAction = convertActionToFrigateCardCustomAction(ev.detail); - if (frigateCardAction) { - this.executeAction(frigateCardAction); - } - }; - - /** - * Execute a card action. - * @param frigateCardAction - * @returns `true` if an action is executed. - */ - public async executeAction(frigateCardAction: FrigateCardCustomAction): Promise { - const config = this._api.getConfigManager().getConfig(); - const mediaLoadedInfoManager = this._api.getMediaLoadedInfoManager(); - - if ( - // Command not intended for this card (e.g. query string command). - frigateCardAction.card_id && - config?.card_id !== frigateCardAction.card_id - ) { - return; - } - - // Note: This function needs to process (view-related) commands even when - // _view has not yet been initialized (since it may be used to set a view - // via the querystring). - const view = this._api.getViewManager().getView(); - - const action = frigateCardAction.frigate_card_action; - - switch (action) { - case 'default': - this._api.getViewManager().setViewDefault(); - break; - case 'clip': - case 'clips': - case 'image': - case 'live': - case 'recording': - case 'recordings': - case 'snapshot': - case 'snapshots': - case 'timeline': - this._api.getViewManager().setViewByParameters({ - viewName: action, - cameraID: view?.camera, - }); - break; - case 'download': - await this._api.getDownloadManager().downloadViewerMedia(); - break; - case 'camera_ui': - this._api.getCameraURLManager().openURL(); - break; - case 'expand': - this._api.getExpandManager().toggleExpanded(); - break; - case 'fullscreen': - this._api.getFullscreenManager().toggleFullscreen(); - break; - case 'menu_toggle': - // This is a rare code path: this would only be used if someone has a - // menu toggle action configured outside of the menu itself (e.g. - // picture elements). - this._api.getCardElementManager().toggleMenu(); - break; - case 'camera_select': - const selectCameraID = frigateCardAction.camera; - if (view) { - const viewOnCameraSelect = config?.view.camera_select ?? 'current'; - const targetViewName = - viewOnCameraSelect === 'current' ? view.view : viewOnCameraSelect; - const verifiedViewName = this._api - .getViewManager() - .isViewSupportedByCamera(selectCameraID, targetViewName) - ? targetViewName - : FRIGATE_CARD_VIEW_DEFAULT; - this._api.getViewManager().setViewByParameters({ - viewName: verifiedViewName, - cameraID: selectCameraID, - }); - } - break; - case 'live_substream_select': { - this._api.getViewManager().setViewWithSubstream(frigateCardAction.camera); - break; - } - case 'live_substream_off': { - this._api.getViewManager().setViewWithoutSubstream(); - break; - } - case 'live_substream_on': { - this._api.getViewManager().setViewWithSubstream(); - break; - } - case 'media_player': - const mediaPlayer = frigateCardAction.media_player; - const mediaPlayerController = this._api.getMediaPlayerManager(); - const media = view?.queryResults?.getSelectedResult() ?? null; - - if (frigateCardAction.media_player_action === 'stop') { - await mediaPlayerController.stop(mediaPlayer); - } else if (view?.is('live')) { - await mediaPlayerController.playLive(mediaPlayer, getStreamCameraID(view)); - } else if (view?.isViewerView() && media) { - await mediaPlayerController.playMedia(mediaPlayer, media); - } - break; - case 'diagnostics': - this._api.getViewManager().setViewByParameters({ viewName: 'diagnostics' }); - break; - case 'microphone_mute': - this._api.getMicrophoneManager().mute(); - break; - case 'microphone_unmute': - await this._api.getMicrophoneManager().unmute(); - break; - case 'mute': - await mediaLoadedInfoManager.get()?.player?.mute(); - break; - case 'unmute': - await mediaLoadedInfoManager.get()?.player?.unmute(); - break; - case 'play': - await mediaLoadedInfoManager.get()?.player?.play(); - break; - case 'pause': - await mediaLoadedInfoManager.get()?.player?.pause(); - break; - case 'screenshot': - await this._api.getDownloadManager().downloadScreenshot(); - break; - case 'display_mode_select': - this._api - .getViewManager() - .setViewWithNewDisplayMode(frigateCardAction.display_mode); - break; - default: - console.warn(`Frigate card received unknown card action: ${action}`); - } - } -} diff --git a/src/utils/card-controller/auto-update-manager.ts b/src/utils/card-controller/auto-update-manager.ts deleted file mode 100644 index bfefc5b0..00000000 --- a/src/utils/card-controller/auto-update-manager.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Timer } from '../timer'; -import { CardAutoRefreshAPI } from './types'; - -export class AutoUpdateManager { - protected _timer = new Timer(); - protected _api: CardAutoRefreshAPI; - - constructor(api: CardAutoRefreshAPI) { - this._api = api; - } - - /** - * Set the update timer to trigger an update refresh every - * `view.update_seconds`. - */ - public startDefaultViewTimer(): void { - this._timer.stop(); - const updateSeconds = this._api.getConfigManager().getConfig() - ?.view.update_seconds; - if (updateSeconds) { - this._timer.start(updateSeconds, () => { - if (this._isAutomatedUpdateAllowed()) { - this._api.getViewManager().setViewDefault(); - } else { - // Not allowed to update this time around, but try again at the next - // interval. - this.startDefaultViewTimer(); - } - }); - } - } - - protected _isAutomatedUpdateAllowed(): boolean { - const triggers = this._api.getTriggersManager(); - const config = this._api.getConfigManager().getConfig(); - const interactionManager = this._api.getInteractionManager(); - - return ( - !triggers.isTriggered() && - (config?.view.update_force || !interactionManager.hasInteraction()) - ); - } -} diff --git a/src/utils/card-controller/automations-manager.ts b/src/utils/card-controller/automations-manager.ts deleted file mode 100644 index dd145502..00000000 --- a/src/utils/card-controller/automations-manager.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Automation, AutomationActions, Automations } from '../../config/types.js'; -import { localize } from '../../localize/localize.js'; -import { frigateCardHandleAction } from '../action.js'; -import { CardAutomationsAPI } from './types.js'; - -const MAX_NESTED_AUTOMATION_EXECUTIONS = 10; - -export class AutomationsManager { - protected _api: CardAutomationsAPI; - - protected _automations: Automations; - protected _priorEvaluations: Map = new Map(); - - // A counter to avoid infinite loops, increases every time actions are run, - // decreases every time actions are complete. - protected _nestedAutomationExecutions = 0; - - constructor(api: CardAutomationsAPI) { - this._api = api; - } - - public setAutomationsFromConfig() { - this._automations = this._api - .getConfigManager() - .getNonOverriddenConfig()?.automations; - } - - public execute(): void { - const hass = this._api.getHASSManager().getHASS(); - - // Never execute automations if there's an error (as our automation loop - // avoidance -- which shows as an error -- would not work!). - if (!hass || this._api.getMessageManager().hasErrorMessage()) { - return; - } - - const actionsToRun: AutomationActions[] = []; - for (const automation of this._automations ?? []) { - const shouldExecute = this._api - .getConditionsManager() - .evaluateCondition(automation.conditions); - const actions = shouldExecute ? automation.actions : automation.actions_not; - const priorEvaluation = this._priorEvaluations.get(automation); - this._priorEvaluations.set(automation, shouldExecute); - if (shouldExecute !== priorEvaluation && actions) { - actionsToRun.push(actions); - } - } - - ++this._nestedAutomationExecutions; - if (this._nestedAutomationExecutions > MAX_NESTED_AUTOMATION_EXECUTIONS) { - this._api.getMessageManager().setMessageIfHigherPriority({ - type: 'error', - message: localize('error.too_many_automations'), - }); - return; - } - - actionsToRun.forEach((actions) => { - frigateCardHandleAction( - this._api.getCardElementManager().getElement(), - hass, - {}, - actions, - ); - }); - --this._nestedAutomationExecutions; - } -} diff --git a/src/utils/card-controller/camera-url-manager.ts b/src/utils/card-controller/camera-url-manager.ts deleted file mode 100644 index 58d9e782..00000000 --- a/src/utils/card-controller/camera-url-manager.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { CardCameraURLAPI } from './types'; - -export class CameraURLManager { - protected _api: CardCameraURLAPI; - - constructor(api: CardCameraURLAPI) { - this._api = api; - } - - public openURL(): void { - const url = this.getCameraURL(); - if (url) { - window.open(url); - } - } - - public hasCameraURL(): boolean { - return !!this.getCameraURL(); - } - - public getCameraURL(): string | null { - const view = this._api.getViewManager().getView(); - const media = view?.queryResults?.getSelectedResult() ?? null; - const endpoints = view?.camera - ? this._api.getCameraManager().getCameraEndpoints(view.camera, { - view: view.view, - ...(media && { media: media }), - }) ?? null - : null; - return endpoints?.ui?.endpoint ?? null; - } -} diff --git a/src/utils/card-controller/card-element-manager.ts b/src/utils/card-controller/card-element-manager.ts deleted file mode 100644 index 2caa067e..00000000 --- a/src/utils/card-controller/card-element-manager.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { LitElement, ReactiveControllerHost } from 'lit'; -import { ActionEventTarget } from '../../action-handler-directive'; -import { setOrRemoveAttribute } from '../basic'; -import { isCardInPanel } from '../ha'; -import { CardElementAPI } from './types'; - -export type ScrollCallback = () => void; -export type MenuToggleCallback = () => void; - -export type CardHTMLElement = LitElement & ReactiveControllerHost & ActionEventTarget; - -export class CardElementManager { - protected _api: CardElementAPI; - - protected _element: CardHTMLElement; - protected _scrollCallback: ScrollCallback; - protected _menuToggleCallback: MenuToggleCallback; - - constructor( - api: CardElementAPI, - element: CardHTMLElement, - scrollCallback: ScrollCallback, - menuToggleCallback: MenuToggleCallback, - ) { - this._api = api; - - this._element = element; - this._scrollCallback = scrollCallback; - this._menuToggleCallback = menuToggleCallback; - } - - public getElement(): HTMLElement { - return this._element; - } - - public scrollReset(): void { - this._scrollCallback(); - } - - public toggleMenu(): void { - this._menuToggleCallback(); - } - - public update(): void { - this._element.requestUpdate(); - } - - public hasUpdated(): boolean { - return this._element.hasUpdated; - } - - public getCardHeight(): number { - return this._element.getBoundingClientRect().height; - } - - public elementConnected(): void { - // Whether or not the card is in panel mode on the dashboard. - setOrRemoveAttribute(this._element, isCardInPanel(this._element), 'panel'); - - this._api.getFullscreenManager().connect(); - - this._element.addEventListener( - 'mousemove', - this._api.getInteractionManager().reportInteraction, - ); - this._element.addEventListener( - 'll-custom', - this._api.getActionsManager().handleActionEvent, - ); - this._element.addEventListener( - '@action', - this._api.getInteractionManager().reportInteraction, - ); - - // Listen for HA `navigate` actions. - // See: https://github.com/home-assistant/frontend/blob/273992c8e9c3062c6e49481b6d7d688a07067232/src/common/navigate.ts#L43 - window.addEventListener( - 'location-changed', - this._api.getQueryStringManager().executeAll, - ); - - // Listen for history state changes (i.e. user using the browser - // back/forward controls). - window.addEventListener('popstate', this._api.getQueryStringManager().executeAll); - - // Manually call the location change handler as the card will be - // disconnected/reconnected when dashboard 'tab' changes happen within HA. - this._api.getQueryStringManager().executeAll(); - } - - public elementDisconnected(): void { - setOrRemoveAttribute(this._element, false, 'panel'); - - // When the dashboard 'tab' is changed, the media is effectively unloaded. - this._api.getMediaLoadedInfoManager().clear(); - this._api.getFullscreenManager().disconnect(); - - this._element.removeEventListener( - 'mousemove', - this._api.getInteractionManager().reportInteraction, - ); - this._element.removeEventListener( - 'll-custom', - this._api.getActionsManager().handleActionEvent, - ); - this._element.removeEventListener( - '@action', - this._api.getInteractionManager().reportInteraction, - ); - - window.removeEventListener( - 'location-changed', - this._api.getQueryStringManager().executeAll, - ); - window.removeEventListener('popstate', this._api.getQueryStringManager().executeAll); - } -} diff --git a/src/utils/card-controller/conditions-manager.ts b/src/utils/card-controller/conditions-manager.ts deleted file mode 100644 index 45703118..00000000 --- a/src/utils/card-controller/conditions-manager.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { HassEntities } from 'home-assistant-js-websocket'; -import merge from 'lodash-es/merge'; -import { copyConfig } from '../../config-mgmt'; -import { - FrigateCardCondition, - frigateConditionalSchema, - OverrideConfigurationKey, - RawFrigateCardConfig, - ViewDisplayMode -} from '../../config/types'; -import { CardConditionAPI } from './types'; - -interface ConditionState { - view?: string; - fullscreen?: boolean; - expand?: boolean; - camera?: string; - state?: HassEntities; - media_loaded?: boolean; - displayMode?: ViewDisplayMode; -} - -export class ConditionEvaluateRequestEvent extends Event { - public condition: FrigateCardCondition; - public evaluation?: boolean; - - constructor(condition: FrigateCardCondition, eventInitDict?: EventInit) { - super('frigate-card:condition:evaluate', eventInitDict); - this.condition = condition; - } -} - -/** - * Evaluate whether a frigateCardCondition is met using an event to evaluate. - * @returns A boolean indicating whether the condition is met. - */ -export function evaluateConditionViaEvent( - element: HTMLElement, - condition?: FrigateCardCondition, -): boolean { - if (!condition) { - return true; - } - - const evaluateEvent = new ConditionEvaluateRequestEvent(condition, { - bubbles: true, - composed: true, - }); - - /* Special note on what's going on here: - * - * Some parts of the card (e.g. ) may have arbitrary - * complexity and layers (that this card doesn't control) between that master - * element and the element that needs to evaluate the condition. In these - * cases there's no clean way to pass state from the rest of card down through - * these layers. Instead, an event is dispatched as a "request for evaluation" - * (ConditionEvaluateRequestEvent) upwards which is caught by the outer card - * and the evaluation result is added to the event object. Because event - * propagation is handled synchronously, the result will be added to the event - * before the flow proceeds. - */ - element.dispatchEvent(evaluateEvent); - return evaluateEvent.evaluation ?? false; -} - -type RawOverrides = { - conditions: FrigateCardCondition; - overrides: RawFrigateCardConfig; -}[]; - -export function getOverriddenConfig( - manager: Readonly, - config: Readonly, - configOverrides?: Readonly, - stateOverrides?: Partial, -): RawFrigateCardConfig { - const output = copyConfig(config); - let overridden = false; - if (configOverrides) { - for (const override of configOverrides) { - if (manager.evaluateCondition(override.conditions, stateOverrides)) { - merge(output, override.overrides); - 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, - })) ?? [] - ); -} - -// A tiny wrapper interface to allow the same manager to be passed around -// immutably within objects that will not be equal (===). Every state change -// generates a new epoch. This is used for Lit rendering to ensure changes to -// condition state are recognized as changes even though the manager is the -// same. -export interface ConditionsManagerEpoch { - manager: Readonly; -} - -export type ConditionsManagerListener = () => void; - -export class ConditionsManager { - protected _api: CardConditionAPI; - - protected _state: ConditionState = {}; - protected _epoch: ConditionsManagerEpoch = this._createEpoch(); - protected _listeners: ConditionsManagerListener[]; - - // Whether or not to include HA state in ConditionState. Doing so increases - // CPU usage as HA state is pumped out very fast, so this is only enabled if - // the configuration needs to consume it. - protected _hasHAStateConditions = false; - protected _mediaQueries: MediaQueryList[] = []; - protected _mediaQueryTrigger = () => this._triggerChange(); - - constructor(api: CardConditionAPI, listener?: ConditionsManagerListener) { - this._api = api; - this._listeners = [ - () => this._api.getConfigManager().computeOverrideConfig(), - () => this._api.getAutomationsManager().execute(), - ...(listener ? [listener] : []) - ]; - } - - public removeConditions(): void { - this._mediaQueries.forEach((mql) => - mql.removeEventListener('change', this._mediaQueryTrigger), - ); - this._mediaQueries = []; - } - - public setConditionsFromConfig(): void { - this.removeConditions(); - - const getAllConditions = (): FrigateCardCondition[] => { - const config = this._api.getConfigManager().getConfig(); - const conditions: FrigateCardCondition[] = []; - config?.overrides?.forEach((override) => conditions.push(override.conditions)); - - // Element conditions can be arbitrarily nested underneath conditionals and - // custom elements that this card may not known. Here we recursively parse - // down the elements tree, parsing as we go to find valid conditions. - const getElementsConditions = (data: unknown): void => { - const parseResult = frigateConditionalSchema.safeParse(data); - if (parseResult.success) { - conditions.push(parseResult.data.conditions); - parseResult.data.elements?.forEach(getElementsConditions); - } else if (data && typeof data === 'object') { - Object.keys(data).forEach((key) => getElementsConditions(data[key])); - } - }; - config?.elements?.forEach(getElementsConditions); - return conditions; - }; - - const conditions = getAllConditions(); - this._hasHAStateConditions = conditions.some( - (condition) => !!condition.state?.length, - ); - conditions.forEach((condition) => { - if (condition.media_query) { - const mql = window.matchMedia(condition.media_query); - mql.addEventListener('change', this._mediaQueryTrigger); - this._mediaQueries.push(mql); - } - }); - } - - public setState(state: Partial): void { - this._state = { - ...this._state, - ...state, - }; - this._triggerChange(); - } - - public hasHAStateConditions(): boolean { - return this._hasHAStateConditions; - } - - public getEpoch(): ConditionsManagerEpoch { - return this._epoch; - } - - public evaluateCondition( - condition: Readonly, - stateOverrides?: Partial, - ): boolean { - const state = { - ...this._state, - ...stateOverrides, - }; - - let result = true; - if (condition.view?.length) { - result &&= !!state?.view && condition.view.includes(state.view); - } - if (condition.fullscreen !== undefined) { - result &&= - state.fullscreen !== undefined && condition.fullscreen === state.fullscreen; - } - if (condition.expand !== undefined) { - result &&= state.expand !== undefined && condition.expand === state.expand; - } - if (condition.camera?.length) { - result &&= !!state.camera && condition.camera.includes(state.camera); - } - if (condition.state?.length) { - for (const stateTest of condition.state) { - result &&= - !!state.state && - ((!stateTest.state && !stateTest.state_not) || - (stateTest.entity in state.state && - (!stateTest.state || - state.state[stateTest.entity].state === stateTest.state) && - (!stateTest.state_not || - state.state[stateTest.entity].state !== stateTest.state_not))); - } - } - if (condition.media_loaded !== undefined) { - result &&= - state.media_loaded !== undefined && - condition.media_loaded === state.media_loaded; - } - if (condition.media_query) { - result &&= window.matchMedia(condition.media_query).matches; - } - if (condition.display_mode) { - result &&= !!state.displayMode && condition.display_mode === state.displayMode; - } - return result; - } - - protected _createEpoch(): ConditionsManagerEpoch { - return { manager: this }; - } - - protected _triggerChange(): void { - this._epoch = this._createEpoch(); - this._listeners.forEach((listener) => listener()); - } -} diff --git a/src/utils/card-controller/config-manager.ts b/src/utils/card-controller/config-manager.ts deleted file mode 100644 index 70af9921..00000000 --- a/src/utils/card-controller/config-manager.ts +++ /dev/null @@ -1,147 +0,0 @@ -import isEqual from 'lodash-es/isEqual'; -import { isConfigUpgradeable } from '../../config-mgmt'; -import { - CardWideConfig, - FrigateCardConfig, - frigateCardConfigSchema, - RawFrigateCardConfig -} from '../../config/types'; -import { localize } from '../../localize/localize'; -import { setLowPerformanceProfile } from '../../performance.js'; -import { getParseErrorPaths } from '../zod.js'; -import { getOverriddenConfig } from './conditions-manager'; -import { InitializationAspect } from './initialization-manager'; -import { CardConfigAPI } from './types'; - -export class ConfigManager { - protected _api: CardConfigAPI; - - // The main base configuration object. For most usecases use getConfig() to - // get the correct configuration (which will return overrides as appropriate). - // This variable must be called `_config` or `config` to be compatible with - // card-mod. - protected _config: FrigateCardConfig | null = null; - protected _overriddenConfig: FrigateCardConfig | null = null; - protected _rawConfig: RawFrigateCardConfig | null = null; - protected _cardWideConfig: CardWideConfig | null = null; - - constructor(api) { - this._api = api; - } - - public hasConfig(): boolean { - return !!this.getConfig(); - } - - public getConfig(): FrigateCardConfig | null { - return this._overriddenConfig ?? this._config; - } - - public getCardWideConfig(): CardWideConfig | null { - return this._cardWideConfig; - } - - public getNonOverriddenConfig(): FrigateCardConfig | null { - return this._config; - } - - public getRawConfig(): RawFrigateCardConfig | null { - return this._rawConfig; - } - - public setConfig(inputConfig?: RawFrigateCardConfig): void { - if (!inputConfig) { - throw new Error(localize('error.invalid_configuration')); - } - - const parseResult = frigateCardConfigSchema.safeParse(inputConfig); - if (!parseResult.success) { - const configUpgradeable = isConfigUpgradeable(inputConfig); - const hint = getParseErrorPaths(parseResult.error); - let upgradeMessage = ''; - if (configUpgradeable) { - upgradeMessage = `${localize('error.upgrade_available')}. `; - } - throw new Error( - upgradeMessage + - `${localize('error.invalid_configuration')}: ` + - (hint && hint.size - ? JSON.stringify([...hint], null, ' ') - : localize('error.invalid_configuration_no_hint')), - ); - } - const config = - parseResult.data.performance.profile !== 'low' - ? parseResult.data - : setLowPerformanceProfile(inputConfig, parseResult.data); - - this._rawConfig = inputConfig; - if (isEqual(this._config, config)) { - return; - } - - this._config = config; - this._cardWideConfig = { - performance: config.performance, - debug: config.debug, - }; - - this._api.getConditionsManager().setConditionsFromConfig(); - this._api.getConditionsManager().setState({ - view: undefined, - displayMode: undefined, - camera: undefined, - }); - this._api.getMediaLoadedInfoManager().clear(); - this._api.getViewManager().reset(); - this._api.getMessageManager().reset(); - this._api.getAutomationsManager().setAutomationsFromConfig(); - this._api.getStyleManager().setPerformance(); - this._api.getCardElementManager().update(); - - this.computeOverrideConfig(); - } - - public computeOverrideConfig(): void { - const conditionsManager = this._api.getConditionsManager(); - if (!this._config) { - return; - } - const overriddenConfig = getOverriddenConfig( - conditionsManager, - this._config, - this._config.overrides, - ) as FrigateCardConfig; - - // Save on Lit re-rendering costs by only updating the configuration if it - // actually changes. - if (isEqual(overriddenConfig, this._overriddenConfig)) { - return; - } - - const previousConfig = this._overriddenConfig; - this._overriddenConfig = overriddenConfig; - - this._api.getStyleManager().setMinMaxHeight(); - - if ( - previousConfig && - (!isEqual(previousConfig?.cameras, this._overriddenConfig?.cameras) || - !isEqual(previousConfig?.cameras_global, this._overriddenConfig?.cameras_global)) - ) { - this._api.getInitializationManager().uninitialize(InitializationAspect.CAMERAS); - } - - if ( - previousConfig && - previousConfig?.live.microphone.always_connected !== - this._overriddenConfig?.live.microphone.always_connected - ) { - this._api - .getInitializationManager() - .uninitialize(InitializationAspect.MICROPHONE_CONNECT); - } - - this._api.getCardElementManager().update(); - } -} diff --git a/src/utils/card-controller/controller.ts b/src/utils/card-controller/controller.ts deleted file mode 100644 index 0b684fd3..00000000 --- a/src/utils/card-controller/controller.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { LovelaceCardEditor } from 'custom-card-helpers'; -import { ReactiveController } from 'lit'; -import { CameraManager } from '../../camera-manager/manager'; -import { FrigateCardConfig } from '../../config/types'; -import { EntityRegistryManager } from '../ha/entity-registry'; -import { EntityCache } from '../ha/entity-registry/cache'; -import { ResolvedMediaCache } from '../ha/resolved-media'; -import { ActionsManager } from './actions-manager'; -import { AutoUpdateManager } from './auto-update-manager'; -import { AutomationsManager } from './automations-manager'; -import { CameraURLManager } from './camera-url-manager'; -import { - CardElementManager, - CardHTMLElement, - MenuToggleCallback, - ScrollCallback, -} from './card-element-manager'; -import { ConditionsManager, ConditionsManagerListener } from './conditions-manager'; -import { ConfigManager } from './config-manager'; -import { DownloadManager } from './download-manager'; -import { ExpandManager } from './expand-manager'; -import { FullscreenManager } from './fullscreen-manager'; -import { HASSManager } from './hass-manager'; -import { InitializationManager } from './initialization-manager'; -import { InteractionManager } from './interaction-manager'; -import { MediaLoadedInfoManager } from './media-info-manager'; -import { MediaPlayerManager } from './media-player-manager'; -import { MessageManager } from './message-manager'; -import { MicrophoneManager } from './microphone-manager'; -import { QueryStringManager } from './query-string-manager'; -import { StyleManager } from './style-manager'; -import { TriggersManager } from './triggers-manager'; -import { - CardActionsManagerAPI, - CardAutoRefreshAPI, - CardAutomationsAPI, - CardCameraAPI, - CardCameraURLAPI, - CardConditionAPI, - CardConfigAPI, - CardDownloadAPI, - CardElementAPI, - CardExpandAPI, - CardFullscreenAPI, - CardHASSAPI, - CardInitializerAPI, - CardInteractionAPI, - CardMediaLoadedAPI, - CardMediaPlayerAPI, - CardMessageAPI, - CardMicrophoneAPI, - CardQueryStringAPI, - CardStyleAPI, - CardTriggersAPI, - CardViewAPI, -} from './types'; -import { ViewManager } from './view-manager'; - -export class CardController - implements - CardActionsManagerAPI, - CardAutomationsAPI, - CardAutoRefreshAPI, - CardCameraAPI, - CardCameraURLAPI, - CardConditionAPI, - CardConfigAPI, - CardDownloadAPI, - CardElementAPI, - CardExpandAPI, - CardFullscreenAPI, - CardHASSAPI, - CardInitializerAPI, - CardInteractionAPI, - CardMediaLoadedAPI, - CardMediaPlayerAPI, - CardMessageAPI, - CardMicrophoneAPI, - CardQueryStringAPI, - CardStyleAPI, - CardTriggersAPI, - CardViewAPI, - ReactiveController -{ - // These properties may be used in the construction of 'managers' (and should - // be created first). - protected _entityRegistryManager = new EntityRegistryManager(new EntityCache()); - protected _resolvedMediaCache = new ResolvedMediaCache(); - - protected _actionsManager = new ActionsManager(this); - protected _automationsManager = new AutomationsManager(this); - protected _autoUpdateManager = new AutoUpdateManager(this); - protected _cameraManager = new CameraManager(this); - protected _cameraURLManager = new CameraURLManager(this); - protected _cardElementManager: CardElementManager; - protected _conditionsManager: ConditionsManager; - protected _configManager = new ConfigManager(this); - protected _downloadManager = new DownloadManager(this); - protected _expandManager = new ExpandManager(this); - protected _fullscreenManager = new FullscreenManager(this); - protected _hassManager = new HASSManager(this); - protected _initializationManager = new InitializationManager(this); - protected _interactionManager = new InteractionManager(this); - protected _mediaLoadedInfoManager = new MediaLoadedInfoManager(this); - protected _mediaPlayerManager = new MediaPlayerManager(this); - protected _messageManager = new MessageManager(this); - protected _microphoneManager = new MicrophoneManager(this); - protected _queryStringManager = new QueryStringManager(this); - protected _styleManager = new StyleManager(this); - protected _triggersManager = new TriggersManager(this); - protected _viewManager = new ViewManager(this); - - constructor( - host: CardHTMLElement, - scrollCallback: ScrollCallback, - menuToggleCallback: MenuToggleCallback, - conditionListener: ConditionsManagerListener, - ) { - host.addController(this); - - this._conditionsManager = new ConditionsManager(this, conditionListener); - this._cardElementManager = new CardElementManager( - this, - host, - scrollCallback, - menuToggleCallback, - ); - } - - // ************************************************************************* - // Accessors - // ************************************************************************* - - public getActionsManager(): ActionsManager { - return this._actionsManager; - } - - public getAutomationsManager(): AutomationsManager { - return this._automationsManager; - } - - public getAutoUpdateManager(): AutoUpdateManager { - return this._autoUpdateManager; - } - - public getCameraManager(): CameraManager { - return this._cameraManager; - } - - public getCameraURLManager(): CameraURLManager { - return this._cameraURLManager; - } - - public getCardElementManager(): CardElementManager { - return this._cardElementManager; - } - - public getConditionsManager(): ConditionsManager { - return this._conditionsManager; - } - - public static async getConfigElement(): Promise { - await import('../../editor.js'); - return document.createElement('frigate-card-editor'); - } - - public getConfigManager(): ConfigManager { - return this._configManager; - } - public getDownloadManager(): DownloadManager { - return this._downloadManager; - } - - public getEntityRegistryManager(): EntityRegistryManager { - return this._entityRegistryManager; - } - - public getExpandManager(): ExpandManager { - return this._expandManager; - } - - public getFullscreenManager(): FullscreenManager { - return this._fullscreenManager; - } - - public getHASSManager(): HASSManager { - return this._hassManager; - } - - public getInitializationManager(): InitializationManager { - return this._initializationManager; - } - - public getInteractionManager(): InteractionManager { - return this._interactionManager; - } - - public getMediaLoadedInfoManager(): MediaLoadedInfoManager { - return this._mediaLoadedInfoManager; - } - - public getMediaPlayerManager(): MediaPlayerManager { - return this._mediaPlayerManager; - } - - public getMessageManager(): MessageManager { - return this._messageManager; - } - - public getMicrophoneManager(): MicrophoneManager { - return this._microphoneManager; - } - - public getQueryStringManager(): QueryStringManager { - return this._queryStringManager; - } - - public getResolvedMediaCache(): ResolvedMediaCache { - return this._resolvedMediaCache; - } - - public static getStubConfig(entities: string[]): FrigateCardConfig { - const cameraEntity = entities.find((element) => element.startsWith('camera.')); - return { - cameras: [ - { - camera_entity: cameraEntity ?? 'camera.demo', - }, - ], - // Need to use 'as unknown' to convince Typescript that this really isn't a - // mistake, despite the miniscule size of the configuration vs the full type - // description. - } as unknown as FrigateCardConfig; - } - - public getStyleManager(): StyleManager { - return this._styleManager; - } - - public getTriggersManager(): TriggersManager { - return this._triggersManager; - } - - public getViewManager(): ViewManager { - return this._viewManager; - } - - // ************************************************************************* - // Handlers - // ************************************************************************* - - public hostConnected(): void { - this.getCardElementManager().elementConnected(); - } - - public hostDisconnected(): void { - this.getCardElementManager().elementDisconnected(); - } -} diff --git a/src/utils/card-controller/download-manager.ts b/src/utils/card-controller/download-manager.ts deleted file mode 100644 index adc929ba..00000000 --- a/src/utils/card-controller/download-manager.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { downloadMedia, downloadURL } from '../download'; -import { generateScreenshotTitle } from '../screenshot'; -import { CardDownloadAPI } from './types'; - -export class DownloadManager { - protected _api: CardDownloadAPI; - - constructor(api: CardDownloadAPI) { - this._api = api; - } - - public async downloadViewerMedia(): Promise { - const media = this._api - .getViewManager() - .getView() - ?.queryResults?.getSelectedResult(); - const hass = this._api.getHASSManager().getHASS(); - - if (!media || !hass) { - return false; - } - - try { - await downloadMedia(hass, this._api.getCameraManager(), media); - } catch (error: unknown) { - this._api.getMessageManager().setErrorIfHigherPriority(error); - return false; - } - return true; - } - - public async downloadScreenshot(): Promise { - const url = await this._api - .getMediaLoadedInfoManager() - .get() - ?.player?.getScreenshotURL(); - if (url) { - downloadURL(url, generateScreenshotTitle(this._api.getViewManager().getView())); - } - } -} diff --git a/src/utils/card-controller/expand-manager.ts b/src/utils/card-controller/expand-manager.ts deleted file mode 100644 index 9516a9f0..00000000 --- a/src/utils/card-controller/expand-manager.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { CardExpandAPI } from './types'; - -export class ExpandManager { - protected _expanded = false; - protected _api: CardExpandAPI; - - constructor(api: CardExpandAPI) { - this._api = api; - } - - public isExpanded(): boolean { - return this._expanded; - } - - public toggleExpanded(): void { - this.setExpanded(!this._expanded); - } - - public setExpanded(expanded: boolean): void { - if (expanded && this._api.getFullscreenManager().isInFullscreen()) { - // Fullscreen and expanded mode are mutually exclusive. - this._api.getFullscreenManager().stopFullscreen(); - } - - this._expanded = expanded; - this._api.getConditionsManager()?.setState({ - expand: expanded, - }); - this._api.getCardElementManager().update(); - } -} diff --git a/src/utils/card-controller/fullscreen-manager.ts b/src/utils/card-controller/fullscreen-manager.ts deleted file mode 100644 index 418bf72f..00000000 --- a/src/utils/card-controller/fullscreen-manager.ts +++ /dev/null @@ -1,47 +0,0 @@ -import screenfull from 'screenfull'; -import { CardFullscreenAPI } from './types'; - -export class FullscreenManager { - protected _api: CardFullscreenAPI; - - constructor(api: CardFullscreenAPI) { - this._api = api; - } - - public connect(): void { - if (screenfull.isEnabled) { - screenfull.on('change', this._fullscreenHandler); - } - } - - public disconnect(): void { - if (screenfull.isEnabled) { - screenfull.off('change', this._fullscreenHandler); - } - } - - public isInFullscreen(): boolean { - return screenfull.isEnabled && screenfull.isFullscreen; - } - - public toggleFullscreen(): void { - screenfull.toggle(this._api.getCardElementManager().getElement()); - } - - public stopFullscreen(): void { - screenfull.exit(); - } - - protected _fullscreenHandler = (): void => { - this._api.getExpandManager().setExpanded(false); - - this._api.getConditionsManager()?.setState({ - fullscreen: this.isInFullscreen(), - }); - - // Re-render after a change to fullscreen mode to take advantage of - // the expanded screen real-estate (vs staying in aspect-ratio locked - // modes). - this._api.getCardElementManager().update(); - }; -} diff --git a/src/utils/card-controller/hass-manager.ts b/src/utils/card-controller/hass-manager.ts deleted file mode 100644 index ebbb55bd..00000000 --- a/src/utils/card-controller/hass-manager.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { CameraConfig } from '../../config/types'; -import { localize } from '../../localize/localize'; -import { ExtendedHomeAssistant } from '../../types'; -import { hasHAConnectionStateChanged, isHassDifferent } from '../ha'; -import { CardHASSAPI } from './types'; - -export class HASSManager { - protected _hass: ExtendedHomeAssistant | null = null; - protected _api: CardHASSAPI; - - constructor(api: CardHASSAPI) { - this._api = api; - } - - public getHASS(): ExtendedHomeAssistant | null { - return this._hass; - } - - public setHASS(hass: ExtendedHomeAssistant): void { - const getSelectedCameraConfig = (): CameraConfig | null => { - const view = this._api.getViewManager().getView(); - const cameraManager = this._api.getCameraManager(); - - return view && cameraManager - ? cameraManager?.getStore().getCameraConfig(view.camera) ?? null - : null; - }; - - const oldHass = this._hass; - this._hass = hass; - - const selectedCamera = getSelectedCameraConfig(); - - if (hasHAConnectionStateChanged(oldHass, hass)) { - if (!this._hass?.connected) { - this._api.getMessageManager().setMessageIfHigherPriority({ - message: localize('error.reconnecting'), - icon: 'mdi:lan-disconnect', - type: 'connection', - dotdotdot: true, - }); - } else { - this._api.getViewManager().setViewDefault(); - } - } else if ( - // Home Assistant pumps a lot of updates through. Re-rendering the card is - // necessary at times (e.g. to update the 'clip' view as new clips - // arrive), but also is a jarring experience for the user (e.g. if they - // are browsing the mini-gallery). Do not allow re-rendering from a Home - // Assistant update if there's been recent interaction (e.g. clicks on the - // card) or if there is media active playing. - this._isAutomatedViewUpdateAllowed() && - isHassDifferent(this._hass, oldHass, [ - ...(this._api.getConfigManager().getConfig()?.view.update_entities ?? []), - ...(selectedCamera?.triggers.entities ?? []), - ]) - ) { - // If entities being monitored have changed then reset the view to the - // default. - this._api.getViewManager().setViewDefault(); - } else if ( - isHassDifferent(this._hass, oldHass, [ - ...(this._api.getConfigManager().getConfig()?.view.render_entities ?? []), - - // Refresh the card if media player state changes: - // https://github.com/dermotduffy/frigate-hass-card/issues/881 - ...this._api.getMediaPlayerManager().getMediaPlayers(), - ]) - ) { - this._api.getCardElementManager().update(); - } - - this._api.getTriggersManager().updateTriggeredCameras(oldHass); - - if (this._api.getConditionsManager().hasHAStateConditions()) { - this._api.getConditionsManager().setState({ state: this._hass.states }); - } - - // Dark mode may depend on HASS. - this._api.getStyleManager().setLightOrDarkMode(); - } - - protected _isAutomatedViewUpdateAllowed(): boolean { - return ( - this._api.getConfigManager().getConfig()?.view.update_force || - !this._api.getInteractionManager().hasInteraction() - ); - } -} diff --git a/src/utils/card-controller/initialization-manager.ts b/src/utils/card-controller/initialization-manager.ts deleted file mode 100644 index cb553a79..00000000 --- a/src/utils/card-controller/initialization-manager.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { loadLanguages } from '../../localize/localize'; -import { sideLoadHomeAssistantElements } from '../ha'; -import { Initializer } from '../initializer/initializer'; -import { CardInitializerAPI } from './types'; - -export enum InitializationAspect { - LANGUAGES = 'languages', - SIDE_LOAD_ELEMENTS = 'side-load-elements', - MEDIA_PLAYERS = 'media-players', - CAMERAS = 'cameras', - MICROPHONE_CONNECT = 'microphone-connect', -} - -export class InitializationManager { - protected _api: CardInitializerAPI; - protected _initializer; - - constructor(api: CardInitializerAPI, initializer?: Initializer) { - this._api = api; - this._initializer = initializer ?? new Initializer(); - } - - public isInitializedMandatory(): boolean { - return this._initializer.isInitializedMultiple([ - InitializationAspect.LANGUAGES, - InitializationAspect.SIDE_LOAD_ELEMENTS, - InitializationAspect.CAMERAS, - ]); - } - - /** - * Initialize the hard requirements for rendering anything. - * @returns `true` if card rendering can continue. - */ - public async initializeMandatory(): Promise { - const hass = this._api.getHASSManager().getHASS(); - if (!hass) { - return false; - } - - if ( - !(await this._initializer.initializeMultipleIfNecessary({ - // Caution: Ensure nothing in this set of initializers requires - // config or languages since they will not yet have been initialized. - [InitializationAspect.LANGUAGES]: async () => await loadLanguages(hass), - [InitializationAspect.SIDE_LOAD_ELEMENTS]: async () => - await sideLoadHomeAssistantElements(), - })) - ) { - return false; - } - - if (!this._api.getConfigManager().hasConfig()) { - return false; - } - - if ( - !(await this._initializer.initializeIfNecessary( - InitializationAspect.CAMERAS, - async () => await this._api.getCameraManager().initializeCamerasFromConfig(), - )) - ) { - return false; - } - - if (!this._api.getMessageManager().hasMessage()) { - // Set a view on initial load. However, if the query string contains a - // view related action, we don't set any view here and allow that content - // to be triggered by the firstUpdated() call that runs query string - // actions. To do otherwise may cause a race condition between the default - // view and the querystring view, see: - // https://github.com/dermotduffy/frigate-hass-card/issues/1200 - const hasViewRelatedActions = this._api - .getQueryStringManager() - .hasViewRelatedActions(); - if (hasViewRelatedActions) { - this._api.getQueryStringManager().executeViewRelated(); - } else { - this._api.getViewManager().setViewDefault(); - } - } - - this._api.getCardElementManager().update(); - return true; - } - - /** - * Initialize aspects of the card that can load in the 'background'. - * @returns `true` if card rendering can continue. - */ - public async initializeBackgroundIfNecessary(): Promise { - const hass = this._api.getHASSManager().getHASS(); - const config = this._api.getConfigManager().getConfig(); - - if (!hass || !config) { - return false; - } - - if ( - this._initializer.isInitializedMultiple([ - ...(config.menu.buttons.media_player.enabled - ? [InitializationAspect.MEDIA_PLAYERS] - : []), - ...(config.live.microphone.always_connected - ? [InitializationAspect.MICROPHONE_CONNECT] - : []), - ]) - ) { - return true; - } - - if ( - !(await this._initializer.initializeMultipleIfNecessary({ - ...(config.menu.buttons.media_player.enabled && { - [InitializationAspect.MEDIA_PLAYERS]: async () => - await this._api.getMediaPlayerManager().initialize(), - }), - ...(config.live.microphone.always_connected && { - [InitializationAspect.MICROPHONE_CONNECT]: async () => - await this._api.getMicrophoneManager().connect(), - }), - })) - ) { - return false; - } - - this._api.getCardElementManager().update(); - return true; - } - - public uninitialize(aspect: InitializationAspect) { - return this._initializer.uninitialize(aspect); - } -} diff --git a/src/utils/card-controller/interaction-manager.ts b/src/utils/card-controller/interaction-manager.ts deleted file mode 100644 index 290f85be..00000000 --- a/src/utils/card-controller/interaction-manager.ts +++ /dev/null @@ -1,50 +0,0 @@ -import throttle from 'lodash-es/throttle'; -import { Timer } from '../timer'; -import { CardInteractionAPI } from './types'; - -export class InteractionManager { - protected _timer = new Timer(); - protected _api: CardInteractionAPI; - - constructor(api: CardInteractionAPI) { - this._api = api; - } - - // The mouse handler may be called continually, throttle it to at most once - // per second for performance reasons. - public reportInteraction = throttle(() => { - this._reportInteraction(); - }, 1 * 1000); - - public hasInteraction(): boolean { - return this._timer.isRunning(); - } - - /** - * Start the user interaction ('screensaver') timer to reset the view to - * default `view.timeout_seconds` after user interaction. - */ - protected _reportInteraction(): void { - this._timer.stop(); - - // Interactions reset the trigger state. - this._api.getTriggersManager().untrigger(); - - const timeoutSeconds = this._api.getConfigManager().getConfig() - ?.view.timeout_seconds; - - if (timeoutSeconds) { - this._timer.start(timeoutSeconds, () => { - if (this._isAutomatedUpdateAllowed()) { - this._api.getViewManager().setViewDefault(); - this._api.getStyleManager().setLightOrDarkMode(); - } - }); - } - this._api.getStyleManager().setLightOrDarkMode(); - } - - protected _isAutomatedUpdateAllowed(): boolean { - return !this._api.getTriggersManager().isTriggered(); - } -} diff --git a/src/utils/card-controller/media-info-manager.ts b/src/utils/card-controller/media-info-manager.ts deleted file mode 100644 index ec00c1a9..00000000 --- a/src/utils/card-controller/media-info-manager.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { MediaLoadedInfo } from '../../types'; -import { log } from '../debug'; -import { isValidMediaLoadedInfo } from '../media-info'; -import { CardMediaLoadedAPI } from './types'; - -export class MediaLoadedInfoManager { - protected _api: CardMediaLoadedAPI; - protected _current: MediaLoadedInfo | null = null; - protected _lastKnown: MediaLoadedInfo | null = null; - - constructor(api: CardMediaLoadedAPI) { - this._api = api; - } - - public set(mediaInfo: MediaLoadedInfo): void { - if (!isValidMediaLoadedInfo(mediaInfo)) { - return; - } - - log( - this._api.getConfigManager().getCardWideConfig(), - `Frigate Card media load: `, - mediaInfo, - ); - - this._current = mediaInfo; - this._lastKnown = mediaInfo; - - this._api.getConditionsManager().setState({ media_loaded: true }); - - // Fresh media information may change how the card is rendered. - this._api.getStyleManager().setExpandedMode(); - this._api.getCardElementManager().update(); - } - - public get(): MediaLoadedInfo | null { - return this._current; - } - - public getLastKnown(): MediaLoadedInfo | null { - return this._lastKnown; - } - - public clear(): void { - this._current = null; - this._api.getConditionsManager().setState({ media_loaded: false }); - } - - public has(): boolean { - return !!this._current; - } -} diff --git a/src/utils/card-controller/media-player-manager.ts b/src/utils/card-controller/media-player-manager.ts deleted file mode 100644 index 58b2099a..00000000 --- a/src/utils/card-controller/media-player-manager.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA } from '../../const'; -import { ViewMedia } from '../../view/media'; -import { ViewMediaClassifier } from '../../view/media-classifier'; -import { errorToConsole } from '../basic'; -import { Entity } from '../ha/entity-registry/types'; -import { supportsFeature } from '../ha/update'; -import { CardMediaPlayerAPI } from './types'; - -export class MediaPlayerManager { - protected _mediaPlayers: string[] = []; - - protected _api: CardMediaPlayerAPI; - - constructor(api: CardMediaPlayerAPI) { - this._api = api; - } - - public getMediaPlayers(): string[] { - return this._mediaPlayers; - } - - public hasMediaPlayers(): boolean { - return this._mediaPlayers.length > 0; - } - - public async initialize(): Promise { - const hass = this._api.getHASSManager().getHASS(); - if (!hass) { - return; - } - - const isValidMediaPlayer = (entityID: string): boolean => { - if (entityID.startsWith('media_player.')) { - const stateObj = hass.states[entityID]; - if ( - stateObj && - stateObj.state !== 'unavailable' && - supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA) - ) { - return true; - } - } - return false; - }; - - const mediaPlayers = Object.keys(hass.states).filter(isValidMediaPlayer); - let mediaPlayerEntities: Map | null = null; - try { - mediaPlayerEntities = await this._api - .getEntityRegistryManager() - .getEntities(hass, mediaPlayers); - } catch (e) { - // Failing to fetch media player information is not considered - // sufficiently serious to block card startup -- it is just logged and we - // move on. - errorToConsole(e as Error); - } - - // Filter out entities that are marked as hidden (this information is not - // available in the HA state, only in the registry). - this._mediaPlayers = mediaPlayers.filter((entityID) => { - // Specifically allow for media players that are not found in the entity registry: - // See: https://github.com/dermotduffy/frigate-hass-card/issues/1016 - const entity = mediaPlayerEntities?.get(entityID); - return !entity || !entity.hidden_by; - }); - } - - public async stop(mediaPlayer: string): Promise { - await this._api - .getHASSManager() - .getHASS() - ?.callService('media_player', 'media_stop', { - entity_id: mediaPlayer, - }); - } - - public async playLive(mediaPlayer: string, cameraID: string): Promise { - const hass = this._api.getHASSManager().getHASS(); - const cameraConfig = this._api - .getCameraManager() - .getStore() - .getCameraConfig(cameraID); - const cameraEntity = cameraConfig?.camera_entity ?? null; - - if (!hass || !cameraEntity) { - return; - } - - const title = - this._api.getCameraManager().getCameraMetadata(cameraID)?.title ?? null; - const thumbnail = hass.states[cameraEntity]?.attributes?.entity_picture ?? null; - - await hass.callService('media_player', 'play_media', { - entity_id: mediaPlayer, - media_content_id: `media-source://camera/${cameraEntity}`, - media_content_type: 'application/vnd.apple.mpegurl', - extra: { - ...(title && { title: title }), - ...(thumbnail && { thumb: thumbnail }), - }, - }); - } - - public async playMedia(mediaPlayer: string, media?: ViewMedia | null): Promise { - const hass = this._api.getHASSManager().getHASS(); - - if (!hass || !media) { - return; - } - - const title = media.getTitle(); - const thumbnail = media.getThumbnail(); - - await hass.callService('media_player', 'play_media', { - entity_id: mediaPlayer, - media_content_id: media.getContentID(), - media_content_type: ViewMediaClassifier.isVideo(media) ? 'video' : 'image', - extra: { - ...(title && { title: title }), - ...(thumbnail && { thumb: thumbnail }), - }, - }); - } -} diff --git a/src/utils/card-controller/message-manager.ts b/src/utils/card-controller/message-manager.ts deleted file mode 100644 index 1c52a257..00000000 --- a/src/utils/card-controller/message-manager.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { FrigateCardError, MESSAGE_TYPE_PRIORITIES, Message } from '../../types'; -import { errorToConsole } from '../basic'; -import { CardMessageAPI } from './types'; - -export class MessageManager { - protected _message: Message | null = null; - protected _api: CardMessageAPI; - - constructor(api: CardMessageAPI) { - this._api = api; - } - - public getMessage(): Message | null { - return this._message; - } - - public hasMessage(): boolean { - return !!this._message; - } - - public hasErrorMessage(): boolean { - return this._message?.type === 'error'; - } - - public reset(): void { - const hadMessage = this.hasMessage(); - this._message = null; - - if (hadMessage) { - this._api.getCardElementManager().update(); - } - } - - public setErrorIfHigherPriority(error: unknown): void { - // This object should accept unknown objects to be able to seamlessly - // process arguments to catch() which can only be unknown/any. - if (!(error instanceof Error)) { - return; - } - - errorToConsole(error); - this.setMessageIfHigherPriority({ - message: error.message, - type: 'error', - ...(error instanceof FrigateCardError && { context: error.context }), - }); - } - - public setMessageIfHigherPriority(message: Message): boolean { - const currentPriority = this._message - ? MESSAGE_TYPE_PRIORITIES[this._message.type] - : 0; - const newPriority = MESSAGE_TYPE_PRIORITIES[message.type]; - - if (this._message && newPriority < currentPriority) { - return false; - } - - this._message = message; - - // When a message is displayed it effectively unloads the media. - this._api.getMediaLoadedInfoManager().clear(); - this._api.getCardElementManager().scrollReset(); - this._api.getCardElementManager().update(); - return true; - } -} diff --git a/src/utils/card-controller/microphone-manager.ts b/src/utils/card-controller/microphone-manager.ts deleted file mode 100644 index 85a7917c..00000000 --- a/src/utils/card-controller/microphone-manager.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { errorToConsole } from '../basic'; -import { Timer } from '../timer'; -import { CardMicrophoneAPI } from './types'; - -export class MicrophoneManager { - protected _api: CardMicrophoneAPI; - protected _stream?: MediaStream | null; - protected _timer = new Timer(); - - // We keep mute state separate from the stream state so that mute/unmute can - // be expressed before the stream is created -- and when it's create it will - // have the right mute status. - protected _mute = true; - - constructor(api: CardMicrophoneAPI) { - this._api = api; - } - - public async connect(): Promise { - try { - this._stream = await navigator.mediaDevices.getUserMedia({ - audio: true, - video: false, - }); - } catch (e: unknown) { - errorToConsole(e as Error); - this._stream = null; - } - this._setMute(); - } - - public async disconnect(): Promise { - this._stream?.getTracks().forEach((track) => track.stop()); - this._stream = undefined; - - this._api.getCardElementManager().update(); - } - - public getStream(): MediaStream | undefined { - return this._stream ?? undefined; - } - - protected _setMute(): void { - this._stream?.getTracks().forEach((track) => { - track.enabled = !this._mute; - }); - this._startTimer(); - - this._api.getCardElementManager().update(); - } - - public mute(): void { - this._mute = true; - this._setMute(); - } - - public async unmute(): Promise { - const unmute = (): void => { - this._mute = false; - this._setMute(); - }; - - if (!this.isConnected() && !this.isForbidden()) { - // The connect() call is async and make take an arbitrary amount of - // time for the user to grant access to their microphone. With a - // momentary microphone button the mute call (on mouse release) may - // arrive before the connection is even granted, so we unmute first - // before the connection is made, so the mute call on release will not - // be 'overwritten' incorrectly. - unmute(); - await this.connect(); - } else if (this.isConnected()) { - unmute(); - } - } - - public isConnected(): boolean { - return !!this._stream; - } - - public isForbidden(): boolean { - return this._stream === null; - } - - public isMuted(): boolean { - // For safety, this function always returns the stream mute status directly - // (rather the internal state). - return !this._stream || this._stream.getTracks().every((track) => !track.enabled); - } - - protected _startTimer(): void { - const microphoneConfig = this._api.getConfigManager().getConfig() - ?.live.microphone; - - if (microphoneConfig?.always_connected) { - return; - } - - const disconnectSeconds = microphoneConfig?.disconnect_seconds ?? 0; - - if (disconnectSeconds) { - this._timer.start(disconnectSeconds, () => { - this.disconnect(); - }); - } - } -} diff --git a/src/utils/card-controller/query-string-manager.ts b/src/utils/card-controller/query-string-manager.ts deleted file mode 100644 index 81420f88..00000000 --- a/src/utils/card-controller/query-string-manager.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { FrigateCardCustomAction, FrigateCardViewAction } from '../../config/types'; -import { createFrigateCardCustomAction } from '../action.js'; -import { CardQueryStringAPI } from './types'; -import { ViewManagerSetViewParameters } from './view-manager'; - -interface QueryStringViewIntent { - view?: ViewManagerSetViewParameters & { - default?: boolean; - }; - other?: FrigateCardCustomAction[]; -} - -export class QueryStringManager { - protected _api: CardQueryStringAPI; - - constructor(api: CardQueryStringAPI) { - this._api = api; - } - - public hasViewRelatedActions(): boolean { - return !!this._calculateIntent().view; - } - - public executeNonViewRelated(): void { - this._executeNonViewRelated(this._calculateIntent()); - } - - public executeViewRelated(): void { - this._executeViewRelated(this._calculateIntent()); - } - - public executeAll(): void { - const intent = this._calculateIntent(); - this._executeViewRelated(intent); - this._executeNonViewRelated(intent); - } - - protected _executeViewRelated(intent: QueryStringViewIntent): void { - if (intent.view) { - if (intent.view.default) { - this._api.getViewManager().setViewDefault({ - ...(intent.view.cameraID && { cameraID: intent.view.cameraID }), - ...(intent.view.substream && { substream: intent.view.substream }), - }); - } else { - this._api.getViewManager().setViewByParameters({ - ...(intent.view.viewName && { viewName: intent.view.viewName }), - ...(intent.view.cameraID && { cameraID: intent.view.cameraID }), - ...(intent.view.substream && { substream: intent.view.substream }), - }); - } - } - } - - protected _executeNonViewRelated(intent: QueryStringViewIntent): void { - // Only execute non-view actions when the card has rendered at least once. - if (!this._api.getCardElementManager().hasUpdated()) { - return; - } - - intent.other?.forEach((action) => - this._api.getActionsManager().executeAction(action), - ); - } - - protected _calculateIntent(): QueryStringViewIntent { - const result: QueryStringViewIntent = {}; - for (const action of this._getActions()) { - if (this._isViewAction(action)) { - (result.view ??= {}).viewName = action.frigate_card_action; - (result.view ??= {}).default = undefined; - } else if (action.frigate_card_action === 'default') { - (result.view ??= {}).default = true; - (result.view ??= {}).viewName = undefined; - } else if (action.frigate_card_action === 'camera_select') { - (result.view ??= {}).cameraID = action.camera; - } else if (action.frigate_card_action === 'live_substream_select') { - (result.view ??= {}).substream = action.camera; - } else { - (result.other ??= []).push(action); - } - } - return result; - } - - protected _getActions(): FrigateCardCustomAction[] { - const params = new URLSearchParams(window.location.search); - const actions: FrigateCardCustomAction[] = []; - const actionRE = new RegExp( - /^frigate-card-action([.:](?\w+))?[.:](?\w+)/, - ); - for (const [key, value] of params.entries()) { - const match = key.match(actionRE); - if (!match || !match.groups) { - continue; - } - const cardID: string | undefined = match.groups['cardID']; - const action = match.groups['action']; - - let customAction: FrigateCardCustomAction | null = null; - switch (action) { - case 'camera_select': - case 'live_substream_select': - if (value) { - customAction = createFrigateCardCustomAction(action, { - camera: value, - cardID: cardID, - }); - } - break; - case 'camera_ui': - case 'clip': - case 'clips': - case 'default': - case 'diagnostics': - case 'download': - case 'expand': - case 'image': - case 'live': - case 'menu_toggle': - case 'recording': - case 'recordings': - case 'snapshot': - case 'snapshots': - case 'timeline': - customAction = createFrigateCardCustomAction(action, { - cardID: cardID, - }); - break; - default: - console.warn( - `Frigate card received unknown card action in query string: ${action}`, - ); - } - if (customAction) { - actions.push(customAction); - } - } - return actions; - } - - protected _isViewAction = ( - action: FrigateCardCustomAction, - ): action is FrigateCardViewAction => { - switch (action.frigate_card_action) { - case 'clip': - case 'clips': - case 'diagnostics': - case 'image': - case 'live': - case 'recording': - case 'recordings': - case 'snapshot': - case 'snapshots': - case 'timeline': - return true; - } - return false; - }; -} diff --git a/src/utils/card-controller/style-manager.ts b/src/utils/card-controller/style-manager.ts deleted file mode 100644 index 3376288e..00000000 --- a/src/utils/card-controller/style-manager.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { FrigateCardConfig } from '../../config/types'; -import { setPerformanceCSSStyles } from '../../performance'; -import { View } from '../../view/view'; -import { setOrRemoveAttribute } from '../basic'; -import { CardStyleAPI } from './types'; - -export class StyleManager { - protected _api: CardStyleAPI; - - constructor(api: CardStyleAPI) { - this._api = api; - } - - public setLightOrDarkMode(): void { - const config = this._api.getConfigManager().getConfig(); - const isDarkMode = - config?.view.dark_mode === 'on' || - (config?.view.dark_mode === 'auto' && - (!this._api.getInteractionManager().hasInteraction() || - !!this._api.getHASSManager().getHASS()?.themes.darkMode)); - - setOrRemoveAttribute( - this._api.getCardElementManager().getElement(), - isDarkMode, - 'dark', - ); - } - - public setExpandedMode(): void { - const card = this._api.getCardElementManager().getElement(); - const view = this._api.getViewManager().getView(); - - // When a new media loads, set the aspect ratio for when the card is - // expanded/popped-up. This is based exclusively on last media content, - // as dimension configuration does not apply in fullscreen or expanded mode. - const lastKnown = this._api.getMediaLoadedInfoManager().getLastKnown(); - card.style.setProperty( - '--frigate-card-expand-aspect-ratio', - view?.isAnyMediaView() && lastKnown - ? `${lastKnown.width} / ${lastKnown.height}` - : 'unset', - ); - // Non-media may have no intrinsic dimensions (or multiple media items in a - // grid) and so we need to explicit request the dialog to use all available - // space. - const isGrid = view?.isGrid(); - card.style.setProperty( - '--frigate-card-expand-width', - !isGrid && view?.isAnyMediaView() - ? 'none' - : 'var(--frigate-card-expand-max-width)', - ); - card.style.setProperty( - '--frigate-card-expand-height', - !isGrid && view?.isAnyMediaView() - ? 'none' - : 'var(--frigate-card-expand-max-height)', - ); - } - - public setMinMaxHeight(): void { - const config = this._api.getConfigManager().getConfig(); - if (config) { - const card = this._api.getCardElementManager().getElement(); - card.style.setProperty('--frigate-card-min-height', config.dimensions.min_height); - card.style.setProperty('--frigate-card-max-height', config.dimensions.max_height); - } - } - - public setPerformance(): void { - setPerformanceCSSStyles( - this._api.getCardElementManager().getElement(), - this._api.getConfigManager().getCardWideConfig()?.performance, - ); - } - - protected _isAspectRatioEnforced( - config: FrigateCardConfig, - view?: View | null, - ): boolean { - const aspectRatioMode = config.dimensions.aspect_ratio_mode; - - // Do not artifically constrain aspect ratio if: - // - It's fullscreen. - // - It's in expanded mode. - // - Aspect ratio enforcement is disabled. - // - Aspect ratio enforcement is dynamic and it's a media view (i.e. not the - // gallery) or diagnostics / timeline. - return !( - this._api.getFullscreenManager().isInFullscreen() || - this._api.getExpandManager().isExpanded() || - aspectRatioMode === 'unconstrained' || - (aspectRatioMode === 'dynamic' && - (!view || - view?.isAnyMediaView() || - view?.is('timeline') || - view?.is('diagnostics'))) - ); - } - - /** - * Get the aspect ratio padding required to enforce the aspect ratio (if it is - * required). - * @returns A padding percentage. - */ - public getAspectRatioStyle(): string { - const config = this._api.getConfigManager().getConfig(); - const view = this._api.getViewManager().getView(); - - if (config) { - if (!this._isAspectRatioEnforced(config, view)) { - return 'auto'; - } - - const aspectRatioMode = config.dimensions.aspect_ratio_mode; - - const lastKnown = this._api.getMediaLoadedInfoManager().getLastKnown(); - if (lastKnown && aspectRatioMode === 'dynamic') { - return `${lastKnown.width} / ${lastKnown.height}`; - } - - return `${config.dimensions.aspect_ratio[0]} / ${config.dimensions.aspect_ratio[1]}`; - } - return '16 / 9'; - } -} diff --git a/src/utils/card-controller/triggers-manager.ts b/src/utils/card-controller/triggers-manager.ts deleted file mode 100644 index af938adb..00000000 --- a/src/utils/card-controller/triggers-manager.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { HomeAssistant } from 'custom-card-helpers'; -import orderBy from 'lodash-es/orderBy'; -import { getHassDifferences, isTriggeredState } from '../ha'; -import { Timer } from '../timer'; -import { CardTriggersAPI } from './types'; - -export class TriggersManager { - protected _api: CardTriggersAPI; - - protected _triggers: Map = new Map(); - protected _untriggerTimer = new Timer(); - - constructor(api: CardTriggersAPI) { - this._api = api; - } - - public isTriggered(): boolean { - return !!this._triggers.size || this._untriggerTimer.isRunning(); - } - - public updateTriggeredCameras(oldHass?: HomeAssistant | null): boolean { - if (!this._shouldTrackTriggers()) { - return false; - } - - const hass = this._api.getHASSManager().getHASS(); - - const now = new Date(); - let triggerChanges = false; - - const cameras = this._api.getCameraManager().getStore().getVisibleCameras(); - for (const [cameraID, config] of cameras?.entries()) { - const triggerEntities = config.triggers.entities; - const diffs = getHassDifferences(hass, oldHass, triggerEntities, { - stateOnly: true, - }); - const shouldTrigger = diffs.some((diff) => isTriggeredState(diff.newState)); - const shouldUntrigger = triggerEntities.every( - (entity) => !isTriggeredState(hass?.states[entity]), - ); - if (shouldTrigger) { - this._triggers.set(cameraID, now); - triggerChanges = true; - } else if (shouldUntrigger && this._triggers.has(cameraID)) { - this._triggers.delete(cameraID); - triggerChanges = true; - } - } - - if (triggerChanges) { - const targetCameraID = this._getMostRecentTrigger(); - if (targetCameraID) { - this._triggerAction(targetCameraID); - return true; - } else { - this._startUntriggerTimer(); - } - } - return false; - } - - public untrigger(): void { - const wasTriggered = this.isTriggered(); - this._triggers.clear(); - this._untriggerTimer.stop(); - - if (wasTriggered) { - this._untriggerAction(); - } - } - - protected _triggerAction(cameraID: string): void { - const view = this._api.getViewManager().getView(); - if ( - this._isAutomatedViewUpdateAllowed() && - (view?.camera !== cameraID || !view?.is('live')) - ) { - this._api.getViewManager().setViewByParameters({ - viewName: 'live', - cameraID: cameraID, - }); - } - } - - protected _untriggerAction(): void { - if ( - !this.isTriggered() && - this._isAutomatedViewUpdateAllowed() && - this._api.getConfigManager().getConfig()?.view.scan.untrigger_reset - ) { - this._api.getViewManager().setViewDefault(); - } - } - - protected _isAutomatedViewUpdateAllowed(): boolean { - return ( - this._api.getConfigManager().getConfig()?.view.update_force || - !this._api.getInteractionManager().hasInteraction() - ); - } - - protected _shouldTrackTriggers(): boolean { - return !!this._api.getConfigManager().getConfig()?.view.scan.enabled; - } - - protected _startUntriggerTimer(): void { - this._untriggerTimer.start( - /* istanbul ignore next: the case of config being null here cannot be - reached, as there's no way to have the untrigger call happen without - a config. -- @preserve */ - this._api.getConfigManager().getConfig()?.view.scan.untrigger_seconds ?? 0, - () => { - this._untriggerAction(); - }, - ); - } - - protected _getMostRecentTrigger(): string | null { - const sorted = orderBy( - [...this._triggers.entries()], - (entry) => entry[1].getTime(), - 'desc', - ); - return sorted.length ? sorted[0][0] : null; - } -} diff --git a/src/utils/card-controller/types.ts b/src/utils/card-controller/types.ts deleted file mode 100644 index f914c4c6..00000000 --- a/src/utils/card-controller/types.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { CameraManager } from '../../camera-manager/manager'; -import { ConditionsManager } from './conditions-manager'; -import { EntityRegistryManager } from '../ha/entity-registry'; -import { ResolvedMediaCache } from '../ha/resolved-media'; -import { ActionsManager } from './actions-manager'; -import { AutoUpdateManager } from './auto-update-manager'; -import { AutomationsManager } from './automations-manager'; -import { CameraURLManager } from './camera-url-manager'; -import { CardElementManager } from './card-element-manager'; -import { ConfigManager } from './config-manager'; -import { DownloadManager } from './download-manager'; -import { ExpandManager } from './expand-manager'; -import { FullscreenManager } from './fullscreen-manager'; -import { HASSManager } from './hass-manager'; -import { InitializationManager } from './initialization-manager'; -import { InteractionManager } from './interaction-manager'; -import { MediaLoadedInfoManager } from './media-info-manager'; -import { MediaPlayerManager } from './media-player-manager'; -import { MessageManager } from './message-manager'; -import { MicrophoneManager } from './microphone-manager'; -import { StyleManager } from './style-manager'; -import { TriggersManager } from './triggers-manager'; -import { ViewManager } from './view-manager'; -import { QueryStringManager } from './query-string-manager'; - -/** - * This defines a series of limited APIs that various manager helpers use to - * control the card. Explicitly specifying them helps make coupling intentional - * and avoids cyclic importing. - */ - -export interface CardActionsManagerAPI { - getCameraManager(): CameraManager; - getCameraURLManager(): CameraURLManager; - getCardElementManager(): CardElementManager; - getConfigManager(): ConfigManager; - getDownloadManager(): DownloadManager; - getExpandManager(): ExpandManager; - getFullscreenManager(): FullscreenManager; - getHASSManager(): HASSManager; - getMediaLoadedInfoManager(): MediaLoadedInfoManager; - getMediaPlayerManager(): MediaPlayerManager; - getMessageManager(): MessageManager; - getViewManager(): ViewManager; - getMicrophoneManager(): MicrophoneManager; -} - -export interface CardAutomationsAPI { - getCardElementManager(): CardElementManager; - getConditionsManager(): ConditionsManager; - getConfigManager(): ConfigManager; - getHASSManager(): HASSManager; - getMessageManager(): MessageManager; -} - -export interface CardAutoRefreshAPI { - getConfigManager(): ConfigManager; - getViewManager(): ViewManager; - getTriggersManager(): TriggersManager; - getInteractionManager(): InteractionManager; -} - -export interface CardCameraAPI { - getConfigManager(): ConfigManager; - getEntityRegistryManager(): EntityRegistryManager; - getResolvedMediaCache(): ResolvedMediaCache; - getHASSManager(): HASSManager; - getMessageManager(): MessageManager; -} - -export interface CardCameraURLAPI { - getCameraManager(): CameraManager; - getViewManager(): ViewManager; -} - -export interface CardConditionAPI { - getAutomationsManager(): AutomationsManager; - getConfigManager(): ConfigManager; -} - -export interface CardConfigAPI { - getAutomationsManager(): AutomationsManager; - getCardElementManager(): CardElementManager; - getConditionsManager(): ConditionsManager; - getInitializationManager(): InitializationManager; - getMediaLoadedInfoManager(): MediaLoadedInfoManager; - getMessageManager(): MessageManager; - getStyleManager(): StyleManager; - getViewManager(): ViewManager; -} - -export interface CardDownloadAPI { - getCameraManager(): CameraManager; - getHASSManager(): HASSManager; - getMediaLoadedInfoManager(): MediaLoadedInfoManager; - getMessageManager(): MessageManager; - getViewManager(): ViewManager; -} - -export interface CardElementAPI { - getActionsManager(): ActionsManager; - getFullscreenManager(): FullscreenManager; - getInteractionManager(): InteractionManager; - getMediaLoadedInfoManager(): MediaLoadedInfoManager; - getQueryStringManager(): QueryStringManager; -} - -export interface CardExpandAPI { - getFullscreenManager(): FullscreenManager; - getConditionsManager(): ConditionsManager; - getCardElementManager(): CardElementManager; -} - -export interface CardFullscreenAPI { - getCardElementManager(): CardElementManager; - getExpandManager(): ExpandManager; - getConditionsManager(): ConditionsManager; - getMediaPlayerManager(): MediaPlayerManager; -} - -export interface CardHASSAPI { - getCameraManager(): CameraManager; - getCardElementManager(): CardElementManager; - getConditionsManager(): ConditionsManager; - getConfigManager(): ConfigManager; - getInteractionManager(): InteractionManager; - getMediaPlayerManager(): MediaPlayerManager; - getMessageManager(): MessageManager; - getStyleManager(): StyleManager; - getTriggersManager(): TriggersManager; - getViewManager(): ViewManager; -} - -export interface CardInitializerAPI { - getCameraManager(): CameraManager; - getCardElementManager(): CardElementManager; - getConfigManager(): ConfigManager; - getEntityRegistryManager(): EntityRegistryManager; - getHASSManager(): HASSManager; - getMediaPlayerManager(): MediaPlayerManager; - getMessageManager(): MessageManager; - getMicrophoneManager(): MicrophoneManager; - getQueryStringManager(): QueryStringManager; - getResolvedMediaCache(): ResolvedMediaCache; - getViewManager(): ViewManager; -} - -export interface CardInteractionAPI { - getConfigManager(): ConfigManager; - getStyleManager(): StyleManager; - getTriggersManager(): TriggersManager; - getViewManager(): ViewManager; -} - -export interface CardMediaLoadedAPI { - getConditionsManager(): ConditionsManager; - getConfigManager(): ConfigManager; - getCardElementManager(): CardElementManager; - getStyleManager(): StyleManager; -} - -export interface CardMediaPlayerAPI { - getHASSManager(): HASSManager; - getCameraManager(): CameraManager; - getEntityRegistryManager(): EntityRegistryManager; -} - -export interface CardMessageAPI { - getConditionsManager(): ConditionsManager; - getCardElementManager(): CardElementManager; - getMediaLoadedInfoManager(): MediaLoadedInfoManager; -} - -export interface CardMicrophoneAPI { - getCardElementManager(): CardElementManager; - getConfigManager(): ConfigManager; -} - -export interface CardQueryStringAPI { - getCardElementManager(): CardElementManager; - getViewManager(): ViewManager; - getActionsManager(): ActionsManager; -} - -export interface CardStyleAPI { - getCardElementManager(): CardElementManager; - getConfigManager(): ConfigManager; - getExpandManager(): ExpandManager; - getFullscreenManager(): FullscreenManager; - getHASSManager(): HASSManager; - getInteractionManager(): InteractionManager; - getMediaLoadedInfoManager(): MediaLoadedInfoManager; - getViewManager(): ViewManager; -} - -export interface CardTriggersAPI { - getCameraManager(): CameraManager; - getConfigManager(): ConfigManager; - getHASSManager(): HASSManager; - getInteractionManager(): InteractionManager; - getViewManager(): ViewManager; -} - -export interface CardViewAPI { - getAutoUpdateManager(): AutoUpdateManager; - getCameraManager(): CameraManager; - getConfigManager(): ConfigManager; - getHASSManager(): HASSManager; - getMediaLoadedInfoManager(): MediaLoadedInfoManager; - getMessageManager(): MessageManager; - getStyleManager(): StyleManager; - getConditionsManager(): ConditionsManager; - getCardElementManager(): CardElementManager; -} diff --git a/src/utils/card-controller/view-manager.ts b/src/utils/card-controller/view-manager.ts deleted file mode 100644 index 3e42a786..00000000 --- a/src/utils/card-controller/view-manager.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { ViewContext } from 'view'; -import { FrigateCardConfig, FrigateCardView, ViewDisplayMode } from '../../config/types'; -import { View } from '../../view/view'; -import { getAllDependentCameras } from '../camera'; -import { log } from '../debug'; -import { executeMediaQueryForView } from '../media-to-view'; -import { CardViewAPI } from './types'; - -interface ViewManagerSetViewDefaultParameters { - cameraID?: string; - substream?: string; -} - -export interface ViewManagerSetViewParameters - extends ViewManagerSetViewDefaultParameters { - viewName?: FrigateCardView; -} - -export class ViewManager { - protected _view: View | null = null; - protected _api: CardViewAPI; - - constructor(api: CardViewAPI) { - this._api = api; - } - - public getView(): View | null { - return this._view; - } - - public setView(view: View): void { - this._setView(view); - } - - public setViewDefault(params?: ViewManagerSetViewDefaultParameters): void { - const config = this._api.getConfigManager().getConfig(); - if (config) { - let forceCameraID: string | null = params?.cameraID ?? null; - if (!forceCameraID && this._view?.camera && config.view.update_cycle_camera) { - const cameraIDs = [ - ...this._api.getCameraManager().getStore().getVisibleCameraIDs(), - ]; - const currentIndex = cameraIDs.indexOf(this._view.camera); - const targetIndex = currentIndex + 1 >= cameraIDs.length ? 0 : currentIndex + 1; - forceCameraID = cameraIDs[targetIndex]; - } - - this.setViewByParameters({ - ...params, - viewName: config.view.default, - ...(forceCameraID && { cameraID: forceCameraID }), - }); - - // Restart the refresh timer, so the default view is refreshed at a fixed - // interval from now (if so configured). - this._api.getAutoUpdateManager().startDefaultViewTimer(); - } - } - - public setViewByParameters(params: ViewManagerSetViewParameters): void { - const config = this._api.getConfigManager().getConfig(); - - if (config) { - let cameraID: string | null = null; - - const cameras = this._api.getCameraManager().getStore().getVisibleCameraIDs(); - if (cameras.size) { - if (params?.cameraID && cameras.has(params.cameraID)) { - cameraID = params.cameraID; - } else { - // Reset to the default camera. - cameraID = cameras.keys().next().value; - } - } - const viewName = params?.viewName ?? this._view?.view ?? config.view.default; - if (cameraID && viewName && this.isViewSupportedByCamera(cameraID, viewName)) { - const displayMode = - this._view?.displayMode ?? - this._getDefaultDisplayModeForView(viewName, config); - let view: View = new View({ - view: viewName, - camera: cameraID, - displayMode: displayMode, - }); - if (params.substream) { - view = this._createViewWithSelectedSubstream(view, params.substream); - } - this._setView(view); - } - } - } - - public setViewWithNewContext(context: ViewContext): void { - if (this._view) { - return this._setView(this._view?.clone().mergeInContext(context)); - } - } - - public reset(): void { - this._view = null; - } - - public async setViewWithNewDisplayMode(displayMode: ViewDisplayMode): Promise { - const hass = this._api.getHASSManager().getHASS(); - - if (this._view && hass) { - const view = this._view.evolve({ - displayMode: displayMode, - }); - - const cameraCount = this._api - .getCameraManager() - .getStore() - .getVisibleCameraCount(); - const queryCameraCount = view.query?.getQueryCameraIDs()?.size ?? 0; - const generateNewQuery = - view?.query && - queryCameraCount && - ((view.isGrid() && queryCameraCount < cameraCount) || - (!view.isGrid() && queryCameraCount > 1)); - - if (generateNewQuery && view && view.query) { - // If the user requests a grid but the current query does not have a - // query for more than one camera, reset the query results, change the - // existing query to refer to all cameras and execute it to fetch new - // results. - let viewWithNewQuery: View | null = null; - try { - viewWithNewQuery = await executeMediaQueryForView( - this._api.getCameraManager(), - view, - view.query - .clone() - .setQueryCameraIDs( - view.isGrid() - ? this._api.getCameraManager().getStore().getVisibleCameraIDs() - : view.camera, - ), - ); - } catch (e: unknown) { - this._api.getMessageManager().setErrorIfHigherPriority(e); - } - - if (viewWithNewQuery) { - return this._setView(viewWithNewQuery); - } - } else { - return this._setView(view); - } - } - } - - public setViewWithSubstream(substream?: string): void { - if (!this._view) { - return; - } - this._setView( - substream - ? this._createViewWithSelectedSubstream(this._view, substream) - : this._createViewWithNextStream(this._view), - ); - } - - public setViewWithoutSubstream(): void { - const view = this._createViewWithoutSubstream(); - if (view) { - return this._setView(view); - } - } - - public isViewSupportedByCamera(cameraID: string, view: FrigateCardView): boolean { - const capabilities = this._api.getCameraManager().getCameraCapabilities(cameraID); - switch (view) { - case 'live': - case 'image': - case 'diagnostics': - return true; - case 'clip': - case 'clips': - return !!capabilities?.supportsClips; - case 'snapshot': - case 'snapshots': - return !!capabilities?.supportsSnapshots; - case 'recording': - case 'recordings': - return !!capabilities?.supportsRecordings; - case 'timeline': - return !!capabilities?.supportsTimeline; - case 'media': - return ( - !!capabilities?.supportsClips || - !!capabilities?.supportsSnapshots || - !!capabilities?.supportsRecordings - ); - } - } - - protected _getDefaultDisplayModeForView( - viewName: FrigateCardView, - config?: FrigateCardConfig, - ): ViewDisplayMode { - let mode: ViewDisplayMode | null = null; - switch (viewName) { - case 'media': - case 'clip': - case 'recording': - case 'snapshot': - mode = config?.media_viewer.display?.mode ?? null; - break; - case 'live': - mode = config?.live.display?.mode ?? null; - break; - } - return mode ?? 'single'; - } - - protected _setView(view: View): void { - const oldView = this._view; - View.adoptFromViewIfAppropriate(view, oldView); - - log( - this._api.getConfigManager().getCardWideConfig(), - `Frigate Card view change: `, - view.view, - ); - this._view = view; - - if (View.isMajorMediaChange(oldView, view)) { - this._api.getMediaLoadedInfoManager().clear(); - } - - if (oldView?.view !== view.view) { - this._api.getCardElementManager().scrollReset(); - } - - this._api.getMessageManager().reset(); - this._api.getStyleManager().setExpandedMode(); - - this._api.getConditionsManager()?.setState({ - view: view.view, - camera: view.camera, - displayMode: view.displayMode ?? undefined, - }); - this._api.getCardElementManager().update(); - } - - protected _createViewWithSelectedSubstream(baseView: View, substreamID: string): View { - const overrides: Map = - baseView?.context?.live?.overrides ?? new Map(); - overrides.set(baseView.camera, substreamID); - return baseView.clone().mergeInContext({ - live: { overrides: overrides }, - }); - } - - protected _createViewWithNextStream(baseView: View): View { - const dependencies = [ - ...getAllDependentCameras(this._api.getCameraManager(), baseView.camera), - ]; - if (dependencies.length <= 1) { - return baseView.clone(); - } - - const view = baseView.clone(); - const overrides: Map = view.context?.live?.overrides ?? new Map(); - const currentOverride = overrides.get(view.camera) ?? view.camera; - const currentIndex = dependencies.indexOf(currentOverride); - const newIndex = currentIndex < 0 ? 0 : (currentIndex + 1) % dependencies.length; - overrides.set(view.camera, dependencies[newIndex]); - view.mergeInContext({ live: { overrides: overrides } }); - - return view; - } - - protected _createViewWithoutSubstream(): View | null { - if (!this._view) { - return null; - } - const view = this._view.clone(); - const overrides: Map | undefined = view.context?.live?.overrides; - if (overrides && overrides.has(view.camera)) { - view.context?.live?.overrides?.delete(view.camera); - } - return view; - } -} diff --git a/src/utils/menu-controller.ts b/src/utils/menu-controller.ts index 906fbce2..23d43cb2 100644 --- a/src/utils/menu-controller.ts +++ b/src/utils/menu-controller.ts @@ -15,8 +15,8 @@ import { import { View } from '../view/view'; import { createFrigateCardCustomAction } from './action'; import { getAllDependentCameras } from './camera'; -import { MediaPlayerManager } from './card-controller/media-player-manager'; -import { MicrophoneManager } from './card-controller/microphone-manager'; +import { MediaPlayerManager } from '../card-controller/media-player-manager'; +import { MicrophoneManager } from '../card-controller/microphone-manager'; import { getEntityIcon, getEntityTitle } from './ha'; import { hasSubstream } from './substream'; export interface MenuButtonControllerOptions { diff --git a/tests/test-utils.ts b/tests/test-utils.ts index c73926e7..7458701c 100644 --- a/tests/test-utils.ts +++ b/tests/test-utils.ts @@ -22,28 +22,28 @@ import { performanceConfigSchema, } from '../src/config/types'; import { ExtendedHomeAssistant, MediaLoadedInfo } from '../src/types'; -import { ActionsManager } from '../src/utils/card-controller/actions-manager'; -import { AutoUpdateManager } from '../src/utils/card-controller/auto-update-manager'; -import { AutomationsManager } from '../src/utils/card-controller/automations-manager'; -import { CameraURLManager } from '../src/utils/card-controller/camera-url-manager'; -import { CardElementManager } from '../src/utils/card-controller/card-element-manager'; -import { ConditionsManager } from '../src/utils/card-controller/conditions-manager'; -import { ConfigManager } from '../src/utils/card-controller/config-manager'; -import { CardController } from '../src/utils/card-controller/controller'; -import { DownloadManager } from '../src/utils/card-controller/download-manager'; -import { ExpandManager } from '../src/utils/card-controller/expand-manager'; -import { FullscreenManager } from '../src/utils/card-controller/fullscreen-manager'; -import { HASSManager } from '../src/utils/card-controller/hass-manager'; -import { InitializationManager } from '../src/utils/card-controller/initialization-manager'; -import { InteractionManager } from '../src/utils/card-controller/interaction-manager'; -import { MediaLoadedInfoManager } from '../src/utils/card-controller/media-info-manager'; -import { MediaPlayerManager } from '../src/utils/card-controller/media-player-manager'; -import { MessageManager } from '../src/utils/card-controller/message-manager'; -import { MicrophoneManager } from '../src/utils/card-controller/microphone-manager'; -import { QueryStringManager } from '../src/utils/card-controller/query-string-manager'; -import { StyleManager } from '../src/utils/card-controller/style-manager'; -import { TriggersManager } from '../src/utils/card-controller/triggers-manager'; -import { ViewManager } from '../src/utils/card-controller/view-manager'; +import { ActionsManager } from '../src/card-controller/actions-manager'; +import { AutoUpdateManager } from '../src/card-controller/auto-update-manager'; +import { AutomationsManager } from '../src/card-controller/automations-manager'; +import { CameraURLManager } from '../src/card-controller/camera-url-manager'; +import { CardElementManager } from '../src/card-controller/card-element-manager'; +import { ConditionsManager } from '../src/card-controller/conditions-manager'; +import { ConfigManager } from '../src/card-controller/config-manager'; +import { CardController } from '../src/card-controller/controller'; +import { DownloadManager } from '../src/card-controller/download-manager'; +import { ExpandManager } from '../src/card-controller/expand-manager'; +import { FullscreenManager } from '../src/card-controller/fullscreen-manager'; +import { HASSManager } from '../src/card-controller/hass-manager'; +import { InitializationManager } from '../src/card-controller/initialization-manager'; +import { InteractionManager } from '../src/card-controller/interaction-manager'; +import { MediaLoadedInfoManager } from '../src/card-controller/media-info-manager'; +import { MediaPlayerManager } from '../src/card-controller/media-player-manager'; +import { MessageManager } from '../src/card-controller/message-manager'; +import { MicrophoneManager } from '../src/card-controller/microphone-manager'; +import { QueryStringManager } from '../src/card-controller/query-string-manager'; +import { StyleManager } from '../src/card-controller/style-manager'; +import { TriggersManager } from '../src/card-controller/triggers-manager'; +import { ViewManager } from '../src/card-controller/view-manager'; import { Entity } from '../src/utils/ha/entity-registry/types'; import { ViewMedia, ViewMediaType } from '../src/view/media'; import { MediaQueriesResults } from '../src/view/media-queries-results'; diff --git a/tests/utils/card-controller/actions-manager.test.ts b/tests/utils/card-controller/actions-manager.test.ts deleted file mode 100644 index 8985a534..00000000 --- a/tests/utils/card-controller/actions-manager.test.ts +++ /dev/null @@ -1,728 +0,0 @@ -import { afterAll, describe, expect, it, vi } from 'vitest'; -import { mock } from 'vitest-mock-extended'; -import { - ActionType, - FrigateCardCustomAction, - FrigateCardView, - frigateCardCustomActionSchema, -} from '../../../src/config/types'; -import { FrigateCardMediaPlayer } from '../../../src/types'; -import { - convertActionToFrigateCardCustomAction, - frigateCardHandleActionConfig, - getActionConfigGivenAction, -} from '../../../src/utils/action.js'; -import { ActionsManager } from '../../../src/utils/card-controller/actions-manager'; -import { - createCardAPI, - createConfig, - createHASS, - createMediaLoadedInfo, - createView, - createViewWithMedia, -} from '../../test-utils'; - -vi.mock('../../../src/utils/action.js'); -vi.mock('../../../src/camera-manager/manager.js'); - -const createAction = ( - action: Record, -): FrigateCardCustomAction | null => { - const result = frigateCardCustomActionSchema.safeParse({ - action: 'custom:frigate-card-action', - ...action, - }); - return result.success ? result.data : null; -}; - -describe('ActionsManager.getMergedActions', () => { - const config = { - view: { - actions: { - tap_action: { - action: 'navigate', - navigation_path: '1', - }, - }, - }, - live: { - actions: { - tap_action: { - action: 'navigate', - navigation_path: '2', - }, - }, - }, - media_gallery: { - actions: { - tap_action: { - action: 'navigate', - navigation_path: '3', - }, - }, - }, - media_viewer: { - actions: { - tap_action: { - action: 'navigate', - navigation_path: '4', - }, - }, - }, - image: { - actions: { - tap_action: { - action: 'navigate', - navigation_path: '5', - }, - }, - }, - }; - - afterAll(() => { - vi.restoreAllMocks(); - }); - - it('should get no merged actions with a message', () => { - const api = createCardAPI(); - vi.mocked(api.getViewManager().getView).mockReturnValue( - createView({ view: 'live' }), - ); - vi.mocked(api.getMessageManager().hasMessage).mockReturnValue(true); - - const manager = new ActionsManager(api); - - expect(manager.getMergedActions()).toEqual({}); - }); - - describe('should get merged actions with live view', () => { - it.each([ - [ - 'live' as const, - { - tap_action: { - action: 'navigate', - navigation_path: '2', - }, - }, - ], - [ - 'clips' as const, - { - tap_action: { - action: 'navigate', - navigation_path: '3', - }, - }, - ], - [ - 'clip' as const, - { - tap_action: { - action: 'navigate', - navigation_path: '4', - }, - }, - ], - [ - 'image' as const, - { - tap_action: { - action: 'navigate', - navigation_path: '5', - }, - }, - ], - ['timeline' as const, {}], - ])('%s', (viewName: FrigateCardView, result: Record) => { - const api = createCardAPI(); - vi.mocked(api.getViewManager().getView).mockReturnValue( - createView({ view: viewName }), - ); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig(config)); - - const manager = new ActionsManager(api); - - expect(manager.getMergedActions()).toEqual(result); - }); - }); -}); - -// @vitest-environment jsdom -describe('ActionsManager.handleInteraction', () => { - it('should handle interaction', () => { - const api = createCardAPI(); - const element = document.createElement('div'); - vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element); - const manager = new ActionsManager(api); - - const hass = createHASS(); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass); - - const actionForThisInteraction: ActionType = { - action: 'none', - }; - vi.mocked(getActionConfigGivenAction).mockReturnValue(actionForThisInteraction); - - manager.handleInteraction('tap'); - - expect(frigateCardHandleActionConfig).toBeCalledWith( - element, - hass, - manager.getMergedActions(), - 'tap', - actionForThisInteraction, - ); - }); - - it('should not handle interaction', () => { - const api = createCardAPI(); - const manager = new ActionsManager(api); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null); - - // No values of hass. - manager.handleInteraction('tap'); - expect(frigateCardHandleActionConfig).not.toBeCalledWith(); - }); -}); - -describe('ActionsManager.handleActionEvent', () => { - it('should handle event', () => { - const action = createAction({ frigate_card_action: 'default' })!; - const event: CustomEvent = new CustomEvent('ll-custom', { - detail: action, - }); - - // The file containing convertActionToFrigateCardCustomAction (action.ts) is - // mocked, so need to provide a value here. - vi.mocked(convertActionToFrigateCardCustomAction).mockReturnValue(action); - - const api = createCardAPI(); - const manager = new ActionsManager(api); - - manager.handleActionEvent(event); - expect(api.getViewManager().setViewDefault).toBeCalled(); - }); - - it('should not handle event without detail', () => { - const action = createAction({ frigate_card_action: 'default' })!; - const event = new Event('ll-custom'); - - // Mock this out just so that if the sentinel in handleActionEvent failed, - // it would still trigger a test failure below. - vi.mocked(convertActionToFrigateCardCustomAction).mockReturnValue(action); - - const api = createCardAPI(); - const manager = new ActionsManager(api); - manager.handleActionEvent(event); - - expect(api.getViewManager().setViewDefault).not.toBeCalled(); - }); - - it('should not handle malformed action', () => { - const action = createAction({ frigate_card_action: 'default' })!; - const event: CustomEvent = new CustomEvent('ll-custom', { - detail: action, - }); - - vi.mocked(convertActionToFrigateCardCustomAction).mockReturnValue(null); - - const api = createCardAPI(); - const manager = new ActionsManager(api); - manager.handleActionEvent(event); - - expect(api.getViewManager().setViewDefault).not.toBeCalled(); - }); -}); - -describe('ActionsManager.executeAction', () => { - it('should not handle actions with different card_id', async () => { - const api = createCardAPI(); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue( - createConfig({ - card_id: 'foo', - }), - ); - - const manager = new ActionsManager(api); - - await manager.executeAction( - createAction({ - card_id: 'NOT_foo', - frigate_card_action: 'default', - })!, - ); - - expect(api.getViewManager().setViewDefault).not.toBeCalled(); - }); - - it('should handle default action', async () => { - const api = createCardAPI(); - const manager = new ActionsManager(api); - - await manager.executeAction( - createAction({ - frigate_card_action: 'default', - })!, - ); - - expect(api.getViewManager().setViewDefault).toBeCalled(); - }); - - describe('should handle view action', async () => { - it.each([ - ['clip' as const], - ['clips' as const], - ['image' as const], - ['live' as const], - ['recording' as const], - ['recordings' as const], - ['snapshot' as const], - ['snapshots' as const], - ['timeline' as const], - ])('%s', async (viewName: FrigateCardView) => { - const api = createCardAPI(); - const manager = new ActionsManager(api); - - await manager.executeAction( - createAction({ - frigate_card_action: viewName, - })!, - ); - - expect(api.getViewManager().setViewByParameters).toBeCalledWith( - expect.objectContaining({ - viewName: viewName, - }), - ); - }); - }); - - it('should handle download action', async () => { - const api = createCardAPI(); - const manager = new ActionsManager(api); - - await manager.executeAction( - createAction({ - frigate_card_action: 'download', - })!, - ); - - expect(api.getDownloadManager().downloadViewerMedia).toBeCalled(); - }); - - it('should handle camera ui action', async () => { - const api = createCardAPI(); - const manager = new ActionsManager(api); - - await manager.executeAction( - createAction({ - frigate_card_action: 'camera_ui', - })!, - ); - - expect(api.getCameraURLManager().openURL).toBeCalled(); - }); - - it('should handle expand action', async () => { - const api = createCardAPI(); - const manager = new ActionsManager(api); - - await manager.executeAction( - createAction({ - frigate_card_action: 'expand', - })!, - ); - - expect(api.getExpandManager().toggleExpanded).toBeCalled(); - }); - - it('should handle fullscreen action', async () => { - const api = createCardAPI(); - const manager = new ActionsManager(api); - - await manager.executeAction( - createAction({ - frigate_card_action: 'fullscreen', - })!, - ); - - expect(api.getFullscreenManager().toggleFullscreen).toBeCalled(); - }); - - it('should handle menu toggle action', async () => { - const api = createCardAPI(); - const manager = new ActionsManager(api); - - await manager.executeAction( - createAction({ - frigate_card_action: 'menu_toggle', - })!, - ); - - expect(api.getCardElementManager().toggleMenu).toBeCalled(); - }); - - describe('should handle camera_select action', () => { - it('with valid camera and view', async () => { - const api = createCardAPI(); - const manager = new ActionsManager(api); - - vi.mocked(api.getViewManager().getView).mockReturnValue(createView()); - vi.mocked(api.getViewManager().isViewSupportedByCamera).mockReturnValue(true); - - await manager.executeAction( - createAction({ - frigate_card_action: 'camera_select', - camera: 'camera', - })!, - ); - - expect(api.getViewManager().setViewByParameters).toBeCalledWith( - expect.objectContaining({ - viewName: 'live', - cameraID: 'camera', - }), - ); - }); - - it('without config', async () => { - const api = createCardAPI(); - const manager = new ActionsManager(api); - - vi.mocked(api.getConfigManager().getConfig).mockReturnValue(null); - vi.mocked(api.getViewManager().getView).mockReturnValue( - createView({ - view: 'timeline', - }), - ); - vi.mocked(api.getViewManager().isViewSupportedByCamera).mockReturnValue(true); - - await manager.executeAction( - createAction({ - frigate_card_action: 'camera_select', - camera: 'camera', - })!, - ); - - expect(api.getViewManager().setViewByParameters).toBeCalledWith( - expect.objectContaining({ - viewName: 'timeline', - cameraID: 'camera', - }), - ); - }); - - it('with target view', async () => { - const api = createCardAPI(); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue( - createConfig({ - view: { - // Change to clips view when the camera changes. - camera_select: 'clips', - }, - }), - ); - vi.mocked(api.getViewManager().getView).mockReturnValue( - createView({ - view: 'live', - }), - ); - vi.mocked(api.getViewManager().isViewSupportedByCamera).mockReturnValue(true); - const manager = new ActionsManager(api); - - await manager.executeAction( - createAction({ - frigate_card_action: 'camera_select', - camera: 'camera', - })!, - ); - - expect(api.getViewManager().setViewByParameters).toBeCalledWith( - expect.objectContaining({ - viewName: 'clips', - cameraID: 'camera', - }), - ); - }); - - it('without a current view', async () => { - const api = createCardAPI(); - const manager = new ActionsManager(api); - - await manager.executeAction( - createAction({ - frigate_card_action: 'camera_select', - camera: 'camera', - })!, - ); - - expect(api.getViewManager().setViewByParameters).not.toBeCalled(); - }); - - it('with an unsupported view', async () => { - const api = createCardAPI(); - const manager = new ActionsManager(api); - - vi.mocked(api.getViewManager().getView).mockReturnValue( - createView({ - view: 'timeline', - }), - ); - - await manager.executeAction( - createAction({ - frigate_card_action: 'camera_select', - camera: 'camera', - })!, - ); - - expect(api.getViewManager().setViewByParameters).toBeCalledWith( - expect.objectContaining({ - // Should have fallen back to the default view. - viewName: 'live', - cameraID: 'camera', - }), - ); - }); - }); - - it('should handle live_substream_select action', async () => { - const api = createCardAPI(); - const manager = new ActionsManager(api); - - await manager.executeAction( - createAction({ - frigate_card_action: 'live_substream_select', - camera: 'substream', - })!, - ); - - expect(api.getViewManager().setViewWithSubstream).toBeCalledWith('substream'); - }); - - it('should handle live_substream_off action', async () => { - const api = createCardAPI(); - const manager = new ActionsManager(api); - - await manager.executeAction( - createAction({ - frigate_card_action: 'live_substream_off', - })!, - ); - - expect(api.getViewManager().setViewWithoutSubstream).toBeCalled(); - }); - - it('should handle live_substream_on action', async () => { - const api = createCardAPI(); - const manager = new ActionsManager(api); - - await manager.executeAction( - createAction({ - frigate_card_action: 'live_substream_on', - })!, - ); - - expect(api.getViewManager().setViewWithSubstream).toBeCalledWith(); - }); - - describe('should handle media_player action', () => { - it('to stop', async () => { - const api = createCardAPI(); - const manager = new ActionsManager(api); - - await manager.executeAction( - createAction({ - frigate_card_action: 'media_player', - media_player_action: 'stop', - media_player: 'this_is_a_media_player', - })!, - ); - - expect(api.getMediaPlayerManager().stop).toBeCalledWith('this_is_a_media_player'); - }); - - it('to play live', async () => { - const api = createCardAPI(); - vi.mocked(api.getViewManager().getView).mockReturnValue( - createView({ - camera: 'camera', - view: 'live', - }), - ); - const manager = new ActionsManager(api); - - await manager.executeAction( - createAction({ - frigate_card_action: 'media_player', - media_player_action: 'play', - media_player: 'this_is_a_media_player', - })!, - ); - - expect(api.getMediaPlayerManager().playLive).toBeCalledWith( - 'this_is_a_media_player', - 'camera', - ); - }); - - it('to play media', async () => { - const api = createCardAPI(); - const view = createViewWithMedia({ - camera: 'camera', - view: 'media', - }); - - vi.mocked(api.getViewManager().getView).mockReturnValue(view); - const manager = new ActionsManager(api); - - await manager.executeAction( - createAction({ - frigate_card_action: 'media_player', - media_player_action: 'play', - media_player: 'this_is_a_media_player', - })!, - ); - - expect(api.getMediaPlayerManager().playMedia).toBeCalledWith( - 'this_is_a_media_player', - view.queryResults?.getSelectedResult(), - ); - }); - - it('to play media without selected media', async () => { - const api = createCardAPI(); - vi.mocked(api.getViewManager().getView).mockReturnValue( - createView({ - view: 'media', - }), - ); - const manager = new ActionsManager(api); - - await manager.executeAction( - createAction({ - frigate_card_action: 'media_player', - media_player_action: 'play', - media_player: 'this_is_a_media_player', - })!, - ); - - expect(api.getMediaPlayerManager().playMedia).not.toBeCalled(); - }); - }); - - it('should handle diagnostics action', async () => { - const api = createCardAPI(); - const manager = new ActionsManager(api); - - await manager.executeAction( - createAction({ - frigate_card_action: 'diagnostics', - })!, - ); - - expect(api.getViewManager().setViewByParameters).toBeCalledWith( - expect.objectContaining({ - viewName: 'diagnostics', - }), - ); - }); - - it('should handle microphone_mute action', async () => { - const api = createCardAPI(); - const manager = new ActionsManager(api); - - await manager.executeAction( - createAction({ - frigate_card_action: 'microphone_mute', - })!, - ); - - expect(api.getMicrophoneManager().mute).toBeCalled(); - }); - - it('should handle microphone_unmute action', async () => { - const api = createCardAPI(); - const manager = new ActionsManager(api); - - await manager.executeAction( - createAction({ - frigate_card_action: 'microphone_unmute', - })!, - ); - - expect(api.getMicrophoneManager().unmute).toBeCalled(); - }); - - describe('should handle media player action', () => { - it.each([ - ['mute' as const], - ['unmute' as const], - ['play' as const], - ['pause' as const], - ])('%s', async (action: 'mute' | 'unmute' | 'play' | 'pause') => { - const api = createCardAPI(); - const player = mock(); - vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue( - createMediaLoadedInfo({ - player: player, - }), - ); - const manager = new ActionsManager(api); - - await manager.executeAction( - createAction({ - frigate_card_action: action, - })!, - ); - - expect(player[action]).toBeCalled(); - }); - }); - - it('should handle screenshot action', async () => { - const api = createCardAPI(); - const manager = new ActionsManager(api); - - await manager.executeAction( - createAction({ - frigate_card_action: 'screenshot', - })!, - ); - - expect(api.getDownloadManager().downloadScreenshot).toBeCalled(); - }); - - it('should handle display_mode_select action', async () => { - const api = createCardAPI(); - const manager = new ActionsManager(api); - - await manager.executeAction( - createAction({ - frigate_card_action: 'display_mode_select', - display_mode: 'grid', - })!, - ); - - expect(api.getViewManager().setViewWithNewDisplayMode).toBeCalledWith('grid'); - }); - - it('should handle unknown action', async () => { - const manager = new ActionsManager(createCardAPI()); - - const spy = vi.spyOn(global.console, 'warn').mockImplementation(() => true); - - await manager.executeAction( - // Have to manually create the action (vs using `createAction()`) since - // it's malformed. - { - frigate_card_action: 'not_a_real_action', - } as unknown as FrigateCardCustomAction, - ); - - expect(spy).toBeCalledWith( - 'Frigate card received unknown card action: not_a_real_action', - ); - }); -}); diff --git a/tests/utils/card-controller/auto-update-manager.test.ts b/tests/utils/card-controller/auto-update-manager.test.ts deleted file mode 100644 index b7799a7c..00000000 --- a/tests/utils/card-controller/auto-update-manager.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import add from 'date-fns/add'; -import { afterEach, describe, expect, it, vi } from 'vitest'; -import { AutoUpdateManager } from '../../../src/utils/card-controller/auto-update-manager'; -import { createCardAPI, createConfig } from '../../test-utils'; - -// @vitest-environment jsdom -describe('AutoUpdateManager', () => { - const start = new Date('2023-09-23T19:12:00'); - - afterEach(() => { - vi.useRealTimers(); - }); - - it('should set default view when allowed', () => { - const api = createCardAPI(); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue( - createConfig({ - view: { - update_seconds: 10, - }, - }), - ); - // Card is triggered. - vi.mocked(api.getTriggersManager().isTriggered).mockReturnValue(true); - vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(false); - - vi.useFakeTimers(); - vi.setSystemTime(start); - - const manager = new AutoUpdateManager(api); - manager.startDefaultViewTimer(); - - expect(api.getViewManager().setViewDefault).not.toBeCalled(); - - vi.setSystemTime(add(start, { seconds: 10 })); - vi.runOnlyPendingTimers(); - - expect(api.getViewManager().setViewDefault).not.toBeCalled(); - - vi.mocked(api.getTriggersManager().isTriggered).mockReturnValue(false); - - vi.setSystemTime(add(start, { seconds: 20 })); - vi.runOnlyPendingTimers(); - - expect(api.getViewManager().setViewDefault).toBeCalled(); - }); - - it('should not set default view when not configured', () => { - const api = createCardAPI(); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue( - createConfig({ - view: { - update_seconds: 0, - }, - }), - ); - vi.mocked(api.getTriggersManager().isTriggered).mockReturnValue(false); - vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(false); - - vi.useFakeTimers(); - vi.setSystemTime(start); - - const manager = new AutoUpdateManager(api); - manager.startDefaultViewTimer(); - - expect(api.getViewManager().setViewDefault).not.toBeCalled(); - - vi.setSystemTime(add(start, { seconds: 10 })); - vi.runOnlyPendingTimers(); - - expect(api.getViewManager().setViewDefault).not.toBeCalled(); - }); -}); diff --git a/tests/utils/card-controller/automations-manager.test.ts b/tests/utils/card-controller/automations-manager.test.ts deleted file mode 100644 index 1194a935..00000000 --- a/tests/utils/card-controller/automations-manager.test.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; -import { frigateCardHandleAction } from '../../../src/utils/action.js'; -import { - AutomationsManager, -} from '../../../src/utils/card-controller/automations-manager'; -import { createCardAPI, createConfig, createHASS } from '../../test-utils'; - -vi.mock('../../../src/utils/action.js'); - -describe('AutomationsManager', () => { - const actions = [ - { - action: 'custom:frigate-card-action', - frigate_card_action: 'clips', - }, - ]; - const conditions = { fullscreen: true }; - - afterEach(() => { - vi.clearAllMocks(); - }); - - it('should do nothing without hass', () => { - const api = createCardAPI(); - - const automationsManager = new AutomationsManager(api); - automationsManager.execute(); - expect(frigateCardHandleAction).not.toBeCalled(); - }); - - it('should do nothing without automations', () => { - const api = createCardAPI(); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); - - const automationsManager = new AutomationsManager(api); - automationsManager.setAutomationsFromConfig(); - automationsManager.execute(); - expect(frigateCardHandleAction).not.toBeCalled(); - }); - - it('should execute actions', () => { - const config = createConfig({ - automations: [ - { - conditions: conditions, - actions: actions, - }, - ], - }); - - const api = createCardAPI(); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); - vi.mocked(api.getConfigManager().getNonOverriddenConfig).mockReturnValue(config); - - const automationsManager = new AutomationsManager(api); - automationsManager.setAutomationsFromConfig(); - - automationsManager.execute(); - expect(frigateCardHandleAction).not.toBeCalled(); - - vi.mocked(api.getConditionsManager().evaluateCondition).mockReturnValue(true); - - automationsManager.execute(); - expect(frigateCardHandleAction).toBeCalledTimes(1); - - // Automation will not re-fire when condition continues to evaluate the - // same. - automationsManager.execute(); - expect(frigateCardHandleAction).toBeCalledTimes(1); - - vi.mocked(api.getConditionsManager().evaluateCondition).mockReturnValue(false); - - automationsManager.execute(); - expect(frigateCardHandleAction).toBeCalledTimes(1); - - vi.mocked(api.getConditionsManager().evaluateCondition).mockReturnValue(true); - - automationsManager.execute(); - expect(frigateCardHandleAction).toBeCalledTimes(2); - }); - - it('should execute actions_not', () => { - const config = createConfig({ - automations: [ - { - conditions: conditions, - actions_not: actions, - }, - ], - }); - - const api = createCardAPI(); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); - vi.mocked(api.getConfigManager().getNonOverriddenConfig).mockReturnValue(config); - vi.mocked(api.getConditionsManager().evaluateCondition).mockReturnValue(false); - - const automationsManager = new AutomationsManager(api); - automationsManager.setAutomationsFromConfig(); - - automationsManager.execute(); - - expect(frigateCardHandleAction).toBeCalled(); - }); - - it('should prevent automation loops', () => { - const config = createConfig({ - automations: [ - { - conditions: { fullscreen: true }, - actions: actions, - }, - { - conditions: { fullscreen: true }, - actions_not: actions, - }, - ], - }); - - const api = createCardAPI(); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); - vi.mocked(api.getConfigManager().getNonOverriddenConfig).mockReturnValue(config); - - const automationsManager = new AutomationsManager(api); - automationsManager.setAutomationsFromConfig(); - - // Create a setup where one automation action causes another... - let evaluation = true; - vi.mocked(frigateCardHandleAction).mockImplementation(() => { - evaluation = !evaluation; - vi.mocked(api.getConditionsManager().evaluateCondition).mockReturnValue( - evaluation, - ); - automationsManager.execute(); - }); - - vi.mocked(api.getConditionsManager().evaluateCondition).mockReturnValue(evaluation); - - automationsManager.execute(); - - expect(api.getMessageManager().setMessageIfHigherPriority).toBeCalledWith( - expect.objectContaining({ - type: 'error', - message: - 'Too many nested automation calls, please check your configuration for loops', - }), - ); - - expect(frigateCardHandleAction).toBeCalledTimes(10); - }); -}); diff --git a/tests/utils/card-controller/camera-url-manager.test.ts b/tests/utils/card-controller/camera-url-manager.test.ts deleted file mode 100644 index 12c88c63..00000000 --- a/tests/utils/card-controller/camera-url-manager.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { CameraEndpoint } from '../../../src/camera-manager/types'; -import { CameraURLManager } from '../../../src/utils/card-controller/camera-url-manager'; -import { - CardCameraURLAPI -} from '../../../src/utils/card-controller/types'; -import { createCardAPI, createViewWithMedia } from '../../test-utils'; - -const createAPIWithMedia = (): CardCameraURLAPI => { - const api = createCardAPI(); - vi.mocked(api.getViewManager().getView).mockReturnValue( - createViewWithMedia() - ) - return api; -}; - -// @vitest-environment jsdom -describe('CameraURLManager', () => { - beforeEach(() => { - vi.restoreAllMocks(); - }); - - it('should get URL', () => { - const api = createAPIWithMedia(); - const manager = new CameraURLManager(api); - - const endpoint: CameraEndpoint = { - endpoint: 'http://frigate', - }; - - vi.mocked(api.getCameraManager().getCameraEndpoints)?.mockReturnValue({ - ui: endpoint, - }); - - expect(manager.getCameraURL()).toBe('http://frigate'); - expect(manager.hasCameraURL()).toBeTruthy(); - - const windowSpy = vi.spyOn(window, 'open').mockReturnValue(null); - manager.openURL(); - expect(windowSpy).toBeCalledWith('http://frigate'); - }); - - it('should not get URL without view', () => { - const manager = new CameraURLManager(createCardAPI()); - expect(manager.getCameraURL()).toBeNull(); - - const windowSpy = vi.spyOn(window, 'open').mockReturnValue(null); - manager.openURL(); - expect(windowSpy).not.toBeCalled(); - }); - - it('should not get URL without cameraManager endpoints', () => { - const api = createAPIWithMedia(); - vi.mocked(api.getCameraManager().getCameraEndpoints)?.mockReturnValue(null); - const manager = new CameraURLManager(api); - expect(manager.getCameraURL()).toBeNull(); - }); -}); diff --git a/tests/utils/card-controller/card-element-manager.test.ts b/tests/utils/card-controller/card-element-manager.test.ts deleted file mode 100644 index bea938a3..00000000 --- a/tests/utils/card-controller/card-element-manager.test.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; -import { - CardElementManager, - CardHTMLElement, -} from '../../../src/utils/card-controller/card-element-manager'; -import { createCardAPI } from '../../test-utils'; -import { mock } from 'vitest-mock-extended'; - -const createElement = (): CardHTMLElement => { - const element = document.createElement('div') as unknown as CardHTMLElement; - element.requestUpdate = vi.fn(); - return element as CardHTMLElement; -}; - -// @vitest-environment jsdom -describe('CardElementManager', () => { - afterEach(() => { - vi.unstubAllGlobals(); - global.window.location = mock(); - }); - - it('should get element', () => { - const element = createElement(); - const manager = new CardElementManager( - createCardAPI(), - element, - () => undefined, - () => undefined, - ); - - expect(manager.getElement()).toBe(element); - }); - - it('should reset scroll', () => { - const callback = vi.fn(); - const manager = new CardElementManager( - createCardAPI(), - createElement(), - callback, - () => undefined, - ); - - manager.scrollReset(); - - expect(callback).toBeCalled(); - }); - - it('should toggle menu', () => { - const callback = vi.fn(); - const manager = new CardElementManager( - createCardAPI(), - createElement(), - () => undefined, - callback, - ); - - manager.toggleMenu(); - - expect(callback).toBeCalled(); - }); - - it('should update', () => { - const element = createElement(); - const manager = new CardElementManager( - createCardAPI(), - element, - () => undefined, - () => undefined, - ); - - manager.update(); - expect(element.requestUpdate).toBeCalled(); - }); - - it('should get hasUpdated', () => { - const element = createElement(); - element.hasUpdated = true; - const manager = new CardElementManager( - createCardAPI(), - element, - () => undefined, - () => undefined, - ); - - expect(manager.hasUpdated()).toBeTruthy(); - }); - - it('should get height', () => { - const element = createElement(); - element.getBoundingClientRect = vi.fn().mockReturnValue({ - width: 200, - height: 800, - }); - - const manager = new CardElementManager( - createCardAPI(), - element, - () => undefined, - () => undefined, - ); - - expect(manager.getCardHeight()).toBe(800); - }); - - it('should connect', () => { - const windowAddEventListener = vi.spyOn(global.window, 'addEventListener'); - - const addEventListener = vi.fn(); - const element = createElement(); - element.addEventListener = addEventListener; - - const api = createCardAPI(); - const manager = new CardElementManager( - api, - element, - () => undefined, - () => undefined, - ); - - manager.elementConnected(); - - expect(element.getAttribute('panel')).toBeNull(); - expect(api.getFullscreenManager().connect).toBeCalled(); - - expect(addEventListener).toBeCalledWith( - 'mousemove', - api.getInteractionManager().reportInteraction, - ); - expect(addEventListener).toBeCalledWith( - 'll-custom', - api.getActionsManager().handleActionEvent, - ); - expect(addEventListener).toBeCalledWith( - '@action', - api.getInteractionManager().reportInteraction, - ); - expect(windowAddEventListener).toBeCalledWith('location-changed', expect.anything()); - expect(windowAddEventListener).toBeCalledWith('popstate', expect.anything()); - }); - - it('should disconnect', () => { - const windowRemoveEventListener = vi.spyOn(global.window, 'removeEventListener'); - - const element = createElement(); - element.setAttribute('panel', ''); - - const removeEventListener = vi.fn(); - element.removeEventListener = removeEventListener; - - const api = createCardAPI(); - const manager = new CardElementManager( - api, - element, - () => undefined, - () => undefined, - ); - - manager.elementDisconnected(); - - expect(element.getAttribute('panel')).toBeNull(); - expect(api.getMediaLoadedInfoManager().clear).toBeCalled(); - expect(api.getFullscreenManager().disconnect).toBeCalled(); - - expect(removeEventListener).toBeCalledWith( - 'mousemove', - api.getInteractionManager().reportInteraction, - ); - expect(removeEventListener).toBeCalledWith( - 'll-custom', - api.getActionsManager().handleActionEvent, - ); - expect(removeEventListener).toBeCalledWith( - '@action', - api.getInteractionManager().reportInteraction, - ); - expect(windowRemoveEventListener).toBeCalledWith( - 'location-changed', - expect.anything(), - ); - expect(windowRemoveEventListener).toBeCalledWith('popstate', expect.anything()); - }); -}); diff --git a/tests/utils/card-controller/conditions-manager.test.ts b/tests/utils/card-controller/conditions-manager.test.ts deleted file mode 100644 index e0c65618..00000000 --- a/tests/utils/card-controller/conditions-manager.test.ts +++ /dev/null @@ -1,363 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; -import { FrigateCardCondition } from '../../../src/config/types'; -import { - ConditionEvaluateRequestEvent, - ConditionsManager, - evaluateConditionViaEvent, - getOverriddenConfig, - getOverridesByKey, -} from '../../../src/utils/card-controller/conditions-manager'; -import { - createCardAPI, - createCondition, - createConfig, - createStateEntity, -} from '../../test-utils'; - -// @vitest-environment jsdom -describe('ConditionEvaluateRequestEvent', () => { - it('should construct', () => { - const condition = createCondition({ fullscreen: true }); - const event = new ConditionEvaluateRequestEvent(condition, { - bubbles: true, - composed: true, - }); - - expect(event.type).toBe('frigate-card:condition:evaluate'); - expect(event.condition).toBe(condition); - expect(event.bubbles).toBeTruthy(); - expect(event.composed).toBeTruthy(); - }); -}); - -describe('evaluateConditionViaEvent', () => { - it('should evaluate true without condition', () => { - const element = document.createElement('div'); - expect(evaluateConditionViaEvent(element)).toBeTruthy(); - }); - it('should dispatch event with condition and evaluate true', () => { - const element = document.createElement('div'); - const condition = createCondition({ fullscreen: true }); - const handler = vi.fn().mockImplementation((ev: ConditionEvaluateRequestEvent) => { - expect(ev.condition).toBe(condition); - ev.evaluation = true; - }); - element.addEventListener('frigate-card:condition:evaluate', handler); - - expect(evaluateConditionViaEvent(element, condition)).toBeTruthy(); - expect(handler).toBeCalled(); - }); - it('should dispatch event with condition and evaluate false', () => { - const element = document.createElement('div'); - const condition = createCondition({ fullscreen: true }); - const handler = vi.fn().mockImplementation((ev: ConditionEvaluateRequestEvent) => { - expect(ev.condition).toBe(condition); - ev.evaluation = false; - }); - element.addEventListener('frigate-card:condition:evaluate', handler); - - expect(evaluateConditionViaEvent(element, condition)).toBeFalsy(); - expect(handler).toBeCalled(); - }); - it('should dispatch event evaluate false if no evaluation', () => { - const element = document.createElement('div'); - const condition = createCondition({ fullscreen: true }); - const handler = vi.fn(); - element.addEventListener('frigate-card:condition:evaluate', handler); - - expect(evaluateConditionViaEvent(element, condition)).toBeFalsy(); - expect(handler).toBeCalled(); - }); -}); - -describe('getOverriddenConfig', () => { - const config = { - menu: { - style: 'none', - }, - }; - const overrides = [ - { - overrides: { - menu: { - style: 'above', - }, - }, - conditions: { - 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', () => { - const manager = new ConditionsManager(createCardAPI()); - manager.setState({ fullscreen: true }); - - expect(getOverriddenConfig(manager, config)).toBe(config); - }); -}); - -describe('getOverridesByKey', () => { - const condition = { - fullscreen: true, - }; - const override = { - menu: { - style: 'above', - }, - }; - const overrides = [ - { - overrides: override, - conditions: condition, - }, - ]; - - it('should get overrides', () => { - expect(getOverridesByKey('menu', overrides)).toEqual([ - { conditions: condition, overrides: { style: 'above' } }, - ]); - }); - - it('should get no overrides', () => { - expect(getOverridesByKey('live', overrides)).toEqual([]); - }); - - it('should get no overrides when undefined', () => { - expect(getOverridesByKey('live')).toEqual([]); - }); -}); - -describe('ConditionsManager', () => { - const config = { - type: 'custom:frigate-card', - cameras: [], - elements: [ - { - type: 'custom:frigate-card-conditional', - conditions: { - fullscreen: true, - }, - elements: [ - { - type: 'custom:nested-unknown-object', - unknown_key: { - type: 'custom:frigate-card-conditional', - conditions: { - media_query: 'media query goes here', - }, - elements: [], - }, - }, - ], - }, - ], - overrides: [ - { - overrides: { - menu: { - style: 'overlay', - }, - }, - conditions: { - fullscreen: true, - state: [ - { - entity: 'binary_sensor.foo', - state: 'on', - }, - ], - }, - }, - ], - }; - - afterEach(() => { - vi.restoreAllMocks(); - }); - - it('should get epoch', () => { - const manager = new ConditionsManager(createCardAPI()); - const epoch_1 = manager.getEpoch(); - expect(epoch_1).toEqual({ manager: manager }); - - manager.setState({ fullscreen: true }); - - const epoch_2 = manager.getEpoch(); - expect(epoch_2).toEqual({ manager: manager }); - - // Since the state was set the wrappers should be different. - expect(epoch_1).not.toBe(epoch_2); - }); - - it('should not return hasHAStateConditions without HA state conditions', () => { - const manager = new ConditionsManager(createCardAPI()); - expect(manager.hasHAStateConditions()).toBeFalsy(); - }); - - it('should return hasHAStateConditions with HA state conditions', () => { - vi.spyOn(window, 'matchMedia').mockReturnValueOnce({ - matches: false, - addEventListener: vi.fn(), - } as unknown as MediaQueryList); - const api = createCardAPI(); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig(config)); - const manager = new ConditionsManager(api); - - manager.setConditionsFromConfig(); - - expect(manager.hasHAStateConditions()).toBeTruthy(); - }); - - it('should evaluate conditions with a view', () => { - const manager = new ConditionsManager(createCardAPI()); - const condition = { view: ['foo'] }; - expect(manager.evaluateCondition(condition)).toBeFalsy(); - manager.setState({ view: 'foo' }); - expect(manager.evaluateCondition(condition)).toBeTruthy(); - }); - - it('should evaluate conditions with fullscreen', () => { - const manager = new ConditionsManager(createCardAPI()); - const condition = { fullscreen: true }; - expect(manager.evaluateCondition(condition)).toBeFalsy(); - manager.setState({ fullscreen: true }); - expect(manager.evaluateCondition(condition)).toBeTruthy(); - manager.setState({ fullscreen: false }); - expect(manager.evaluateCondition(condition)).toBeFalsy(); - }); - - it('should evaluate conditions with expand', () => { - const manager = new ConditionsManager(createCardAPI()); - const condition = { expand: true }; - expect(manager.evaluateCondition(condition)).toBeFalsy(); - manager.setState({ expand: true }); - expect(manager.evaluateCondition(condition)).toBeTruthy(); - manager.setState({ expand: false }); - expect(manager.evaluateCondition(condition)).toBeFalsy(); - }); - - it('should evaluate conditions with camera', () => { - const manager = new ConditionsManager(createCardAPI()); - const condition = { camera: ['bar'] }; - expect(manager.evaluateCondition(condition)).toBeFalsy(); - manager.setState({ camera: 'bar' }); - expect(manager.evaluateCondition(condition)).toBeTruthy(); - manager.setState({ camera: 'will-not-match' }); - expect(manager.evaluateCondition(condition)).toBeFalsy(); - }); - - it('should evaluate conditions with ha state positive check', () => { - const manager = new ConditionsManager(createCardAPI()); - const condition = { - state: [ - { - entity: 'binary_sensor.foo', - state: 'on', - }, - ], - }; - expect(manager.evaluateCondition(condition)).toBeFalsy(); - manager.setState({ state: { 'binary_sensor.foo': createStateEntity() } }); - expect(manager.evaluateCondition(condition)).toBeTruthy(); - manager.setState({ - state: { 'binary_sensor.foo': createStateEntity({ state: 'off' }) }, - }); - expect(manager.evaluateCondition(condition)).toBeFalsy(); - }); - - it('should evaluate conditions with ha state negative check', () => { - const manager = new ConditionsManager(createCardAPI()); - const condition = { - state: [ - { - entity: 'binary_sensor.foo', - state_not: 'on', - }, - ], - }; - expect(manager.evaluateCondition(condition)).toBeFalsy(); - manager.setState({ state: { 'binary_sensor.foo': createStateEntity() } }); - expect(manager.evaluateCondition(condition)).toBeFalsy(); - manager.setState({ - state: { 'binary_sensor.foo': createStateEntity({ state: 'off' }) }, - }); - expect(manager.evaluateCondition(condition)).toBeTruthy(); - }); - - it('should evaluate conditions with media_loaded', () => { - const manager = new ConditionsManager(createCardAPI()); - const condition = { media_loaded: true }; - expect(manager.evaluateCondition(condition)).toBeFalsy(); - manager.setState({ media_loaded: true }); - expect(manager.evaluateCondition(condition)).toBeTruthy(); - manager.setState({ media_loaded: false }); - expect(manager.evaluateCondition(condition)).toBeFalsy(); - }); - - it('should evaluate conditions with media query', () => { - vi.spyOn(window, 'matchMedia') - .mockReturnValueOnce({ matches: true }) - .mockReturnValueOnce({ matches: false }); - - const manager = new ConditionsManager(createCardAPI()); - const condition = { media_query: 'whatever' }; - expect(manager.evaluateCondition(condition)).toBeTruthy(); - expect(manager.evaluateCondition(condition)).toBeFalsy(); - }); - - it('should trigger on changes to media query conditions', () => { - const addEventListener = vi.fn(); - const removeEventListener = vi.fn(); - vi.spyOn(window, 'matchMedia').mockReturnValueOnce({ - matches: true, - addEventListener: addEventListener, - removeEventListener: removeEventListener, - } as unknown as MediaQueryList); - const api = createCardAPI(); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig(config)); - const callback = vi.fn(); - const manager = new ConditionsManager(api, callback); - - manager.setConditionsFromConfig(); - - expect(addEventListener).toHaveBeenCalledWith('change', expect.anything()); - - // Call the media query callback and use it to pretend a match happened. The - // callback is the 0th mock innvocation and the 1st argument. - addEventListener.mock.calls[0][1](); - - // This should result in a callback to our state listener. - expect(callback).toBeCalled(); - - // Remove the conditions, which should remove the media query listener. - manager.removeConditions(); - expect(removeEventListener).toBeCalled(); - }); - - it('should evaluate conditions with display mode', () => { - const manager = new ConditionsManager(createCardAPI()); - const condition: FrigateCardCondition = { display_mode: 'grid' }; - expect(manager.evaluateCondition(condition)).toBeFalsy(); - manager.setState({ displayMode: 'grid' }); - expect(manager.evaluateCondition(condition)).toBeTruthy(); - manager.setState({ displayMode: 'single' }); - expect(manager.evaluateCondition(condition)).toBeFalsy(); - }); -}); diff --git a/tests/utils/card-controller/config-manager.test.ts b/tests/utils/card-controller/config-manager.test.ts deleted file mode 100644 index dcabf1c3..00000000 --- a/tests/utils/card-controller/config-manager.test.ts +++ /dev/null @@ -1,304 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { ZodError } from 'zod'; -import { frigateCardConfigSchema } from '../../../src/config/types'; -import { getOverriddenConfig } from '../../../src/utils/card-controller/conditions-manager'; -import { ConfigManager } from '../../../src/utils/card-controller/config-manager'; -import { InitializationAspect } from '../../../src/utils/card-controller/initialization-manager'; -import { createCardAPI, createConfig } from '../../test-utils'; - -vi.mock('../../../src/utils/card-controller/conditions-manager.js'); - -describe('ConfigManager', () => { - beforeEach(() => { - vi.restoreAllMocks(); - }); - - describe('should handle error when', () => { - it('no input', () => { - const manager = new ConfigManager(createCardAPI()); - expect(() => manager.setConfig()).toThrowError(/Invalid configuration/); - }); - - it('invalid configuration', () => { - const spy = vi.spyOn(frigateCardConfigSchema, 'safeParse').mockReturnValue({ - success: false, - error: new ZodError([]), - }); - - const manager = new ConfigManager(createCardAPI()); - expect(() => manager.setConfig({})).toThrowError( - 'Invalid configuration: No location hint available (bad or missing type?)', - ); - - spy.mockRestore(); - }); - - it('invalid configuration with hint', () => { - const manager = new ConfigManager(createCardAPI()); - expect(() => manager.setConfig({})).toThrowError( - 'Invalid configuration: [\n "cameras",\n "type"\n]', - ); - }); - - it('upgradeable', () => { - const manager = new ConfigManager(createCardAPI()); - expect(() => - manager.setConfig({ - cameras: [ - { - frigate: { - label: 'foo', - }, - }, - ], - }), - ).toThrowError( - 'An automated card configuration upgrade is ' + - 'available, please visit the visual card editor. ' + - 'Invalid configuration: [\n "type"\n]', - ); - }); - }); - - it('should have initial state', () => { - const manager = new ConfigManager(createCardAPI()); - - expect(manager.getConfig()).toBeNull(); - expect(manager.getNonOverriddenConfig()).toBeNull(); - expect(manager.getRawConfig()).toBeNull(); - }); - - it('should successfully parse basic config', () => { - const api = createCardAPI(); - const manager = new ConfigManager(api); - const config = { - type: 'custom:frigate-card', - cameras: [{ camera_entity: 'camera.office' }], - }; - - manager.setConfig(config); - - expect(manager.hasConfig()).toBeTruthy() - expect(manager.getRawConfig()).toBe(config); - - // Verify at least the camera is set. - expect(manager.getConfig()?.cameras[0].camera_entity).toBe('camera.office'); - - // Verify at least one default was set. - expect(manager.getConfig()?.menu.alignment).toBe('left'); - - // Verify appropriate API calls are made. - expect(api.getConditionsManager().setConditionsFromConfig).toBeCalled(); - expect(api.getConditionsManager().setState).toBeCalledWith({ - view: undefined, - displayMode: undefined, - camera: undefined, - }); - expect(api.getMediaLoadedInfoManager().clear).toBeCalled(); - expect(api.getViewManager().reset).toBeCalled(); - expect(api.getMessageManager().reset).toBeCalled(); - expect(api.getAutomationsManager().setAutomationsFromConfig).toBeCalled(); - expect(api.getStyleManager().setPerformance).toBeCalled(); - expect(api.getCardElementManager().update).toBeCalled(); - }); - - it('should apply low performance defaults', () => { - const manager = new ConfigManager(createCardAPI()); - const config = { - type: 'custom:frigate-card', - cameras: [{ camera_entity: 'camera.office' }], - performance: { profile: 'low' }, - }; - - manager.setConfig(config); - - // Verify at least one low performance default. - expect(manager.getConfig()?.live.draggable).toBeFalsy(); - }); - - it('should skip identical configs', () => { - const api = createCardAPI(); - const manager = new ConfigManager(api); - const config = { - type: 'custom:frigate-card', - cameras: [{ camera_entity: 'camera.office' }], - }; - - manager.setConfig(config); - expect(api.getViewManager().reset).toBeCalled(); - - vi.mocked(api.getViewManager().reset).mockClear(); - - manager.setConfig(config); - expect(api.getViewManager().reset).not.toBeCalled(); - }); - - it('should get card wide config', () => { - const api = createCardAPI(); - const manager = new ConfigManager(api); - const config = { - type: 'custom:frigate-card', - cameras: [{ camera_entity: 'camera.office' }], - debug: { - logging: true, - }, - performance: { - profile: 'low', - }, - }; - - manager.setConfig(config); - - expect(manager.getCardWideConfig()).toEqual({ - debug: { - logging: true, - }, - performance: { - features: { - animated_progress_indicator: false, - media_chunk_size: 10, - }, - profile: 'low', - style: { - border_radius: false, - box_shadow: false, - }, - }, - }); - }); - - it('should ignore overrides without a config', () => { - const api = createCardAPI(); - const manager = new ConfigManager(api); - - manager.computeOverrideConfig(); - - expect(manager.getConfig()).toBeNull(); - expect(api.getStyleManager().setMinMaxHeight).not.toBeCalled(); - }); - - it('should ignore overrides with same config', () => { - const api = createCardAPI(); - const manager = new ConfigManager(api); - const config = { - type: 'custom:frigate-card', - cameras: [{ camera_entity: 'camera.office' }], - }; - vi.mocked(getOverriddenConfig).mockReturnValue(config); - - manager.setConfig(config); - expect(api.getStyleManager().setMinMaxHeight).toBeCalled(); - - vi.mocked(api.getStyleManager().setMinMaxHeight).mockClear(); - manager.computeOverrideConfig(); - - expect(api.getStyleManager().setMinMaxHeight).not.toBeCalled(); - }); - - it('should override', () => { - const api = createCardAPI(); - const manager = new ConfigManager(api); - const config_1 = { - type: 'custom:frigate-card', - cameras: [{ camera_entity: 'camera.office' }], - }; - manager.setConfig(config_1); - vi.mocked(api.getStyleManager().setMinMaxHeight).mockClear(); - - const config_2 = { - type: 'custom:frigate-card', - cameras: [{ camera_entity: 'camera.kitchen' }], - }; - vi.mocked(getOverriddenConfig).mockReturnValue(config_2); - manager.computeOverrideConfig(); - - expect(api.getStyleManager().setMinMaxHeight).toBeCalled(); - expect(api.getCardElementManager().update).toBeCalled(); - expect(manager.getConfig()).not.toEqual(manager.getNonOverriddenConfig()); - }); - - describe('should uninitialize on override', () => { - it('cameras', () => { - const api = createCardAPI(); - const manager = new ConfigManager(api); - const config_1 = { - type: 'custom:frigate-card', - cameras: [{ camera_entity: 'camera.office' }], - }; - vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_1)); - - manager.setConfig(config_1); - expect(api.getInitializationManager().uninitialize).not.toBeCalled(); - - const config_2 = { - type: 'custom:frigate-card', - cameras: [{ camera_entity: 'camera.kitchen' }], - }; - vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_2)); - manager.computeOverrideConfig(); - - expect(api.getInitializationManager().uninitialize).toBeCalledWith( - InitializationAspect.CAMERAS, - ); - }); - - it('cameras_global', () => { - const api = createCardAPI(); - const manager = new ConfigManager(api); - const config_1 = { - type: 'custom:frigate-card', - cameras: [{ camera_entity: 'camera.office' }], - }; - vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_1)); - - manager.setConfig(config_1); - expect(api.getInitializationManager().uninitialize).not.toBeCalled(); - - const config_2 = { - ...config_1, - cameras_global: { - live_provider: 'jsmpeg' - } - }; - vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_2)); - manager.computeOverrideConfig(); - - expect(api.getInitializationManager().uninitialize).toBeCalledWith( - InitializationAspect.CAMERAS, - ); - }); - - it('live.microphone.always_connected', () => { - const api = createCardAPI(); - const manager = new ConfigManager(api); - const config_1 = { - type: 'custom:frigate-card', - cameras: [{ camera_entity: 'camera.office' }], - live: { - microphone: { - always_connected: false - } - } - }; - vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_1)); - - manager.setConfig(config_1); - expect(api.getInitializationManager().uninitialize).not.toBeCalled(); - - const config_2 = { - ...config_1, - live: { - microphone: { - always_connected: true - } - } - }; - vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_2)); - manager.computeOverrideConfig(); - - expect(api.getInitializationManager().uninitialize).toBeCalledWith( - InitializationAspect.MICROPHONE_CONNECT, - ); - }); - }); -}); diff --git a/tests/utils/card-controller/controller.test.ts b/tests/utils/card-controller/controller.test.ts deleted file mode 100644 index ca200db7..00000000 --- a/tests/utils/card-controller/controller.test.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { CameraManager } from '../../../src/camera-manager/manager'; -import { FrigateCardEditor } from '../../../src/editor'; -import { ActionsManager } from '../../../src/utils/card-controller/actions-manager'; -import { AutoUpdateManager } from '../../../src/utils/card-controller/auto-update-manager'; -import { AutomationsManager } from '../../../src/utils/card-controller/automations-manager'; -import { CameraURLManager } from '../../../src/utils/card-controller/camera-url-manager'; -import { - CardElementManager, - CardHTMLElement, -} from '../../../src/utils/card-controller/card-element-manager'; -import { ConditionsManager } from '../../../src/utils/card-controller/conditions-manager'; -import { ConfigManager } from '../../../src/utils/card-controller/config-manager'; -import { CardController } from '../../../src/utils/card-controller/controller'; -import { DownloadManager } from '../../../src/utils/card-controller/download-manager'; -import { ExpandManager } from '../../../src/utils/card-controller/expand-manager'; -import { FullscreenManager } from '../../../src/utils/card-controller/fullscreen-manager'; -import { HASSManager } from '../../../src/utils/card-controller/hass-manager'; -import { InitializationManager } from '../../../src/utils/card-controller/initialization-manager'; -import { InteractionManager } from '../../../src/utils/card-controller/interaction-manager'; -import { MediaLoadedInfoManager } from '../../../src/utils/card-controller/media-info-manager'; -import { MediaPlayerManager } from '../../../src/utils/card-controller/media-player-manager'; -import { MessageManager } from '../../../src/utils/card-controller/message-manager'; -import { MicrophoneManager } from '../../../src/utils/card-controller/microphone-manager'; -import { QueryStringManager } from '../../../src/utils/card-controller/query-string-manager'; -import { StyleManager } from '../../../src/utils/card-controller/style-manager'; -import { TriggersManager } from '../../../src/utils/card-controller/triggers-manager'; -import { ViewManager } from '../../../src/utils/card-controller/view-manager'; -import { EntityRegistryManager } from '../../../src/utils/ha/entity-registry'; -import { ResolvedMediaCache } from '../../../src/utils/ha/resolved-media'; - -vi.mock('../../../src/camera-manager/manager'); -vi.mock('../../../src/utils/card-controller/actions-manager'); -vi.mock('../../../src/utils/card-controller/auto-update-manager'); -vi.mock('../../../src/utils/card-controller/automations-manager'); -vi.mock('../../../src/utils/card-controller/camera-url-manager'); -vi.mock('../../../src/utils/card-controller/card-element-manager'); -vi.mock('../../../src/utils/card-controller/conditions-manager'); -vi.mock('../../../src/utils/card-controller/config-manager'); -vi.mock('../../../src/utils/card-controller/download-manager'); -vi.mock('../../../src/utils/card-controller/expand-manager'); -vi.mock('../../../src/utils/card-controller/fullscreen-manager'); -vi.mock('../../../src/utils/card-controller/hass-manager'); -vi.mock('../../../src/utils/card-controller/initialization-manager'); -vi.mock('../../../src/utils/card-controller/interaction-manager'); -vi.mock('../../../src/utils/card-controller/media-info-manager'); -vi.mock('../../../src/utils/card-controller/media-player-manager'); -vi.mock('../../../src/utils/card-controller/message-manager'); -vi.mock('../../../src/utils/card-controller/microphone-manager'); -vi.mock('../../../src/utils/card-controller/query-string-manager'); -vi.mock('../../../src/utils/card-controller/style-manager'); -vi.mock('../../../src/utils/card-controller/triggers-manager'); -vi.mock('../../../src/utils/card-controller/view-manager'); -vi.mock('../../../src/utils/ha/entity-registry'); -vi.mock('../../../src/utils/ha/resolved-media'); - -const createCardElement = (): CardHTMLElement => { - const element = document.createElement('div') as unknown as CardHTMLElement; - element.addController = vi.fn(); - return element; -}; - -const createController = (): CardController => { - return new CardController(createCardElement(), vi.fn(), vi.fn(), vi.fn()); -}; - -// @vitest-environment jsdom -describe('CardController', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('should construct correctly', () => { - const element = createCardElement(); - const scrollCallback = vi.fn(); - const menuToggleCallback = vi.fn(); - const conditionListener = vi.fn(); - - const manager = new CardController( - element, - scrollCallback, - menuToggleCallback, - conditionListener, - ); - - expect(ConditionsManager).toBeCalledWith(manager, conditionListener); - expect(CardElementManager).toBeCalledWith( - manager, - element, - scrollCallback, - menuToggleCallback, - ); - }); - - describe('accessors', () => { - it('getActionsManager', () => { - expect(createController().getActionsManager()).toBe( - vi.mocked(ActionsManager).mock.instances[0], - ); - }); - - it('getAutomationsManager', () => { - expect(createController().getAutomationsManager()).toBe( - vi.mocked(AutomationsManager).mock.instances[0], - ); - }); - - it('getAutoUpdateManager', () => { - expect(createController().getAutoUpdateManager()).toBe( - vi.mocked(AutoUpdateManager).mock.instances[0], - ); - }); - - it('getCameraManager', () => { - expect(createController().getCameraManager()).toBe( - vi.mocked(CameraManager).mock.instances[0], - ); - }); - - it('getCameraURLManager', () => { - expect(createController().getCameraURLManager()).toBe( - vi.mocked(CameraURLManager).mock.instances[0], - ); - }); - - it('getCardElementManager', () => { - expect(createController().getCardElementManager()).toBe( - vi.mocked(CardElementManager).mock.instances[0], - ); - }); - - it('getConditionsManager', () => { - expect(createController().getConditionsManager()).toBe( - vi.mocked(ConditionsManager).mock.instances[0], - ); - }); - - it('getConfigElement', async () => { - expect((await CardController.getConfigElement()) instanceof FrigateCardEditor); - }); - - it('getConfigManager', () => { - expect(createController().getConfigManager()).toBe( - vi.mocked(ConfigManager).mock.instances[0], - ); - }); - - it('getDownloadManager', () => { - expect(createController().getDownloadManager()).toBe( - vi.mocked(DownloadManager).mock.instances[0], - ); - }); - - it('getEntityRegistryManager', () => { - expect(createController().getEntityRegistryManager()).toBe( - vi.mocked(EntityRegistryManager).mock.instances[0], - ); - }); - - it('getExpandManager', () => { - expect(createController().getExpandManager()).toBe( - vi.mocked(ExpandManager).mock.instances[0], - ); - }); - - it('getFullscreenManager', () => { - expect(createController().getFullscreenManager()).toBe( - vi.mocked(FullscreenManager).mock.instances[0], - ); - }); - - it('getHASSManager', () => { - expect(createController().getHASSManager()).toBe( - vi.mocked(HASSManager).mock.instances[0], - ); - }); - - it('getInitializationManager', () => { - expect(createController().getInitializationManager()).toBe( - vi.mocked(InitializationManager).mock.instances[0], - ); - }); - - it('getInteractionManager', () => { - expect(createController().getInteractionManager()).toBe( - vi.mocked(InteractionManager).mock.instances[0], - ); - }); - - it('getMediaLoadedInfoManager', () => { - expect(createController().getMediaLoadedInfoManager()).toBe( - vi.mocked(MediaLoadedInfoManager).mock.instances[0], - ); - }); - - it('getMediaPlayerManager', () => { - expect(createController().getMediaPlayerManager()).toBe( - vi.mocked(MediaPlayerManager).mock.instances[0], - ); - }); - - it('getMessageManager', () => { - expect(createController().getMessageManager()).toBe( - vi.mocked(MessageManager).mock.instances[0], - ); - }); - - it('getMicrophoneManager', () => { - expect(createController().getMicrophoneManager()).toBe( - vi.mocked(MicrophoneManager).mock.instances[0], - ); - }); - - it('getResolvedMediaCache', () => { - expect(createController().getResolvedMediaCache()).toBe( - vi.mocked(ResolvedMediaCache).mock.instances[0], - ); - }); - - describe('getStubConfig', () => { - it('with camera entities', () => { - expect( - CardController.getStubConfig(['camera.office', 'binary_sensor.motion']), - ).toEqual({ - cameras: [{ camera_entity: 'camera.office' }], - }); - }); - - it('without camera entities', () => { - expect(CardController.getStubConfig(['binary_sensor.motion'])).toEqual({ - cameras: [{ camera_entity: 'camera.demo' }], - }); - }); - }); - - it('getQueryStringManager', () => { - expect(createController().getQueryStringManager()).toBe( - vi.mocked(QueryStringManager).mock.instances[0], - ); - }); - - it('getStyleManager', () => { - expect(createController().getStyleManager()).toBe( - vi.mocked(StyleManager).mock.instances[0], - ); - }); - - it('getTriggersManager', () => { - expect(createController().getTriggersManager()).toBe( - vi.mocked(TriggersManager).mock.instances[0], - ); - }); - - it('getViewManager', () => { - expect(createController().getViewManager()).toBe( - vi.mocked(ViewManager).mock.instances[0], - ); - }); - }); - - describe('handlers', () => { - it('hostConnected', () => { - createController().hostConnected(); - expect( - vi.mocked(CardElementManager).mock.instances[0].elementConnected, - ).toBeCalled(); - }); - - it('hostDisconnected', () => { - createController().hostDisconnected(); - expect( - vi.mocked(CardElementManager).mock.instances[0].elementDisconnected, - ).toBeCalled(); - }); - }); -}); diff --git a/tests/utils/card-controller/download-manager.test.ts b/tests/utils/card-controller/download-manager.test.ts deleted file mode 100644 index 88ca0237..00000000 --- a/tests/utils/card-controller/download-manager.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { mock } from 'vitest-mock-extended'; -import { FrigateCardMediaPlayer } from '../../../src/types'; -import { DownloadManager } from '../../../src/utils/card-controller/download-manager'; -import { downloadMedia, downloadURL } from '../../../src/utils/download.js'; -import { - createCardAPI, - createHASS, - createMediaLoadedInfo, - createViewWithMedia, -} from '../../test-utils'; - -vi.mock('../../../src/utils/download.js'); - -describe('DownloadManager.downloadViewerMedia', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - - it('should download', async () => { - const api = createCardAPI(); - vi.mocked(api.getViewManager().getView).mockReturnValue(createViewWithMedia()); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); - const manager = new DownloadManager(api); - - expect(await manager.downloadViewerMedia()).toBeTruthy(); - expect(downloadMedia).toBeCalledWith( - api.getHASSManager().getHASS(), - api.getCameraManager(), - api.getViewManager().getView()?.queryResults?.getResult(0), - ); - }); - - it('should not download due to exception thrown', async () => { - const api = createCardAPI(); - vi.mocked(api.getViewManager().getView).mockReturnValue(createViewWithMedia()); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); - const manager = new DownloadManager(api); - - const error = new Error(); - vi.mocked(downloadMedia).mockRejectedValue(error); - - expect(await manager.downloadViewerMedia()).toBeFalsy(); - expect(api.getMessageManager().setErrorIfHigherPriority).toBeCalledWith(error); - }); - - it('should not download without hass', async () => { - const api = createCardAPI(); - vi.mocked(api.getViewManager().getView).mockReturnValue(createViewWithMedia()); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null); - const manager = new DownloadManager(api); - - expect(await manager.downloadViewerMedia()).toBeFalsy(); - expect(downloadMedia).not.toBeCalled(); - }); -}); - -describe('DownloadManager.downloadScreenshot', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('with url', async () => { - const api = createCardAPI(); - const player = mock(); - player.getScreenshotURL.mockResolvedValue('http://screenshot'); - - vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue( - createMediaLoadedInfo({ - player: player, - }), - ); - const manager = new DownloadManager(api); - await manager.downloadScreenshot(); - - expect(downloadURL).toBeCalledWith('http://screenshot', 'screenshot.jpg'); - }); - - it('without url', async () => { - const api = createCardAPI(); - const player = mock(); - player.getScreenshotURL.mockResolvedValue(null); - - vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue( - createMediaLoadedInfo({ - player: player, - }), - ); - const manager = new DownloadManager(api); - await manager.downloadScreenshot(); - - expect(downloadURL).not.toBeCalled(); - }); -}); diff --git a/tests/utils/card-controller/expand-manager.test.ts b/tests/utils/card-controller/expand-manager.test.ts deleted file mode 100644 index 4679ab9d..00000000 --- a/tests/utils/card-controller/expand-manager.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; -import { ExpandManager } from '../../../src/utils/card-controller/expand-manager'; -import { createCardAPI } from '../../test-utils'; - -describe('ExpandManager', () => { - it('should construct', () => { - const api = createCardAPI(); - const manager = new ExpandManager(api); - expect(manager.isExpanded()).toBeFalsy(); - }); - - it('should set expanded', () => { - const api = createCardAPI(); - vi.mocked(api.getFullscreenManager().isInFullscreen).mockReturnValue(true); - const manager = new ExpandManager(api); - - manager.setExpanded(true); - - expect(manager.isExpanded()).toBeTruthy(); - expect(api.getFullscreenManager().stopFullscreen).toBeCalled(); - expect(api.getConditionsManager().setState).toBeCalledWith({ expand: true }); - expect(api.getCardElementManager().update).toBeCalled(); - }); - - it('should not exit fullscreen when not in fullscreen', () => { - const api = createCardAPI(); - vi.mocked(api.getFullscreenManager().isInFullscreen).mockReturnValue(false); - const manager = new ExpandManager(api); - - manager.setExpanded(true); - - expect(api.getFullscreenManager().stopFullscreen).not.toBeCalled(); - }); - - it('should toggle expanded', () => { - const api = createCardAPI(); - vi.mocked(api.getFullscreenManager().isInFullscreen).mockReturnValue(false); - const manager = new ExpandManager(api); - - manager.toggleExpanded(); - expect(manager.isExpanded()).toBeTruthy(); - - manager.toggleExpanded(); - expect(manager.isExpanded()).toBeFalsy(); - }); -}); diff --git a/tests/utils/card-controller/fullscreen-manager.test.ts b/tests/utils/card-controller/fullscreen-manager.test.ts deleted file mode 100644 index 93fe4c00..00000000 --- a/tests/utils/card-controller/fullscreen-manager.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -import screenfull from 'screenfull'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { FullscreenManager } from '../../../src/utils/card-controller/fullscreen-manager'; -import { createCardAPI } from '../../test-utils'; - -vi.mock('screenfull', () => ({ - default: { - exit: vi.fn(), - toggle: vi.fn(), - off: vi.fn(), - on: vi.fn(), - }, -})); - -const setScreenfulEnabled = (enabled: boolean): void => { - Object.defineProperty(screenfull, 'isEnabled', { value: enabled, writable: true }); -}; - -const setScreenfulFullscreen = (fullscreen: boolean): void => { - Object.defineProperty(screenfull, 'isFullscreen', { - value: fullscreen, - writable: true, - }); -}; - -// @vitest-environment jsdom -describe('FullscreenManager', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('should correctly determine whether in fullscreen', () => { - const manager = new FullscreenManager(createCardAPI()); - - setScreenfulEnabled(true); - setScreenfulFullscreen(true); - expect(manager.isInFullscreen()).toBeTruthy(); - - setScreenfulFullscreen(false); - expect(manager.isInFullscreen()).toBeFalsy(); - }); - - it('should toggle fullscreen', () => { - const toggle = vi.mocked(screenfull.toggle); - const element = document.createElement('div') - const api = createCardAPI(); - vi.mocked(api.getCardElementManager().getElement).mockReturnValue( - element, - ); - const manager = new FullscreenManager(api); - - manager.toggleFullscreen(); - - expect(toggle).toBeCalledWith(element); - }); - - it('should stop fullscreen', () => { - const manager = new FullscreenManager(createCardAPI()); - const exit = vi.mocked(screenfull.exit); - - manager.stopFullscreen(); - - expect(exit).toBeCalled(); - }); - - it('should disconnect', () => { - const manager = new FullscreenManager(createCardAPI()); - const off = vi.mocked(screenfull.off); - - setScreenfulEnabled(true); - - manager.disconnect(); - - expect(off).toBeCalledWith('change', expect.anything()); - }); - - it('should not disconnect when screenfull disabled', () => { - const manager = new FullscreenManager(createCardAPI()); - const off = vi.mocked(screenfull.off); - - setScreenfulEnabled(false); - - manager.disconnect(); - - expect(off).not.toBeCalled(); - }); - - it('should connect', () => { - const manager = new FullscreenManager(createCardAPI()); - const on = vi.mocked(screenfull.on); - - setScreenfulEnabled(true); - - manager.connect(); - - expect(on).toBeCalledWith('change', expect.anything()); - }); - - it('should not connect when screenfull disabled', () => { - const manager = new FullscreenManager(createCardAPI()); - const on = vi.mocked(screenfull.on); - - setScreenfulEnabled(false); - - manager.connect(); - - expect(on).not.toBeCalled(); - }); - - it('should make correct api calls on fullscreen change', () => { - const api = createCardAPI(); - const manager = new FullscreenManager(api); - const on = vi.mocked(screenfull.on); - - setScreenfulEnabled(true); - setScreenfulFullscreen(true); - - manager.connect(); - - expect(on).toBeCalled(); - on.mock.calls[0][1](new Event('fullscreen')); - - expect(api.getExpandManager().setExpanded).toBeCalledWith(false); - expect(api.getConditionsManager().setState).toBeCalledWith({ - fullscreen: true, - }); - expect(api.getCardElementManager().update).toBeCalled(); - }); -}); diff --git a/tests/utils/card-controller/hass-manager.test.ts b/tests/utils/card-controller/hass-manager.test.ts deleted file mode 100644 index 01fc6992..00000000 --- a/tests/utils/card-controller/hass-manager.test.ts +++ /dev/null @@ -1,279 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { HASSManager } from '../../../src/utils/card-controller/hass-manager'; -import { CardHASSAPI } from '../../../src/utils/card-controller/types'; -import { - createCameraConfig, - createCameraManager, - createCardAPI, - createConfig, - createHASS, - createStateEntity, - createView, -} from '../../test-utils'; - -vi.mock('../../../src/camera-manager/manager.js'); - -const createAPIWithoutMediaPlayers = (): CardHASSAPI => { - const api = createCardAPI(); - vi.mocked(api.getMediaPlayerManager().getMediaPlayers).mockReturnValue([]); - return api; -}; - -describe('HASSManager', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - - it('should have null hass on construction', () => { - const manager = new HASSManager(createCardAPI()); - expect(manager.getHASS()).toBeNull(); - }); - - it('should set light or dark mode upon setting hass', () => { - const api = createAPIWithoutMediaPlayers(); - const manager = new HASSManager(api); - - manager.setHASS(createHASS()); - - expect(api.getStyleManager().setLightOrDarkMode).toBeCalled(); - }); - - describe('should set condition manager state', () => { - it('positively', () => { - const api = createAPIWithoutMediaPlayers(); - const manager = new HASSManager(api); - vi.mocked(api.getConditionsManager().hasHAStateConditions).mockReturnValue(true); - - const states = { 'switch.foo': createStateEntity() }; - const hass = createHASS(states); - - manager.setHASS(hass); - - expect(api.getConditionsManager().setState).toBeCalledWith( - expect.objectContaining({ - state: states, - }), - ); - }); - - it('negatively', () => { - const api = createAPIWithoutMediaPlayers(); - const manager = new HASSManager(api); - vi.mocked(api.getConditionsManager().hasHAStateConditions).mockReturnValue(false); - - manager.setHASS(createHASS()); - - expect(api.getConditionsManager().setState).not.toBeCalled(); - }); - }); - - it('should update triggered cameras', () => { - const api = createAPIWithoutMediaPlayers(); - const manager = new HASSManager(api); - - const originalHASS = createHASS(); - manager.setHASS(originalHASS); - expect(api.getTriggersManager().updateTriggeredCameras).toBeCalledWith(null); - - manager.setHASS(createHASS()); - expect(api.getTriggersManager().updateTriggeredCameras).toBeCalledWith(originalHASS); - }); - - describe('should handle connection state change when', () => { - it('initially disconnected', () => { - const api = createAPIWithoutMediaPlayers(); - const manager = new HASSManager(api); - - const disconnectedHASS = createHASS(); - disconnectedHASS.connected = false; - - manager.setHASS(disconnectedHASS); - - expect(api.getMessageManager().setMessageIfHigherPriority).toBeCalledWith( - expect.objectContaining({ - message: 'Reconnecting', - icon: 'mdi:lan-disconnect', - type: 'connection', - dotdotdot: true, - }), - ); - }); - - it('disconnected', () => { - const api = createAPIWithoutMediaPlayers(); - const manager = new HASSManager(api); - - manager.setHASS(createHASS()); - - const disconnectedHASS = createHASS(); - disconnectedHASS.connected = false; - manager.setHASS(disconnectedHASS); - - expect(api.getMessageManager().setMessageIfHigherPriority).toBeCalledWith( - expect.objectContaining({ - message: 'Reconnecting', - icon: 'mdi:lan-disconnect', - type: 'connection', - dotdotdot: true, - }), - ); - }); - - it('reconnected', () => { - const api = createAPIWithoutMediaPlayers(); - const manager = new HASSManager(api); - - const disconnectedHASS = createHASS(); - disconnectedHASS.connected = false; - manager.setHASS(disconnectedHASS); - - const reconnectedHASS = createHASS(); - manager.setHASS(reconnectedHASS); - - expect(api.getViewManager().setViewDefault).toBeCalled(); - }); - }); - - describe('should set default view when', () => { - it('selected camera trigger entity changes', () => { - const cameraManager = createCameraManager({ - configs: new Map([ - [ - 'camera.foo', - createCameraConfig({ - triggers: { - entities: ['binary_sensor.motion'], - }, - }), - ], - ]), - }); - const api = createAPIWithoutMediaPlayers(); - vi.mocked(api.getCameraManager).mockReturnValue(cameraManager); - vi.mocked(api.getViewManager().getView).mockReturnValue( - createView({ - camera: 'camera.foo', - }), - ); - - const manager = new HASSManager(api); - const hass = createHASS({ - 'binary_sensor.motion': createStateEntity(), - }); - - manager.setHASS(hass); - - expect(api.getViewManager().setViewDefault).toBeCalled(); - }); - - it('selected camera is unknown', () => { - const cameraManager = createCameraManager({ - configs: new Map([ - [ - 'camera.foo', - createCameraConfig({ - triggers: { - entities: ['binary_sensor.motion'], - }, - }), - ], - ]), - }); - const api = createAPIWithoutMediaPlayers(); - vi.mocked(api.getCameraManager).mockReturnValue(cameraManager); - vi.mocked(api.getViewManager().getView).mockReturnValue( - createView({ - camera: 'camera.UNKNOWN', - }), - ); - - const manager = new HASSManager(api); - const hass = createHASS({ - 'binary_sensor.motion': createStateEntity(), - }); - - manager.setHASS(hass); - - expect(api.getViewManager().setViewDefault).not.toBeCalled(); - }); - - it('view.update_entities changes', () => { - const api = createAPIWithoutMediaPlayers(); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue( - createConfig({ - view: { - update_entities: ['sensor.force_default_view'], - }, - }), - ); - - const manager = new HASSManager(api); - const hass = createHASS({ - 'sensor.force_default_view': createStateEntity(), - }); - - manager.setHASS(hass); - - expect(api.getViewManager().setViewDefault).toBeCalled(); - }); - }); - - describe('should update card when', () => { - it('render entity changes', () => { - const api = createAPIWithoutMediaPlayers(); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue( - createConfig({ - view: { - render_entities: ['sensor.force_update'], - }, - }), - ); - - const manager = new HASSManager(api); - const hass = createHASS({ - 'sensor.force_update': createStateEntity(), - }); - - manager.setHASS(hass); - - expect(api.getCardElementManager().update).toBeCalled(); - }); - - it('media player entity changes', () => { - const api = createCardAPI(); - vi.mocked(api.getMediaPlayerManager().getMediaPlayers).mockReturnValue([ - 'media_player.foo', - ]); - - const manager = new HASSManager(api); - const hass = createHASS({ - 'media_player.foo': createStateEntity(), - }); - - manager.setHASS(hass); - - expect(api.getCardElementManager().update).toBeCalled(); - }); - }); - - it('set view default is not called when there is card interaction', () => { - const api = createAPIWithoutMediaPlayers(); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue( - createConfig({ - view: { - update_entities: ['sensor.force_default_view'], - }, - }), - ); - vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(true); - - const manager = new HASSManager(api); - const hass = createHASS({ - 'sensor.force_default_view': createStateEntity(), - }); - - manager.setHASS(hass); - - expect(api.getViewManager().setViewDefault).not.toBeCalled(); - }); -}); diff --git a/tests/utils/card-controller/initialization-manager.test.ts b/tests/utils/card-controller/initialization-manager.test.ts deleted file mode 100644 index 721f93bc..00000000 --- a/tests/utils/card-controller/initialization-manager.test.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { mock } from 'vitest-mock-extended'; -import { loadLanguages } from '../../../src/localize/localize'; -import { - InitializationAspect, - InitializationManager, -} from '../../../src/utils/card-controller/initialization-manager'; -import { sideLoadHomeAssistantElements } from '../../../src/utils/ha'; -import { Initializer } from '../../../src/utils/initializer/initializer'; -import { createCardAPI, createConfig, createHASS } from '../../test-utils'; - -vi.mock('../../../src/localize/localize.js'); -vi.mock('../../../src/utils/ha/index.js'); - -// @vitest-environment jsdom -describe('InitializationManager', () => { - beforeEach(async () => { - vi.resetAllMocks(); - }); - - it('should not be initialized', () => { - const manager = new InitializationManager(createCardAPI()); - expect(manager.isInitializedMandatory()).toBeFalsy(); - }); - - describe('should initialize mandatory', () => { - it('without hass', async () => { - const manager = new InitializationManager(createCardAPI()); - expect(await manager.initializeMandatory()).toBeFalsy(); - }); - - it('without config', async () => { - const api = createCardAPI(); - const manager = new InitializationManager(api); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); - expect(await manager.initializeMandatory()).toBeFalsy(); - }); - - it('successfully', async () => { - const api = createCardAPI(); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); - vi.mocked(api.getConfigManager().hasConfig).mockReturnValue(true); - vi.mocked(api.getMessageManager().hasMessage).mockReturnValue(false); - vi.mocked(api.getQueryStringManager().hasViewRelatedActions).mockReturnValue(false); - const manager = new InitializationManager(api); - - expect(await manager.initializeMandatory()).toBeTruthy(); - - expect(loadLanguages).toBeCalled(); - expect(sideLoadHomeAssistantElements).toBeCalled(); - expect(api.getCameraManager().initializeCamerasFromConfig).toBeCalled(); - expect(api.getViewManager().setViewDefault).toBeCalled(); - expect(api.getCardElementManager().update).toBeCalled(); - }); - - it('successfully with querystring view', async () => { - const api = createCardAPI(); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); - vi.mocked(api.getConfigManager().hasConfig).mockReturnValue(true); - vi.mocked(api.getMessageManager().hasMessage).mockReturnValue(false); - vi.mocked(api.getQueryStringManager().hasViewRelatedActions).mockReturnValue(true); - const manager = new InitializationManager(api); - - expect(await manager.initializeMandatory()).toBeTruthy(); - - expect(api.getQueryStringManager().executeViewRelated).toBeCalled(); - }); - - it('with message set during initialization', async () => { - const api = createCardAPI(); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); - vi.mocked(api.getConfigManager().hasConfig).mockReturnValue(true); - vi.mocked(api.getMessageManager().hasMessage).mockReturnValue(true); - vi.mocked(api.getQueryStringManager().hasViewRelatedActions).mockReturnValue(false); - const manager = new InitializationManager(api); - - expect(await manager.initializeMandatory()).toBeTruthy(); - - expect(api.getViewManager().setViewByParameters).not.toBeCalled(); - expect(api.getViewManager().setViewDefault).not.toBeCalled(); - }); - - it('with languages and side load elements in progress', async () => { - const api = createCardAPI(); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); - const initializer = mock(); - const manager = new InitializationManager(api, initializer); - initializer.initializeMultipleIfNecessary.mockResolvedValue(false); - - expect(await manager.initializeMandatory()).toBeFalsy(); - }); - - it('with cameras in progress', async () => { - const api = createCardAPI(); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); - vi.mocked(api.getConfigManager().hasConfig).mockReturnValue(true); - - const initializer = mock(); - const manager = new InitializationManager(api, initializer); - initializer.initializeMultipleIfNecessary.mockResolvedValue(true); - initializer.initializeIfNecessary.mockResolvedValue(false); - - expect(await manager.initializeMandatory()).toBeFalsy(); - }); - }); - - describe('should initialize background', () => { - it('without hass and config', async () => { - const manager = new InitializationManager(createCardAPI()); - expect(await manager.initializeBackgroundIfNecessary()).toBeFalsy(); - }); - - it('successfully with minimal initializers', async () => { - const api = createCardAPI(); - const manager = new InitializationManager(api); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue( - createConfig({ - menu: { - buttons: { - media_player: { - enabled: false, - }, - }, - }, - live: { - microphone: { - always_connected: false, - }, - }, - }), - ); - - expect(await manager.initializeBackgroundIfNecessary()).toBeTruthy(); - expect(api.getMediaPlayerManager().initialize).not.toBeCalled(); - expect(api.getMicrophoneManager().connect).not.toBeCalled(); - expect(api.getCardElementManager().update).not.toBeCalled(); - }); - - it('successfully with all inititalizers', async () => { - const api = createCardAPI(); - const manager = new InitializationManager(api); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue( - createConfig({ - menu: { - buttons: { - media_player: { - enabled: true, - }, - }, - }, - live: { - microphone: { - always_connected: true, - }, - }, - }), - ); - - expect(await manager.initializeBackgroundIfNecessary()).toBeTruthy(); - expect(api.getMediaPlayerManager().initialize).toBeCalled(); - expect(api.getMicrophoneManager().connect).toBeCalled(); - expect(api.getCardElementManager().update).toBeCalled(); - }); - - it('with media player and microphone connect in progress', async () => { - const api = createCardAPI(); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue( - createConfig({ - menu: { - buttons: { - media_player: { - enabled: true, - }, - }, - }, - live: { - microphone: { - always_connected: true, - }, - }, - }), - ); - const initializer = mock(); - - const manager = new InitializationManager(api, initializer); - initializer.initializeMultipleIfNecessary.mockResolvedValue(false); - - expect(await manager.initializeBackgroundIfNecessary()).toBeFalsy(); - }); - }); - - it('should uninitialize', () => { - const initializer = mock(); - const manager = new InitializationManager(createCardAPI(), initializer); - - manager.uninitialize(InitializationAspect.CAMERAS); - - expect(initializer.uninitialize).toBeCalledWith(InitializationAspect.CAMERAS); - }); -}); diff --git a/tests/utils/card-controller/interaction-manager.test.ts b/tests/utils/card-controller/interaction-manager.test.ts deleted file mode 100644 index 83080cee..00000000 --- a/tests/utils/card-controller/interaction-manager.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -import add from 'date-fns/add'; -import { afterEach, describe, expect, it, vi } from 'vitest'; -import { InteractionManager } from '../../../src/utils/card-controller/interaction-manager'; -import { createCardAPI, createConfig } from '../../test-utils'; - -vi.mock('lodash-es/throttle', () => ({ - default: vi.fn((fn) => fn), -})); - -// @vitest-environment jsdom -describe('InteractionManager', () => { - const start = new Date('2023-09-24T20:20:00'); - - afterEach(() => { - vi.useRealTimers(); - }); - - it('should take action when interaction is reported', () => { - const api = createCardAPI(); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue( - createConfig({ - view: { - timeout_seconds: 10, - }, - }), - ); - const manager = new InteractionManager(api); - vi.useFakeTimers(); - vi.setSystemTime(start); - - manager.reportInteraction(); - - expect(api.getTriggersManager().untrigger).toBeCalled(); - expect(manager.hasInteraction()).toBeTruthy(); - expect(api.getViewManager().setViewDefault).not.toBeCalled(); - - vi.mocked(api.getTriggersManager().isTriggered).mockReturnValue(false); - vi.setSystemTime(add(start, { seconds: 10 })); - vi.runOnlyPendingTimers(); - - expect(api.getViewManager().setViewDefault).toBeCalled(); - }); - - it('should not take action when triggered', () => { - const api = createCardAPI(); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue( - createConfig({ - view: { - timeout_seconds: 10, - }, - }), - ); - const manager = new InteractionManager(api); - vi.useFakeTimers(); - vi.setSystemTime(start); - - manager.reportInteraction(); - - vi.mocked(api.getTriggersManager().isTriggered).mockReturnValue(true); - vi.setSystemTime(add(start, { seconds: 10 })); - vi.runOnlyPendingTimers(); - - // First call is blocked by triggers (above), so interaction will report - // true but the default view will not have been set. - expect(api.getViewManager().setViewDefault).not.toBeCalled(); - }); - - it('should not take action when not configured', () => { - const api = createCardAPI(); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue( - createConfig({ - view: { - timeout_seconds: 0, - }, - }), - ); - const manager = new InteractionManager(api); - - manager.reportInteraction(); - - // First call is blocked by triggers (above), so interaction will report - // true but the default view will not have been set. - expect(api.getViewManager().setViewDefault).not.toBeCalled(); - }); -}); diff --git a/tests/utils/card-controller/media-info-manager.test.ts b/tests/utils/card-controller/media-info-manager.test.ts deleted file mode 100644 index 8fd04e0c..00000000 --- a/tests/utils/card-controller/media-info-manager.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { MediaLoadedInfoManager } from '../../../src/utils/card-controller/media-info-manager'; -import { createCardAPI, createMediaLoadedInfo } from '../../test-utils.js'; - -describe('MediaLoadedInfoManager', () => { - it('should set', () => { - const api = createCardAPI(); - const manager = new MediaLoadedInfoManager(api); - const mediaInfo = createMediaLoadedInfo(); - - manager.set(mediaInfo); - - expect(manager.has()).toBeTruthy(); - expect(manager.get()).toBe(mediaInfo); - expect(api.getConditionsManager().setState).toBeCalledWith( - expect.objectContaining({ media_loaded: true }), - ); - expect(api.getStyleManager().setExpandedMode).toBeCalled(); - expect(api.getCardElementManager().update).toBeCalled(); - }); - - it('should not set invalid media info', () => { - const api = createCardAPI(); - const manager = new MediaLoadedInfoManager(api); - const mediaInfo = createMediaLoadedInfo({ width: 0, height: 0 }); - - manager.set(mediaInfo); - - expect(manager.has()).toBeFalsy(); - expect(manager.get()).toBeNull(); - expect(api.getConditionsManager().setState).not.toBeCalled(); - }); - - it('should get last known', () => { - const api = createCardAPI(); - const manager = new MediaLoadedInfoManager(api); - const mediaInfo = createMediaLoadedInfo(); - - manager.set(mediaInfo); - - expect(manager.has()).toBeTruthy(); - - manager.clear(); - - expect(manager.has()).toBeFalsy(); - expect(manager.getLastKnown()).toBe(mediaInfo); - expect(api.getConditionsManager().setState).toBeCalledWith( - expect.objectContaining({ media_loaded: false }), - ); - }); -}); diff --git a/tests/utils/card-controller/media-player-manager.test.ts b/tests/utils/card-controller/media-player-manager.test.ts deleted file mode 100644 index 53a2df3d..00000000 --- a/tests/utils/card-controller/media-player-manager.test.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { - TestViewMedia, - createCameraConfig, - createCameraManager, - createCardAPI, - createHASS, - createRegistryEntity, - createStateEntity, -} from '../../test-utils'; -import { MediaPlayerManager } from '../../../src/utils/card-controller/media-player-manager'; -import { ExtendedHomeAssistant } from '../../../src/types'; -import { MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA } from '../../../src/const'; -import { EntityRegistryManager } from '../../../src/utils/ha/entity-registry'; -import { mock } from 'vitest-mock-extended'; - -vi.mock('../../../src/camera-manager/manager.js'); - -const createHASSWithMediaPlayers = (): ExtendedHomeAssistant => { - const attributesSupported = { - supported_features: MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA, - }; - const attributesUnsupported = { - supported_features: 0, - }; - - return createHASS({ - 'media_player.ok1': createStateEntity({ - entity_id: 'media_player.ok1', - state: 'on', - attributes: attributesSupported, - }), - 'media_player.ok2': createStateEntity({ - entity_id: 'media_player.ok2', - state: 'on', - attributes: attributesSupported, - }), - 'media_player.ok3': createStateEntity({ - entity_id: 'media_player.ok3', - state: 'on', - attributes: attributesSupported, - }), - 'media_player.unavailable': createStateEntity({ - entity_id: 'media_player.sitting_room', - state: 'unavailable', - attributes: attributesSupported, - }), - 'media_player.unsupported': createStateEntity({ - entity_id: 'media_player.sitting_room', - state: 'on', - attributes: attributesUnsupported, - }), - 'switch.unrelated': createStateEntity({ - entity_id: 'switch.unrelated', - state: 'on', - }), - }); -}; - -describe('MediaPlayerManager', () => { - beforeEach(() => { - vi.restoreAllMocks(); - }); - - describe('should initialize', () => { - it('correctly', async () => { - const entityRegistryManager = mock(); - entityRegistryManager.getEntities.mockResolvedValue( - new Map([ - ['media_player.ok1', createRegistryEntity({ hidden_by: '' })], - ['media_player.ok2', createRegistryEntity({ hidden_by: 'user' })], - ]), - ); - - const api = createCardAPI(); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue( - createHASSWithMediaPlayers(), - ); - vi.mocked(api.getEntityRegistryManager).mockReturnValue(entityRegistryManager); - const manager = new MediaPlayerManager(api); - - await manager.initialize(); - - expect(manager.getMediaPlayers()).toEqual([ - 'media_player.ok1', - 'media_player.ok3', - ]); - expect(manager.hasMediaPlayers()).toBeTruthy(); - }); - - it('without hass', async () => { - const api = createCardAPI(); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null); - const manager = new MediaPlayerManager(api); - - await manager.initialize(); - - expect(manager.getMediaPlayers()).toEqual([]); - expect(manager.hasMediaPlayers()).toBeFalsy(); - }); - - it('even if entity registry call fails', async () => { - const spy = vi.spyOn(global.console, 'warn').mockImplementation(() => true); - - const entityRegistryManager = mock(); - entityRegistryManager.getEntities.mockRejectedValue(new Error('message')); - - const api = createCardAPI(); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue( - createHASSWithMediaPlayers(), - ); - vi.mocked(api.getEntityRegistryManager).mockReturnValue(entityRegistryManager); - const manager = new MediaPlayerManager(api); - - await manager.initialize(); - - expect(manager.getMediaPlayers()).toEqual([ - 'media_player.ok1', - 'media_player.ok2', - 'media_player.ok3', - ]); - expect(manager.hasMediaPlayers()).toBeTruthy(); - expect(spy).toBeCalled(); - }); - }); - - it('should stop', async () => { - const api = createCardAPI(); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); - - const manager = new MediaPlayerManager(api); - - await manager.stop('media_player.foo'); - - expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith( - 'media_player', - 'media_stop', - { - entity_id: 'media_player.foo', - }, - ); - }); - - describe('should play', () => { - describe('live', () => { - it('successfully', async () => { - const api = createCardAPI(); - const cameraManager = createCameraManager(); - vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue( - createCameraConfig({ - camera_entity: 'camera.foo', - }), - ); - vi.mocked(cameraManager.getCameraMetadata).mockReturnValue({ - title: 'camera title', - icon: 'icon', - }); - vi.mocked(api.getCameraManager).mockReturnValue(cameraManager); - const hass = createHASS({ - 'camera.foo': createStateEntity({ - attributes: { - entity_picture: 'http://thumbnail', - }, - }), - }); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass); - const manager = new MediaPlayerManager(api); - - await manager.playLive('media_player.foo', 'camera'); - - expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith( - 'media_player', - 'play_media', - { - entity_id: 'media_player.foo', - media_content_id: 'media-source://camera/camera.foo', - media_content_type: 'application/vnd.apple.mpegurl', - extra: { - title: 'camera title', - thumb: 'http://thumbnail', - }, - }, - ); - }); - - it('without camera_entity', async () => { - const api = createCardAPI(); - const cameraManager = createCameraManager(); - vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue( - createCameraConfig({}), - ); - vi.mocked(api.getCameraManager).mockReturnValue(cameraManager); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); - const manager = new MediaPlayerManager(api); - - await manager.playLive('media_player.foo', 'camera'); - - expect(api.getHASSManager().getHASS()?.callService).not.toBeCalled(); - }); - - it('without title and thumbnail', async () => { - const api = createCardAPI(); - const cameraManager = createCameraManager(); - vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue( - createCameraConfig({ - camera_entity: 'camera.foo', - }), - ); - vi.mocked(api.getCameraManager).mockReturnValue(cameraManager); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); - const manager = new MediaPlayerManager(api); - - await manager.playLive('media_player.foo', 'camera'); - - expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith( - 'media_player', - 'play_media', - { - entity_id: 'media_player.foo', - media_content_id: 'media-source://camera/camera.foo', - media_content_type: 'application/vnd.apple.mpegurl', - extra: {}, - }, - ); - }); - }); - - describe('media', () => { - describe('successfully with', () => { - it.each([ - ['clip' as const, 'video' as const], - ['snapshot' as const, 'image' as const], - ])( - '%s', - async (mediaType: 'clip' | 'snapshot', contentType: 'video' | 'image') => { - const media = new TestViewMedia({ - title: 'media title', - thumbnail: 'http://thumbnail', - contentID: 'media-source://contentid', - mediaType: mediaType, - }); - const api = createCardAPI(); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); - const manager = new MediaPlayerManager(api); - - await manager.playMedia('media_player.foo', media); - - expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith( - 'media_player', - 'play_media', - { - entity_id: 'media_player.foo', - media_content_id: 'media-source://contentid', - media_content_type: contentType, - extra: { - title: 'media title', - thumb: 'http://thumbnail', - }, - }, - ); - }, - ); - }); - - it('without hass', async () => { - const api = createCardAPI(); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null); - const manager = new MediaPlayerManager(api); - const media = new TestViewMedia(); - - await manager.playMedia('media_player.foo', media); - - // No actual test can be performed here as nothing observable happens. - // This test serves only as code-coverage long-tail. - }); - }); - }); -}); diff --git a/tests/utils/card-controller/message-manager.test.ts b/tests/utils/card-controller/message-manager.test.ts deleted file mode 100644 index ed893c80..00000000 --- a/tests/utils/card-controller/message-manager.test.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { afterAll, describe, expect, it, vi } from 'vitest'; -import { FrigateCardError, Message } from '../../../src/types'; -import { MessageManager } from '../../../src/utils/card-controller/message-manager'; -import { createCardAPI } from '../../test-utils'; - -const createMessage = (options?: Partial): Message => { - return { - message: options?.message ?? 'message', - type: options?.type ?? 'info', - ...(!!options?.icon && { icon: options.icon }), - ...(!!options?.context && { context: options.context }), - ...(!!options?.dotdotdot && { dotdotdot: options.dotdotdot }), - }; -}; - -describe('MessageManager', () => { - afterAll(() => { - vi.restoreAllMocks(); - }); - - it('should construct', () => { - const manager = new MessageManager(createCardAPI()); - expect(manager.hasMessage()).toBeFalsy(); - expect(manager.getMessage()).toBeNull(); - expect(manager.hasErrorMessage()).toBeFalsy(); - }); - - it('should set info message', () => { - const api = createCardAPI(); - const manager = new MessageManager(api); - const message = createMessage(); - manager.setMessageIfHigherPriority(message); - expect(manager.hasMessage()).toBeTruthy(); - expect(manager.getMessage()).toBe(message); - expect(manager.hasErrorMessage()).toBeFalsy(); - - expect(api.getMediaLoadedInfoManager().clear).toBeCalled(); - expect(api.getCardElementManager().scrollReset).toBeCalled(); - expect(api.getCardElementManager().update).toBeCalled(); - }); - - it('should set error message', () => { - const api = createCardAPI(); - const manager = new MessageManager(api); - const message = createMessage({ type: 'error' }); - manager.setMessageIfHigherPriority(message); - expect(manager.hasMessage()).toBeTruthy(); - expect(manager.getMessage()).toBe(message); - expect(manager.hasErrorMessage()).toBeTruthy(); - - expect(api.getMediaLoadedInfoManager().clear).toBeCalled(); - expect(api.getCardElementManager().scrollReset).toBeCalled(); - expect(api.getCardElementManager().update).toBeCalled(); - }); - - it('should reset message', () => { - const api = createCardAPI(); - const manager = new MessageManager(api); - - manager.reset(); - expect(manager.hasMessage()).toBeFalsy(); - - const message = createMessage({ type: 'error' }); - manager.setMessageIfHigherPriority(message); - expect(manager.hasMessage()).toBeTruthy(); - - vi.mocked(api.getCardElementManager().update).mockClear(); - manager.reset(); - - expect(manager.hasMessage()).toBeFalsy(); - expect(api.getCardElementManager().update).toBeCalled(); - }); - - it('should respect priority', () => { - const api = createCardAPI(); - const manager = new MessageManager(api); - - manager.reset(); - expect(manager.hasMessage()).toBeFalsy(); - - const errorMessage = createMessage({ type: 'error' }); - manager.setMessageIfHigherPriority(errorMessage); - - const infoMessage = createMessage({ type: 'info' }); - manager.setMessageIfHigherPriority(infoMessage); - - expect(manager.getMessage()).toBe(errorMessage); - - const connectionMessage = createMessage({ type: 'connection' }); - manager.setMessageIfHigherPriority(connectionMessage); - - expect(manager.getMessage()).toBe(connectionMessage); - }); - - it('should set FrigateCardError object', () => { - const consoleSpy = vi.spyOn(global.console, 'warn').mockReturnValue(undefined); - - const api = createCardAPI(); - const manager = new MessageManager(api); - const context = { foo: 'bar' }; - - manager.setErrorIfHigherPriority( - new FrigateCardError('frigate card message', context), - ); - expect(manager.hasMessage()).toBeTruthy(); - expect(manager.getMessage()).toEqual({ - message: 'frigate card message', - type: 'error', - context: context, - }); - - expect(consoleSpy).toBeCalled(); - }); - - it('should set Error object', () => { - const consoleSpy = vi.spyOn(global.console, 'warn').mockReturnValue(undefined); - - const api = createCardAPI(); - const manager = new MessageManager(api); - - manager.setErrorIfHigherPriority(new Error('generic error message')); - expect(manager.hasMessage()).toBeTruthy(); - expect(manager.getMessage()).toEqual({ - message: 'generic error message', - type: 'error', - }); - - expect(consoleSpy).toBeCalled(); - }); - - it('should not set unknown error type', () => { - const consoleSpy = vi.spyOn(global.console, 'warn').mockReturnValue(undefined); - - const api = createCardAPI(); - const manager = new MessageManager(api); - - manager.setErrorIfHigherPriority('not_an_error_object'); - expect(manager.hasMessage()).toBeFalsy(); - expect(consoleSpy).not.toBeCalled(); - }); -}); diff --git a/tests/utils/card-controller/microphone-manager.test.ts b/tests/utils/card-controller/microphone-manager.test.ts deleted file mode 100644 index 69063ad4..00000000 --- a/tests/utils/card-controller/microphone-manager.test.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { mock } from 'vitest-mock-extended'; -import { MicrophoneManager } from '../../../src/utils/card-controller/microphone-manager'; -import { createCardAPI, createConfig } from '../../test-utils'; - -const navigatorMock = { - mediaDevices: { - getUserMedia: vi.fn(), - }, -}; - -// @vitest-environment jsdom -describe('MicrophoneManager', () => { - beforeEach(() => { - vi.stubGlobal('navigator', navigatorMock); - vi.useRealTimers(); - }); - - afterEach(() => { - vi.resetAllMocks(); - vi.unstubAllGlobals; - }); - - const createMockStream = (mute?: boolean): MediaStream => { - const stream = mock(); - const track = mock(); - track.enabled = !mute; - stream.getTracks.mockImplementation(() => [track]); - return stream; - }; - - it('should be muted on creation', () => { - const manager = new MicrophoneManager(createCardAPI()); - expect(manager).toBeTruthy(); - expect(manager.isMuted()).toBeTruthy(); - }); - - it('should be undefined without creation', () => { - const manager = new MicrophoneManager(createCardAPI()); - expect(manager.getStream()).toBeUndefined(); - }); - - it('should connect', async () => { - const api = createCardAPI(); - const manager = new MicrophoneManager(api); - - const stream = createMockStream(); - navigatorMock.mediaDevices.getUserMedia.mockReturnValue(stream); - - await manager.connect(); - - expect(manager.isConnected()).toBeTruthy(); - expect(manager.getStream()).toBe(stream); - expect(manager.isMuted()).toBeTruthy(); - expect(api.getCardElementManager().update).toBeCalled(); - }); - - it('should be forbidden when permission denied', async () => { - // Don't actually log messages to the console during the test. - vi.spyOn(global.console, 'warn').mockReturnValue(undefined); - - const api = createCardAPI(); - const manager = new MicrophoneManager(api); - navigatorMock.mediaDevices.getUserMedia.mockRejectedValue(new Error()); - - await manager.connect(); - - expect(manager.isConnected()).toBeFalsy(); - expect(manager.isForbidden()).toBeTruthy(); - expect(api.getCardElementManager().update).toBeCalled(); - }); - - it('should mute and unmute', async () => { - const api = createCardAPI(); - const manager = new MicrophoneManager(api); - navigatorMock.mediaDevices.getUserMedia.mockReturnValue(createMockStream()); - - await manager.connect(); - expect(manager.isMuted()).toBeTruthy(); - expect(api.getCardElementManager().update).toBeCalledTimes(1); - - manager.mute(); - expect(manager.isMuted()).toBeTruthy(); - expect(api.getCardElementManager().update).toBeCalledTimes(2); - - await manager.unmute(); - expect(manager.isMuted()).toBeFalsy(); - expect(api.getCardElementManager().update).toBeCalledTimes(3); - }); - - it('should not unmute when microphone forbidden', async () => { - const api = createCardAPI(); - const manager = new MicrophoneManager(api); - navigatorMock.mediaDevices.getUserMedia.mockReturnValue(null); - - await manager.connect(); - - expect(manager.isMuted()).toBeTruthy(); - expect(api.getCardElementManager().update).toBeCalledTimes(1); - - await manager.unmute(); - expect(manager.isMuted()).toBeTruthy(); - expect(api.getCardElementManager().update).toBeCalledTimes(1); - }); - - it('should connect on unmute', async () => { - const api = createCardAPI(); - const manager = new MicrophoneManager(api); - navigatorMock.mediaDevices.getUserMedia.mockReturnValue(createMockStream()); - - expect(manager.isConnected()).toBeFalsy(); - - await manager.unmute(); - - expect(manager.isConnected()).toBeTruthy(); - expect(manager.isMuted()).toBeFalsy(); - - expect(api.getCardElementManager().update).toBeCalledTimes(2); - }); - - it('should disconnect', async () => { - const api = createCardAPI(); - const manager = new MicrophoneManager(api); - - navigatorMock.mediaDevices.getUserMedia.mockReturnValue(createMockStream()); - - await manager.connect(); - expect(manager.isConnected()).toBeTruthy(); - expect(api.getCardElementManager().update).toBeCalledTimes(1); - - await manager.disconnect(); - expect(manager.isConnected()).toBeFalsy(); - expect(api.getCardElementManager().update).toBeCalledTimes(2); - }); - - it('should automatically disconnect', async () => { - vi.useFakeTimers(); - - const disconnectSeconds = 10; - const api = createCardAPI(); - const manager = new MicrophoneManager(api); - navigatorMock.mediaDevices.getUserMedia.mockReturnValue(createMockStream()); - - vi.mocked(api.getConfigManager().getConfig).mockReturnValue( - createConfig({ - live: { - microphone: { - always_connected: false, - disconnect_seconds: disconnectSeconds, - }, - }, - }), - ); - - await manager.connect(); - expect(manager.isConnected()).toBeTruthy(); - expect(api.getCardElementManager().update).toBeCalledTimes(1); - - vi.advanceTimersByTime(disconnectSeconds * 1000); - - expect(manager.isConnected()).toBeFalsy(); - expect(api.getCardElementManager().update).toBeCalledTimes(2); - }); - - it('should not automatically disconnect when always connected', async () => { - vi.useFakeTimers(); - - const disconnectSeconds = 10; - const api = createCardAPI(); - const manager = new MicrophoneManager(api); - navigatorMock.mediaDevices.getUserMedia.mockReturnValue(createMockStream()); - - vi.mocked(api.getConfigManager().getConfig).mockReturnValue( - createConfig({ - live: { - microphone: { - always_connected: true, - disconnect_seconds: disconnectSeconds, - }, - }, - }), - ); - - await manager.connect(); - expect(manager.isConnected()).toBeTruthy(); - expect(api.getCardElementManager().update).toBeCalledTimes(1); - - vi.advanceTimersByTime(disconnectSeconds * 1000); - - expect(manager.isConnected()).toBeTruthy(); - expect(api.getCardElementManager().update).toBeCalledTimes(1); - }); -}); diff --git a/tests/utils/card-controller/query-string-manager.test.ts b/tests/utils/card-controller/query-string-manager.test.ts deleted file mode 100644 index 1cd6f361..00000000 --- a/tests/utils/card-controller/query-string-manager.test.ts +++ /dev/null @@ -1,298 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { createCardAPI } from '../../test-utils'; -import { QueryStringManager } from '../../../src/utils/card-controller/query-string-manager'; -import { mock } from 'vitest-mock-extended'; - -const setQueryString = (qs: string): void => { - const location: Location = mock(); - location.search = qs; - global.window.location = location; -}; - -// @vitest-environment jsdom -describe('QueryStringManager', () => { - beforeEach(() => { - global.window.location = mock(); - }); - - it('should reject malformed query string', () => { - setQueryString('BOGUS_KEY=BOGUS_VALUE'); - const api = createCardAPI(); - vi.mocked(api.getMessageManager().hasMessage).mockReturnValue(true); - const manager = new QueryStringManager(api); - - manager.executeAll(); - - expect(manager.hasViewRelatedActions()).toBeFalsy(); - expect(api.getActionsManager().executeAction).not.toBeCalled(); - expect(api.getViewManager().setViewByParameters).not.toBeCalled(); - }); - - describe('should execute view name action from query string', () => { - it.each([ - ['clip' as const], - ['clips' as const], - ['diagnostics' as const], - ['image' as const], - ['live' as const], - ['recording' as const], - ['recordings' as const], - ['snapshot' as const], - ['snapshots' as const], - ['timeline' as const], - ])('%s', (viewName: string) => { - setQueryString(`?frigate-card-action.id.${viewName}=`); - const api = createCardAPI(); - - // View actions do not need the card to have been updated. - vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(false); - const manager = new QueryStringManager(api); - - manager.executeAll(); - - expect(manager.hasViewRelatedActions()).toBeTruthy(); - expect(api.getViewManager().setViewByParameters).toBeCalledWith({ - viewName: viewName, - }); - }); - }); - - describe('should execute non-view action from query string', () => { - it.each([ - ['camera_ui' as const], - ['download' as const], - ['expand' as const], - ['menu_toggle' as const], - ])('%s', (action: string) => { - setQueryString(`?frigate-card-action.id.${action}=`); - const api = createCardAPI(); - vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true); - const manager = new QueryStringManager(api); - - manager.executeAll(); - - expect(manager.hasViewRelatedActions()).toBeFalsy(); - expect(api.getActionsManager().executeAction).toBeCalledWith({ - action: 'fire-dom-event', - card_id: 'id', - frigate_card_action: action, - }); - }); - }); - - it('should execute view default action', () => { - setQueryString('?frigate-card-action.id.default='); - const api = createCardAPI(); - // View actions do not need the card to have been updated. - vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(false); - - const manager = new QueryStringManager(api); - - manager.executeAll(); - - expect(api.getViewManager().setViewDefault).toBeCalled(); - - expect(manager.hasViewRelatedActions()).toBeTruthy(); - expect(api.getActionsManager().executeAction).not.toBeCalled(); - expect(api.getViewManager().setViewByParameters).not.toBeCalled(); - }); - - it('should execute camera_select action', () => { - setQueryString('?frigate-card-action.id.camera_select=camera.office'); - const api = createCardAPI(); - vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true); - const manager = new QueryStringManager(api); - - manager.executeAll(); - - expect(api.getViewManager().setViewByParameters).toBeCalledWith({ - cameraID: 'camera.office', - }); - - expect(manager.hasViewRelatedActions()).toBeTruthy(); - expect(api.getActionsManager().executeAction).not.toBeCalled(); - expect(api.getViewManager().setViewDefault).not.toBeCalled(); - }); - - it('should execute live_substream_select action', () => { - setQueryString('?frigate-card-action.id.live_substream_select=camera.office_hd'); - const api = createCardAPI(); - vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true); - const manager = new QueryStringManager(api); - - manager.executeAll(); - - expect(api.getViewManager().setViewByParameters).toBeCalledWith({ - substream: 'camera.office_hd', - }); - - expect(manager.hasViewRelatedActions()).toBeTruthy(); - expect(api.getActionsManager().executeAction).not.toBeCalled(); - expect(api.getViewManager().setViewDefault).not.toBeCalled(); - }); - - describe('should ignore action without value', () => { - it.each([['camera_select' as const], ['live_substream_select' as const]])( - '%s', - (action: string) => { - setQueryString(`?frigate-card-action.id.${action}=`); - const api = createCardAPI(); - vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true); - const manager = new QueryStringManager(api); - - manager.executeAll(); - - expect(manager.hasViewRelatedActions()).toBeFalsy(); - expect(api.getActionsManager().executeAction).not.toBeCalled(); - expect(api.getViewManager().setViewDefault).not.toBeCalled(); - expect(api.getViewManager().setViewByParameters).not.toBeCalled(); - }, - ); - }); - - it('should handle unknown action', () => { - const consoleSpy = vi.spyOn(global.console, 'warn').mockReturnValue(undefined); - - setQueryString('?frigate-card-action.id.not_an_action=value'); - const api = createCardAPI(); - vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true); - const manager = new QueryStringManager(api); - - manager.executeAll(); - - expect(manager.hasViewRelatedActions()).toBeFalsy(); - expect(api.getActionsManager().executeAction).not.toBeCalled(); - expect(api.getViewManager().setViewDefault).not.toBeCalled(); - expect(api.getViewManager().setViewByParameters).not.toBeCalled(); - expect(consoleSpy).toBeCalled(); - }); - - describe('should execute view name action from query string', () => { - it.each([ - ['clip' as const], - ['clips' as const], - ['diagnostics' as const], - ['image' as const], - ['live' as const], - ['recording' as const], - ['recordings' as const], - ['snapshot' as const], - ['snapshots' as const], - ['timeline' as const], - ])('%s', (viewName: string) => { - setQueryString(`?frigate-card-action.id.${viewName}=`); - const api = createCardAPI(); - vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true); - const manager = new QueryStringManager(api); - - manager.executeAll(); - - expect(manager.hasViewRelatedActions()).toBeTruthy(); - expect(api.getViewManager().setViewByParameters).toBeCalledWith({ - viewName: viewName, - }); - }); - }); - - describe('should not execute non-view actions without an initial update', () => { - it.each([ - ['camera_ui' as const], - ['download' as const], - ['expand' as const], - ['menu_toggle' as const], - ])('%s', (action: string) => { - setQueryString(`?frigate-card-action.id.${action}=value`); - const api = createCardAPI(); - vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(false); - const manager = new QueryStringManager(api); - - manager.executeAll(); - - expect(api.getActionsManager().executeAction).not.toBeCalled(); - expect(api.getViewManager().setViewDefault).not.toBeCalled(); - expect(api.getViewManager().setViewByParameters).not.toBeCalled(); - }); - }); - - describe('should handle conflicting but valid actions', () => { - it('view and default with camera and substream specified', () => { - setQueryString( - '?frigate-card-action.id.clips=' + - '&frigate-card-action.id.live_substream_select=camera.kitchen_hd' + - '&frigate-card-action.id.default=' + - '&frigate-card-action.id.camera_select=camera.kitchen', - ); - const api = createCardAPI(); - vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true); - const manager = new QueryStringManager(api); - - manager.executeAll(); - - expect(api.getViewManager().setViewDefault).toBeCalledWith({ - cameraID: 'camera.kitchen', - substream: 'camera.kitchen_hd', - }); - expect(api.getViewManager().setViewByParameters).not.toBeCalled(); - }); - - it('multiple cameras specified', () => { - setQueryString( - '?frigate-card-action.id.camera_select=camera.kitchen' + - '&frigate-card-action.id.camera_select=camera.office', - ); - const api = createCardAPI(); - vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true); - const manager = new QueryStringManager(api); - - manager.executeAll(); - - expect(api.getViewManager().setViewByParameters).toBeCalledWith({ - cameraID: 'camera.office', - }); - }); - }); - - describe('should not execute view related actions', () => { - it.each([ - ['clip' as const], - ['clips' as const], - ['default' as const], - ['diagnostics' as const], - ['image' as const], - ['live' as const], - ['recording' as const], - ['recordings' as const], - ['snapshot' as const], - ['snapshots' as const], - ['timeline' as const], - ])('%s', (viewName: string) => { - setQueryString(`?frigate-card-action.id.${viewName}=`); - const api = createCardAPI(); - vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true); - const manager = new QueryStringManager(api); - - manager.executeNonViewRelated(); - - expect(api.getViewManager().setViewByParameters).not.toBeCalled(); - expect(api.getViewManager().setViewDefault).not.toBeCalled(); - }); - }); - - describe('should not execute non-view related actions', () => { - it.each([ - ['camera_ui' as const], - ['download' as const], - ['expand' as const], - ['menu_toggle' as const], - ])('%s', (viewName: string) => { - setQueryString(`?frigate-card-action.id.${viewName}=`); - const api = createCardAPI(); - vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true); - const manager = new QueryStringManager(api); - - manager.executeViewRelated(); - - expect(api.getActionsManager().executeAction).not.toBeCalled(); - }); - }); -}); diff --git a/tests/utils/card-controller/style-manager.test.ts b/tests/utils/card-controller/style-manager.test.ts deleted file mode 100644 index 0c25273e..00000000 --- a/tests/utils/card-controller/style-manager.test.ts +++ /dev/null @@ -1,387 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { FrigateCardView } from '../../../src/config/types'; -import { setPerformanceCSSStyles } from '../../../src/performance'; -import { StyleManager } from '../../../src/utils/card-controller/style-manager'; -import { createCardAPI, createConfig, createHASS, createView } from '../../test-utils'; - -vi.mock('../../../src/performance'); - -// @vitest-environment jsdom -describe('StyleManager', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - - describe('setLightOrDarkMode', () => { - it('dark mode unspecified', () => { - const api = createCardAPI(); - const element = document.createElement('div'); - vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element); - const manager = new StyleManager(api); - - manager.setLightOrDarkMode(); - - expect(element.getAttribute('dark')).toBeNull(); - }); - - it('dark mode explicitly off', () => { - const api = createCardAPI(); - const element = document.createElement('div'); - vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue( - createConfig({ - view: { - dark_mode: 'off', - }, - }), - ); - const manager = new StyleManager(api); - - manager.setLightOrDarkMode(); - - expect(element.getAttribute('dark')).toBeNull(); - }); - - it('dark mode explicitly set', () => { - const api = createCardAPI(); - const element = document.createElement('div'); - vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue( - createConfig({ - view: { - dark_mode: 'on', - }, - }), - ); - const manager = new StyleManager(api); - - manager.setLightOrDarkMode(); - - expect(element.getAttribute('dark')).not.toBeNull(); - }); - - it('dark mode auto without interaction', () => { - const api = createCardAPI(); - const element = document.createElement('div'); - vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue( - createConfig({ - view: { - dark_mode: 'auto', - }, - }), - ); - vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(false); - const manager = new StyleManager(api); - - manager.setLightOrDarkMode(); - - expect(element.getAttribute('dark')).not.toBeNull(); - }); - - it('dark mode auto with HA dark mode', () => { - const api = createCardAPI(); - const element = document.createElement('div'); - vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue( - createConfig({ - view: { - dark_mode: 'auto', - }, - }), - ); - vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(true); - const hass = createHASS(); - hass.themes.darkMode = true; - vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass); - const manager = new StyleManager(api); - - manager.setLightOrDarkMode(); - - expect(element.getAttribute('dark')).not.toBeNull(); - }); - }); - - describe('setExpandedMode', () => { - it('with no view or known media', () => { - const api = createCardAPI(); - const element = document.createElement('div'); - vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element); - vi.mocked(api.getMediaLoadedInfoManager().getLastKnown).mockReturnValue(null); - const manager = new StyleManager(api); - - manager.setExpandedMode(); - - expect(element.style.getPropertyValue('--frigate-card-expand-aspect-ratio')).toBe( - 'unset', - ); - expect(element.style.getPropertyValue('--frigate-card-expand-width')).toBe( - 'var(--frigate-card-expand-max-width)', - ); - expect(element.style.getPropertyValue('--frigate-card-expand-height')).toBe( - 'var(--frigate-card-expand-max-height)', - ); - }); - - it('with view but without media', () => { - const api = createCardAPI(); - const element = document.createElement('div'); - vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element); - const view = createView({ view: 'media', displayMode: 'single' }); - vi.mocked(api.getViewManager().getView).mockReturnValue(view); - vi.mocked(api.getMediaLoadedInfoManager().getLastKnown).mockReturnValue(null); - const manager = new StyleManager(api); - - manager.setExpandedMode(); - - expect(element.style.getPropertyValue('--frigate-card-expand-aspect-ratio')).toBe( - 'unset', - ); - expect(element.style.getPropertyValue('--frigate-card-expand-width')).toBe('none'); - expect(element.style.getPropertyValue('--frigate-card-expand-height')).toBe( - 'none', - ); - }); - - it('with view and media', () => { - const api = createCardAPI(); - const element = document.createElement('div'); - vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element); - const view = createView({ view: 'media', displayMode: 'single' }); - vi.mocked(api.getViewManager().getView).mockReturnValue(view); - vi.mocked(api.getMediaLoadedInfoManager().getLastKnown).mockReturnValue({ - width: 800, - height: 600, - }); - const manager = new StyleManager(api); - - manager.setExpandedMode(); - - expect(element.style.getPropertyValue('--frigate-card-expand-aspect-ratio')).toBe( - '800 / 600', - ); - expect(element.style.getPropertyValue('--frigate-card-expand-width')).toBe('none'); - expect(element.style.getPropertyValue('--frigate-card-expand-height')).toBe( - 'none', - ); - }); - - it('with view and grid display mode', () => { - const api = createCardAPI(); - const element = document.createElement('div'); - vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element); - const view = createView({ view: 'media', displayMode: 'grid' }); - vi.mocked(api.getViewManager().getView).mockReturnValue(view); - vi.mocked(api.getMediaLoadedInfoManager().getLastKnown).mockReturnValue({ - width: 800, - height: 600, - }); - const manager = new StyleManager(api); - - manager.setExpandedMode(); - - expect(element.style.getPropertyValue('--frigate-card-expand-aspect-ratio')).toBe( - '800 / 600', - ); - expect(element.style.getPropertyValue('--frigate-card-expand-width')).toBe( - 'var(--frigate-card-expand-max-width)', - ); - expect(element.style.getPropertyValue('--frigate-card-expand-height')).toBe( - 'var(--frigate-card-expand-max-height)', - ); - }); - }); - - describe('setMinMaxHeight', () => { - it('without a config', () => { - const api = createCardAPI(); - const element = document.createElement('div'); - vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element); - const manager = new StyleManager(api); - - manager.setMinMaxHeight(); - - expect(element.style.getPropertyValue('--frigate-card-max-height')).toBeFalsy(); - expect(element.style.getPropertyValue('--frigate-card-expand-height')).toBeFalsy(); - }); - - it('with a config', () => { - const api = createCardAPI(); - const element = document.createElement('div'); - vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue( - createConfig({ - dimensions: { - max_height: '800px', - min_height: '400px', - }, - }), - ); - const manager = new StyleManager(api); - - manager.setMinMaxHeight(); - - expect(element.style.getPropertyValue('--frigate-card-min-height')).toBe('400px'); - expect(element.style.getPropertyValue('--frigate-card-max-height')).toBe('800px'); - }); - }); - - it('setPerformance', () => { - const api = createCardAPI(); - const element = document.createElement('div'); - vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element); - const config = createConfig(); - vi.mocked(api.getConfigManager().getCardWideConfig).mockReturnValue({ - performance: config.performance, - }); - const manager = new StyleManager(api); - - manager.setPerformance(); - - expect(setPerformanceCSSStyles).toBeCalledWith(element, config.performance); - }); - - describe('getAspectRatioStyle', () => { - it('without config or view', () => { - const api = createCardAPI(); - const manager = new StyleManager(api); - expect(manager.getAspectRatioStyle()).toBe('16 / 9'); - }); - - it('should be auto with unconstrained aspect ratio', () => { - const api = createCardAPI(); - const view = createView({ view: 'media' }); - vi.mocked(api.getViewManager().getView).mockReturnValue(view); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue( - createConfig({ - dimensions: { - aspect_ratio_mode: 'unconstrained', - }, - }), - ); - const manager = new StyleManager(api); - expect(manager.getAspectRatioStyle()).toBe('auto'); - }); - - it('should be auto in fullscreen', () => { - const api = createCardAPI(); - const view = createView({ view: 'media' }); - vi.mocked(api.getViewManager().getView).mockReturnValue(view); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig()); - vi.mocked(api.getFullscreenManager().isInFullscreen).mockReturnValue(true); - const manager = new StyleManager(api); - - expect(manager.getAspectRatioStyle()).toBe('auto'); - }); - - it('should be auto when expanded', () => { - const api = createCardAPI(); - const view = createView({ view: 'media' }); - vi.mocked(api.getViewManager().getView).mockReturnValue(view); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig()); - vi.mocked(api.getExpandManager().isExpanded).mockReturnValue(true); - const manager = new StyleManager(api); - - expect(manager.getAspectRatioStyle()).toBe('auto'); - }); - - it('should be auto when there is yet to be a view', () => { - const api = createCardAPI(); - vi.mocked(api.getViewManager().getView).mockReturnValue(null); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig()); - const manager = new StyleManager(api); - - expect(manager.getAspectRatioStyle()).toBe('auto'); - }); - - describe('should be auto when dynamic in certain views', () => { - it.each([ - ['clip' as const], - ['diagnostics' as const], - ['image' as const], - ['media' as const], - ['live' as const], - ['recording' as const], - ['snapshot' as const], - ['timeline' as const], - ])('%s', (viewName: FrigateCardView) => { - const api = createCardAPI(); - const view = createView({ view: viewName }); - vi.mocked(api.getViewManager().getView).mockReturnValue(view); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue( - createConfig({ - dimensions: { - aspect_ratio_mode: 'dynamic', - }, - }), - ); - const manager = new StyleManager(api); - - expect(manager.getAspectRatioStyle()).toBe('auto'); - }); - }); - - describe('should be enforced when dynamic in certain views', () => { - it.each([['clips' as const], ['recordings' as const], ['snapshots' as const]])( - '%s', - (viewName: FrigateCardView) => { - const api = createCardAPI(); - const view = createView({ view: viewName }); - vi.mocked(api.getViewManager().getView).mockReturnValue(view); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue( - createConfig({ - dimensions: { - aspect_ratio_mode: 'dynamic', - }, - }), - ); - const manager = new StyleManager(api); - - expect(manager.getAspectRatioStyle()).toBe('16 / 9'); - }, - ); - }); - - describe('should use media dimensions in dynamic', () => { - it.each([['clips' as const], ['recordings' as const], ['snapshots' as const]])( - '%s', - (viewName: FrigateCardView) => { - const api = createCardAPI(); - const view = createView({ view: viewName }); - vi.mocked(api.getViewManager().getView).mockReturnValue(view); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue( - createConfig({ - dimensions: { - aspect_ratio_mode: 'dynamic', - }, - }), - ); - vi.mocked(api.getMediaLoadedInfoManager().getLastKnown).mockReturnValue({ - width: 800, - height: 600, - }); - const manager = new StyleManager(api); - - expect(manager.getAspectRatioStyle()).toBe('800 / 600'); - }, - ); - }); - - it('should respect default aspect ratio', () => { - const api = createCardAPI(); - const view = createView({ view: 'clips' }); - vi.mocked(api.getViewManager().getView).mockReturnValue(view); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue( - createConfig({ - dimensions: { - aspect_ratio_mode: 'dynamic', - aspect_ratio: '4:3', - }, - }), - ); - const manager = new StyleManager(api); - - expect(manager.getAspectRatioStyle()).toBe('4 / 3'); - }); - }); -}); diff --git a/tests/utils/card-controller/triggers-manager.test.ts b/tests/utils/card-controller/triggers-manager.test.ts deleted file mode 100644 index 2bd6beb3..00000000 --- a/tests/utils/card-controller/triggers-manager.test.ts +++ /dev/null @@ -1,190 +0,0 @@ -import add from 'date-fns/add'; -import { HassEntities } from 'home-assistant-js-websocket'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { ScanOptions } from '../../../src/config/types'; -import { TriggersManager } from '../../../src/utils/card-controller/triggers-manager'; -import { - createCameraConfig, - createCameraManager, - createCardAPI, - createConfig, - createHASS, - createStateEntity, - createView, -} from '../../test-utils'; - -vi.mock('../../../src/camera-manager/manager.js'); - -// Creating and mocking a trigger API is a lot of boilerplate, this convenience -// function reduces it. -const createTriggerAPI = (options?: { - config?: Partial; - hassStates?: HassEntities; - interaction?: boolean; -}) => { - const api = createCardAPI(); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue( - createConfig({ - view: { - scan: options?.config ?? { - enabled: true, - untrigger_reset: true, - untrigger_seconds: 10, - }, - }, - }), - ); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue( - createHASS(options?.hassStates), - ); - vi.mocked(api.getCameraManager).mockReturnValue( - createCameraManager({ - configs: new Map([ - [ - 'camera_1', - createCameraConfig({ - triggers: { - entities: ['binary_sensor.motion'], - }, - }), - ], - ]), - }), - ); - vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue( - options?.interaction ?? false, - ); - - return api; -}; - -// @vitest-environment jsdom -describe('TriggersManager', () => { - const hassActiveState = { - 'binary_sensor.motion': createStateEntity({ state: 'on' }), - }; - const hassInactiveState = { - 'binary_sensor.motion': createStateEntity({ state: 'off' }), - }; - - beforeEach(() => { - vi.resetAllMocks(); - vi.useFakeTimers(); - }); - - it('should not be triggered by default', () => { - const manager = new TriggersManager(createCardAPI()); - expect(manager.isTriggered()).toBeFalsy(); - }); - - it('should not trigger when scan mode disabled default', () => { - const api = createTriggerAPI({ - config: { enabled: false }, - hassStates: hassActiveState, - }); - const manager = new TriggersManager(api); - - manager.updateTriggeredCameras(null); - - expect(manager.isTriggered()).toBeFalsy(); - }); - - it('should trigger and untrigger based on entity state', () => { - const start = new Date('2023-10-01T17:14'); - vi.setSystemTime(start); - const api = createTriggerAPI({ - hassStates: hassActiveState, - }); - const manager = new TriggersManager(api); - - manager.updateTriggeredCameras(createHASS(hassInactiveState)); - - expect(manager.isTriggered()).toBeTruthy(); - expect(api.getViewManager().setViewByParameters).toBeCalledWith({ - viewName: 'live', - cameraID: 'camera_1', - }); - - vi.mocked(api.getHASSManager().getHASS).mockReturnValue( - createHASS(hassInactiveState), - ); - - manager.updateTriggeredCameras(createHASS(hassActiveState)); - - // Intentional state update with no change. - manager.updateTriggeredCameras(createHASS(hassActiveState)); - - // Will still be triggered, but untrigger timer will be running. - expect(manager.isTriggered()).toBeTruthy(); - - vi.setSystemTime(add(start, { seconds: 10 })); - vi.runOnlyPendingTimers(); - - expect(manager.isTriggered()).toBeFalsy(); - expect(api.getViewManager().setViewDefault).toBeCalled(); - }); - - it('should trigger and set view if current view is wrong', () => { - const api = createTriggerAPI({ - hassStates: hassActiveState, - }); - vi.mocked(api.getViewManager().getView).mockReturnValue( - createView({ - // Correct camera, but wrong view. - view: 'clips', - camera: 'camera_1', - }), - ); - const manager = new TriggersManager(api); - manager.updateTriggeredCameras(null); - - expect(api.getViewManager().setViewByParameters).toBeCalledWith({ - viewName: 'live', - cameraID: 'camera_1', - }); - }); - - it('should trigger when entity state is active on startup', () => { - const api = createTriggerAPI({ - hassStates: hassActiveState, - }); - const manager = new TriggersManager(api); - expect(manager.isTriggered()).toBeFalsy(); - - manager.updateTriggeredCameras(null); - expect(manager.isTriggered()).toBeTruthy(); - }); - - it('should untrigger manually', () => { - const api = createTriggerAPI({ - hassStates: hassActiveState, - }); - const manager = new TriggersManager(api); - - // Untriggering when not triggered. - manager.untrigger(); - expect(api.getViewManager().setViewDefault).not.toBeCalled(); - - manager.updateTriggeredCameras(null); - expect(manager.isTriggered()).toBeTruthy(); - - manager.untrigger(); - expect(manager.isTriggered()).toBeFalsy(); - expect(api.getViewManager().setViewDefault).toBeCalled(); - }); - - it('should take no actions when automated actions are not allowed', () => { - const api = createTriggerAPI({ - hassStates: hassActiveState, - // Interaction present. - interaction: true, - }); - const manager = new TriggersManager(api); - manager.updateTriggeredCameras(null); - expect(manager.isTriggered()).toBeTruthy(); - expect(api.getViewManager().setViewByParameters).not.toBeCalled(); - - manager.untrigger(); - expect(api.getViewManager().setViewDefault).not.toBeCalled(); - }); -}); diff --git a/tests/utils/card-controller/view-manager.test.ts b/tests/utils/card-controller/view-manager.test.ts deleted file mode 100644 index a8616725..00000000 --- a/tests/utils/card-controller/view-manager.test.ts +++ /dev/null @@ -1,698 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; -import { QueryType } from '../../../src/camera-manager/types'; -import { FrigateCardView } from '../../../src/config/types'; -import { getAllDependentCameras } from '../../../src/utils/camera'; -import { ViewManager } from '../../../src/utils/card-controller/view-manager'; -import { EventMediaQueries } from '../../../src/view/media-queries'; -import { - createCameraManager, - createCardAPI, - createConfig, - createHASS, - createView, - generateViewMediaArray, -} from '../../test-utils'; - -vi.mock('../../../src/camera-manager/manager.js'); -vi.mock('../../../src/utils/camera'); - -describe('ViewManager.setView', () => { - it('should set view', () => { - const api = createCardAPI(); - const manager = new ViewManager(api); - - const view = createView({ - view: 'live', - camera: 'camera', - displayMode: 'grid', - }); - manager.setView(view); - - expect(manager.getView()).toBe(view); - expect(api.getMediaLoadedInfoManager().clear).toBeCalled(); - expect(api.getCardElementManager().scrollReset).toBeCalled(); - expect(api.getMessageManager().reset).toBeCalled(); - expect(api.getStyleManager().setExpandedMode).toBeCalled(); - expect(api.getConditionsManager()?.setState).toBeCalledWith({ - view: 'live', - camera: 'camera', - displayMode: 'grid', - }); - expect(api.getCardElementManager().update).toBeCalled(); - }); - - it('should set view with minor changes without media clearing or scroll', () => { - const api = createCardAPI(); - const manager = new ViewManager(api); - - const view_1 = createView({ - view: 'live', - camera: 'camera', - }); - manager.setView(view_1); - - vi.mocked(api.getMediaLoadedInfoManager().clear).mockClear(); - vi.mocked(api.getCardElementManager().scrollReset).mockClear(); - - const view_2 = createView({ - view: 'live', - camera: 'camera', - displayMode: 'single', - }); - - manager.setView(view_2); - - expect(manager.getView()).toBe(view_2); - - // The new view is neither a major media change, nor a different view name, - // so media clearing and scrolling should not happen. - expect(api.getMediaLoadedInfoManager().clear).not.toBeCalled(); - expect(api.getCardElementManager().scrollReset).not.toBeCalled(); - }); - - it('should set view with new context', () => { - const api = createCardAPI(); - const manager = new ViewManager(api); - const context = { thumbnails: { fetch: false } }; - - // Setting context with no existing view does nothing. - manager.setViewWithNewContext(context); - expect(manager.getView()).toBeNull(); - - const view = createView({ - view: 'live', - camera: 'camera', - }); - manager.setView(view); - manager.setViewWithNewContext(context); - - expect(manager.getView()?.camera).toBe('camera'); - expect(manager.getView()?.view).toBe('live'); - expect(manager.getView()?.context).toEqual(context); - }); -}); - -describe('ViewManager.reset', () => { - it('should reset', () => { - const manager = new ViewManager(createCardAPI()); - - const view = createView(); - manager.setView(view); - manager.reset(); - - expect(manager.getView()).toBeNull(); - }); -}); - -describe('ViewManager.setViewDefault', () => { - it('should set default view', () => { - const api = createCardAPI(); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig()); - vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager()); - - const manager = new ViewManager(api); - manager.setViewDefault(); - - expect(manager.getView()?.view).toBe('live'); - expect(manager.getView()?.camera).toBe('camera'); - expect(api.getAutoUpdateManager().startDefaultViewTimer).toBeCalled(); - }); - - it('should not set default view without config', () => { - const api = createCardAPI(); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue(null); - - const manager = new ViewManager(api); - manager.setViewDefault(); - - expect(manager.getView()).toBeNull(); - expect(api.getAutoUpdateManager().startDefaultViewTimer).not.toBeCalled(); - }); - - it('should cycle camera when configured', () => { - const cameraManager = createCameraManager(); - vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue( - new Set(['camera_1', 'camera_2']), - ); - const api = createCardAPI(); - vi.mocked(api.getCameraManager).mockReturnValue(cameraManager); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue( - createConfig({ - view: { - update_cycle_camera: true, - }, - }), - ); - const manager = new ViewManager(api); - - manager.setViewDefault(); - expect(manager.getView()?.camera).toBe('camera_1'); - - manager.setViewDefault(); - expect(manager.getView()?.camera).toBe('camera_2'); - - manager.setViewDefault(); - expect(manager.getView()?.camera).toBe('camera_1'); - - // When a parameter is specified, it will not cycle. - manager.setViewDefault({ cameraID: 'camera_1' }); - expect(manager.getView()?.camera).toBe('camera_1'); - }); - - it('should respect parameters', () => { - const cameraManager = createCameraManager(); - vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue( - new Set(['camera.kitchen', 'camera.office']), - ); - const api = createCardAPI(); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig()); - vi.mocked(api.getCameraManager).mockReturnValue(cameraManager); - const manager = new ViewManager(api); - - manager.setViewDefault({ - cameraID: 'camera.office', - substream: 'camera.office_hd', - }); - expect(manager.getView()?.view).toBe('live'); - expect(manager.getView()?.camera).toBe('camera.office'); - expect(manager.getView()?.context?.live?.overrides).toEqual( - new Map([['camera.office', 'camera.office_hd']]), - ); - }); -}); - -describe('ViewManager.setViewByParameters', () => { - it('should set view by parameters specifying camera and view', () => { - const api = createCardAPI(); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig()); - vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager()); - - const manager = new ViewManager(api); - manager.setViewByParameters({ - cameraID: 'camera', - viewName: 'clips', - }); - - expect(manager.getView()?.view).toBe('clips'); - expect(manager.getView()?.camera).toBe('camera'); - }); - - it('should set view by parameters using existing view if unspecified', () => { - const cameraManager = createCameraManager(); - vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue( - new Set(['camera_1', 'camera_2']), - ); - - const api = createCardAPI(); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig()); - vi.mocked(api.getCameraManager).mockReturnValue(cameraManager); - - const manager = new ViewManager(api); - manager.setViewByParameters({ - cameraID: 'camera_1', - viewName: 'clips', - }); - - manager.setViewByParameters({ - cameraID: 'camera_2', - }); - - expect(manager.getView()?.view).toBe('clips'); - expect(manager.getView()?.camera).toBe('camera_2'); - }); - - it('should set view by parameters using config as fallback', () => { - const cameraManager = createCameraManager(); - vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue( - new Set(['camera_1', 'camera_2']), - ); - - const api = createCardAPI(); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig()); - vi.mocked(api.getCameraManager).mockReturnValue(cameraManager); - - const manager = new ViewManager(api); - manager.setViewByParameters({ - cameraID: 'camera_1', - // No prior view, and no specified view. This could happen during query - // string based initialization. - }); - - expect(manager.getView()?.view).toBe('live'); - expect(manager.getView()?.camera).toBe('camera_1'); - }); - - it('should not set view by parameters without config', () => { - const manager = new ViewManager(createCardAPI()); - - manager.setViewByParameters({ - viewName: 'live', - }); - - expect(manager.getView()).toBeNull(); - }); - - it('should not set view by parameters without visible cameras', () => { - const cameraManager = createCameraManager(); - vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(new Set()); - - const api = createCardAPI(); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig()); - vi.mocked(api.getCameraManager).mockReturnValue(cameraManager); - - const manager = new ViewManager(api); - manager.setViewByParameters({ - viewName: 'live', - }); - - expect(manager.getView()).toBeNull(); - }); - - describe('should set view by parameters and respect display mode in config for view', () => { - it.each([ - ['media' as const], - ['clip' as const], - ['recording' as const], - ['snapshot' as const], - ['live' as const], - ])('%s', (viewName: FrigateCardView) => { - const api = createCardAPI(); - vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager()); - vi.mocked(api.getConfigManager()).getConfig.mockReturnValue( - createConfig({ - media_viewer: { - display: { - mode: 'grid', - }, - }, - live: { - display: { - mode: 'grid', - }, - }, - }), - ); - const manager = new ViewManager(api); - - manager.setViewByParameters({ - cameraID: 'camera', - viewName: viewName, - }); - - expect(manager.getView()?.displayMode).toBe('grid'); - }); - }); - - describe('should set view by parameters and leave display mode unset for view', () => { - it.each([ - ['media' as const], - ['clip' as const], - ['recording' as const], - ['snapshot' as const], - ['live' as const], - ])('%s', (viewName: FrigateCardView) => { - const api = createCardAPI(); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig()); - vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager()); - const manager = new ViewManager(api); - - manager.setViewByParameters({ - cameraID: 'camera', - viewName: viewName, - }); - - expect(manager.getView()?.displayMode).toBe('single'); - }); - }); - - it('should set view by parameters using config as fallback', () => { - const cameraManager = createCameraManager(); - vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue( - new Set(['camera_1', 'camera_2']), - ); - - const api = createCardAPI(); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig()); - vi.mocked(api.getCameraManager).mockReturnValue(cameraManager); - vi.mocked(getAllDependentCameras).mockReturnValue( - new Set(['camera_1', 'camera_1_hd']), - ); - - const manager = new ViewManager(api); - manager.setViewByParameters({ - cameraID: 'camera_1', - viewName: 'live', - substream: 'camera_1_hd', - }); - - expect(manager.getView()?.view).toBe('live'); - expect(manager.getView()?.camera).toBe('camera_1'); - expect(manager.getView()?.context?.live?.overrides).toEqual( - new Map([['camera_1', 'camera_1_hd']]), - ); - }); -}); - -// @vitest-environment jsdom -describe('ViewManager.setViewWithNewDisplayMode', () => { - it('should set display mode', async () => { - const api = createCardAPI(); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig()); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); - vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager()); - const manager = new ViewManager(api); - manager.setView(createView()); - - await manager.setViewWithNewDisplayMode('grid'); - - expect(manager.getView()?.displayMode).toBe('grid'); - }); - - it('should not set display mode without view', async () => { - const api = createCardAPI(); - vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig()); - vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager()); - const manager = new ViewManager(api); - - manager.setViewWithNewDisplayMode('grid'); - - expect(manager.getView()).toBeNull(); - }); - - it('should set display mode to grid and create new query', async () => { - const cameraManager = createCameraManager(); - vi.mocked(cameraManager.getStore().getVisibleCameraCount).mockReturnValue(2); - vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue( - new Set(['camera_1', 'camera_2']), - ); - - const api = createCardAPI(); - vi.mocked(api.getCameraManager).mockReturnValue(cameraManager); - - const hass = createHASS(); - vi.mocked(api.getHASSManager()).getHASS.mockReturnValue(hass); - - const media = generateViewMediaArray({ count: 5 }); - vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(media); - - const manager = new ViewManager(api); - const query = new EventMediaQueries([ - { type: QueryType.Event, cameraIDs: new Set(['camera_1']), hasClip: true }, - ]); - - manager.setView( - createView({ - camera: 'camera_1', - view: 'clip', - query: query, - }), - ); - - await manager.setViewWithNewDisplayMode('grid'); - - expect(manager.getView()?.queryResults?.getResults()).toBe(media); - expect(cameraManager.executeMediaQueries).toBeCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - type: 'event-query', - cameraIDs: new Set(['camera_1', 'camera_2']), - hasClip: true, - }), - ]), - ); - }); - - it('should set display mode to single and create new query', async () => { - const cameraManager = createCameraManager(); - vi.mocked(cameraManager.getStore().getVisibleCameraCount).mockReturnValue(2); - vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue( - new Set(['camera_1', 'camera_2']), - ); - - const api = createCardAPI(); - vi.mocked(api.getCameraManager).mockReturnValue(cameraManager); - - const hass = createHASS(); - vi.mocked(api.getHASSManager()).getHASS.mockReturnValue(hass); - - const media = generateViewMediaArray({ count: 5 }); - vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(media); - - const manager = new ViewManager(api); - const query = new EventMediaQueries([ - { - type: QueryType.Event, - cameraIDs: new Set(['camera_1', 'camera_2']), - hasClip: true, - }, - ]); - - manager.setView( - createView({ - view: 'clip', - camera: 'camera_2', - query: query, - }), - ); - - await manager.setViewWithNewDisplayMode('single'); - - expect(manager.getView()?.queryResults?.getResults()).toBe(media); - expect(cameraManager.executeMediaQueries).toBeCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - type: 'event-query', - cameraIDs: new Set(['camera_2']), - hasClip: true, - }), - ]), - ); - }); - - it('should set display mode to single and handle failed new query', async () => { - const cameraManager = createCameraManager(); - vi.mocked(cameraManager.getStore().getVisibleCameraCount).mockReturnValue(2); - vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue( - new Set(['camera_1', 'camera_2']), - ); - - const api = createCardAPI(); - vi.mocked(api.getCameraManager).mockReturnValue(cameraManager); - - const manager = new ViewManager(api); - const query = new EventMediaQueries([ - { - type: QueryType.Event, - cameraIDs: new Set(['camera_1', 'camera_2']), - hasClip: true, - }, - ]); - - const originalView = createView({ - view: 'clip', - camera: 'camera_2', - query: query, - }); - manager.setView(originalView); - - // Query execution fails / returns null. - vi.mocked(cameraManager.executeMediaQueries).mockRejectedValue(null); - - await manager.setViewWithNewDisplayMode('single'); - - expect(manager.getView()).toBe(originalView); - }); - - it('should set display mode and handle empty new query results', async () => { - const cameraManager = createCameraManager(); - vi.mocked(cameraManager.getStore().getVisibleCameraCount).mockReturnValue(2); - vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue( - new Set(['camera_1', 'camera_2']), - ); - - const api = createCardAPI(); - vi.mocked(api.getCameraManager).mockReturnValue(cameraManager); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); - - const manager = new ViewManager(api); - - const query = new EventMediaQueries([ - { - type: QueryType.Event, - cameraIDs: new Set(['camera_1']), - hasClip: true, - }, - ]); - const originalView = createView({ - view: 'clip', - camera: 'camera_2', - query: query, - }); - manager.setView(originalView); - - await manager.setViewWithNewDisplayMode('grid'); - - vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(null); - - // Empty queries will not be executed, so view will not be changed. - expect(manager.getView()?.displayMode).toBeNull(); - }); -}); - -describe('ViewManager.setViewWithSubstream', () => { - it('should set new equal view with no dependencies', () => { - const view = createView({ - view: 'live', - camera: 'camera', - }); - vi.mocked(getAllDependentCameras).mockReturnValue(new Set(['camera'])); - - const manager = new ViewManager(createCardAPI()); - manager.setView(view); - manager.setViewWithSubstream(); - - expect(manager.getView()?.camera).toBe(view.camera); - expect(manager.getView()?.view).toBe(view.view); - expect(manager.getView()?.context).toEqual(view.context); - }); - - it('should set new view with next substream', () => { - const view = createView({ - view: 'live', - camera: 'camera', - }); - vi.mocked(getAllDependentCameras).mockReturnValue(new Set(['camera', 'camera2'])); - - const manager = new ViewManager(createCardAPI()); - manager.setView(view); - manager.setViewWithSubstream(); - - expect(manager.getView()?.context?.live?.overrides).toEqual( - new Map([['camera', 'camera2']]), - ); - }); - - it('should set new view with next substream when view has invalid substream', () => { - const view = createView({ - view: 'live', - camera: 'camera', - context: { - live: { - overrides: new Map([['camera', 'camera-that-does-not-exist']]), - }, - }, - }); - vi.mocked(getAllDependentCameras).mockReturnValue(new Set(['camera', 'camera2'])); - - const manager = new ViewManager(createCardAPI()); - manager.setView(view); - manager.setViewWithSubstream(); - - expect(manager.getView()?.context?.live?.overrides).toEqual( - new Map([['camera', 'camera']]), - ); - }); - - it('should set new view with selected substream', () => { - const view = createView({ - view: 'live', - camera: 'camera', - }); - - const manager = new ViewManager(createCardAPI()); - manager.setView(view); - manager.setViewWithSubstream('substream'); - - expect(manager.getView()?.context?.live?.overrides).toEqual( - new Map([['camera', 'substream']]), - ); - }); - - it('should not set view with next substream without an existing view', () => { - const manager = new ViewManager(createCardAPI()); - manager.setViewWithSubstream(); - expect(manager.getView()).toBeNull(); - }); - - it('should not set view with selected substream without an existing view', () => { - const manager = new ViewManager(createCardAPI()); - manager.setViewWithSubstream('substream'); - expect(manager.getView()).toBeNull(); - }); - - it('should not set view without substream without an existing view', () => { - const manager = new ViewManager(createCardAPI()); - manager.setViewWithoutSubstream(); - expect(manager.getView()).toBeNull(); - }); - - it('should set new view without substream', () => { - const view = createView({ - view: 'live', - camera: 'camera', - context: { - live: { - overrides: new Map([['camera', 'camera']]), - }, - }, - }); - - const manager = new ViewManager(createCardAPI()); - manager.setView(view); - manager.setViewWithoutSubstream(); - - expect(manager.getView()?.context?.live?.overrides).toEqual(new Map()); - }); - - it('should set new view without substream', () => { - const view = createView({ - view: 'live', - camera: 'camera', - context: { - live: { - overrides: new Map([['camera-2', 'camera-3']]), - }, - }, - }); - - const manager = new ViewManager(createCardAPI()); - manager.setView(view); - manager.setViewWithoutSubstream(); - - expect(manager.getView()?.context?.live?.overrides).toEqual( - view.context?.live?.overrides, - ); - }); -}); - -describe('ViewManager.isViewSupportedByCamera', () => { - it.each([ - ['live' as const, true], - ['image' as const, true], - ['diagnostics' as const, true], - ['clip' as const, false], - ['clips' as const, false], - ['snapshot' as const, false], - ['snapshots' as const, false], - ['recording' as const, false], - ['recordings' as const, false], - ['timeline' as const, false], - ['media' as const, false], - ])('%s', (viewName: FrigateCardView, expected: boolean) => { - const api = createCardAPI(); - const cameraManager = createCameraManager(); - vi.mocked(cameraManager.getCameraCapabilities).mockReturnValue({ - canFavoriteEvents: false, - canFavoriteRecordings: false, - canSeek: false, - supportsClips: false, - supportsRecordings: false, - supportsSnapshots: false, - supportsTimeline: false, - }); - vi.mocked(api.getCameraManager).mockReturnValue(cameraManager); - const manager = new ViewManager(api); - - expect(manager.isViewSupportedByCamera('camera', viewName)).toBe(expected); - }); -}); diff --git a/tests/utils/menu-controller.test.ts b/tests/utils/menu-controller.test.ts index 43f2e629..1799c611 100644 --- a/tests/utils/menu-controller.test.ts +++ b/tests/utils/menu-controller.test.ts @@ -7,8 +7,8 @@ import { CameraManagerCameraMetadata } from '../../src/camera-manager/types'; import { FrigateCardConfig, MenuItem, ViewDisplayMode } from '../../src/config/types'; import { FrigateCardMediaPlayer } from '../../src/types'; import { createFrigateCardCustomAction } from '../../src/utils/action'; -import { MediaPlayerManager } from '../../src/utils/card-controller/media-player-manager'; -import { MicrophoneManager } from '../../src/utils/card-controller/microphone-manager'; +import { MediaPlayerManager } from '../../src/card-controller/media-player-manager'; +import { MicrophoneManager } from '../../src/card-controller/microphone-manager'; import { MenuButtonController, MenuButtonControllerOptions, @@ -31,7 +31,7 @@ import { vi.mock('../../src/camera-manager/manager.js'); vi.mock('../../src/utils/media-player-controller.js'); -vi.mock('../../src/utils/card-controller/microphone-manager.js'); +vi.mock('../../src/card-controller/microphone-manager.js'); const calculateButtons = ( controller: MenuButtonController,