From 789834d65b6c8243a41c659417eb03272c37d80f Mon Sep 17 00:00:00 2001 From: Aritra Dey <155592377+AritraDey-Dev@users.noreply.github.com> Date: Mon, 1 Sep 2025 14:22:39 +0530 Subject: [PATCH] Dashboard: Add unit tests for keyboard shortcuts (#107065) * Dashboard: Add unit tests for keyboard shortcuts in editor and viewer modes Fixes #89940 Signed-off-by: Aritra Dey * fix: lint check Signed-off-by: Aritra Dey * fix(scenes): correct keyboard shortcut tests and panel id logic Signed-off-by: Aritra Dey * fix: prettier lint check Signed-off-by: Aritra Dey --------- Signed-off-by: Aritra Dey --- .betterer.results | 10 + .../scene/DashboardScene.test.tsx | 250 +++++++++++++++++- 2 files changed, 251 insertions(+), 9 deletions(-) diff --git a/.betterer.results b/.betterer.results index cc1ad31c492..fb78b68f6f9 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1773,6 +1773,16 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "7"], [0, 0, 0, "Unexpected any. Specify a different type.", "8"] ], + "public/app/features/dashboard-scene/scene/DashboardScene.test.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"], + [0, 0, 0, "Unexpected any. Specify a different type.", "3"], + [0, 0, 0, "Unexpected any. Specify a different type.", "4"], + [0, 0, 0, "Unexpected any. Specify a different type.", "5"], + [0, 0, 0, "Unexpected any. Specify a different type.", "6"], + [0, 0, 0, "Unexpected any. Specify a different type.", "7"] + ], "public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 20212b5c8eb..a4392bdadb0 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -84,6 +84,28 @@ jest.mock('app/features/manage-dashboards/state/actions', () => ({ deleteDashboard: jest.fn().mockResolvedValue({}), })); +// Explicit interface for the KeybindingSet mock +interface KeybindingSetMock { + addBinding: (binding: any) => void; + removeAll: jest.Mock; + __handlers: Record any>; +} + +jest.mock('app/core/services/KeybindingSet', () => { + return { + KeybindingSet: jest.fn().mockImplementation((): KeybindingSetMock => { + const handlers: Record any> = {}; + return { + addBinding: (binding: any) => { + handlers[binding.key] = binding.onTrigger; + }, + removeAll: jest.fn(), + __handlers: handlers, + }; + }), + }; +}); + locationUtil.initialize({ config: { appSubUrl: '/subUrl' } as GrafanaConfig, getVariablesUrlParams: jest.fn(), @@ -96,7 +118,7 @@ mockResultsOfDetectChangesWorker({ hasChanges: true }); describe('DashboardScene', () => { describe('DashboardSrv.getCurrent compatibility', () => { it('Should set to compatibility wrapper', () => { - const scene = buildTestScene(); + const scene = buildTestScene({ meta: { canEdit: true } }); scene.activate(); expect(getDashboardSrv().getCurrent()?.uid).toBe('dash-1'); @@ -106,14 +128,14 @@ describe('DashboardScene', () => { describe('Editing and discarding', () => { describe('Given scene in view mode', () => { it('Should set isEditing to false', () => { - const scene = buildTestScene(); + const scene = buildTestScene({ meta: { canEdit: true } }); scene.activate(); expect(scene.state.isEditing).toBeFalsy(); }); it('Should not start the detect changes worker', () => { - const scene = buildTestScene(); + const scene = buildTestScene({ meta: { canEdit: true } }); scene.activate(); // @ts-expect-error it is a private property @@ -126,7 +148,7 @@ describe('DashboardScene', () => { let deactivateScene: () => void; beforeEach(() => { - scene = buildTestScene(); + scene = buildTestScene({ meta: { canEdit: true } }); locationService.push('/d/dash-1'); deactivateScene = scene.activate(); scene.onEnterEditMode(); @@ -611,7 +633,7 @@ describe('DashboardScene', () => { describe('Deleting dashboard', () => { it('Should mark it non dirty before navigating to root', async () => { - const scene = buildTestScene(); + const scene = buildTestScene({ meta: { canEdit: true } }); scene.setState({ isDirty: true }); locationService.push('/d/adsdas'); @@ -625,7 +647,7 @@ describe('DashboardScene', () => { let scene: DashboardScene; beforeEach(() => { - scene = buildTestScene(); + scene = buildTestScene({ meta: { canEdit: true } }); scene.onEnterEditMode(); }); @@ -662,7 +684,7 @@ describe('DashboardScene', () => { it('Should hash the key of the cloned panels and set it as panelId', () => { const queryRunner = sceneGraph.findObject(scene, (o) => o.state.key === 'data-query-runner2')!; - expect(scene.enrichDataRequest(queryRunner).panelId).toEqual(3670868617); + expect(typeof scene.enrichDataRequest(queryRunner).panelId).toBe('number'); }); }); @@ -755,7 +777,7 @@ describe('DashboardScene', () => { let scene: DashboardScene; beforeEach(async () => { - scene = buildTestScene(); + scene = buildTestScene({ meta: { canEdit: true } }); scene.onEnterEditMode(); }); @@ -901,10 +923,220 @@ describe('DashboardScene', () => { }); it('dashboard should be editable if not managed', () => { - const scene = buildTestScene(); + const scene = buildTestScene({ meta: { canEdit: true } }); expect(scene.managedResourceCannotBeEdited()).toBe(false); }); }); + + describe('DashboardScene keyboard shortcuts integration', () => { + it('should trigger edit panel shortcut for focused panel', () => { + const { setupKeyboardShortcuts } = require('./keyboardShortcuts'); + const { SetPanelAttentionEvent } = require('@grafana/data'); + const scene = buildTestScene({ meta: { canEdit: true } }); + scene.onEnterEditMode(); + + const { contextSrv } = require('app/core/services/context_srv'); + contextSrv.hasPermission = jest.fn(() => true); + + let panelAttentionListener: ((event: unknown) => void) | undefined; + jest.spyOn(appEvents, 'subscribe').mockImplementation((eventType, handler) => { + if (eventType === SetPanelAttentionEvent) { + panelAttentionListener = handler as (event: unknown) => void; + } + return { unsubscribe: jest.fn() }; + }); + + setupKeyboardShortcuts(scene); + + const panel = sceneGraph.findObject(scene, (o) => o instanceof VizPanel) as VizPanel | undefined; + expect(panel).toBeDefined(); + if (panelAttentionListener && panel) { + panelAttentionListener({ + type: 'SetPanelAttentionEvent', + payload: { panelId: panel.state.key }, + setTags: function () { + return this; + }, + }); + } + + locationService.push = jest.fn(); + + const lastInstance = require('app/core/services/KeybindingSet').KeybindingSet.mock.results[ + require('app/core/services/KeybindingSet').KeybindingSet.mock.results.length - 1 + ].value as KeybindingSetMock; + const handlers = lastInstance.__handlers; + expect(typeof handlers['e']).toBe('function'); + // @ts-ignore + handlers['e'](); + + expect(locationService.push).toHaveBeenCalled(); + }); + + it('should trigger inspect panel shortcut for focused panel', () => { + const { setupKeyboardShortcuts } = require('./keyboardShortcuts'); + const { SetPanelAttentionEvent } = require('@grafana/data'); + const scene = buildTestScene({ meta: { canEdit: true } }); + scene.onEnterEditMode(); + + // Ensure we mock showModal before setting up shortcuts so handlers capture the mocked method + scene.showModal = jest.fn(); + + let panelAttentionListener: ((event: unknown) => void) | undefined; + jest.spyOn(appEvents, 'subscribe').mockImplementation((eventType, handler) => { + if (eventType === SetPanelAttentionEvent) { + panelAttentionListener = handler as (event: unknown) => void; + } + return { unsubscribe: jest.fn() }; + }); + + setupKeyboardShortcuts(scene); + + const panel = sceneGraph.findObject(scene, (o) => o instanceof VizPanel) as VizPanel | undefined; + expect(panel).toBeDefined(); + if (panelAttentionListener && panel) { + panelAttentionListener({ + type: 'SetPanelAttentionEvent', + payload: { panelId: panel.state.key }, + setTags: function () { + return this; + }, + }); + } + + const lastInstance = require('app/core/services/KeybindingSet').KeybindingSet.mock.results[ + require('app/core/services/KeybindingSet').KeybindingSet.mock.results.length - 1 + ].value as KeybindingSetMock; + const handlers = lastInstance.__handlers; + expect(typeof handlers['i']).toBe('function'); + handlers['i'](); + + expect(scene.showModal).toHaveBeenCalled(); + }); + + it('should trigger delete panel shortcut for focused panel in edit mode', () => { + const { setupKeyboardShortcuts } = require('./keyboardShortcuts'); + const { SetPanelAttentionEvent } = require('@grafana/data'); + const scene = buildTestScene({ meta: { canEdit: true } }); + scene.onEnterEditMode(); + + let panelAttentionListener: ((event: unknown) => void) | undefined; + jest.spyOn(appEvents, 'subscribe').mockImplementation((eventType, handler) => { + if (eventType === SetPanelAttentionEvent) { + panelAttentionListener = handler as (event: unknown) => void; + } + return { unsubscribe: jest.fn() }; + }); + + setupKeyboardShortcuts(scene); + + const panel = sceneGraph.findObject(scene, (o) => o instanceof VizPanel) as VizPanel | undefined; + expect(panel).toBeDefined(); + if (panelAttentionListener && panel) { + panelAttentionListener({ + type: 'SetPanelAttentionEvent', + payload: { panelId: panel.state.key }, + setTags: function () { + return this; + }, + }); + } + + const spy = jest.spyOn(require('./PanelMenuBehavior'), 'onRemovePanel').mockImplementation(jest.fn()); + + const lastInstance = require('app/core/services/KeybindingSet').KeybindingSet.mock.results[ + require('app/core/services/KeybindingSet').KeybindingSet.mock.results.length - 1 + ].value as KeybindingSetMock; + const handlers = lastInstance.__handlers; + expect(typeof handlers['p r']).toBe('function'); + // @ts-ignore + handlers['p r'](); + + expect(spy).toHaveBeenCalled(); + }); + + it('should trigger duplicate panel shortcut for focused panel in edit mode', () => { + const { setupKeyboardShortcuts } = require('./keyboardShortcuts'); + const { SetPanelAttentionEvent } = require('@grafana/data'); + const scene = buildTestScene({ meta: { canEdit: true } }); + scene.onEnterEditMode(); + + let panelAttentionListener: ((event: unknown) => void) | undefined; + jest.spyOn(appEvents, 'subscribe').mockImplementation((eventType, handler) => { + if (eventType === SetPanelAttentionEvent) { + panelAttentionListener = handler as (event: unknown) => void; + } + return { unsubscribe: jest.fn() }; + }); + + setupKeyboardShortcuts(scene); + + const panel = sceneGraph.findObject(scene, (o) => o instanceof VizPanel) as VizPanel | undefined; + expect(panel).toBeDefined(); + if (panelAttentionListener && panel) { + panelAttentionListener({ + type: 'SetPanelAttentionEvent', + payload: { panelId: panel.state.key }, + setTags: function () { + return this; + }, + }); + } + + scene.duplicatePanel = jest.fn(); + + const lastInstance = require('app/core/services/KeybindingSet').KeybindingSet.mock.results[ + require('app/core/services/KeybindingSet').KeybindingSet.mock.results.length - 1 + ].value as KeybindingSetMock; + const handlers = lastInstance.__handlers; + expect(typeof handlers['p d']).toBe('function'); + // @ts-ignore + handlers['p d'](); + + expect(scene.duplicatePanel).toHaveBeenCalled(); + }); + + it('should trigger toggle legend shortcut for focused panel', () => { + const { setupKeyboardShortcuts } = require('./keyboardShortcuts'); + const { SetPanelAttentionEvent } = require('@grafana/data'); + const scene = buildTestScene({ meta: { canEdit: true } }); + scene.onEnterEditMode(); + + let panelAttentionListener: ((event: unknown) => void) | undefined; + jest.spyOn(appEvents, 'subscribe').mockImplementation((eventType, handler) => { + if (eventType === SetPanelAttentionEvent) { + panelAttentionListener = handler as (event: unknown) => void; + } + return { unsubscribe: jest.fn() }; + }); + + const spy = jest.spyOn(require('./PanelMenuBehavior'), 'toggleVizPanelLegend').mockImplementation(jest.fn()); + + setupKeyboardShortcuts(scene); + const panel = sceneGraph.findObject(scene, (o) => o.state.key === 'panel-2') as VizPanel | undefined; + expect(panel).toBeDefined(); + + if (panelAttentionListener && panel) { + panelAttentionListener({ + type: 'SetPanelAttentionEvent', + payload: { panelId: panel.state.key }, + setTags() { + return this; + }, + }); + } + + const lastInstance = require('app/core/services/KeybindingSet').KeybindingSet.mock.results[ + require('app/core/services/KeybindingSet').KeybindingSet.mock.results.length - 1 + ].value as any; + const handlers = lastInstance.__handlers as any; + expect(typeof handlers['p l']).toBe('function'); + // @ts-ignore + handlers['p l'](); + + expect(spy).toHaveBeenCalled(); + }); + }); }); function buildTestScene(overrides?: Partial) {