detail.spec.tsx 47 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541
  1. import {browserHistory} from 'react-router';
  2. import {Organization} from 'sentry-fixture/organization';
  3. import {initializeOrg} from 'sentry-test/initializeOrg';
  4. import {
  5. act,
  6. render,
  7. screen,
  8. userEvent,
  9. waitFor,
  10. within,
  11. } from 'sentry-test/reactTestingLibrary';
  12. import * as modals from 'sentry/actionCreators/modal';
  13. import ProjectsStore from 'sentry/stores/projectsStore';
  14. import CreateDashboard from 'sentry/views/dashboards/create';
  15. import * as types from 'sentry/views/dashboards/types';
  16. import ViewEditDashboard from 'sentry/views/dashboards/view';
  17. import {OrganizationContext} from 'sentry/views/organizationContext';
  18. describe('Dashboards > Detail', function () {
  19. const organization = Organization({
  20. features: ['global-views', 'dashboards-basic', 'dashboards-edit', 'discover-query'],
  21. });
  22. const projects = [TestStubs.Project()];
  23. describe('prebuilt dashboards', function () {
  24. let initialData;
  25. beforeEach(function () {
  26. act(() => ProjectsStore.loadInitialData(projects));
  27. initialData = initializeOrg({organization});
  28. MockApiClient.addMockResponse({
  29. url: '/organizations/org-slug/tags/',
  30. body: [],
  31. });
  32. MockApiClient.addMockResponse({
  33. url: '/organizations/org-slug/projects/',
  34. body: [TestStubs.Project()],
  35. });
  36. MockApiClient.addMockResponse({
  37. url: '/organizations/org-slug/dashboards/',
  38. body: [
  39. TestStubs.Dashboard([], {id: 'default-overview', title: 'Default'}),
  40. TestStubs.Dashboard([], {id: '1', title: 'Custom Errors'}),
  41. ],
  42. });
  43. MockApiClient.addMockResponse({
  44. url: '/organizations/org-slug/dashboards/default-overview/',
  45. body: TestStubs.Dashboard([], {id: 'default-overview', title: 'Default'}),
  46. });
  47. MockApiClient.addMockResponse({
  48. url: '/organizations/org-slug/dashboards/1/visit/',
  49. method: 'POST',
  50. body: [],
  51. statusCode: 200,
  52. });
  53. MockApiClient.addMockResponse({
  54. url: '/organizations/org-slug/users/',
  55. method: 'GET',
  56. body: [],
  57. });
  58. MockApiClient.addMockResponse({
  59. url: '/organizations/org-slug/sdk-updates/',
  60. body: [],
  61. });
  62. MockApiClient.addMockResponse({
  63. url: '/prompts-activity/',
  64. body: {},
  65. });
  66. MockApiClient.addMockResponse({
  67. url: '/organizations/org-slug/events/',
  68. method: 'GET',
  69. body: [],
  70. });
  71. MockApiClient.addMockResponse({
  72. url: '/organizations/org-slug/events-stats/',
  73. body: {data: []},
  74. });
  75. MockApiClient.addMockResponse({
  76. method: 'GET',
  77. url: '/organizations/org-slug/issues/',
  78. body: [],
  79. });
  80. MockApiClient.addMockResponse({
  81. url: '/organizations/org-slug/releases/',
  82. body: [],
  83. });
  84. });
  85. afterEach(function () {
  86. MockApiClient.clearMockResponses();
  87. });
  88. it('assigns unique IDs to all widgets so grid keys are unique', async function () {
  89. MockApiClient.addMockResponse({
  90. url: '/organizations/org-slug/events-stats/',
  91. body: {data: []},
  92. });
  93. MockApiClient.addMockResponse({
  94. url: '/organizations/org-slug/dashboards/default-overview/',
  95. body: TestStubs.Dashboard(
  96. [
  97. TestStubs.Widget(
  98. [
  99. {
  100. name: '',
  101. conditions: 'event.type:error',
  102. fields: ['count()'],
  103. aggregates: ['count()'],
  104. columns: [],
  105. },
  106. ],
  107. {
  108. title: 'Default Widget 1',
  109. interval: '1d',
  110. }
  111. ),
  112. TestStubs.Widget(
  113. [
  114. {
  115. name: '',
  116. conditions: 'event.type:transaction',
  117. fields: ['count()'],
  118. aggregates: ['count()'],
  119. columns: [],
  120. },
  121. ],
  122. {
  123. title: 'Default Widget 2',
  124. interval: '1d',
  125. }
  126. ),
  127. ],
  128. {id: 'default-overview', title: 'Default'}
  129. ),
  130. });
  131. initialData = initializeOrg({
  132. organization: Organization({
  133. features: ['global-views', 'dashboards-basic', 'discover-query'],
  134. projects: [TestStubs.Project()],
  135. }),
  136. });
  137. render(
  138. <OrganizationContext.Provider value={initialData.organization}>
  139. <ViewEditDashboard
  140. {...TestStubs.routeComponentProps()}
  141. organization={initialData.organization}
  142. params={{orgId: 'org-slug', dashboardId: 'default-overview'}}
  143. router={initialData.router}
  144. location={initialData.router.location}
  145. >
  146. {null}
  147. </ViewEditDashboard>
  148. </OrganizationContext.Provider>,
  149. {context: initialData.routerContext}
  150. );
  151. expect(await screen.findByText('Default Widget 1')).toBeInTheDocument();
  152. expect(screen.getByText('Default Widget 2')).toBeInTheDocument();
  153. });
  154. it('opens the widget viewer modal in a prebuilt dashboard using the widget id specified in the url', async () => {
  155. const openWidgetViewerModal = jest.spyOn(modals, 'openWidgetViewerModal');
  156. render(
  157. <CreateDashboard
  158. {...TestStubs.routeComponentProps()}
  159. organization={initialData.organization}
  160. params={{templateId: 'default-template', widgetId: '2'}}
  161. router={initialData.router}
  162. location={{...initialData.router.location, pathname: '/widget/2/'}}
  163. >
  164. {null}
  165. </CreateDashboard>,
  166. {context: initialData.routerContext, organization: initialData.organization}
  167. );
  168. await waitFor(() => {
  169. expect(openWidgetViewerModal).toHaveBeenCalledWith(
  170. expect.objectContaining({
  171. organization: initialData.organization,
  172. widget: expect.objectContaining({
  173. displayType: 'line',
  174. interval: '5m',
  175. queries: [
  176. {
  177. aggregates: ['count()'],
  178. columns: [],
  179. conditions: '!event.type:transaction',
  180. fields: ['count()'],
  181. name: 'Events',
  182. orderby: 'count()',
  183. },
  184. ],
  185. title: 'Events',
  186. widgetType: 'discover',
  187. }),
  188. onClose: expect.anything(),
  189. })
  190. );
  191. });
  192. });
  193. });
  194. describe('custom dashboards', function () {
  195. let initialData, widgets, mockVisit, mockPut;
  196. beforeEach(function () {
  197. window.confirm = jest.fn();
  198. initialData = initializeOrg({
  199. organization,
  200. router: {
  201. location: TestStubs.location(),
  202. },
  203. });
  204. widgets = [
  205. TestStubs.Widget(
  206. [
  207. {
  208. name: '',
  209. conditions: 'event.type:error',
  210. fields: ['count()'],
  211. columns: [],
  212. aggregates: ['count()'],
  213. },
  214. ],
  215. {
  216. title: 'Errors',
  217. interval: '1d',
  218. widgetType: 'discover',
  219. id: '1',
  220. }
  221. ),
  222. TestStubs.Widget(
  223. [
  224. {
  225. name: '',
  226. conditions: 'event.type:transaction',
  227. fields: ['count()'],
  228. columns: [],
  229. aggregates: ['count()'],
  230. },
  231. ],
  232. {
  233. title: 'Transactions',
  234. interval: '1d',
  235. widgetType: 'discover',
  236. id: '2',
  237. }
  238. ),
  239. TestStubs.Widget(
  240. [
  241. {
  242. name: '',
  243. conditions: 'event.type:transaction transaction:/api/cats',
  244. fields: ['p50()'],
  245. columns: [],
  246. aggregates: ['p50()'],
  247. },
  248. ],
  249. {
  250. title: 'p50 of /api/cats',
  251. interval: '1d',
  252. id: '3',
  253. }
  254. ),
  255. ];
  256. mockVisit = MockApiClient.addMockResponse({
  257. url: '/organizations/org-slug/dashboards/1/visit/',
  258. method: 'POST',
  259. body: [],
  260. statusCode: 200,
  261. });
  262. MockApiClient.addMockResponse({
  263. url: '/organizations/org-slug/tags/',
  264. body: [],
  265. });
  266. MockApiClient.addMockResponse({
  267. url: '/organizations/org-slug/projects/',
  268. body: [TestStubs.Project()],
  269. });
  270. MockApiClient.addMockResponse({
  271. url: '/organizations/org-slug/dashboards/',
  272. body: [
  273. TestStubs.Dashboard([], {
  274. id: 'default-overview',
  275. title: 'Default',
  276. widgetDisplay: ['area'],
  277. }),
  278. TestStubs.Dashboard([], {
  279. id: '1',
  280. title: 'Custom Errors',
  281. widgetDisplay: ['area'],
  282. }),
  283. ],
  284. });
  285. MockApiClient.addMockResponse({
  286. url: '/organizations/org-slug/dashboards/1/',
  287. body: TestStubs.Dashboard(widgets, {
  288. id: '1',
  289. title: 'Custom Errors',
  290. filters: {},
  291. }),
  292. });
  293. mockPut = MockApiClient.addMockResponse({
  294. url: '/organizations/org-slug/dashboards/1/',
  295. method: 'PUT',
  296. body: TestStubs.Dashboard(widgets, {id: '1', title: 'Custom Errors'}),
  297. });
  298. MockApiClient.addMockResponse({
  299. url: '/organizations/org-slug/events-stats/',
  300. body: {data: []},
  301. });
  302. MockApiClient.addMockResponse({
  303. method: 'POST',
  304. url: '/organizations/org-slug/dashboards/widgets/',
  305. body: [],
  306. });
  307. MockApiClient.addMockResponse({
  308. method: 'GET',
  309. url: '/organizations/org-slug/recent-searches/',
  310. body: [],
  311. });
  312. MockApiClient.addMockResponse({
  313. method: 'GET',
  314. url: '/organizations/org-slug/issues/',
  315. body: [],
  316. });
  317. MockApiClient.addMockResponse({
  318. url: '/organizations/org-slug/events/',
  319. method: 'GET',
  320. body: [],
  321. });
  322. MockApiClient.addMockResponse({
  323. url: '/organizations/org-slug/users/',
  324. method: 'GET',
  325. body: [],
  326. });
  327. MockApiClient.addMockResponse({
  328. url: '/organizations/org-slug/releases/',
  329. body: [],
  330. });
  331. MockApiClient.addMockResponse({
  332. url: '/organizations/org-slug/sdk-updates/',
  333. body: [],
  334. });
  335. MockApiClient.addMockResponse({
  336. url: '/prompts-activity/',
  337. body: {},
  338. });
  339. });
  340. afterEach(function () {
  341. MockApiClient.clearMockResponses();
  342. jest.clearAllMocks();
  343. });
  344. it('can remove widgets', async function () {
  345. const updateMock = MockApiClient.addMockResponse({
  346. url: '/organizations/org-slug/dashboards/1/',
  347. method: 'PUT',
  348. body: TestStubs.Dashboard([widgets[0]], {id: '1', title: 'Custom Errors'}),
  349. });
  350. render(
  351. <OrganizationContext.Provider value={initialData.organization}>
  352. <ViewEditDashboard
  353. {...TestStubs.routeComponentProps()}
  354. organization={initialData.organization}
  355. params={{orgId: 'org-slug', dashboardId: '1'}}
  356. router={initialData.router}
  357. location={initialData.router.location}
  358. >
  359. {null}
  360. </ViewEditDashboard>
  361. </OrganizationContext.Provider>,
  362. {context: initialData.routerContext}
  363. );
  364. await waitFor(() => expect(mockVisit).toHaveBeenCalledTimes(1));
  365. // Enter edit mode.
  366. await userEvent.click(screen.getByRole('button', {name: 'Edit Dashboard'}));
  367. // Remove the second and third widgets
  368. await userEvent.click(screen.getAllByRole('button', {name: 'Delete Widget'})[1]);
  369. await userEvent.click(screen.getAllByRole('button', {name: 'Delete Widget'})[1]);
  370. // Save changes
  371. await userEvent.click(screen.getByRole('button', {name: 'Save and Finish'}));
  372. expect(updateMock).toHaveBeenCalled();
  373. expect(updateMock).toHaveBeenCalledWith(
  374. '/organizations/org-slug/dashboards/1/',
  375. expect.objectContaining({
  376. data: expect.objectContaining({
  377. title: 'Custom Errors',
  378. widgets: [expect.objectContaining(widgets[0])],
  379. }),
  380. })
  381. );
  382. // Visit should not be called again on dashboard update
  383. expect(mockVisit).toHaveBeenCalledTimes(1);
  384. });
  385. it('appends dashboard-level filters to series request', async function () {
  386. MockApiClient.addMockResponse({
  387. url: '/organizations/org-slug/dashboards/1/',
  388. body: TestStubs.Dashboard(widgets, {
  389. id: '1',
  390. title: 'Custom Errors',
  391. filters: {release: ['abc@1.2.0']},
  392. }),
  393. });
  394. const mock = MockApiClient.addMockResponse({
  395. url: '/organizations/org-slug/events-stats/',
  396. body: [],
  397. });
  398. render(
  399. <OrganizationContext.Provider value={initialData.organization}>
  400. <ViewEditDashboard
  401. {...TestStubs.routeComponentProps()}
  402. organization={initialData.organization}
  403. params={{orgId: 'org-slug', dashboardId: '1'}}
  404. router={initialData.router}
  405. location={initialData.router.location}
  406. >
  407. {null}
  408. </ViewEditDashboard>
  409. </OrganizationContext.Provider>,
  410. {context: initialData.routerContext}
  411. );
  412. await waitFor(() =>
  413. expect(mock).toHaveBeenLastCalledWith(
  414. '/organizations/org-slug/events-stats/',
  415. expect.objectContaining({
  416. query: expect.objectContaining({
  417. query: 'event.type:transaction transaction:/api/cats release:abc@1.2.0 ',
  418. }),
  419. })
  420. )
  421. );
  422. });
  423. it('shows add widget option', async function () {
  424. render(
  425. <OrganizationContext.Provider value={initialData.organization}>
  426. <ViewEditDashboard
  427. {...TestStubs.routeComponentProps()}
  428. organization={initialData.organization}
  429. params={{orgId: 'org-slug', dashboardId: '1'}}
  430. router={initialData.router}
  431. location={initialData.router.location}
  432. >
  433. {null}
  434. </ViewEditDashboard>
  435. </OrganizationContext.Provider>,
  436. {context: initialData.routerContext}
  437. );
  438. // Enter edit mode.
  439. await userEvent.click(screen.getByRole('button', {name: 'Edit Dashboard'}));
  440. expect(await screen.findByRole('button', {name: 'Add widget'})).toBeInTheDocument();
  441. });
  442. it('shows top level release filter', async function () {
  443. const mockReleases = MockApiClient.addMockResponse({
  444. url: '/organizations/org-slug/releases/',
  445. body: [TestStubs.Release()],
  446. });
  447. initialData = initializeOrg({
  448. organization: Organization({
  449. features: [
  450. 'global-views',
  451. 'dashboards-basic',
  452. 'dashboards-edit',
  453. 'discover-query',
  454. ],
  455. projects: [TestStubs.Project()],
  456. }),
  457. });
  458. render(
  459. <OrganizationContext.Provider value={initialData.organization}>
  460. <ViewEditDashboard
  461. {...TestStubs.routeComponentProps()}
  462. organization={initialData.organization}
  463. params={{orgId: 'org-slug', dashboardId: '1'}}
  464. router={initialData.router}
  465. location={initialData.router.location}
  466. >
  467. {null}
  468. </ViewEditDashboard>
  469. </OrganizationContext.Provider>,
  470. {context: initialData.routerContext}
  471. );
  472. expect(await screen.findByText('All Releases')).toBeInTheDocument();
  473. expect(mockReleases).toHaveBeenCalledTimes(1);
  474. });
  475. it('hides add widget option', async function () {
  476. // @ts-expect-error this is assigning to readonly property...
  477. types.MAX_WIDGETS = 1;
  478. render(
  479. <OrganizationContext.Provider value={initialData.organization}>
  480. <ViewEditDashboard
  481. {...TestStubs.routeComponentProps()}
  482. organization={initialData.organization}
  483. params={{orgId: 'org-slug', dashboardId: '1'}}
  484. router={initialData.router}
  485. location={initialData.router.location}
  486. >
  487. {null}
  488. </ViewEditDashboard>
  489. </OrganizationContext.Provider>,
  490. {context: initialData.routerContext}
  491. );
  492. // Enter edit mode.
  493. await userEvent.click(await screen.findByRole('button', {name: 'Edit Dashboard'}));
  494. expect(screen.queryByRole('button', {name: 'Add widget'})).not.toBeInTheDocument();
  495. });
  496. it('renders successfully if more widgets than stored layouts', async function () {
  497. // A case where someone has async added widgets to a dashboard
  498. MockApiClient.addMockResponse({
  499. url: '/organizations/org-slug/dashboards/1/',
  500. body: TestStubs.Dashboard(
  501. [
  502. TestStubs.Widget(
  503. [
  504. {
  505. name: '',
  506. conditions: 'event.type:error',
  507. fields: ['count()'],
  508. aggregates: ['count()'],
  509. columns: [],
  510. },
  511. ],
  512. {
  513. title: 'First Widget',
  514. interval: '1d',
  515. id: '1',
  516. layout: {i: 'grid-item-1', x: 0, y: 0, w: 2, h: 6},
  517. }
  518. ),
  519. TestStubs.Widget(
  520. [
  521. {
  522. name: '',
  523. conditions: 'event.type:error',
  524. fields: ['count()'],
  525. aggregates: ['count()'],
  526. columns: [],
  527. },
  528. ],
  529. {
  530. title: 'Second Widget',
  531. interval: '1d',
  532. id: '2',
  533. }
  534. ),
  535. ],
  536. {id: '1', title: 'Custom Errors'}
  537. ),
  538. });
  539. render(
  540. <ViewEditDashboard
  541. {...TestStubs.routeComponentProps()}
  542. organization={initialData.organization}
  543. params={{orgId: 'org-slug', dashboardId: '1'}}
  544. router={initialData.router}
  545. location={initialData.router.location}
  546. >
  547. {null}
  548. </ViewEditDashboard>,
  549. {context: initialData.routerContext, organization: initialData.organization}
  550. );
  551. expect(await screen.findByText('First Widget')).toBeInTheDocument();
  552. expect(await screen.findByText('Second Widget')).toBeInTheDocument();
  553. });
  554. it('does not trigger request if layout not updated', async () => {
  555. MockApiClient.addMockResponse({
  556. url: '/organizations/org-slug/dashboards/1/',
  557. body: TestStubs.Dashboard(
  558. [
  559. TestStubs.Widget(
  560. [
  561. {
  562. name: '',
  563. conditions: 'event.type:error',
  564. fields: ['count()'],
  565. aggregates: ['count()'],
  566. columns: [],
  567. },
  568. ],
  569. {
  570. title: 'First Widget',
  571. interval: '1d',
  572. id: '1',
  573. layout: {i: 'grid-item-1', x: 0, y: 0, w: 2, h: 6},
  574. }
  575. ),
  576. ],
  577. {id: '1', title: 'Custom Errors'}
  578. ),
  579. });
  580. render(
  581. <ViewEditDashboard
  582. {...TestStubs.routeComponentProps()}
  583. organization={initialData.organization}
  584. params={{orgId: 'org-slug', dashboardId: '1'}}
  585. router={initialData.router}
  586. location={initialData.router.location}
  587. >
  588. {null}
  589. </ViewEditDashboard>,
  590. {context: initialData.routerContext, organization: initialData.organization}
  591. );
  592. await userEvent.click(await screen.findByText('Edit Dashboard'));
  593. await userEvent.click(await screen.findByText('Save and Finish'));
  594. expect(screen.getByText('Edit Dashboard')).toBeInTheDocument();
  595. expect(mockPut).not.toHaveBeenCalled();
  596. });
  597. it('renders the custom resize handler for a widget', async () => {
  598. MockApiClient.addMockResponse({
  599. url: '/organizations/org-slug/dashboards/1/',
  600. body: TestStubs.Dashboard(
  601. [
  602. TestStubs.Widget(
  603. [
  604. {
  605. name: '',
  606. conditions: 'event.type:error',
  607. fields: ['count()'],
  608. aggregates: ['count()'],
  609. columns: [],
  610. },
  611. ],
  612. {
  613. title: 'First Widget',
  614. interval: '1d',
  615. id: '1',
  616. layout: {i: 'grid-item-1', x: 0, y: 0, w: 2, h: 6},
  617. }
  618. ),
  619. ],
  620. {id: '1', title: 'Custom Errors'}
  621. ),
  622. });
  623. render(
  624. <ViewEditDashboard
  625. {...TestStubs.routeComponentProps()}
  626. organization={initialData.organization}
  627. params={{orgId: 'org-slug', dashboardId: '1'}}
  628. router={initialData.router}
  629. location={initialData.router.location}
  630. >
  631. {null}
  632. </ViewEditDashboard>,
  633. {context: initialData.routerContext, organization: initialData.organization}
  634. );
  635. await userEvent.click(await screen.findByText('Edit Dashboard'));
  636. const widget = screen
  637. .getByText('First Widget')
  638. .closest('.react-grid-item') as HTMLElement;
  639. const resizeHandle = within(widget).getByTestId('custom-resize-handle');
  640. expect(resizeHandle).toBeVisible();
  641. });
  642. it('does not trigger an alert when the widgets have no layout and user cancels without changes', async () => {
  643. MockApiClient.addMockResponse({
  644. url: '/organizations/org-slug/dashboards/1/',
  645. body: TestStubs.Dashboard(
  646. [
  647. TestStubs.Widget(
  648. [
  649. {
  650. name: '',
  651. conditions: 'event.type:error',
  652. fields: ['count()'],
  653. aggregates: ['count()'],
  654. columns: [],
  655. },
  656. ],
  657. {
  658. title: 'First Widget',
  659. interval: '1d',
  660. id: '1',
  661. layout: null,
  662. }
  663. ),
  664. ],
  665. {id: '1', title: 'Custom Errors'}
  666. ),
  667. });
  668. render(
  669. <ViewEditDashboard
  670. {...TestStubs.routeComponentProps()}
  671. organization={initialData.organization}
  672. params={{orgId: 'org-slug', dashboardId: '1'}}
  673. router={initialData.router}
  674. location={initialData.router.location}
  675. >
  676. {null}
  677. </ViewEditDashboard>,
  678. {context: initialData.routerContext, organization: initialData.organization}
  679. );
  680. await userEvent.click(await screen.findByText('Edit Dashboard'));
  681. await userEvent.click(await screen.findByText('Cancel'));
  682. expect(window.confirm).not.toHaveBeenCalled();
  683. });
  684. it('opens the widget viewer modal using the widget id specified in the url', async () => {
  685. const openWidgetViewerModal = jest.spyOn(modals, 'openWidgetViewerModal');
  686. const widget = TestStubs.Widget(
  687. [
  688. {
  689. name: '',
  690. conditions: 'event.type:error',
  691. fields: ['count()'],
  692. aggregates: ['count()'],
  693. columns: [],
  694. orderby: '',
  695. },
  696. ],
  697. {
  698. title: 'First Widget',
  699. interval: '1d',
  700. id: '1',
  701. layout: null,
  702. }
  703. );
  704. MockApiClient.addMockResponse({
  705. url: '/organizations/org-slug/dashboards/1/',
  706. body: TestStubs.Dashboard([widget], {id: '1', title: 'Custom Errors'}),
  707. });
  708. render(
  709. <ViewEditDashboard
  710. {...TestStubs.routeComponentProps()}
  711. organization={initialData.organization}
  712. params={{orgId: 'org-slug', dashboardId: '1', widgetId: 1}}
  713. router={initialData.router}
  714. location={{...initialData.router.location, pathname: '/widget/123/'}}
  715. >
  716. {null}
  717. </ViewEditDashboard>,
  718. {context: initialData.routerContext, organization: initialData.organization}
  719. );
  720. await waitFor(() => {
  721. expect(openWidgetViewerModal).toHaveBeenCalledWith(
  722. expect.objectContaining({
  723. organization: initialData.organization,
  724. widget,
  725. onClose: expect.anything(),
  726. })
  727. );
  728. });
  729. });
  730. it('redirects user to dashboard url if widget is not found', async () => {
  731. const openWidgetViewerModal = jest.spyOn(modals, 'openWidgetViewerModal');
  732. MockApiClient.addMockResponse({
  733. url: '/organizations/org-slug/dashboards/1/',
  734. body: TestStubs.Dashboard([], {id: '1', title: 'Custom Errors'}),
  735. });
  736. render(
  737. <ViewEditDashboard
  738. {...TestStubs.routeComponentProps()}
  739. organization={initialData.organization}
  740. params={{orgId: 'org-slug', dashboardId: '1', widgetId: 123}}
  741. router={initialData.router}
  742. location={{...initialData.router.location, pathname: '/widget/123/'}}
  743. >
  744. {null}
  745. </ViewEditDashboard>,
  746. {context: initialData.routerContext, organization: initialData.organization}
  747. );
  748. expect(await screen.findByText('All Releases')).toBeInTheDocument();
  749. expect(openWidgetViewerModal).not.toHaveBeenCalled();
  750. expect(initialData.router.replace).toHaveBeenCalledWith(
  751. expect.objectContaining({
  752. pathname: '/organizations/org-slug/dashboard/1/',
  753. query: {},
  754. })
  755. );
  756. });
  757. it('saves a new dashboard with the page filters', async () => {
  758. const mockPOST = MockApiClient.addMockResponse({
  759. url: '/organizations/org-slug/dashboards/',
  760. method: 'POST',
  761. body: [],
  762. });
  763. render(
  764. <CreateDashboard
  765. {...TestStubs.routeComponentProps()}
  766. organization={initialData.organization}
  767. params={{templateId: undefined}}
  768. router={initialData.router}
  769. location={{
  770. ...initialData.router.location,
  771. query: {
  772. ...initialData.router.location.query,
  773. statsPeriod: '7d',
  774. project: [2],
  775. environment: ['alpha', 'beta'],
  776. },
  777. }}
  778. >
  779. {null}
  780. </CreateDashboard>,
  781. {
  782. context: initialData.routerContext,
  783. organization: initialData.organization,
  784. }
  785. );
  786. await userEvent.click(await screen.findByText('Save and Finish'));
  787. expect(mockPOST).toHaveBeenCalledWith(
  788. '/organizations/org-slug/dashboards/',
  789. expect.objectContaining({
  790. data: expect.objectContaining({
  791. projects: [2],
  792. environment: ['alpha', 'beta'],
  793. period: '7d',
  794. }),
  795. })
  796. );
  797. });
  798. it('saves a template with the page filters', async () => {
  799. const mockPOST = MockApiClient.addMockResponse({
  800. url: '/organizations/org-slug/dashboards/',
  801. method: 'POST',
  802. body: [],
  803. });
  804. render(
  805. <CreateDashboard
  806. {...TestStubs.routeComponentProps()}
  807. organization={initialData.organization}
  808. params={{templateId: 'default-template'}}
  809. router={initialData.router}
  810. location={{
  811. ...initialData.router.location,
  812. query: {
  813. ...initialData.router.location.query,
  814. statsPeriod: '7d',
  815. project: [2],
  816. environment: ['alpha', 'beta'],
  817. },
  818. }}
  819. >
  820. {null}
  821. </CreateDashboard>,
  822. {
  823. context: initialData.routerContext,
  824. organization: initialData.organization,
  825. }
  826. );
  827. await userEvent.click(await screen.findByText('Add Dashboard'));
  828. expect(mockPOST).toHaveBeenCalledWith(
  829. '/organizations/org-slug/dashboards/',
  830. expect.objectContaining({
  831. data: expect.objectContaining({
  832. projects: [2],
  833. environment: ['alpha', 'beta'],
  834. period: '7d',
  835. }),
  836. })
  837. );
  838. });
  839. it('does not render save and cancel buttons on templates', async () => {
  840. MockApiClient.addMockResponse({
  841. url: '/organizations/org-slug/releases/',
  842. body: [
  843. TestStubs.Release({
  844. shortVersion: 'sentry-android-shop@1.2.0',
  845. version: 'sentry-android-shop@1.2.0',
  846. }),
  847. ],
  848. });
  849. render(
  850. <CreateDashboard
  851. {...TestStubs.routeComponentProps()}
  852. organization={initialData.organization}
  853. params={{templateId: 'default-template'}}
  854. router={initialData.router}
  855. location={initialData.router.location}
  856. >
  857. {null}
  858. </CreateDashboard>,
  859. {
  860. context: initialData.routerContext,
  861. organization: initialData.organization,
  862. }
  863. );
  864. await userEvent.click(await screen.findByText('24H'));
  865. await userEvent.click(screen.getByText('Last 7 days'));
  866. await screen.findByText('7D');
  867. expect(screen.queryByText('Cancel')).not.toBeInTheDocument();
  868. expect(screen.queryByText('Save')).not.toBeInTheDocument();
  869. });
  870. it('opens the widget viewer with saved dashboard filters', async () => {
  871. const openWidgetViewerModal = jest.spyOn(modals, 'openWidgetViewerModal');
  872. MockApiClient.addMockResponse({
  873. url: '/organizations/org-slug/dashboards/1/',
  874. body: TestStubs.Dashboard(widgets, {
  875. id: '1',
  876. filters: {release: ['sentry-android-shop@1.2.0']},
  877. }),
  878. });
  879. render(
  880. <ViewEditDashboard
  881. {...TestStubs.routeComponentProps()}
  882. organization={initialData.organization}
  883. params={{orgId: 'org-slug', dashboardId: '1', widgetId: 1}}
  884. router={initialData.router}
  885. location={{...initialData.router.location, pathname: '/widget/1/'}}
  886. >
  887. {null}
  888. </ViewEditDashboard>,
  889. {context: initialData.routerContext, organization: initialData.organization}
  890. );
  891. await waitFor(() => {
  892. expect(openWidgetViewerModal).toHaveBeenCalledWith(
  893. expect.objectContaining({
  894. dashboardFilters: {release: ['sentry-android-shop@1.2.0']},
  895. })
  896. );
  897. });
  898. });
  899. it('opens the widget viewer with unsaved dashboard filters', async () => {
  900. const openWidgetViewerModal = jest.spyOn(modals, 'openWidgetViewerModal');
  901. MockApiClient.addMockResponse({
  902. url: '/organizations/org-slug/dashboards/1/',
  903. body: TestStubs.Dashboard(widgets, {
  904. id: '1',
  905. filters: {release: ['sentry-android-shop@1.2.0']},
  906. }),
  907. });
  908. render(
  909. <ViewEditDashboard
  910. {...TestStubs.routeComponentProps()}
  911. organization={initialData.organization}
  912. params={{orgId: 'org-slug', dashboardId: '1', widgetId: 1}}
  913. router={initialData.router}
  914. location={{
  915. ...initialData.router.location,
  916. pathname: '/widget/1/',
  917. query: {release: ['unsaved-release-filter@1.2.0']},
  918. }}
  919. >
  920. {null}
  921. </ViewEditDashboard>,
  922. {context: initialData.routerContext, organization: initialData.organization}
  923. );
  924. await waitFor(() => {
  925. expect(openWidgetViewerModal).toHaveBeenCalledWith(
  926. expect.objectContaining({
  927. dashboardFilters: {release: ['unsaved-release-filter@1.2.0']},
  928. })
  929. );
  930. });
  931. });
  932. it('can save dashboard filters in existing dashboard', async () => {
  933. MockApiClient.addMockResponse({
  934. url: '/organizations/org-slug/releases/',
  935. body: [
  936. TestStubs.Release({
  937. shortVersion: 'sentry-android-shop@1.2.0',
  938. version: 'sentry-android-shop@1.2.0',
  939. }),
  940. ],
  941. });
  942. const testData = initializeOrg({
  943. organization: Organization({
  944. features: [
  945. 'global-views',
  946. 'dashboards-basic',
  947. 'dashboards-edit',
  948. 'discover-query',
  949. ],
  950. }),
  951. router: {
  952. location: {
  953. ...TestStubs.location(),
  954. query: {
  955. statsPeriod: '7d',
  956. release: ['sentry-android-shop@1.2.0'],
  957. },
  958. },
  959. },
  960. });
  961. render(
  962. <ViewEditDashboard
  963. {...TestStubs.routeComponentProps()}
  964. organization={testData.organization}
  965. params={{orgId: 'org-slug', dashboardId: '1'}}
  966. router={testData.router}
  967. location={testData.router.location}
  968. >
  969. {null}
  970. </ViewEditDashboard>,
  971. {context: testData.routerContext, organization: testData.organization}
  972. );
  973. await userEvent.click(await screen.findByText('Save'));
  974. expect(mockPut).toHaveBeenCalledWith(
  975. '/organizations/org-slug/dashboards/1/',
  976. expect.objectContaining({
  977. data: expect.objectContaining({
  978. period: '7d',
  979. filters: {release: ['sentry-android-shop@1.2.0']},
  980. }),
  981. })
  982. );
  983. });
  984. it('can clear dashboard filters in compact select', async () => {
  985. MockApiClient.addMockResponse({
  986. url: '/organizations/org-slug/dashboards/1/',
  987. body: TestStubs.Dashboard(widgets, {
  988. id: '1',
  989. title: 'Custom Errors',
  990. filters: {release: ['sentry-android-shop@1.2.0']},
  991. }),
  992. });
  993. MockApiClient.addMockResponse({
  994. url: '/organizations/org-slug/releases/',
  995. body: [
  996. TestStubs.Release({
  997. shortVersion: 'sentry-android-shop@1.2.0',
  998. version: 'sentry-android-shop@1.2.0',
  999. }),
  1000. ],
  1001. });
  1002. const testData = initializeOrg({
  1003. organization: Organization({
  1004. features: [
  1005. 'global-views',
  1006. 'dashboards-basic',
  1007. 'dashboards-edit',
  1008. 'discover-query',
  1009. ],
  1010. }),
  1011. router: {
  1012. location: {
  1013. ...TestStubs.location(),
  1014. query: {
  1015. statsPeriod: '7d',
  1016. },
  1017. },
  1018. },
  1019. });
  1020. render(
  1021. <ViewEditDashboard
  1022. {...TestStubs.routeComponentProps()}
  1023. organization={testData.organization}
  1024. params={{orgId: 'org-slug', dashboardId: '1'}}
  1025. router={testData.router}
  1026. location={testData.router.location}
  1027. >
  1028. {null}
  1029. </ViewEditDashboard>,
  1030. {context: testData.routerContext, organization: testData.organization}
  1031. );
  1032. await screen.findByText('7D');
  1033. await userEvent.click(await screen.findByText('sentry-android-shop@1.2.0'));
  1034. await userEvent.click(screen.getByText('Clear'));
  1035. screen.getByText('All Releases');
  1036. await userEvent.click(document.body);
  1037. expect(browserHistory.push).toHaveBeenCalledWith(
  1038. expect.objectContaining({
  1039. query: expect.objectContaining({
  1040. release: '',
  1041. }),
  1042. })
  1043. );
  1044. });
  1045. it('can save absolute time range in existing dashboard', async () => {
  1046. const testData = initializeOrg({
  1047. organization: Organization({
  1048. features: [
  1049. 'global-views',
  1050. 'dashboards-basic',
  1051. 'dashboards-edit',
  1052. 'discover-query',
  1053. ],
  1054. }),
  1055. router: {
  1056. location: {
  1057. ...TestStubs.location(),
  1058. query: {
  1059. start: '2022-07-14T07:00:00',
  1060. end: '2022-07-19T23:59:59',
  1061. utc: 'true',
  1062. },
  1063. },
  1064. },
  1065. });
  1066. render(
  1067. <ViewEditDashboard
  1068. {...TestStubs.routeComponentProps()}
  1069. organization={testData.organization}
  1070. params={{orgId: 'org-slug', dashboardId: '1'}}
  1071. router={testData.router}
  1072. location={testData.router.location}
  1073. >
  1074. {null}
  1075. </ViewEditDashboard>,
  1076. {context: testData.routerContext, organization: testData.organization}
  1077. );
  1078. await userEvent.click(await screen.findByText('Save'));
  1079. expect(mockPut).toHaveBeenCalledWith(
  1080. '/organizations/org-slug/dashboards/1/',
  1081. expect.objectContaining({
  1082. data: expect.objectContaining({
  1083. start: '2022-07-14T07:00:00.000',
  1084. end: '2022-07-19T23:59:59.000',
  1085. utc: true,
  1086. }),
  1087. })
  1088. );
  1089. });
  1090. it('can clear dashboard filters in existing dashboard', async () => {
  1091. MockApiClient.addMockResponse({
  1092. url: '/organizations/org-slug/releases/',
  1093. body: [
  1094. TestStubs.Release({
  1095. shortVersion: 'sentry-android-shop@1.2.0',
  1096. version: 'sentry-android-shop@1.2.0',
  1097. }),
  1098. ],
  1099. });
  1100. const testData = initializeOrg({
  1101. organization: Organization({
  1102. features: [
  1103. 'global-views',
  1104. 'dashboards-basic',
  1105. 'dashboards-edit',
  1106. 'discover-query',
  1107. ],
  1108. }),
  1109. router: {
  1110. location: {
  1111. ...TestStubs.location(),
  1112. query: {
  1113. statsPeriod: '7d',
  1114. environment: ['alpha', 'beta'],
  1115. },
  1116. },
  1117. },
  1118. });
  1119. render(
  1120. <ViewEditDashboard
  1121. {...TestStubs.routeComponentProps()}
  1122. organization={testData.organization}
  1123. params={{orgId: 'org-slug', dashboardId: '1'}}
  1124. router={testData.router}
  1125. location={testData.router.location}
  1126. >
  1127. {null}
  1128. </ViewEditDashboard>,
  1129. {context: testData.routerContext, organization: testData.organization}
  1130. );
  1131. await screen.findByText('7D');
  1132. await userEvent.click(await screen.findByText('All Releases'));
  1133. await userEvent.click(screen.getByText('sentry-android-shop@1.2.0'));
  1134. await userEvent.keyboard('{Escape}');
  1135. await userEvent.click(screen.getByText('Cancel'));
  1136. screen.getByText('All Releases');
  1137. expect(browserHistory.replace).toHaveBeenCalledWith(
  1138. expect.objectContaining({
  1139. query: expect.objectContaining({
  1140. project: undefined,
  1141. statsPeriod: undefined,
  1142. environment: undefined,
  1143. }),
  1144. })
  1145. );
  1146. });
  1147. it('disables the Edit Dashboard button when there are unsaved filters', async () => {
  1148. MockApiClient.addMockResponse({
  1149. url: '/organizations/org-slug/releases/',
  1150. body: [
  1151. TestStubs.Release({
  1152. shortVersion: 'sentry-android-shop@1.2.0',
  1153. version: 'sentry-android-shop@1.2.0',
  1154. }),
  1155. ],
  1156. });
  1157. const testData = initializeOrg({
  1158. organization: Organization({
  1159. features: [
  1160. 'global-views',
  1161. 'dashboards-basic',
  1162. 'dashboards-edit',
  1163. 'discover-basic',
  1164. 'discover-query',
  1165. ],
  1166. }),
  1167. router: {
  1168. location: {
  1169. ...TestStubs.location(),
  1170. query: {
  1171. statsPeriod: '7d',
  1172. environment: ['alpha', 'beta'],
  1173. },
  1174. },
  1175. },
  1176. });
  1177. render(
  1178. <ViewEditDashboard
  1179. {...TestStubs.routeComponentProps()}
  1180. organization={testData.organization}
  1181. params={{orgId: 'org-slug', dashboardId: '1'}}
  1182. router={testData.router}
  1183. location={testData.router.location}
  1184. >
  1185. {null}
  1186. </ViewEditDashboard>,
  1187. {context: testData.routerContext, organization: testData.organization}
  1188. );
  1189. expect(await screen.findByText('Save')).toBeInTheDocument();
  1190. expect(screen.getByText('Cancel')).toBeInTheDocument();
  1191. expect(screen.getByRole('button', {name: 'Edit Dashboard'})).toBeDisabled();
  1192. });
  1193. it('ignores the order of selection of page filters to render unsaved filters', async () => {
  1194. const testProjects = [
  1195. TestStubs.Project({id: '1', name: 'first', environments: ['alpha', 'beta']}),
  1196. TestStubs.Project({id: '2', name: 'second', environments: ['alpha', 'beta']}),
  1197. ];
  1198. act(() => ProjectsStore.loadInitialData(testProjects));
  1199. MockApiClient.addMockResponse({
  1200. url: '/organizations/org-slug/projects/',
  1201. body: testProjects,
  1202. });
  1203. MockApiClient.addMockResponse({
  1204. url: '/organizations/org-slug/dashboards/1/',
  1205. body: TestStubs.Dashboard(widgets, {
  1206. id: '1',
  1207. title: 'Custom Errors',
  1208. filters: {},
  1209. environment: ['alpha', 'beta'],
  1210. }),
  1211. });
  1212. const testData = initializeOrg({
  1213. organization: Organization({
  1214. features: [
  1215. 'global-views',
  1216. 'dashboards-basic',
  1217. 'dashboards-edit',
  1218. 'discover-query',
  1219. ],
  1220. }),
  1221. router: {
  1222. location: {
  1223. ...TestStubs.location(),
  1224. query: {
  1225. environment: ['beta', 'alpha'], // Reversed order from saved dashboard
  1226. },
  1227. },
  1228. },
  1229. });
  1230. render(
  1231. <ViewEditDashboard
  1232. {...TestStubs.routeComponentProps()}
  1233. organization={testData.organization}
  1234. params={{orgId: 'org-slug', dashboardId: '1'}}
  1235. router={testData.router}
  1236. location={testData.router.location}
  1237. >
  1238. {null}
  1239. </ViewEditDashboard>,
  1240. {context: testData.routerContext, organization: testData.organization}
  1241. );
  1242. await waitFor(() => expect(screen.queryAllByText('Loading\u2026')).toEqual([]));
  1243. await screen.findByText(/beta, alpha/);
  1244. // Save and Cancel should not appear because alpha, beta is the same as beta, alpha
  1245. expect(screen.queryByText('Save')).not.toBeInTheDocument();
  1246. expect(screen.queryByText('Cancel')).not.toBeInTheDocument();
  1247. });
  1248. it('uses releases from the URL query params', async function () {
  1249. const testData = initializeOrg({
  1250. organization: Organization({
  1251. features: [
  1252. 'global-views',
  1253. 'dashboards-basic',
  1254. 'dashboards-edit',
  1255. 'discover-query',
  1256. ],
  1257. }),
  1258. router: {
  1259. location: {
  1260. ...TestStubs.location(),
  1261. query: {
  1262. release: ['not-selected-1'],
  1263. },
  1264. },
  1265. },
  1266. });
  1267. render(
  1268. <ViewEditDashboard
  1269. {...TestStubs.routeComponentProps()}
  1270. organization={testData.organization}
  1271. params={{orgId: 'org-slug', dashboardId: '1'}}
  1272. router={testData.router}
  1273. location={testData.router.location}
  1274. >
  1275. {null}
  1276. </ViewEditDashboard>,
  1277. {context: testData.routerContext, organization: testData.organization}
  1278. );
  1279. await screen.findByText(/not-selected-1/);
  1280. screen.getByText('Save');
  1281. screen.getByText('Cancel');
  1282. });
  1283. it('resets release in URL params', async function () {
  1284. MockApiClient.addMockResponse({
  1285. url: '/organizations/org-slug/dashboards/1/',
  1286. body: TestStubs.Dashboard(widgets, {
  1287. id: '1',
  1288. title: 'Custom Errors',
  1289. filters: {
  1290. release: ['abc'],
  1291. },
  1292. }),
  1293. });
  1294. const testData = initializeOrg({
  1295. organization: Organization({
  1296. features: [
  1297. 'global-views',
  1298. 'dashboards-basic',
  1299. 'dashboards-edit',
  1300. 'discover-query',
  1301. ],
  1302. }),
  1303. router: {
  1304. location: {
  1305. ...TestStubs.location(),
  1306. query: {
  1307. release: ['not-selected-1'],
  1308. },
  1309. },
  1310. },
  1311. });
  1312. render(
  1313. <ViewEditDashboard
  1314. {...TestStubs.routeComponentProps()}
  1315. organization={testData.organization}
  1316. params={{orgId: 'org-slug', dashboardId: '1'}}
  1317. router={testData.router}
  1318. location={testData.router.location}
  1319. >
  1320. {null}
  1321. </ViewEditDashboard>,
  1322. {context: testData.routerContext, organization: testData.organization}
  1323. );
  1324. await screen.findByText(/not-selected-1/);
  1325. await userEvent.click(screen.getByText('Cancel'));
  1326. // release isn't used in the redirect
  1327. expect(browserHistory.replace).toHaveBeenCalledWith(
  1328. expect.objectContaining({
  1329. query: {
  1330. end: undefined,
  1331. environment: undefined,
  1332. project: undefined,
  1333. start: undefined,
  1334. statsPeriod: undefined,
  1335. utc: undefined,
  1336. },
  1337. })
  1338. );
  1339. });
  1340. it('reflects selections in the release filter in the query params', async function () {
  1341. MockApiClient.addMockResponse({
  1342. url: '/organizations/org-slug/releases/',
  1343. body: [
  1344. TestStubs.Release({
  1345. shortVersion: 'sentry-android-shop@1.2.0',
  1346. version: 'sentry-android-shop@1.2.0',
  1347. }),
  1348. ],
  1349. });
  1350. const testData = initializeOrg({
  1351. organization: Organization({
  1352. features: [
  1353. 'global-views',
  1354. 'dashboards-basic',
  1355. 'dashboards-edit',
  1356. 'discover-query',
  1357. ],
  1358. }),
  1359. router: {
  1360. location: TestStubs.location(),
  1361. },
  1362. });
  1363. render(
  1364. <ViewEditDashboard
  1365. {...TestStubs.routeComponentProps()}
  1366. organization={testData.organization}
  1367. params={{orgId: 'org-slug', dashboardId: '1'}}
  1368. router={testData.router}
  1369. location={testData.router.location}
  1370. >
  1371. {null}
  1372. </ViewEditDashboard>,
  1373. {context: testData.routerContext, organization: testData.organization}
  1374. );
  1375. await userEvent.click(await screen.findByText('All Releases'));
  1376. await userEvent.click(screen.getByText('sentry-android-shop@1.2.0'));
  1377. await userEvent.click(document.body);
  1378. expect(browserHistory.push).toHaveBeenCalledWith(
  1379. expect.objectContaining({
  1380. query: expect.objectContaining({
  1381. release: ['sentry-android-shop@1.2.0'],
  1382. }),
  1383. })
  1384. );
  1385. });
  1386. it('persists release selections made during search requests that do not appear in default query', async function () {
  1387. // Default response
  1388. MockApiClient.addMockResponse({
  1389. url: '/organizations/org-slug/releases/',
  1390. body: [
  1391. TestStubs.Release({
  1392. shortVersion: 'sentry-android-shop@1.2.0',
  1393. version: 'sentry-android-shop@1.2.0',
  1394. }),
  1395. ],
  1396. });
  1397. // Mocked search results
  1398. MockApiClient.addMockResponse({
  1399. url: '/organizations/org-slug/releases/',
  1400. body: [
  1401. TestStubs.Release({
  1402. id: '9',
  1403. shortVersion: 'search-result',
  1404. version: 'search-result',
  1405. }),
  1406. ],
  1407. match: [MockApiClient.matchData({query: 's'})],
  1408. });
  1409. const testData = initializeOrg({
  1410. organization: Organization({
  1411. features: [
  1412. 'global-views',
  1413. 'dashboards-basic',
  1414. 'dashboards-edit',
  1415. 'discover-basic',
  1416. 'discover-query',
  1417. ],
  1418. }),
  1419. router: {
  1420. location: TestStubs.location(),
  1421. },
  1422. });
  1423. render(
  1424. <ViewEditDashboard
  1425. {...TestStubs.routeComponentProps()}
  1426. organization={testData.organization}
  1427. params={{orgId: 'org-slug', dashboardId: '1'}}
  1428. router={testData.router}
  1429. location={testData.router.location}
  1430. >
  1431. {null}
  1432. </ViewEditDashboard>,
  1433. {context: testData.routerContext, organization: testData.organization}
  1434. );
  1435. await userEvent.click(await screen.findByText('All Releases'));
  1436. await userEvent.type(screen.getByPlaceholderText('Search\u2026'), 's');
  1437. await userEvent.click(await screen.findByRole('option', {name: 'search-result'}));
  1438. // Validate that after search is cleared, search result still appears
  1439. expect(await screen.findByText('Latest Release(s)')).toBeInTheDocument();
  1440. expect(screen.getByRole('option', {name: 'search-result'})).toBeInTheDocument();
  1441. });
  1442. });
  1443. });