diff --git a/src/card-controller/message-manager.ts b/src/card-controller/message-manager.ts index 940f56e7..b754c7b1 100644 --- a/src/card-controller/message-manager.ts +++ b/src/card-controller/message-manager.ts @@ -1,12 +1,18 @@ -import { - FrigateCardError, - MESSAGE_TYPE_PRIORITIES, - Message, - MessageType, -} from '../types'; +import { FrigateCardError, Message, MessageType } from '../types'; import { errorToConsole } from '../utils/basic'; import { CardMessageAPI } from './types'; +type MessagePriority = { + [type in MessageType]: number; +}; + +const MESSAGE_TYPE_PRIORITIES: MessagePriority = { + info: 10, + error: 20, + connection: 30, + diagnostics: 40, +}; + export class MessageManager { protected _message: Message | null = null; protected _api: CardMessageAPI; @@ -52,17 +58,20 @@ export class MessageManager { errorToConsole(error); this.setMessageIfHigherPriority({ - message: prefix ? `${prefix}: ${error.message}` : error.message, + message: prefix ? `${prefix}: ${error.message}` : String(error.message), type: 'error', ...(error instanceof FrigateCardError && { context: error.context }), }); } public setMessageIfHigherPriority(message: Message): boolean { + const resolveMessageType = (message: Message): MessageType => { + return message.type ?? 'info'; + }; const currentPriority = this._message - ? MESSAGE_TYPE_PRIORITIES[this._message.type] + ? MESSAGE_TYPE_PRIORITIES[resolveMessageType(this._message)] : 0; - const newPriority = MESSAGE_TYPE_PRIORITIES[message.type]; + const newPriority = MESSAGE_TYPE_PRIORITIES[resolveMessageType(message)]; if (this._message && newPriority < currentPriority) { return false; diff --git a/src/components-lib/live/live-controller.ts b/src/components-lib/live/live-controller.ts index 101dbe71..5616292c 100644 --- a/src/components-lib/live/live-controller.ts +++ b/src/components-lib/live/live-controller.ts @@ -1,10 +1,10 @@ import { LitElement, ReactiveController } from 'lit'; -import { FrigateCardMessageEventTarget } from '../../components/message.js'; import { MediaLoadedInfo } from '../../types.js'; import { FrigateCardMediaLoadedEventTarget, dispatchExistingMediaLoadedInfoAsEvent, } from '../../utils/media-info.js'; +import { FrigateCardMessageEventTarget } from '../message/dispatch.js'; interface LiveViewContext { // A cameraID override (used for dependencies/substreams to force a different diff --git a/src/components-lib/message/controller.ts b/src/components-lib/message/controller.ts new file mode 100644 index 00000000..d201a33e --- /dev/null +++ b/src/components-lib/message/controller.ts @@ -0,0 +1,43 @@ +import yaml from 'js-yaml'; +import { TROUBLESHOOTING_URL } from '../../const'; +import { Message } from '../../types'; + +export class MessageController { + public getMessageString(message: Message): string { + return ( + message.message + + (message.context && typeof message.context === 'string' + ? ': ' + message.context + : '') + ); + } + + public getIcon(message: Message): string { + return message.icon + ? message.icon + : message.type === 'error' + ? 'mdi:alert-circle' + : 'mdi:information-outline'; + } + + public shouldShowTroubleshootingURL(message: Message): boolean { + return message.type === 'error'; + } + + public getTroubleshootingURL(message: Message): string { + return message.troubleshootingURL ?? TROUBLESHOOTING_URL; + } + + public getContextStrings(message: Message): string[] { + if (Array.isArray(message.context)) { + return message.context.map((contextItem) => yaml.dump(contextItem)); + } + if (typeof message.context === 'object') { + return [yaml.dump(message.context)]; + } + if (typeof message.context === 'string') { + return [message.context]; + } + return []; + } +} diff --git a/src/components-lib/message/dispatch.ts b/src/components-lib/message/dispatch.ts new file mode 100644 index 00000000..052730d5 --- /dev/null +++ b/src/components-lib/message/dispatch.ts @@ -0,0 +1,47 @@ +import { FrigateCardError, Message } from '../../types'; +import { dispatchFrigateCardEvent } from '../../utils/basic'; + +// Facilitates correct typing of event handlers. +export interface FrigateCardMessageEventTarget extends EventTarget { + addEventListener( + event: 'frigate-card:message', + listener: (this: FrigateCardMessageEventTarget, ev: CustomEvent) => void, + options?: AddEventListenerOptions | boolean, + ): void; + addEventListener( + type: string, + callback: EventListenerOrEventListenerObject, + options?: AddEventListenerOptions | boolean, + ): void; + removeEventListener( + event: 'frigate-card:message', + listener: (this: FrigateCardMessageEventTarget, ev: CustomEvent) => void, + options?: boolean | EventListenerOptions, + ): void; + removeEventListener( + type: string, + callback: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions, + ): void; +} + +/** + * Dispatch an event with an error message to show to the user. Calling this + * method will grind the card to a halt, so should only be used for "global" / + * critical errors (i.e. not for individual errors with a given camera, since + * there may be multiple correctly functioning cameras in a grid). + * @param element The element to send the event. + * @param message The message to show. + */ +export const dispatchFrigateCardErrorEvent = ( + element: EventTarget, + error: unknown, +): void => { + if (error instanceof Error) { + dispatchFrigateCardEvent(element, 'message', { + message: error.message, + type: 'error', + ...(error instanceof FrigateCardError && { context: error.context }), + }); + } +}; diff --git a/src/components/diagnostics.ts b/src/components/diagnostics.ts index b44469d3..e97c0f12 100644 --- a/src/components/diagnostics.ts +++ b/src/components/diagnostics.ts @@ -41,8 +41,7 @@ export class FrigateCardDiagnostics extends LitElement { protected render(): TemplateResult | void { return renderMessage({ message: localize('error.diagnostics'), - type: 'diagnostics', - icon: 'mdi:information', + icon: 'mdi:cogs', context: this._diagnostics, }); } diff --git a/src/components/elements.ts b/src/components/elements.ts index 5206a1f0..20430822 100644 --- a/src/components/elements.ts +++ b/src/components/elements.ts @@ -12,6 +12,7 @@ import { ConditionsManagerEpoch, evaluateConditionViaEvent, } from '../card-controller/conditions-manager.js'; +import { dispatchFrigateCardErrorEvent } from '../components-lib/message/dispatch.js'; import { FrigateConditional, MenuIcon, @@ -29,7 +30,6 @@ import { localize } from '../localize/localize.js'; import elementsStyle from '../scss/elements.scss'; import { FrigateCardError } from '../types.js'; import { dispatchFrigateCardEvent, errorToConsole } from '../utils/basic.js'; -import { dispatchFrigateCardErrorEvent } from './message.js'; /* A note on picture element rendering: * diff --git a/src/components/gallery.ts b/src/components/gallery.ts index fdc12af0..30c74be1 100644 --- a/src/components/gallery.ts +++ b/src/components/gallery.ts @@ -30,7 +30,10 @@ import { EventMediaQueries, RecordingMediaQueries } from '../view/media-queries' import { MediaQueriesClassifier } from '../view/media-queries-classifier'; import { MediaQueriesResults } from '../view/media-queries-results'; import './media-filter'; -import { renderMessage, renderProgressIndicator } from './message.js'; +import './message.js'; +import { renderMessage } from './message.js'; +import './progress-indicator.js'; +import { renderProgressIndicator } from './progress-indicator.js'; import './surround-basic'; import './thumbnail.js'; import { THUMBNAIL_DETAILS_WIDTH_MIN } from './thumbnail.js'; diff --git a/src/components/live/carousel.ts b/src/components/live/carousel.ts index 960a4df1..9002e08c 100644 --- a/src/components/live/carousel.ts +++ b/src/components/live/carousel.ts @@ -18,6 +18,7 @@ import { import { MicrophoneState } from '../../card-controller/types.js'; import { ViewManagerEpoch } from '../../card-controller/view/types.js'; import { MediaActionsController } from '../../components-lib/media-actions-controller.js'; +import { dispatchFrigateCardErrorEvent } from '../../components-lib/message/dispatch.js'; import { ZoomSettingsObserved } from '../../components-lib/zoom/types.js'; import { handleZoomSettingsObservedEvent } from '../../components-lib/zoom/zoom-view-context.js'; import { @@ -42,7 +43,6 @@ import { getTextDirection } from '../../utils/text-direction.js'; import { View } from '../../view/view.js'; import '../carousel'; import { EmblaCarouselPlugins } from '../carousel.js'; -import { dispatchFrigateCardErrorEvent } from '../message.js'; import '../next-prev-control.js'; import '../ptz.js'; import { FrigateCardPTZ } from '../ptz.js'; diff --git a/src/components/live/providers/jsmpeg.ts b/src/components/live/providers/jsmpeg.ts index ef026aac..062425fe 100644 --- a/src/components/live/providers/jsmpeg.ts +++ b/src/components/live/providers/jsmpeg.ts @@ -26,7 +26,10 @@ import { dispatchMediaPlayEvent, } from '../../../utils/media-info.js'; import { Timer } from '../../../utils/timer.js'; -import { renderMessage, renderProgressIndicator } from '../../message.js'; +import '../../message.js'; +import { renderMessage } from '../../message.js'; +import '../../progress-indicator.js'; +import { renderProgressIndicator } from '../../progress-indicator.js'; // Number of seconds a signed URL is valid for. const JSMPEG_URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60; diff --git a/src/components/live/providers/webrtc-card.ts b/src/components/live/providers/webrtc-card.ts index 88352756..b19eaf6f 100644 --- a/src/components/live/providers/webrtc-card.ts +++ b/src/components/live/providers/webrtc-card.ts @@ -30,7 +30,10 @@ import { } from '../../../utils/media.js'; import { screenshotMedia } from '../../../utils/screenshot.js'; import { renderTask } from '../../../utils/task.js'; -import { renderMessage, renderProgressIndicator } from '../../message.js'; +import '../../message.js'; +import { renderMessage } from '../../message.js'; +import '../../progress-indicator.js'; +import { renderProgressIndicator } from '../../progress-indicator.js'; import { VideoRTC } from './go2rtc/video-rtc.js'; // Create a wrapper for AlexxIT's WebRTC card diff --git a/src/components/message.ts b/src/components/message.ts index 41b344cd..71510026 100644 --- a/src/components/message.ts +++ b/src/components/message.ts @@ -1,39 +1,39 @@ -import yaml from 'js-yaml'; import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit'; import { customElement, property } from 'lit/decorators.js'; -import { ClassInfo, classMap } from 'lit/directives/class-map.js'; -import { ref, Ref } from 'lit/directives/ref.js'; -import { CardWideConfig } from '../config/types.js'; -import { TROUBLESHOOTING_URL } from '../const.js'; +import { classMap } from 'lit/directives/class-map.js'; +import { MessageController } from '../components-lib/message/controller.js'; import { localize } from '../localize/localize.js'; import messageStyle from '../scss/message.scss'; -import { FrigateCardError, Message, MessageType } from '../types.js'; -import { dispatchFrigateCardEvent } from '../utils/basic.js'; +import { Message } from '../types.js'; import './icon.js'; +export function renderMessage(message: Message | null): TemplateResult { + return html` `; +} @customElement('frigate-card-message') export class FrigateCardMessage extends LitElement { @property({ attribute: false }) - public message: string | TemplateResult<1> = ''; + public message?: Message; - @property({ attribute: false }) - public context?: unknown; + private _controller = new MessageController(); - @property({ attribute: false }) - public icon?: string; + protected render(): TemplateResult | void { + if (!this.message) { + return; + } - @property({ attribute: true, type: Boolean }) - public dotdotdot?: boolean; + const messageTemplate = html` + ${this._controller.getMessageString(this.message)}. + ${this._controller.shouldShowTroubleshootingURL(this.message) + ? html`${localize('error.troubleshooting')}` + : ''} + `; - // Render the menu. - protected render(): TemplateResult { - const icon = this.icon ? this.icon : 'mdi:information-outline'; + const icon = this._controller.getIcon(this.message); const classes = { - dotdotdot: !!this.dotdotdot, - }; - - const renderContext = (contextItem: unknown): TemplateResult => { - return html`
${yaml.dump(contextItem)}
`; + dotdotdot: !!this.message?.dotdotdot, }; return html`
@@ -42,18 +42,10 @@ export class FrigateCardMessage extends LitElement {
- - ${this.message - ? html`${this.message}${this.context && typeof this.context === 'string' - ? ': ' + this.context - : ''}` - : ''} - - ${this.context && Array.isArray(this.context) - ? this.context.map((contextItem) => renderContext(contextItem)) - : typeof this.context === 'object' - ? renderContext(this.context) - : ''} + ${messageTemplate} + ${this._controller + .getContextStrings(this.message) + .map((contextItem) => html`
${contextItem}
`)}
`; @@ -64,190 +56,8 @@ export class FrigateCardMessage extends LitElement { } } -@customElement('frigate-card-error-message') -export class FrigateCardErrorMessage extends LitElement { - @property({ attribute: false }) - public message?: Message; - - protected render(): TemplateResult | void { - if (!this.message) { - return; - } - return html` ${localize('error.troubleshooting')}.`} - .icon=${this.message.icon ?? 'mdi:alert-circle'} - .context=${this.message.context} - .dotdotdot=${this.message.dotdotdot} - > - `; - } - - static get styles(): CSSResultGroup { - return unsafeCSS(messageStyle); - } -} - -type FrigateCardProgressIndicatorSize = 'tiny' | 'small' | 'medium' | 'large'; - -@customElement('frigate-card-progress-indicator') -export class FrigateCardProgressIndicator extends LitElement { - @property({ attribute: false }) - public message: string | TemplateResult = ''; - - @property({ attribute: false }) - public animated = false; - - @property({ attribute: false }) - public size: FrigateCardProgressIndicatorSize = 'large'; - - protected render(): TemplateResult { - return html`
- ${this.animated - ? html` - ` - : html``} - ${this.message ? html`${this.message}` : html``} -
`; - } - - static get styles(): CSSResultGroup { - return unsafeCSS(messageStyle); - } -} - -export function renderMessage(message: Message | null): TemplateResult { - if (message?.type === 'error') { - return html` `; - } else if (message) { - return html` `; - } - return html``; -} - -export function renderProgressIndicator(options?: { - message?: string; - cardWideConfig?: CardWideConfig | null; - componentRef?: Ref; - classes?: ClassInfo; - size?: FrigateCardProgressIndicatorSize; -}): TemplateResult { - return html` - - - `; -} - -/** - * Dispatch an event with a message to show to the user. Calling this method - * will grind the card to a halt, so should only be used for "global" / critical - * errors (i.e. not for individual errors with a given camera, since there may - * be multiple correctly functioning cameras in a grid). - * @param element The element to send the event. - * @param message The message to show. - * @param options Optional icon and context to include. - */ -function dispatchMessageEvent( - element: EventTarget, - message: string, - type: MessageType, - options?: { - icon?: string; - context?: unknown; - }, -): void { - dispatchFrigateCardEvent(element, 'message', { - message: message, - type: type, - icon: options?.icon, - context: options?.context, - }); -} - -/** - * Dispatch an event with an error message to show to the user. Calling this - * method will grind the card to a halt, so should only be used for "global" / - * critical errors (i.e. not for individual errors with a given camera, since - * there may be multiple correctly functioning cameras in a grid). - * @param element The element to send the event. - * @param message The message to show. - * @param options Optional context to include. - */ -function dispatchErrorMessageEvent( - element: EventTarget, - message: string, - options?: { - context?: unknown; - }, -): void { - dispatchMessageEvent(element, message, 'error', { - context: options?.context, - }); -} - -/** - * Dispatch an event with an error message to show to the user. Calling this - * method will grind the card to a halt, so should only be used for "global" / - * critical errors (i.e. not for individual errors with a given camera, since - * there may be multiple correctly functioning cameras in a grid). - * @param element The element to send the event. - * @param message The message to show. - */ -export function dispatchFrigateCardErrorEvent( - element: EventTarget, - error: unknown, -): void { - if (error instanceof Error) { - dispatchErrorMessageEvent(element, error.message, { - ...(error instanceof FrigateCardError && { context: error.context }), - }); - } -} - -// Facilitates correct typing of event handlers. -export interface FrigateCardMessageEventTarget extends EventTarget { - addEventListener( - event: 'frigate-card:message', - listener: (this: FrigateCardMessageEventTarget, ev: CustomEvent) => void, - options?: AddEventListenerOptions | boolean, - ): void; - addEventListener( - type: string, - callback: EventListenerOrEventListenerObject, - options?: AddEventListenerOptions | boolean, - ): void; - removeEventListener( - event: 'frigate-card:message', - listener: (this: FrigateCardMessageEventTarget, ev: CustomEvent) => void, - options?: boolean | EventListenerOptions, - ): void; - removeEventListener( - type: string, - callback: EventListenerOrEventListenerObject, - options?: boolean | EventListenerOptions, - ): void; -} - declare global { interface HTMLElementTagNameMap { - 'frigate-card-progress-indicator': FrigateCardProgressIndicator; - 'frigate-card-error-message': FrigateCardErrorMessage; 'frigate-card-message': FrigateCardMessage; } } diff --git a/src/components/progress-indicator.ts b/src/components/progress-indicator.ts new file mode 100644 index 00000000..196a4367 --- /dev/null +++ b/src/components/progress-indicator.ts @@ -0,0 +1,63 @@ +import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { ClassInfo, classMap } from 'lit/directives/class-map.js'; +import { ref, Ref } from 'lit/directives/ref.js'; +import { CardWideConfig } from '../config/types.js'; +import messageStyle from '../scss/message.scss'; +import './icon.js'; + +type FrigateCardProgressIndicatorSize = 'tiny' | 'small' | 'medium' | 'large'; + +export function renderProgressIndicator(options?: { + message?: string; + cardWideConfig?: CardWideConfig | null; + componentRef?: Ref; + classes?: ClassInfo; + size?: FrigateCardProgressIndicatorSize; +}): TemplateResult { + return html` + + + `; +} + +@customElement('frigate-card-progress-indicator') +export class FrigateCardProgressIndicator extends LitElement { + @property({ attribute: false }) + public message: string = ''; + + @property({ attribute: false }) + public animated = false; + + @property({ attribute: false }) + public size: FrigateCardProgressIndicatorSize = 'large'; + + protected render(): TemplateResult { + return html`
+ ${this.animated + ? html` + ` + : html``} + ${this.message ? html`${this.message}` : html``} +
`; + } + + static get styles(): CSSResultGroup { + return unsafeCSS(messageStyle); + } +} + +declare global { + interface HTMLElementTagNameMap { + 'frigate-card-progress-indicator': FrigateCardProgressIndicator; + } +} diff --git a/src/components/thumbnail.ts b/src/components/thumbnail.ts index 056c692f..23c8bd86 100644 --- a/src/components/thumbnail.ts +++ b/src/components/thumbnail.ts @@ -14,6 +14,7 @@ import { CameraManager } from '../camera-manager/manager.js'; import { CameraManagerCameraMetadata } from '../camera-manager/types.js'; import { RemoveContextViewModifier } from '../card-controller/view/modifiers/remove-context.js'; import { ViewManagerEpoch } from '../card-controller/view/types.js'; +import { dispatchFrigateCardErrorEvent } from '../components-lib/message/dispatch.js'; import { localize } from '../localize/localize.js'; import thumbnailDetailsStyle from '../scss/thumbnail-details.scss'; import thumbnailFeatureTextStyle from '../scss/thumbnail-feature-text.scss'; @@ -32,7 +33,6 @@ import { renderTask } from '../utils/task.js'; import { createFetchThumbnailTask, FetchThumbnailTaskArgs } from '../utils/thumbnail.js'; import { ViewMediaClassifier } from '../view/media-classifier.js'; import { EventViewMedia, RecordingViewMedia, ViewMedia } from '../view/media.js'; -import { dispatchFrigateCardErrorEvent } from './message.js'; // The minimum width of a thumbnail with details enabled. export const THUMBNAIL_DETAILS_WIDTH_MIN = 300; diff --git a/src/components/viewer/provider.ts b/src/components/viewer/provider.ts index b706ea3a..b9b7376d 100644 --- a/src/components/viewer/provider.ts +++ b/src/components/viewer/provider.ts @@ -51,7 +51,7 @@ import { screenshotMedia } from '../../utils/screenshot.js'; import { ViewMediaClassifier } from '../../view/media-classifier.js'; import { MediaQueriesClassifier } from '../../view/media-queries-classifier.js'; import { VideoContentType, ViewMedia } from '../../view/media.js'; -import { renderProgressIndicator } from '../message.js'; +import { renderProgressIndicator } from '../progress-indicator.js'; @customElement('frigate-card-viewer-provider') export class FrigateCardViewerProvider diff --git a/src/types.ts b/src/types.ts index 5ded0c3b..ed03741d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -45,23 +45,13 @@ export interface MediaLoadedInfo { export type MessageType = 'info' | 'error' | 'connection' | 'diagnostics'; -type MessagePriority = { - [type in MessageType]: number; -}; - -export const MESSAGE_TYPE_PRIORITIES: MessagePriority = { - info: 10, - error: 20, - connection: 30, - diagnostics: 40, -}; - export interface Message { - message: unknown; - type: MessageType; + message: string; + type?: MessageType; icon?: string; context?: unknown; dotdotdot?: boolean; + troubleshootingURL?: string; } export interface FrigateCardMediaPlayer { diff --git a/src/utils/task.ts b/src/utils/task.ts index ed93049d..22502712 100644 --- a/src/utils/task.ts +++ b/src/utils/task.ts @@ -1,6 +1,6 @@ import { Task } from '@lit-labs/task'; import { html, TemplateResult } from 'lit'; -import { renderProgressIndicator } from '../components/message'; +import { renderProgressIndicator } from '../components/progress-indicator'; import { CardWideConfig } from '../config/types'; import { errorToConsole } from './basic'; diff --git a/tests/card-controller/message-manager.test.ts b/tests/card-controller/message-manager.test.ts index 38283cae..ffedd7f6 100644 --- a/tests/card-controller/message-manager.test.ts +++ b/tests/card-controller/message-manager.test.ts @@ -6,7 +6,7 @@ import { createCardAPI } from '../test-utils'; const createMessage = (options?: Partial): Message => { return { message: options?.message ?? 'message', - type: options?.type ?? 'info', + ...(!!options?.type && { type: options.type }), ...(!!options?.icon && { icon: options.icon }), ...(!!options?.context && { context: options.context }), ...(!!options?.dotdotdot && { dotdotdot: options.dotdotdot }), @@ -97,8 +97,11 @@ describe('MessageManager', () => { const errorMessage = createMessage({ type: 'error' }); manager.setMessageIfHigherPriority(errorMessage); - const infoMessage = createMessage({ type: 'info' }); - manager.setMessageIfHigherPriority(infoMessage); + const explicitInfoMessage = createMessage({ type: 'info' }); + manager.setMessageIfHigherPriority(explicitInfoMessage); + + const implicitInfoMessage = createMessage(); + manager.setMessageIfHigherPriority(implicitInfoMessage); expect(manager.getMessage()).toBe(errorMessage); diff --git a/tests/components-lib/message/controller.test.ts b/tests/components-lib/message/controller.test.ts new file mode 100644 index 00000000..444efcca --- /dev/null +++ b/tests/components-lib/message/controller.test.ts @@ -0,0 +1,149 @@ +import yaml from 'js-yaml'; +import { describe, expect, it } from 'vitest'; +import { MessageController } from '../../../src/components-lib/message/controller'; +import { TROUBLESHOOTING_URL } from '../../../src/const'; +import { Message, MessageType } from '../../../src/types'; + +describe('MessageController', () => { + describe('should return the correct message string', () => { + it('should return simple message string', () => { + const controller = new MessageController(); + const message: Message = { + message: 'Message', + type: 'info', + }; + expect(controller.getMessageString(message)).toBe('Message'); + }); + + it('should embed simple string context', () => { + const controller = new MessageController(); + const message: Message = { + message: 'Message', + context: 'Context', + type: 'info', + }; + expect(controller.getMessageString(message)).toBe('Message: Context'); + }); + }); + + describe('should return the correct icon', () => { + describe('when icon is specified', () => { + it.each([['info' as const], ['error' as const], ['connection' as const]])( + '%s', + (type: MessageType) => { + const controller = new MessageController(); + const message: Message = { + message: 'Message', + icon: 'mdi:car', + type, + }; + expect(controller.getIcon(message)).toBe('mdi:car'); + }, + ); + }); + + it('when type is an error', () => { + const controller = new MessageController(); + const message: Message = { + message: 'Message', + type: 'error', + }; + expect(controller.getIcon(message)).toBe('mdi:alert-circle'); + }); + + it('by default', () => { + const controller = new MessageController(); + const message: Message = { + message: 'Message', + }; + expect(controller.getIcon(message)).toBe('mdi:information-outline'); + }); + }); + + describe('should show troubleshooting link', () => { + it('should show for errors', () => { + const controller = new MessageController(); + const message: Message = { message: 'Error message', type: 'error' }; + expect(controller.shouldShowTroubleshootingURL(message)).toBe(true); + }); + + describe('should not show for other types', () => { + it.each([['info' as const], ['connection' as const]])( + '%s', + (type: MessageType) => { + const controller = new MessageController(); + const message: Message = { + message: 'Message', + icon: 'mdi:car', + type, + }; + expect(controller.shouldShowTroubleshootingURL(message)).toBe(false); + }, + ); + }); + + describe('should show correct URL', () => { + it('by default', () => { + const controller = new MessageController(); + const message: Message = { message: 'Error message', type: 'error' }; + expect(controller.getTroubleshootingURL(message)).toBe(TROUBLESHOOTING_URL); + }); + + it('when specified', () => { + const controller = new MessageController(); + const troubleshootingURL = 'http://localhost/troubleshooting.md'; + const message: Message = { + message: 'Error message', + type: 'error', + troubleshootingURL: troubleshootingURL, + }; + expect(controller.getTroubleshootingURL(message)).toBe(troubleshootingURL); + }); + }); + }); + + describe('should get context strings', () => { + it('for no context', () => { + const controller = new MessageController(); + const message: Message = { + message: 'Message', + type: 'info', + }; + expect(controller.getContextStrings(message)).toEqual([]); + }); + + it('for simple string', () => { + const controller = new MessageController(); + const message: Message = { + message: 'Message', + context: 'Context', + type: 'info', + }; + expect(controller.getContextStrings(message)).toEqual(['Context']); + }); + + it('for object', () => { + const controller = new MessageController(); + const obj = { one: 1, two: 2 }; + const message: Message = { + message: 'Message', + context: obj, + type: 'info', + }; + expect(controller.getContextStrings(message)).toEqual([yaml.dump(obj)]); + }); + + it('for array', () => { + const controller = new MessageController(); + const array = ['one', 'two']; + const message: Message = { + message: 'Message', + context: array, + type: 'info', + }; + expect(controller.getContextStrings(message)).toEqual( + array.map((item) => yaml.dump(item)), + ); + }); + }); +}); diff --git a/tests/components-lib/message/dispatch.test.ts b/tests/components-lib/message/dispatch.test.ts new file mode 100644 index 00000000..c81e4299 --- /dev/null +++ b/tests/components-lib/message/dispatch.test.ts @@ -0,0 +1,47 @@ +import { expect, it, vi } from 'vitest'; +import { dispatchFrigateCardErrorEvent } from '../../../src/components-lib/message/dispatch'; +import { FrigateCardError } from '../../../src/types'; + +// @vitest-environment jsdom +it('should ignore non-error', () => { + const element = document.createElement('div'); + const handler = vi.fn(); + element.addEventListener('frigate-card:message', handler); + + dispatchFrigateCardErrorEvent(element, 'NOT_FRIGATE_EVENT'); + + expect(handler).not.toBeCalled(); +}); + +it('should dispatch error', () => { + const element = document.createElement('div'); + const handler = vi.fn(); + element.addEventListener('frigate-card:message', handler); + + dispatchFrigateCardErrorEvent(element, new Error('ERROR')); + + expect(handler).toBeCalledWith( + expect.objectContaining({ + detail: expect.objectContaining({ + message: 'ERROR', + }), + }), + ); +}); + +it('should dispatch error with context', () => { + const element = document.createElement('div'); + const handler = vi.fn(); + element.addEventListener('frigate-card:message', handler); + + dispatchFrigateCardErrorEvent(element, new FrigateCardError('ERROR', 'CONTEXT')); + + expect(handler).toBeCalledWith( + expect.objectContaining({ + detail: expect.objectContaining({ + message: 'ERROR', + context: 'CONTEXT', + }), + }), + ); +});