refactor: Refactor internal error / info message components (#1842)

This commit is contained in:
Dermot Duffy
2025-01-20 15:26:53 -08:00
committed by GitHub
parent 99fe613811
commit faa5ec3eec
19 changed files with 421 additions and 252 deletions
+18 -9
View File
@@ -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;
+1 -1
View File
@@ -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
+43
View File
@@ -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 [];
}
}
+47
View File
@@ -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<Message>) => void,
options?: AddEventListenerOptions | boolean,
): void;
addEventListener(
type: string,
callback: EventListenerOrEventListenerObject,
options?: AddEventListenerOptions | boolean,
): void;
removeEventListener(
event: 'frigate-card:message',
listener: (this: FrigateCardMessageEventTarget, ev: CustomEvent<Message>) => 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<Message>(element, 'message', {
message: error.message,
type: 'error',
...(error instanceof FrigateCardError && { context: error.context }),
});
}
};
+1 -2
View File
@@ -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,
});
}
+1 -1
View File
@@ -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:
*
+4 -1
View File
@@ -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';
+1 -1
View File
@@ -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';
+4 -1
View File
@@ -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;
+4 -1
View File
@@ -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
+26 -216
View File
@@ -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` <frigate-card-message .message=${message}></frigate-card-message>`;
}
@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`<a href="${this._controller.getTroubleshootingURL(this.message)}"
>${localize('error.troubleshooting')}</a
>`
: ''}
`;
// 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`<pre>${yaml.dump(contextItem)}</pre>`;
dotdotdot: !!this.message?.dotdotdot,
};
return html` <div class="wrapper">
@@ -42,18 +42,10 @@ export class FrigateCardMessage extends LitElement {
<frigate-card-icon .icon="${{ icon: icon }}"></frigate-card-icon>
</div>
<div class="contents">
<span class="${classMap(classes)}">
${this.message
? html`${this.message}${this.context && typeof this.context === 'string'
? ': ' + this.context
: ''}`
: ''}
</span>
${this.context && Array.isArray(this.context)
? this.context.map((contextItem) => renderContext(contextItem))
: typeof this.context === 'object'
? renderContext(this.context)
: ''}
<span class="${classMap(classes)}">${messageTemplate}</span>
${this._controller
.getContextStrings(this.message)
.map((contextItem) => html`<pre>${contextItem}</pre>`)}
</div>
</div>
</div>`;
@@ -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` <frigate-card-message
.message=${html` ${this.message.message}.
<a href="${TROUBLESHOOTING_URL}"> ${localize('error.troubleshooting')}</a>.`}
.icon=${this.message.icon ?? 'mdi:alert-circle'}
.context=${this.message.context}
.dotdotdot=${this.message.dotdotdot}
>
</frigate-card-message>`;
}
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` <div class="message vertical">
${this.animated
? html`<ha-circular-progress indeterminate size="${this.size}">
</ha-circular-progress>`
: html`<frigate-card-icon
.icon=${{ icon: 'mdi:timer-sand' }}
></frigate-card-icon>`}
${this.message ? html`<span>${this.message}</span>` : html``}
</div>`;
}
static get styles(): CSSResultGroup {
return unsafeCSS(messageStyle);
}
}
export function renderMessage(message: Message | null): TemplateResult {
if (message?.type === 'error') {
return html` <frigate-card-error-message
.message=${message}
></frigate-card-error-message>`;
} else if (message) {
return html` <frigate-card-message
.message=${message.message}
.icon=${message.icon}
.context=${message.context}
.dotdotdot=${message.dotdotdot}
></frigate-card-message>`;
}
return html``;
}
export function renderProgressIndicator(options?: {
message?: string;
cardWideConfig?: CardWideConfig | null;
componentRef?: Ref<HTMLElement>;
classes?: ClassInfo;
size?: FrigateCardProgressIndicatorSize;
}): TemplateResult {
return html`
<frigate-card-progress-indicator
class="${classMap(options?.classes ?? {})}"
.size=${options?.size}
${options?.componentRef ? ref(options.componentRef) : ''}
.message=${options?.message || ''}
.animated=${options?.cardWideConfig?.performance?.features
.animated_progress_indicator ?? true}
>
</frigate-card-progress-indicator>
`;
}
/**
* 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<Message>(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<Message>) => void,
options?: AddEventListenerOptions | boolean,
): void;
addEventListener(
type: string,
callback: EventListenerOrEventListenerObject,
options?: AddEventListenerOptions | boolean,
): void;
removeEventListener(
event: 'frigate-card:message',
listener: (this: FrigateCardMessageEventTarget, ev: CustomEvent<Message>) => 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;
}
}
+63
View File
@@ -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<HTMLElement>;
classes?: ClassInfo;
size?: FrigateCardProgressIndicatorSize;
}): TemplateResult {
return html`
<frigate-card-progress-indicator
class="${classMap(options?.classes ?? {})}"
.size=${options?.size}
${options?.componentRef ? ref(options.componentRef) : ''}
.message=${options?.message || ''}
.animated=${options?.cardWideConfig?.performance?.features
.animated_progress_indicator ?? true}
>
</frigate-card-progress-indicator>
`;
}
@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` <div class="message vertical">
${this.animated
? html`<ha-circular-progress indeterminate size="${this.size}">
</ha-circular-progress>`
: html`<frigate-card-icon
.icon=${{ icon: 'mdi:timer-sand' }}
></frigate-card-icon>`}
${this.message ? html`<span>${this.message}</span>` : html``}
</div>`;
}
static get styles(): CSSResultGroup {
return unsafeCSS(messageStyle);
}
}
declare global {
interface HTMLElementTagNameMap {
'frigate-card-progress-indicator': FrigateCardProgressIndicator;
}
}
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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
+3 -13
View File
@@ -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 {
+1 -1
View File
@@ -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';
@@ -6,7 +6,7 @@ import { createCardAPI } from '../test-utils';
const createMessage = (options?: Partial<Message>): 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);
@@ -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)),
);
});
});
});
@@ -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',
}),
}),
);
});