index.spec.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. import {OrganizationFixture} from 'sentry-fixture/organization';
  2. import {ProjectFixture} from 'sentry-fixture/project';
  3. import {RouteComponentPropsFixture} from 'sentry-fixture/routeComponentPropsFixture';
  4. import {TeamFixture} from 'sentry-fixture/team';
  5. import {
  6. render,
  7. screen,
  8. userEvent,
  9. waitFor,
  10. within,
  11. } from 'sentry-test/reactTestingLibrary';
  12. import * as projectsActions from 'sentry/actionCreators/projects';
  13. import ProjectsStatsStore from 'sentry/stores/projectsStatsStore';
  14. import ProjectsStore from 'sentry/stores/projectsStore';
  15. import {Dashboard} from 'sentry/views/projectsDashboard';
  16. jest.mock('sentry/api');
  17. jest.unmock('lodash/debounce');
  18. jest.mock('lodash/debounce', () => {
  19. const debounceMap = new Map();
  20. const mockDebounce =
  21. (fn, timeout) =>
  22. (...args) => {
  23. if (debounceMap.has(fn)) {
  24. clearTimeout(debounceMap.get(fn));
  25. }
  26. debounceMap.set(
  27. fn,
  28. setTimeout(() => {
  29. fn.apply(fn, args);
  30. debounceMap.delete(fn);
  31. }, timeout)
  32. );
  33. };
  34. return mockDebounce;
  35. });
  36. describe('ProjectsDashboard', function () {
  37. const api = new MockApiClient();
  38. const org = OrganizationFixture();
  39. const team = TeamFixture();
  40. const teams = [team];
  41. beforeEach(function () {
  42. MockApiClient.addMockResponse({
  43. url: `/teams/${org.slug}/${team.slug}/members/`,
  44. body: [],
  45. });
  46. ProjectsStatsStore.reset();
  47. ProjectsStore.loadInitialData([]);
  48. });
  49. afterEach(function () {
  50. projectsActions._projectStatsToFetch.clear();
  51. MockApiClient.clearMockResponses();
  52. });
  53. describe('empty state', function () {
  54. it('renders with no projects', async function () {
  55. const noProjectTeams = [TeamFixture({isMember: false, projects: []})];
  56. render(
  57. <Dashboard
  58. api={api}
  59. error={null}
  60. loadingTeams={false}
  61. teams={noProjectTeams}
  62. organization={org}
  63. {...RouteComponentPropsFixture()}
  64. />
  65. );
  66. expect(
  67. await screen.findByRole('button', {name: 'Join a Team'})
  68. ).toBeInTheDocument();
  69. });
  70. it('renders with 1 project, with no first event', async function () {
  71. const projects = [ProjectFixture({teams, firstEvent: null})];
  72. ProjectsStore.loadInitialData(projects);
  73. const teamsWithOneProject = [TeamFixture({projects})];
  74. render(
  75. <Dashboard
  76. api={api}
  77. error={null}
  78. loadingTeams={false}
  79. teams={teamsWithOneProject}
  80. organization={org}
  81. {...RouteComponentPropsFixture()}
  82. />
  83. );
  84. expect(await screen.findByTestId('join-team')).toBeInTheDocument();
  85. expect(screen.getByTestId('create-project')).toBeInTheDocument();
  86. expect(
  87. screen.getByPlaceholderText('Search for projects by name')
  88. ).toBeInTheDocument();
  89. expect(screen.getByText('My Teams')).toBeInTheDocument();
  90. expect(screen.getByText('Resources')).toBeInTheDocument();
  91. expect(screen.getByTestId('badge-display-name')).toBeInTheDocument();
  92. });
  93. });
  94. describe('with projects', function () {
  95. it('renders with two projects', async function () {
  96. const teamA = TeamFixture({slug: 'team1', isMember: true});
  97. const projects = [
  98. ProjectFixture({
  99. id: '1',
  100. slug: 'project1',
  101. teams: [teamA],
  102. firstEvent: new Date().toISOString(),
  103. }),
  104. ProjectFixture({
  105. id: '2',
  106. slug: 'project2',
  107. teams: [teamA],
  108. isBookmarked: true,
  109. firstEvent: new Date().toISOString(),
  110. }),
  111. ];
  112. ProjectsStore.loadInitialData(projects);
  113. const teamsWithTwoProjects = [TeamFixture({projects})];
  114. render(
  115. <Dashboard
  116. api={api}
  117. error={null}
  118. loadingTeams={false}
  119. organization={org}
  120. teams={teamsWithTwoProjects}
  121. {...RouteComponentPropsFixture()}
  122. />
  123. );
  124. expect(await screen.findByText('My Teams')).toBeInTheDocument();
  125. expect(screen.getAllByTestId('badge-display-name')).toHaveLength(2);
  126. });
  127. it('renders correct project with selected team', async function () {
  128. const teamC = TeamFixture({
  129. id: '1',
  130. slug: 'teamC',
  131. isMember: true,
  132. projects: [
  133. ProjectFixture({
  134. id: '1',
  135. slug: 'project1',
  136. }),
  137. ProjectFixture({
  138. id: '2',
  139. slug: 'project2',
  140. }),
  141. ],
  142. });
  143. const teamD = TeamFixture({
  144. id: '2',
  145. slug: 'teamD',
  146. isMember: true,
  147. projects: [
  148. ProjectFixture({
  149. id: '3',
  150. slug: 'project3',
  151. }),
  152. ],
  153. });
  154. const teamsWithSpecificProjects = [teamC, teamD];
  155. MockApiClient.addMockResponse({
  156. url: `/organizations/${org.slug}/teams/?team=2`,
  157. body: teamsWithSpecificProjects,
  158. });
  159. const projects = [
  160. ProjectFixture({
  161. id: '1',
  162. slug: 'project1',
  163. teams: [teamC],
  164. firstEvent: new Date().toISOString(),
  165. stats: [],
  166. }),
  167. ProjectFixture({
  168. id: '2',
  169. slug: 'project2',
  170. teams: [teamC],
  171. isBookmarked: true,
  172. firstEvent: new Date().toISOString(),
  173. stats: [],
  174. }),
  175. ProjectFixture({
  176. id: '3',
  177. slug: 'project3',
  178. teams: [teamD],
  179. firstEvent: new Date().toISOString(),
  180. stats: [],
  181. }),
  182. ];
  183. ProjectsStore.loadInitialData(projects);
  184. MockApiClient.addMockResponse({
  185. url: `/organizations/${org.slug}/projects/`,
  186. body: projects,
  187. });
  188. render(
  189. <Dashboard
  190. api={api}
  191. error={null}
  192. loadingTeams={false}
  193. teams={teamsWithSpecificProjects}
  194. organization={org}
  195. {...RouteComponentPropsFixture({
  196. location: {
  197. pathname: '',
  198. hash: '',
  199. state: '',
  200. action: 'PUSH',
  201. key: '',
  202. query: {team: '2'},
  203. search: '?team=2`',
  204. },
  205. })}
  206. />
  207. );
  208. expect(await screen.findByText('project3')).toBeInTheDocument();
  209. expect(screen.queryByText('project2')).not.toBeInTheDocument();
  210. });
  211. it('renders projects by search', async function () {
  212. const teamA = TeamFixture({slug: 'team1', isMember: true});
  213. MockApiClient.addMockResponse({
  214. url: `/organizations/${org.slug}/projects/`,
  215. body: [],
  216. });
  217. const projects = [
  218. ProjectFixture({
  219. id: '1',
  220. slug: 'project1',
  221. teams: [teamA],
  222. firstEvent: new Date().toISOString(),
  223. }),
  224. ProjectFixture({
  225. id: '2',
  226. slug: 'project2',
  227. teams: [teamA],
  228. isBookmarked: true,
  229. firstEvent: new Date().toISOString(),
  230. }),
  231. ];
  232. ProjectsStore.loadInitialData(projects);
  233. const teamsWithTwoProjects = [TeamFixture({projects})];
  234. render(
  235. <Dashboard
  236. api={api}
  237. error={null}
  238. loadingTeams={false}
  239. teams={teamsWithTwoProjects}
  240. organization={org}
  241. {...RouteComponentPropsFixture()}
  242. />
  243. );
  244. await userEvent.type(
  245. screen.getByPlaceholderText('Search for projects by name'),
  246. 'project2{enter}'
  247. );
  248. expect(screen.getByText('project2')).toBeInTheDocument();
  249. await waitFor(() => {
  250. expect(screen.queryByText('project1')).not.toBeInTheDocument();
  251. });
  252. });
  253. it('renders bookmarked projects first in team list', async function () {
  254. const teamA = TeamFixture({slug: 'team1', isMember: true});
  255. const projects = [
  256. ProjectFixture({
  257. id: '11',
  258. slug: 'm',
  259. teams: [teamA],
  260. isBookmarked: false,
  261. stats: [],
  262. }),
  263. ProjectFixture({
  264. id: '12',
  265. slug: 'm-fave',
  266. teams: [teamA],
  267. isBookmarked: true,
  268. stats: [],
  269. }),
  270. ProjectFixture({
  271. id: '13',
  272. slug: 'a-fave',
  273. teams: [teamA],
  274. isBookmarked: true,
  275. stats: [],
  276. }),
  277. ProjectFixture({
  278. id: '14',
  279. slug: 'z-fave',
  280. teams: [teamA],
  281. isBookmarked: true,
  282. stats: [],
  283. }),
  284. ProjectFixture({
  285. id: '15',
  286. slug: 'a',
  287. teams: [teamA],
  288. isBookmarked: false,
  289. stats: [],
  290. }),
  291. ProjectFixture({
  292. id: '16',
  293. slug: 'z',
  294. teams: [teamA],
  295. isBookmarked: false,
  296. stats: [],
  297. }),
  298. ];
  299. ProjectsStore.loadInitialData(projects);
  300. const teamsWithFavProjects = [TeamFixture({projects})];
  301. MockApiClient.addMockResponse({
  302. url: `/organizations/${org.slug}/projects/`,
  303. body: [
  304. ProjectFixture({
  305. teams,
  306. stats: [
  307. [1517281200, 2],
  308. [1517310000, 1],
  309. ],
  310. }),
  311. ],
  312. });
  313. render(
  314. <Dashboard
  315. api={api}
  316. error={null}
  317. loadingTeams={false}
  318. organization={org}
  319. teams={teamsWithFavProjects}
  320. {...RouteComponentPropsFixture()}
  321. />
  322. );
  323. // check that all projects are displayed
  324. expect(await screen.findAllByTestId('badge-display-name')).toHaveLength(6);
  325. const projectName = screen.getAllByTestId('badge-display-name');
  326. // check that projects are in the correct order - alphabetical with bookmarked projects in front
  327. expect(within(projectName[0]).getByText('a-fave')).toBeInTheDocument();
  328. expect(within(projectName[1]).getByText('m-fave')).toBeInTheDocument();
  329. expect(within(projectName[2]).getByText('z-fave')).toBeInTheDocument();
  330. expect(within(projectName[3]).getByText('a')).toBeInTheDocument();
  331. expect(within(projectName[4]).getByText('m')).toBeInTheDocument();
  332. expect(within(projectName[5]).getByText('z')).toBeInTheDocument();
  333. });
  334. });
  335. describe('ProjectsStatsStore', function () {
  336. const teamA = TeamFixture({slug: 'team1', isMember: true});
  337. const projects = [
  338. ProjectFixture({
  339. id: '1',
  340. slug: 'm',
  341. teams,
  342. isBookmarked: false,
  343. }),
  344. ProjectFixture({
  345. id: '2',
  346. slug: 'm-fave',
  347. teams: [teamA],
  348. isBookmarked: true,
  349. }),
  350. ProjectFixture({
  351. id: '3',
  352. slug: 'a-fave',
  353. teams: [teamA],
  354. isBookmarked: true,
  355. }),
  356. ProjectFixture({
  357. id: '4',
  358. slug: 'z-fave',
  359. teams: [teamA],
  360. isBookmarked: true,
  361. }),
  362. ProjectFixture({
  363. id: '5',
  364. slug: 'a',
  365. teams: [teamA],
  366. isBookmarked: false,
  367. }),
  368. ProjectFixture({
  369. id: '6',
  370. slug: 'z',
  371. teams: [teamA],
  372. isBookmarked: false,
  373. }),
  374. ];
  375. const teamsWithStatTestProjects = [TeamFixture({projects})];
  376. it('uses ProjectsStatsStore to load stats', async function () {
  377. ProjectsStore.loadInitialData(projects);
  378. ProjectsStatsStore.onStatsLoadSuccess([{...projects[0], stats: [[1517281200, 2]]}]);
  379. const loadStatsSpy = jest.spyOn(projectsActions, 'loadStatsForProject');
  380. const mock = MockApiClient.addMockResponse({
  381. url: `/organizations/${org.slug}/projects/`,
  382. body: projects.map(project => ({
  383. ...project,
  384. stats: [
  385. [1517281200, 2],
  386. [1517310000, 1],
  387. ],
  388. })),
  389. });
  390. const {unmount} = render(
  391. <Dashboard
  392. api={api}
  393. error={null}
  394. loadingTeams={false}
  395. teams={teamsWithStatTestProjects}
  396. organization={org}
  397. {...RouteComponentPropsFixture()}
  398. />
  399. );
  400. expect(loadStatsSpy).toHaveBeenCalledTimes(6);
  401. // Advance timers so that batched request fires
  402. await waitFor(() => expect(mock).toHaveBeenCalledTimes(1));
  403. // query ids = 3, 2, 4 = bookmarked
  404. // 1 - already loaded in store so shouldn't be in query
  405. expect(mock).toHaveBeenCalledWith(
  406. expect.anything(),
  407. expect.objectContaining({
  408. query: expect.objectContaining({
  409. query: 'id:3 id:2 id:4 id:5 id:6',
  410. }),
  411. })
  412. );
  413. // All cards have loaded
  414. const projectSummary = await screen.findAllByTestId('summary-links');
  415. expect(within(projectSummary[0]).getByText('Errors: 3')).toBeInTheDocument();
  416. expect(within(projectSummary[1]).getByText('Errors: 3')).toBeInTheDocument();
  417. expect(within(projectSummary[2]).getByText('Errors: 3')).toBeInTheDocument();
  418. expect(within(projectSummary[3]).getByText('Errors: 3')).toBeInTheDocument();
  419. expect(within(projectSummary[4]).getByText('Errors: 3')).toBeInTheDocument();
  420. expect(within(projectSummary[5]).getByText('Errors: 3')).toBeInTheDocument();
  421. // Resets store when it unmounts
  422. unmount();
  423. expect(ProjectsStatsStore.getAll()).toEqual({});
  424. });
  425. it('renders an error from withTeamsForUser', function () {
  426. ProjectsStore.loadInitialData(projects);
  427. render(
  428. <Dashboard
  429. api={api}
  430. loadingTeams={false}
  431. error={Error('uhoh')}
  432. organization={org}
  433. teams={[]}
  434. {...RouteComponentPropsFixture()}
  435. />
  436. );
  437. expect(
  438. screen.getByText('An error occurred while fetching your projects')
  439. ).toBeInTheDocument();
  440. });
  441. });
  442. });