Files
advanced-camera-card/tests/config/management.test.ts
T

6590 lines
205 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
addConfigArrayItem,
copyConfig,
createRangedTransform,
deleteConfigArrayItem,
deleteConfigValue,
deleteTransform,
deleteWithOverrides,
getConfigValue,
hasConfigUpgradeFailures,
isConfigUpgradeable,
moveConfigArrayItem,
moveConfigValue,
setConfigValue,
upgradeArrayOfObjects,
upgradeConfig,
upgradeMoveTo,
upgradeMoveToWithOverrides,
upgradeObjectRecursively,
upgradeWithOverrides,
} from '../../src/config/management.js';
import type { PTZControlAction } from '../../src/config/schema/actions/custom/ptz.js';
import type { CallServiceActionConfig } from '../../src/config/schema/actions/stock/call-service.js';
import type { PerformActionActionConfig } from '../../src/config/schema/actions/stock/perform-action.js';
import type { Actions } from '../../src/config/schema/actions/types.js';
import { advancedCameraCardConfigSchema } from '../../src/config/schema/types.js';
import type { RawAdvancedCameraCardConfig } from '../../src/config/types.js';
import { getParseErrorPaths } from '../../src/utils/zod/parse-errors.js';
describe('general functions', () => {
it('should set value', () => {
const target = {};
setConfigValue(target, 'a', 10);
expect(target).toEqual({
a: 10,
});
});
describe('should get value', () => {
it('should get a present value', () => {
expect(getConfigValue({ b: 11 }, 'b')).toEqual(11);
});
it('should return undefined for an absent value', () => {
expect(getConfigValue({ b: 11 }, 'c')).toBeUndefined();
});
it('should return the default for an absent value', () => {
expect(getConfigValue({ b: 11 }, 'c', 12)).toBe(12);
});
});
describe('should unset value', () => {
it('should unset a nested value', () => {
const target = {
moo: {
foo: {
a: 10,
},
bar: {
b: 11,
},
},
};
deleteConfigValue(target, 'moo.foo');
expect(target).toEqual({ moo: { bar: { b: 11 } } });
});
it('should unset a top-level value', () => {
const target = {
a: 10,
b: 11,
};
deleteConfigValue(target, 'a');
expect(target).toEqual({ b: 11 });
});
});
it('should copy config', () => {
const target = {
a: {
b: {
c: 10,
},
},
};
const copy = copyConfig(target);
expect(copy).toEqual(target);
expect(copy).not.toBe(target);
});
describe('addConfigArrayItem', () => {
it('should append an item', () => {
const config = { cameras: [{ id: 'a' }] };
expect(addConfigArrayItem(config, 'cameras', { id: 'b' })).toBe(true);
expect(config.cameras).toEqual([{ id: 'a' }, { id: 'b' }]);
});
it('should append at a segment-array path', () => {
const config = { cameras: [{ triggers: { events: [{ a: 1 }] } }] };
expect(
addConfigArrayItem(config, ['cameras', 0, 'triggers', 'events'], { b: 2 }),
).toBe(true);
expect(config.cameras[0].triggers.events).toEqual([{ a: 1 }, { b: 2 }]);
});
it('should create the array when the configuration has none', () => {
const config = {};
expect(addConfigArrayItem(config, 'cameras', { id: 'a' })).toBe(true);
expect(config).toEqual({ cameras: [{ id: 'a' }] });
});
it('should replace a value that is not an array', () => {
const config = { cameras: 'junk' };
expect(addConfigArrayItem(config, 'cameras', { id: 'a' })).toBe(true);
expect(config).toEqual({ cameras: [{ id: 'a' }] });
});
});
describe('moveConfigArrayItem', () => {
it('should move an item at a segment-array path', () => {
const config = { cameras: [{ triggers: { events: [{ a: 1 }, { b: 2 }] } }] };
expect(
moveConfigArrayItem(config, ['cameras', 0, 'triggers', 'events'], 0, 1),
).toBe(true);
expect(config.cameras[0].triggers.events).toEqual([{ b: 2 }, { a: 1 }]);
});
it('should move an item in place and report a change', () => {
const config = { cameras: [{ id: 'a' }, { id: 'b' }] };
expect(moveConfigArrayItem(config, 'cameras', 0, 1)).toBe(true);
expect(config).toEqual({ cameras: [{ id: 'b' }, { id: 'a' }] });
});
it('should return false when the path is not an array', () => {
const config = { cameras: 'nope' };
expect(moveConfigArrayItem(config, 'cameras', 0, 1)).toBe(false);
expect(config).toEqual({ cameras: 'nope' });
});
it.each([
['same index', 0, 0],
['from out of bounds', 2, 0],
['to out of bounds', 0, 2],
['negative from', -1, 0],
['negative to', 0, -1],
['non-integer from', 0.5, 1],
['non-integer to', 0, Number.NaN],
])(
'should return false and not modify for an impossible move (%s)',
(_name, from, to) => {
const config = { cameras: [{ id: 'a' }, { id: 'b' }] };
expect(moveConfigArrayItem(config, 'cameras', from, to)).toBe(false);
expect(config).toEqual({ cameras: [{ id: 'a' }, { id: 'b' }] });
},
);
});
describe('deleteConfigArrayItem', () => {
it('should delete an item in place and report a change', () => {
const config = { cameras: [{ id: 'a' }, { id: 'b' }] };
expect(deleteConfigArrayItem(config, 'cameras', 0)).toBe(true);
expect(config).toEqual({ cameras: [{ id: 'b' }] });
});
it('should return false when the path is not an array', () => {
const config = { cameras: 'nope' };
expect(deleteConfigArrayItem(config, 'cameras', 0)).toBe(false);
expect(config).toEqual({ cameras: 'nope' });
});
it.each([
['out of bounds', 1],
['negative', -1],
['non-integer', 0.5],
])(
'should return false and not modify for an impossible delete (%s)',
(_name, index) => {
const config = { cameras: [{ id: 'a' }] };
expect(deleteConfigArrayItem(config, 'cameras', index)).toBe(false);
expect(config).toEqual({ cameras: [{ id: 'a' }] });
},
);
});
});
describe('upgrade functions', () => {
it('should determine if config is upgradeable', () => {
expect(
// Upgrade example: rename of service_data to data.
isConfigUpgradeable({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'icon',
icon: 'mdi:cow',
style: {
right: '20px',
top: '20px',
color: 'white',
},
tap_action: {
action: 'call-service',
service: 'notify.persistent_notification',
service_data: {
message: 'Hello 1',
},
},
},
],
}),
).toBeTruthy();
});
describe('should detect config upgrade failures', () => {
it('should report no failures for a null config', () => {
expect(hasConfigUpgradeFailures(null)).toBeFalsy();
});
it('should report no failures when nothing has failed', () => {
expect(
hasConfigUpgradeFailures({ type: 'custom:advanced-camera-card' }),
).toBeFalsy();
});
it('should report no failures when the namespace is not a record', () => {
expect(hasConfigUpgradeFailures({ __UPGRADE_FAILURE__: 'nonsense' })).toBeFalsy();
});
it('should report no failures for an empty namespace', () => {
expect(hasConfigUpgradeFailures({ __UPGRADE_FAILURE__: {} })).toBeFalsy();
});
it('should report a failure when the namespace is non-empty', () => {
expect(
hasConfigUpgradeFailures({
__UPGRADE_FAILURE__: { automations: [{ actions_not: [] }] },
}),
).toBeTruthy();
});
});
describe('should create ranged transform', () => {
describe('with numbers', () => {
it('should keep a value inside the range', () => {
expect(createRangedTransform((val) => val, 10, 20)(11)).toBe(11);
});
it('should clamp a value outside the range', () => {
expect(createRangedTransform((val) => val, 10, 20)(1)).toBe(10);
});
it('should pass a value through with no range bounds', () => {
expect(createRangedTransform((val) => val)(100)).toBe(100);
});
});
it('should pass a non-number through', () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
expect(createRangedTransform((_val) => 'foo')(1)).toBe('foo');
});
});
it('should return null from delete property transform', () => {
expect(deleteTransform(10)).toBeNull();
});
describe('should move config value', () => {
it('should move a value', () => {
const config = {
foo: {
c: 10,
},
};
expect(moveConfigValue(config, 'foo', 'bar')).toBeTruthy();
expect(config).toEqual({
bar: {
c: 10,
},
});
});
describe('in place', () => {
it('should not modify a non-transformed in-place value', () => {
const config = {
foo: {
c: 10,
},
};
expect(moveConfigValue(config, 'foo', 'foo')).toBeFalsy();
expect(config).toEqual({
foo: {
c: 10,
},
});
});
it('should transform an in-place value', () => {
const config = {
foo: {
c: 10,
},
};
expect(
moveConfigValue(config, 'foo.c', 'foo.c', { transform: (val) => String(val) }),
).toBeTruthy();
expect(config).toEqual({
foo: {
c: '10',
},
});
});
});
describe('with transform result', () => {
it('should move with a transform result', () => {
const config = {
c: 10,
};
expect(
moveConfigValue(config, 'c', 'd', { transform: (val) => String(val) }),
).toBeTruthy();
expect(config).toEqual({ d: '10' });
});
it('should keep the original with a transform result', () => {
const config = {
c: 10,
};
expect(
moveConfigValue(config, 'c', 'd', {
transform: (val) => String(val),
keepOriginal: true,
}),
).toBeTruthy();
expect(config).toEqual({ c: 10, d: '10' });
});
});
describe('with transform null result', () => {
it('should remove on a null transform result', () => {
const config = {
c: 10,
};
expect(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
moveConfigValue(config, 'c', 'd', { transform: (_val) => null }),
).toBeTruthy();
expect(config).toEqual({});
});
it('should keep the original on a null transform result', () => {
const config = {
c: 10,
};
expect(
moveConfigValue(config, 'c', 'd', {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
transform: (_val) => null,
keepOriginal: true,
}),
).toBeFalsy();
expect(config).toEqual({ c: 10 });
});
});
it('should not modify on an undefined transform result', () => {
const config = {
c: 10,
};
expect(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
moveConfigValue(config, 'c', 'd', { transform: (_val) => undefined }),
).toBeFalsy();
expect(config).toEqual({ c: 10 });
});
});
it('should upgrade with a move', () => {
const config = {
c: 10,
};
expect(upgradeMoveTo('c', 'd')(config)).toBeTruthy();
expect(config).toEqual({ d: 10 });
});
it('should upgrade config and overrides with a move', () => {
const config = {
c: 10,
overrides: [
{
merge: {
c: 10,
},
set: {
c: 10,
},
},
],
};
expect(upgradeMoveToWithOverrides('c', 'd')(config)).toBeTruthy();
expect(config).toEqual({ d: 10, overrides: [{ merge: { d: 10 }, set: { d: 10 } }] });
});
it('should upgrade config and overrides in-place', () => {
const config = {
c: 10,
overrides: [
{
merge: {
c: 10,
},
},
],
};
expect(upgradeWithOverrides('c', (val) => String(val))(config)).toBeTruthy();
expect(config).toEqual({ c: '10', overrides: [{ merge: { c: '10' } }] });
});
describe('should upgrade array', () => {
it('should handle a non-array', () => {
const config = { c: 10 };
// eslint-disable-next-line @typescript-eslint/no-unused-vars
expect(upgradeArrayOfObjects('c', (_val) => false)(config)).toBeFalsy();
});
it('should handle non-object items', () => {
const config = { c: [10, 11] };
// eslint-disable-next-line @typescript-eslint/no-unused-vars
expect(upgradeArrayOfObjects('c', (_val) => false)(config)).toBeFalsy();
});
it('should upgrade each object in an array', () => {
const config = { c: [{ d: 10 }, { d: 11 }] };
expect(
upgradeArrayOfObjects('c', (val) => {
val['e'] = 12;
return true;
})(config),
).toBeTruthy();
expect(config).toEqual({
c: [
{
d: 10,
e: 12,
},
{
d: 11,
e: 12,
},
],
});
});
});
describe('should recursively upgrade', () => {
it('should ignore simple objects', () => {
const config = { c: 10, d: 10 };
// eslint-disable-next-line @typescript-eslint/no-unused-vars
expect(upgradeObjectRecursively((_val) => false)(config)).toBeFalsy();
expect(config).toEqual({ c: 10, d: 10 });
});
it('should get sub-objects', () => {
const config = { c: 10, target_obj: { target_val: 10 } };
expect(
upgradeObjectRecursively(
(val: RawAdvancedCameraCardConfig) => {
(val['target_val'] as number)++;
return true;
},
(obj) => obj['target_obj'] as RawAdvancedCameraCardConfig,
)(config),
).toBeTruthy();
expect(config).toEqual({ c: 10, target_obj: { target_val: 11 } });
});
it('should iterate into arrays', () => {
const config = { values: [{ c: 10 }, { d: 10 }, 'random'] };
expect(
upgradeObjectRecursively((val) => {
if (!Array.isArray(val)) {
val['e'] = 11;
}
return true;
})(config),
).toBeTruthy();
expect(config).toEqual({
e: 11,
values: [{ c: 10, e: 11 }, { d: 10, e: 11 }, 'random'],
});
});
});
it('should have upgrades with bad input data', () => {
expect(upgradeConfig(3 as unknown as RawAdvancedCameraCardConfig)).toBeFalsy();
});
it('should delete properties', () => {
const config = { c: 10, d: 10 };
expect(deleteWithOverrides('c')(config)).toBeTruthy();
expect(config).toEqual({ d: 10 });
});
});
describe('should handle version specific upgrades', () => {
const postUpgradeChecks = (config: RawAdvancedCameraCardConfig): void => {
// Should be no additional upgrades.
expect(upgradeConfig(config)).toBeFalsy();
// Result should be parseable.
const result = advancedCameraCardConfigSchema.safeParse(config);
if (!result.success) {
expect(
result.success,
'Post-upgrade parse error.\n\n' +
'Problem paths: ' +
[...getParseErrorPaths(result.error)] +
'\n\n' +
JSON.stringify(config, null, 2),
).toBeTruthy();
}
};
describe('v5.2.0 -> v6.0.0', () => {
describe('should rename service_data -> data', () => {
it('should rename service_data -> data', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'icon',
icon: 'mdi:cow',
style: {
right: '20px',
top: '20px',
color: 'white',
},
tap_action: {
action: 'call-service',
service: 'notify.persistent_notification',
service_data: {
message: 'Hello 1',
},
},
},
{
type: 'service-button',
title: 'title',
service: 'service',
service_data: {
message: "It's a trick",
},
},
],
view: {
actions: {
double_tap_action: {
action: 'call-service',
service: 'notify.persistent_notification',
service_data: {
message: 'Hello 2',
},
},
hold_action: {
action: 'call-service',
service: 'notify.persistent_notification',
data: {
message: 'Hello 3',
},
},
},
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'icon',
icon: 'mdi:cow',
style: {
right: '20px',
top: '20px',
color: 'white',
},
tap_action: {
action: 'perform-action',
perform_action: 'notify.persistent_notification',
data: {
message: 'Hello 1',
},
},
},
{
type: 'service-button',
title: 'title',
service: 'service',
// Trick: This *is* still called service_data in HA, so should not
// be modified.
service_data: {
message: "It's a trick",
},
},
],
view: {
actions: {
double_tap_action: {
action: 'perform-action',
perform_action: 'notify.persistent_notification',
data: {
message: 'Hello 2',
},
},
hold_action: {
action: 'perform-action',
perform_action: 'notify.persistent_notification',
data: {
message: 'Hello 3',
},
},
},
},
});
postUpgradeChecks(config);
});
it('should not change a config with nothing to rename', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
view: {
default: 'live',
},
};
expect(upgradeConfig(config)).toBeFalsy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
view: {
default: 'live',
},
});
postUpgradeChecks(config);
});
});
describe('should move PTZ elements to live', () => {
it('should move a single PTZ element to live', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:advanced-camera-card-ptz',
orientation: 'vertical',
style: {
right: '20px',
top: '20px',
color: 'white',
},
actions_up: {
tap_action: {
action: 'call-service',
service: 'notify.persistent_notification',
service_data: {
message: 'Hello 1',
},
},
},
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
cameras: [{ camera_entity: 'camera.office' }],
cameras_global: {
ptz: {
actions_up: {
action: 'perform-action',
data: {
message: 'Hello 1',
},
perform_action: 'notify.persistent_notification',
},
},
},
live: {
controls: {
ptz: {
orientation: 'vertical',
style: {
color: 'white',
right: '20px',
top: '20px',
},
},
},
},
type: 'custom:advanced-camera-card',
});
postUpgradeChecks(config);
});
it('should move a PTZ element to live and keep other elements', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:advanced-camera-card-ptz',
orientation: 'vertical',
style: {
right: '20px',
top: '20px',
color: 'white',
},
actions_up: {
tap_action: {
action: 'call-service',
service: 'notify.persistent_notification',
service_data: {
message: 'Hello 1',
},
},
},
},
{
type: 'service-button',
title: 'title',
service: 'service',
service_data: {
message: "It's a trick",
},
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
cameras: [{ camera_entity: 'camera.office' }],
cameras_global: {
ptz: {
actions_up: {
action: 'perform-action',
data: {
message: 'Hello 1',
},
perform_action: 'notify.persistent_notification',
},
},
},
elements: [
{
service: 'service',
service_data: {
message: "It's a trick",
},
title: 'title',
type: 'service-button',
},
],
live: {
controls: {
ptz: {
orientation: 'vertical',
style: {
color: 'white',
right: '20px',
top: '20px',
},
},
},
},
type: 'custom:advanced-camera-card',
});
postUpgradeChecks(config);
});
it('should move PTZ from a custom conditional element with nothing else remaining', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:advanced-camera-card-conditional',
conditions: {
fullscreen: true,
media_loaded: true,
},
elements: [
{
type: 'custom:advanced-camera-card-ptz',
orientation: 'vertical',
style: {
right: '20px',
top: '20px',
color: 'white',
},
actions_up: {
tap_action: {
action: 'call-service',
service: 'notify.persistent_notification',
service_data: {
message: 'Hello 1',
},
},
},
},
{
type: 'custom:advanced-camera-card-ptz',
orientation: 'vertical',
style: {
right: '20px',
top: '20px',
color: 'white',
},
actions_up: {
tap_action: {
action: 'call-service',
service: 'notify.persistent_notification',
service_data: {
message: 'Hello 2',
},
},
},
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
cameras: [{ camera_entity: 'camera.office' }],
cameras_global: {
ptz: {
actions_up: {
action: 'perform-action',
data: {
message: 'Hello 1',
},
perform_action: 'notify.persistent_notification',
},
},
},
live: {
controls: {
ptz: {
orientation: 'vertical',
style: {
color: 'white',
right: '20px',
top: '20px',
},
},
},
},
type: 'custom:advanced-camera-card',
});
postUpgradeChecks(config);
});
it('should move PTZ from a custom conditional element keeping the other element', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:advanced-camera-card-conditional',
conditions: {
fullscreen: true,
media_loaded: true,
},
elements: [
{
type: 'service-button',
title: 'title',
service: 'service',
service_data: {
message: "It's a trick",
},
},
{
type: 'custom:advanced-camera-card-ptz',
orientation: 'vertical',
style: {
right: '20px',
top: '20px',
color: 'white',
},
actions_up: {
tap_action: {
action: 'call-service',
service: 'notify.persistent_notification',
service_data: {
message: 'Hello 1',
},
},
},
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
cameras: [{ camera_entity: 'camera.office' }],
cameras_global: {
ptz: {
actions_up: {
action: 'perform-action',
data: {
message: 'Hello 1',
},
perform_action: 'notify.persistent_notification',
},
},
},
live: {
controls: {
ptz: {
orientation: 'vertical',
style: {
color: 'white',
right: '20px',
top: '20px',
},
},
},
},
elements: [
{
type: 'custom:advanced-camera-card-conditional',
conditions: [
{
condition: 'fullscreen' as const,
fullscreen: true,
},
{
condition: 'media_loaded' as const,
media_loaded: true,
},
],
elements: [
{
type: 'service-button',
title: 'title',
service: 'service',
service_data: {
message: "It's a trick",
},
},
],
},
],
type: 'custom:advanced-camera-card',
});
postUpgradeChecks(config);
});
it('should move PTZ from a stock conditional element', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'conditional',
conditions: [{ entity: 'light.office', state: 'on' }],
elements: [
{
type: 'custom:advanced-camera-card-ptz',
orientation: 'vertical',
style: {
right: '20px',
top: '20px',
color: 'white',
},
actions_up: {
tap_action: {
action: 'call-service',
service: 'notify.persistent_notification',
service_data: {
message: 'Hello 1',
},
},
},
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
cameras: [{ camera_entity: 'camera.office' }],
cameras_global: {
ptz: {
actions_up: {
action: 'perform-action',
data: {
message: 'Hello 1',
},
perform_action: 'notify.persistent_notification',
},
},
},
live: {
controls: {
ptz: {
orientation: 'vertical',
style: {
color: 'white',
right: '20px',
top: '20px',
},
},
},
},
type: 'custom:advanced-camera-card',
});
postUpgradeChecks(config);
});
it('should not overwrite an existing live.controls.ptz', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
live: {
controls: {
ptz: {
actions_up: {
tap_action: {
action: 'call-service',
data: {
message: 'Original',
},
service: 'notify.persistent_notification',
},
},
orientation: 'vertical',
style: {
color: 'white',
right: '20px',
top: '20px',
},
},
},
},
elements: [
{
type: 'custom:advanced-camera-card-ptz',
orientation: 'vertical',
style: {
right: '20px',
top: '20px',
color: 'white',
},
actions_up: {
tap_action: {
action: 'call-service',
service: 'notify.persistent_notification',
service_data: {
message: 'Replacement that should be ignored',
},
},
},
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
cameras: [{ camera_entity: 'camera.office' }],
cameras_global: {
ptz: {
actions_up: {
action: 'perform-action',
data: {
message: 'Original',
},
perform_action: 'notify.persistent_notification',
},
},
},
live: {
controls: {
ptz: {
orientation: 'vertical',
style: {
color: 'white',
right: '20px',
top: '20px',
},
},
},
},
type: 'custom:advanced-camera-card',
});
postUpgradeChecks(config);
});
it('should not upgrade ptz settings-only config', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
live: {
controls: {
ptz: {
type: 'gestures',
hide_type: true,
mode: 'on',
orientation: 'vertical',
position: 'bottom-right',
},
},
},
};
expect(upgradeConfig(config)).toBeFalsy();
});
});
it('should move view.timeout_seconds', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
view: {
timeout_seconds: 200,
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
view: {
interaction_seconds: 200,
},
});
postUpgradeChecks(config);
});
describe('should handle all and never action conditions', () => {
describe('live', () => {
describe('lazy_unload', () => {
it('should handle all', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
lazy_unload: 'all',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
lazy_unload: ['unselected', 'hidden'],
},
});
postUpgradeChecks(config);
});
it('should handle never', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
lazy_unload: 'never',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {},
});
postUpgradeChecks(config);
});
it('should handle another value', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
lazy_unload: 'unselected',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
lazy_unload: ['unselected'],
},
});
postUpgradeChecks(config);
});
});
describe('auto_play', () => {
it('should handle all', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
auto_play: 'all',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {},
});
postUpgradeChecks(config);
});
it('should handle never', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
auto_play: 'never',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
auto_play: [],
},
});
postUpgradeChecks(config);
});
it('should handle another value', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
auto_play: 'selected',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
auto_play: ['selected'],
},
});
postUpgradeChecks(config);
});
});
describe('auto_pause', () => {
it('should handle all', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
auto_pause: 'all',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
auto_pause: ['unselected', 'hidden'],
},
});
postUpgradeChecks(config);
});
it('should handle never', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
auto_pause: 'never',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {},
});
postUpgradeChecks(config);
});
it('should handle another value', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
auto_pause: 'unselected',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
auto_pause: ['unselected'],
},
});
postUpgradeChecks(config);
});
});
describe('auto_mute', () => {
it('should handle all', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
auto_mute: 'all',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {},
});
postUpgradeChecks(config);
});
it('should handle never', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
auto_mute: 'never',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
auto_mute: [],
},
});
postUpgradeChecks(config);
});
it('should handle another value', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
auto_mute: 'unselected',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
auto_mute: ['unselected'],
},
});
postUpgradeChecks(config);
});
});
describe('auto_unmute', () => {
it('should handle all', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
auto_unmute: 'all',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
auto_unmute: ['selected', 'visible', 'microphone'],
},
});
postUpgradeChecks(config);
});
it('should handle never', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
auto_unmute: 'never',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {},
});
postUpgradeChecks(config);
});
it('should handle another value', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
auto_unmute: 'selected',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
auto_unmute: ['selected'],
},
});
postUpgradeChecks(config);
});
});
});
describe('media_viewer', () => {
describe('auto_play', () => {
it('should handle all', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
media_viewer: {
auto_play: 'all',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
media_viewer: {},
});
postUpgradeChecks(config);
});
it('should handle never', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
media_viewer: {
auto_play: 'never',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
media_viewer: {
auto_play: [],
},
});
postUpgradeChecks(config);
});
it('should handle another value', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
media_viewer: {
auto_play: 'selected',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
media_viewer: {
auto_play: ['selected'],
},
});
postUpgradeChecks(config);
});
});
describe('auto_pause', () => {
it('should handle all', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
media_viewer: {
auto_pause: 'all',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
media_viewer: {},
});
postUpgradeChecks(config);
});
it('should handle never', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
media_viewer: {
auto_pause: 'never',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
media_viewer: {
auto_pause: [],
},
});
postUpgradeChecks(config);
});
it('should handle another value', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
media_viewer: {
auto_pause: 'unselected',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
media_viewer: {
auto_pause: ['unselected'],
},
});
postUpgradeChecks(config);
});
});
describe('auto_mute', () => {
it('should handle all', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
media_viewer: {
auto_mute: 'all',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
media_viewer: {},
});
postUpgradeChecks(config);
});
it('should handle never', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
media_viewer: {
auto_mute: 'never',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
media_viewer: {
auto_mute: [],
},
});
postUpgradeChecks(config);
});
it('should handle another value', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
media_viewer: {
auto_mute: 'unselected',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
media_viewer: {
auto_mute: ['unselected'],
},
});
postUpgradeChecks(config);
});
});
describe('auto_unmute', () => {
it('should handle all', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
media_viewer: {
auto_unmute: 'all',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
media_viewer: {
auto_unmute: ['selected', 'visible'],
},
});
postUpgradeChecks(config);
});
it('should handle never', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
media_viewer: {
auto_unmute: 'never',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
media_viewer: {},
});
postUpgradeChecks(config);
});
it('should handle another value', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
media_viewer: {
auto_unmute: 'selected',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
media_viewer: {
auto_unmute: ['selected'],
},
});
postUpgradeChecks(config);
});
});
});
});
describe('should rename thumbnails.media -> thumbnails.events_media_type', () => {
it.each([['all' as const], ['clips' as const], ['snapshots' as const]])(
'should handle %s',
(mediaEventType: string) => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
controls: {
thumbnails: {
media: mediaEventType,
},
},
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
...(mediaEventType !== 'all' && {
cameras_global: {
media: {
events_type: mediaEventType,
},
},
}),
live: {
controls: {
thumbnails: {
// v8.0.0+ migration moves events_media_type to cameras_global .
},
},
},
});
postUpgradeChecks(config);
},
);
});
describe('should rename timeline.media -> timeline.events_media_type', () => {
it.each([['all' as const], ['clips' as const], ['snapshots' as const]])(
'should handle %s',
(mediaEventType: string) => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
timeline: {
media: mediaEventType,
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
timeline: {
events_media_type: mediaEventType,
},
});
postUpgradeChecks(config);
},
);
});
describe('should rename live.controls.timeline.media -> live.controls.timeline.events_media_type', () => {
it.each([['all' as const], ['clips' as const], ['snapshots' as const]])(
'should handle %s',
(mediaEventType: string) => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
controls: {
timeline: {
media: mediaEventType,
},
},
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
controls: {
timeline: {
events_media_type: mediaEventType,
},
},
},
});
postUpgradeChecks(config);
},
);
});
describe('should rename media_viewer.controls.timeline.media -> media_viewer.controls.timeline.events_media_type', () => {
it.each([['all' as const], ['clips' as const], ['snapshots' as const]])(
'should handle %s',
(mediaEventType: string) => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
media_viewer: {
controls: {
timeline: {
media: mediaEventType,
},
},
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
media_viewer: {
controls: {
timeline: {
events_media_type: mediaEventType,
},
},
},
});
postUpgradeChecks(config);
},
);
});
describe('should transform scan mode', () => {
describe('should move and transform untrigger_reset', () => {
it('should handle when true', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
view: {
scan: {
untrigger_reset: true,
},
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
view: {
triggers: {
actions: {
untrigger: 'default',
},
},
},
});
postUpgradeChecks(config);
});
it('should handle when false', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
view: {
scan: {
untrigger_reset: false,
},
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
view: {
triggers: {},
},
});
postUpgradeChecks(config);
});
});
describe('should rename view.scan.enabled to a trigger action', () => {
it('should handle when true', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
view: {
scan: {
enabled: true,
},
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
view: {
triggers: {
filter_selected_camera: false,
actions: {
trigger: 'live',
},
},
},
});
postUpgradeChecks(config);
});
it('should handle when false', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
view: {
scan: {
enabled: false,
},
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
view: {
triggers: {},
},
});
postUpgradeChecks(config);
});
});
});
describe('should handle media layout changes', () => {
it('should move live.layout -> cameras_global.dimensions', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
layout: {
fit: 'cover',
position: {
x: 42,
y: 43,
},
},
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {},
cameras_global: {
dimensions: {
layout: {
fit: 'cover',
position: {
x: 42,
y: 43,
},
},
},
},
});
postUpgradeChecks(config);
});
describe('from delete old media layouts', () => {
it.each([['media_viewer' as const], ['image' as const]])(
'should handle %s',
(section: string) => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
[section]: {
layout: {
fit: 'cover',
position: {
x: 42,
y: 43,
},
},
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
[section]: {},
});
postUpgradeChecks(config);
},
);
});
});
describe('from condition object to condition array', () => {
describe('with view condition', () => {
it('should convert conditions on elements', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:random',
conditions: {
view: ['clips', 'snapshots'],
},
},
{
type: 'custom:random2',
conditions: 'not an object',
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:random',
conditions: [
{
condition: 'view' as const,
views: ['clips', 'snapshots'],
},
],
},
{
type: 'custom:random2',
conditions: 'not an object',
},
],
});
postUpgradeChecks(config);
});
it('should convert conditions on overrides', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
overrides: [
{
conditions: {
view: ['clips', 'snapshots'],
},
overrides: {
view: {
default: 'clips',
},
},
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
overrides: [
{
conditions: [
{
condition: 'view' as const,
views: ['clips', 'snapshots'],
},
],
merge: {
view: {
default: 'clips',
},
},
},
],
});
postUpgradeChecks(config);
});
it('should convert conditions on automations', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: {
view: ['clips', 'snapshots'],
},
actions: [
{
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'substream_on' as const,
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
triggers: [
{
trigger: 'view' as const,
views: ['clips', 'snapshots'],
},
],
actions: [
{
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'substream_on' as const,
},
],
},
],
});
postUpgradeChecks(config);
});
});
describe('with camera condition', () => {
it('should convert conditions on elements', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:random',
conditions: {
camera: ['camera_1', 'camera_2'],
},
},
{
type: 'custom:random2',
conditions: 'not an object',
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:random',
conditions: [
{
condition: 'camera' as const,
cameras: ['camera_1', 'camera_2'],
},
],
},
{
type: 'custom:random2',
conditions: 'not an object',
},
],
});
postUpgradeChecks(config);
});
it('should convert conditions on overrides', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
overrides: [
{
conditions: {
camera: ['camera_1', 'camera_2'],
},
overrides: {
view: {
default: 'clips',
},
},
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
overrides: [
{
conditions: [
{
condition: 'camera' as const,
cameras: ['camera_1', 'camera_2'],
},
],
merge: {
view: {
default: 'clips',
},
},
},
],
});
postUpgradeChecks(config);
});
it('should convert conditions on automations', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: {
camera: ['camera_1', 'camera_2'],
},
actions: [
{
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'substream_on' as const,
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
triggers: [
{
trigger: 'camera' as const,
cameras: ['camera_1', 'camera_2'],
},
],
actions: [
{
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'substream_on' as const,
},
],
},
],
});
postUpgradeChecks(config);
});
});
describe('with boolean conditions', () => {
describe.each([
['fullscreen' as const],
['expand' as const],
['media_loaded' as const],
])('%s', (condition: string) => {
it('should convert conditions on elements', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:random',
conditions: {
[condition]: true,
},
},
{
type: 'custom:random2',
conditions: 'not an object',
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:random',
conditions: [
{
condition: condition,
[condition]: true,
},
],
},
{
type: 'custom:random2',
conditions: 'not an object',
},
],
});
postUpgradeChecks(config);
});
it('should convert conditions on overrides', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
overrides: [
{
conditions: {
[condition]: true,
},
overrides: {
view: {
default: 'clips',
},
},
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
overrides: [
{
conditions: [
{
condition: condition,
[condition]: true,
},
],
merge: {
view: {
default: 'clips',
},
},
},
],
});
postUpgradeChecks(config);
});
it('should convert conditions on automations', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: {
[condition]: true,
},
actions: [
{
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'substream_on' as const,
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
triggers: [
{
trigger: condition,
[condition]: true,
},
],
actions: [
{
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'substream_on' as const,
},
],
},
],
});
postUpgradeChecks(config);
});
});
});
describe('with state condition', () => {
it('should convert conditions on elements', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:random',
conditions: {
state: [
{
entity: 'binary_sensor.first',
state: 'on',
},
{
entity: 'binary_sensor.second',
state_not: 'off',
},
{},
],
},
},
{
type: 'custom:random2',
conditions: 'not an object',
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:random',
conditions: [
{
condition: 'state' as const,
entity_id: 'binary_sensor.first',
state: 'on',
},
{
condition: 'state' as const,
entity_id: 'binary_sensor.second',
state_not: 'off',
},
],
},
{
type: 'custom:random2',
conditions: 'not an object',
},
],
});
postUpgradeChecks(config);
});
it('should convert conditions on overrides', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
overrides: [
{
conditions: {
state: [
{
entity: 'binary_sensor.first',
state: 'on',
},
{
entity: 'binary_sensor.second',
state_not: 'off',
},
{},
],
},
overrides: {
view: {
default: 'clips',
},
},
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
overrides: [
{
conditions: [
{
condition: 'state' as const,
entity_id: 'binary_sensor.first',
state: 'on',
},
{
condition: 'state' as const,
entity_id: 'binary_sensor.second',
state_not: 'off',
},
],
merge: {
view: {
default: 'clips',
},
},
},
],
});
postUpgradeChecks(config);
});
it('should convert conditions on automations', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: {
state: [
{
entity: 'binary_sensor.first',
state: 'on',
},
{
entity: 'binary_sensor.second',
state_not: 'off',
},
{},
],
},
actions: [
{
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'substream_on' as const,
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [
{
condition: 'state' as const,
entity_id: 'binary_sensor.first',
state: 'on',
},
{
condition: 'state' as const,
entity_id: 'binary_sensor.second',
state_not: 'off',
},
],
triggers: [
{
trigger: 'state' as const,
entity_id: 'binary_sensor.first',
to: 'on',
},
{
trigger: 'state' as const,
entity_id: 'binary_sensor.second',
not_to: 'off',
},
],
actions: [
{
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'substream_on' as const,
},
],
},
],
});
postUpgradeChecks(config);
});
});
describe('with media query condition', () => {
it('should convert conditions on elements', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:random',
conditions: {
media_query: 'query',
},
},
{
type: 'custom:random2',
conditions: 'not an object',
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:random',
conditions: [
{
condition: 'screen' as const,
media_query: 'query',
},
],
},
{
type: 'custom:random2',
conditions: 'not an object',
},
],
});
postUpgradeChecks(config);
});
it('should convert conditions on overrides', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
overrides: [
{
conditions: {
media_query: 'query',
},
overrides: {
view: {
default: 'clips',
},
},
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
overrides: [
{
conditions: [
{
condition: 'screen' as const,
media_query: 'query',
},
],
merge: {
view: {
default: 'clips',
},
},
},
],
});
postUpgradeChecks(config);
});
it('should convert conditions on automations', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: {
media_query: 'query',
},
actions: [
{
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'substream_on' as const,
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
triggers: [
{
trigger: 'screen' as const,
media_query: 'query',
},
],
actions: [
{
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'substream_on' as const,
},
],
},
],
});
postUpgradeChecks(config);
});
});
});
describe('from hide to substream capability disable', () => {
it('should disable substream capability from a hidden camera', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [
{ camera_entity: 'camera.office', hide: true },
{ camera_entity: 'camera.sitting_room', hide: false },
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [
{
camera_entity: 'camera.office',
capabilities: { disable_except: ['substream'] },
},
{ camera_entity: 'camera.sitting_room' },
],
});
postUpgradeChecks(config);
});
describe('from performance profile to generic profile', () => {
it('should migrate the low performance profile', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
performance: {
profile: 'low',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
profiles: ['low-performance'],
performance: {},
});
postUpgradeChecks(config);
});
it('should handle the high performance profile', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
performance: {
profile: 'high',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
performance: {},
});
postUpgradeChecks(config);
});
});
});
it('should move overrides -> merge', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
overrides: [
{
conditions: [
{
condition: 'view',
views: ['clips'],
},
],
overrides: {
menu: {
style: 'hidden',
},
},
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
overrides: [
{
conditions: [
{
condition: 'view',
views: ['clips'],
},
],
merge: {
menu: {
style: 'hidden',
},
},
},
],
});
postUpgradeChecks(config);
});
describe('split live.ptz', () => {
const getActionBefore = (action: PTZControlAction): CallServiceActionConfig => ({
action: 'call-service',
service: 'service',
data: {
arg: action,
},
});
const getActionAfter = (action: PTZControlAction): PerformActionActionConfig => ({
action: 'perform-action',
perform_action: 'service',
data: {
arg: action,
},
});
const getTapAction = (action: PTZControlAction): Actions => ({
tap_action: getActionBefore(action),
});
it('should transform the action_ format', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
controls: {
ptz: {
actions_up: getTapAction('up'),
actions_down: getTapAction('down'),
actions_left: getTapAction('left'),
actions_right: getTapAction('right'),
actions_zoom_in: getTapAction('zoom_in'),
actions_zoom_out: getTapAction('zoom_out'),
actions_home: {
...getTapAction('home'),
double_tap_action: getActionBefore('up'),
},
mode: 'auto' as const,
position: 'bottom-right' as const,
orientation: 'horizontal' as const,
hide_pan_tilt: false,
hide_zoom: false,
hide_home: false,
style: {
color: 'red',
},
something_not_related: 'very_unrelated',
},
},
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
cameras_global: {
ptz: {
actions_left: getActionAfter('left'),
actions_right: getActionAfter('right'),
actions_up: getActionAfter('up'),
actions_down: getActionAfter('down'),
actions_zoom_in: getActionAfter('zoom_in'),
actions_zoom_out: getActionAfter('zoom_out'),
presets: {
home: getActionAfter('home'),
},
},
},
live: {
controls: {
ptz: {
mode: 'auto' as const,
position: 'bottom-right' as const,
orientation: 'horizontal' as const,
hide_pan_tilt: false,
hide_zoom: false,
hide_home: false,
style: {
color: 'red',
},
},
},
},
});
postUpgradeChecks(config);
});
it('should transform the data_ format', () => {
const getDataAction = (action: PTZControlAction): Actions => ({
action: {
device: '048123',
cmd: action,
},
});
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
controls: {
ptz: {
service: 'service',
data_up: getDataAction('up'),
data_down: getDataAction('down'),
data_left: getDataAction('left'),
data_right: getDataAction('right'),
data_zoom_in: getDataAction('zoom_in'),
data_zoom_out: getDataAction('zoom_out'),
data_home: getDataAction('home'),
mode: 'auto' as const,
position: 'bottom-right' as const,
orientation: 'horizontal' as const,
hide_pan_tilt: false,
hide_zoom: false,
hide_home: false,
style: {
color: 'red',
},
something_not_related: 'very_unrelated',
},
},
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
cameras_global: {
ptz: {
service: 'service',
data_left: getDataAction('left'),
data_right: getDataAction('right'),
data_up: getDataAction('up'),
data_down: getDataAction('down'),
data_zoom_in: getDataAction('zoom_in'),
data_zoom_out: getDataAction('zoom_out'),
presets: {
service: 'service',
data_home: getDataAction('home'),
},
},
},
live: {
controls: {
ptz: {
mode: 'auto' as const,
position: 'bottom-right' as const,
orientation: 'horizontal' as const,
hide_pan_tilt: false,
hide_zoom: false,
hide_home: false,
style: {
color: 'red',
},
},
},
},
});
postUpgradeChecks(config);
});
it('should handle an invalid ptz type', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
controls: {
ptz: 3, // Not an object.
},
},
};
expect(upgradeConfig(config)).toBeFalsy();
});
it('should handle nothing to transform', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
controls: {
ptz: {
mode: 'auto',
},
},
},
};
expect(upgradeConfig(config)).toBeFalsy();
});
it('should handle a missing tap_action', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
controls: {
ptz: {
actions_up: {
double_tap_action: getActionBefore('up'),
},
actions_down: getTapAction('down'),
},
},
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual(
expect.objectContaining({
cameras_global: {
ptz: {
actions_down: getActionAfter('down'),
},
},
}),
);
postUpgradeChecks(config);
});
it('should handle no pre-existing presets', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
controls: {
ptz: {
presets: {
home: getActionBefore('left'),
other: getActionBefore('right'),
},
actions_home: getTapAction('home'),
},
},
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual(
expect.objectContaining({
cameras_global: {
ptz: {
presets: {
other: getActionAfter('right'),
home: getActionAfter('home'),
},
},
},
}),
);
postUpgradeChecks(config);
});
});
it('should rename view.update_cycle_camera -> view.default_cycle_camera', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
view: {
update_cycle_camera: true,
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.view).toEqual({
default_cycle_camera: true,
});
postUpgradeChecks(config);
});
describe('view.update_force -> view.default_reset.interaction_mode', () => {
it('should convert to all when true', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
view: {
update_force: true,
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.view).toEqual({
default_reset: {
interaction_mode: 'all',
},
});
postUpgradeChecks(config);
});
it('should remove when false', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
view: {
update_force: false,
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.view).toEqual({});
postUpgradeChecks(config);
});
});
it('should move view.update_seconds', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
view: {
update_seconds: 42,
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.view).toEqual({
default_reset: {
every_seconds: 42,
},
});
postUpgradeChecks(config);
});
it('should move view.update_entities', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
view: {
update_entities: ['binary_sensor.foo', 'camera.bar'],
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.view).toEqual({
default_reset: {
entities: ['binary_sensor.foo', 'camera.bar'],
},
});
postUpgradeChecks(config);
});
describe('title controls to status bar', () => {
it('should handle when mode is none', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
controls: {
title: {
mode: 'none',
},
},
},
media_viewer: {
controls: {
title: {
mode: 'none',
},
},
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: { controls: {} },
media_viewer: { controls: {} },
status_bar: {
style: 'none',
},
});
postUpgradeChecks(config);
});
describe('when mode is invalid type', () => {
it.each([[{ mode: { should_not_be: 'an object' } }], ['sideways']])(
'should handle %s',
(mode: unknown) => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
controls: {
title: {
mode,
},
},
},
media_viewer: {
controls: {
title: {
mode,
},
},
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: { controls: {} },
media_viewer: { controls: {} },
});
postUpgradeChecks(config);
},
);
});
describe.each([['bottom'], ['top']])('on the %s', (position: string) => {
it.each([
[`popup-${position}-left` as const],
[`popup-${position}-right` as const],
])('should handle %s', (mode: string) => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
controls: {
title: {
mode,
},
},
},
media_viewer: {
controls: {
title: {
mode,
},
},
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: { controls: {} },
media_viewer: { controls: {} },
status_bar: {
position,
},
});
postUpgradeChecks(config);
});
});
});
it('should rename call-service -> perform-action', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'icon',
icon: 'mdi:cow',
tap_action: {
action: 'call-service',
service: 'foo',
},
},
],
view: {
actions: {
double_tap_action: {
action: 'call-service',
service: 'bar',
data: {
key: 'value',
},
},
},
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'icon',
icon: 'mdi:cow',
tap_action: {
action: 'perform-action',
perform_action: 'foo',
},
},
],
view: {
actions: {
double_tap_action: {
action: 'perform-action',
perform_action: 'bar',
data: {
key: 'value',
},
},
},
},
});
postUpgradeChecks(config);
});
it('should rename dimensions.max_height -> dimensions.height', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
dimensions: {
max_height: '500px',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
dimensions: {
height: '500px',
},
});
postUpgradeChecks(config);
});
it('should delete dimensions.min_height', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
dimensions: {
min_height: '100px',
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
dimensions: {},
});
postUpgradeChecks(config);
});
});
describe('v6.1.2+', () => {
describe('view.dark_mode -> view.dim', () => {
it.each([
['on' as const, true],
['auto' as const, false],
['off' as const, false],
])('should handle %s', (darkMode: 'on' | 'off' | 'auto', expected: boolean) => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
view: {
dark_mode: darkMode,
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
view: {
dim: expected,
},
});
postUpgradeChecks(config);
});
});
});
describe('v7.0.0+', () => {
it('should rename custom:frigate-card -> custom:advanced-camera-card', () => {
const config = {
type: 'custom:frigate-card',
cameras: [{}],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
});
postUpgradeChecks(config);
});
it('should rename custom:frigate-card-action -> custom:advanced-camera-card-action', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'icon',
icon: 'mdi:cow',
tap_action: {
action: 'custom:frigate-card-action',
advanced_camera_card_action: 'camera_ui',
},
},
],
view: {
actions: {
double_tap_action: {
action: 'custom:frigate-card-action',
advanced_camera_card_action: 'camera_ui',
},
},
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'icon',
icon: 'mdi:cow',
tap_action: {
action: 'custom:advanced-camera-card-action',
advanced_camera_card_action: 'camera_ui',
},
},
],
view: {
actions: {
double_tap_action: {
action: 'custom:advanced-camera-card-action',
advanced_camera_card_action: 'camera_ui',
},
},
},
});
postUpgradeChecks(config);
});
it('should rename custom:frigate-card-menu-icon -> custom:advanced-camera-card-menu-icon', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:frigate-card-menu-icon',
icon: 'mdi:cow',
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:advanced-camera-card-menu-icon',
icon: 'mdi:cow',
},
],
});
postUpgradeChecks(config);
});
it('should rename custom:frigate-card-menu-state-icon -> custom:advanced-camera-card-menu-state-icon', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:frigate-card-menu-state-icon',
entity: 'binary_sensor.office',
icon: 'mdi:cow',
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:advanced-camera-card-menu-state-icon',
entity: 'binary_sensor.office',
icon: 'mdi:cow',
},
],
});
postUpgradeChecks(config);
});
it('should rename custom:frigate-card-menu-submenu -> custom:advanced-camera-card-menu-submenu', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:frigate-card-menu-submenu',
icon: 'mdi:cow',
items: [],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:advanced-camera-card-menu-submenu',
icon: 'mdi:cow',
items: [],
},
],
});
postUpgradeChecks(config);
});
it('should rename custom:frigate-card-status-bar-icon -> custom:advanced-camera-card-status-bar-icon', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:frigate-card-status-bar-icon',
icon: 'mdi:cow',
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:advanced-camera-card-status-bar-icon',
icon: 'mdi:cow',
},
],
});
postUpgradeChecks(config);
});
it('should rename custom:frigate-card-status-bar-image -> custom:advanced-camera-card-status-bar-image', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:frigate-card-status-bar-image',
image: 'image',
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:advanced-camera-card-status-bar-image',
image: 'image',
},
],
});
postUpgradeChecks(config);
});
it('should rename custom:frigate-card-status-bar-string -> custom:advanced-camera-card-status-bar-string', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:frigate-card-status-bar-string',
string: 'string',
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:advanced-camera-card-status-bar-string',
string: 'string',
},
],
});
postUpgradeChecks(config);
});
it('should rename custom:frigate-card-conditional -> custom:advanced-camera-card-conditional', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:frigate-card-conditional',
conditions: [],
elements: [],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:advanced-camera-card-conditional',
conditions: [],
elements: [],
},
],
});
postUpgradeChecks(config);
});
it('should rename frigate_card_action -> advanced_camera_card_action', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'icon',
icon: 'mdi:cow',
tap_action: {
action: 'custom:advanced-camera-card-action',
frigate_card_action: 'camera_ui',
},
},
],
view: {
actions: {
double_tap_action: {
action: 'custom:advanced-camera-card-action',
frigate_card_action: 'camera_ui',
},
},
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'icon',
icon: 'mdi:cow',
tap_action: {
action: 'custom:advanced-camera-card-action',
advanced_camera_card_action: 'camera_ui',
},
},
],
view: {
actions: {
double_tap_action: {
action: 'custom:advanced-camera-card-action',
advanced_camera_card_action: 'camera_ui',
},
},
},
});
postUpgradeChecks(config);
});
describe('frigate card style overrides -> advanced camera card style overrides', () => {
it('should rename valid style overrides', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
view: {
theme: {
overrides: {
'--zero': 'zero',
'--frigate-card-one': 'one',
},
},
},
overrides: [
{
conditions: [
{
condition: 'media_loaded' as const,
media_loaded: true,
},
],
merge: {
view: {
theme: {
overrides: {
'--frigate-card-two': 'two',
},
},
},
},
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
view: {
theme: {
overrides: {
'--zero': 'zero',
'--advanced-camera-card-one': 'one',
},
},
},
overrides: [
{
conditions: [
{
condition: 'media_loaded' as const,
media_loaded: true,
},
],
merge: {
view: {
theme: {
overrides: {
'--advanced-camera-card-two': 'two',
},
},
},
},
},
],
});
postUpgradeChecks(config);
});
it('should handle invalid style overrides', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
view: {
theme: {
overrides: ['should', 'not', 'be', 'an', 'array'],
},
},
};
expect(upgradeConfig(config)).toBeFalsy();
});
});
it('should rename the frigate card button -> the iris button', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
menu: {
buttons: {
frigate: {
icon: 'mdi:cow',
},
},
},
overrides: [
{
conditions: [
{
condition: 'media_loaded' as const,
media_loaded: true,
},
],
merge: {
menu: {
buttons: {
frigate: {
icon: 'mdi:cow',
},
},
},
},
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
menu: {
buttons: {
iris: {
icon: 'mdi:cow',
},
},
},
overrides: [
{
conditions: [
{
condition: 'media_loaded' as const,
media_loaded: true,
},
],
merge: {
menu: {
buttons: {
iris: {
icon: 'mdi:cow',
},
},
},
},
},
],
});
postUpgradeChecks(config);
});
});
describe('v8.0.0+', () => {
describe('should remove the retired microphone options', () => {
it('should delete disconnect_seconds', () => {
const config = {
type: 'custom:advanced-camera-card' as const,
cameras: [{}],
live: { microphone: { disconnect_seconds: 120, always_connected: true } },
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: { microphone: { always_connected: true } },
});
postUpgradeChecks(config);
});
it('should delete disconnect_seconds in overrides', () => {
const config = {
type: 'custom:advanced-camera-card' as const,
cameras: [{}],
overrides: [
{
conditions: [{ condition: 'fullscreen' as const, fullscreen: true }],
merge: { live: { microphone: { disconnect_seconds: 120 } } },
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.overrides[0].merge).toEqual({ live: { microphone: {} } });
postUpgradeChecks(config);
});
it('should strip call from the microphone auto_mute', () => {
const config = {
type: 'custom:advanced-camera-card' as const,
cameras: [{}],
live: { microphone: { auto_mute: ['hidden', 'call'] } },
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: { microphone: { auto_mute: ['hidden'] } },
});
postUpgradeChecks(config);
});
it('should empty the microphone auto_mute when only call is listed', () => {
const config = {
type: 'custom:advanced-camera-card' as const,
cameras: [{}],
live: { microphone: { auto_mute: ['call'], always_connected: true } },
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: { microphone: { auto_mute: [], always_connected: true } },
});
postUpgradeChecks(config);
});
it('should empty an overridden microphone auto_mute rather than delete it', () => {
const config = {
type: 'custom:advanced-camera-card' as const,
cameras: [{}],
live: { microphone: { auto_mute: ['hidden' as const] } },
overrides: [
{
conditions: [{ condition: 'fullscreen' as const, fullscreen: true }],
set: { 'live.microphone.auto_mute': ['call'] },
},
{
conditions: [{ condition: 'fullscreen' as const, fullscreen: false }],
merge: { live: { microphone: { auto_mute: ['call'] } } },
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
// Deleting the key would restore the base `['hidden']` for anyone the
// override applies to, rather than leaving them with nothing.
expect(config.overrides[0].set).toEqual({
live: { microphone: { auto_mute: [] } },
});
expect(config.overrides[1].merge).toEqual({
live: { microphone: { auto_mute: [] } },
});
postUpgradeChecks(config);
});
it('should not touch a microphone auto_mute without call or unselected', () => {
const config = {
type: 'custom:advanced-camera-card' as const,
cameras: [{}],
live: { microphone: { auto_mute: ['hidden'] } },
};
expect(upgradeConfig(config)).toBeFalsy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: { microphone: { auto_mute: ['hidden'] } },
});
});
it('should strip unselected from the microphone auto_mute', () => {
const config = {
type: 'custom:advanced-camera-card' as const,
cameras: [{}],
live: { microphone: { auto_mute: ['unselected', 'hidden', 'call'] } },
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: { microphone: { auto_mute: ['hidden'] } },
});
postUpgradeChecks(config);
});
it('should strip selected from the microphone auto_unmute', () => {
const config = {
type: 'custom:advanced-camera-card' as const,
cameras: [{}],
live: { microphone: { auto_unmute: ['selected', 'visible', 'call'] } },
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: { microphone: { auto_unmute: ['visible', 'call'] } },
});
postUpgradeChecks(config);
});
it('should empty an overridden microphone auto_unmute rather than delete it', () => {
const config = {
type: 'custom:advanced-camera-card' as const,
cameras: [{}],
live: { microphone: { auto_unmute: ['call' as const] } },
overrides: [
{
conditions: [{ condition: 'fullscreen' as const, fullscreen: true }],
set: { 'live.microphone.auto_unmute': ['selected'] },
},
{
conditions: [{ condition: 'fullscreen' as const, fullscreen: false }],
merge: { live: { microphone: { auto_unmute: ['selected'] } } },
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
// Deleting the key would restore the base `['call']` for anyone the
// override applies to, rather than leaving them with nothing.
expect(config.overrides[0].set).toEqual({
live: { microphone: { auto_unmute: [] } },
});
expect(config.overrides[1].merge).toEqual({
live: { microphone: { auto_unmute: [] } },
});
postUpgradeChecks(config);
});
it('should not touch a microphone auto_unmute without selected', () => {
const config = {
type: 'custom:advanced-camera-card' as const,
cameras: [{}],
live: { microphone: { auto_unmute: ['visible', 'call'] } },
};
expect(upgradeConfig(config)).toBeFalsy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: { microphone: { auto_unmute: ['visible', 'call'] } },
});
});
it('should not strip selected from the live auto_unmute', () => {
const config = {
type: 'custom:advanced-camera-card' as const,
cameras: [{}],
live: { auto_unmute: ['selected', 'visible'] },
};
expect(upgradeConfig(config)).toBeFalsy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: { auto_unmute: ['selected', 'visible'] },
});
});
it('should not strip call from the live auto_mute', () => {
const config = {
type: 'custom:advanced-camera-card' as const,
cameras: [{}],
live: { auto_mute: ['hidden', 'call'] },
};
expect(upgradeConfig(config)).toBeFalsy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: { auto_mute: ['hidden', 'call'] },
});
});
});
describe('should remove the retired microphone actions', () => {
it('should delete a singular tap_action', () => {
const config = {
type: 'custom:advanced-camera-card' as const,
cameras: [{}],
elements: [
{
type: 'custom:advanced-camera-card-menu-icon' as const,
icon: 'mdi:microphone',
tap_action: {
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'microphone_connect' as const,
},
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.elements[0]).toEqual({
type: 'custom:advanced-camera-card-menu-icon',
icon: 'mdi:microphone',
});
postUpgradeChecks(config);
});
it('should empty a required actions property rather than delete it', () => {
const config = {
type: 'custom:advanced-camera-card' as const,
cameras: [{}],
automations: [
{
triggers: [{ trigger: 'initialized' as const }],
actions: {
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'microphone_connect' as const,
},
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
triggers: [{ trigger: 'initialized' }],
actions: [],
});
postUpgradeChecks(config);
});
it('should splice an action array and keep the order of the rest', () => {
const config = {
type: 'custom:advanced-camera-card' as const,
cameras: [{}],
elements: [
{
type: 'custom:advanced-camera-card-menu-icon' as const,
icon: 'mdi:microphone',
tap_action: [
{
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'microphone_connect' as const,
},
{
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'microphone_unmute' as const,
},
{
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'microphone_disconnect' as const,
},
{
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'microphone_mute' as const,
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.elements[0]['tap_action']).toEqual([
{
action: 'custom:advanced-camera-card-action',
advanced_camera_card_action: 'microphone_unmute',
},
{
action: 'custom:advanced-camera-card-action',
advanced_camera_card_action: 'microphone_mute',
},
]);
postUpgradeChecks(config);
});
it('should delete the start and end tap actions of a momentary button', () => {
const config = {
type: 'custom:advanced-camera-card' as const,
cameras: [{}],
elements: [
{
type: 'custom:advanced-camera-card-menu-icon' as const,
icon: 'mdi:microphone',
start_tap_action: {
action: 'fire-dom-event' as const,
advanced_camera_card_action: 'microphone_connect' as const,
},
end_tap_action: {
action: 'fire-dom-event' as const,
advanced_camera_card_action: 'microphone_disconnect' as const,
},
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.elements[0]).toEqual({
type: 'custom:advanced-camera-card-menu-icon',
icon: 'mdi:microphone',
});
postUpgradeChecks(config);
});
it('should remove the actions from an override', () => {
const config = {
type: 'custom:advanced-camera-card' as const,
cameras: [{}],
overrides: [
{
conditions: [{ condition: 'fullscreen' as const, fullscreen: true }],
merge: {
'menu.buttons.microphone': {
tap_action: {
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'microphone_connect' as const,
},
},
},
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.overrides[0].merge).toEqual({
'menu.buttons.microphone': {},
});
postUpgradeChecks(config);
});
it('should keep an automation that retains other actions', () => {
const config = {
type: 'custom:advanced-camera-card' as const,
cameras: [{}],
automations: [
{
conditions: [{ condition: 'fullscreen' as const, fullscreen: true }],
actions: [
{
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'microphone_connect' as const,
},
{
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'microphone_unmute' as const,
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]['actions']).toEqual([
{
action: 'custom:advanced-camera-card-action',
advanced_camera_card_action: 'microphone_unmute',
},
]);
postUpgradeChecks(config);
});
it('should empty the actions of an automation left with none', () => {
const config = {
type: 'custom:advanced-camera-card' as const,
cameras: [{}],
automations: [
{
conditions: [{ condition: 'fullscreen' as const, fullscreen: true }],
actions: [
{
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'microphone_connect' as const,
},
],
},
{
conditions: [{ condition: 'fullscreen' as const, fullscreen: false }],
actions: [
{
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'microphone_mute' as const,
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations).toHaveLength(2);
expect(config.automations[0]['actions']).toEqual([]);
expect(config.automations[1]['actions']).toEqual([
{
action: 'custom:advanced-camera-card-action',
advanced_camera_card_action: 'microphone_mute',
},
]);
postUpgradeChecks(config);
});
it('should keep the required then of an if action that empties', () => {
const config = {
type: 'custom:advanced-camera-card' as const,
cameras: [{}],
automations: [
{
triggers: [{ trigger: 'camera' as const }],
actions: [
{
if: [{ condition: 'fullscreen' as const, fullscreen: true }],
then: [
{
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'microphone_disconnect' as const,
},
],
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
// `then` is required by the schema, so it must survive as an empty
// array rather than be deleted.
expect(config.automations[0]['actions']).toEqual([
{ if: [{ condition: 'fullscreen', fullscreen: true }], then: [] },
]);
postUpgradeChecks(config);
});
it('should leave an unrelated action payload alone', () => {
const config = {
type: 'custom:advanced-camera-card' as const,
cameras: [{}],
elements: [
{
type: 'custom:advanced-camera-card-menu-icon' as const,
icon: 'mdi:microphone',
tap_action: {
action: 'perform-action' as const,
perform_action: 'input_boolean.turn_on',
data: {
advanced_camera_card_action: 'microphone_connect',
},
},
},
],
};
expect(upgradeConfig(config)).toBeFalsy();
expect(config.elements[0]['tap_action']).toEqual({
action: 'perform-action',
perform_action: 'input_boolean.turn_on',
data: {
advanced_camera_card_action: 'microphone_connect',
},
});
});
it('should leave an action-shaped service data payload alone', () => {
const config = {
type: 'custom:advanced-camera-card' as const,
cameras: [{}],
elements: [
{
type: 'custom:advanced-camera-card-menu-icon' as const,
icon: 'mdi:microphone',
tap_action: {
action: 'perform-action' as const,
perform_action: 'script.relay',
data: {
payload: {
action: 'fire-dom-event',
advanced_camera_card_action: 'microphone_connect',
},
},
},
},
],
};
expect(upgradeConfig(config)).toBeFalsy();
// `data` is opaque to the card: whatever it holds is the user's to
// send on, even when it looks like an action.
expect(config.elements[0]['tap_action']['data']).toEqual({
payload: {
action: 'fire-dom-event',
advanced_camera_card_action: 'microphone_connect',
},
});
});
it('should leave the surviving microphone actions alone', () => {
const config = {
type: 'custom:advanced-camera-card' as const,
cameras: [{}],
elements: [
{
type: 'custom:advanced-camera-card-menu-icon' as const,
icon: 'mdi:microphone',
tap_action: {
action: 'custom:advanced-camera-card-action' as const,
advanced_camera_card_action: 'microphone_mute' as const,
},
},
],
};
expect(upgradeConfig(config)).toBeFalsy();
});
});
describe('live.controls.thumbnails.media_type -> cameras_global.media.type', () => {
it.each([['events' as const], ['recordings' as const]])(
'should handle %s',
(mediaType: string) => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
controls: {
thumbnails: {
media_type: mediaType,
},
},
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
controls: {
thumbnails: {},
},
},
cameras_global: {
media: {
type: mediaType,
},
},
});
postUpgradeChecks(config);
},
);
});
describe('live.controls.thumbnails.events_media_type -> cameras_global.media.events_type', () => {
it.each([['clips' as const], ['snapshots' as const]])(
'should handle %s',
(eventsType: string) => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
controls: {
thumbnails: {
events_media_type: eventsType,
},
},
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
controls: {
thumbnails: {},
},
},
cameras_global: {
media: {
events_type: eventsType,
},
},
});
postUpgradeChecks(config);
},
);
it('should handle all', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
controls: {
thumbnails: {
events_media_type: 'all',
},
},
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
live: {
controls: {
thumbnails: {},
},
},
});
postUpgradeChecks(config);
});
});
it('should rename view.triggers.untrigger_seconds -> view.triggers.untrigger_delay_seconds', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
view: {
triggers: {
untrigger_seconds: 42,
},
},
overrides: [
{
conditions: [
{
condition: 'media_loaded' as const,
media_loaded: true,
},
],
merge: {
view: {
triggers: {
untrigger_seconds: 7,
},
},
},
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
type: 'custom:advanced-camera-card',
cameras: [{}],
view: {
triggers: {
untrigger_delay_seconds: 42,
},
},
overrides: [
{
conditions: [
{
condition: 'media_loaded' as const,
media_loaded: true,
},
],
merge: {
view: {
triggers: {
untrigger_delay_seconds: 7,
},
},
},
},
],
});
postUpgradeChecks(config);
});
describe('ptz data_*_start/stop -> data_start/end_* (WebRTC ordering)', () => {
it('should transform in cameras_global.ptz', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
cameras_global: {
ptz: {
service: 'service.ptz',
data_left_start: { cmd: 'left_start' },
data_left_stop: { cmd: 'left_stop' },
},
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.cameras_global.ptz).toEqual({
service: 'service.ptz',
data_start_left: { cmd: 'left_start' },
data_end_left: { cmd: 'left_stop' },
});
postUpgradeChecks(config);
});
it('should transform in cameras[n].ptz', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [
{
ptz: {
service: 'service.ptz',
data_right_start: { cmd: 'right_start' },
data_right_stop: { cmd: 'right_stop' },
},
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.cameras[0].ptz).toEqual({
service: 'service.ptz',
data_start_right: { cmd: 'right_start' },
data_end_right: { cmd: 'right_stop' },
});
postUpgradeChecks(config);
});
it('should ignore a non-object ptz value', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
cameras_global: {
ptz: 'not-an-object',
},
};
expect(upgradeConfig(config)).toBeFalsy();
});
it('should not overwrite an existing WebRTC key', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{}],
cameras_global: {
ptz: {
service: 'service.ptz',
data_left_stop: { cmd: 'old' },
data_end_left: { cmd: 'new' },
},
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.cameras_global.ptz).toEqual({
service: 'service.ptz',
data_end_left: { cmd: 'new' },
});
postUpgradeChecks(config);
});
});
describe('automation conditions -> triggers', () => {
it('should promote a state condition to a trigger and keep its other fields', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [
{
condition: 'state',
entity_id: 'binary_sensor.motion',
state: 'on',
attribute: 'friendly_name',
for: '00:01:00',
enabled: false,
},
],
actions: [
{ action: 'fire-dom-event', advanced_camera_card_action: 'live' },
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual(
expect.objectContaining({
triggers: [
{
trigger: 'state',
entity_id: 'binary_sensor.motion',
to: 'on',
attribute: 'friendly_name',
for: '00:01:00',
enabled: false,
},
],
}),
);
postUpgradeChecks(config);
});
it('should promote a call condition to a trigger arriving at its phases', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [
{
condition: 'call',
call: ['ringing', 'answered'],
enabled: false,
},
],
actions: [
{ action: 'fire-dom-event', advanced_camera_card_action: 'live' },
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual(
expect.objectContaining({
triggers: [
{
trigger: 'call',
to: ['ringing', 'answered'],
enabled: false,
},
],
}),
);
postUpgradeChecks(config);
});
it('should flatten a composite condition into trigger leaves and keep the composite', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [
{
condition: 'or',
conditions: [
{ condition: 'camera', cameras: ['front'] },
{ condition: 'view', views: ['live'] },
],
},
],
actions: [
{ action: 'fire-dom-event', advanced_camera_card_action: 'live' },
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
conditions: [
{
condition: 'or',
conditions: [
{ condition: 'camera', cameras: ['front'] },
{ condition: 'view', views: ['live'] },
],
},
],
triggers: [
{ trigger: 'camera', cameras: ['front'] },
{ trigger: 'view', views: ['live'] },
],
actions: [{ action: 'fire-dom-event', advanced_camera_card_action: 'live' }],
});
postUpgradeChecks(config);
});
it('should leave non-object conditions untouched while rewriting the rest', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [
'not-a-condition',
{ condition: 'camera', cameras: ['front'] },
],
actions: [
{ action: 'fire-dom-event', advanced_camera_card_action: 'live' },
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
conditions: ['not-a-condition', { condition: 'camera', cameras: ['front'] }],
triggers: ['not-a-condition', { trigger: 'camera', cameras: ['front'] }],
actions: [{ action: 'fire-dom-event', advanced_camera_card_action: 'live' }],
});
});
it('should leave an automation with no conditions untouched', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
actions: [
{ action: 'fire-dom-event', advanced_camera_card_action: 'live' },
],
},
],
};
upgradeConfig(config);
expect(config.automations[0]).toEqual({
actions: [{ action: 'fire-dom-event', advanced_camera_card_action: 'live' }],
});
});
it('should map a discriminator-less state condition to a state trigger', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [{ entity: 'binary_sensor.x', state: 'on' }],
actions: [
{ action: 'fire-dom-event', advanced_camera_card_action: 'live' },
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
triggers: [{ trigger: 'state', entity_id: 'binary_sensor.x', to: 'on' }],
actions: [{ action: 'fire-dom-event', advanced_camera_card_action: 'live' }],
});
postUpgradeChecks(config);
});
it('should drop a composite with no nested conditions from the trigger list', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [
{ condition: 'or' },
{ condition: 'camera', cameras: ['front'] },
],
actions: [
{ action: 'fire-dom-event', advanced_camera_card_action: 'live' },
],
},
],
};
upgradeConfig(config);
expect(config.automations[0]).toEqual({
conditions: [{ condition: 'or' }, { condition: 'camera', cameras: ['front'] }],
triggers: [{ trigger: 'camera', cameras: ['front'] }],
actions: [{ action: 'fire-dom-event', advanced_camera_card_action: 'live' }],
});
});
it('should promote stock numeric_state and template by swapping the discriminator', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [
{ condition: 'numeric_state', entity_id: 'sensor.t', above: 25 },
{ condition: 'template', value_template: '{{ true }}' },
],
actions: [
{ action: 'fire-dom-event', advanced_camera_card_action: 'live' },
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
conditions: [
{ condition: 'numeric_state', entity_id: 'sensor.t', above: 25 },
{ condition: 'template', value_template: '{{ true }}' },
],
triggers: [
{ trigger: 'numeric_state', entity_id: 'sensor.t', above: 25 },
{ trigger: 'template', value_template: '{{ true }}' },
],
actions: [{ action: 'fire-dom-event', advanced_camera_card_action: 'live' }],
});
postUpgradeChecks(config);
});
it('should drop a trigger-only condition from the retained conditions', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [
{ condition: 'camera' },
{ condition: 'state', entity_id: 'binary_sensor.door', state: 'on' },
],
actions: [
{ action: 'fire-dom-event', advanced_camera_card_action: 'live' },
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
conditions: [
{ condition: 'state', entity_id: 'binary_sensor.door', state: 'on' },
],
triggers: [
{ trigger: 'camera' },
{ trigger: 'state', entity_id: 'binary_sensor.door', to: 'on' },
],
actions: [{ action: 'fire-dom-event', advanced_camera_card_action: 'live' }],
});
postUpgradeChecks(config);
});
it('should drop the conditions block when all are trigger-only', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [{ condition: 'camera' }, { condition: 'view' }],
actions: [
{ action: 'fire-dom-event', advanced_camera_card_action: 'live' },
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
triggers: [{ trigger: 'camera' }, { trigger: 'view' }],
actions: [{ action: 'fire-dom-event', advanced_camera_card_action: 'live' }],
});
postUpgradeChecks(config);
});
it('should prune trigger-only leaves from a retained composite', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [
{
condition: 'or',
conditions: [
{ condition: 'camera' },
{ condition: 'view', views: ['live'] },
],
},
],
actions: [
{ action: 'fire-dom-event', advanced_camera_card_action: 'live' },
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
conditions: [
{ condition: 'or', conditions: [{ condition: 'view', views: ['live'] }] },
],
triggers: [{ trigger: 'camera' }, { trigger: 'view', views: ['live'] }],
actions: [{ action: 'fire-dom-event', advanced_camera_card_action: 'live' }],
});
postUpgradeChecks(config);
});
it('should drop a composite whose leaves are all trigger-only', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [
{ condition: 'or', conditions: [{ condition: 'camera' }] },
{ condition: 'state', entity_id: 'binary_sensor.door', state: 'on' },
],
actions: [
{ action: 'fire-dom-event', advanced_camera_card_action: 'live' },
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
conditions: [
{ condition: 'state', entity_id: 'binary_sensor.door', state: 'on' },
],
triggers: [
{ trigger: 'camera' },
{ trigger: 'state', entity_id: 'binary_sensor.door', to: 'on' },
],
actions: [{ action: 'fire-dom-event', advanced_camera_card_action: 'live' }],
});
postUpgradeChecks(config);
});
it('should promote a config condition to a config trigger', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [
{ condition: 'config', paths: ['menu.style'] },
{ condition: 'state', entity_id: 'binary_sensor.door', state: 'on' },
],
actions: [
{ action: 'fire-dom-event', advanced_camera_card_action: 'live' },
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
conditions: [
{ condition: 'state', entity_id: 'binary_sensor.door', state: 'on' },
],
triggers: [
{ trigger: 'config', paths: ['menu.style'] },
{ trigger: 'state', entity_id: 'binary_sensor.door', to: 'on' },
],
actions: [{ action: 'fire-dom-event', advanced_camera_card_action: 'live' }],
});
postUpgradeChecks(config);
});
it('should retain a state condition holding both state and state_not', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [
{
condition: 'state',
entity: 'binary_sensor.door',
state: 'on',
state_not: 'unavailable',
},
],
actions: [
{ action: 'fire-dom-event', advanced_camera_card_action: 'live' },
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
conditions: [
{
condition: 'state',
entity: 'binary_sensor.door',
state: 'on',
state_not: 'unavailable',
},
],
triggers: [{ trigger: 'state', entity_id: 'binary_sensor.door' }],
actions: [{ action: 'fire-dom-event', advanced_camera_card_action: 'live' }],
});
postUpgradeChecks(config);
});
it('should retain a user condition rather than promote it to a trigger', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [
{ condition: 'user', users: ['abc'] },
{ condition: 'fullscreen', fullscreen: true },
],
actions: [
{ action: 'fire-dom-event', advanced_camera_card_action: 'live' },
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
conditions: [
{ condition: 'user', users: ['abc'] },
{ condition: 'fullscreen', fullscreen: true },
],
triggers: [{ trigger: 'fullscreen', fullscreen: true }],
actions: [{ action: 'fire-dom-event', advanced_camera_card_action: 'live' }],
});
postUpgradeChecks(config);
});
it('should retain a lone user condition and trigger on initialization', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [{ condition: 'user', users: ['abc'] }],
actions: [
{ action: 'fire-dom-event', advanced_camera_card_action: 'live' },
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
conditions: [{ condition: 'user', users: ['abc'] }],
triggers: [{ trigger: 'initialized' }],
actions: [{ action: 'fire-dom-event', advanced_camera_card_action: 'live' }],
});
postUpgradeChecks(config);
});
it('should promote a malformed lone condition for the schema to reject', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: ['nonsense'],
actions: [
{ action: 'fire-dom-event', advanced_camera_card_action: 'live' },
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
triggers: ['nonsense'],
actions: [{ action: 'fire-dom-event', advanced_camera_card_action: 'live' }],
});
});
it('should trigger on initialization when no condition yields a trigger', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [
{ condition: 'user', users: ['abc'] },
{ condition: 'user_agent', user_agent: 'Chrome' },
],
actions: [
{ action: 'fire-dom-event', advanced_camera_card_action: 'live' },
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
conditions: [
{ condition: 'user', users: ['abc'] },
{ condition: 'user_agent', user_agent: 'Chrome' },
],
triggers: [{ trigger: 'initialized' }],
actions: [{ action: 'fire-dom-event', advanced_camera_card_action: 'live' }],
});
postUpgradeChecks(config);
});
});
describe("automation singular keys and Home Assistant's single-item forms", () => {
const live = { action: 'fire-dom-event', advanced_camera_card_action: 'live' };
const automate = (
automation: RawAdvancedCameraCardConfig,
): RawAdvancedCameraCardConfig & {
automations: RawAdvancedCameraCardConfig[];
} => ({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [automation],
});
it('should leave an automation using the singular trigger key untouched', () => {
const config = automate({
trigger: [{ trigger: 'initialized' }],
conditions: [{ condition: 'view', views: ['live'] }],
actions: [live],
});
expect(upgradeConfig(config)).toBeFalsy();
expect(config.automations[0]).toEqual({
trigger: [{ trigger: 'initialized' }],
conditions: [{ condition: 'view', views: ['live'] }],
actions: [live],
});
postUpgradeChecks(config);
});
it('should promote a singular condition key and retain the ongoing conditions under it', () => {
const config = automate({
condition: [{ condition: 'camera' }, { condition: 'view', views: ['live'] }],
actions: [live],
});
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
condition: [{ condition: 'view', views: ['live'] }],
triggers: [{ trigger: 'camera' }, { trigger: 'view', views: ['live'] }],
actions: [live],
});
postUpgradeChecks(config);
});
it('should promote a single condition standing in for the conditions list', () => {
const config = automate({
conditions: { condition: 'view', views: ['live'] },
actions: [live],
});
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
triggers: [{ trigger: 'view', views: ['live'] }],
actions: [live],
});
postUpgradeChecks(config);
});
it('should convert actions_not when the conditions are a single condition', () => {
const config = automate({
conditions: {
condition: 'state',
entity_id: 'binary_sensor.door',
state: 'on',
},
actions: [live],
actions_not: [
{ action: 'fire-dom-event', advanced_camera_card_action: 'clips' },
],
});
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
triggers: [{ trigger: 'state', entity_id: 'binary_sensor.door' }],
actions: [
{
if: [{ condition: 'state', entity_id: 'binary_sensor.door', state: 'on' }],
then: [live],
else: [{ action: 'fire-dom-event', advanced_camera_card_action: 'clips' }],
},
],
});
postUpgradeChecks(config);
});
it('should convert actions_not under the singular action key', () => {
const config = automate({
conditions: [
{ condition: 'state', entity_id: 'binary_sensor.door', state: 'on' },
],
action: [live],
actions_not: [
{ action: 'fire-dom-event', advanced_camera_card_action: 'clips' },
],
});
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
triggers: [{ trigger: 'state', entity_id: 'binary_sensor.door' }],
action: [
{
if: [{ condition: 'state', entity_id: 'binary_sensor.door', state: 'on' }],
then: [live],
else: [{ action: 'fire-dom-event', advanced_camera_card_action: 'clips' }],
},
],
});
postUpgradeChecks(config);
});
});
describe('automation actions_not -> if/then/else', () => {
const automate = (
...automations: RawAdvancedCameraCardConfig[]
): RawAdvancedCameraCardConfig & {
automations: RawAdvancedCameraCardConfig[];
__UPGRADE_FAILURE__?: RawAdvancedCameraCardConfig;
} => ({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations,
});
const then = [{ action: 'fire-dom-event', advanced_camera_card_action: 'live' }];
const elseActions = [
{ action: 'fire-dom-event', advanced_camera_card_action: 'clips' },
];
it('should migrate a self-gating state toggle to an if/then/else action', () => {
const config = automate({
conditions: [
{ condition: 'state', entity_id: 'binary_sensor.x', state: 'on' },
],
actions: then,
actions_not: elseActions,
});
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
triggers: [{ trigger: 'state', entity_id: 'binary_sensor.x' }],
actions: [
{
if: [{ condition: 'state', entity_id: 'binary_sensor.x', state: 'on' }],
then,
else: elseActions,
},
],
});
postUpgradeChecks(config);
});
it('should default then to an empty list when there are no then actions', () => {
const config = automate({
conditions: [
{ condition: 'state', entity_id: 'binary_sensor.x', state: 'on' },
],
actions_not: elseActions,
});
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
triggers: [{ trigger: 'state', entity_id: 'binary_sensor.x' }],
actions: [
{
if: [{ condition: 'state', entity_id: 'binary_sensor.x', state: 'on' }],
then: [],
else: elseActions,
},
],
});
postUpgradeChecks(config);
});
it('should synthesize a valueless trigger for a card-state condition', () => {
const config = automate({
conditions: [{ condition: 'fullscreen', fullscreen: true }],
actions: then,
actions_not: elseActions,
});
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
triggers: [{ trigger: 'fullscreen' }],
actions: [
{
if: [{ condition: 'fullscreen', fullscreen: true }],
then,
else: elseActions,
},
],
});
postUpgradeChecks(config);
});
it('should watch the entity for a numeric_state condition', () => {
const config = automate({
conditions: [{ condition: 'numeric_state', entity_id: 'sensor.t', above: 25 }],
actions: then,
actions_not: elseActions,
});
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]['triggers']).toEqual([
{ trigger: 'state', entity_id: 'sensor.t' },
]);
postUpgradeChecks(config);
});
it('should preserve config paths in a synthesized config trigger', () => {
const config = automate({
conditions: [
{ condition: 'state', entity_id: 'binary_sensor.x', state: 'on' },
// `config` is trigger-only: it is dropped from the `if` predicate but
// still contributes a trigger (with its `paths` preserved).
{ condition: 'config', paths: ['menu.style'] },
],
actions: then,
actions_not: elseActions,
});
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]['triggers']).toEqual([
{ trigger: 'state', entity_id: 'binary_sensor.x' },
{ trigger: 'config', paths: ['menu.style'] },
]);
expect(config.automations[0]['actions']).toEqual([
{
if: [{ condition: 'state', entity_id: 'binary_sensor.x', state: 'on' }],
then,
else: elseActions,
},
]);
postUpgradeChecks(config);
});
it('should deduplicate identical synthesized triggers', () => {
const config = automate({
conditions: [
{ condition: 'state', entity_id: 'binary_sensor.x', state: 'on' },
{ condition: 'state', entity_id: 'binary_sensor.x', state_not: 'off' },
],
actions: then,
actions_not: elseActions,
});
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]['triggers']).toEqual([
{ trigger: 'state', entity_id: 'binary_sensor.x' },
]);
postUpgradeChecks(config);
});
it('should record a template condition as an upgrade failure, untouched', () => {
const failing = {
conditions: [{ condition: 'template', value_template: '{{ true }}' }],
actions: then,
actions_not: elseActions,
};
const config = automate({ ...failing });
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations).toEqual([]);
expect(config['__UPGRADE_FAILURE__']).toEqual({ automations: [failing] });
postUpgradeChecks(config);
});
it('should keep a retired microphone action in a recorded upgrade failure', () => {
const failing = {
conditions: [{ condition: 'template', value_template: '{{ true }}' }],
actions: [
{
action: 'fire-dom-event',
advanced_camera_card_action: 'microphone_connect',
},
],
actions_not: elseActions,
};
const config = automate({ ...failing });
expect(upgradeConfig(config)).toBeTruthy();
expect(config['__UPGRADE_FAILURE__']).toEqual({ automations: [failing] });
});
it('should record a screen condition as an upgrade failure, untouched', () => {
const failing = {
conditions: [{ condition: 'screen', media_query: '(orientation: landscape)' }],
actions: then,
actions_not: elseActions,
};
const config = automate({ ...failing });
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations).toEqual([]);
expect(config['__UPGRADE_FAILURE__']).toEqual({ automations: [failing] });
postUpgradeChecks(config);
});
it('should record a numeric_state condition with no entity to watch as an upgrade failure', () => {
const failing = {
conditions: [
{ condition: 'numeric_state', above: 25, value_template: '{{ 5 }}' },
],
actions: then,
actions_not: elseActions,
};
const config = automate({ ...failing });
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations).toEqual([]);
expect(config['__UPGRADE_FAILURE__']).toEqual({ automations: [failing] });
postUpgradeChecks(config);
});
it('should record mixed conditions as an upgrade failure when any leaf is rising-edge-only', () => {
const failing = {
conditions: [
{ condition: 'state', entity_id: 'binary_sensor.x', state: 'on' },
{ condition: 'template', value_template: '{{ true }}' },
],
actions: then,
actions_not: elseActions,
};
const config = automate({ ...failing });
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations).toEqual([]);
expect(config['__UPGRADE_FAILURE__']).toEqual({ automations: [failing] });
postUpgradeChecks(config);
});
it('should convert one automation and fail another in the same config', () => {
const failing = {
conditions: [{ condition: 'screen', media_query: '(orientation: landscape)' }],
actions: then,
actions_not: elseActions,
};
const config = automate(
{
conditions: [
{ condition: 'state', entity_id: 'binary_sensor.x', state: 'on' },
],
actions: then,
actions_not: elseActions,
},
{ ...failing },
);
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations).toEqual([
{
triggers: [{ trigger: 'state', entity_id: 'binary_sensor.x' }],
actions: [
{
if: [{ condition: 'state', entity_id: 'binary_sensor.x', state: 'on' }],
then,
else: elseActions,
},
],
},
]);
expect(config['__UPGRADE_FAILURE__']).toEqual({ automations: [failing] });
postUpgradeChecks(config);
});
it('should append to existing automation failures and leave others untouched', () => {
const earlier = { some: 'earlier failed automation' };
const failing = {
conditions: [{ condition: 'screen', media_query: '(orientation: landscape)' }],
actions: then,
actions_not: elseActions,
};
const config = automate({ ...failing });
config['__UPGRADE_FAILURE__'] = {
automations: [earlier],
overrides: ['untouched'],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config['__UPGRADE_FAILURE__']).toEqual({
automations: [earlier, failing],
overrides: ['untouched'],
});
postUpgradeChecks(config);
});
it('should fall back to an initialized trigger for all-static conditions', () => {
const config = automate({
conditions: [{ condition: 'user', users: ['abc'] }],
actions: then,
actions_not: elseActions,
});
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
triggers: [{ trigger: 'initialized' }],
actions: [
{
if: [{ condition: 'user', users: ['abc'] }],
then,
else: elseActions,
},
],
});
postUpgradeChecks(config);
});
it('should not synthesize a trigger for a user condition that cannot change at runtime', () => {
const config = automate({
conditions: [
{ condition: 'user', users: ['abc'] },
{ condition: 'state', entity_id: 'binary_sensor.x', state: 'on' },
],
actions: then,
actions_not: elseActions,
});
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]['triggers']).toEqual([
{ trigger: 'state', entity_id: 'binary_sensor.x' },
]);
postUpgradeChecks(config);
});
it('should drop actions_not when there are no conditions', () => {
const config = automate({
triggers: [{ trigger: 'state', entity_id: 'binary_sensor.x' }],
actions: then,
actions_not: elseActions,
});
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
triggers: [{ trigger: 'state', entity_id: 'binary_sensor.x' }],
actions: then,
});
postUpgradeChecks(config);
});
it('should keep only the then actions when the conditions are entirely trigger-only', () => {
const config = automate({
conditions: [{ condition: 'config', paths: ['menu.style'] }],
actions: then,
actions_not: elseActions,
});
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
triggers: [{ trigger: 'config', paths: ['menu.style'] }],
actions: then,
});
postUpgradeChecks(config);
});
it('should synthesize a valueless config trigger when no paths are given', () => {
const config = automate({
conditions: [{ condition: 'config' }],
actions: then,
actions_not: elseActions,
});
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
triggers: [{ trigger: 'config' }],
actions: then,
});
postUpgradeChecks(config);
});
it('should map a discriminator-less state condition to a state trigger', () => {
const config = automate({
conditions: [{ entity: 'binary_sensor.x', state: 'on' }],
actions: then,
actions_not: elseActions,
});
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
triggers: [{ trigger: 'state', entity_id: 'binary_sensor.x' }],
actions: [
{
if: [{ entity: 'binary_sensor.x', state: 'on' }],
then,
else: elseActions,
},
],
});
postUpgradeChecks(config);
});
it('should omit else when actions_not is not a list', () => {
const config = automate({
conditions: [
{ condition: 'state', entity_id: 'binary_sensor.x', state: 'on' },
],
actions: then,
actions_not: 'not-a-list',
});
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
triggers: [{ trigger: 'state', entity_id: 'binary_sensor.x' }],
actions: [
{
if: [{ condition: 'state', entity_id: 'binary_sensor.x', state: 'on' }],
then,
},
],
});
postUpgradeChecks(config);
});
it('should contribute no trigger for a non-object condition leaf', () => {
const config = automate({
conditions: [
'not-a-condition',
{ condition: 'state', entity_id: 'binary_sensor.x', state: 'on' },
],
actions: then,
actions_not: elseActions,
});
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]['triggers']).toEqual([
{ trigger: 'state', entity_id: 'binary_sensor.x' },
]);
});
it('should leave a non-object automation entry untouched', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
'not-an-automation',
{
conditions: [
{ condition: 'state', entity_id: 'binary_sensor.x', state: 'on' },
],
actions: then,
actions_not: elseActions,
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toBe('not-an-automation');
expect(config.automations[1]).toEqual({
triggers: [{ trigger: 'state', entity_id: 'binary_sensor.x' }],
actions: [
{
if: [{ condition: 'state', entity_id: 'binary_sensor.x', state: 'on' }],
then,
else: elseActions,
},
],
});
});
});
describe('trigger-only conditions in overrides and elements', () => {
it('should drop an override whose only condition is a config condition', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
overrides: [
{
conditions: [{ condition: 'config', paths: ['menu.style'] }],
merge: { menu: { style: 'none' } },
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.overrides).toEqual([]);
postUpgradeChecks(config);
});
it('should strip a config condition from an override with other conditions', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
overrides: [
{
conditions: [
{ condition: 'config', paths: ['menu.style'] },
{ condition: 'state', entity_id: 'binary_sensor.door', state: 'on' },
],
merge: { menu: { style: 'none' } },
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.overrides).toEqual([
{
conditions: [
{ condition: 'state', entity_id: 'binary_sensor.door', state: 'on' },
],
merge: { menu: { style: 'none' } },
},
]);
postUpgradeChecks(config);
});
it('should drop an override whose only condition is a valueless camera condition', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
overrides: [
{
conditions: [{ condition: 'camera' }],
merge: { menu: { style: 'none' } },
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.overrides).toEqual([]);
postUpgradeChecks(config);
});
it('should drop a conditional element whose only condition is a config condition', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:advanced-camera-card-conditional',
conditions: [{ condition: 'config', paths: ['menu.style'] }],
elements: [{ type: 'icon', icon: 'mdi:cow' }],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.elements).toBeUndefined();
postUpgradeChecks(config);
});
it('should strip a config condition from a conditional element with other conditions', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:advanced-camera-card-conditional',
conditions: [
{ condition: 'config', paths: ['menu.style'] },
{ condition: 'state', entity_id: 'binary_sensor.door', state: 'on' },
],
elements: [{ type: 'icon', icon: 'mdi:cow' }],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.elements).toEqual([
{
type: 'custom:advanced-camera-card-conditional',
conditions: [
{ condition: 'state', entity_id: 'binary_sensor.door', state: 'on' },
],
elements: [{ type: 'icon', icon: 'mdi:cow' }],
},
]);
postUpgradeChecks(config);
});
it('should recurse into nested conditional elements, keeping siblings', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:advanced-camera-card-conditional',
conditions: [
{ condition: 'state', entity_id: 'binary_sensor.door', state: 'on' },
],
elements: [
{ type: 'icon', icon: 'mdi:cow' },
{
type: 'custom:advanced-camera-card-conditional',
conditions: [{ condition: 'config', paths: ['menu.style'] }],
elements: [{ type: 'icon', icon: 'mdi:pig' }],
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.elements).toEqual([
{
type: 'custom:advanced-camera-card-conditional',
conditions: [
{ condition: 'state', entity_id: 'binary_sensor.door', state: 'on' },
],
elements: [{ type: 'icon', icon: 'mdi:cow' }],
},
]);
postUpgradeChecks(config);
});
it('should leave malformed entries (no conditions / no elements) untouched', () => {
// These shapes are schema-invalid, so the transform must not crash on
// them (a non-object override, an entry without a `conditions` array, a
// conditional without an `elements` array); it simply leaves them as-is.
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
overrides: ['malformed', { merge: {} }],
elements: [
{
type: 'custom:advanced-camera-card-conditional',
conditions: [
{ condition: 'state', entity_id: 'binary_sensor.door', state: 'on' },
],
},
],
};
upgradeConfig(config);
expect(config.overrides).toEqual(['malformed', { merge: {} }]);
expect(config.elements).toEqual([
{
type: 'custom:advanced-camera-card-conditional',
conditions: [
{ condition: 'state', entity_id: 'binary_sensor.door', state: 'on' },
],
},
]);
});
});
describe('composite condition shorthand', () => {
it('should keep a conditional element whose shorthand conditions are all retained', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:advanced-camera-card-conditional',
conditions: [
{
or: [
{ condition: 'state', state: 'off', entity: 'input_boolean.door' },
{ condition: 'call', call: 'answered' },
],
},
],
elements: [{ type: 'icon', icon: 'mdi:cow' }],
},
],
};
expect(upgradeConfig(config)).toBeFalsy();
expect(config.elements).toEqual([
{
type: 'custom:advanced-camera-card-conditional',
conditions: [
{
or: [
{ condition: 'state', state: 'off', entity: 'input_boolean.door' },
{ condition: 'call', call: 'answered' },
],
},
],
elements: [{ type: 'icon', icon: 'mdi:cow' }],
},
]);
postUpgradeChecks(config);
});
it('should prune trigger-only leaves from an operator shorthand composite', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
overrides: [
{
conditions: [
{
or: [{ condition: 'camera' }, { condition: 'view', views: ['live'] }],
},
],
merge: { menu: { style: 'none' } },
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.overrides).toEqual([
{
conditions: [{ or: [{ condition: 'view', views: ['live'] }] }],
merge: { menu: { style: 'none' } },
},
]);
postUpgradeChecks(config);
});
it('should prune trigger-only leaves from an implicit and composite', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
overrides: [
{
conditions: [
{
condition: [
{ condition: 'camera' },
{ condition: 'view', views: ['live'] },
],
},
],
merge: { menu: { style: 'none' } },
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.overrides).toEqual([
{
conditions: [{ condition: [{ condition: 'view', views: ['live'] }] }],
merge: { menu: { style: 'none' } },
},
]);
postUpgradeChecks(config);
});
it('should promote the leaves of a shorthand composite to triggers', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [
{
or: [{ condition: 'camera' }, { condition: 'view', views: ['live'] }],
},
],
actions: [
{ action: 'fire-dom-event', advanced_camera_card_action: 'live' },
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
conditions: [{ or: [{ condition: 'view', views: ['live'] }] }],
triggers: [{ trigger: 'camera' }, { trigger: 'view', views: ['live'] }],
actions: [{ action: 'fire-dom-event', advanced_camera_card_action: 'live' }],
});
postUpgradeChecks(config);
});
it('should keep a composite the user wrote empty', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:advanced-camera-card-conditional',
conditions: [{ condition: [] }],
elements: [{ type: 'icon', icon: 'mdi:cow' }],
},
],
};
expect(upgradeConfig(config)).toBeFalsy();
expect(config.elements).toEqual([
{
type: 'custom:advanced-camera-card-conditional',
conditions: [{ condition: [] }],
elements: [{ type: 'icon', icon: 'mdi:cow' }],
},
]);
postUpgradeChecks(config);
});
it('should keep an untouched single-condition composite as it was written', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
overrides: [
{
conditions: [{ or: { condition: 'view', views: ['live'] } }],
merge: { menu: { style: 'none' } },
},
],
};
expect(upgradeConfig(config)).toBeFalsy();
expect(config.overrides).toEqual([
{
conditions: [{ or: { condition: 'view', views: ['live'] } }],
merge: { menu: { style: 'none' } },
},
]);
postUpgradeChecks(config);
});
it('should prune a trigger-only leaf from a single-condition composite', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
overrides: [
{
conditions: [{ or: { condition: 'camera' } }],
merge: { menu: { style: 'none' } },
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.overrides).toEqual([]);
postUpgradeChecks(config);
});
it('should promote the leaf of a single-condition composite to a trigger', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [{ or: { condition: 'view', views: ['live'] } }],
actions: [
{ action: 'fire-dom-event', advanced_camera_card_action: 'live' },
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0]).toEqual({
conditions: [{ or: { condition: 'view', views: ['live'] } }],
triggers: [{ trigger: 'view', views: ['live'] }],
actions: [{ action: 'fire-dom-event', advanced_camera_card_action: 'live' }],
});
postUpgradeChecks(config);
});
it('should drop a bare picture-element state condition with no state match', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
overrides: [
{
conditions: [
{ entity: 'binary_sensor.door' },
{ condition: 'view', views: ['live'] },
],
merge: { menu: { style: 'none' } },
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.overrides).toEqual([
{
conditions: [{ condition: 'view', views: ['live'] }],
merge: { menu: { style: 'none' } },
},
]);
postUpgradeChecks(config);
});
});
describe('trigger template paths -> top-level trigger.*', () => {
it('should rewrite every legacy trigger path in an automation action', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
triggers: [{ trigger: 'state', entity_id: 'binary_sensor.door' }],
actions: [
{
action: 'fire-dom-event',
advanced_camera_card_action: 'log',
message:
'{{ acc.trigger.state.entity }} {{ acc.trigger.state.from }} ' +
'{{ acc.trigger.state.to }} {{ acc.trigger.camera.from }} ' +
'{{ acc.trigger.camera.to }} {{ acc.trigger.view.from }} ' +
'{{ acc.trigger.view.to }} {{ acc.trigger.config.from }} ' +
'{{ acc.trigger.config.to }}',
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0].actions[0].message).toBe(
'{{ trigger.entity_id }} {{ trigger.from_state.state }} ' +
'{{ trigger.to_state.state }} {{ trigger.from_acc.camera }} ' +
'{{ trigger.to_acc.camera }} {{ trigger.from_acc.view }} ' +
'{{ trigger.to_acc.view }} {{ trigger.from_acc.config }} ' +
'{{ trigger.to_acc.config }}',
);
postUpgradeChecks(config);
});
it('should also migrate the long advanced_camera_card prefix', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
triggers: [{ trigger: 'state', entity_id: 'binary_sensor.door' }],
actions: [
{
action: 'fire-dom-event',
advanced_camera_card_action: 'log',
message: '{{ advanced_camera_card.trigger.state.to }}',
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0].actions[0].message).toBe(
'{{ trigger.to_state.state }}',
);
postUpgradeChecks(config);
});
it('should rewrite paths in a non-automation menu action', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:advanced-camera-card-menu-icon',
icon: 'mdi:cctv',
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'camera_select',
camera: '{{ acc.trigger.camera.to }}',
},
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.elements[0].tap_action.camera).toBe('{{ trigger.to_acc.camera }}');
postUpgradeChecks(config);
});
it('should not rewrite a string without a template marker', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
triggers: [{ trigger: 'state', entity_id: 'binary_sensor.door' }],
actions: [
{
action: 'fire-dom-event',
advanced_camera_card_action: 'log',
// No `{{` or `{%`, so it is opaque text, not a template, and is
// left alone.
message: 'see acc.trigger.state.to for details',
},
],
},
],
};
expect(upgradeConfig(config)).toBeFalsy();
expect(config.automations[0].actions[0].message).toBe(
'see acc.trigger.state.to for details',
);
postUpgradeChecks(config);
});
it('should migrate a path inside a statement-only template', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
triggers: [{ trigger: 'state', entity_id: 'binary_sensor.door' }],
actions: [
{
action: 'fire-dom-event',
advanced_camera_card_action: 'log',
// A `{% %}` statement with no `{{` is still a template.
message: "{% if acc.trigger.state.to == 'on' %}on{% endif %}",
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0].actions[0].message).toBe(
"{% if trigger.to_state.state == 'on' %}on{% endif %}",
);
postUpgradeChecks(config);
});
it('should migrate the config trigger path', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
triggers: [{ trigger: 'config' }],
actions: [
{
action: 'fire-dom-event',
advanced_camera_card_action: 'log',
message: '{{ acc.trigger.config.to }}',
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0].actions[0].message).toBe(
'{{ trigger.to_acc.config }}',
);
postUpgradeChecks(config);
});
it('should be idempotent', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
triggers: [{ trigger: 'state', entity_id: 'binary_sensor.door' }],
actions: [
{
action: 'fire-dom-event',
advanced_camera_card_action: 'log',
message: '{{ acc.trigger.state.to }}',
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(upgradeConfig(config)).toBeFalsy();
expect(config.automations[0].actions[0].message).toBe(
'{{ trigger.to_state.state }}',
);
});
});
describe('ambient advanced_camera_card.* -> acc.*', () => {
it('should rewrite the ambient namespace in a template', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:advanced-camera-card-menu-icon',
icon: 'mdi:cctv',
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'log',
message:
'{{ advanced_camera_card.camera }} / {{ advanced_camera_card.view }}',
},
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.elements[0].tap_action.message).toBe(
'{{ acc.camera }} / {{ acc.view }}',
);
postUpgradeChecks(config);
});
it('should leave the short acc spelling untouched', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:advanced-camera-card-menu-icon',
icon: 'mdi:cctv',
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'log',
message: '{{ acc.camera }}',
},
},
],
};
expect(upgradeConfig(config)).toBeFalsy();
expect(config.elements[0].tap_action.message).toBe('{{ acc.camera }}');
});
});
describe('cameras[].triggers.events string[] -> triggers.media_events', () => {
it('should migrate a legacy string array to media_events', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [
{
camera_entity: 'camera.office',
triggers: { events: ['events', 'clips'] },
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.cameras[0].triggers).toEqual({
media_events: ['events', 'clips'],
});
postUpgradeChecks(config);
});
it('should leave an empty array alone', () => {
// An empty list says nothing about which of the two shapes it is, and
// is valid under the new schema. Claiming it would rename the key of a
// user who has simply deleted their last event trigger.
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office', triggers: { events: [] } }],
};
upgradeConfig(config);
expect(config.cameras[0].triggers).toEqual({ events: [] });
});
it('should not report an upgrade for an empty array', () => {
// Otherwise the editor offers an upgrade to a user who has only
// deleted their last event trigger.
expect(
isConfigUpgradeable({
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office', triggers: { events: [] } }],
}),
).toBeFalsy();
});
it('should leave the new object-array shape untouched', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [
{
camera_entity: 'camera.office',
triggers: { events: [{ event_type: 'zha_event' }] },
},
],
};
// The new shape might still trigger OTHER upgrades to fire on the
// config, so we don't assert the overall upgrade result -- only that
// this specific field is not touched.
upgradeConfig(config);
expect(config.cameras[0].triggers).toEqual({
events: [{ event_type: 'zha_event' }],
});
});
it('should drop legacy events but keep an existing media_events untouched', () => {
// Both fields present is implausible in real user configs, but if it
// happens we still must remove the legacy `events: string[]` because
// the new schema would reject it; the explicit `media_events` wins.
const config = {
type: 'custom:advanced-camera-card',
cameras: [
{
camera_entity: 'camera.office',
triggers: {
events: ['snapshots'],
media_events: ['clips'],
},
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.cameras[0].triggers).toEqual({
media_events: ['clips'],
});
postUpgradeChecks(config);
});
it('should be idempotent across repeated runs', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [
{
camera_entity: 'camera.office',
triggers: { events: ['events'] },
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(upgradeConfig(config)).toBeFalsy();
expect(config.cameras[0].triggers).toEqual({ media_events: ['events'] });
});
it('should migrate per camera independently', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [
{ camera_entity: 'camera.a', triggers: { events: ['clips'] } },
{
camera_entity: 'camera.b',
triggers: { events: [{ event_type: 'zha_event' }] },
},
{ camera_entity: 'camera.c' },
],
};
upgradeConfig(config);
expect(config.cameras[0].triggers).toEqual({ media_events: ['clips'] });
expect(config.cameras[1].triggers).toEqual({
events: [{ event_type: 'zha_event' }],
});
expect(config.cameras[2].triggers).toBeUndefined();
});
it('should migrate a legacy cameras_global.triggers.events string array', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
cameras_global: {
triggers: {
events: ['clips', 'snapshots'],
},
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.cameras_global.triggers).toEqual({
media_events: ['clips', 'snapshots'],
});
postUpgradeChecks(config);
});
it('should leave cameras_global new-shape events untouched', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
cameras_global: {
triggers: {
events: [{ event_type: 'zha_event' }],
},
},
};
upgradeConfig(config);
expect(config.cameras_global.triggers).toEqual({
events: [{ event_type: 'zha_event' }],
});
});
it('should be a no-op when triggers is not an object', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office', triggers: 'not-an-object' }],
};
upgradeConfig(config);
expect(config.cameras[0].triggers).toBe('not-an-object');
});
});
describe('live_substream_{on,off,select} -> substream_{on,off}', () => {
it('should rewrite live_substream_on -> substream_on in an automation', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [{ condition: 'initialized' }],
actions: [
{
action: 'fire-dom-event',
advanced_camera_card_action: 'live_substream_on',
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0].actions).toEqual([
{
action: 'fire-dom-event',
advanced_camera_card_action: 'substream_on',
},
]);
postUpgradeChecks(config);
});
it('should rewrite live_substream_off -> substream_off in an automation', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [{ condition: 'initialized' }],
actions: [
{
action: 'fire-dom-event',
advanced_camera_card_action: 'live_substream_off',
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0].actions).toEqual([
{
action: 'fire-dom-event',
advanced_camera_card_action: 'substream_off',
},
]);
postUpgradeChecks(config);
});
it('should rewrite live_substream_select -> substream_on with camera -> stream', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [{ condition: 'initialized' }],
actions: [
{
action: 'fire-dom-event',
advanced_camera_card_action: 'live_substream_select',
camera: 'camera.office_hd',
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0].actions).toEqual([
{
action: 'fire-dom-event',
advanced_camera_card_action: 'substream_on',
stream: 'camera.office_hd',
},
]);
postUpgradeChecks(config);
});
it('should rewrite a malformed live_substream_select with no camera field', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [{ condition: 'initialized' }],
actions: [
{
action: 'fire-dom-event',
advanced_camera_card_action: 'live_substream_select',
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.automations[0].actions).toEqual([
{
action: 'fire-dom-event',
advanced_camera_card_action: 'substream_on',
},
]);
postUpgradeChecks(config);
});
it('should migrate actions on elements and overrides', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:advanced-camera-card-menu-icon',
icon: 'mdi:video-input-component',
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'live_substream_select',
camera: 'camera.office_hd',
},
},
],
overrides: [
{
conditions: [{ condition: 'fullscreen', fullscreen: true }],
merge: {
menu: {
buttons: {
substreams: {
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'live_substream_on',
},
},
},
},
},
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.elements[0].tap_action).toEqual({
action: 'fire-dom-event',
advanced_camera_card_action: 'substream_on',
stream: 'camera.office_hd',
});
expect(config.overrides[0].merge.menu.buttons.substreams.tap_action).toEqual({
action: 'fire-dom-event',
advanced_camera_card_action: 'substream_on',
});
postUpgradeChecks(config);
});
it('should leave opaque user payloads alone', () => {
// A perform-action service payload that happens to mention the legacy
// action string in its `data` block must not be rewritten -- `data` is
// opaque, not an action.
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
triggers: [{ trigger: 'initialized' }],
actions: [
{
action: 'perform-action',
perform_action: 'script.bogus',
data: {
advanced_camera_card_action: 'live_substream_select',
camera: 'camera.office_hd',
},
},
],
},
],
};
expect(upgradeConfig(config)).toBeFalsy();
expect(config.automations[0].actions[0]).toEqual({
action: 'perform-action',
perform_action: 'script.bogus',
data: {
advanced_camera_card_action: 'live_substream_select',
camera: 'camera.office_hd',
},
});
});
it('should be idempotent', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
automations: [
{
conditions: [{ condition: 'initialized' }],
actions: [
{
action: 'fire-dom-event',
advanced_camera_card_action: 'live_substream_select',
camera: 'camera.office_hd',
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
// Running upgradeConfig again should not change anything.
expect(upgradeConfig(config)).toBeFalsy();
expect(config.automations[0].actions).toEqual([
{
action: 'fire-dom-event',
advanced_camera_card_action: 'substream_on',
stream: 'camera.office_hd',
},
]);
postUpgradeChecks(config);
});
});
});
});