import {RouterFixture} from 'sentry-fixture/routerFixture'; import {initializeOrg} from 'sentry-test/initializeOrg'; import {act} from 'sentry-test/reactTestingLibrary'; import { initializeUrlState, revertToPinnedFilters, updateDateTime, updateEnvironments, updatePersistence, updateProjects, } from 'sentry/actionCreators/pageFilters'; import * as PageFilterPersistence from 'sentry/components/organizations/pageFilters/persistence'; import PageFiltersStore from 'sentry/stores/pageFiltersStore'; import localStorage from 'sentry/utils/localStorage'; jest.mock('sentry/utils/localStorage'); const {organization, projects} = initializeOrg({ projects: [ {id: '1', slug: 'project-1', environments: ['prod', 'staging']}, {id: '2', slug: 'project-2', environments: ['prod', 'stage']}, ], }); describe('PageFilters ActionCreators', function () { beforeEach(function () { jest.spyOn(PageFiltersStore, 'updateProjects'); jest.spyOn(PageFiltersStore, 'onInitializeUrlState').mockImplementation(); jest.clearAllMocks(); }); describe('initializeUrlState', function () { let router; const key = `global-selection:${organization.slug}`; beforeEach(() => { router = RouterFixture(); localStorage.setItem( key, JSON.stringify({ environments: [], projects: [1], }) ); }); it('loads from local storage when no query params and filters are pinned', function () { localStorage.setItem( key, JSON.stringify({ environments: [], projects: [1], pinnedFilters: ['projects', 'environments'], }) ); initializeUrlState({ organization, queryParams: {}, router, memberProjects: projects, nonMemberProjects: [], shouldEnforceSingleProject: false, }); expect(localStorage.getItem).toHaveBeenCalledWith( `global-selection:${organization.slug}` ); expect(PageFiltersStore.onInitializeUrlState).toHaveBeenCalledWith( expect.objectContaining({ environments: [], projects: [1], }), new Set(['projects', 'environments']), true ); expect(router.replace).toHaveBeenCalledWith( expect.objectContaining({ query: { environment: [], project: ['1'], }, }) ); }); it('does not load from local storage when no query params and `skipLoadLastUsed` is true', function () { jest.spyOn(localStorage, 'getItem'); initializeUrlState({ organization, queryParams: {}, skipLoadLastUsed: true, memberProjects: projects, nonMemberProjects: [], shouldEnforceSingleProject: false, router, }); expect(localStorage.getItem).not.toHaveBeenCalled(); }); it('does not update local storage (persist) when `shouldPersist` is false', async function () { jest.clearAllMocks(); jest.spyOn(localStorage, 'getItem').mockReturnValueOnce( JSON.stringify({ environments: [], projects: [], pinnedFilters: ['projects'], }) ); initializeUrlState({ organization, queryParams: {}, shouldPersist: false, router, memberProjects: projects, nonMemberProjects: [], shouldEnforceSingleProject: false, }); expect(PageFiltersStore.onInitializeUrlState).toHaveBeenCalledWith( expect.objectContaining({ environments: [], projects: [], }), new Set(['projects']), false ); // `onInitializeUrlState` is being spied on, so PageFiltersStore wasn't actually // updated. We need to call `updatePersistence` manually. updatePersistence(false); await act(async () => { // Filters shouldn't persist even when `save` is true updateProjects([1], router, {save: true}); // Page filter values are asynchronously persisted to local storage after a tick, // so we need to wait before checking for commits to local storage await tick(); }); // New value wasn't committed to local storage expect(localStorage.setItem).not.toHaveBeenCalled(); }); it('does not change dates with no query params or defaultSelection', function () { initializeUrlState({ organization, queryParams: { project: '1', }, memberProjects: projects, nonMemberProjects: [], shouldEnforceSingleProject: false, router, }); expect(PageFiltersStore.onInitializeUrlState).toHaveBeenCalledWith( expect.objectContaining({ datetime: { start: null, end: null, period: '14d', utc: null, }, }), new Set(), true ); }); it('does changes to default dates with defaultSelection and no query params', function () { initializeUrlState({ organization, queryParams: { project: '1', }, memberProjects: projects, nonMemberProjects: [], shouldEnforceSingleProject: false, defaultSelection: { datetime: { period: '3h', utc: null, start: null, end: null, }, }, router, }); expect(PageFiltersStore.onInitializeUrlState).toHaveBeenCalledWith( expect.objectContaining({ datetime: { start: null, end: null, period: '3h', utc: null, }, }), new Set(), true ); }); it('uses query params statsPeriod over defaults', function () { initializeUrlState({ organization, queryParams: { statsPeriod: '1h', project: '1', }, memberProjects: projects, nonMemberProjects: [], shouldEnforceSingleProject: false, defaultSelection: { datetime: { period: '24h', utc: null, start: null, end: null, }, }, router, }); expect(router.replace).toHaveBeenCalledWith( expect.objectContaining({ query: { cursor: undefined, project: ['1'], environment: [], statsPeriod: '1h', }, }) ); }); it('uses absolute dates over defaults', function () { initializeUrlState({ organization, queryParams: { start: '2020-03-22T00:53:38', end: '2020-04-21T00:53:38', project: '1', }, memberProjects: projects, nonMemberProjects: [], shouldEnforceSingleProject: false, defaultSelection: { datetime: { period: '24h', utc: null, start: null, end: null, }, }, router, }); expect(router.replace).toHaveBeenCalledWith( expect.objectContaining({ query: { cursor: undefined, project: ['1'], environment: [], start: '2020-03-22T00:53:38', end: '2020-04-21T00:53:38', }, }) ); }); it('does not load from local storage when there are query params', function () { initializeUrlState({ organization, queryParams: { project: '1', }, memberProjects: projects, nonMemberProjects: [], shouldEnforceSingleProject: false, router, }); expect(PageFiltersStore.onInitializeUrlState).toHaveBeenCalledWith( { datetime: { start: null, end: null, period: '14d', utc: null, }, projects: [1], environments: [], }, new Set(), true ); expect(router.replace).toHaveBeenCalledWith( expect.objectContaining({ query: { environment: [], project: ['1'], }, }) ); }); it('does not invalidate all projects from query params', function () { initializeUrlState({ organization, queryParams: { project: '-1', }, memberProjects: projects, nonMemberProjects: [], shouldEnforceSingleProject: false, router, }); expect(PageFiltersStore.onInitializeUrlState).toHaveBeenCalledWith( { datetime: { start: null, end: null, period: '14d', utc: null, }, projects: [-1], environments: [], }, new Set(), true ); }); it('does invalidate all projects from query params if forced into single project', function () { initializeUrlState({ organization, queryParams: { project: '-1', }, memberProjects: projects, nonMemberProjects: [], // User does not have access to global views shouldEnforceSingleProject: true, router, }); expect(PageFiltersStore.onInitializeUrlState).toHaveBeenCalledWith( { datetime: { start: null, end: null, period: '14d', utc: null, }, projects: [1], environments: [], }, new Set(), true ); }); it('does not add non-pinned filters to query for pages with new page filters', function () { // Mock storage to have a saved value const pageFilterStorageMock = jest .spyOn(PageFilterPersistence, 'getPageFilterStorage') .mockReturnValueOnce({ state: { project: [1], environment: [], start: null, end: null, period: '14d', utc: null, }, pinnedFilters: new Set(), }); // Initialize state with a page that shouldn't restore from local storage initializeUrlState({ organization, queryParams: {}, router, memberProjects: projects, nonMemberProjects: [], shouldEnforceSingleProject: false, }); // Confirm that query params are not restored from local storage expect(router.replace).not.toHaveBeenCalled(); pageFilterStorageMock.mockRestore(); }); it('uses pinned filters for pages with new page filters', function () { // Mock storage to have a saved/pinned value const pageFilterStorageMock = jest .spyOn(PageFilterPersistence, 'getPageFilterStorage') .mockReturnValueOnce({ state: { project: [1], environment: ['prod'], start: null, end: null, period: '7d', utc: null, }, pinnedFilters: new Set(['environments', 'datetime', 'projects']), }); // Initialize state with a page that uses pinned filters initializeUrlState({ organization, queryParams: {}, router, memberProjects: projects, nonMemberProjects: [], shouldEnforceSingleProject: false, }); // Confirm that only environment is restored from local storage expect(router.replace).toHaveBeenCalledWith( expect.objectContaining({ query: { environment: ['prod'], project: ['1'], statsPeriod: '7d', }, }) ); pageFilterStorageMock.mockRestore(); }); it('retrieves filters from a separate key when storageNamespace is provided', function () { const starfishKey = `global-selection:starfish:${organization.slug}`; localStorage.setItem( starfishKey, JSON.stringify({ environments: [], projects: [1], pinnedFilters: ['projects', 'environments'], }) ); initializeUrlState({ organization, queryParams: {}, router, memberProjects: projects, nonMemberProjects: [], shouldEnforceSingleProject: false, storageNamespace: 'starfish', }); expect(localStorage.getItem).toHaveBeenCalledWith(starfishKey); expect(PageFiltersStore.onInitializeUrlState).toHaveBeenCalledWith( expect.objectContaining({ environments: [], projects: [1], }), new Set(['projects', 'environments']), true ); expect(router.replace).toHaveBeenCalledWith( expect.objectContaining({ query: { environment: [], project: ['1'], }, }) ); }); }); describe('updateProjects()', function () { it('updates', function () { updateProjects([1, 2]); expect(PageFiltersStore.updateProjects).toHaveBeenCalledWith([1, 2], null); }); it('updates history when queries are different', function () { const router = RouterFixture({ location: { pathname: '/test/', query: {project: '2'}, }, }); // this can be passed w/ `project` as an array (e.g. multiple projects being selected) // however react-router will treat it as a string if there is only one param updateProjects([1], router); expect(router.push).toHaveBeenCalledWith({ pathname: '/test/', query: {project: ['1']}, }); }); it('does not update history when queries are the same', function () { const router = RouterFixture({ location: { pathname: '/test/', query: {project: '1'}, }, }); // this can be passed w/ `project` as an array (e.g. multiple projects // being selected) however react-router will treat it as a string if // there is only one param updateProjects([1], router); expect(router.push).not.toHaveBeenCalled(); }); it('updates history when queries are different with replace', function () { const router = RouterFixture({ location: { pathname: '/test/', query: {project: '2'}, }, }); updateProjects([1], router, {replace: true}); expect(router.replace).toHaveBeenCalledWith({ pathname: '/test/', query: {project: ['1']}, }); }); it('does not update history when queries are the same with replace', function () { const router = RouterFixture({ location: { pathname: '/test/', query: {project: '1'}, }, }); updateProjects([1], router, {replace: true}); expect(router.replace).not.toHaveBeenCalled(); }); it('does not override an absolute date selection', function () { const router = RouterFixture({ location: { pathname: '/test/', query: {project: '1', start: '2020-03-22T00:53:38', end: '2020-04-21T00:53:38'}, }, }); updateProjects([2], router, {replace: true}); expect(router.replace).toHaveBeenCalledWith({ pathname: '/test/', query: {project: ['2'], start: '2020-03-22T00:53:38', end: '2020-04-21T00:53:38'}, }); }); }); describe('updateEnvironments()', function () { it('updates single', function () { const router = RouterFixture({ location: { pathname: '/test/', query: {environment: 'test'}, }, }); updateEnvironments(['new-env'], router); expect(router.push).toHaveBeenCalledWith({ pathname: '/test/', query: {environment: ['new-env']}, }); }); it('updates multiple', function () { const router = RouterFixture({ location: { pathname: '/test/', query: {environment: 'test'}, }, }); updateEnvironments(['new-env', 'another-env'], router); expect(router.push).toHaveBeenCalledWith({ pathname: '/test/', query: {environment: ['new-env', 'another-env']}, }); }); it('removes environment', function () { const router = RouterFixture({ location: { pathname: '/test/', query: {environment: 'test'}, }, }); updateEnvironments(null, router); expect(router.push).toHaveBeenCalledWith({ pathname: '/test/', query: {}, }); }); it('does not override an absolute date selection', function () { const router = RouterFixture({ location: { pathname: '/test/', query: { environment: 'test', start: '2020-03-22T00:53:38', end: '2020-04-21T00:53:38', }, }, }); updateEnvironments(['new-env'], router, {replace: true}); expect(router.replace).toHaveBeenCalledWith({ pathname: '/test/', query: { environment: ['new-env'], start: '2020-03-22T00:53:38', end: '2020-04-21T00:53:38', }, }); }); }); describe('updateDateTime()', function () { it('updates statsPeriod when there is no existing stats period', function () { const router = RouterFixture({ location: { pathname: '/test/', query: {}, }, }); updateDateTime({period: '24h'}, router); expect(router.push).toHaveBeenCalledWith({ pathname: '/test/', query: { statsPeriod: '24h', }, }); }); it('updates statsPeriod when there is an existing stats period', function () { const router = RouterFixture({ location: { pathname: '/test/', query: {statsPeriod: '14d'}, }, }); updateDateTime({period: '24h'}, router); expect(router.push).toHaveBeenCalledWith({ pathname: '/test/', query: { statsPeriod: '24h', }, }); }); it('changes to absolute date', function () { const router = RouterFixture({ location: { pathname: '/test/', query: {statsPeriod: '24h'}, }, }); updateDateTime({start: '2020-03-22T00:53:38', end: '2020-04-21T00:53:38'}, router); expect(router.push).toHaveBeenCalledWith({ pathname: '/test/', query: { start: '2020-03-22T00:53:38', end: '2020-04-21T00:53:38', }, }); }); }); describe('revertToPinnedFilters()', function () { it('reverts all filters that are desynced from localStorage', function () { const router = RouterFixture({ location: { pathname: '/test/', query: {}, }, }); // Mock storage to have a saved value const pageFilterStorageMock = jest .spyOn(PageFilterPersistence, 'getPageFilterStorage') .mockReturnValueOnce({ state: { project: [1], environment: [], start: null, end: null, period: '14d', utc: null, }, pinnedFilters: new Set(['projects', 'environments', 'datetime']), }); PageFiltersStore.onInitializeUrlState( { projects: [2], environments: ['prod'], datetime: { start: null, end: null, period: '1d', utc: null, }, }, new Set() ); PageFiltersStore.updateDesyncedFilters( new Set(['projects', 'environments', 'datetime']) ); revertToPinnedFilters('org-slug', router); expect(router.push).toHaveBeenCalledWith({ pathname: '/test/', query: { environment: [], project: ['1'], statsPeriod: '14d', }, }); pageFilterStorageMock.mockRestore(); }); }); });