123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529 |
- import {OrganizationFixture} from 'sentry-fixture/organization';
- import {ProjectFixture} from 'sentry-fixture/project';
- import {RouteComponentPropsFixture} from 'sentry-fixture/routeComponentPropsFixture';
- import {TeamFixture} from 'sentry-fixture/team';
- import {
- act,
- render,
- screen,
- userEvent,
- waitFor,
- within,
- } from 'sentry-test/reactTestingLibrary';
- import * as projectsActions from 'sentry/actionCreators/projects';
- import ProjectsStatsStore from 'sentry/stores/projectsStatsStore';
- import ProjectsStore from 'sentry/stores/projectsStore';
- import {Dashboard} from 'sentry/views/projectsDashboard';
- jest.mock('sentry/api');
- jest.unmock('lodash/debounce');
- jest.mock('lodash/debounce', () => {
- const debounceMap = new Map();
- const mockDebounce =
- (fn, timeout) =>
- (...args) => {
- if (debounceMap.has(fn)) {
- clearTimeout(debounceMap.get(fn));
- }
- debounceMap.set(
- fn,
- setTimeout(() => {
- fn.apply(fn, args);
- debounceMap.delete(fn);
- }, timeout)
- );
- };
- return mockDebounce;
- });
- describe('ProjectsDashboard', function () {
- const api = new MockApiClient();
- const org = OrganizationFixture();
- const team = TeamFixture();
- const teams = [team];
- beforeEach(function () {
- MockApiClient.addMockResponse({
- url: `/teams/${org.slug}/${team.slug}/members/`,
- body: [],
- });
- MockApiClient.addMockResponse({
- url: `/organizations/${org.slug}/projects/`,
- body: [],
- });
- ProjectsStatsStore.reset();
- ProjectsStore.loadInitialData([]);
- });
- afterEach(function () {
- projectsActions._projectStatsToFetch.clear();
- MockApiClient.clearMockResponses();
- });
- describe('empty state', function () {
- it('renders with no projects', async function () {
- const noProjectTeams = [TeamFixture({isMember: false, projects: []})];
- render(
- <Dashboard
- api={api}
- error={null}
- loadingTeams={false}
- teams={noProjectTeams}
- organization={org}
- {...RouteComponentPropsFixture()}
- />
- );
- expect(
- await screen.findByRole('button', {name: 'Join a Team'})
- ).toBeInTheDocument();
- expect(screen.getByTestId('create-project')).toBeInTheDocument();
- expect(screen.queryByTestId('loading-placeholder')).not.toBeInTheDocument();
- });
- it('renders with 1 project, with no first event', async function () {
- const projects = [ProjectFixture({teams, firstEvent: null, stats: []})];
- ProjectsStore.loadInitialData(projects);
- const teamsWithOneProject = [TeamFixture({projects})];
- render(
- <Dashboard
- api={api}
- error={null}
- loadingTeams={false}
- teams={teamsWithOneProject}
- organization={org}
- {...RouteComponentPropsFixture()}
- />
- );
- expect(await screen.findByTestId('join-team')).toBeInTheDocument();
- expect(screen.getByTestId('create-project')).toBeInTheDocument();
- expect(
- screen.getByPlaceholderText('Search for projects by name')
- ).toBeInTheDocument();
- expect(screen.getByText('My Teams')).toBeInTheDocument();
- expect(screen.getByText('Resources')).toBeInTheDocument();
- expect(screen.getByTestId('badge-display-name')).toBeInTheDocument();
- expect(screen.queryByTestId('loading-placeholder')).not.toBeInTheDocument();
- });
- });
- describe('with projects', function () {
- it('renders with two projects', async function () {
- const teamA = TeamFixture({slug: 'team1', isMember: true});
- const projects = [
- ProjectFixture({
- id: '1',
- slug: 'project1',
- teams: [teamA],
- firstEvent: new Date().toISOString(),
- stats: [],
- }),
- ProjectFixture({
- id: '2',
- slug: 'project2',
- teams: [teamA],
- isBookmarked: true,
- firstEvent: new Date().toISOString(),
- stats: [],
- }),
- ];
- ProjectsStore.loadInitialData(projects);
- const teamsWithTwoProjects = [TeamFixture({projects})];
- render(
- <Dashboard
- api={api}
- error={null}
- loadingTeams={false}
- organization={org}
- teams={teamsWithTwoProjects}
- {...RouteComponentPropsFixture()}
- />
- );
- expect(await screen.findByText('My Teams')).toBeInTheDocument();
- expect(screen.getAllByTestId('badge-display-name')).toHaveLength(2);
- expect(screen.queryByTestId('loading-placeholder')).not.toBeInTheDocument();
- });
- it('renders correct project with selected team', async function () {
- const teamC = TeamFixture({
- id: '1',
- slug: 'teamC',
- isMember: true,
- projects: [
- ProjectFixture({
- id: '1',
- slug: 'project1',
- stats: [],
- }),
- ProjectFixture({
- id: '2',
- slug: 'project2',
- stats: [],
- }),
- ],
- });
- const teamD = TeamFixture({
- id: '2',
- slug: 'teamD',
- isMember: true,
- projects: [
- ProjectFixture({
- id: '3',
- slug: 'project3',
- }),
- ],
- });
- const teamsWithSpecificProjects = [teamC, teamD];
- MockApiClient.addMockResponse({
- url: `/organizations/${org.slug}/teams/?team=2`,
- body: teamsWithSpecificProjects,
- });
- const projects = [
- ProjectFixture({
- id: '1',
- slug: 'project1',
- teams: [teamC],
- firstEvent: new Date().toISOString(),
- stats: [],
- }),
- ProjectFixture({
- id: '2',
- slug: 'project2',
- teams: [teamC],
- isBookmarked: true,
- firstEvent: new Date().toISOString(),
- stats: [],
- }),
- ProjectFixture({
- id: '3',
- slug: 'project3',
- teams: [teamD],
- firstEvent: new Date().toISOString(),
- stats: [],
- }),
- ];
- ProjectsStore.loadInitialData(projects);
- MockApiClient.addMockResponse({
- url: `/organizations/${org.slug}/projects/`,
- body: projects,
- });
- render(
- <Dashboard
- api={api}
- error={null}
- loadingTeams={false}
- teams={teamsWithSpecificProjects}
- organization={org}
- {...RouteComponentPropsFixture({
- location: {
- pathname: '',
- hash: '',
- state: '',
- action: 'PUSH',
- key: '',
- query: {team: '2'},
- search: '?team=2`',
- },
- })}
- />
- );
- expect(await screen.findByText('project3')).toBeInTheDocument();
- expect(screen.queryByText('project2')).not.toBeInTheDocument();
- });
- it('renders projects by search', async function () {
- const teamA = TeamFixture({slug: 'team1', isMember: true});
- MockApiClient.addMockResponse({
- url: `/organizations/${org.slug}/projects/`,
- body: [],
- });
- const projects = [
- ProjectFixture({
- id: '1',
- slug: 'project1',
- teams: [teamA],
- firstEvent: new Date().toISOString(),
- stats: [],
- }),
- ProjectFixture({
- id: '2',
- slug: 'project2',
- teams: [teamA],
- isBookmarked: true,
- firstEvent: new Date().toISOString(),
- stats: [],
- }),
- ];
- ProjectsStore.loadInitialData(projects);
- const teamsWithTwoProjects = [TeamFixture({projects})];
- render(
- <Dashboard
- api={api}
- error={null}
- loadingTeams={false}
- teams={teamsWithTwoProjects}
- organization={org}
- {...RouteComponentPropsFixture()}
- />
- );
- await userEvent.type(
- screen.getByPlaceholderText('Search for projects by name'),
- 'project2{enter}'
- );
- expect(screen.getByText('project2')).toBeInTheDocument();
- await waitFor(() => {
- expect(screen.queryByText('project1')).not.toBeInTheDocument();
- });
- expect(screen.queryByTestId('loading-placeholder')).not.toBeInTheDocument();
- });
- it('renders bookmarked projects first in team list', async function () {
- const teamA = TeamFixture({slug: 'team1', isMember: true});
- const projects = [
- ProjectFixture({
- id: '11',
- slug: 'm',
- teams: [teamA],
- isBookmarked: false,
- stats: [],
- }),
- ProjectFixture({
- id: '12',
- slug: 'm-fave',
- teams: [teamA],
- isBookmarked: true,
- stats: [],
- }),
- ProjectFixture({
- id: '13',
- slug: 'a-fave',
- teams: [teamA],
- isBookmarked: true,
- stats: [],
- }),
- ProjectFixture({
- id: '14',
- slug: 'z-fave',
- teams: [teamA],
- isBookmarked: true,
- stats: [],
- }),
- ProjectFixture({
- id: '15',
- slug: 'a',
- teams: [teamA],
- isBookmarked: false,
- stats: [],
- }),
- ProjectFixture({
- id: '16',
- slug: 'z',
- teams: [teamA],
- isBookmarked: false,
- stats: [],
- }),
- ];
- ProjectsStore.loadInitialData(projects);
- const teamsWithFavProjects = [TeamFixture({projects})];
- MockApiClient.addMockResponse({
- url: `/organizations/${org.slug}/projects/`,
- body: [
- ProjectFixture({
- teams,
- stats: [
- [1517281200, 2],
- [1517310000, 1],
- ],
- }),
- ],
- });
- render(
- <Dashboard
- api={api}
- error={null}
- loadingTeams={false}
- organization={org}
- teams={teamsWithFavProjects}
- {...RouteComponentPropsFixture()}
- />
- );
- // check that all projects are displayed
- await waitFor(() =>
- expect(screen.getAllByTestId('badge-display-name')).toHaveLength(6)
- );
- const projectName = screen.getAllByTestId('badge-display-name');
- // check that projects are in the correct order - alphabetical with bookmarked projects in front
- expect(within(projectName[0]).getByText('a-fave')).toBeInTheDocument();
- expect(within(projectName[1]).getByText('m-fave')).toBeInTheDocument();
- expect(within(projectName[2]).getByText('z-fave')).toBeInTheDocument();
- expect(within(projectName[3]).getByText('a')).toBeInTheDocument();
- expect(within(projectName[4]).getByText('m')).toBeInTheDocument();
- expect(within(projectName[5]).getByText('z')).toBeInTheDocument();
- });
- });
- describe('ProjectsStatsStore', function () {
- const teamA = TeamFixture({slug: 'team1', isMember: true});
- const projects = [
- ProjectFixture({
- id: '1',
- slug: 'm',
- teams,
- isBookmarked: false,
- }),
- ProjectFixture({
- id: '2',
- slug: 'm-fave',
- teams: [teamA],
- isBookmarked: true,
- }),
- ProjectFixture({
- id: '3',
- slug: 'a-fave',
- teams: [teamA],
- isBookmarked: true,
- }),
- ProjectFixture({
- id: '4',
- slug: 'z-fave',
- teams: [teamA],
- isBookmarked: true,
- }),
- ProjectFixture({
- id: '5',
- slug: 'a',
- teams: [teamA],
- isBookmarked: false,
- }),
- ProjectFixture({
- id: '6',
- slug: 'z',
- teams: [teamA],
- isBookmarked: false,
- }),
- ];
- const teamsWithStatTestProjects = [TeamFixture({projects})];
- it('uses ProjectsStatsStore to load stats', async function () {
- ProjectsStore.loadInitialData(projects);
- jest.useFakeTimers();
- ProjectsStatsStore.onStatsLoadSuccess([{...projects[0], stats: [[1517281200, 2]]}]);
- const loadStatsSpy = jest.spyOn(projectsActions, 'loadStatsForProject');
- const mock = MockApiClient.addMockResponse({
- url: `/organizations/${org.slug}/projects/`,
- body: projects.map(project => ({
- ...project,
- stats: [
- [1517281200, 2],
- [1517310000, 1],
- ],
- })),
- });
- const {unmount} = render(
- <Dashboard
- api={api}
- error={null}
- loadingTeams={false}
- teams={teamsWithStatTestProjects}
- organization={org}
- {...RouteComponentPropsFixture()}
- />
- );
- expect(loadStatsSpy).toHaveBeenCalledTimes(6);
- expect(mock).not.toHaveBeenCalled();
- const projectSummary = screen.getAllByTestId('summary-links');
- // Has 5 Loading Cards because 1 project has been loaded in store already
- expect(
- within(projectSummary[0]).getByTestId('loading-placeholder')
- ).toBeInTheDocument();
- expect(
- within(projectSummary[1]).getByTestId('loading-placeholder')
- ).toBeInTheDocument();
- expect(
- within(projectSummary[2]).getByTestId('loading-placeholder')
- ).toBeInTheDocument();
- expect(
- within(projectSummary[3]).getByTestId('loading-placeholder')
- ).toBeInTheDocument();
- expect(within(projectSummary[4]).getByText('Errors: 2')).toBeInTheDocument();
- expect(
- within(projectSummary[5]).getByTestId('loading-placeholder')
- ).toBeInTheDocument();
- // Advance timers so that batched request fires
- act(() => jest.advanceTimersByTime(51));
- expect(mock).toHaveBeenCalledTimes(1);
- // query ids = 3, 2, 4 = bookmarked
- // 1 - already loaded in store so shouldn't be in query
- expect(mock).toHaveBeenCalledWith(
- expect.anything(),
- expect.objectContaining({
- query: expect.objectContaining({
- query: 'id:3 id:2 id:4 id:5 id:6',
- }),
- })
- );
- jest.useRealTimers();
- // All cards have loaded
- await waitFor(() => {
- expect(within(projectSummary[0]).getByText('Errors: 3')).toBeInTheDocument();
- });
- expect(within(projectSummary[1]).getByText('Errors: 3')).toBeInTheDocument();
- expect(within(projectSummary[2]).getByText('Errors: 3')).toBeInTheDocument();
- expect(within(projectSummary[3]).getByText('Errors: 3')).toBeInTheDocument();
- expect(within(projectSummary[4]).getByText('Errors: 3')).toBeInTheDocument();
- expect(within(projectSummary[5]).getByText('Errors: 3')).toBeInTheDocument();
- // Resets store when it unmounts
- unmount();
- expect(ProjectsStatsStore.getAll()).toEqual({});
- });
- it('renders an error from withTeamsForUser', function () {
- ProjectsStore.loadInitialData(projects);
- render(
- <Dashboard
- api={api}
- loadingTeams={false}
- error={Error('uhoh')}
- organization={org}
- teams={[]}
- {...RouteComponentPropsFixture()}
- />
- );
- expect(
- screen.getByText('An error occurred while fetching your projects')
- ).toBeInTheDocument();
- });
- });
- });
|