From c9c7e93e19123c583a4afb48acbddcf59e90119c Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Tue, 9 Dec 2025 20:22:22 -0800 Subject: [PATCH] fix: Ignore URL actions for other card IDs (#2272) - Closes #2190 --- src/card-controller/query-string-manager.ts | 18 ++-- src/card-controller/types.ts | 1 + .../query-string-manager.test.ts | 99 ++++++++++++++++++- 3 files changed, 108 insertions(+), 10 deletions(-) diff --git a/src/card-controller/query-string-manager.ts b/src/card-controller/query-string-manager.ts index e3ab16d9..0783165e 100644 --- a/src/card-controller/query-string-manager.ts +++ b/src/card-controller/query-string-manager.ts @@ -96,6 +96,7 @@ export class QueryStringManager { protected _getActions(): AdvancedCameraCardCustomActionConfig[] { const params = new URLSearchParams(window.location.search); const actions: AdvancedCameraCardCustomActionConfig[] = []; + const configuredCardID = this._api.getConfigManager().getConfig()?.card_id; const actionRE = new RegExp( /^(advanced-camera-card|frigate-card)-action([.:](?\w+))?[.:](?\w+)/, ); @@ -107,14 +108,17 @@ export class QueryStringManager { const cardID: string | undefined = match.groups['cardID']; const actionName = match.groups['action']; + // Skip actions targeted at other cards. + if (cardID && cardID !== configuredCardID) { + continue; + } + let action: AdvancedCameraCardCustomActionConfig | null = null; switch (actionName) { case 'camera_select': case 'live_substream_select': if (value) { - action = createCameraAction(actionName, value, { - cardID: cardID, - }); + action = createCameraAction(actionName, value, { cardID }); } break; case 'camera_ui': @@ -122,9 +126,7 @@ export class QueryStringManager { case 'download': case 'expand': case 'menu_toggle': - action = createGeneralAction(actionName, { - cardID: cardID, - }); + action = createGeneralAction(actionName, { cardID }); break; case 'clip': case 'clips': @@ -136,9 +138,7 @@ export class QueryStringManager { case 'snapshot': case 'snapshots': case 'timeline': - action = createViewAction(actionName, { - cardID: cardID, - }); + action = createViewAction(actionName, { cardID }); break; default: console.warn( diff --git a/src/card-controller/types.ts b/src/card-controller/types.ts index b4eab40a..75f0cfda 100644 --- a/src/card-controller/types.ts +++ b/src/card-controller/types.ts @@ -254,6 +254,7 @@ export interface CardMicrophoneAPI { export interface CardQueryStringAPI { getActionsManager(): ActionsManager; getCardElementManager(): CardElementManager; + getConfigManager(): ConfigManager; getViewManager(): ViewManager; } diff --git a/tests/card-controller/query-string-manager.test.ts b/tests/card-controller/query-string-manager.test.ts index 0bcc61b8..3e9ff60b 100644 --- a/tests/card-controller/query-string-manager.test.ts +++ b/tests/card-controller/query-string-manager.test.ts @@ -1,8 +1,9 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import { mock } from 'vitest-mock-extended'; +import { CardController } from '../../src/card-controller/controller'; import { QueryStringManager } from '../../src/card-controller/query-string-manager'; import { SubstreamSelectViewModifier } from '../../src/card-controller/view/modifiers/substream-select'; -import { createCardAPI } from '../test-utils'; +import { createCardAPI, createConfig } from '../test-utils'; const setQueryString = (qs: string): void => { const location: Location = mock(); @@ -11,6 +12,14 @@ const setQueryString = (qs: string): void => { vi.spyOn(window, 'location', 'get').mockReturnValue(location); }; +const setCardID = (api: CardController, cardID: string): void => { + vi.mocked(api.getConfigManager().getConfig).mockReturnValue( + createConfig({ + card_id: cardID, + }), + ); +}; + // @vitest-environment jsdom describe('QueryStringManager', () => { afterEach(() => { @@ -45,6 +54,7 @@ describe('QueryStringManager', () => { ])('%s', async (viewName: string) => { setQueryString(`?advanced-camera-card-action.id.${viewName}=`); const api = createCardAPI(); + setCardID(api, 'id'); // View actions do not need the card to have been updated. vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(false); @@ -71,6 +81,7 @@ describe('QueryStringManager', () => { ])('%s', async (action: string) => { setQueryString(`?advanced-camera-card-action.id.${action}=`); const api = createCardAPI(); + setCardID(api, 'id'); vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true); const manager = new QueryStringManager(api); @@ -92,6 +103,7 @@ describe('QueryStringManager', () => { it('should execute view default action', async () => { setQueryString('?advanced-camera-card-action.id.default='); const api = createCardAPI(); + setCardID(api, 'id'); // View actions do not need the card to have been updated. vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(false); @@ -109,6 +121,7 @@ describe('QueryStringManager', () => { it('should execute camera_select action', async () => { setQueryString('?advanced-camera-card-action.id.camera_select=camera.office'); const api = createCardAPI(); + setCardID(api, 'id'); vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true); const manager = new QueryStringManager(api); @@ -130,6 +143,7 @@ describe('QueryStringManager', () => { '?advanced-camera-card-action.id.live_substream_select=camera.office_hd', ); const api = createCardAPI(); + setCardID(api, 'id'); vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true); const manager = new QueryStringManager(api); @@ -152,6 +166,7 @@ describe('QueryStringManager', () => { async (action: string) => { setQueryString(`?advanced-camera-card-action.id.${action}=`); const api = createCardAPI(); + setCardID(api, 'id'); vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true); const manager = new QueryStringManager(api); @@ -170,6 +185,7 @@ describe('QueryStringManager', () => { setQueryString('?advanced-camera-card-action.id.not_an_action=value'); const api = createCardAPI(); + setCardID(api, 'id'); vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true); const manager = new QueryStringManager(api); @@ -197,6 +213,7 @@ describe('QueryStringManager', () => { ])('%s', async (viewName: string) => { setQueryString(`?advanced-camera-card-action.id.${viewName}=`); const api = createCardAPI(); + setCardID(api, 'id'); vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true); const manager = new QueryStringManager(api); @@ -221,6 +238,7 @@ describe('QueryStringManager', () => { '&advanced-camera-card-action.id.camera_select=camera.kitchen', ); const api = createCardAPI(); + setCardID(api, 'id'); vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true); const manager = new QueryStringManager(api); @@ -241,6 +259,7 @@ describe('QueryStringManager', () => { '&advanced-camera-card-action.id.camera_select=camera.office', ); const api = createCardAPI(); + setCardID(api, 'id'); vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true); const manager = new QueryStringManager(api); @@ -259,6 +278,7 @@ describe('QueryStringManager', () => { '?advanced-camera-card-action.id.live_substream_select=camera.office_hd', ); const api = createCardAPI(); + setCardID(api, 'id'); vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true); const manager = new QueryStringManager(api); @@ -282,6 +302,7 @@ describe('QueryStringManager', () => { it('should execute actions with old frigate-card-action key', async () => { setQueryString(`?frigate-card-action.id.clips=`); const api = createCardAPI(); + setCardID(api, 'id'); // View actions do not need the card to have been updated. vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(false); @@ -297,4 +318,80 @@ describe('QueryStringManager', () => { }, }); }); + + describe('should filter by card_id', () => { + it('should execute view action when card_id matches', async () => { + setQueryString('?advanced-camera-card-action.my_card.clips='); + const api = createCardAPI(); + setCardID(api, 'my_card'); + vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(false); + const manager = new QueryStringManager(api); + + expect(manager.hasViewRelatedActionsToRun()).toBeTruthy(); + await manager.executeIfNecessary(); + + expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith({ + params: { + view: 'clips', + }, + }); + }); + + it('should NOT execute view action when card_id does not match', async () => { + setQueryString('?advanced-camera-card-action.other_card.clips='); + const api = createCardAPI(); + setCardID(api, 'my_card'); + vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(false); + const manager = new QueryStringManager(api); + + expect(manager.hasViewRelatedActionsToRun()).toBeFalsy(); + await manager.executeIfNecessary(); + + expect(api.getViewManager().setViewByParametersWithNewQuery).not.toBeCalled(); + expect(api.getActionsManager().executeActions).not.toBeCalled(); + }); + + it('should execute action without card_id on any card', async () => { + setQueryString('?advanced-camera-card-action.clips='); + const api = createCardAPI(); + setCardID(api, 'my_card'); + vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(false); + const manager = new QueryStringManager(api); + + expect(manager.hasViewRelatedActionsToRun()).toBeTruthy(); + await manager.executeIfNecessary(); + + expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith({ + params: { + view: 'clips', + }, + }); + }); + + it('should NOT execute non-view action when card_id does not match', async () => { + setQueryString('?advanced-camera-card-action.other_card.menu_toggle='); + const api = createCardAPI(); + setCardID(api, 'my_card'); + vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true); + const manager = new QueryStringManager(api); + + expect(manager.hasViewRelatedActionsToRun()).toBeFalsy(); + await manager.executeIfNecessary(); + + expect(api.getActionsManager().executeActions).not.toBeCalled(); + }); + + it('should execute action when card has no card_id and URL has card_id', async () => { + setQueryString('?advanced-camera-card-action.some_card.clips='); + const api = createCardAPI(); + vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(false); + const manager = new QueryStringManager(api); + + // Should NOT execute since URL targets 'some_card' but this card has no card_id + expect(manager.hasViewRelatedActionsToRun()).toBeFalsy(); + await manager.executeIfNecessary(); + + expect(api.getViewManager().setViewByParametersWithNewQuery).not.toBeCalled(); + }); + }); });