import { ViewContext } from 'view'; import { describe, expect, it, vi } from 'vitest'; import { mock } from 'vitest-mock-extended'; import { CardController } from '../../../src/card-controller/controller'; import { ViewFactory } from '../../../src/card-controller/view/factory'; import { SetQueryViewModifier } from '../../../src/card-controller/view/modifiers/set-query'; import { QueryExecutorOptions, ViewModifier, } from '../../../src/card-controller/view/types'; import { ViewManager } from '../../../src/card-controller/view/view-manager'; import { ViewQueryExecutor } from '../../../src/card-controller/view/view-query-executor'; import { AdvancedCameraCardView } from '../../../src/config/schema/common/const'; import { ViewMedia, ViewMediaType } from '../../../src/view/item'; import { QueryResults } from '../../../src/view/query-results'; import { UnifiedQuery } from '../../../src/view/unified-query'; import { View } from '../../../src/view/view'; import { createCardAPI, createEventQuery, createView } from '../../test-utils'; const createInitializedCardAPI = (initialized?: boolean): CardController => { const api = createCardAPI(); vi.mocked(api.getInitializationManager().isInitialized).mockReturnValue( initialized ?? true, ); return api; }; describe('should act correctly when view is set', () => { it('should set basic view', () => { const view = createView({ view: 'live', camera: 'camera', displayMode: 'grid', }); const factory = mock(); factory.getViewDefault.mockReturnValue(view); const api = createInitializedCardAPI(); const manager = new ViewManager(api, { viewFactory: factory }); manager.setViewDefault(); expect(manager.getView()).toBe(view); expect(manager.hasView()).toBeTruthy(); expect(api.getMediaLoadedInfoManager().setSelected).toBeCalledWith('camera'); expect(api.getCardElementManager().scrollReset).toBeCalled(); expect(api.getStyleManager().setExpandedMode).toBeCalled(); expect(api.getConditionStateManager()?.setState).toBeCalledWith({ view: 'live', camera: 'camera', displayMode: 'grid', targetID: 'camera', substreamID: undefined, }); expect(api.getCardElementManager().update).toBeCalled(); }); it('should set the engaged substream in condition state', () => { const view = createView({ view: 'live', camera: 'camera', context: { live: { overrides: new Map([['camera', 'substream']]) } }, }); const factory = mock(); factory.getViewDefault.mockReturnValue(view); const api = createInitializedCardAPI(); new ViewManager(api, { viewFactory: factory }).setViewDefault(); expect(api.getConditionStateManager()?.setState).toBeCalledWith( expect.objectContaining({ substreamID: 'substream' }), ); }); it('should set view with minor changes without scroll', () => { const view_1 = createView({ view: 'live', camera: 'camera', }); const factory = mock(); factory.getViewDefault.mockReturnValue(view_1); const api = createInitializedCardAPI(); const manager = new ViewManager(api, { viewFactory: factory }); manager.setViewDefault(); vi.mocked(api.getCardElementManager().scrollReset).mockClear(); const view_2 = createView({ view: 'live', camera: 'camera', displayMode: 'single', }); factory.getViewDefault.mockReturnValue(view_2); manager.setViewDefault(); expect(manager.getView()).toBe(view_2); // Same view name, so scrolling should not happen. expect(api.getCardElementManager().scrollReset).not.toBeCalled(); }); }); it('should set view with merged context', () => { const api = createInitializedCardAPI(); const factory = mock(); const manager = new ViewManager(api, { viewFactory: factory }); const context: ViewContext = { timeline: {} }; // Setting context with no existing view does nothing. manager.setViewWithMergedContext(context); expect(manager.getView()).toBeNull(); const view = createView({ view: 'live', camera: 'camera', }); factory.getViewDefault.mockReturnValue(view); manager.setViewDefault(); manager.setViewWithMergedContext(context); expect(manager.getView()?.camera).toBe('camera'); expect(manager.getView()?.view).toBe('live'); expect(manager.getView()?.context).toEqual(context); }); it('should return epoch', () => { const factory = mock(); const manager = new ViewManager(createCardAPI(), { viewFactory: factory }); expect(manager.getEpoch()).toBeTruthy(); expect(manager.getEpoch().manager).toBe(manager); }); it('should reset view', () => { const factory = mock(); const manager = new ViewManager(createInitializedCardAPI(), { viewFactory: factory }); manager.reset(); expect(manager.getView()).toBeNull(); expect(manager.hasView()).toBeFalsy(); factory.getViewDefault.mockReturnValue(createView()); manager.setViewDefault(); expect(manager.getView()).not.toBeNull(); expect(manager.hasView()).toBeTruthy(); manager.reset(); expect(manager.getView()).toBeNull(); expect(manager.hasView()).toBeFalsy(); }); describe('should not set view without cameras being initialized', () => { it('should ignore requests to set generic view without cameras being initialized', () => { const factory = mock(); factory.getViewDefault.mockReturnValue(createView()); const manager = new ViewManager(createInitializedCardAPI(false), { viewFactory: factory, }); manager.setViewDefault(); expect(manager.getView()).toBeNull(); }); it('should ignore requests to set view then modify without cameras being initialized', () => { const factory = mock(); factory.getViewDefault.mockReturnValue(createView()); const manager = new ViewManager(createInitializedCardAPI(false), { viewFactory: factory, }); manager.setViewDefaultWithNewQuery(); expect(manager.getView()).toBeNull(); }); }); describe('should respect microphone navigation lock', () => { it('should ignore generic view changes when microphone is locking navigation', () => { const factory = mock(); factory.getViewDefault.mockReturnValue(createView()); const api = createInitializedCardAPI(); vi.mocked(api.getLockManager().isLocked).mockReturnValue(true); const manager = new ViewManager(api, { viewFactory: factory }); manager.setViewDefault(); expect(manager.getView()).toBeNull(); }); it('should ignore async view changes when microphone is locking navigation', async () => { const viewFactory = mock(); viewFactory.getViewDefault.mockReturnValue(createView()); const viewQueryExecutor = mock(); viewQueryExecutor.getNewQueryModifiers.mockResolvedValue([]); const api = createInitializedCardAPI(); vi.mocked(api.getLockManager().isLocked).mockReturnValue(true); const manager = new ViewManager(api, { viewFactory: viewFactory, viewQueryExecutor: viewQueryExecutor, }); await manager.setViewDefaultWithNewQuery(); expect(manager.getView()).toBeNull(); }); it('should allow generic view changes when force is set', () => { const view = createView(); const factory = mock(); factory.getViewDefault.mockReturnValue(view); const api = createInitializedCardAPI(); vi.mocked(api.getLockManager().isLocked).mockReturnValue(true); const manager = new ViewManager(api, { viewFactory: factory }); manager.setViewDefault({ force: true }); expect(manager.getView()).toBe(view); }); it('should allow async view changes when force is set', async () => { const viewFactory = mock(); viewFactory.getViewDefault.mockReturnValue(createView()); const viewQueryExecutor = mock(); viewQueryExecutor.getNewQueryModifiers.mockResolvedValue([]); const api = createInitializedCardAPI(); vi.mocked(api.getLockManager().isLocked).mockReturnValue(true); const manager = new ViewManager(api, { viewFactory: viewFactory, viewQueryExecutor: viewQueryExecutor, }); await manager.setViewDefaultWithNewQuery({ force: true }); expect(manager.getView()?.view).toBe('live'); expect(manager.getView()?.camera).toBe('camera'); }); }); it('should set view default', () => { const factory = mock(); factory.getViewDefault.mockReturnValue(createView()); const manager = new ViewManager(createInitializedCardAPI(), { viewFactory: factory }); manager.setViewDefault(); expect(manager.getView()?.view).toBe('live'); expect(manager.getView()?.camera).toBe('camera'); }); it('should set view by parameters', () => { const factory = mock(); factory.getViewByParameters.mockReturnValue(createView()); const manager = new ViewManager(createInitializedCardAPI(), { viewFactory: factory }); manager.setViewByParameters(); expect(manager.getView()?.view).toBe('live'); expect(manager.getView()?.camera).toBe('camera'); }); it('should set view default with new query', async () => { const viewFactory = mock(); viewFactory.getViewDefault.mockReturnValue(createView()); const viewQueryExecutor = mock(); viewQueryExecutor.getNewQueryModifiers.mockResolvedValue([]); const manager = new ViewManager(createInitializedCardAPI(), { viewFactory: viewFactory, viewQueryExecutor: viewQueryExecutor, }); await manager.setViewDefaultWithNewQuery(); expect(manager.getView()?.view).toBe('live'); expect(manager.getView()?.camera).toBe('camera'); }); it('should set view by parameters with new query', async () => { const viewFactory = mock(); viewFactory.getViewByParameters.mockReturnValue(createView()); const viewQueryExecutor = mock(); viewQueryExecutor.getNewQueryModifiers.mockResolvedValue([]); const manager = new ViewManager(createInitializedCardAPI(), { viewFactory: viewFactory, viewQueryExecutor: viewQueryExecutor, }); await manager.setViewByParametersWithNewQuery(); expect(manager.getView()?.view).toBe('live'); expect(manager.getView()?.camera).toBe('camera'); }); it('should set view by parameters with existing query', async () => { const viewFactory = mock(); viewFactory.getViewByParameters.mockReturnValue(createView()); const viewQueryExecutor = mock(); viewQueryExecutor.getNewQueryModifiers.mockResolvedValue([]); const manager = new ViewManager(createInitializedCardAPI(), { viewFactory: viewFactory, viewQueryExecutor: viewQueryExecutor, }); await manager.setViewByParametersWithExistingQuery(); expect(manager.getView()?.view).toBe('live'); expect(manager.getView()?.camera).toBe('camera'); }); it('should set view by parameters with an explicitly provided existing query', async () => { const viewFactory = mock(); viewFactory.getViewByParameters.mockReturnValue(createView()); const viewQueryExecutor = mock(); viewQueryExecutor.getExistingQueryModifiers.mockResolvedValue([]); const manager = new ViewManager(createInitializedCardAPI(), { viewFactory: viewFactory, viewQueryExecutor: viewQueryExecutor, }); const query = new UnifiedQuery(); await manager.setViewByParametersWithExistingQuery({ params: { query } }); // An explicitly-passed query is used as-is rather than the base view's. expect(viewFactory.getViewByParameters).toBeCalledWith( expect.objectContaining({ params: expect.objectContaining({ query }) }), ); }); it('should clear the query when an explicit null query is passed', async () => { const viewFactory = mock(); viewFactory.getViewByParameters.mockReturnValue(createView()); const viewQueryExecutor = mock(); viewQueryExecutor.getExistingQueryModifiers.mockResolvedValue([]); const manager = new ViewManager(createInitializedCardAPI(), { viewFactory: viewFactory, viewQueryExecutor: viewQueryExecutor, }); await manager.setViewByParametersWithExistingQuery({ params: { query: null } }); // An explicit `null` is respected (clears the query), not overridden by the // base view's query. expect(viewFactory.getViewByParameters).toBeCalledWith( expect.objectContaining({ params: expect.objectContaining({ query: null }) }), ); }); describe('should handle exceptions', () => { it('should retry with failSafe when no existing view in sync calls', () => { const viewFactory = mock(); const failSafeView = createView(); const error = new Error('message'); viewFactory.getViewDefault .mockImplementationOnce(() => { throw error; }) .mockReturnValueOnce(failSafeView); const api = createInitializedCardAPI(); const manager = new ViewManager(api, { viewFactory: viewFactory }); manager.setViewDefault(); expect(manager.hasView()).toBeTruthy(); expect(manager.getView()).toBe(failSafeView); expect(viewFactory.getViewDefault).toBeCalledWith( expect.objectContaining({ baseView: null, failSafe: true }), ); expect(api.getIssueManager().trigger).toBeCalledWith('view_incompatible', { error, }); expect(api.getNotificationManager().setNotification).not.toBeCalled(); }); it('should not retry with failSafe when existing view in sync calls', () => { const viewFactory = mock(); const existingView = createView(); const error = new Error('message'); viewFactory.getViewDefault .mockReturnValueOnce(existingView) .mockImplementationOnce(() => { throw error; }); const api = createInitializedCardAPI(); const manager = new ViewManager(api, { viewFactory: viewFactory }); manager.setViewDefault(); manager.setViewDefault(); expect(manager.getView()).toBe(existingView); expect(viewFactory.getViewDefault).toBeCalledTimes(2); expect(api.getIssueManager().trigger).toBeCalledWith('view_incompatible', { error, }); }); it('should retry with failSafe when no existing view in async calls', async () => { const viewFactory = mock(); const failSafeView = createView(); const error = new Error('message'); viewFactory.getViewDefault .mockImplementationOnce(() => { throw error; }) .mockReturnValueOnce(failSafeView); const api = createInitializedCardAPI(); const manager = new ViewManager(api, { viewFactory: viewFactory }); await manager.setViewDefaultWithNewQuery(); expect(manager.hasView()).toBeTruthy(); expect(viewFactory.getViewDefault).toBeCalledWith( expect.objectContaining({ baseView: null, failSafe: true }), ); expect(api.getIssueManager().trigger).toBeCalledWith('view_incompatible', { error, }); expect(api.getNotificationManager().setNotification).not.toBeCalled(); }); it('should not retry with failSafe when existing view in async calls', async () => { const viewFactory = mock(); const existingView = createView(); const error = new Error('message'); viewFactory.getViewDefault .mockReturnValueOnce(existingView) .mockImplementationOnce(() => { throw error; }); const api = createInitializedCardAPI(); const manager = new ViewManager(api, { viewFactory: viewFactory }); manager.setViewDefault(); await manager.setViewDefaultWithNewQuery(); expect(manager.getView()).not.toBeNull(); expect(viewFactory.getViewDefault).toBeCalledTimes(2); expect(api.getIssueManager().trigger).toBeCalledWith('view_incompatible', { error, }); }); it('should reset view_incompatible on successful view set', () => { const viewFactory = mock(); viewFactory.getViewDefault.mockReturnValue(createView()); const api = createInitializedCardAPI(); const manager = new ViewManager(api, { viewFactory }); manager.setViewDefault(); expect(api.getIssueManager().reset).toBeCalledWith('view_incompatible'); }); it('should return null when failSafe view factory also throws', () => { const viewFactory = mock(); viewFactory.getViewDefault.mockImplementation(() => { throw new Error('message'); }); const api = createInitializedCardAPI(); const manager = new ViewManager(api, { viewFactory }); manager.setViewDefault(); expect(manager.hasView()).toBeFalsy(); expect(viewFactory.getViewDefault).toBeCalledTimes(2); }); it('should handle viewQueryExecutor exceptions in async calls', async () => { const error = new Error(); const viewFactory = mock(); viewFactory.getViewDefault.mockReturnValue(createView()); const viewQueryExecutor = mock(); viewQueryExecutor.getNewQueryModifiers.mockRejectedValue(error); const api = createInitializedCardAPI(); const manager = new ViewManager(api, { viewFactory: viewFactory, viewQueryExecutor: viewQueryExecutor, }); await manager.setViewDefaultWithNewQuery(); // The initial view will have been set. expect(manager.hasView()).toBeTruthy(); // But an error will also be generated. expect(api.getIssueManager().trigger).toBeCalledWith( 'media_query', expect.objectContaining({ error }), ); // The loading flag must be cleared on error -- otherwise gallery/viewer // components render "Awaiting media" indefinitely on top of the error // notification. expect(manager.getView()?.context?.loading?.query).toBeUndefined(); }); it('should reset media_query when navigating via the sync path', () => { const viewFactory = mock(); viewFactory.getViewByParameters.mockReturnValue(createView({ view: 'live' })); const api = createInitializedCardAPI(); const manager = new ViewManager(api, { viewFactory }); manager.setViewByParameters(); expect(api.getIssueManager().reset).toBeCalledWith('media_query'); }); it('should tolerate the view being reset during a failing async query', async () => { const error = new Error(); const viewFactory = mock(); viewFactory.getViewDefault.mockReturnValue(createView()); const viewQueryExecutor = mock(); const api = createInitializedCardAPI(); const manager = new ViewManager(api, { viewFactory: viewFactory, viewQueryExecutor: viewQueryExecutor, }); // Concurrent reset during the await -- clears `_view` before the rejection // is processed. The error path must not crash on the null view when // attempting to clear the loading flag. viewQueryExecutor.getNewQueryModifiers.mockImplementation(async () => { manager.reset(); throw error; }); await manager.setViewDefaultWithNewQuery(); expect(manager.getView()).toBeNull(); expect(api.getIssueManager().trigger).toBeCalledWith( 'media_query', expect.objectContaining({ error }), ); }); it('should reset media_query at the start of a new async query', async () => { const viewFactory = mock(); viewFactory.getViewDefault.mockReturnValue(createView()); const viewQueryExecutor = mock(); viewQueryExecutor.getNewQueryModifiers.mockResolvedValue(null); const api = createInitializedCardAPI(); const manager = new ViewManager(api, { viewFactory: viewFactory, viewQueryExecutor: viewQueryExecutor, }); await manager.setViewDefaultWithNewQuery(); // Reset is called twice: once at dispatch (supersedes any prior error) // and once after success (clears on confirmed success). expect(api.getIssueManager().reset).toBeCalledWith('media_query'); expect( vi .mocked(api.getIssueManager().reset) .mock.calls.filter(([key]) => key === 'media_query').length, ).toBe(2); }); }); describe('hasMajorMediaChange', () => { it('should consider undefined views as major', () => { const manager = new ViewManager(createCardAPI()); expect(manager.hasMajorMediaChange(undefined)).toBeFalsy(); expect(manager.hasMajorMediaChange(createView())).toBeTruthy(); expect(manager.hasMajorMediaChange()).toBeFalsy(); }); it('should consider view change as major', () => { const factory = mock(); factory.getViewDefault.mockReturnValue(createView({ view: 'live' })); const manager = new ViewManager(createInitializedCardAPI(), { viewFactory: factory, }); manager.setViewDefault(); expect(manager.hasMajorMediaChange(createView({ view: 'clips' }))).toBeTruthy(); }); it('should consider camera change as major', () => { const factory = mock(); factory.getViewDefault.mockReturnValue(createView({ camera: 'camera-1' })); const manager = new ViewManager(createInitializedCardAPI(), { viewFactory: factory, }); manager.setViewDefault(); expect(manager.hasMajorMediaChange(createView({ camera: 'camera-2' }))).toBeTruthy(); }); it('should consider live substream change as major in live view', () => { const overrides_1: Map = new Map(); overrides_1.set('camera', 'camera-2'); const overrides_2: Map = new Map(); overrides_2.set('camera', 'camera-3'); const factory = mock(); factory.getViewDefault.mockReturnValue( createView({ context: { live: { overrides: overrides_1 } } }), ); const manager = new ViewManager(createInitializedCardAPI(), { viewFactory: factory, }); manager.setViewDefault(); expect( manager.hasMajorMediaChange( createView({ context: { live: { overrides: overrides_2 } } }), ), ).toBeTruthy(); }); it('should not consider live substream change as major in other view', () => { const overrides_1: Map = new Map(); overrides_1.set('camera', 'camera-2'); const overrides_2: Map = new Map(); overrides_2.set('camera', 'camera-3'); const factory = mock(); factory.getViewDefault.mockReturnValue( createView({ view: 'clips', context: { live: { overrides: overrides_1 } } }), ); const manager = new ViewManager(createInitializedCardAPI(), { viewFactory: factory, }); manager.setViewDefault(); expect( manager.hasMajorMediaChange( createView({ view: 'clips', context: { live: { overrides: overrides_2 } } }), ), ).toBeFalsy(); }); it('should consider result change as major in other view', () => { const media = [ new ViewMedia(ViewMediaType.Clip, { cameraID: 'camera-1', }), new ViewMedia(ViewMediaType.Clip, { cameraID: 'camera-2', }), ]; const queryResults_1 = new QueryResults({ results: media, selectedIndex: 0 }); const queryResults_2 = new QueryResults({ results: media, selectedIndex: 1 }); const factory = mock(); factory.getViewDefault.mockReturnValue( createView({ view: 'media', queryResults: queryResults_1 }), ); const manager = new ViewManager(createInitializedCardAPI(), { viewFactory: factory, }); manager.setViewDefault(); expect( manager.hasMajorMediaChange( createView({ view: 'media', queryResults: queryResults_2 }), ), ).toBeTruthy(); }); it('should not consider selected result change as major in live view', () => { const media = [ new ViewMedia(ViewMediaType.Clip, { cameraID: 'camera-1', }), new ViewMedia(ViewMediaType.Clip, { cameraID: 'camera-2', }), ]; const queryResults_1 = new QueryResults({ results: media, selectedIndex: 0 }); const queryResults_2 = new QueryResults({ results: media, selectedIndex: 1 }); const factory = mock(); factory.getViewDefault.mockReturnValue(createView({ queryResults: queryResults_1 })); const manager = new ViewManager(createInitializedCardAPI(), { viewFactory: factory, }); manager.setViewDefault(); expect( manager.hasMajorMediaChange(createView({ queryResults: queryResults_2 })), ).toBeFalsy(); }); }); describe('should initialize', () => { it('should initialize without querystring', async () => { const view = createView({ view: 'live', camera: 'camera', }); const viewFactory = mock(); viewFactory.getViewDefault.mockReturnValue(view); const api = createInitializedCardAPI(); const manager = new ViewManager(api, { viewFactory: viewFactory, }); await manager.initialize(); expect(manager.getView()).toBe(view); }); it('should initialize with querystring', async () => { const api = createCardAPI(); const factory = mock(); const manager = new ViewManager(api, { viewFactory: factory }); vi.mocked(api.getQueryStringManager().hasViewRelatedActionsToRun).mockReturnValue( true, ); await manager.initialize(); expect(manager.hasView()).toBeFalsy(); }); }); describe('should apply async view modifications', () => { it('should apply modifications successfully', async () => { const viewFactory = mock(); viewFactory.getViewDefault.mockReturnValue(createView({ view: 'live' })); const query = new UnifiedQuery(); const queryResults = new QueryResults(); const viewQueryExecutor = mock(); viewQueryExecutor.getNewQueryModifiers.mockResolvedValue([ new SetQueryViewModifier({ query: query, queryResults: queryResults, }), ]); const api = createInitializedCardAPI(); const manager = new ViewManager(api, { viewFactory: viewFactory, viewQueryExecutor: viewQueryExecutor, }); await manager.setViewDefaultWithNewQuery(); expect(manager.getView()?.query).toBe(query); expect(manager.getView()?.queryResults).toBe(queryResults); expect(manager.getView()?.context?.loading?.query).toBeUndefined(); expect(api.getIssueManager().reset).toBeCalledWith('media_query'); }); it('should not apply modifications if there is a major media change', async () => { const viewFactory = mock(); viewFactory.getViewDefault.mockReturnValueOnce(createView({ view: 'live' })); const query = new UnifiedQuery(); const queryResults = new QueryResults(); const viewQueryExecutor = mock(); const manager = new ViewManager(createInitializedCardAPI(), { viewFactory: viewFactory, viewQueryExecutor: viewQueryExecutor, }); viewQueryExecutor.getNewQueryModifiers.mockImplementation( async ( // eslint-disable-next-line @typescript-eslint/no-unused-vars _view: View, // eslint-disable-next-line @typescript-eslint/no-unused-vars _executorqueryExecutorOptions?: QueryExecutorOptions, ): Promise => { // Simulate a major media change while the async operation is running. viewFactory.getViewDefault.mockReturnValueOnce( createView({ view: 'clips', context: { loading: { query: 1 } } }), ); manager.setViewDefault(); // Now return the modifiers (which should be ignored since there has // been a major change in the meantime). return [ new SetQueryViewModifier({ query: query, queryResults: queryResults, }), ]; }, ); await manager.setViewDefaultWithNewQuery(); // View set during the async operation should not be touched. expect(manager.getView()?.is('clips')).toBeTruthy(); expect(manager.getView()?.query).toBeNull(); expect(manager.getView()?.queryResults).toBeNull(); expect(manager.getView()?.context?.loading?.query).toBeUndefined(); }); describe('should manage loading state correctly', () => { it('should mark as not loading when with major media change', async () => { const viewFactory = mock(); viewFactory.getViewDefault.mockReturnValueOnce(createView({ view: 'live' })); const viewQueryExecutor = mock(); const manager = new ViewManager(createInitializedCardAPI(), { viewFactory: viewFactory, viewQueryExecutor: viewQueryExecutor, }); viewQueryExecutor.getNewQueryModifiers.mockImplementation( async ( // eslint-disable-next-line @typescript-eslint/no-unused-vars _view: View, // eslint-disable-next-line @typescript-eslint/no-unused-vars _executorqueryExecutorOptions?: QueryExecutorOptions, ): Promise => { expect(manager.getView()?.context?.loading?.query).not.toBeUndefined(); // Simulate a major media change while the async operation is running. viewFactory.getViewDefault.mockReturnValueOnce( createView({ view: 'clips', context: { loading: { query: 1 } } }), ); manager.setViewDefault(); // Now return the modifiers (which should be ignored since there has // been a major change in the meantime). return []; }, ); await manager.setViewDefaultWithNewQuery(); expect(manager.getView()?.context?.loading?.query).toBeUndefined(); }); it('should not change loading status if something else is being loaded', async () => { const viewFactory = mock(); viewFactory.getViewDefault.mockReturnValueOnce(createView({ view: 'live' })); const viewQueryExecutor = mock(); const manager = new ViewManager(createInitializedCardAPI(), { viewFactory: viewFactory, viewQueryExecutor: viewQueryExecutor, }); viewQueryExecutor.getNewQueryModifiers.mockImplementation( async ( // eslint-disable-next-line @typescript-eslint/no-unused-vars _view: View, // eslint-disable-next-line @typescript-eslint/no-unused-vars _executorqueryExecutorOptions?: QueryExecutorOptions, ): Promise => { expect(manager.getView()?.context?.loading?.query).not.toBeUndefined(); // Simulate a major media change while the async operation is running. viewFactory.getViewDefault.mockReturnValueOnce( createView({ view: 'clips', context: { loading: { query: 2 } } }), ); manager.setViewDefault(); // Now return the modifiers (which should be ignored since there has // been a major change in the meantime). return []; }, ); await manager.setViewDefaultWithNewQuery(); expect(manager.getView()?.context?.loading?.query).toBe(2); }); it('should not change loading status if it is unexpected', async () => { const viewFactory = mock(); viewFactory.getViewDefault.mockReturnValueOnce(createView({ view: 'live' })); const viewQueryExecutor = mock(); const manager = new ViewManager(createInitializedCardAPI(), { viewFactory: viewFactory, viewQueryExecutor: viewQueryExecutor, }); viewQueryExecutor.getNewQueryModifiers.mockImplementation( async ( // eslint-disable-next-line @typescript-eslint/no-unused-vars _view: View, // eslint-disable-next-line @typescript-eslint/no-unused-vars _executorqueryExecutorOptions?: QueryExecutorOptions, ): Promise => { expect(manager.getView()?.context?.loading?.query).not.toBeUndefined(); // Simulate another view update (without a major media change) viewFactory.getViewDefault.mockReturnValueOnce( createView({ context: { loading: { query: 100 } } }), ); manager.setViewDefault(); // Now return the modifiers (which should be ignored since there has // been a major change in the meantime). return []; }, ); await manager.setViewDefaultWithNewQuery(); expect(manager.getView()?.context?.loading?.query).toBe(100); }); }); describe('should adopt query results when useful', () => { const testAdoption = async ( originView: AdvancedCameraCardView | undefined, targetViewName: AdvancedCameraCardView, startViewName: AdvancedCameraCardView = 'media', ): Promise => { const startView = createView({ view: startViewName, camera: 'camera', query: new UnifiedQuery([createEventQuery('c1')]), queryResults: new QueryResults(), context: originView ? { gallery: { originView } } : undefined, }); const targetView = createView({ view: targetViewName, camera: 'camera', }); const viewFactory = mock(); viewFactory.getViewDefault .mockReturnValueOnce(startView) .mockReturnValueOnce(targetView); const viewQueryExecutor = mock(); viewQueryExecutor.getNewQueryModifiers.mockResolvedValue([]); const manager = new ViewManager(createInitializedCardAPI(), { viewFactory: viewFactory, viewQueryExecutor: viewQueryExecutor, }); manager.setViewDefault(); await manager.setViewDefaultWithNewQuery(); return vi.mocked(viewQueryExecutor.getNewQueryModifiers).mock.calls.length === 0; }; it('should return false when no originView context', async () => { expect(await testAdoption(undefined, 'clips')).toBe(false); }); it('should return true when originView matches target', async () => { expect(await testAdoption('clips', 'clips')).toBe(true); expect(await testAdoption('snapshots', 'snapshots')).toBe(true); expect(await testAdoption('recordings', 'recordings')).toBe(true); expect(await testAdoption('reviews', 'reviews')).toBe(true); }); it('should return false when originView does not match target', async () => { expect(await testAdoption('clips', 'snapshots')).toBe(false); expect(await testAdoption('clips', 'recordings')).toBe(false); expect(await testAdoption('snapshots', 'clips')).toBe(false); expect(await testAdoption('recordings', 'reviews')).toBe(false); }); it('should return false when start view is not viewer', async () => { expect(await testAdoption('clips', 'clips', 'live')).toBe(false); }); it('should return false when target view is not gallery', async () => { expect(await testAdoption('clips', 'live')).toBe(false); }); }); });