fix: Expose gallery and media views as media type agnostic (#2335)

This commit is contained in:
Dermot Duffy
2026-02-08 08:04:11 -08:00
committed by GitHub
parent 0a099b95f7
commit 21f9469784
22 changed files with 363 additions and 238 deletions
@@ -568,48 +568,54 @@ describe('ViewQueryExecutor', () => {
});
});
describe('with a media view', () => {
it('should set query and queryResults for events', async () => {
const api = createPopulatedAPI();
const cameraManager = api.getCameraManager();
if (cameraManager) {
vi.mocked(cameraManager.getCameraCapabilities).mockReturnValue(
new Capabilities({ clips: true }),
);
}
describe('with a media or gallery view', () => {
it.each(['media' as const, 'gallery' as const])(
'should set query and queryResults for events with %s view',
async (viewName) => {
const api = createPopulatedAPI();
const cameraManager = api.getCameraManager();
if (cameraManager) {
vi.mocked(cameraManager.getCameraCapabilities).mockReturnValue(
new Capabilities({ clips: true }),
);
}
const viewQueryExecutor = new ViewQueryExecutor(api);
const view = new View({
view: 'media',
camera: 'camera.office',
});
const viewQueryExecutor = new ViewQueryExecutor(api);
const view = new View({
view: viewName,
camera: 'camera.office',
});
const modifiers = await viewQueryExecutor.getNewQueryModifiers(view);
applyViewModifiers(view, modifiers);
const modifiers = await viewQueryExecutor.getNewQueryModifiers(view);
applyViewModifiers(view, modifiers);
expect(view.query).toBeInstanceOf(UnifiedQuery);
const queries = view.query?.getMediaQueries({ type: QueryType.Event });
expect(queries?.length).toBeGreaterThan(0);
expect(queries?.[0].cameraIDs.has('camera.office')).toBe(true);
expect(view.queryResults).toBeDefined();
});
expect(view.query).toBeInstanceOf(UnifiedQuery);
const queries = view.query?.getMediaQueries({ type: QueryType.Event });
expect(queries?.length).toBeGreaterThan(0);
expect(queries?.[0].cameraIDs.has('camera.office')).toBe(true);
expect(view.queryResults).toBeDefined();
},
);
it('should query all cameras in media view grid mode', async () => {
const api = createPopulatedAPI();
const viewQueryExecutor = new ViewQueryExecutor(api);
const view = createView({
view: 'media',
camera: 'camera.office',
displayMode: 'grid',
});
it.each(['media' as const, 'gallery' as const])(
'should query all cameras in %s view grid mode',
async (viewName) => {
const api = createPopulatedAPI();
const viewQueryExecutor = new ViewQueryExecutor(api);
const view = createView({
view: viewName,
camera: 'camera.office',
displayMode: 'grid',
});
const modifiers = await viewQueryExecutor.getNewQueryModifiers(view);
applyViewModifiers(view, modifiers);
const modifiers = await viewQueryExecutor.getNewQueryModifiers(view);
applyViewModifiers(view, modifiers);
expect(view.query).toBeInstanceOf(UnifiedQuery);
const allCameraIDs = view.query?.getAllCameraIDs();
expect(allCameraIDs?.size).toBeGreaterThan(0);
});
expect(view.query).toBeInstanceOf(UnifiedQuery);
const allCameraIDs = view.query?.getAllCameraIDs();
expect(allCameraIDs?.size).toBeGreaterThan(0);
},
);
});
describe('with an events-based view', () => {
@@ -561,9 +561,7 @@ describe('MenuButtonController', () => {
describe('should have clips menu button', () => {
it('when in clips view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockImplementation(
(view) => view !== 'reviews',
);
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
const buttons = calculateButtons(controller, {
view: createView({ view: 'clips' }),
viewManager: viewManager,
@@ -571,7 +569,7 @@ describe('MenuButtonController', () => {
expect(buttons).toContainEqual({
icon: 'mdi:filmstrip',
enabled: true,
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Clips gallery',
@@ -583,16 +581,14 @@ describe('MenuButtonController', () => {
it('when not in clips view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockImplementation(
(view) => view !== 'reviews',
);
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
expect(buttons).toContainEqual({
icon: 'mdi:filmstrip',
enabled: true,
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Clips gallery',
@@ -613,30 +609,6 @@ describe('MenuButtonController', () => {
expect.arrayContaining([expect.objectContaining({ title: 'Clips gallery' })]),
);
});
it('should be hidden when reviews are supported', () => {
vi.mocked(isViewSupportedByCamera).mockImplementation(
(view) => view === 'clips' || view === 'reviews',
);
const buttons = calculateButtons(controller);
expect(buttons).not.toEqual(
expect.arrayContaining([expect.objectContaining({ title: 'Clips gallery' })]),
);
});
it('should be shown when reviews are supported but button is explicitly enabled', () => {
vi.mocked(isViewSupportedByCamera).mockImplementation(
(view) => view === 'clips' || view === 'reviews',
);
const buttons = calculateButtons(controller, {
config: createConfig({ menu: { buttons: { clips: { enabled: true } } } }),
});
expect(buttons).toContainEqual(
expect.objectContaining({ title: 'Clips gallery', enabled: true }),
);
});
});
describe('should have snapshots menu button', () => {
@@ -652,7 +624,7 @@ describe('MenuButtonController', () => {
expect(buttons).toContainEqual({
icon: 'mdi:camera',
enabled: true,
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Snapshots gallery',
@@ -670,16 +642,14 @@ describe('MenuButtonController', () => {
it('when not in snapshots view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockImplementation(
(view) => view !== 'reviews',
);
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
expect(buttons).toContainEqual({
icon: 'mdi:camera',
enabled: true,
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Snapshots gallery',
@@ -708,32 +678,6 @@ describe('MenuButtonController', () => {
]),
);
});
it('should be hidden when reviews are supported', () => {
vi.mocked(isViewSupportedByCamera).mockImplementation(
(view) => view === 'snapshots' || view === 'reviews',
);
const buttons = calculateButtons(controller);
expect(buttons).not.toEqual(
expect.arrayContaining([
expect.objectContaining({ title: 'Snapshots gallery' }),
]),
);
});
it('should be shown when reviews are supported but button is explicitly enabled', () => {
vi.mocked(isViewSupportedByCamera).mockImplementation(
(view) => view === 'snapshots' || view === 'reviews',
);
const buttons = calculateButtons(controller, {
config: createConfig({ menu: { buttons: { snapshots: { enabled: true } } } }),
});
expect(buttons).toContainEqual(
expect.objectContaining({ title: 'Snapshots gallery', enabled: true }),
);
});
});
describe('should have reviews menu button', () => {
@@ -746,8 +690,8 @@ describe('MenuButtonController', () => {
});
expect(buttons).toContainEqual({
icon: 'mdi:play-box-multiple',
enabled: true,
icon: 'mdi:play-box-edit-outline',
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Reviews gallery',
@@ -771,8 +715,8 @@ describe('MenuButtonController', () => {
});
expect(buttons).toContainEqual({
icon: 'mdi:play-box-multiple',
enabled: true,
icon: 'mdi:play-box-edit-outline',
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Reviews gallery',
@@ -801,6 +745,71 @@ describe('MenuButtonController', () => {
});
});
describe('should have gallery menu button', () => {
it('when in gallery view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
const buttons = calculateButtons(controller, {
view: createView({ view: 'gallery' }),
viewManager: viewManager,
});
expect(buttons).toContainEqual({
icon: 'mdi:play-box-multiple',
enabled: true,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Gallery',
style: { color: 'var(--advanced-camera-card-menu-button-active-color)' },
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'gallery',
},
hold_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'media',
},
});
});
it('when not in gallery view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockReturnValue(true);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
expect(buttons).toContainEqual({
icon: 'mdi:play-box-multiple',
enabled: true,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Gallery',
style: {},
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'gallery',
},
hold_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'media',
},
});
});
it('when not supported', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupportedByCamera).mockReturnValue(false);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
expect(buttons).not.toEqual(
expect.arrayContaining([expect.objectContaining({ title: 'Gallery' })]),
);
});
});
describe('should have recordings menu button', () => {
it('when in recordings view', () => {
const viewManager = mock<ViewManager>();
+5 -1
View File
@@ -231,6 +231,10 @@ describe('config defaults', () => {
enabled: true,
priority: 50,
},
gallery: {
enabled: true,
priority: 50,
},
image: {
enabled: false,
priority: 50,
@@ -277,7 +281,7 @@ describe('config defaults', () => {
priority: 50,
},
reviews: {
enabled: true,
enabled: false,
priority: 50,
},
set_review: {
+72 -57
View File
@@ -17,7 +17,7 @@ import {
describe('getCameraIDsForViewName', () => {
describe('views that are always supported', () => {
it.each([['diagnostics' as const], ['image' as const], ['media' as const]])(
it.each([['diagnostics' as const], ['image' as const]])(
'%s',
(viewName: AdvancedCameraCardView) => {
const cameraManager = createCameraManager();
@@ -51,6 +51,15 @@ describe('getCameraIDsForViewName', () => {
['timeline' as const, 'clips' as const],
['timeline' as const, 'recordings' as const],
['timeline' as const, 'snapshots' as const],
['timeline' as const, 'reviews' as const],
['media' as const, 'clips' as const],
['media' as const, 'recordings' as const],
['media' as const, 'snapshots' as const],
['media' as const, 'reviews' as const],
['gallery' as const, 'clips' as const],
['gallery' as const, 'recordings' as const],
['gallery' as const, 'snapshots' as const],
['gallery' as const, 'reviews' as const],
])('%s', (viewName: AdvancedCameraCardView, capabilityKey: CapabilityKey) => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
@@ -111,67 +120,73 @@ describe('getCameraIDsForViewName', () => {
describe('views that respect a folder', () => {
describe('should return cameras when a folder is present', () => {
it.each([['folder' as const], ['folders' as const], ['timeline' as const]])(
'%s',
(viewName: AdvancedCameraCardView) => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera-1',
config: createCameraConfig({ dependencies: { cameras: ['camera-2'] } }),
},
{
cameraID: 'camera-2',
},
]),
);
const foldersManager = mock<FoldersManager>();
vi.mocked(foldersManager.getFolder).mockReturnValue(createFolder());
it.each([
['folder' as const],
['folders' as const],
['timeline' as const],
['media' as const],
['gallery' as const],
])('%s', (viewName: AdvancedCameraCardView) => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera-1',
config: createCameraConfig({ dependencies: { cameras: ['camera-2'] } }),
},
{
cameraID: 'camera-2',
},
]),
);
const foldersManager = mock<FoldersManager>();
vi.mocked(foldersManager.getFolder).mockReturnValue(createFolder());
expect(
getCameraIDsForViewName(viewName, cameraManager, foldersManager),
).toEqual(new Set(['camera-1', 'camera-2']));
expect(
getCameraIDsForViewName(viewName, cameraManager, foldersManager, 'camera-1'),
).toEqual(new Set(['camera-1', 'camera-2']));
expect(
getCameraIDsForViewName(viewName, cameraManager, foldersManager, 'camera-2'),
).toEqual(new Set(['camera-1', 'camera-2']));
},
);
expect(getCameraIDsForViewName(viewName, cameraManager, foldersManager)).toEqual(
new Set(['camera-1', 'camera-2']),
);
expect(
getCameraIDsForViewName(viewName, cameraManager, foldersManager, 'camera-1'),
).toEqual(new Set(['camera-1', 'camera-2']));
expect(
getCameraIDsForViewName(viewName, cameraManager, foldersManager, 'camera-2'),
).toEqual(new Set(['camera-1', 'camera-2']));
});
});
describe('should not return cameras when a folder is absent', () => {
it.each([['folder' as const], ['folders' as const], ['timeline' as const]])(
'%s',
(viewName: AdvancedCameraCardView) => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera-1',
config: createCameraConfig({ dependencies: { cameras: ['camera-2'] } }),
},
{
cameraID: 'camera-2',
},
]),
);
const foldersManager = mock<FoldersManager>();
vi.mocked(foldersManager.getFolder).mockReturnValue(null);
it.each([
['folder' as const],
['folders' as const],
['timeline' as const],
['media' as const],
['gallery' as const],
])('%s', (viewName: AdvancedCameraCardView) => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera-1',
config: createCameraConfig({ dependencies: { cameras: ['camera-2'] } }),
},
{
cameraID: 'camera-2',
},
]),
);
const foldersManager = mock<FoldersManager>();
vi.mocked(foldersManager.getFolder).mockReturnValue(null);
expect(
getCameraIDsForViewName(viewName, cameraManager, foldersManager),
).toEqual(new Set());
expect(
getCameraIDsForViewName(viewName, cameraManager, foldersManager, 'camera-1'),
).toEqual(new Set());
expect(
getCameraIDsForViewName(viewName, cameraManager, foldersManager, 'camera-2'),
).toEqual(new Set());
},
);
expect(getCameraIDsForViewName(viewName, cameraManager, foldersManager)).toEqual(
new Set(),
);
expect(
getCameraIDsForViewName(viewName, cameraManager, foldersManager, 'camera-1'),
).toEqual(new Set());
expect(
getCameraIDsForViewName(viewName, cameraManager, foldersManager, 'camera-2'),
).toEqual(new Set());
});
});
});
});