feat: Allow user generated notifications (#2401)
This commit is contained in:
@@ -28,7 +28,7 @@ describe('InfoAction', () => {
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getOverlayMessageManager().setMessage).toBeCalled();
|
||||
expect(api.getNotificationManager().setNotification).toBeCalled();
|
||||
});
|
||||
|
||||
it('should not handle info action without media', async () => {
|
||||
@@ -45,6 +45,6 @@ describe('InfoAction', () => {
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getOverlayMessageManager().setMessage).not.toBeCalled();
|
||||
expect(api.getNotificationManager().setNotification).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { NotificationAction } from '../../../../src/card-controller/actions/actions/notification';
|
||||
import { createCardAPI } from '../../../test-utils';
|
||||
|
||||
describe('NotificationAction', () => {
|
||||
it('should set notification on manager', async () => {
|
||||
const api = createCardAPI();
|
||||
const notification = {
|
||||
heading: { text: 'Test Heading' },
|
||||
text: 'Test text',
|
||||
};
|
||||
|
||||
const action = new NotificationAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'notification',
|
||||
notification,
|
||||
},
|
||||
);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getNotificationManager().setNotification).toHaveBeenCalledWith(
|
||||
notification,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -22,9 +22,10 @@ import { MoreInfoAction } from '../../../src/card-controller/actions/actions/mor
|
||||
import { MuteAction } from '../../../src/card-controller/actions/actions/mute';
|
||||
import { NavigateAction } from '../../../src/card-controller/actions/actions/navigate';
|
||||
import { NoneAction } from '../../../src/card-controller/actions/actions/none';
|
||||
import { NotificationAction } from '../../../src/card-controller/actions/actions/notification';
|
||||
import { PauseAction } from '../../../src/card-controller/actions/actions/pause';
|
||||
import { PIPAction } from '../../../src/card-controller/actions/actions/pip';
|
||||
import { PerformActionAction } from '../../../src/card-controller/actions/actions/perform-action';
|
||||
import { PIPAction } from '../../../src/card-controller/actions/actions/pip';
|
||||
import { PlayAction } from '../../../src/card-controller/actions/actions/play';
|
||||
import { PTZAction } from '../../../src/card-controller/actions/actions/ptz';
|
||||
import { PTZControlsAction } from '../../../src/card-controller/actions/actions/ptz-controls';
|
||||
@@ -126,6 +127,13 @@ describe('ActionFactory', () => {
|
||||
},
|
||||
LogAction,
|
||||
],
|
||||
[
|
||||
{
|
||||
advanced_camera_card_action: 'notification' as const,
|
||||
notification: { text: 'test' },
|
||||
},
|
||||
NotificationAction,
|
||||
],
|
||||
[
|
||||
{
|
||||
advanced_camera_card_action: 'media_player' as const,
|
||||
|
||||
@@ -121,6 +121,27 @@ describe('ConfigManager', () => {
|
||||
expect(manager.getConfig()).toBeNull();
|
||||
expect(manager.getNonOverriddenConfig()).toBeNull();
|
||||
expect(manager.getRawConfig()).toBeNull();
|
||||
expect(manager.isUpgradeable()).toBe(false);
|
||||
});
|
||||
|
||||
describe('isUpgradeable', () => {
|
||||
it('should return true for upgradeable config', () => {
|
||||
const manager = new ConfigManager(createCardAPI());
|
||||
manager.setConfig({
|
||||
type: 'custom:frigate-card',
|
||||
cameras: [TEST_CAMERAS.OFFICE],
|
||||
});
|
||||
expect(manager.isUpgradeable()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-upgradeable config', () => {
|
||||
const manager = new ConfigManager(createCardAPI());
|
||||
manager.setConfig({
|
||||
type: 'custom:advanced-camera-card',
|
||||
cameras: [TEST_CAMERAS.OFFICE],
|
||||
});
|
||||
expect(manager.isUpgradeable()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should successfully parse basic config', () => {
|
||||
|
||||
@@ -21,7 +21,7 @@ import { MediaLoadedInfoManager } from '../../src/card-controller/media-info-man
|
||||
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 { OverlayMessageManager } from '../../src/card-controller/overlay-message-manager';
|
||||
import { NotificationManager } from '../../src/card-controller/notification-manager';
|
||||
import { PIPManager } from '../../src/card-controller/pip-manager';
|
||||
import { QueryStringManager } from '../../src/card-controller/query-string-manager';
|
||||
import { StatusBarItemManager } from '../../src/card-controller/status-bar-item-manager';
|
||||
@@ -54,7 +54,7 @@ vi.mock('../../src/card-controller/media-info-manager');
|
||||
vi.mock('../../src/card-controller/media-player-manager');
|
||||
vi.mock('../../src/card-controller/message-manager');
|
||||
vi.mock('../../src/card-controller/microphone-manager');
|
||||
vi.mock('../../src/card-controller/overlay-message-manager');
|
||||
vi.mock('../../src/card-controller/notification-manager');
|
||||
vi.mock('../../src/card-controller/pip-manager');
|
||||
vi.mock('../../src/card-controller/query-string-manager');
|
||||
vi.mock('../../src/card-controller/status-bar-item-manager');
|
||||
@@ -226,9 +226,9 @@ describe('CardController', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('getOverlayMessageManager', () => {
|
||||
expect(createController().getOverlayMessageManager()).toBe(
|
||||
vi.mocked(OverlayMessageManager).mock.instances[0],
|
||||
it('getNotificationManager', () => {
|
||||
expect(createController().getNotificationManager()).toBe(
|
||||
vi.mocked(NotificationManager).mock.instances[0],
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { CardElementManager } from '../../src/card-controller/card-element-manager';
|
||||
import { NotificationManager } from '../../src/card-controller/notification-manager';
|
||||
import { CardNotificationAPI } from '../../src/card-controller/types';
|
||||
|
||||
describe('NotificationManager', () => {
|
||||
const cardElementManager = mock<CardElementManager>();
|
||||
const api = mock<CardNotificationAPI>();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
api.getCardElementManager.mockReturnValue(cardElementManager);
|
||||
});
|
||||
|
||||
it('should be constructed', () => {
|
||||
const manager = new NotificationManager(api);
|
||||
expect(manager).toBeDefined();
|
||||
expect(manager.getNotification()).toBeNull();
|
||||
expect(manager.hasNotification()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should set and get notification', () => {
|
||||
const manager = new NotificationManager(api);
|
||||
const notification = { text: 'foo' };
|
||||
manager.setNotification(notification);
|
||||
|
||||
expect(manager.getNotification()).toBe(notification);
|
||||
expect(manager.hasNotification()).toBeTruthy();
|
||||
expect(cardElementManager.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reset notification', () => {
|
||||
const manager = new NotificationManager(api);
|
||||
manager.setNotification({ text: 'foo' });
|
||||
vi.clearAllMocks();
|
||||
|
||||
manager.reset();
|
||||
|
||||
expect(manager.getNotification()).toBeNull();
|
||||
expect(manager.hasNotification()).toBeFalsy();
|
||||
expect(cardElementManager.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not update if reset is called with no notification', () => {
|
||||
const manager = new NotificationManager(api);
|
||||
manager.reset();
|
||||
|
||||
expect(cardElementManager.update).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { CardElementManager } from '../../src/card-controller/card-element-manager';
|
||||
import { OverlayMessageManager } from '../../src/card-controller/overlay-message-manager';
|
||||
import { CardOverlayMessageAPI } from '../../src/card-controller/types';
|
||||
|
||||
describe('OverlayMessageManager', () => {
|
||||
const cardElementManager = mock<CardElementManager>();
|
||||
const api = mock<CardOverlayMessageAPI>();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
api.getCardElementManager.mockReturnValue(cardElementManager);
|
||||
});
|
||||
|
||||
it('should be constructed', () => {
|
||||
const manager = new OverlayMessageManager(api);
|
||||
expect(manager).toBeDefined();
|
||||
expect(manager.getMessage()).toBeNull();
|
||||
expect(manager.hasMessage()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should set and get message', () => {
|
||||
const manager = new OverlayMessageManager(api);
|
||||
const message = { text: 'foo' };
|
||||
manager.setMessage(message);
|
||||
|
||||
expect(manager.getMessage()).toBe(message);
|
||||
expect(manager.hasMessage()).toBeTruthy();
|
||||
expect(cardElementManager.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reset message', () => {
|
||||
const manager = new OverlayMessageManager(api);
|
||||
manager.setMessage({ text: 'foo' });
|
||||
vi.clearAllMocks();
|
||||
|
||||
manager.reset();
|
||||
|
||||
expect(manager.getMessage()).toBeNull();
|
||||
expect(manager.hasMessage()).toBeFalsy();
|
||||
expect(cardElementManager.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not update if reset is called with no message', () => {
|
||||
const manager = new OverlayMessageManager(api);
|
||||
manager.reset();
|
||||
|
||||
expect(cardElementManager.update).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -241,6 +241,56 @@ describe('StatusBarItemManager', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('upgrade', () => {
|
||||
it('should show upgrade item when upgradeable', () => {
|
||||
const manager = new StatusBarItemManager(createCardAPI());
|
||||
|
||||
const items = manager.calculateItems({
|
||||
isUpgradeable: true,
|
||||
});
|
||||
|
||||
expect(items).toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: 'custom:advanced-camera-card-status-bar-icon' as const,
|
||||
icon: 'mdi:update',
|
||||
severity: 'medium',
|
||||
actions: expect.objectContaining({
|
||||
tap_action: expect.objectContaining({
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'notification',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not show upgrade item when not upgradeable', () => {
|
||||
const manager = new StatusBarItemManager(createCardAPI());
|
||||
|
||||
const items = manager.calculateItems({
|
||||
isUpgradeable: false,
|
||||
});
|
||||
|
||||
expect(items).not.toContainEqual(
|
||||
expect.objectContaining({
|
||||
icon: 'mdi:update',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not show upgrade item by default', () => {
|
||||
const manager = new StatusBarItemManager(createCardAPI());
|
||||
|
||||
const items = manager.calculateItems();
|
||||
|
||||
expect(items).not.toContainEqual(
|
||||
expect.objectContaining({
|
||||
icon: 'mdi:update',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('severity', () => {
|
||||
it('should have severity in a viewer view', () => {
|
||||
const manager = new StatusBarItemManager(createCardAPI());
|
||||
|
||||
@@ -1,18 +1,39 @@
|
||||
import { format } from 'date-fns';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { afterEach, assert, describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { CameraManager } from '../../../src/camera-manager/manager';
|
||||
import { ActionFactory } from '../../../src/card-controller/actions/factory';
|
||||
import { CardController } from '../../../src/card-controller/controller';
|
||||
import { ViewItemManager } from '../../../src/card-controller/view/item-manager';
|
||||
import { ViewManagerEpoch } from '../../../src/card-controller/view/types';
|
||||
import {
|
||||
MediaDetailsController,
|
||||
OverlayControlsContext,
|
||||
NotificationControlsContext,
|
||||
} from '../../../src/components-lib/media/details-controller';
|
||||
import { OverlayMessageControl } from '../../../src/types';
|
||||
import { NotificationControl } from '../../../src/config/schema/actions/types';
|
||||
import { formatDateAndTime } from '../../../src/utils/basic';
|
||||
import { downloadMedia, navigateToTimeline } from '../../../src/utils/media-actions';
|
||||
import { ViewFolder, ViewMediaType } from '../../../src/view/item';
|
||||
import { createCardAPI, createFolder, TestViewMedia } from '../../test-utils';
|
||||
|
||||
vi.mock('../../../src/utils/media-actions', async (importOriginal) => ({
|
||||
...((await importOriginal()) as object),
|
||||
downloadMedia: vi.fn(),
|
||||
navigateToTimeline: vi.fn(),
|
||||
}));
|
||||
|
||||
async function executeControlAction(
|
||||
control: NotificationControl,
|
||||
api: CardController,
|
||||
): Promise<void> {
|
||||
const tapAction = control.actions?.tap_action;
|
||||
|
||||
assert(tapAction && !Array.isArray(tapAction));
|
||||
const action = new ActionFactory().createAction({}, tapAction);
|
||||
|
||||
await action?.execute(api);
|
||||
}
|
||||
|
||||
describe('MediaDetailsController', () => {
|
||||
describe('should set heading', () => {
|
||||
it('should set heading on event with what, tags and score', () => {
|
||||
@@ -24,7 +45,7 @@ describe('MediaDetailsController', () => {
|
||||
|
||||
const controller = new MediaDetailsController();
|
||||
controller.calculate(null, item);
|
||||
expect(controller.getHeading()?.title).toBe('Person, Car: Tag1, Tag2 50.00%');
|
||||
expect(controller.getHeading()?.text).toBe('Person, Car: Tag1, Tag2 50.00%');
|
||||
});
|
||||
|
||||
it('should set heading on event with tags', () => {
|
||||
@@ -34,7 +55,7 @@ describe('MediaDetailsController', () => {
|
||||
|
||||
const controller = new MediaDetailsController();
|
||||
controller.calculate(null, item);
|
||||
expect(controller.getHeading()?.title).toBe('Tag1, Tag2');
|
||||
expect(controller.getHeading()?.text).toBe('Tag1, Tag2');
|
||||
});
|
||||
|
||||
it('should set heading on event with what', () => {
|
||||
@@ -44,7 +65,7 @@ describe('MediaDetailsController', () => {
|
||||
|
||||
const controller = new MediaDetailsController();
|
||||
controller.calculate(null, item);
|
||||
expect(controller.getHeading()?.title).toBe('Person, Car');
|
||||
expect(controller.getHeading()?.text).toBe('Person, Car');
|
||||
});
|
||||
|
||||
it('should set null heading on event with no other information', () => {
|
||||
@@ -73,7 +94,7 @@ describe('MediaDetailsController', () => {
|
||||
|
||||
const controller = new MediaDetailsController();
|
||||
controller.calculate(cameraManager, item);
|
||||
expect(controller.getHeading()?.title).toBe('Camera Title');
|
||||
expect(controller.getHeading()?.text).toBe('Camera Title');
|
||||
});
|
||||
|
||||
it('should set heading on recording without camera metadata', () => {
|
||||
@@ -106,9 +127,9 @@ describe('MediaDetailsController', () => {
|
||||
const controller = new MediaDetailsController();
|
||||
controller.calculate(null, item);
|
||||
expect(controller.getDetails()).toContainEqual({
|
||||
title: 'Test Event',
|
||||
icon: { icon: 'mdi:rename' },
|
||||
hint: 'Title',
|
||||
text: 'Test Event',
|
||||
icon: 'mdi:rename',
|
||||
tooltip: 'Title',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -121,7 +142,7 @@ describe('MediaDetailsController', () => {
|
||||
controller.calculate(null, item);
|
||||
expect(controller.getDetails()).toEqual([
|
||||
{
|
||||
title: 'Test Event',
|
||||
text: 'Test Event',
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -136,7 +157,7 @@ describe('MediaDetailsController', () => {
|
||||
controller.calculate(null, item);
|
||||
expect(controller.getDetails()).not.toContainEqual(
|
||||
expect.objectContaining({
|
||||
title: 'Test Event',
|
||||
text: 'Test Event',
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -153,9 +174,9 @@ describe('MediaDetailsController', () => {
|
||||
|
||||
// Use formatDateAndTime to generate expected value (formats in local time with seconds)
|
||||
expect(controller.getDetails()).toContainEqual({
|
||||
title: formatDateAndTime(startTime, true),
|
||||
hint: 'Start',
|
||||
icon: { icon: 'mdi:calendar-clock-outline' },
|
||||
text: formatDateAndTime(startTime, true),
|
||||
tooltip: 'Start',
|
||||
icon: 'mdi:calendar-clock-outline',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -169,9 +190,9 @@ describe('MediaDetailsController', () => {
|
||||
const controller = new MediaDetailsController();
|
||||
controller.calculate(null, item);
|
||||
expect(controller.getDetails()).toContainEqual({
|
||||
title: '1m 0s',
|
||||
hint: 'Duration',
|
||||
icon: { icon: 'mdi:clock-outline' },
|
||||
text: '1m 0s',
|
||||
tooltip: 'Duration',
|
||||
icon: 'mdi:clock-outline',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -185,9 +206,9 @@ describe('MediaDetailsController', () => {
|
||||
const controller = new MediaDetailsController();
|
||||
controller.calculate(null, item);
|
||||
expect(controller.getDetails()).toContainEqual({
|
||||
title: 'In Progress',
|
||||
hint: 'Duration',
|
||||
icon: { icon: 'mdi:clock-outline' },
|
||||
text: 'In Progress',
|
||||
tooltip: 'Duration',
|
||||
icon: 'mdi:clock-outline',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -201,9 +222,9 @@ describe('MediaDetailsController', () => {
|
||||
const controller = new MediaDetailsController();
|
||||
controller.calculate(null, item);
|
||||
expect(controller.getDetails()).toContainEqual({
|
||||
title: '1m 0s In Progress',
|
||||
hint: 'Duration',
|
||||
icon: { icon: 'mdi:clock-outline' },
|
||||
text: '1m 0s In Progress',
|
||||
tooltip: 'Duration',
|
||||
icon: 'mdi:clock-outline',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -222,9 +243,9 @@ describe('MediaDetailsController', () => {
|
||||
const controller = new MediaDetailsController();
|
||||
controller.calculate(cameraManager, item);
|
||||
expect(controller.getDetails()).toContainEqual({
|
||||
title: 'Camera Title',
|
||||
hint: 'Camera',
|
||||
icon: { icon: 'mdi:cctv' },
|
||||
text: 'Camera Title',
|
||||
tooltip: 'Camera',
|
||||
icon: 'mdi:cctv',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -237,9 +258,9 @@ describe('MediaDetailsController', () => {
|
||||
const controller = new MediaDetailsController();
|
||||
controller.calculate(null, item);
|
||||
expect(controller.getDetails()).toContainEqual({
|
||||
title: 'Where1, Where2',
|
||||
hint: 'Where',
|
||||
icon: { icon: 'mdi:map-marker-outline' },
|
||||
text: 'Where1, Where2',
|
||||
tooltip: 'Where',
|
||||
icon: 'mdi:map-marker-outline',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -252,9 +273,9 @@ describe('MediaDetailsController', () => {
|
||||
const controller = new MediaDetailsController();
|
||||
controller.calculate(null, item);
|
||||
expect(controller.getDetails()).toContainEqual({
|
||||
title: 'Tag1, Tag2',
|
||||
hint: 'Tag',
|
||||
icon: { icon: 'mdi:tag' },
|
||||
text: 'Tag1, Tag2',
|
||||
tooltip: 'Tag',
|
||||
icon: 'mdi:tag',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -267,9 +288,9 @@ describe('MediaDetailsController', () => {
|
||||
|
||||
// Use format() to generate expected value (formats in local time)
|
||||
expect(controller.getDetails()).toContainEqual({
|
||||
title: format(seekTime, 'HH:mm:ss'),
|
||||
hint: 'Seek',
|
||||
icon: { icon: 'mdi:clock-fast' },
|
||||
text: format(seekTime, 'HH:mm:ss'),
|
||||
tooltip: 'Seek',
|
||||
icon: 'mdi:clock-fast',
|
||||
});
|
||||
});
|
||||
it('should set heading on review', () => {
|
||||
@@ -282,10 +303,10 @@ describe('MediaDetailsController', () => {
|
||||
const controller = new MediaDetailsController();
|
||||
controller.calculate(null, item);
|
||||
const heading = controller.getHeading();
|
||||
expect(heading?.title).toBe('Review Title');
|
||||
expect(heading?.emphasis).toBe('high');
|
||||
expect(heading?.icon).toEqual({ icon: 'mdi:circle-medium' });
|
||||
expect(heading?.hint).toBe('Severity: High');
|
||||
expect(heading?.text).toBe('Review Title');
|
||||
expect(heading?.severity).toBe('high');
|
||||
expect(heading?.icon).toBe('mdi:circle-medium');
|
||||
expect(heading?.tooltip).toBe('Severity: High');
|
||||
});
|
||||
|
||||
it('should set heading on review without severity', () => {
|
||||
@@ -298,8 +319,8 @@ describe('MediaDetailsController', () => {
|
||||
const controller = new MediaDetailsController();
|
||||
controller.calculate(null, item);
|
||||
const heading = controller.getHeading();
|
||||
expect(heading?.title).toBe('Review Title');
|
||||
expect(heading?.emphasis).toBeUndefined();
|
||||
expect(heading?.text).toBe('Review Title');
|
||||
expect(heading?.severity).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should set null heading on review with no title', () => {
|
||||
@@ -321,12 +342,12 @@ describe('MediaDetailsController', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('should get message', () => {
|
||||
describe('should get notification', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should get message', () => {
|
||||
it('should get notification', () => {
|
||||
const item = new TestViewMedia({
|
||||
title: 'Test Title',
|
||||
what: ['person'],
|
||||
@@ -336,25 +357,25 @@ describe('MediaDetailsController', () => {
|
||||
const controller = new MediaDetailsController();
|
||||
controller.calculate(null, item);
|
||||
|
||||
const message = controller.getMessage();
|
||||
expect(message.heading?.title).toBe('Person');
|
||||
expect(message.details).toContainEqual({
|
||||
title: 'Test Title',
|
||||
const notification = controller.getNotification();
|
||||
expect(notification.heading?.text).toBe('Person');
|
||||
expect(notification.details).toContainEqual({
|
||||
text: 'Test Title',
|
||||
});
|
||||
expect(message.text).toBe('Test Description');
|
||||
expect(notification.text).toBe('Test Description');
|
||||
});
|
||||
|
||||
it('should get message without media', () => {
|
||||
it('should get notification without media', () => {
|
||||
const item = new ViewFolder(createFolder(), []);
|
||||
|
||||
const controller = new MediaDetailsController();
|
||||
controller.calculate(null, item);
|
||||
|
||||
const message = controller.getMessage();
|
||||
expect(message.text).toBeUndefined();
|
||||
const notification = controller.getNotification();
|
||||
expect(notification.text).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should get message with null description', () => {
|
||||
it('should get notification with null description', () => {
|
||||
const item = new TestViewMedia({
|
||||
description: null,
|
||||
});
|
||||
@@ -362,11 +383,11 @@ describe('MediaDetailsController', () => {
|
||||
const controller = new MediaDetailsController();
|
||||
controller.calculate(null, item);
|
||||
|
||||
const message = controller.getMessage();
|
||||
expect(message.text).toBeUndefined();
|
||||
const notification = controller.getNotification();
|
||||
expect(notification.text).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should get message with controls', async () => {
|
||||
it('should get notification with controls', async () => {
|
||||
const item = new TestViewMedia({
|
||||
title: 'Test Title',
|
||||
mediaType: ViewMediaType.Review,
|
||||
@@ -390,45 +411,52 @@ describe('MediaDetailsController', () => {
|
||||
const controller = new MediaDetailsController();
|
||||
controller.calculate(null, item);
|
||||
|
||||
const message = controller.getMessage(context);
|
||||
const controls = message.controls;
|
||||
const notification = controller.getNotification(context);
|
||||
const controls = notification.controls;
|
||||
assert(controls);
|
||||
expect(controls).toHaveLength(4);
|
||||
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
// 1. Review control
|
||||
expect(controls?.[0].title).toBe('Mark as reviewed');
|
||||
const reviewResult = await controls?.[0].callback?.();
|
||||
expect(reviewResult).not.toBeNull();
|
||||
expect(controls?.[0].tooltip).toBe('Mark as reviewed');
|
||||
expect(controls?.[0].dismiss).toBe(false);
|
||||
await executeControlAction(controls[0], cardAPI);
|
||||
expect(cardAPI.getNotificationManager().setNotification).toHaveBeenCalled();
|
||||
|
||||
// 1b. Review control (failure)
|
||||
vi.mocked(cardAPI.getNotificationManager().setNotification).mockClear();
|
||||
viewItemManager.reviewMedia.mockRejectedValue(new Error('fail'));
|
||||
|
||||
const reviewFailureResult = await controls?.[0].callback?.();
|
||||
expect(reviewFailureResult).toBeNull();
|
||||
await executeControlAction(controls[0], cardAPI);
|
||||
expect(cardAPI.getNotificationManager().setNotification).not.toHaveBeenCalled();
|
||||
|
||||
// 2. Favorite control
|
||||
expect(controls?.[1].title).toBe('Media will be indefinitely retained');
|
||||
const favoriteResult = await controls?.[1].callback?.();
|
||||
expect(favoriteResult).not.toBeNull();
|
||||
expect(controls?.[1].tooltip).toBe('Media will be indefinitely retained');
|
||||
expect(controls?.[1].dismiss).toBe(false);
|
||||
await executeControlAction(controls[1], cardAPI);
|
||||
expect(cardAPI.getNotificationManager().setNotification).toHaveBeenCalled();
|
||||
|
||||
// 2b. Favorite control (failure)
|
||||
vi.mocked(cardAPI.getNotificationManager().setNotification).mockClear();
|
||||
viewItemManager.favorite.mockRejectedValue(new Error('fail'));
|
||||
const favoriteFailureResult = await controls?.[1].callback?.();
|
||||
expect(favoriteFailureResult).toBeNull();
|
||||
await executeControlAction(controls[1], cardAPI);
|
||||
expect(cardAPI.getNotificationManager().setNotification).not.toHaveBeenCalled();
|
||||
|
||||
// 3. Download control
|
||||
expect(controls?.[2].title).toBe('Download media');
|
||||
const downloadResult = await controls?.[2].callback?.();
|
||||
expect(downloadResult).toBeNull();
|
||||
expect(controls?.[2].tooltip).toBe('Download media');
|
||||
expect(controls?.[2].dismiss).toBe(true);
|
||||
vi.mocked(downloadMedia).mockResolvedValue(true);
|
||||
await executeControlAction(controls[2], cardAPI);
|
||||
expect(downloadMedia).toHaveBeenCalledWith(item, viewItemManager);
|
||||
|
||||
// 4. Timeline control
|
||||
expect(controls?.[3].title).toBe('See media in timeline');
|
||||
const timelineResult = await controls?.[3].callback?.();
|
||||
expect(timelineResult).toBeNull();
|
||||
expect(controls?.[3].tooltip).toBe('See media in timeline');
|
||||
expect(controls?.[3].dismiss).toBe(true);
|
||||
await executeControlAction(controls[3], cardAPI);
|
||||
expect(navigateToTimeline).toHaveBeenCalledWith(item, viewManagerEpoch);
|
||||
});
|
||||
|
||||
it('should get message with controls for already reviewed/favorited items', () => {
|
||||
it('should get notification with controls for already reviewed/favorited items', () => {
|
||||
const item = new TestViewMedia({
|
||||
mediaType: ViewMediaType.Review,
|
||||
reviewed: true,
|
||||
@@ -444,18 +472,18 @@ describe('MediaDetailsController', () => {
|
||||
const controller = new MediaDetailsController();
|
||||
controller.calculate(null, item);
|
||||
|
||||
const message = controller.getMessage(context);
|
||||
const controls = message.controls;
|
||||
const notification = controller.getNotification(context);
|
||||
const controls = notification.controls;
|
||||
expect(controls).toHaveLength(2);
|
||||
|
||||
expect(controls?.[0].title).toBe('Mark as unreviewed');
|
||||
expect(controls?.[0].icon).toEqual({ icon: 'mdi:check-circle' });
|
||||
expect(controls?.[0].tooltip).toBe('Mark as unreviewed');
|
||||
expect(controls?.[0].icon).toBe('mdi:check-circle');
|
||||
|
||||
expect(controls?.[1].emphasis).toBe('medium');
|
||||
expect(controls?.[1].icon).toEqual({ icon: 'mdi:star' });
|
||||
expect(controls?.[1].severity).toBe('medium');
|
||||
expect(controls?.[1].icon).toBe('mdi:star');
|
||||
});
|
||||
|
||||
it('should get message with controls when item has no ID', () => {
|
||||
it('should get notification with controls when item has no ID', () => {
|
||||
const item = new TestViewMedia({
|
||||
id: null,
|
||||
});
|
||||
@@ -469,11 +497,11 @@ describe('MediaDetailsController', () => {
|
||||
const controller = new MediaDetailsController();
|
||||
controller.calculate(null, item);
|
||||
|
||||
const message = controller.getMessage(context);
|
||||
expect(message.controls).toHaveLength(0);
|
||||
const notification = controller.getNotification(context);
|
||||
expect(notification.controls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should get message with controls when context has no capabilities', () => {
|
||||
it('should get notification with controls when context has no capabilities', () => {
|
||||
const item = new TestViewMedia({
|
||||
id: 'id',
|
||||
});
|
||||
@@ -482,8 +510,8 @@ describe('MediaDetailsController', () => {
|
||||
const controller = new MediaDetailsController();
|
||||
controller.calculate(null, item);
|
||||
|
||||
const message = controller.getMessage(context);
|
||||
expect(message.controls).toHaveLength(0);
|
||||
const notification = controller.getNotification(context);
|
||||
expect(notification.controls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should get empty controls when item is null', () => {
|
||||
@@ -492,7 +520,7 @@ describe('MediaDetailsController', () => {
|
||||
// Use cast to unknown first to avoid any-related lint errors.
|
||||
const controls = (
|
||||
controller as unknown as {
|
||||
_getControls: (context: OverlayControlsContext) => OverlayMessageControl[];
|
||||
_getControls: (context: NotificationControlsContext) => NotificationControl[];
|
||||
}
|
||||
)._getControls({});
|
||||
expect(controls).toEqual([]);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { iconSchema } from '../../../../src/config/schema/common/icon';
|
||||
|
||||
describe('iconSchema', () => {
|
||||
it('should parse icon with all fields', () => {
|
||||
expect(
|
||||
iconSchema.parse({
|
||||
icon: 'mdi:star',
|
||||
entity: 'light.office',
|
||||
stateColor: true,
|
||||
}),
|
||||
).toEqual({
|
||||
icon: 'mdi:star',
|
||||
entity: 'light.office',
|
||||
stateColor: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse empty object', () => {
|
||||
expect(iconSchema.parse({})).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -441,6 +441,10 @@ describe('config defaults', () => {
|
||||
enabled: true,
|
||||
priority: 50,
|
||||
},
|
||||
upgrade: {
|
||||
enabled: true,
|
||||
priority: 50,
|
||||
},
|
||||
},
|
||||
popup_seconds: 3,
|
||||
position: 'bottom',
|
||||
@@ -1066,6 +1070,30 @@ describe('config defaults', () => {
|
||||
action: 'custom:advanced-camera-card-action',
|
||||
advanced_camera_card_action: 'mute',
|
||||
},
|
||||
{
|
||||
action: 'custom:advanced-camera-card-action',
|
||||
advanced_camera_card_action: 'notification',
|
||||
notification: {
|
||||
heading: {
|
||||
text: 'Attention',
|
||||
icon: 'mdi:alert',
|
||||
severity: 'high',
|
||||
},
|
||||
text: 'Something happened.',
|
||||
details: [{ text: 'Detail 1', icon: 'mdi:info' }],
|
||||
controls: [
|
||||
{
|
||||
icon: 'mdi:check',
|
||||
tooltip: 'Acknowledge',
|
||||
dismiss: true,
|
||||
},
|
||||
{
|
||||
icon: 'mdi:eye',
|
||||
dismiss: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
action: 'custom:advanced-camera-card-action',
|
||||
advanced_camera_card_action: 'pause',
|
||||
|
||||
+2
-2
@@ -45,7 +45,7 @@ import { MediaLoadedInfoManager } from '../src/card-controller/media-info-manage
|
||||
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 { OverlayMessageManager } from '../src/card-controller/overlay-message-manager';
|
||||
import { NotificationManager } from '../src/card-controller/notification-manager';
|
||||
import { PIPManager } from '../src/card-controller/pip-manager';
|
||||
import { QueryStringManager } from '../src/card-controller/query-string-manager';
|
||||
import { StatusBarItemManager } from '../src/card-controller/status-bar-item-manager';
|
||||
@@ -678,7 +678,7 @@ export const createCardAPI = (): CardController => {
|
||||
api.getMediaPlayerManager.mockReturnValue(mock<MediaPlayerManager>());
|
||||
api.getMessageManager.mockReturnValue(mock<MessageManager>());
|
||||
api.getMicrophoneManager.mockReturnValue(mock<MicrophoneManager>());
|
||||
api.getOverlayMessageManager.mockReturnValue(mock<OverlayMessageManager>());
|
||||
api.getNotificationManager.mockReturnValue(mock<NotificationManager>());
|
||||
api.getPIPManager.mockReturnValue(mock<PIPManager>());
|
||||
api.getQueryStringManager.mockReturnValue(mock<QueryStringManager>());
|
||||
api.getStatusBarItemManager.mockReturnValue(mock<StatusBarItemManager>());
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
createInternalCallbackAction,
|
||||
createLogAction,
|
||||
createMediaPlayerAction,
|
||||
createNotificationAction,
|
||||
createPerformAction,
|
||||
createPTZAction,
|
||||
createPTZControlsAction,
|
||||
@@ -357,6 +358,27 @@ describe('createSetReviewAction', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('createNotificationAction', () => {
|
||||
it('should create notification action', () => {
|
||||
const notification = { text: 'test' };
|
||||
expect(createNotificationAction(notification)).toEqual({
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'notification',
|
||||
notification,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create notification action with cardID', () => {
|
||||
const notification = { text: 'test' };
|
||||
expect(createNotificationAction(notification, { cardID: 'card_id' })).toEqual({
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'notification',
|
||||
notification,
|
||||
card_id: 'card_id',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getActionConfigGivenAction', () => {
|
||||
const action = createViewAction('clips');
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { dispatchDismissNotificationEvent } from '../../src/utils/notification';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('notification utils', () => {
|
||||
it('should dispatch dismiss notification event', () => {
|
||||
const element = document.createElement('div');
|
||||
const handler = vi.fn();
|
||||
element.addEventListener('advanced-camera-card:notification:dismiss', handler);
|
||||
|
||||
dispatchDismissNotificationEvent(element);
|
||||
|
||||
expect(handler).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
dispatchDismissOverlayMessageEvent,
|
||||
dispatchShowOverlayMessageEvent,
|
||||
} from '../../src/utils/overlay-message';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('overlay-message utils', () => {
|
||||
it('should dispatch show overlay message event', () => {
|
||||
const element = document.createElement('div');
|
||||
const message = { text: 'test' };
|
||||
const handler = vi.fn();
|
||||
element.addEventListener('advanced-camera-card:overlay-message:show', handler);
|
||||
|
||||
dispatchShowOverlayMessageEvent(element, message);
|
||||
|
||||
expect(handler).toHaveBeenCalled();
|
||||
const event = handler.mock.calls[0][0];
|
||||
expect(event.detail).toBe(message);
|
||||
});
|
||||
|
||||
it('should dispatch dismiss overlay message event', () => {
|
||||
const element = document.createElement('div');
|
||||
const handler = vi.fn();
|
||||
element.addEventListener('advanced-camera-card:overlay-message:dismiss', handler);
|
||||
|
||||
dispatchDismissOverlayMessageEvent(element);
|
||||
|
||||
expect(handler).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user