detail.spec.jsx 46 KB

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