/usr/share/grafana/public/app/features/scopes
import { BehaviorSubject } from 'rxjs'; import { LocationService } from '@grafana/runtime'; import { ScopesService } from './ScopesService'; import { ScopesDashboardsService } from './dashboards/ScopesDashboardsService'; import { ScopesSelectorService } from './selector/ScopesSelectorService'; jest.mock('./selector/ScopesSelectorService'); jest.mock('./dashboards/ScopesDashboardsService'); describe('ScopesService', () => { let service: ScopesService; let selectorService: jest.Mocked<ScopesSelectorService>; let dashboardsService: jest.Mocked<ScopesDashboardsService>; let locationService: jest.Mocked<LocationService>; let stateSubscription: | (( state: { appliedScopes: Array<{ scopeId: string; scopeNodeId?: string; parentNodeId?: string }> }, prevState: { appliedScopes: Array<{ scopeId: string; scopeNodeId?: string; parentNodeId?: string }> } ) => void) | undefined; beforeEach(() => { stateSubscription = undefined; selectorService = { state: { appliedScopes: [], selectedScopes: [], scopes: {}, nodes: {}, loading: false, opened: false, loadingNodeName: undefined, tree: { scopeNodeId: '', expanded: false, query: '', children: {} }, }, stateObservable: new BehaviorSubject({ appliedScopes: [], selectedScopes: [], scopes: {}, nodes: {}, loading: false, opened: false, loadingNodeName: undefined, tree: { scopeNodeId: '', expanded: false, query: '', children: {} }, }), subscribeToState: jest.fn((callback) => { stateSubscription = callback; return { unsubscribe: jest.fn() }; }), changeScopes: jest.fn(), resolvePathToRoot: jest.fn().mockResolvedValue({ path: [], tree: {} }), } as unknown as jest.Mocked<ScopesSelectorService>; dashboardsService = { state: { drawerOpened: false, dashboards: [], scopeNavigations: [], filteredFolders: {}, folders: {}, forScopeNames: [], loading: false, searchQuery: '', }, stateObservable: new BehaviorSubject({ drawerOpened: false, dashboards: [], scopeNavigations: [], filteredFolders: {}, folders: {}, forScopeNames: [], loading: false, searchQuery: '', }), } as unknown as jest.Mocked<ScopesDashboardsService>; locationService = { getLocation: jest.fn().mockReturnValue({ pathname: '/test', search: '', }), getLocationObservable: jest.fn().mockReturnValue( new BehaviorSubject({ pathname: '/test', search: '', }) ), partial: jest.fn(), } as unknown as jest.Mocked<LocationService>; }); describe('URL initialization', () => { it('should read scope_node from URL on init', () => { locationService.getLocation = jest.fn().mockReturnValue({ pathname: '/test', search: '?scopes=scope1&scope_node=node1', }); service = new ScopesService(selectorService, dashboardsService, locationService); expect(selectorService.changeScopes).toHaveBeenCalledWith(['scope1'], undefined, 'node1'); }); it('should read scope_parent for backward compatibility', () => { locationService.getLocation = jest.fn().mockReturnValue({ pathname: '/test', search: '?scopes=scope1&scope_parent=parent1', }); service = new ScopesService(selectorService, dashboardsService, locationService); expect(selectorService.changeScopes).toHaveBeenCalledWith(['scope1'], 'parent1', undefined); }); it('should prefer scope_node when both scope_node and scope_parent exist', () => { locationService.getLocation = jest.fn().mockReturnValue({ pathname: '/test', search: '?scopes=scope1&scope_node=node1&scope_parent=parent1', }); service = new ScopesService(selectorService, dashboardsService, locationService); // Should call with parent1 as parentNodeId and node1 as scopeNodeId expect(selectorService.changeScopes).toHaveBeenCalledWith(['scope1'], 'parent1', 'node1'); // Should preload node1 (not parent1) expect(selectorService.resolvePathToRoot).toHaveBeenCalledWith('node1', expect.anything()); }); it('should preload scope_node when provided', () => { locationService.getLocation = jest.fn().mockReturnValue({ pathname: '/test', search: '?scopes=scope1&scope_node=node1', }); service = new ScopesService(selectorService, dashboardsService, locationService); expect(selectorService.resolvePathToRoot).toHaveBeenCalledWith('node1', expect.anything()); }); it('should fallback to preload scope_parent when scope_node is not provided', () => { locationService.getLocation = jest.fn().mockReturnValue({ pathname: '/test', search: '?scopes=scope1&scope_parent=parent1', }); service = new ScopesService(selectorService, dashboardsService, locationService); expect(selectorService.resolvePathToRoot).toHaveBeenCalledWith('parent1', expect.anything()); }); it('should handle multiple scopes from URL', () => { locationService.getLocation = jest.fn().mockReturnValue({ pathname: '/test', search: '?scopes=scope1&scopes=scope2&scope_node=node1', }); service = new ScopesService(selectorService, dashboardsService, locationService); expect(selectorService.changeScopes).toHaveBeenCalledWith(['scope1', 'scope2'], undefined, 'node1'); }); }); describe('URL synchronization', () => { beforeEach(() => { locationService.getLocation = jest.fn().mockReturnValue({ pathname: '/test', search: '', }); service = new ScopesService(selectorService, dashboardsService, locationService); }); it('should write scope_node to URL when scopes change', () => { if (!stateSubscription) { throw new Error('stateSubscription not set'); } stateSubscription( { appliedScopes: [{ scopeId: 'scope1', scopeNodeId: 'node1' }], }, { appliedScopes: [], } ); expect(locationService.partial).toHaveBeenCalledWith( { scopes: ['scope1'], scope_node: 'node1', scope_parent: null, }, true ); }); it('should reset scope_parent to null when writing URL', () => { if (!stateSubscription) { throw new Error('stateSubscription not set'); } stateSubscription( { appliedScopes: [{ scopeId: 'scope1', scopeNodeId: 'node1', parentNodeId: 'parent1' }], }, { appliedScopes: [], } ); expect(locationService.partial).toHaveBeenCalledWith( expect.objectContaining({ scope_parent: null, }), true ); }); it('should handle scopeNodeId changes without scope changes', () => { if (!stateSubscription) { throw new Error('stateSubscription not set'); } stateSubscription( { appliedScopes: [{ scopeId: 'scope1', scopeNodeId: 'node2' }], }, { appliedScopes: [{ scopeId: 'scope1', scopeNodeId: 'node1' }], } ); expect(locationService.partial).toHaveBeenCalledWith( { scopes: ['scope1'], scope_node: 'node2', scope_parent: null, }, true ); }); it('should handle missing scopeNodeId gracefully', () => { if (!stateSubscription) { throw new Error('stateSubscription not set'); } stateSubscription( { appliedScopes: [{ scopeId: 'scope1' }], }, { appliedScopes: [], } ); expect(locationService.partial).toHaveBeenCalledWith( { scopes: ['scope1'], scope_node: null, scope_parent: null, }, true ); }); it('should not update URL when scopes and scopeNodeId have not changed', () => { if (!stateSubscription) { throw new Error('stateSubscription not set'); } jest.clearAllMocks(); stateSubscription( { appliedScopes: [{ scopeId: 'scope1', scopeNodeId: 'node1' }], }, { appliedScopes: [{ scopeId: 'scope1', scopeNodeId: 'node1' }], } ); expect(locationService.partial).not.toHaveBeenCalled(); }); }); describe('setEnabled', () => { beforeEach(() => { locationService.getLocation = jest.fn().mockReturnValue({ pathname: '/test', search: '', }); service = new ScopesService(selectorService, dashboardsService, locationService); }); it('should sync scopeNodeId when enabling scopes', () => { selectorService.state.appliedScopes = [{ scopeId: 'scope1', scopeNodeId: 'node1' }]; service.setEnabled(true); expect(locationService.partial).toHaveBeenCalledWith( expect.objectContaining({ scope_node: 'node1', }), true ); }); it('should reset scope_parent when enabling scopes', () => { selectorService.state.appliedScopes = [{ scopeId: 'scope1', scopeNodeId: 'node1' }]; service.setEnabled(true); expect(locationService.partial).toHaveBeenCalledWith( expect.objectContaining({ scope_parent: null, }), true ); }); }); });
.
Edit
..
Edit
ScopesApiClient.ts
Edit
ScopesContextProvider.tsx
Edit
ScopesService.test.ts
Edit
ScopesService.ts
Edit
ScopesServiceBase.ts
Edit
dashboards
Edit
selector
Edit
tests
Edit
utils.ts
Edit