import { describe, expect, it } from 'vitest'; import { getHassDifferences } from '../../src/ha/get-hass-differences'; import { FakeHASS } from './fake-hass'; const CAMERA = 'camera.office'; const SWITCH = 'input_boolean.zoom'; const createFakeHASS = (): FakeHASS => new FakeHASS({ entities: { [CAMERA]: { state: 'idle' }, [SWITCH]: { state: 'off' } }, registry: { [CAMERA]: { device_id: 'device-1' } }, }); // @vitest-environment jsdom describe('FakeHASS', () => { describe('identity', () => { it('should hand out a new object every time it is renewed', () => { const hass = createFakeHASS(); const before = hass.getHASS(); hass.renew(); expect(hass.getHASS()).not.toBe(before); // The states map itself is only rebuilt when a state changed, as Home // Assistant rebuilds it. A renewal carrying no state change hands back the // same map, so a card gating on the map's identity sees the truth. expect(hass.getHASS().states).toBe(before.states); }); it('should hand out a new states map when a state changed', () => { const hass = createFakeHASS(); const before = hass.getHASS(); hass.setState(SWITCH, 'on'); expect(hass.getHASS().states).not.toBe(before.states); }); it('should replace the state object only for the entity that changed', () => { const hass = createFakeHASS(); const before = hass.getHASS(); hass.setState(SWITCH, 'on'); const after = hass.getHASS(); expect(after.states[SWITCH]).not.toBe(before.states[SWITCH]); expect(after.states[CAMERA]).toBe(before.states[CAMERA]); }); // Updating immutably is what lets any Home Assistant consumer identify the // entities that changed by comparing references. `getHassDifferences` is // borrowed here as a ready-made reference comparison over a named set of // entities: the instrument, not the thing being specified. it('should report exactly the entities that changed under a reference comparison', () => { const hass = createFakeHASS(); const before = hass.getHASS(); hass.setState(SWITCH, 'on'); expect( getHassDifferences(hass.getHASS(), before, [CAMERA, SWITCH]).map( (difference) => difference.entityID, ), ).toEqual([SWITCH]); }); it('should report no differences when nothing changed', () => { const hass = createFakeHASS(); const before = hass.getHASS(); hass.renew(); expect(getHassDifferences(hass.getHASS(), before, [CAMERA, SWITCH])).toEqual([]); }); it('should report a removed entity as a difference', () => { const hass = createFakeHASS(); const before = hass.getHASS(); hass.removeState(SWITCH); expect(hass.getHASS().states[SWITCH]).toBeUndefined(); expect( getHassDifferences(hass.getHASS(), before, [SWITCH]).map( (difference) => difference.entityID, ), ).toEqual([SWITCH]); }); }); describe('websocket commands', () => { it('should answer a registered command', async () => { const hass = createFakeHASS(); await expect( hass.getHASS().callWS({ type: 'config/entity_registry/get', entity_id: CAMERA }), ).resolves.toEqual(expect.objectContaining({ entity_id: CAMERA })); }); it('should reject an unregistered command', async () => { const hass = createFakeHASS(); await expect( hass.getHASS().callWS({ type: 'camera/stream', entity_id: CAMERA }), ).rejects.toThrow('FakeHASS received an unregistered WS command: camera/stream'); }); it('should let a test replace a command handler', async () => { const hass = createFakeHASS(); hass.registerCommand('camera/stream', () => ({ url: '/stream.m3u8' })); await expect( hass.getHASS().callWS({ type: 'camera/stream', entity_id: CAMERA }), ).resolves.toEqual({ url: '/stream.m3u8' }); }); it('should answer a command sent through the connection from the same table', async () => { const hass = createFakeHASS(); hass.registerCommand('camera/stream', () => ({ url: '/stream.m3u8' })); await expect( hass.getHASS().connection.sendMessagePromise({ type: 'camera/stream' }), ).resolves.toEqual({ url: '/stream.m3u8' }); }); it('should reject an unregistered command sent through the connection', async () => { const hass = createFakeHASS(); await expect( hass.getHASS().connection.sendMessagePromise({ type: 'camera/stream' }), ).rejects.toThrow('FakeHASS received an unregistered WS command: camera/stream'); }); it('should record every command in order', async () => { const hass = createFakeHASS(); await hass.getHASS().callWS({ type: 'lovelace/resources' }); await hass .getHASS() .callWS({ type: 'config/entity_registry/get', entity_id: CAMERA }); expect(hass.getCommandLog().map((message) => message.type)).toEqual([ 'lovelace/resources', 'config/entity_registry/get', ]); }); }); describe('unimplemented methods', () => { it('should throw rather than quietly do nothing', () => { const hass = createFakeHASS().getHASS(); expect(() => hass.callService('camera', 'turn_on')).toThrow( 'FakeHASS does not implement callService', ); expect(() => hass.sendWS({ type: 'ping' })).toThrow( 'FakeHASS does not implement sendWS', ); }); }); });