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 ```
464 lines
14 KiB
TypeScript
464 lines
14 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
|
import { StatusBarController } from '../../src/components-lib/status-bar-controller';
|
|
import {
|
|
StatusBarConfig,
|
|
statusBarConfigSchema,
|
|
} from '../../src/config/schema/status-bar';
|
|
import { setOrRemoveAttribute } from '../../src/utils/basic';
|
|
import { createInteractionActionEvent, createLitElement } from '../test-utils';
|
|
|
|
const createConfig = (config?: unknown): StatusBarConfig => {
|
|
return statusBarConfigSchema.parse(config);
|
|
};
|
|
|
|
// @vitest-environment jsdom
|
|
describe('StatusBarController', () => {
|
|
describe('should set config', () => {
|
|
it('should set config', () => {
|
|
const host = createLitElement();
|
|
const controller = new StatusBarController(host);
|
|
const config = createConfig({
|
|
position: 'top',
|
|
style: 'hover',
|
|
height: 50,
|
|
});
|
|
controller.setConfig(config);
|
|
|
|
expect(controller.getConfig()).toEqual(config);
|
|
expect(
|
|
host.style.getPropertyValue('--advanced-camera-card-status-bar-height'),
|
|
).toBe('50px');
|
|
expect(host.getAttribute('data-style')).toBe('hover');
|
|
expect(host.getAttribute('data-position')).toBe('top');
|
|
expect(host.requestUpdate).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not hide when not in popup style', () => {
|
|
const host = createLitElement();
|
|
setOrRemoveAttribute(host, true, 'hide');
|
|
|
|
const controller = new StatusBarController(host);
|
|
|
|
controller.setConfig(
|
|
createConfig({
|
|
position: 'top',
|
|
style: 'hover',
|
|
height: 50,
|
|
}),
|
|
);
|
|
|
|
expect(host.getAttribute('hide')).toBe(null);
|
|
});
|
|
|
|
it('should not show when in popup style', () => {
|
|
const host = createLitElement();
|
|
setOrRemoveAttribute(host, true, 'hide');
|
|
|
|
const controller = new StatusBarController(host);
|
|
|
|
controller.setConfig(
|
|
createConfig({
|
|
position: 'top',
|
|
style: 'popup',
|
|
height: 50,
|
|
}),
|
|
);
|
|
|
|
expect(host.getAttribute('hide')).not.toBe(null);
|
|
});
|
|
});
|
|
|
|
describe('should set items', () => {
|
|
beforeAll(() => {
|
|
vi.useFakeTimers();
|
|
});
|
|
|
|
afterAll(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('set/get basic items', () => {
|
|
const host = createLitElement();
|
|
const controller = new StatusBarController(host);
|
|
const items = [
|
|
{
|
|
type: 'custom:advanced-camera-card-status-bar-string' as const,
|
|
string: 'Test',
|
|
},
|
|
];
|
|
|
|
controller.setItems(items);
|
|
expect(controller.getRenderItems()).toEqual(items);
|
|
});
|
|
|
|
it('should order items', () => {
|
|
const host = createLitElement();
|
|
const controller = new StatusBarController(host);
|
|
const item1 = {
|
|
type: 'custom:advanced-camera-card-status-bar-string' as const,
|
|
string: 'Item 1',
|
|
priority: 40,
|
|
};
|
|
const item2 = {
|
|
type: 'custom:advanced-camera-card-status-bar-string' as const,
|
|
string: 'Item 2',
|
|
priority: 10,
|
|
};
|
|
const item3 = {
|
|
type: 'custom:advanced-camera-card-status-bar-string' as const,
|
|
string: 'Item 3',
|
|
priority: 60,
|
|
};
|
|
const item4 = {
|
|
type: 'custom:advanced-camera-card-status-bar-string' as const,
|
|
string: 'Item 4',
|
|
priority: undefined,
|
|
};
|
|
|
|
controller.setItems([item1, item2, item3, item4]);
|
|
expect(controller.getRenderItems()).toEqual([item3, item4, item1, item2]);
|
|
});
|
|
|
|
it('should treat exclusive items exclusively', () => {
|
|
const host = createLitElement();
|
|
const controller = new StatusBarController(host);
|
|
const item1 = {
|
|
type: 'custom:advanced-camera-card-status-bar-string' as const,
|
|
string: 'Item 1',
|
|
priority: 100,
|
|
};
|
|
const exclusiveItem = {
|
|
type: 'custom:advanced-camera-card-status-bar-string' as const,
|
|
string: 'Item 2',
|
|
priority: 1,
|
|
exclusive: true,
|
|
};
|
|
|
|
controller.setItems([item1, exclusiveItem]);
|
|
expect(controller.getRenderItems()).toEqual([exclusiveItem]);
|
|
});
|
|
|
|
describe('should recognize sufficient items', () => {
|
|
it('with sufficient item', () => {
|
|
const host = createLitElement();
|
|
const controller = new StatusBarController(host);
|
|
const insufficientItem = {
|
|
type: 'custom:advanced-camera-card-status-bar-string' as const,
|
|
string: 'Item 1',
|
|
sufficient: false,
|
|
};
|
|
const sufficientItem = {
|
|
type: 'custom:advanced-camera-card-status-bar-string' as const,
|
|
string: 'Item 2',
|
|
sufficient: true,
|
|
};
|
|
|
|
controller.setItems([insufficientItem, sufficientItem]);
|
|
expect(controller.getRenderItems()).toEqual([insufficientItem, sufficientItem]);
|
|
expect(controller.shouldRender()).toBeTruthy();
|
|
});
|
|
|
|
it('without sufficient item', () => {
|
|
const host = createLitElement();
|
|
const controller = new StatusBarController(host);
|
|
const insufficientItem = {
|
|
type: 'custom:advanced-camera-card-status-bar-string' as const,
|
|
string: 'Item 1',
|
|
sufficient: false,
|
|
};
|
|
const sufficientItem = {
|
|
type: 'custom:advanced-camera-card-status-bar-string' as const,
|
|
string: 'Item 2',
|
|
sufficient: false,
|
|
};
|
|
|
|
controller.setItems([insufficientItem, sufficientItem]);
|
|
expect(controller.getRenderItems()).toEqual([insufficientItem, sufficientItem]);
|
|
expect(controller.shouldRender()).toBeFalsy();
|
|
});
|
|
});
|
|
|
|
describe('should deal with popup styles correctly', () => {
|
|
it('should show from empty to sufficient', () => {
|
|
const host = createLitElement();
|
|
setOrRemoveAttribute(host, true, 'hide');
|
|
|
|
const controller = new StatusBarController(host);
|
|
controller.setConfig(
|
|
createConfig({
|
|
style: 'popup',
|
|
}),
|
|
);
|
|
|
|
const sufficientItem = {
|
|
type: 'custom:advanced-camera-card-status-bar-string' as const,
|
|
string: 'Item 1',
|
|
priority: 100,
|
|
sufficient: true,
|
|
};
|
|
|
|
controller.setItems([sufficientItem]);
|
|
expect(host.getAttribute('hide')).toBe(null);
|
|
});
|
|
|
|
it('should not show from empty to insufficient', () => {
|
|
const host = createLitElement();
|
|
setOrRemoveAttribute(host, true, 'hide');
|
|
|
|
const controller = new StatusBarController(host);
|
|
controller.setConfig(
|
|
createConfig({
|
|
style: 'popup',
|
|
}),
|
|
);
|
|
|
|
const insufficientItem = {
|
|
type: 'custom:advanced-camera-card-status-bar-string' as const,
|
|
string: 'Item 1',
|
|
priority: 100,
|
|
sufficient: false,
|
|
};
|
|
|
|
controller.setItems([insufficientItem]);
|
|
expect(host.getAttribute('hide')).not.toBeNull();
|
|
});
|
|
|
|
it('should show from sufficient to different sufficient', () => {
|
|
const host = createLitElement();
|
|
|
|
const controller = new StatusBarController(host);
|
|
controller.setConfig(
|
|
createConfig({
|
|
style: 'popup',
|
|
}),
|
|
);
|
|
|
|
const sufficientString = {
|
|
type: 'custom:advanced-camera-card-status-bar-string' as const,
|
|
string: 'String',
|
|
priority: 100,
|
|
sufficient: true,
|
|
};
|
|
const sufficientIcon = {
|
|
type: 'custom:advanced-camera-card-status-bar-icon' as const,
|
|
icon: 'Icon',
|
|
priority: 100,
|
|
sufficient: true,
|
|
};
|
|
const sufficientImage = {
|
|
type: 'custom:advanced-camera-card-status-bar-image' as const,
|
|
image: 'Image',
|
|
priority: 100,
|
|
sufficient: true,
|
|
};
|
|
|
|
controller.setItems([sufficientString]);
|
|
|
|
// Emulate the popup being hidden.
|
|
setOrRemoveAttribute(host, true, 'hide');
|
|
controller.setItems([sufficientIcon]);
|
|
|
|
expect(host.getAttribute('hide')).toBe(null);
|
|
|
|
// Emulate the popup being hidden.
|
|
setOrRemoveAttribute(host, true, 'hide');
|
|
controller.setItems([sufficientImage]);
|
|
|
|
expect(host.getAttribute('hide')).toBe(null);
|
|
});
|
|
|
|
it('should not start popup timer when permanent items are present', () => {
|
|
const host = createLitElement();
|
|
setOrRemoveAttribute(host, true, 'hide');
|
|
|
|
const controller = new StatusBarController(host);
|
|
controller.setConfig(
|
|
createConfig({
|
|
style: 'popup',
|
|
}),
|
|
);
|
|
|
|
const permanentItem = {
|
|
type: 'custom:advanced-camera-card-status-bar-icon' as const,
|
|
icon: 'mdi:alert',
|
|
sufficient: true,
|
|
permanent: true,
|
|
};
|
|
|
|
controller.setItems([permanentItem]);
|
|
expect(host.getAttribute('hide')).toBe(null);
|
|
|
|
// Timer should not hide the bar.
|
|
vi.advanceTimersByTime(10000);
|
|
expect(host.getAttribute('hide')).toBe(null);
|
|
});
|
|
|
|
it('should start popup timer when permanent items are removed', () => {
|
|
const host = createLitElement();
|
|
|
|
const controller = new StatusBarController(host);
|
|
controller.setConfig(
|
|
createConfig({
|
|
style: 'popup',
|
|
}),
|
|
);
|
|
|
|
const permanentItem = {
|
|
type: 'custom:advanced-camera-card-status-bar-icon' as const,
|
|
icon: 'mdi:alert',
|
|
sufficient: true,
|
|
permanent: true,
|
|
};
|
|
const nonPermanentItem = {
|
|
type: 'custom:advanced-camera-card-status-bar-string' as const,
|
|
string: 'Title',
|
|
sufficient: true,
|
|
};
|
|
|
|
// Start with permanent item — bar stays visible.
|
|
controller.setItems([permanentItem, nonPermanentItem]);
|
|
vi.advanceTimersByTime(10000);
|
|
expect(host.getAttribute('hide')).toBe(null);
|
|
|
|
// Remove permanent item — popup timer starts.
|
|
controller.setItems([nonPermanentItem]);
|
|
expect(host.getAttribute('hide')).toBe(null);
|
|
|
|
vi.advanceTimersByTime(3000);
|
|
expect(host.getAttribute('hide')).not.toBe(null);
|
|
});
|
|
|
|
it('should start popup timer when permanent item is removed without changing sufficient items', () => {
|
|
const host = createLitElement();
|
|
|
|
const controller = new StatusBarController(host);
|
|
controller.setConfig(
|
|
createConfig({
|
|
style: 'popup',
|
|
}),
|
|
);
|
|
|
|
const sufficientItem = {
|
|
type: 'custom:advanced-camera-card-status-bar-string' as const,
|
|
string: 'Title',
|
|
sufficient: true,
|
|
};
|
|
// A permanent item that is NOT sufficient — removing it does not
|
|
// change the sufficient-values set, so the popup timer takes the
|
|
// dedicated permanent-removal branch.
|
|
const permanentInsufficientItem = {
|
|
type: 'custom:advanced-camera-card-status-bar-icon' as const,
|
|
icon: 'mdi:alert',
|
|
sufficient: false,
|
|
permanent: true,
|
|
};
|
|
|
|
controller.setItems([sufficientItem, permanentInsufficientItem]);
|
|
vi.advanceTimersByTime(10000);
|
|
expect(host.getAttribute('hide')).toBe(null);
|
|
|
|
// Remove the permanent (insufficient) item — sufficient values are
|
|
// unchanged, but the popup timer must still start.
|
|
controller.setItems([sufficientItem]);
|
|
expect(host.getAttribute('hide')).toBe(null);
|
|
|
|
vi.advanceTimersByTime(3000);
|
|
expect(host.getAttribute('hide')).not.toBe(null);
|
|
});
|
|
|
|
it('should hide popup after expiry', () => {
|
|
const host = createLitElement();
|
|
|
|
const controller = new StatusBarController(host);
|
|
controller.setConfig(
|
|
createConfig({
|
|
style: 'popup',
|
|
}),
|
|
);
|
|
|
|
const sufficientItem = {
|
|
type: 'custom:advanced-camera-card-status-bar-string' as const,
|
|
string: 'Item 1',
|
|
priority: 100,
|
|
sufficient: true,
|
|
};
|
|
|
|
controller.setItems([sufficientItem]);
|
|
expect(host.getAttribute('hide')).toBe(null);
|
|
|
|
vi.advanceTimersByTime(1000);
|
|
expect(host.getAttribute('hide')).toBe(null);
|
|
|
|
vi.advanceTimersByTime(2000);
|
|
expect(host.getAttribute('hide')).not.toBe(null);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('should handle actions', () => {
|
|
it('should bail without action', () => {
|
|
const host = createLitElement();
|
|
const handler = vi.fn();
|
|
host.addEventListener('advanced-camera-card:action:execution-request', handler);
|
|
|
|
const controller = new StatusBarController(host);
|
|
controller.actionHandler(createInteractionActionEvent('tap'));
|
|
expect(handler).not.toBeCalled();
|
|
});
|
|
|
|
it('should request action execution', () => {
|
|
const host = createLitElement();
|
|
const handler = vi.fn();
|
|
host.addEventListener('advanced-camera-card:action:execution-request', handler);
|
|
|
|
const controller = new StatusBarController(host);
|
|
|
|
const action = {
|
|
action: 'fire-dom-event' as const,
|
|
};
|
|
const tapActionConfig = {
|
|
tap_action: action,
|
|
};
|
|
|
|
controller.actionHandler(createInteractionActionEvent('tap'), tapActionConfig);
|
|
|
|
expect(handler).toBeCalledWith(
|
|
expect.objectContaining({
|
|
detail: { actions: [action], config: tapActionConfig },
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('auto-hide', () => {
|
|
const sufficientItem = {
|
|
type: 'custom:advanced-camera-card-status-bar-string' as const,
|
|
string: 'Item',
|
|
sufficient: true,
|
|
};
|
|
|
|
it('should render with a config and no auto-hide state', () => {
|
|
const controller = new StatusBarController(createLitElement());
|
|
controller.setConfig(createConfig({ auto_hide: ['call'] }));
|
|
controller.setItems([sufficientItem]);
|
|
expect(controller.shouldRender()).toBe(true);
|
|
});
|
|
|
|
it('should render when no auto-hide condition is active', () => {
|
|
const controller = new StatusBarController(createLitElement());
|
|
controller.setConfig(createConfig({ auto_hide: ['call'] }));
|
|
controller.setItems([sufficientItem]);
|
|
controller.setAutoHideState({ call: false, casting: true });
|
|
expect(controller.shouldRender()).toBe(true);
|
|
});
|
|
|
|
it('should not render when a configured auto-hide condition is active', () => {
|
|
const controller = new StatusBarController(createLitElement());
|
|
controller.setConfig(createConfig({ auto_hide: ['call'] }));
|
|
controller.setItems([sufficientItem]);
|
|
controller.setAutoHideState({ call: true, casting: false });
|
|
expect(controller.shouldRender()).toBe(false);
|
|
});
|
|
});
|
|
});
|