index.spec.jsx 13 KB

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