import { beforeEach, describe, expect, it, vi } from 'vitest'; import { mock } from 'vitest-mock-extended'; import { MediaPlayerManager } from '../../src/card-controller/media-player-manager.js'; import { MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA, MEDIA_PLAYER_SUPPORT_STOP, MEDIA_PLAYER_SUPPORT_TURN_OFF, } from '../../src/const'; import { EntityRegistryManager } from '../../src/ha/registry/entity/types.js'; import { HomeAssistant } from '../../src/ha/types.js'; import { ViewMediaType } from '../../src/view/item.js'; import { EntityRegistryManagerMock } from '../ha/registry/entity/mock.js'; import { createCameraConfig, createCameraManager, createCardAPI, createConfig, createHASS, createRegistryEntity, createStateEntity, createStore, TestViewMedia, } from '../test-utils.js'; const createHASSWithMediaPlayers = (): HomeAssistant => { const attributesSupported = { supported_features: MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA, }; const attributesUnsupported = { supported_features: 0, }; return createHASS({ 'media_player.ok1': createStateEntity({ entity_id: 'media_player.ok1', state: 'on', attributes: attributesSupported, }), 'media_player.ok2': createStateEntity({ entity_id: 'media_player.ok2', state: 'on', attributes: attributesSupported, }), 'media_player.ok3': createStateEntity({ entity_id: 'media_player.ok3', state: 'on', attributes: attributesSupported, }), 'media_player.unavailable': createStateEntity({ entity_id: 'media_player.sitting_room', state: 'unavailable', attributes: attributesSupported, }), 'media_player.unsupported': createStateEntity({ entity_id: 'media_player.sitting_room', state: 'on', attributes: attributesUnsupported, }), 'switch.unrelated': createStateEntity({ entity_id: 'switch.unrelated', state: 'on', }), }); }; describe('MediaPlayerManager', () => { beforeEach(() => { vi.restoreAllMocks(); }); describe('should initialize', () => { it('should initialize correctly', async () => { const entityRegistryManager = new EntityRegistryManagerMock([ createRegistryEntity({ entity_id: 'media_player.ok1', hidden_by: '', }), createRegistryEntity({ entity_id: 'media_player.ok2', hidden_by: 'user', }), ]); const api = createCardAPI(); vi.mocked(api.getHASSManager().getHASS).mockReturnValue( createHASSWithMediaPlayers(), ); vi.mocked(api.getConfigManager().getConfig).mockReturnValue( createConfig({ menu: { buttons: { media_player: { enabled: true, }, }, }, }), ); vi.mocked(api.getEntityRegistryManager).mockReturnValue(entityRegistryManager); const manager = new MediaPlayerManager(api); await manager.initialize(); expect(manager.getMediaPlayers()).toEqual([ 'media_player.ok1', 'media_player.ok3', ]); expect(manager.hasMediaPlayers()).toBeTruthy(); }); it('should handle without hass', async () => { const api = createCardAPI(); vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null); const manager = new MediaPlayerManager(api); await manager.initialize(); expect(manager.getMediaPlayers()).toEqual([]); expect(manager.hasMediaPlayers()).toBeFalsy(); }); it('should handle entity registry call failure', async () => { const spy = vi.spyOn(global.console, 'warn').mockImplementation(() => true); const entityRegistryManager = mock(); entityRegistryManager.getEntities.mockRejectedValue(new Error('message')); const api = createCardAPI(); vi.mocked(api.getHASSManager().getHASS).mockReturnValue( createHASSWithMediaPlayers(), ); vi.mocked(api.getConfigManager().getConfig).mockReturnValue( createConfig({ menu: { buttons: { media_player: { enabled: true, }, }, }, }), ); vi.mocked(api.getEntityRegistryManager).mockReturnValue(entityRegistryManager); const manager = new MediaPlayerManager(api); await manager.initialize(); expect(manager.getMediaPlayers()).toEqual([ 'media_player.ok1', 'media_player.ok2', 'media_player.ok3', ]); expect(manager.hasMediaPlayers()).toBeTruthy(); expect(spy).toBeCalled(); }); it('should reinitialize when there is a config change', async () => { const entityRegistryManager = new EntityRegistryManagerMock([ createRegistryEntity({ entity_id: 'media_player.ok1', hidden_by: '', }), createRegistryEntity({ entity_id: 'media_player.ok2', hidden_by: 'user', }), ]); const api = createCardAPI(); vi.mocked(api.getHASSManager().getHASS).mockReturnValue( createHASSWithMediaPlayers(), ); const config = createConfig({ menu: { buttons: { media_player: { enabled: true, }, }, }, }); vi.mocked(api.getConfigManager().getConfig).mockReturnValue(config); vi.mocked(api.getEntityRegistryManager).mockReturnValue(entityRegistryManager); const manager = new MediaPlayerManager(api); await manager.initializeIfNecessary(null); expect(manager.hasMediaPlayers()).toBeTruthy(); await manager.initializeIfNecessary(config); expect(manager.hasMediaPlayers()).toBeTruthy(); }); }); describe('should stop', () => { it('should call media_stop when device supports it', async () => { const api = createCardAPI(); vi.mocked(api.getHASSManager().getHASS).mockReturnValue( createHASS({ 'media_player.foo': createStateEntity({ entity_id: 'media_player.foo', attributes: { supported_features: MEDIA_PLAYER_SUPPORT_STOP, }, }), }), ); const manager = new MediaPlayerManager(api); await manager.stop('media_player.foo'); expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith( 'media_player', 'media_stop', { entity_id: 'media_player.foo', }, ); }); it('should call turn_off when device does supports it', async () => { const api = createCardAPI(); vi.mocked(api.getHASSManager().getHASS).mockReturnValue( createHASS({ 'media_player.foo': createStateEntity({ entity_id: 'media_player.foo', attributes: { supported_features: MEDIA_PLAYER_SUPPORT_TURN_OFF, }, }), }), ); const manager = new MediaPlayerManager(api); await manager.stop('media_player.foo'); expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith( 'media_player', 'turn_off', { entity_id: 'media_player.foo', }, ); }); it('should do nothing without some supported feature', async () => { const api = createCardAPI(); vi.mocked(api.getHASSManager().getHASS).mockReturnValue( createHASS({ 'media_player.foo': createStateEntity({ entity_id: 'media_player.foo', }), }), ); const manager = new MediaPlayerManager(api); await manager.stop('media_player.foo'); expect(api.getHASSManager().getHASS()?.callService).not.toBeCalled(); }); it('should do nothing without hass state', async () => { const api = createCardAPI(); vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); const manager = new MediaPlayerManager(api); await manager.stop('media_player.foo'); expect(api.getHASSManager().getHASS()?.callService).not.toBeCalled(); }); }); describe('should play', () => { describe('live', () => { it('should handle without camera config', async () => { const api = createCardAPI(); vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager()); vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); const manager = new MediaPlayerManager(api); await manager.playLive('media_player.foo', 'camera'); expect(api.getHASSManager().getHASS()?.callService).not.toBeCalled(); }); describe('using standard method', () => { it('should succeed', async () => { const api = createCardAPI(); vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager()); vi.mocked(api.getCameraManager().getStore).mockReturnValue( createStore([ { cameraID: 'camera.foo', config: createCameraConfig({ camera_entity: 'camera.foo', }), }, ]), ); vi.mocked(api.getCameraManager().getCameraMetadata).mockReturnValue({ title: 'camera title', icon: { icon: 'icon' }, }); vi.mocked(api.getHASSManager().getHASS).mockReturnValue( createHASS({ 'camera.foo': createStateEntity({ attributes: { entity_picture: 'http://thumbnail', }, }), }), ); const manager = new MediaPlayerManager(api); await manager.playLive('media_player.foo', 'camera.foo'); expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith( 'media_player', 'play_media', { entity_id: 'media_player.foo', media_content_id: 'media-source://camera/camera.foo', media_content_type: 'application/vnd.apple.mpegurl', extra: { title: 'camera title', thumb: 'http://thumbnail', }, }, ); }); it('should handle without camera_entity', async () => { const api = createCardAPI(); vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager()); vi.mocked(api.getCameraManager().getStore).mockReturnValue( createStore([ { cameraID: 'camera.foo', config: createCameraConfig(), }, ]), ); vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); const manager = new MediaPlayerManager(api); await manager.playLive('media_player.foo', 'camera.foo'); expect(api.getHASSManager().getHASS()?.callService).not.toBeCalled(); }); it('should handle without title and thumbnail', async () => { const api = createCardAPI(); vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager()); vi.mocked(api.getCameraManager().getStore).mockReturnValue( createStore([ { cameraID: 'camera.foo', config: createCameraConfig({ camera_entity: 'camera.foo', }), }, ]), ); vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); const manager = new MediaPlayerManager(api); await manager.playLive('media_player.foo', 'camera.foo'); expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith( 'media_player', 'play_media', { entity_id: 'media_player.foo', media_content_id: 'media-source://camera/camera.foo', media_content_type: 'application/vnd.apple.mpegurl', extra: {}, }, ); }); }); describe('using dashboard method', () => { it('should succeed', async () => { const api = createCardAPI(); vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager()); vi.mocked(api.getCameraManager().getStore).mockReturnValue( createStore([ { cameraID: 'camera.foo', config: createCameraConfig({ camera_entity: 'camera.foo', cast: { method: 'dashboard', dashboard: { dashboard_path: 'dashboard_path', view_path: 'view_path', }, }, }), }, ]), ); vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); const manager = new MediaPlayerManager(api); await manager.playLive('media_player.foo', 'camera.foo'); expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith( 'cast', 'show_lovelace_view', { entity_id: 'media_player.foo', dashboard_path: 'dashboard_path', view_path: 'view_path', }, ); }); it('should handle without hass', async () => { const api = createCardAPI(); vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager()); vi.mocked(api.getCameraManager().getStore).mockReturnValue( createStore([ { cameraID: 'camera.foo', config: createCameraConfig({ camera_entity: 'camera.foo', cast: { method: 'dashboard', dashboard: { dashboard_path: 'dashboard_path', view_path: 'view_path', }, }, }), }, ]), ); vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null); const manager = new MediaPlayerManager(api); await manager.playLive('media_player.foo', 'camera.foo'); // No actual test can be performed here as nothing observable happens. // This test serves only as code-coverage long-tail. }); it('should handle without dashboard_path or view_path', async () => { const api = createCardAPI(); vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager()); // Bypass schema validation -- the code has a runtime guard // (TypeScript narrowing) for the case where dashboard config is // present but dashboard_path / view_path are missing. const configWithNoDashboardPaths = createCameraConfig({ camera_entity: 'camera.foo', }); (configWithNoDashboardPaths as Record).cast = { method: 'dashboard', dashboard: { dashboard_path: undefined, view_path: undefined }, }; vi.mocked(api.getCameraManager().getStore).mockReturnValue( createStore([ { cameraID: 'camera.foo', config: configWithNoDashboardPaths, }, ]), ); vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); const manager = new MediaPlayerManager(api); await manager.playLive('media_player.foo', 'camera.foo'); expect(api.getHASSManager().getHASS()?.callService).not.toBeCalled(); }); }); }); describe('media', () => { describe('successfully with', () => { it.each([ [ViewMediaType.Clip, 'video' as const], [ViewMediaType.Snapshot, 'image' as const], ])('%s', async (mediaType: ViewMediaType, contentType: 'video' | 'image') => { const media = new TestViewMedia({ title: 'media title', thumbnail: 'http://thumbnail', contentID: 'media-source://contentid', mediaType, }); const api = createCardAPI(); vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); const manager = new MediaPlayerManager(api); await manager.playMedia('media_player.foo', media); expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith( 'media_player', 'play_media', { entity_id: 'media_player.foo', media_content_id: 'media-source://contentid', media_content_type: contentType, extra: { title: 'media title', thumb: 'http://thumbnail', }, }, ); }); }); it('should handle without hass', async () => { const api = createCardAPI(); vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null); const manager = new MediaPlayerManager(api); const media = new TestViewMedia(); await manager.playMedia('media_player.foo', media); // No actual test can be performed here as nothing observable happens. // This test serves only as code-coverage long-tail. }); }); }); });