Draws significant inspiration (and direct styling) from https://github.com/dermotduffy/advanced-camera-card/pull/2447 . Thank you @Maudfer ! BREAKING CHANGE: The microphone condition previously bundled two unrelated signals — whether a two-way-audio session was connected and whether the microphone was muted. Connection state is now its own dedicated call condition, and microphone is reserved purely for mute state. Configs are upgraded automatically (the card rewrites affected conditions under overrides, elements, and automations). If you maintain config by hand, convert as follows: If you only used connected: # Before ```yaml condition: microphone connected: true ``` # After ```yaml condition: call call: true ``` If you used both connected and muted — they must be split into two conditions, since they no longer live together: # Before ```yaml condition: microphone connected: true muted: false ``` # After ```yaml condition: and conditions: - condition: call call: true - condition: microphone muted: false ```
451 lines
14 KiB
TypeScript
451 lines
14 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import { StatusBarItemManager } from '../../src/card-controller/status-bar-item-manager';
|
|
import { StatusBarString } from '../../src/config/schema/actions/types';
|
|
import { QueryResults } from '../../src/view/query-results';
|
|
import {
|
|
TestViewMedia,
|
|
createCameraManager,
|
|
createCardAPI,
|
|
createStore,
|
|
createView,
|
|
} from '../test-utils';
|
|
|
|
describe('StatusBarItemManager', () => {
|
|
const testItem: StatusBarString = {
|
|
type: 'custom:advanced-camera-card-status-bar-string' as const,
|
|
string: 'test',
|
|
};
|
|
|
|
it('should add', () => {
|
|
const manager = new StatusBarItemManager(createCardAPI());
|
|
manager.addDynamicStatusBarItem(testItem);
|
|
manager.addDynamicStatusBarItem(testItem);
|
|
|
|
expect(manager.calculateItems()).toEqual([testItem]);
|
|
});
|
|
|
|
it('should remove', () => {
|
|
const manager = new StatusBarItemManager(createCardAPI());
|
|
manager.addDynamicStatusBarItem(testItem);
|
|
|
|
manager.removeDynamicStatusBarItem({ ...testItem });
|
|
expect(manager.calculateItems()).not.toContain(testItem);
|
|
|
|
manager.removeDynamicStatusBarItem({ ...testItem, string: 'not-present' });
|
|
expect(manager.calculateItems()).not.toContain({
|
|
...testItem,
|
|
string: 'not-present',
|
|
});
|
|
});
|
|
|
|
it('should remove all', () => {
|
|
const manager = new StatusBarItemManager(createCardAPI());
|
|
manager.addDynamicStatusBarItem(testItem);
|
|
manager.removeAllDynamicStatusBarItems();
|
|
expect(manager.calculateItems()).not.toContain(testItem);
|
|
});
|
|
|
|
describe('should have standard status bar items', () => {
|
|
describe('should have title', () => {
|
|
describe('live', () => {
|
|
it('should show with metadata', () => {
|
|
const manager = new StatusBarItemManager(createCardAPI());
|
|
const store = createStore([
|
|
{
|
|
cameraID: 'camera-1',
|
|
},
|
|
]);
|
|
const cameraManager = createCameraManager(store);
|
|
vi.mocked(cameraManager.getCameraMetadata).mockReturnValue({
|
|
title: 'Camera Title',
|
|
icon: { icon: 'mdi:camera' },
|
|
});
|
|
|
|
expect(
|
|
manager.calculateItems({
|
|
cameraManager: cameraManager,
|
|
view: createView({ view: 'live', camera: 'camera-1' }),
|
|
}),
|
|
).toContainEqual({
|
|
type: 'custom:advanced-camera-card-status-bar-string' as const,
|
|
string: 'Camera Title',
|
|
expand: true,
|
|
sufficient: true,
|
|
});
|
|
});
|
|
|
|
it('should handle without metadata', () => {
|
|
const manager = new StatusBarItemManager(createCardAPI());
|
|
const cameraManager = createCameraManager();
|
|
expect(
|
|
manager.calculateItems({
|
|
cameraManager: cameraManager,
|
|
view: createView({ view: 'live', camera: 'MISSING-CAMERA' }),
|
|
}),
|
|
).not.toContainEqual(expect.objectContaining({ sufficient: true }));
|
|
});
|
|
});
|
|
|
|
describe('media', () => {
|
|
it('should show with a title', () => {
|
|
const manager = new StatusBarItemManager(createCardAPI());
|
|
const cameraManager = createCameraManager();
|
|
|
|
const media = [new TestViewMedia({ title: 'Media Title' })];
|
|
const queryResults = new QueryResults({
|
|
results: media,
|
|
});
|
|
|
|
const view = createView({
|
|
view: 'media',
|
|
queryResults: queryResults,
|
|
});
|
|
|
|
expect(
|
|
manager.calculateItems({
|
|
cameraManager: cameraManager,
|
|
view: view,
|
|
}),
|
|
).toContainEqual({
|
|
type: 'custom:advanced-camera-card-status-bar-string' as const,
|
|
string: 'Media Title',
|
|
expand: true,
|
|
sufficient: true,
|
|
});
|
|
});
|
|
|
|
it('should handle without a title', () => {
|
|
const manager = new StatusBarItemManager(createCardAPI());
|
|
const cameraManager = createCameraManager();
|
|
|
|
const media = [new TestViewMedia()];
|
|
const queryResults = new QueryResults({
|
|
results: media,
|
|
});
|
|
|
|
const view = createView({
|
|
view: 'media',
|
|
queryResults: queryResults,
|
|
});
|
|
|
|
expect(
|
|
manager.calculateItems({
|
|
cameraManager: cameraManager,
|
|
view: view,
|
|
}),
|
|
).not.toContainEqual(expect.objectContaining({ sufficient: true }));
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('should have resolution', () => {
|
|
it.each([
|
|
['1080p landscape', '1080p', 1920, 1080],
|
|
['1080p portrait', '1080p', 1080, 1920],
|
|
['1080p approximate', '1080p', 1922, 1082],
|
|
|
|
['720p landscape', '720p', 1280, 720],
|
|
['720p portrait', '720p', 720, 1280],
|
|
['720p approximate', '720p', 1282, 722],
|
|
|
|
['VGA landscape', 'VGA', 640, 480],
|
|
['VGA portrait', 'VGA', 480, 640],
|
|
['VGA approximate', 'VGA', 642, 482],
|
|
|
|
['4K landscape', '4K', 3840, 2160],
|
|
['4K portrait', '4K', 2160, 3840],
|
|
['4K approximate', '4K', 3842, 2162],
|
|
|
|
['480p landscape', '480p', 720, 480],
|
|
['480p portrait', '480p', 480, 720],
|
|
['480p approximate', '480p', 722, 482],
|
|
|
|
['576p landscape', '576p', 720, 576],
|
|
['576p portrait', '576p', 576, 720],
|
|
['576p approximate', '576p', 722, 578],
|
|
|
|
['8K landscape', '8K', 7680, 4320],
|
|
['8K portrait', '8K', 4320, 7680],
|
|
['8K approximate', '8K', 7682, 4322],
|
|
|
|
['random', '123x456', 123, 456],
|
|
])(
|
|
'%s',
|
|
(_testName: string, expectedName: string, width: number, height: number) => {
|
|
const manager = new StatusBarItemManager(createCardAPI());
|
|
|
|
expect(
|
|
manager.calculateItems({
|
|
mediaLoadedInfo: { width, height },
|
|
}),
|
|
).toContainEqual({
|
|
type: 'custom:advanced-camera-card-status-bar-string' as const,
|
|
string: expectedName,
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
describe('should have technology', () => {
|
|
it('should show webrtc icon', () => {
|
|
const manager = new StatusBarItemManager(createCardAPI());
|
|
|
|
expect(
|
|
manager.calculateItems({
|
|
mediaLoadedInfo: { width: 640, height: 480, technology: ['webrtc'] },
|
|
}),
|
|
).toContainEqual({
|
|
type: 'custom:advanced-camera-card-status-bar-icon' as const,
|
|
icon: 'mdi:webrtc',
|
|
});
|
|
});
|
|
|
|
it('should show non-webrtc string', () => {
|
|
const manager = new StatusBarItemManager(createCardAPI());
|
|
|
|
expect(
|
|
manager.calculateItems({
|
|
mediaLoadedInfo: { width: 640, height: 480, technology: ['hls'] },
|
|
}),
|
|
).toContainEqual({
|
|
type: 'custom:advanced-camera-card-status-bar-string' as const,
|
|
string: 'HLS',
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should have engine', () => {
|
|
const manager = new StatusBarItemManager(createCardAPI());
|
|
const store = createStore([
|
|
{
|
|
cameraID: 'camera-1',
|
|
},
|
|
]);
|
|
const cameraManager = createCameraManager(store);
|
|
vi.mocked(cameraManager.getCameraMetadata).mockReturnValue({
|
|
title: 'Camera Title',
|
|
icon: {
|
|
icon: 'mdi:camera',
|
|
},
|
|
engineIcon: 'ENGINE_ICON',
|
|
});
|
|
|
|
expect(
|
|
manager.calculateItems({
|
|
cameraManager: cameraManager,
|
|
view: createView({ view: 'live', camera: 'camera-1' }),
|
|
}),
|
|
).toContainEqual({
|
|
type: 'custom:advanced-camera-card-status-bar-icon' as const,
|
|
icon: 'ENGINE_ICON',
|
|
});
|
|
});
|
|
|
|
describe('issues', () => {
|
|
it('should show issue items', () => {
|
|
const manager = new StatusBarItemManager(createCardAPI());
|
|
|
|
const items = manager.calculateItems({
|
|
issues: [
|
|
{
|
|
key: 'config_upgrade',
|
|
issue: {
|
|
icon: 'mdi:update',
|
|
severity: 'medium',
|
|
notification: {
|
|
heading: {
|
|
text: 'Upgrade available',
|
|
icon: 'mdi:update',
|
|
severity: 'medium',
|
|
},
|
|
body: { text: 'Upgrade text' },
|
|
},
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(items).toContainEqual(
|
|
expect.objectContaining({
|
|
type: 'custom:advanced-camera-card-status-bar-icon' as const,
|
|
icon: 'mdi:update',
|
|
severity: 'medium',
|
|
title: 'Upgrade available',
|
|
actions: expect.objectContaining({
|
|
tap_action: expect.objectContaining({
|
|
action: 'fire-dom-event',
|
|
advanced_camera_card_action: 'notification',
|
|
}),
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should not show issue items when empty', () => {
|
|
const manager = new StatusBarItemManager(createCardAPI());
|
|
|
|
const items = manager.calculateItems({
|
|
issues: [],
|
|
});
|
|
|
|
expect(items).not.toContainEqual(
|
|
expect.objectContaining({
|
|
icon: 'mdi:update',
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should not show issue items by default', () => {
|
|
const manager = new StatusBarItemManager(createCardAPI());
|
|
|
|
const items = manager.calculateItems();
|
|
|
|
expect(items).not.toContainEqual(
|
|
expect.objectContaining({
|
|
icon: 'mdi:update',
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should filter out all issues when disabled', () => {
|
|
const manager = new StatusBarItemManager(createCardAPI());
|
|
|
|
const items = manager.calculateItems({
|
|
statusConfig: {
|
|
auto_hide: [],
|
|
position: 'bottom',
|
|
style: 'popup',
|
|
popup_seconds: 3,
|
|
height: 40,
|
|
items: {
|
|
engine: { enabled: true, priority: 50 },
|
|
issues: { enabled: false, priority: 50 },
|
|
resolution: { enabled: true, priority: 50 },
|
|
severity: { enabled: true, priority: 50 },
|
|
technology: { enabled: true, priority: 50 },
|
|
title: { enabled: true, priority: 50 },
|
|
},
|
|
},
|
|
issues: [
|
|
{
|
|
key: 'config_upgrade',
|
|
issue: {
|
|
icon: 'mdi:update',
|
|
severity: 'medium',
|
|
notification: {
|
|
heading: {
|
|
text: 'Upgrade available',
|
|
icon: 'mdi:update',
|
|
severity: 'medium',
|
|
},
|
|
body: { text: 'Upgrade text' },
|
|
},
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(items).not.toContainEqual(
|
|
expect.objectContaining({
|
|
icon: 'mdi:update',
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should apply config overrides to issue items', () => {
|
|
const manager = new StatusBarItemManager(createCardAPI());
|
|
|
|
const items = manager.calculateItems({
|
|
statusConfig: {
|
|
auto_hide: [],
|
|
position: 'bottom',
|
|
style: 'popup',
|
|
popup_seconds: 3,
|
|
height: 40,
|
|
items: {
|
|
engine: { enabled: true, priority: 50 },
|
|
issues: { enabled: true, priority: 90 },
|
|
resolution: { enabled: true, priority: 50 },
|
|
severity: { enabled: true, priority: 50 },
|
|
technology: { enabled: true, priority: 50 },
|
|
title: { enabled: true, priority: 50 },
|
|
},
|
|
},
|
|
issues: [
|
|
{
|
|
key: 'config_upgrade',
|
|
issue: {
|
|
icon: 'mdi:update',
|
|
severity: 'medium',
|
|
notification: {
|
|
heading: {
|
|
text: 'Upgrade available',
|
|
icon: 'mdi:update',
|
|
severity: 'medium',
|
|
},
|
|
body: { text: 'Upgrade text' },
|
|
},
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(items).toContainEqual(
|
|
expect.objectContaining({
|
|
icon: 'mdi:update',
|
|
priority: 90,
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('severity', () => {
|
|
it('should have severity in a viewer view', () => {
|
|
const manager = new StatusBarItemManager(createCardAPI());
|
|
const selectedResult = new TestViewMedia({
|
|
cameraID: 'camera-1',
|
|
});
|
|
vi.spyOn(selectedResult, 'getSeverity').mockReturnValue('high');
|
|
|
|
const queryResults = new QueryResults({
|
|
results: [selectedResult],
|
|
selectedIndex: 0,
|
|
});
|
|
|
|
expect(
|
|
manager.calculateItems({
|
|
view: createView({ view: 'media', queryResults: queryResults }),
|
|
}),
|
|
).toContainEqual({
|
|
type: 'custom:advanced-camera-card-status-bar-icon' as const,
|
|
icon: 'mdi:circle-medium',
|
|
severity: 'high',
|
|
});
|
|
});
|
|
|
|
it('should not have severity in live view', () => {
|
|
const manager = new StatusBarItemManager(createCardAPI());
|
|
const selectedResult = new TestViewMedia({
|
|
cameraID: 'camera-1',
|
|
});
|
|
vi.spyOn(selectedResult, 'getSeverity').mockReturnValue('high');
|
|
|
|
const queryResults = new QueryResults({
|
|
results: [selectedResult],
|
|
selectedIndex: 0,
|
|
});
|
|
|
|
expect(
|
|
manager.calculateItems({
|
|
view: createView({ view: 'live', queryResults: queryResults }),
|
|
}),
|
|
).not.toContainEqual(
|
|
expect.objectContaining({
|
|
icon: 'mdi:circle-medium',
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
});
|
|
});
|