import { afterEach, describe, expect, it, vi } from 'vitest'; import { EntityRegistryManagerLive } from '../../../../src/ha/registry/entity'; import { EntityCache } from '../../../../src/ha/registry/entity/types'; import { AdvancedCameraCardError } from '../../../../src/types'; import { createHASS, createRegistryEntity } from '../../../test-utils.js'; vi.spyOn(global.console, 'warn').mockImplementation(() => true); describe('EntityRegistryManager', () => { afterEach(() => { vi.clearAllMocks(); }); describe('getEntity', () => { it('should not fetch when cached', async () => { const cache = new EntityCache(); const testEntity = createRegistryEntity({ entity_id: 'test' }); cache.set('test', testEntity); const hass = createHASS(); const manager = new EntityRegistryManagerLive(cache); expect(await manager.getEntity(hass, 'test')).toEqual(testEntity); expect(hass.callWS).not.toHaveBeenCalled(); }); it('should fetch and cache when not cached', async () => { const testEntity = createRegistryEntity({ entity_id: 'test' }); const hass = createHASS(); const manager = new EntityRegistryManagerLive(new EntityCache()); vi.mocked(hass.callWS).mockResolvedValueOnce(testEntity); expect(await manager.getEntity(hass, 'test')).toEqual(testEntity); expect(hass.callWS).toHaveBeenCalledTimes(1); expect(await manager.getEntity(hass, 'test')).toEqual(testEntity); expect(hass.callWS).toHaveBeenCalledTimes(1); }); it('should return null when entity does not exist', async () => { const hass = createHASS(); vi.mocked(hass.callWS).mockRejectedValueOnce(new Error('Not found')); const manager = new EntityRegistryManagerLive(new EntityCache()); expect(await manager.getEntity(hass, 'missing')).toBeNull(); expect(console.warn).toHaveBeenCalledWith( expect.any(AdvancedCameraCardError), expect.anything(), ); }); }); it('getEntities', async () => { const cachedEntity = createRegistryEntity({ entity_id: 'cached' }); const notCachedEntity = createRegistryEntity({ entity_id: 'not-cached' }); const cache = new EntityCache(); cache.set('cached', cachedEntity); const hass = createHASS(); const manager = new EntityRegistryManagerLive(cache); vi.mocked(hass.callWS).mockResolvedValueOnce(notCachedEntity); vi.mocked(hass.callWS).mockRejectedValueOnce(new Error('Not found')); expect(await manager.getEntities(hass, ['cached', 'not-cached', 'missing'])).toEqual( new Map([ ['cached', cachedEntity], ['not-cached', notCachedEntity], ]), ); expect(console.warn).toHaveBeenCalledWith( expect.any(AdvancedCameraCardError), expect.anything(), ); }); describe('fetchEntityList', async () => { it('should fetch entire entity list once', async () => { const hass = createHASS(); const entity = createRegistryEntity({ entity_id: 'cached' }); vi.mocked(hass.callWS).mockResolvedValueOnce([entity]); const manager = new EntityRegistryManagerLive(new EntityCache()); await manager.fetchEntityList(hass); expect(hass.callWS).toHaveBeenCalledTimes(1); expect(hass.callWS).toHaveBeenCalledWith({ type: 'config/entity_registry/list', }); expect(await manager.getEntity(hass, 'cached')).toEqual(entity); expect(hass.callWS).toHaveBeenCalledTimes(1); await manager.fetchEntityList(hass); expect(hass.callWS).toHaveBeenCalledTimes(1); }); it('should fetch once for callers that arrive while a fetch is running', async () => { const hass = createHASS(); const entity = createRegistryEntity({ entity_id: 'cached' }); vi.mocked(hass.callWS).mockResolvedValueOnce([entity]); const manager = new EntityRegistryManagerLive(new EntityCache()); await Promise.all([manager.fetchEntityList(hass), manager.fetchEntityList(hass)]); expect(hass.callWS).toHaveBeenCalledTimes(1); expect(await manager.getEntity(hass, 'cached')).toEqual(entity); }); it('should use the first caller hass for callers that join a running fetch', async () => { const entity = createRegistryEntity({ entity_id: 'cached' }); const firstHASS = createHASS(); vi.mocked(firstHASS.callWS).mockResolvedValueOnce([entity]); // A later `hass` arrives mid-fetch (Home Assistant replaces the object on // every state update). The joining caller uses the running fetch, so its // own `hass` is never called. const laterHASS = createHASS(); const manager = new EntityRegistryManagerLive(new EntityCache()); await Promise.all([ manager.fetchEntityList(firstHASS), manager.fetchEntityList(laterHASS), ]); expect(firstHASS.callWS).toHaveBeenCalledTimes(1); expect(laterHASS.callWS).not.toHaveBeenCalled(); }); it('should log to console on error', async () => { const hass = createHASS(); vi.mocked(hass.callWS).mockRejectedValueOnce(new Error('Fetch error')); const manager = new EntityRegistryManagerLive(new EntityCache()); await manager.fetchEntityList(hass); expect(console.warn).toHaveBeenCalledWith( expect.any(AdvancedCameraCardError), expect.anything(), ); }); it('should fetch again after a failure', async () => { const hass = createHASS(); const entity = createRegistryEntity({ entity_id: 'cached' }); vi.mocked(hass.callWS) .mockRejectedValueOnce(new Error('Fetch error')) .mockResolvedValueOnce([entity]); const manager = new EntityRegistryManagerLive(new EntityCache()); await manager.fetchEntityList(hass); await manager.fetchEntityList(hass); expect(hass.callWS).toHaveBeenCalledTimes(2); expect(await manager.getEntity(hass, 'cached')).toEqual(entity); }); }); it('getMatchingEntities', async () => { const matchingEntity = createRegistryEntity({ entity_id: 'matching' }); const notMatchingEntity = createRegistryEntity({ entity_id: 'not-matching' }); const hass = createHASS(); vi.mocked(hass.callWS).mockResolvedValueOnce([matchingEntity, notMatchingEntity]); const manager = new EntityRegistryManagerLive(new EntityCache()); expect( await manager.getMatchingEntities( hass, (entity) => entity.entity_id == 'matching', ), ).toEqual([matchingEntity]); }); });