Files
advanced-camera-card/tests/card-controller/config/load-control-entities.test.ts
T
Dermot Duffy ea251ca988 perf(bundle): lazy-load the nunjucks template engine (#2535)
Closes #2531.

## Summary

The full `nunjucks` templating engine (~226KB) plus `ha-nunjucks`
(~46KB) — roughly **272KB, ~13% of the eager entry chunk** — was
statically imported and downloaded by every card on initial load, even
though templates only apply when a config value contains a `{{ … }}` /
`{% … %}` delimiter. Most cards use no templates and never need the
engine.

This defers the engine behind a dynamic `import('ha-nunjucks/dist')` so
it ships in a separate, on-demand chunk instead of the eager
`card-*.js`.

## Approach

The render path (`TemplateRenderer.renderRecursively`) is **kept
synchronous** — it is called from many synchronous hot paths
(condition/trigger evaluators, picture-elements rendering, actions,
folder matchers), and making it async would be a large, high-risk
refactor of the evaluation core.

Instead:

- **New `src/card-controller/templates/engine.ts`** — a module-level
singleton lazy loader (`loadTemplateEngine()` / `getTemplateEngine()`)
shared across all `TemplateRenderer` instances, plus a
`containsTemplate()` delimiter helper.
- **Delimiter gating** — strings without a delimiter never touch the
engine (the overwhelming majority of renders).
- **Pre-warm at config time** — because every template string originates
in the config, a new mandatory `TEMPLATE_ENGINE` initialization aspect
loads the engine before first render whenever the config contains a
delimiter. This **guarantees no raw `{{ … }}` flash**: content/condition
rendering is blocked until the engine is present for template-using
cards. Cards without templates never load it.

## Result

- nunjucks + ha-nunjucks move out of the eager chunk into a separate
chunk fetched only when a card actually uses templates.
- No change to the synchronous public render API; condition/trigger
evaluation core untouched.

## Tests

- New `engine.ts` loader coverage (concurrent load, cached reuse,
not-loaded fallback).
- The 11 existing test files that render real (delimiter-bearing)
templates declare their dependency explicitly via
`beforeAll(loadTemplateEngine)` — no global/implicit setup hook.
- Full suite green (4764 tests), lint and ts-prune clean, per-file 100%
coverage maintained for the affected directories.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_016ykWemdkZrgywvC71pHc6c

---
_Generated by [Claude
Code](https://claude.ai/code/session_016ykWemdkZrgywvC71pHc6c)_
2026-06-30 17:45:13 -07:00

778 lines
24 KiB
TypeScript

import { assert, describe, expect, it, vi } from 'vitest';
import { AutomationsManager } from '../../../src/card-controller/automations-manager';
import { setRemoteControlEntityFromConfig } from '../../../src/card-controller/config/load-control-entities';
import type { CardController } from '../../../src/card-controller/controller';
import { ConditionStateManager } from '../../../src/condition-trigger/conditions/state-manager';
import {
GENERATED_ACTION,
type ActionGenerator,
type GeneratedActionConfig,
} from '../../../src/config/schema/actions/custom/generated-action';
import {
INTERNAL_CALLBACK_ACTION,
type InternalCallbackActionConfig,
} from '../../../src/config/schema/actions/custom/internal';
import type { ActionConfig } from '../../../src/config/schema/actions/types';
import {
createCameraAction,
isAdvancedCameraCardCustomAction,
} from '../../../src/utils/action';
import { arrayify } from '../../../src/utils/basic';
import {
createCardAPI,
createConfig,
createHASS,
createStateEntity,
createStore,
createView,
} from '../../test-utils';
const isGeneratedAction = (action: ActionConfig): action is GeneratedActionConfig =>
'advanced_camera_card_action' in action &&
action.advanced_camera_card_action === GENERATED_ACTION;
describe('setRemoteControlEntityFromConfig', () => {
it('without control entity', () => {
const api = createCardAPI();
setRemoteControlEntityFromConfig(api);
expect(api.getAutomationsManager().deleteAutomations).toBeCalled();
expect(api.getAutomationsManager().addAutomations).not.toBeCalled();
});
it('with control entity and card priority', () => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
remote_control: {
entities: {
camera: 'input_select.camera',
camera_priority: 'card',
},
},
}),
);
setRemoteControlEntityFromConfig(api);
expect(api.getAutomationsManager().deleteAutomations).toBeCalled();
expect(api.getAutomationsManager().addAutomations).toBeCalledWith([
{
actions: [
{
action: 'fire-dom-event',
advanced_camera_card_action: '__INTERNAL_CALLBACK_ACTION__',
callback: expect.any(Function),
},
],
triggers: [
{
trigger: 'config',
paths: ['cameras', 'remote_control.entities.camera'],
},
],
tag: setRemoteControlEntityFromConfig,
},
{
actions: [
{
action: 'fire-dom-event',
advanced_camera_card_action: '__INTERNAL_CALLBACK_ACTION__',
callback: expect.any(Function),
},
],
triggers: [
{
trigger: 'camera',
},
],
tag: setRemoteControlEntityFromConfig,
},
{
actions: [
{
action: 'fire-dom-event',
advanced_camera_card_action: '__INTERNAL_CALLBACK_ACTION__',
callback: expect.any(Function),
},
],
triggers: [
{
trigger: 'initialized',
},
],
tag: setRemoteControlEntityFromConfig,
},
{
actions: [
{
action: 'fire-dom-event',
advanced_camera_card_action: '__GENERATED_ACTION__',
generator: expect.any(Function),
},
],
triggers: [
{
trigger: 'state',
entity_id: 'input_select.camera',
},
],
tag: setRemoteControlEntityFromConfig,
},
]);
});
it('with control entity and entity priority', () => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
remote_control: {
entities: {
camera: 'input_select.camera',
camera_priority: 'entity',
},
},
}),
);
setRemoteControlEntityFromConfig(api);
expect(api.getAutomationsManager().deleteAutomations).toBeCalled();
expect(api.getAutomationsManager().addAutomations).toBeCalledWith([
{
actions: [
{
action: 'fire-dom-event',
advanced_camera_card_action: '__INTERNAL_CALLBACK_ACTION__',
callback: expect.any(Function),
},
],
triggers: [
{
trigger: 'config',
paths: ['cameras', 'remote_control.entities.camera'],
},
],
tag: setRemoteControlEntityFromConfig,
},
{
actions: [
{
action: 'fire-dom-event',
advanced_camera_card_action: '__INTERNAL_CALLBACK_ACTION__',
callback: expect.any(Function),
},
],
triggers: [
{
trigger: 'camera',
},
],
tag: setRemoteControlEntityFromConfig,
},
{
actions: [
{
action: 'fire-dom-event',
advanced_camera_card_action: '__GENERATED_ACTION__',
generator: expect.any(Function),
},
],
triggers: [
{
trigger: 'initialized',
},
],
tag: setRemoteControlEntityFromConfig,
},
{
actions: [
{
action: 'fire-dom-event',
advanced_camera_card_action: '__GENERATED_ACTION__',
generator: expect.any(Function),
},
],
triggers: [
{
trigger: 'state',
entity_id: 'input_select.camera',
},
],
tag: setRemoteControlEntityFromConfig,
},
]);
});
describe('should set options', () => {
it('should set options when they are incorrect', () => {
const hass = createHASS();
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
remote_control: {
entities: {
camera: 'input_select.camera',
},
},
}),
);
const store = createStore([
{
cameraID: 'camera.one',
},
{
cameraID: 'camera.two',
},
]);
vi.mocked(api.getCameraManager().getStore).mockReturnValue(store);
setRemoteControlEntityFromConfig(api);
const addOptionsAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
.calls[0][0][0].actions?.[0] as InternalCallbackActionConfig;
expect(addOptionsAction).toBeTruthy();
expect(isAdvancedCameraCardCustomAction(addOptionsAction)).toBeTruthy();
expect(addOptionsAction.advanced_camera_card_action).toBe(
INTERNAL_CALLBACK_ACTION,
);
addOptionsAction.callback(api);
expect(hass.callService).toBeCalledWith(
'input_select',
'set_options',
{
options: ['camera.one', 'camera.two'],
},
{
entity_id: 'input_select.camera',
},
);
});
it('should not set options when they are already correct', () => {
const hass = createHASS({
'input_select.camera': createStateEntity({
attributes: { options: ['camera.one', 'camera.two'] },
}),
});
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
remote_control: {
entities: {
camera: 'input_select.camera',
},
},
}),
);
const store = createStore([
{
cameraID: 'camera.one',
},
{
cameraID: 'camera.two',
},
]);
vi.mocked(api.getCameraManager().getStore).mockReturnValue(store);
setRemoteControlEntityFromConfig(api);
const addOptionsAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
.calls[0][0][0].actions?.[0] as InternalCallbackActionConfig;
expect(addOptionsAction).toBeTruthy();
expect(isAdvancedCameraCardCustomAction(addOptionsAction)).toBeTruthy();
expect(addOptionsAction.advanced_camera_card_action).toBe(
INTERNAL_CALLBACK_ACTION,
);
addOptionsAction.callback(api);
expect(hass.callService).not.toBeCalled();
});
it('should not throw when hass is undefined setting options', () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
remote_control: {
entities: {
camera: 'input_select.camera',
},
},
}),
);
const store = createStore([
{
cameraID: 'camera.one',
},
{
cameraID: 'camera.two',
},
]);
vi.mocked(api.getCameraManager().getStore).mockReturnValue(store);
setRemoteControlEntityFromConfig(api);
const addOptionsAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
.calls[0][0][0].actions?.[0] as InternalCallbackActionConfig;
expect(addOptionsAction).toBeTruthy();
expect(isAdvancedCameraCardCustomAction(addOptionsAction)).toBeTruthy();
expect(addOptionsAction.advanced_camera_card_action).toBe(
INTERNAL_CALLBACK_ACTION,
);
// Should not throw
addOptionsAction.callback(api);
});
});
describe('should select option on entity', () => {
it('should select option when camera differs from entity state', async () => {
const hass = createHASS({
'input_select.camera': createStateEntity({
state: 'camera.one',
}),
});
const api = createCardAPI();
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([{ cameraID: 'camera.two' }]),
);
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
remote_control: {
entities: {
camera: 'input_select.camera',
camera_priority: 'card',
},
},
}),
);
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
camera: 'camera.two',
view: 'live',
}),
);
setRemoteControlEntityFromConfig(api);
// Get the camera sync callback (automation index 1)
const cameraSyncAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
.calls[0][0][1].actions?.[0] as InternalCallbackActionConfig;
expect(cameraSyncAction).toBeTruthy();
expect(isAdvancedCameraCardCustomAction(cameraSyncAction)).toBeTruthy();
expect(cameraSyncAction.advanced_camera_card_action).toBe(
INTERNAL_CALLBACK_ACTION,
);
await cameraSyncAction.callback(api);
expect(hass.callService).toBeCalledWith(
'input_select',
'select_option',
{
option: 'camera.two',
},
{
entity_id: 'input_select.camera',
},
);
});
it('should not select option when camera matches entity state', () => {
const hass = createHASS({
'input_select.camera': createStateEntity({
state: 'camera.one',
}),
});
const api = createCardAPI();
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([{ cameraID: 'camera.one' }]),
);
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
remote_control: {
entities: {
camera: 'input_select.camera',
camera_priority: 'card',
},
},
}),
);
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
camera: 'camera.one',
view: 'live',
}),
);
setRemoteControlEntityFromConfig(api);
// Get the camera sync callback (automation index 1)
const cameraSyncAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
.calls[0][0][1].actions?.[0] as InternalCallbackActionConfig;
expect(cameraSyncAction).toBeTruthy();
expect(isAdvancedCameraCardCustomAction(cameraSyncAction)).toBeTruthy();
expect(cameraSyncAction.advanced_camera_card_action).toBe(
INTERNAL_CALLBACK_ACTION,
);
cameraSyncAction.callback(api);
// Should NOT call select_option since entity already shows camera.one
expect(hass.callService).not.toBeCalled();
});
it('should not select option when camera is undefined', () => {
const hass = createHASS({
'input_select.camera': createStateEntity({
state: 'camera.one',
}),
});
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
remote_control: {
entities: {
camera: 'input_select.camera',
camera_priority: 'card',
},
},
}),
);
vi.mocked(api.getViewManager().getView).mockReturnValue(null);
setRemoteControlEntityFromConfig(api);
// Get the camera sync callback (automation index 1)
const cameraSyncAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
.calls[0][0][1].actions?.[0] as InternalCallbackActionConfig;
expect(cameraSyncAction).toBeTruthy();
expect(isAdvancedCameraCardCustomAction(cameraSyncAction)).toBeTruthy();
expect(cameraSyncAction.advanced_camera_card_action).toBe(
INTERNAL_CALLBACK_ACTION,
);
cameraSyncAction.callback(api);
// Should NOT call select_option since camera is undefined
expect(hass.callService).not.toBeCalled();
});
it('should not select option when view exists but has no camera', () => {
const hass = createHASS({
'input_select.camera': createStateEntity({
state: 'camera.one',
}),
});
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
remote_control: {
entities: {
camera: 'input_select.camera',
camera_priority: 'card',
},
},
}),
);
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
camera: null,
view: 'live',
}),
);
setRemoteControlEntityFromConfig(api);
// Test the 'camera' condition callback (automation index 1)
const cameraSyncAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
.calls[0][0][1].actions?.[0] as InternalCallbackActionConfig;
cameraSyncAction.callback(api);
expect(hass.callService).not.toBeCalled();
// Also test the 'initialized' condition callback (automation index 2)
const initializedSyncAction = vi.mocked(api.getAutomationsManager().addAutomations)
.mock.calls[0][0][2].actions?.[0] as InternalCallbackActionConfig;
initializedSyncAction.callback(api);
expect(hass.callService).not.toBeCalled();
});
it('should not throw when hass is undefined', async () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
remote_control: {
entities: {
camera: 'input_select.camera',
camera_priority: 'card',
},
},
}),
);
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
camera: 'camera.two',
view: 'live',
}),
);
setRemoteControlEntityFromConfig(api);
const cameraSyncAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
.calls[0][0][1].actions?.[0] as InternalCallbackActionConfig;
expect(cameraSyncAction).toBeTruthy();
expect(isAdvancedCameraCardCustomAction(cameraSyncAction)).toBeTruthy();
expect(cameraSyncAction.advanced_camera_card_action).toBe(
INTERNAL_CALLBACK_ACTION,
);
// Should not throw and obviously not call service (as hass is null)
await cameraSyncAction.callback(api);
});
it('should select option when entity state is undefined', async () => {
const hass = createHASS({});
const api = createCardAPI();
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([{ cameraID: 'camera.two' }]),
);
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
remote_control: {
entities: {
camera: 'input_select.camera',
camera_priority: 'card',
},
},
}),
);
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
camera: 'camera.two',
view: 'live',
}),
);
setRemoteControlEntityFromConfig(api);
const cameraSyncAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
.calls[0][0][1].actions?.[0] as InternalCallbackActionConfig;
expect(cameraSyncAction).toBeTruthy();
expect(isAdvancedCameraCardCustomAction(cameraSyncAction)).toBeTruthy();
expect(cameraSyncAction.advanced_camera_card_action).toBe(
INTERNAL_CALLBACK_ACTION,
);
await cameraSyncAction.callback(api);
expect(hass.callService).toBeCalledWith(
'input_select',
'select_option',
{
option: 'camera.two',
},
{
entity_id: 'input_select.camera',
},
);
});
it('should select option on initialization with card priority', async () => {
const hass = createHASS({
'input_select.camera': createStateEntity({
state: 'camera.one',
}),
});
const api = createCardAPI();
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([{ cameraID: 'camera.two' }]),
);
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
remote_control: {
entities: {
camera: 'input_select.camera',
camera_priority: 'card',
},
},
}),
);
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
camera: 'camera.two',
view: 'live',
}),
);
setRemoteControlEntityFromConfig(api);
// Get the initialization callback (automation index 2)
const initAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
.calls[0][0][2].actions?.[0] as InternalCallbackActionConfig;
expect(initAction).toBeTruthy();
expect(isAdvancedCameraCardCustomAction(initAction)).toBeTruthy();
expect(initAction.advanced_camera_card_action).toBe(INTERNAL_CALLBACK_ACTION);
await initAction.callback(api);
expect(hass.callService).toBeCalledWith(
'input_select',
'select_option',
{
option: 'camera.two',
},
{
entity_id: 'input_select.camera',
},
);
});
});
describe('should wire the firing trigger into the camera action', () => {
it('should carry the new entity state to the camera-select action', () => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
remote_control: {
entities: {
camera: 'input_select.camera',
camera_priority: 'card',
},
},
}),
);
// Capture the automations the loader registers, then drive them through a
// real AutomationsManager so the state trigger actually fires. The
// registered action is a generated action whose generator reads the
// firing trigger's `to_state` to pick the camera, so this verifies that
// the generator and the exact triggering value are wired through together
// (the other tests only string-match the registered action shape, so a
// context mis-wire would silently break remote control).
setRemoteControlEntityFromConfig(api);
const automations = vi.mocked(api.getAutomationsManager().addAutomations).mock
.calls[0][0];
vi.mocked(api.getHASSManager().hasHASS).mockReturnValue(true);
vi.mocked(api.getInitializationManager().isInitializedMandatory).mockReturnValue(
true,
);
const stateManager = new ConditionStateManager();
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
new AutomationsManager(api).addAutomations(automations);
stateManager.setState({
hass: createHASS({
'input_select.camera': createStateEntity({ state: 'camera.one' }),
}),
});
const hass = createHASS({
'input_select.camera': createStateEntity({ state: 'camera.two' }),
});
stateManager.setState({ hass });
const captured = vi
.mocked(api.getActionsManager().executeActions)
.mock.calls.at(-1)?.[0];
assert(captured);
expect(captured.actions).toEqual([
{
action: 'fire-dom-event',
advanced_camera_card_action: '__GENERATED_ACTION__',
generator: expect.any(Function),
},
]);
expect(captured.triggerData?.platform).toBe('state');
expect(captured.triggerData?.entity_id).toBe('input_select.camera');
expect(captured.triggerData?.to_state?.state).toBe('camera.two');
// Running the generated action's generator with the firing trigger
// selects the camera named by the new state.
const generated = arrayify(captured.actions)[0];
assert(isGeneratedAction(generated));
expect(generated.generator({ api, triggerData: captured.triggerData })).toEqual({
action: 'fire-dom-event',
advanced_camera_card_action: 'camera_select',
camera: 'camera.two',
});
});
});
describe('should generate a camera-select action only when a camera is named', () => {
// Register the automations for a given camera priority, then pull the
// generator off the generated action on the named trigger's automation.
const getGenerator = (
api: CardController,
cameraPriority: 'card' | 'entity',
triggerName: string,
): ActionGenerator => {
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
remote_control: {
entities: {
camera: 'input_select.camera',
camera_priority: cameraPriority,
},
},
}),
);
setRemoteControlEntityFromConfig(api);
const automations = vi.mocked(api.getAutomationsManager().addAutomations).mock
.calls[0][0];
const automation = automations.find((candidate) =>
candidate.triggers.some((trigger) => trigger.trigger === triggerName),
);
assert(automation);
const action = arrayify(automation.actions)[0];
assert(isGeneratedAction(action));
return action.generator;
};
it('should select the camera named by the entity state on initialize', () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(
createHASS({
'input_select.camera': createStateEntity({ state: 'camera.kitchen' }),
}),
);
expect(getGenerator(api, 'entity', 'initialized')({ api })).toEqual(
createCameraAction('camera.kitchen'),
);
});
it('should generate nothing on initialize when the entity has no state', () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS({}));
expect(getGenerator(api, 'entity', 'initialized')({ api })).toBeNull();
});
it('should generate nothing for a state trigger that carries no to_state', () => {
const api = createCardAPI();
expect(
getGenerator(
api,
'card',
'state',
)({
api,
triggerData: { platform: 'state', entity_id: 'input_select.camera' },
}),
).toBeNull();
});
});
});