123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200 |
- import {mountWithTheme} from 'sentry-test/enzyme';
- import {initializeOrg} from 'sentry-test/initializeOrg';
- import {mockRouterPush} from 'sentry-test/mockRouterPush';
- import * as globalActions from 'app/actionCreators/globalSelection';
- import OrganizationActions from 'app/actions/organizationActions';
- import GlobalSelectionHeader from 'app/components/organizations/globalSelectionHeader';
- import ConfigStore from 'app/stores/configStore';
- import GlobalSelectionStore from 'app/stores/globalSelectionStore';
- import ProjectsStore from 'app/stores/projectsStore';
- import {getItem} from 'app/utils/localStorage';
- const changeQuery = (routerContext, query) => ({
- ...routerContext,
- context: {
- ...routerContext.context,
- router: {
- ...routerContext.context.router,
- location: {
- query,
- },
- },
- },
- });
- jest.mock('app/utils/localStorage', () => ({
- getItem: jest.fn(),
- setItem: jest.fn(),
- }));
- describe('GlobalSelectionHeader', function () {
- let wrapper;
- const {organization, router, routerContext} = initializeOrg({
- organization: {features: ['global-views']},
- projects: [
- {
- id: 2,
- slug: 'project-2',
- },
- {
- id: 3,
- slug: 'project-3',
- environments: ['prod', 'staging'],
- },
- ],
- router: {
- location: {query: {}},
- params: {orgId: 'org-slug'},
- },
- });
- beforeAll(function () {
- jest.spyOn(globalActions, 'updateDateTime');
- jest.spyOn(globalActions, 'updateEnvironments');
- jest.spyOn(globalActions, 'updateProjects');
- jest.spyOn(globalActions, 'updateParams');
- jest.spyOn(globalActions, 'updateParamsWithoutHistory');
- });
- beforeEach(function () {
- MockApiClient.clearMockResponses();
- jest.spyOn(ProjectsStore, 'getState').mockImplementation(() => ({
- projects: organization.projects,
- loading: false,
- }));
- getItem.mockImplementation(() => null);
- MockApiClient.addMockResponse({
- url: '/organizations/org-slug/projects/',
- body: [],
- });
- });
- afterEach(function () {
- wrapper.unmount();
- [
- globalActions.updateDateTime,
- globalActions.updateProjects,
- globalActions.updateEnvironments,
- globalActions.updateParams,
- globalActions.updateParamsWithoutHistory,
- router.push,
- router.replace,
- getItem,
- ].forEach(mock => mock.mockClear());
- GlobalSelectionStore.reset();
- });
- it('does not update router if there is custom routing', function () {
- wrapper = mountWithTheme(
- <GlobalSelectionHeader organization={organization} hasCustomRouting />,
- routerContext
- );
- expect(router.push).not.toHaveBeenCalled();
- });
- it('does not update router if org in URL params is different than org in context/props', function () {
- wrapper = mountWithTheme(
- <GlobalSelectionHeader organization={organization} hasCustomRouting />,
- {
- ...routerContext,
- context: {
- ...routerContext.context,
- router: {...routerContext.context.router, params: {orgId: 'diff-org'}},
- },
- }
- );
- expect(router.push).not.toHaveBeenCalled();
- });
- it('does not replace URL with values from store when mounted with no query params', function () {
- wrapper = mountWithTheme(
- <GlobalSelectionHeader organization={organization} />,
- routerContext
- );
- expect(router.replace).not.toHaveBeenCalled();
- });
- it('only updates GlobalSelection store when mounted with query params', async function () {
- wrapper = mountWithTheme(
- <GlobalSelectionHeader
- organization={organization}
- params={{orgId: organization.slug}}
- />,
- changeQuery(routerContext, {
- statsPeriod: '7d',
- })
- );
- expect(router.push).not.toHaveBeenCalled();
- await tick();
- expect(GlobalSelectionStore.get().selection).toEqual({
- datetime: {
- period: '7d',
- utc: null,
- start: null,
- end: null,
- },
- environments: [],
- projects: [],
- });
- });
- it('updates environments when switching projects', async function () {
- wrapper = mountWithTheme(
- <GlobalSelectionHeader
- organization={organization}
- projects={organization.projects}
- />,
- routerContext
- );
- await tick();
- wrapper.update();
- mockRouterPush(wrapper, router);
- // Open dropdown and select both projects
- wrapper.find('MultipleProjectSelector HeaderItem').simulate('click');
- wrapper.find('MultipleProjectSelector CheckboxFancy').at(0).simulate('click');
- wrapper.find('MultipleProjectSelector CheckboxFancy').at(1).simulate('click');
- wrapper.find('MultipleProjectSelector HeaderItem').simulate('click');
- await tick();
- wrapper.update();
- expect(wrapper.find('MultipleProjectSelector Content').text()).toBe(
- 'project-2, project-3'
- );
- // Select environment
- wrapper.find('MultipleEnvironmentSelector HeaderItem').simulate('click');
- wrapper.find('MultipleEnvironmentSelector CheckboxFancy').at(1).simulate('click');
- wrapper.find('MultipleEnvironmentSelector HeaderItem').simulate('click');
- await tick();
- expect(wrapper.find('MultipleEnvironmentSelector Content').text()).toBe('staging');
- expect(GlobalSelectionStore.get().selection).toEqual({
- datetime: {
- period: '14d',
- utc: null,
- start: null,
- end: null,
- },
- environments: ['staging'],
- projects: [2, 3],
- });
- const query = wrapper.prop('location').query;
- expect(query).toEqual({
- environment: 'staging',
- project: ['2', '3'],
- });
- // Now change projects, first project has no environments
- wrapper.find('MultipleProjectSelector HeaderItem').simulate('click');
- wrapper.find('MultipleProjectSelector CheckboxFancy').at(1).simulate('click');
- wrapper.find('MultipleProjectSelector HeaderItem').simulate('click');
- await tick();
- wrapper.update();
- // Store should not have any environments selected
- expect(GlobalSelectionStore.get().selection).toEqual({
- datetime: {
- period: '14d',
- utc: null,
- start: null,
- end: null,
- },
- environments: [],
- projects: [2],
- });
- expect(wrapper.find('MultipleEnvironmentSelector Content').text()).toBe(
- 'All Environments'
- );
- });
- it('shows environments for non-member projects', async function () {
- const initialData = initializeOrg({
- organization: {features: ['global-views']},
- projects: [
- {id: 1, slug: 'staging-project', environments: ['staging'], isMember: false},
- {id: 2, slug: 'prod-project', environments: ['prod']},
- ],
- router: {
- location: {query: {project: [1]}},
- params: {orgId: 'org-slug'},
- },
- });
- jest.spyOn(ProjectsStore, 'getState').mockImplementation(() => ({
- projects: initialData.projects,
- loading: false,
- }));
- wrapper = mountWithTheme(
- <GlobalSelectionHeader
- router={initialData.router}
- organization={initialData.organization}
- projects={initialData.projects}
- />,
- changeQuery(initialData.routerContext, {project: 1})
- );
- await tick();
- wrapper.update();
- // Open environment picker
- wrapper.find('MultipleEnvironmentSelector HeaderItem').simulate('click');
- const checkboxes = wrapper.find('MultipleEnvironmentSelector AutoCompleteItem');
- expect(checkboxes).toHaveLength(1);
- expect(checkboxes.text()).toBe('staging');
- });
- it('updates GlobalSelection store with default period', async function () {
- wrapper = mountWithTheme(
- <GlobalSelectionHeader organization={organization} />,
- changeQuery(routerContext, {
- environment: 'prod',
- })
- );
- await tick();
- expect(GlobalSelectionStore.get()).toEqual({
- isReady: true,
- selection: {
- datetime: {
- period: '14d',
- utc: null,
- start: null,
- end: null,
- },
- environments: ['prod'],
- projects: [],
- },
- });
- // Not called because of the default date
- expect(router.replace).not.toHaveBeenCalled();
- });
- it('updates GlobalSelection store with empty dates in URL', async function () {
- wrapper = mountWithTheme(
- <GlobalSelectionHeader organization={organization} />,
- changeQuery(routerContext, {
- statsPeriod: null,
- })
- );
- await tick();
- expect(GlobalSelectionStore.get()).toEqual({
- isReady: true,
- selection: {
- datetime: {
- period: '14d',
- utc: null,
- start: null,
- end: null,
- },
- environments: [],
- projects: [],
- },
- });
- });
- it('resets start&end if showAbsolute prop is false', async function () {
- wrapper = mountWithTheme(
- <GlobalSelectionHeader organization={organization} showAbsolute={false} />,
- changeQuery(routerContext, {
- start: '2020-05-05T07:26:53.000',
- end: '2020-05-05T09:19:12.000',
- })
- );
- await tick();
- expect(GlobalSelectionStore.get()).toEqual({
- isReady: true,
- selection: {
- datetime: {
- period: '14d',
- utc: null,
- start: null,
- end: null,
- },
- environments: [],
- projects: [],
- },
- });
- });
- /**
- * I don't think this test is really applicable anymore
- */
- it('does not update store if url params have not changed', async function () {
- wrapper = mountWithTheme(
- <GlobalSelectionHeader organization={organization} />,
- changeQuery(routerContext, {
- statsPeriod: '7d',
- })
- );
- [
- globalActions.updateDateTime,
- globalActions.updateProjects,
- globalActions.updateEnvironments,
- ].forEach(mock => mock.mockClear());
- wrapper.setContext(
- changeQuery(routerContext, {
- statsPeriod: '7d',
- }).context
- );
- await tick();
- wrapper.update();
- expect(globalActions.updateDateTime).not.toHaveBeenCalled();
- expect(globalActions.updateProjects).not.toHaveBeenCalled();
- expect(globalActions.updateEnvironments).not.toHaveBeenCalled();
- expect(GlobalSelectionStore.get()).toEqual({
- isReady: true,
- selection: {
- datetime: {
- period: '7d',
- utc: null,
- start: null,
- end: null,
- },
- environments: [],
- projects: [],
- },
- });
- });
- it('loads from local storage when no URL parameters', async function () {
- getItem.mockImplementation(() =>
- JSON.stringify({projects: [3], environments: ['staging']})
- );
- const initializationObj = initializeOrg({
- organization: {
- features: ['global-views'],
- },
- router: {
- params: {orgId: 'org-slug'}, // we need this to be set to make sure org in context is same as current org in URL
- },
- });
- wrapper = mountWithTheme(
- <GlobalSelectionHeader organization={initializationObj.organization} />,
- initializationObj.routerContext
- );
- await tick(); // reflux tick
- expect(GlobalSelectionStore.get().selection.projects).toEqual([3]);
- // Since these are coming from URL, there should be no changes and
- // router does not need to be called
- expect(initializationObj.router.replace).toHaveBeenLastCalledWith(
- expect.objectContaining({
- query: {
- environment: ['staging'],
- project: [3],
- },
- })
- );
- });
- it('does not load from local storage when there are URL params', async function () {
- getItem.mockImplementation(() =>
- JSON.stringify({projects: [3], environments: ['staging']})
- );
- const initializationObj = initializeOrg({
- organization: {
- features: ['global-views'],
- },
- router: {
- params: {orgId: 'org-slug'}, // we need this to be set to make sure org in context is same as current org in URL
- location: {query: {project: [1, 2]}},
- },
- });
- wrapper = mountWithTheme(
- <GlobalSelectionHeader organization={initializationObj.organization} />,
- initializationObj.routerContext
- );
- await tick(); // reflux tick
- expect(GlobalSelectionStore.get().selection.projects).toEqual([1, 2]);
- // Since these are coming from URL, there should be no changes and
- // router does not need to be called
- expect(initializationObj.router.replace).not.toHaveBeenCalled();
- });
- it('updates store when there are query params in URL', async function () {
- const initializationObj = initializeOrg({
- organization: {
- features: ['global-views'],
- },
- router: {
- params: {orgId: 'org-slug'}, // we need this to be set to make sure org in context is same as current org in URL
- location: {query: {project: [1, 2]}},
- },
- });
- wrapper = mountWithTheme(
- <GlobalSelectionHeader organization={initializationObj.organization} />,
- initializationObj.routerContext
- );
- await tick(); // reflux tick
- expect(GlobalSelectionStore.get().selection.projects).toEqual([1, 2]);
- // Since these are coming from URL, there should be no changes and
- // router does not need to be called
- expect(initializationObj.router.replace).not.toHaveBeenCalled();
- });
- it('updates store with default values when there are no query params in URL', async function () {
- const initializationObj = initializeOrg({
- organization: {
- features: ['global-views'],
- },
- router: {
- params: {orgId: 'org-slug'}, // we need this to be set to make sure org in context is same as current org in URL
- location: {query: {}},
- },
- });
- wrapper = mountWithTheme(
- <GlobalSelectionHeader organization={initializationObj.organization} />,
- initializationObj.routerContext
- );
- // Router does not update because params have not changed
- expect(initializationObj.router.replace).not.toHaveBeenCalled();
- });
- /**
- * GSH: (no global-views)
- * - mounts with no state from router
- * - params org id === org.slug
- * - updateProjects should not be called (enforceSingleProject should not be called)
- * - componentDidUpdate with loadingProjects === true, and pass in list of projects (via projects store)
- * - enforceProject should be called and updateProjects() called with the new project
- * - variation:
- * - params.orgId !== org.slug (e.g. just switched orgs)
- *
- * When switching orgs when not in Issues view, the issues view gets rendered
- * with params.orgId !== org.slug
- * Global selection header gets unmounted and mounted, and in this case nothing should be done
- * until it gets updated and params.orgId === org.slug
- *
- * Separate issue:
- * IssuesList ("child view") renders before a single project is enforced, will require refactoring
- * views so that they depend on GSH enforcing a single project first IF they don't have required feature
- * (and no project id in URL).
- */
- describe('Single project selection mode', function () {
- it('does not do anything while organization is switching in single project', async function () {
- const initialData = initializeOrg({
- organization: {slug: 'old-org-slug'},
- router: {
- params: {orgId: 'org-slug'}, // we need this to be set to make sure org in context is same as current org in URL
- location: {query: {project: [1]}},
- },
- });
- ProjectsStore.getState.mockRestore();
- ProjectsStore.getAll.mockRestore();
- MockApiClient.addMockResponse({
- url: '/organizations/old-org-slug/projects/',
- body: [],
- });
- // This can happen when you switch organization so params.orgId !== the current org in context
- // In this case params.orgId = 'org-slug'
- wrapper = mountWithTheme(
- <GlobalSelectionHeader organization={initialData.organization} />,
- initialData.routerContext
- );
- expect(globalActions.updateProjects).not.toHaveBeenCalled();
- const updatedOrganization = {
- ...organization,
- slug: 'org-slug',
- features: [],
- projects: [TestStubs.Project({id: '123', slug: 'org-slug-project1'})],
- };
- MockApiClient.addMockResponse({
- url: '/organizations/org-slug/',
- body: updatedOrganization,
- });
- // Eventually OrganizationContext will fetch org details for `org-slug` and update `organization` prop
- // emulate fetchOrganizationDetails
- OrganizationActions.update(updatedOrganization);
- wrapper.setContext({
- organization: updatedOrganization,
- location: {query: {}},
- router: {
- ...initialData.router,
- location: {query: {}},
- },
- });
- wrapper.setProps({organization: updatedOrganization});
- ProjectsStore.loadInitialData(updatedOrganization.projects);
- expect(initialData.router.replace).toHaveBeenLastCalledWith(
- expect.objectContaining({
- query: {environment: [], project: [123]},
- })
- );
- });
- it('selects first project if more than one is requested', function () {
- const initializationObj = initializeOrg({
- router: {
- params: {orgId: 'org-slug'}, // we need this to be set to make sure org in context is same as current org in URL
- location: {query: {project: [1, 2]}},
- },
- });
- wrapper = mountWithTheme(
- <GlobalSelectionHeader organization={initializationObj.organization} />,
- initializationObj.routerContext
- );
- expect(initializationObj.router.replace).toHaveBeenCalledWith(
- expect.objectContaining({
- query: {environment: [], project: [1]},
- })
- );
- });
- it('selects first project if none (i.e. all) is requested', async function () {
- const project = TestStubs.Project({id: '3'});
- const org = TestStubs.Organization({projects: [project]});
- jest
- .spyOn(ProjectsStore, 'getState')
- .mockImplementation(() => ({projects: org.projects, loading: false}));
- const initializationObj = initializeOrg({
- organization: org,
- router: {
- params: {orgId: 'org-slug'},
- location: {query: {}},
- },
- });
- wrapper = mountWithTheme(
- <GlobalSelectionHeader organization={initializationObj.organization} />,
- initializationObj.routerContext
- );
- expect(initializationObj.router.replace).toHaveBeenCalledWith(
- expect.objectContaining({
- query: {environment: [], project: [3]},
- })
- );
- });
- });
- describe('forceProject selection mode', function () {
- beforeEach(function () {
- MockApiClient.addMockResponse({
- url: '/organizations/org-slug/projects/',
- body: [],
- });
- const initialData = initializeOrg({
- organization: {features: ['global-views']},
- projects: [
- {id: 1, slug: 'staging-project', environments: ['staging']},
- {id: 2, slug: 'prod-project', environments: ['prod']},
- ],
- router: {
- location: {query: {}},
- },
- });
- jest.spyOn(ProjectsStore, 'getState').mockImplementation(() => ({
- projects: initialData.organization.projects,
- loadingProjects: false,
- }));
- wrapper = mountWithTheme(
- <GlobalSelectionHeader
- organization={initialData.organization}
- shouldForceProject
- forceProject={initialData.organization.projects[0]}
- showIssueStreamLink
- />,
- initialData.routerContext
- );
- });
- it('renders a back button to the forced project', function () {
- const back = wrapper.find('BackButtonWrapper');
- expect(back).toHaveLength(1);
- });
- it('renders only environments from the forced project', async function () {
- await wrapper.find('MultipleEnvironmentSelector HeaderItem').simulate('click');
- await wrapper.update();
- const items = wrapper.find('MultipleEnvironmentSelector EnvironmentSelectorItem');
- expect(items.length).toEqual(1);
- expect(items.at(0).text()).toBe('staging');
- });
- });
- describe('without global-views (multi-project feature)', function () {
- describe('without existing URL params', function () {
- const initialData = initializeOrg({
- projects: [
- {id: 0, slug: 'random project', isMember: true},
- {id: 1, slug: 'staging-project', environments: ['staging']},
- {id: 2, slug: 'prod-project', environments: ['prod']},
- ],
- router: {
- location: {query: {}},
- params: {orgId: 'org-slug'},
- },
- });
- const createWrapper = props => {
- wrapper = mountWithTheme(
- <GlobalSelectionHeader
- params={{orgId: initialData.organization.slug}}
- organization={initialData.organization}
- {...props}
- />,
- initialData.routerContext
- );
- return wrapper;
- };
- beforeEach(function () {
- jest.spyOn(ProjectsStore, 'getState').mockImplementation(() => ({
- projects: initialData.organization.projects,
- loading: false,
- }));
- initialData.router.push.mockClear();
- initialData.router.replace.mockClear();
- });
- it('uses first project in org projects when mounting', async function () {
- createWrapper();
- await tick();
- wrapper.update();
- expect(initialData.router.replace).toHaveBeenLastCalledWith({
- pathname: undefined,
- query: {environment: [], project: [0]},
- });
- });
- it('appends projectId to URL when `forceProject` becomes available (async)', async function () {
- const mockProjectsStoreState = {
- projects: [],
- loading: true,
- };
- jest
- .spyOn(ProjectsStore, 'getState')
- .mockImplementation(() => mockProjectsStoreState);
- // forceProject generally starts undefined
- createWrapper({shouldForceProject: true});
- // load the projects
- mockProjectsStoreState.projects = initialData.organization.projects;
- mockProjectsStoreState.loading = false;
- wrapper.setProps({
- forceProject: initialData.organization.projects[1],
- });
- wrapper.update();
- expect(initialData.router.replace).toHaveBeenLastCalledWith({
- pathname: undefined,
- query: {environment: [], project: [1]},
- });
- expect(initialData.router.replace).toHaveBeenCalledTimes(1);
- });
- it('does not append projectId to URL when `forceProject` becomes available but project id already exists in URL', async function () {
- // forceProject generally starts undefined
- createWrapper({shouldForceProject: true});
- wrapper.setContext({
- router: {
- ...initialData.router,
- location: {
- ...initialData.router.location,
- query: {
- project: 321,
- },
- },
- },
- });
- wrapper.setProps({
- forceProject: initialData.organization.projects[1],
- });
- wrapper.update();
- expect(initialData.router.replace).not.toHaveBeenCalled();
- });
- it('appends projectId to URL when mounted with `forceProject`', async function () {
- // forceProject generally starts undefined
- createWrapper({
- shouldForceProject: true,
- forceProject: initialData.organization.projects[1],
- });
- wrapper.update();
- expect(initialData.router.replace).toHaveBeenLastCalledWith({
- pathname: undefined,
- query: {environment: [], project: [1]},
- });
- });
- });
- describe('with existing URL params', function () {
- const initialData = initializeOrg({
- projects: [
- {id: 0, slug: 'random project', isMember: true},
- {id: 1, slug: 'staging-project', environments: ['staging']},
- {id: 2, slug: 'prod-project', environments: ['prod']},
- ],
- router: {
- location: {query: {statsPeriod: '90d'}},
- params: {orgId: 'org-slug'},
- },
- });
- jest
- .spyOn(ProjectsStore, 'getAll')
- .mockImplementation(() => initialData.organization.projects);
- const createWrapper = props => {
- wrapper = mountWithTheme(
- <GlobalSelectionHeader
- params={{orgId: initialData.organization.slug}}
- organization={initialData.organization}
- {...props}
- />,
- initialData.routerContext
- );
- return wrapper;
- };
- beforeEach(function () {
- initialData.router.push.mockClear();
- initialData.router.replace.mockClear();
- });
- it('appends projectId to URL when mounted with `forceProject`', async function () {
- // forceProject generally starts undefined
- createWrapper({
- shouldForceProject: true,
- forceProject: initialData.organization.projects[1],
- });
- wrapper.update();
- expect(initialData.router.replace).toHaveBeenLastCalledWith({
- pathname: undefined,
- query: {environment: [], project: [1], statsPeriod: '90d'},
- });
- });
- });
- });
- describe('with global-views (multi-project feature)', function () {
- describe('without existing URL params', function () {
- const initialData = initializeOrg({
- organization: {features: ['global-views']},
- projects: [
- {id: 0, slug: 'random project', isMember: true},
- {id: 1, slug: 'staging-project', environments: ['staging']},
- {id: 2, slug: 'prod-project', environments: ['prod']},
- ],
- router: {
- location: {query: {}},
- params: {orgId: 'org-slug'},
- },
- });
- const createWrapper = (props, ctx) => {
- wrapper = mountWithTheme(
- <GlobalSelectionHeader
- params={{orgId: initialData.organization.slug}}
- organization={initialData.organization}
- {...props}
- />,
- {
- ...initialData.routerContext,
- ...ctx,
- }
- );
- return wrapper;
- };
- beforeEach(function () {
- jest.spyOn(ProjectsStore, 'getState').mockImplementation(() => ({
- projects: initialData.organization.projects,
- loading: false,
- }));
- initialData.router.push.mockClear();
- initialData.router.replace.mockClear();
- });
- it('does not use first project in org projects when mounting (and without localStorage data)', async function () {
- createWrapper();
- await tick();
- wrapper.update();
- expect(initialData.router.replace).not.toHaveBeenCalled();
- });
- it('does not append projectId to URL when `loadingProjects` changes and finishes loading', async function () {
- const mockProjectsStoreState = {
- projects: [],
- loading: true,
- };
- jest
- .spyOn(ProjectsStore, 'getState')
- .mockImplementation(() => mockProjectsStoreState);
- createWrapper();
- // load the projects
- mockProjectsStoreState.projects = initialData.organization.projects;
- mockProjectsStoreState.loading = false;
- wrapper.update();
- expect(initialData.router.replace).not.toHaveBeenCalled();
- });
- it('appends projectId to URL when `forceProject` becomes available (async)', async function () {
- const mockProjectsStoreState = {
- projects: [],
- loading: true,
- };
- jest
- .spyOn(ProjectsStore, 'getState')
- .mockImplementation(() => mockProjectsStoreState);
- // forceProject generally starts undefined
- createWrapper({shouldForceProject: true});
- await tick();
- // load the projects
- mockProjectsStoreState.projects = initialData.organization.projects;
- mockProjectsStoreState.loading = false;
- wrapper.setProps({
- forceProject: initialData.organization.projects[1],
- });
- wrapper.update();
- expect(initialData.router.replace).toHaveBeenLastCalledWith({
- pathname: undefined,
- query: {environment: [], project: [1]},
- });
- expect(initialData.router.replace).toHaveBeenCalledTimes(1);
- });
- it('does not append projectId to URL when `forceProject` becomes available but project id already exists in URL', async function () {
- // forceProject generally starts undefined
- createWrapper(
- {shouldForceProject: true},
- changeQuery(initialData.routerContext, {project: 321})
- );
- await tick();
- wrapper.setProps({
- forceProject: initialData.organization.projects[1],
- });
- wrapper.update();
- expect(initialData.router.replace).not.toHaveBeenCalled();
- });
- });
- });
- describe('projects list', function () {
- let memberProject, nonMemberProject, initialData;
- beforeEach(function () {
- memberProject = TestStubs.Project({id: '3', isMember: true});
- nonMemberProject = TestStubs.Project({id: '4', isMember: false});
- initialData = initializeOrg({
- organization: {
- projects: [memberProject, nonMemberProject],
- },
- router: {
- location: {query: {}},
- params: {
- orgId: 'org-slug',
- },
- },
- });
- jest.spyOn(ProjectsStore, 'getState').mockImplementation(() => ({
- projects: initialData.organization.projects,
- loading: false,
- }));
- wrapper = mountWithTheme(
- <GlobalSelectionHeader organization={initialData.organization} />,
- initialData.routerContext
- );
- });
- it('gets member projects', function () {
- expect(wrapper.find('MultipleProjectSelector').prop('projects')).toEqual([
- memberProject,
- ]);
- });
- it('gets all projects if superuser', function () {
- ConfigStore.config = {
- user: {
- isSuperuser: true,
- },
- };
- wrapper = mountWithTheme(
- <GlobalSelectionHeader organization={initialData.organization} />,
- initialData.routerContext
- );
- expect(wrapper.find('MultipleProjectSelector').prop('projects')).toEqual([
- memberProject,
- ]);
- expect(wrapper.find('MultipleProjectSelector').prop('nonMemberProjects')).toEqual([
- nonMemberProject,
- ]);
- });
- it('shows "My Projects" button', async function () {
- initialData.organization.features.push('global-views');
- wrapper = mountWithTheme(
- <GlobalSelectionHeader
- organization={initialData.organization}
- projects={initialData.organization.projects}
- />,
- initialData.routerContext
- );
- await tick();
- wrapper.update();
- // open the project menu.
- wrapper.find('MultipleProjectSelector HeaderItem').simulate('click');
- const projectSelector = wrapper.find('MultipleProjectSelector');
- // Two projects
- expect(projectSelector.find('AutoCompleteItem')).toHaveLength(2);
- // My projects in the footer
- expect(
- projectSelector.find('SelectorFooterControls Button').first().text()
- ).toEqual('View My Projects');
- });
- it('shows "All Projects" button based on features', async function () {
- initialData.organization.features.push('global-views');
- initialData.organization.features.push('open-membership');
- wrapper = mountWithTheme(
- <GlobalSelectionHeader
- organization={initialData.organization}
- projects={initialData.organization.projects}
- />,
- initialData.routerContext
- );
- await tick();
- wrapper.update();
- // open the project menu.
- wrapper.find('MultipleProjectSelector HeaderItem').simulate('click');
- const projectSelector = wrapper.find('MultipleProjectSelector');
- // Two projects
- expect(projectSelector.find('AutoCompleteItem')).toHaveLength(2);
- // All projects in the footer
- expect(
- projectSelector.find('SelectorFooterControls Button').first().text()
- ).toEqual('View All Projects');
- });
- it('shows "All Projects" button based on role', async function () {
- initialData.organization.features.push('global-views');
- initialData.organization.role = 'owner';
- wrapper = mountWithTheme(
- <GlobalSelectionHeader
- organization={initialData.organization}
- projects={initialData.organization.projects}
- />,
- initialData.routerContext
- );
- await tick();
- wrapper.update();
- // open the project menu.
- wrapper.find('MultipleProjectSelector HeaderItem').simulate('click');
- const projectSelector = wrapper.find('MultipleProjectSelector');
- // Two projects
- expect(projectSelector.find('AutoCompleteItem')).toHaveLength(2);
- // All projects in the footer
- expect(
- projectSelector.find('SelectorFooterControls Button').first().text()
- ).toEqual('View All Projects');
- });
- it('shows "My Projects" when "all projects" is selected', async function () {
- initialData.organization.features.push('global-views');
- initialData.organization.role = 'owner';
- wrapper = mountWithTheme(
- <GlobalSelectionHeader
- organization={initialData.organization}
- projects={initialData.organization.projects}
- />,
- changeQuery(initialData.routerContext, {project: -1})
- );
- await tick();
- wrapper.update();
- // open the project menu.
- wrapper.find('MultipleProjectSelector HeaderItem').simulate('click');
- const projectSelector = wrapper.find('MultipleProjectSelector');
- // My projects in the footer
- expect(
- projectSelector.find('SelectorFooterControls Button').first().text()
- ).toEqual('View My Projects');
- });
- });
- describe('project icons', function () {
- const initialData = initializeOrg({
- organization: {features: ['global-views']},
- projects: [
- {id: 0, slug: 'go', platform: 'go'},
- {id: 1, slug: 'javascript', platform: 'javascript'},
- {id: 2, slug: 'other', platform: 'other'},
- {id: 3, slug: 'php', platform: 'php'},
- {id: 4, slug: 'python', platform: 'python'},
- {id: 5, slug: 'rust', platform: 'rust'},
- {id: 6, slug: 'swift', platform: 'swift'},
- ],
- });
- beforeEach(function () {
- jest.spyOn(ProjectsStore, 'getState').mockImplementation(() => ({
- projects: initialData.organization.projects,
- loading: false,
- }));
- });
- it('shows IconProject when no projects are selected', async function () {
- wrapper = mountWithTheme(
- <GlobalSelectionHeader
- organization={initialData.organization}
- projects={initialData.organization.projects}
- />,
- changeQuery(initialData.routerContext, {project: -1})
- );
- await tick();
- wrapper.update();
- const projectSelector = wrapper.find('MultipleProjectSelector');
- expect(projectSelector.find('IconContainer svg').exists()).toBeTruthy();
- expect(projectSelector.find('PlatformIcon').exists()).toBeFalsy();
- expect(projectSelector.find('Content').text()).toEqual('All Projects');
- });
- it('shows PlatformIcon when one project is selected', async function () {
- wrapper = mountWithTheme(
- <GlobalSelectionHeader
- organization={initialData.organization}
- projects={initialData.organization.projects}
- />,
- changeQuery(initialData.routerContext, {project: 1})
- );
- await tick();
- wrapper.update();
- const projectSelector = wrapper.find('MultipleProjectSelector');
- expect(projectSelector.find('StyledPlatformIcon').props().platform).toEqual(
- 'javascript'
- );
- expect(projectSelector.find('Content').text()).toEqual('javascript');
- });
- it('shows multiple PlatformIcons when multiple projects are selected, no more than 5', async function () {
- wrapper = mountWithTheme(
- <GlobalSelectionHeader
- organization={initialData.organization}
- projects={initialData.organization.projects}
- />,
- initialData.routerContext
- );
- await tick();
- wrapper.update();
- // select 6 projects
- const headerItem = wrapper.find('MultipleProjectSelector HeaderItem');
- headerItem.simulate('click');
- wrapper
- .find('MultipleProjectSelector CheckboxFancy')
- .forEach(project => project.simulate('click'));
- headerItem.simulate('click');
- await tick();
- wrapper.update();
- // assert title and icons
- const title = wrapper.find('MultipleProjectSelector Content');
- const icons = wrapper.find('MultipleProjectSelector StyledPlatformIcon');
- expect(title.text()).toBe('javascript, other, php, python, rust, swift');
- expect(icons.length).toBe(5);
- expect(icons.at(3).props().platform).toBe('rust');
- expect(icons.at(4).props().platform).toBe('swift');
- });
- });
- });
|