refactor: Centralize template render type assertion (#2538)
This commit is contained in:
committed by
dermotduffy
parent
a31816c168
commit
5572ec728e
@@ -187,15 +187,14 @@ export class ActionsManager implements ActionsExecutor {
|
|||||||
triggerData?: TriggerData,
|
triggerData?: TriggerData,
|
||||||
): ActionPrepareCallback {
|
): ActionPrepareCallback {
|
||||||
// Render against the state (incl. HASS) as it is *when the action runs* --
|
// Render against the state (incl. HASS) as it is *when the action runs* --
|
||||||
// fixed trigger context, fresh card/HASS state per step. The one cast lives
|
// fixed trigger context, fresh card/HASS state per step.
|
||||||
// here as renderRecursively returns `unknown`.
|
|
||||||
return <T>(value: T): T => {
|
return <T>(value: T): T => {
|
||||||
const hass = this._api.getHASSManager().getHASS();
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
return hass
|
return hass
|
||||||
? (renderer.renderRecursively(hass, value, {
|
? renderer.renderRecursivelyAsType(hass, value, {
|
||||||
conditionState: this._api.getConditionStateManager().getState(),
|
conditionState: this._api.getConditionStateManager().getState(),
|
||||||
triggerData,
|
triggerData,
|
||||||
}) as T)
|
})
|
||||||
: value;
|
: value;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,17 @@ export class TemplateRenderer {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Structure-preserving variant of `renderRecursively`: arrays, records, and
|
||||||
|
// primitives keep their shape (only string leaves are rendered), so the
|
||||||
|
// caller's type is asserted back unchanged. Callers whose template renders to
|
||||||
|
// a *different* type than its input (e.g. a string that yields a boolean)
|
||||||
|
// must use `renderRecursively` and narrow the `unknown` result at runtime.
|
||||||
|
public renderRecursivelyAsType = <T>(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
data: T,
|
||||||
|
options?: TemplateRenderOptions,
|
||||||
|
): T => this.renderRecursively(hass, data, options) as T;
|
||||||
|
|
||||||
private _generateTemplateContext(
|
private _generateTemplateContext(
|
||||||
options?: TemplateRenderOptions,
|
options?: TemplateRenderOptions,
|
||||||
): TemplateContext | undefined {
|
): TemplateContext | undefined {
|
||||||
|
|||||||
@@ -136,9 +136,13 @@ export class AdvancedCameraCardElementsCore extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const elements = this._templateRenderer.renderRecursively(this.hass, this.elements, {
|
const elements = this._templateRenderer.renderRecursivelyAsType(
|
||||||
|
this.hass,
|
||||||
|
this.elements,
|
||||||
|
{
|
||||||
conditionState: this.conditionStateManager?.getState(),
|
conditionState: this.conditionStateManager?.getState(),
|
||||||
}) as PictureElements | undefined;
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Condition state changes won't change the actual rendered config unless
|
// Condition state changes won't change the actual rendered config unless
|
||||||
// `elements` has a template, which is more likely does not. Avoid updating
|
// `elements` has a template, which is more likely does not. Avoid updating
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ describe('ActionsManager', () => {
|
|||||||
const action = createLogAction('{{ acc.camera }}');
|
const action = createLogAction('{{ acc.camera }}');
|
||||||
|
|
||||||
const templateRenderer = mock<TemplateRenderer>();
|
const templateRenderer = mock<TemplateRenderer>();
|
||||||
templateRenderer.renderRecursively.mockReturnValue(action);
|
templateRenderer.renderRecursivelyAsType.mockReturnValue(action);
|
||||||
|
|
||||||
const api = createAPI();
|
const api = createAPI();
|
||||||
const hass = createHASS();
|
const hass = createHASS();
|
||||||
@@ -360,7 +360,7 @@ describe('ActionsManager', () => {
|
|||||||
|
|
||||||
await manager.executeActions({ actions: action, config, triggerData });
|
await manager.executeActions({ actions: action, config, triggerData });
|
||||||
|
|
||||||
expect(templateRenderer.renderRecursively).toBeCalledWith(hass, action, {
|
expect(templateRenderer.renderRecursivelyAsType).toBeCalledWith(hass, action, {
|
||||||
conditionState,
|
conditionState,
|
||||||
triggerData,
|
triggerData,
|
||||||
});
|
});
|
||||||
@@ -377,7 +377,7 @@ describe('ActionsManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const templateRenderer = mock<TemplateRenderer>();
|
const templateRenderer = mock<TemplateRenderer>();
|
||||||
templateRenderer.renderRecursively.mockReturnValue(allowedAction);
|
templateRenderer.renderRecursivelyAsType.mockReturnValue(allowedAction);
|
||||||
|
|
||||||
const api = createAPI();
|
const api = createAPI();
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
@@ -399,7 +399,7 @@ describe('ActionsManager', () => {
|
|||||||
|
|
||||||
const templateRenderer = mock<TemplateRenderer>();
|
const templateRenderer = mock<TemplateRenderer>();
|
||||||
// Identity render -- assert on the render *inputs*, not a swapped output.
|
// Identity render -- assert on the render *inputs*, not a swapped output.
|
||||||
templateRenderer.renderRecursively.mockImplementation((_hass, data) => data);
|
templateRenderer.renderRecursivelyAsType.mockImplementation((_hass, data) => data);
|
||||||
|
|
||||||
const api = createAPI();
|
const api = createAPI();
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
@@ -422,13 +422,13 @@ describe('ActionsManager', () => {
|
|||||||
|
|
||||||
// Each action renders with the state as it is at its turn: the second
|
// Each action renders with the state as it is at its turn: the second
|
||||||
// sees the camera the first action set.
|
// sees the camera the first action set.
|
||||||
expect(templateRenderer.renderRecursively).toHaveBeenNthCalledWith(
|
expect(templateRenderer.renderRecursivelyAsType).toHaveBeenNthCalledWith(
|
||||||
1,
|
1,
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
expect.objectContaining({ conditionState: { camera: 'first' } }),
|
expect.objectContaining({ conditionState: { camera: 'first' } }),
|
||||||
);
|
);
|
||||||
expect(templateRenderer.renderRecursively).toHaveBeenNthCalledWith(
|
expect(templateRenderer.renderRecursivelyAsType).toHaveBeenNthCalledWith(
|
||||||
2,
|
2,
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
@@ -440,7 +440,7 @@ describe('ActionsManager', () => {
|
|||||||
const ran: string[] = [];
|
const ran: string[] = [];
|
||||||
|
|
||||||
const templateRenderer = mock<TemplateRenderer>();
|
const templateRenderer = mock<TemplateRenderer>();
|
||||||
templateRenderer.renderRecursively.mockImplementation((_hass, data) => data);
|
templateRenderer.renderRecursivelyAsType.mockImplementation((_hass, data) => data);
|
||||||
|
|
||||||
const api = createAPI();
|
const api = createAPI();
|
||||||
// No HASS for the first action's render; HASS thereafter.
|
// No HASS for the first action's render; HASS thereafter.
|
||||||
@@ -464,14 +464,14 @@ describe('ActionsManager', () => {
|
|||||||
// Both actions ran; only the second was rendered -- the first saw no
|
// Both actions ran; only the second was rendered -- the first saw no
|
||||||
// HASS, so HASS is read per action rather than captured once.
|
// HASS, so HASS is read per action rather than captured once.
|
||||||
expect(ran).toEqual(['one', 'two']);
|
expect(ran).toEqual(['one', 'two']);
|
||||||
expect(templateRenderer.renderRecursively).toBeCalledTimes(1);
|
expect(templateRenderer.renderRecursivelyAsType).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should abort the remaining actions when one fails to render', async () => {
|
it('should abort the remaining actions when one fails to render', async () => {
|
||||||
const ran: string[] = [];
|
const ran: string[] = [];
|
||||||
|
|
||||||
const templateRenderer = mock<TemplateRenderer>();
|
const templateRenderer = mock<TemplateRenderer>();
|
||||||
templateRenderer.renderRecursively
|
templateRenderer.renderRecursivelyAsType
|
||||||
.mockImplementationOnce((_hass, data) => data)
|
.mockImplementationOnce((_hass, data) => data)
|
||||||
.mockImplementationOnce(() => {
|
.mockImplementationOnce(() => {
|
||||||
throw new Error('bad template');
|
throw new Error('bad template');
|
||||||
|
|||||||
@@ -178,4 +178,18 @@ describe('TemplateRenderer', () => {
|
|||||||
expect(result).toBe('Value:');
|
expect(result).toBe('Value:');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('renderRecursivelyAsType', () => {
|
||||||
|
it('should render in place while preserving the input structure', () => {
|
||||||
|
const renderer = new TemplateRenderer();
|
||||||
|
const hass = createHASS();
|
||||||
|
|
||||||
|
const result = renderer.renderRecursivelyAsType(
|
||||||
|
hass,
|
||||||
|
{ camera: '{{ acc.camera }}', static: 'value' },
|
||||||
|
{ conditionState: { camera: 'camera.office' } },
|
||||||
|
);
|
||||||
|
expect(result).toEqual({ camera: 'camera.office', static: 'value' });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user