1101 lines
38 KiB
TypeScript
1101 lines
38 KiB
TypeScript
import type { ViewContext } from 'view';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
import { mock } from 'vitest-mock-extended';
|
|
|
|
import type { CardController } from '../../../src/card-controller/controller';
|
|
import type { ViewFactory } from '../../../src/card-controller/view/factory';
|
|
import { MergeContextViewModifier } from '../../../src/card-controller/view/modifiers/merge-context';
|
|
import { SetQueryViewModifier } from '../../../src/card-controller/view/modifiers/set-query';
|
|
import type {
|
|
QueryExecutorOptions,
|
|
ViewModifier,
|
|
} from '../../../src/card-controller/view/types';
|
|
import { ViewManager } from '../../../src/card-controller/view/view-manager';
|
|
import type { ViewQueryExecutor } from '../../../src/card-controller/view/view-query-executor';
|
|
import type { 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 type { View } from '../../../src/view/view';
|
|
import { createCardAPI } from '../../test-utils';
|
|
import { createEventQuery, createView } from '../../view/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<ViewFactory>();
|
|
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).toHaveBeenCalledWith('camera');
|
|
expect(api.getCardElementManager().scrollReset).toHaveBeenCalled();
|
|
expect(api.getStyleManager().setExpandedMode).toHaveBeenCalled();
|
|
expect(api.getConditionStateManager()?.setState).toHaveBeenCalledWith({
|
|
view: 'live',
|
|
camera: 'camera',
|
|
displayMode: 'grid',
|
|
targetID: 'camera',
|
|
substreamID: undefined,
|
|
});
|
|
expect(api.getCardElementManager().update).toHaveBeenCalled();
|
|
});
|
|
|
|
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<ViewFactory>();
|
|
factory.getViewDefault.mockReturnValue(view);
|
|
|
|
const api = createInitializedCardAPI();
|
|
new ViewManager(api, { viewFactory: factory }).setViewDefault();
|
|
|
|
expect(api.getConditionStateManager()?.setState).toHaveBeenCalledWith(
|
|
expect.objectContaining({ substreamID: 'substream' }),
|
|
);
|
|
});
|
|
|
|
it('should set view with minor changes without scroll', () => {
|
|
const view_1 = createView({
|
|
view: 'live',
|
|
camera: 'camera',
|
|
});
|
|
const factory = mock<ViewFactory>();
|
|
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.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
it('should set view with merged context', () => {
|
|
const api = createInitializedCardAPI();
|
|
const factory = mock<ViewFactory>();
|
|
|
|
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 set view with modifiers', () => {
|
|
const api = createInitializedCardAPI();
|
|
const factory = mock<ViewFactory>();
|
|
|
|
const manager = new ViewManager(api, { viewFactory: factory });
|
|
const modifier = new MergeContextViewModifier({ timeline: {} });
|
|
|
|
// Applying modifiers with no existing view does nothing.
|
|
manager.setViewWithModifiers([modifier]);
|
|
expect(manager.getView()).toBeNull();
|
|
|
|
factory.getViewDefault.mockReturnValue(createView({ view: 'live', camera: 'camera' }));
|
|
manager.setViewDefault();
|
|
manager.setViewWithModifiers([modifier]);
|
|
|
|
expect(manager.getView()?.camera).toBe('camera');
|
|
expect(manager.getView()?.view).toBe('live');
|
|
expect(manager.getView()?.context).toEqual({ timeline: {} });
|
|
});
|
|
|
|
it('should return epoch', () => {
|
|
const factory = mock<ViewFactory>();
|
|
const manager = new ViewManager(createCardAPI(), { viewFactory: factory });
|
|
expect(manager.getEpoch()).toBeTruthy();
|
|
expect(manager.getEpoch().manager).toBe(manager);
|
|
});
|
|
|
|
it('should reset view', () => {
|
|
const factory = mock<ViewFactory>();
|
|
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<ViewFactory>();
|
|
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<ViewFactory>();
|
|
factory.getViewDefault.mockReturnValue(createView());
|
|
|
|
const manager = new ViewManager(createInitializedCardAPI(false), {
|
|
viewFactory: factory,
|
|
});
|
|
manager.setViewDefaultWithNewQuery();
|
|
|
|
expect(manager.getView()).toBeNull();
|
|
});
|
|
|
|
it('should report whether a default view can be set', () => {
|
|
expect(new ViewManager(createInitializedCardAPI()).canSetViewDefault()).toBe(true);
|
|
expect(new ViewManager(createInitializedCardAPI(false)).canSetViewDefault()).toBe(
|
|
false,
|
|
);
|
|
});
|
|
|
|
it('should set the diagnostics view without cameras being initialized', () => {
|
|
const view = createView({ view: 'diagnostics' });
|
|
const factory = mock<ViewFactory>();
|
|
factory.getViewByParameters.mockReturnValue(view);
|
|
|
|
const manager = new ViewManager(createInitializedCardAPI(false), {
|
|
viewFactory: factory,
|
|
});
|
|
manager.setViewByParameters({ params: { view: 'diagnostics' } });
|
|
|
|
expect(manager.getView()).toBe(view);
|
|
});
|
|
});
|
|
|
|
describe('should respect microphone navigation lock', () => {
|
|
it('should ignore generic view changes when microphone is locking navigation', () => {
|
|
const factory = mock<ViewFactory>();
|
|
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>();
|
|
viewFactory.getViewDefault.mockReturnValue(createView());
|
|
|
|
const viewQueryExecutor = mock<ViewQueryExecutor>();
|
|
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<ViewFactory>();
|
|
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>();
|
|
viewFactory.getViewDefault.mockReturnValue(createView());
|
|
|
|
const viewQueryExecutor = mock<ViewQueryExecutor>();
|
|
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<ViewFactory>();
|
|
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<ViewFactory>();
|
|
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>();
|
|
viewFactory.getViewDefault.mockReturnValue(createView());
|
|
|
|
const viewQueryExecutor = mock<ViewQueryExecutor>();
|
|
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>();
|
|
viewFactory.getViewByParameters.mockReturnValue(createView());
|
|
|
|
const viewQueryExecutor = mock<ViewQueryExecutor>();
|
|
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>();
|
|
viewFactory.getViewByParameters.mockReturnValue(createView());
|
|
|
|
const viewQueryExecutor = mock<ViewQueryExecutor>();
|
|
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>();
|
|
viewFactory.getViewByParameters.mockReturnValue(createView());
|
|
|
|
const viewQueryExecutor = mock<ViewQueryExecutor>();
|
|
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).toHaveBeenCalledWith(
|
|
expect.objectContaining({ params: expect.objectContaining({ query }) }),
|
|
);
|
|
});
|
|
|
|
it('should clear the query when an explicit null query is passed', async () => {
|
|
const viewFactory = mock<ViewFactory>();
|
|
viewFactory.getViewByParameters.mockReturnValue(createView());
|
|
|
|
const viewQueryExecutor = mock<ViewQueryExecutor>();
|
|
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).toHaveBeenCalledWith(
|
|
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<ViewFactory>();
|
|
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).toHaveBeenCalledWith(
|
|
expect.objectContaining({ baseView: null, failSafe: true }),
|
|
);
|
|
expect(api.getIssueManager().trigger).toHaveBeenCalledWith('view_incompatible', {
|
|
error,
|
|
});
|
|
expect(api.getNotificationManager().setNotification).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not retry with failSafe when existing view in sync calls', () => {
|
|
const viewFactory = mock<ViewFactory>();
|
|
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).toHaveBeenCalledTimes(2);
|
|
expect(api.getIssueManager().trigger).toHaveBeenCalledWith('view_incompatible', {
|
|
error,
|
|
});
|
|
});
|
|
|
|
it('should retry with failSafe when no existing view in async calls', async () => {
|
|
const viewFactory = mock<ViewFactory>();
|
|
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).toHaveBeenCalledWith(
|
|
expect.objectContaining({ baseView: null, failSafe: true }),
|
|
);
|
|
expect(api.getIssueManager().trigger).toHaveBeenCalledWith('view_incompatible', {
|
|
error,
|
|
});
|
|
expect(api.getNotificationManager().setNotification).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not retry with failSafe when existing view in async calls', async () => {
|
|
const viewFactory = mock<ViewFactory>();
|
|
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).toHaveBeenCalledTimes(2);
|
|
expect(api.getIssueManager().trigger).toHaveBeenCalledWith('view_incompatible', {
|
|
error,
|
|
});
|
|
});
|
|
|
|
it('should reset view_incompatible on successful view set', () => {
|
|
const viewFactory = mock<ViewFactory>();
|
|
viewFactory.getViewDefault.mockReturnValue(createView());
|
|
|
|
const api = createInitializedCardAPI();
|
|
const manager = new ViewManager(api, { viewFactory });
|
|
manager.setViewDefault();
|
|
|
|
expect(api.getIssueManager().reset).toHaveBeenCalledWith('view_incompatible');
|
|
});
|
|
|
|
it('should retain issues when the diagnostics view is requested', () => {
|
|
const viewFactory = mock<ViewFactory>();
|
|
viewFactory.getViewByParameters.mockReturnValue(createView({ view: 'diagnostics' }));
|
|
|
|
const api = createInitializedCardAPI();
|
|
const manager = new ViewManager(api, { viewFactory });
|
|
manager.setViewByParameters({ params: { view: 'diagnostics' } });
|
|
|
|
expect(manager.getView()?.is('diagnostics')).toBeTruthy();
|
|
expect(api.getIssueManager().reset).not.toHaveBeenCalledWith('view_incompatible');
|
|
expect(api.getIssueManager().reset).not.toHaveBeenCalledWith('media_query');
|
|
});
|
|
|
|
it('should return null when failSafe view factory also throws', () => {
|
|
const viewFactory = mock<ViewFactory>();
|
|
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).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('should handle viewQueryExecutor exceptions in async calls', async () => {
|
|
const error = new Error();
|
|
const viewFactory = mock<ViewFactory>();
|
|
viewFactory.getViewDefault.mockReturnValue(createView());
|
|
const viewQueryExecutor = mock<ViewQueryExecutor>();
|
|
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).toHaveBeenCalledWith(
|
|
'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>();
|
|
viewFactory.getViewByParameters.mockReturnValue(createView({ view: 'live' }));
|
|
|
|
const api = createInitializedCardAPI();
|
|
const manager = new ViewManager(api, { viewFactory });
|
|
manager.setViewByParameters();
|
|
|
|
expect(api.getIssueManager().reset).toHaveBeenCalledWith('media_query');
|
|
});
|
|
|
|
it('should tolerate the view being reset during a failing async query', async () => {
|
|
const error = new Error();
|
|
const viewFactory = mock<ViewFactory>();
|
|
viewFactory.getViewDefault.mockReturnValue(createView());
|
|
const viewQueryExecutor = mock<ViewQueryExecutor>();
|
|
|
|
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).toHaveBeenCalledWith(
|
|
'media_query',
|
|
expect.objectContaining({ error }),
|
|
);
|
|
});
|
|
|
|
it('should reset media_query at the start of a new async query', async () => {
|
|
const viewFactory = mock<ViewFactory>();
|
|
viewFactory.getViewDefault.mockReturnValue(createView());
|
|
const viewQueryExecutor = mock<ViewQueryExecutor>();
|
|
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).toHaveBeenCalledWith('media_query');
|
|
expect(
|
|
vi
|
|
.mocked(api.getIssueManager().reset)
|
|
.mock.calls.filter(([key]) => key === 'media_query').length,
|
|
).toBe(2);
|
|
});
|
|
|
|
it('should not reset media_query on the retry dispatch, keeping the error visible', async () => {
|
|
const error = new Error();
|
|
const viewFactory = mock<ViewFactory>();
|
|
viewFactory.getViewByParameters.mockReturnValue(createView());
|
|
const viewQueryExecutor = mock<ViewQueryExecutor>();
|
|
viewQueryExecutor.getNewQueryModifiers.mockRejectedValue(error);
|
|
|
|
const api = createInitializedCardAPI();
|
|
const manager = new ViewManager(api, {
|
|
viewFactory,
|
|
viewQueryExecutor,
|
|
});
|
|
|
|
await manager.setViewByParametersWithNewQuery({ intent: 'retry' });
|
|
|
|
// A retry re-runs the same failing query, so the existing error must not be
|
|
// cleared up front (it would blink away and back); the failure just
|
|
// re-triggers it.
|
|
expect(api.getIssueManager().reset).not.toHaveBeenCalledWith('media_query');
|
|
expect(api.getIssueManager().trigger).toHaveBeenCalledWith(
|
|
'media_query',
|
|
expect.objectContaining({ error }),
|
|
);
|
|
});
|
|
});
|
|
|
|
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<ViewFactory>();
|
|
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<ViewFactory>();
|
|
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<string, string> = new Map();
|
|
overrides_1.set('camera', 'camera-2');
|
|
|
|
const overrides_2: Map<string, string> = new Map();
|
|
overrides_2.set('camera', 'camera-3');
|
|
|
|
const factory = mock<ViewFactory>();
|
|
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<string, string> = new Map();
|
|
overrides_1.set('camera', 'camera-2');
|
|
|
|
const overrides_2: Map<string, string> = new Map();
|
|
overrides_2.set('camera', 'camera-3');
|
|
|
|
const factory = mock<ViewFactory>();
|
|
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<ViewFactory>();
|
|
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<ViewFactory>();
|
|
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>();
|
|
viewFactory.getViewDefault.mockReturnValue(view);
|
|
|
|
const api = createInitializedCardAPI();
|
|
const manager = new ViewManager(api, {
|
|
viewFactory: viewFactory,
|
|
});
|
|
|
|
expect(await manager.initialize()).toBe(true);
|
|
|
|
expect(manager.getView()).toBe(view);
|
|
});
|
|
|
|
it('should initialize with querystring', async () => {
|
|
const api = createCardAPI();
|
|
const factory = mock<ViewFactory>();
|
|
const manager = new ViewManager(api, { viewFactory: factory });
|
|
vi.mocked(api.getQueryStringManager().hasViewRelatedActionsToRun).mockReturnValue(
|
|
true,
|
|
);
|
|
|
|
expect(await manager.initialize()).toBe(true);
|
|
|
|
expect(manager.hasView()).toBeFalsy();
|
|
});
|
|
|
|
it('should report failure when no view could be set', async () => {
|
|
const viewFactory = mock<ViewFactory>();
|
|
viewFactory.getViewDefault.mockReturnValue(createView());
|
|
|
|
// Cameras not being initialized causes the default view set to decline to
|
|
// run, so initialization must report failure to allow a later retry.
|
|
const api = createInitializedCardAPI(false);
|
|
const manager = new ViewManager(api, {
|
|
viewFactory: viewFactory,
|
|
});
|
|
|
|
expect(await manager.initialize()).toBe(false);
|
|
|
|
expect(manager.hasView()).toBeFalsy();
|
|
});
|
|
});
|
|
|
|
describe('should apply async view modifications', () => {
|
|
it('should apply modifications successfully', async () => {
|
|
const viewFactory = mock<ViewFactory>();
|
|
viewFactory.getViewDefault.mockReturnValue(createView({ view: 'live' }));
|
|
|
|
const query = new UnifiedQuery();
|
|
const queryResults = new QueryResults();
|
|
|
|
const viewQueryExecutor = mock<ViewQueryExecutor>();
|
|
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).toHaveBeenCalledWith('media_query');
|
|
});
|
|
|
|
it('should not apply modifications if there is a major media change', async () => {
|
|
const viewFactory = mock<ViewFactory>();
|
|
viewFactory.getViewDefault.mockReturnValueOnce(createView({ view: 'live' }));
|
|
|
|
const query = new UnifiedQuery();
|
|
const queryResults = new QueryResults();
|
|
|
|
const viewQueryExecutor = mock<ViewQueryExecutor>();
|
|
|
|
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<ViewModifier[] | null> => {
|
|
// 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>();
|
|
viewFactory.getViewDefault.mockReturnValueOnce(createView({ view: 'live' }));
|
|
const viewQueryExecutor = mock<ViewQueryExecutor>();
|
|
|
|
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<ViewModifier[] | null> => {
|
|
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>();
|
|
viewFactory.getViewDefault.mockReturnValueOnce(createView({ view: 'live' }));
|
|
const viewQueryExecutor = mock<ViewQueryExecutor>();
|
|
|
|
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<ViewModifier[] | null> => {
|
|
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>();
|
|
viewFactory.getViewDefault.mockReturnValueOnce(createView({ view: 'live' }));
|
|
const viewQueryExecutor = mock<ViewQueryExecutor>();
|
|
|
|
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<ViewModifier[] | null> => {
|
|
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<boolean> => {
|
|
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>();
|
|
viewFactory.getViewDefault
|
|
.mockReturnValueOnce(startView)
|
|
.mockReturnValueOnce(targetView);
|
|
|
|
const viewQueryExecutor = mock<ViewQueryExecutor>();
|
|
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);
|
|
});
|
|
});
|
|
});
|