widgetBuilderSlideout.spec.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. import {DashboardFixture} from 'sentry-fixture/dashboard';
  2. import {LocationFixture} from 'sentry-fixture/locationFixture';
  3. import {OrganizationFixture} from 'sentry-fixture/organization';
  4. import {RouterFixture} from 'sentry-fixture/routerFixture';
  5. import {
  6. render,
  7. renderGlobalModal,
  8. screen,
  9. userEvent,
  10. waitFor,
  11. } from 'sentry-test/reactTestingLibrary';
  12. import {addErrorMessage, addLoadingMessage} from 'sentry/actionCreators/indicator';
  13. import ModalStore from 'sentry/stores/modalStore';
  14. import useCustomMeasurements from 'sentry/utils/useCustomMeasurements';
  15. import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
  16. import WidgetBuilderSlideout from 'sentry/views/dashboards/widgetBuilder/components/widgetBuilderSlideout';
  17. import {WidgetBuilderProvider} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
  18. import {useSpanTags} from 'sentry/views/explore/contexts/spanTagsContext';
  19. jest.mock('sentry/utils/useCustomMeasurements');
  20. jest.mock('sentry/views/explore/contexts/spanTagsContext');
  21. jest.mock('sentry/actionCreators/indicator');
  22. describe('WidgetBuilderSlideout', () => {
  23. let organization!: ReturnType<typeof OrganizationFixture>;
  24. beforeEach(() => {
  25. organization = OrganizationFixture();
  26. jest.mocked(useCustomMeasurements).mockReturnValue({
  27. customMeasurements: {},
  28. });
  29. jest.mocked(useSpanTags).mockReturnValue({});
  30. MockApiClient.addMockResponse({
  31. url: '/organizations/org-slug/recent-searches/',
  32. });
  33. MockApiClient.addMockResponse({
  34. url: '/organizations/org-slug/dashboards/widgets/',
  35. method: 'POST',
  36. body: {
  37. title: 'Title is required during creation',
  38. },
  39. statusCode: 400,
  40. });
  41. });
  42. afterEach(() => {
  43. ModalStore.reset();
  44. });
  45. it('should show the sort by step if the widget is a chart and there are fields selected', async () => {
  46. render(
  47. <WidgetBuilderProvider>
  48. <WidgetBuilderSlideout
  49. dashboard={DashboardFixture([])}
  50. dashboardFilters={{
  51. release: undefined,
  52. }}
  53. isWidgetInvalid={false}
  54. onClose={jest.fn()}
  55. onQueryConditionChange={jest.fn()}
  56. onSave={jest.fn()}
  57. setIsPreviewDraggable={jest.fn()}
  58. isOpen
  59. openWidgetTemplates={false}
  60. setOpenWidgetTemplates={jest.fn()}
  61. />
  62. </WidgetBuilderProvider>,
  63. {
  64. organization,
  65. router: RouterFixture({
  66. location: LocationFixture({
  67. query: {
  68. field: ['project'],
  69. yAxis: ['count()'],
  70. dataset: WidgetType.TRANSACTIONS,
  71. displayType: DisplayType.LINE,
  72. },
  73. }),
  74. }),
  75. }
  76. );
  77. expect(await screen.findByText('Sort by')).toBeInTheDocument();
  78. expect(await screen.findByText('Limit to 5 results')).toBeInTheDocument();
  79. expect(await screen.findByText('High to low')).toBeInTheDocument();
  80. expect(await screen.findByText('(Required)')).toBeInTheDocument();
  81. });
  82. it('should show the sort by step if the widget is a table', async () => {
  83. render(
  84. <WidgetBuilderProvider>
  85. <WidgetBuilderSlideout
  86. dashboard={DashboardFixture([])}
  87. dashboardFilters={{
  88. release: undefined,
  89. }}
  90. isWidgetInvalid={false}
  91. onClose={jest.fn()}
  92. onQueryConditionChange={jest.fn()}
  93. onSave={jest.fn()}
  94. setIsPreviewDraggable={jest.fn()}
  95. isOpen
  96. openWidgetTemplates={false}
  97. setOpenWidgetTemplates={jest.fn()}
  98. />
  99. </WidgetBuilderProvider>,
  100. {
  101. organization,
  102. router: RouterFixture({
  103. location: LocationFixture({
  104. query: {
  105. field: [],
  106. yAxis: [],
  107. dataset: WidgetType.TRANSACTIONS,
  108. displayType: DisplayType.TABLE,
  109. },
  110. }),
  111. }),
  112. }
  113. );
  114. expect(await screen.findByText('Sort by')).toBeInTheDocument();
  115. });
  116. it('should not show the sort by step if the widget is a chart without fields', async () => {
  117. render(
  118. <WidgetBuilderProvider>
  119. <WidgetBuilderSlideout
  120. dashboard={DashboardFixture([])}
  121. dashboardFilters={{
  122. release: undefined,
  123. }}
  124. isWidgetInvalid={false}
  125. onClose={jest.fn()}
  126. onQueryConditionChange={jest.fn()}
  127. onSave={jest.fn()}
  128. setIsPreviewDraggable={jest.fn()}
  129. isOpen
  130. openWidgetTemplates={false}
  131. setOpenWidgetTemplates={jest.fn()}
  132. />
  133. </WidgetBuilderProvider>,
  134. {
  135. organization,
  136. router: RouterFixture({
  137. location: LocationFixture({
  138. query: {
  139. field: [],
  140. yAxis: ['count()'],
  141. dataset: WidgetType.TRANSACTIONS,
  142. displayType: DisplayType.LINE,
  143. },
  144. }),
  145. }),
  146. }
  147. );
  148. expect(await screen.findByText('count')).toBeInTheDocument();
  149. expect(screen.queryByText('Sort by')).not.toBeInTheDocument();
  150. });
  151. it('should show the confirm modal if the widget is unsaved', async () => {
  152. render(
  153. <WidgetBuilderProvider>
  154. <WidgetBuilderSlideout
  155. dashboard={DashboardFixture([])}
  156. dashboardFilters={{release: undefined}}
  157. isWidgetInvalid={false}
  158. onClose={jest.fn()}
  159. onQueryConditionChange={jest.fn()}
  160. onSave={jest.fn()}
  161. setIsPreviewDraggable={jest.fn()}
  162. isOpen
  163. openWidgetTemplates={false}
  164. setOpenWidgetTemplates={jest.fn()}
  165. />
  166. </WidgetBuilderProvider>,
  167. {organization}
  168. );
  169. renderGlobalModal();
  170. await userEvent.type(await screen.findByPlaceholderText('Name'), 'some name');
  171. await userEvent.click(await screen.findByText('Close'));
  172. expect(screen.getByRole('dialog')).toBeInTheDocument();
  173. expect(
  174. screen.getByText('You have unsaved changes. Are you sure you want to leave?')
  175. ).toBeInTheDocument();
  176. });
  177. it('should not show the confirm modal if the widget is unsaved', async () => {
  178. render(
  179. <WidgetBuilderProvider>
  180. <WidgetBuilderSlideout
  181. dashboard={DashboardFixture([])}
  182. dashboardFilters={{release: undefined}}
  183. isWidgetInvalid={false}
  184. onClose={jest.fn()}
  185. onQueryConditionChange={jest.fn()}
  186. onSave={jest.fn()}
  187. setIsPreviewDraggable={jest.fn()}
  188. isOpen
  189. openWidgetTemplates={false}
  190. setOpenWidgetTemplates={jest.fn()}
  191. />
  192. </WidgetBuilderProvider>,
  193. {organization}
  194. );
  195. renderGlobalModal();
  196. await userEvent.click(await screen.findByText('Close'));
  197. await waitFor(() => {
  198. expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
  199. });
  200. expect(
  201. screen.queryByText('You have unsaved changes. Are you sure you want to leave?')
  202. ).not.toBeInTheDocument();
  203. });
  204. it('should not save and close the widget builder if the widget is invalid', async () => {
  205. render(
  206. <WidgetBuilderProvider>
  207. <WidgetBuilderSlideout
  208. dashboard={DashboardFixture([])}
  209. dashboardFilters={{release: undefined}}
  210. isWidgetInvalid
  211. onClose={jest.fn()}
  212. onQueryConditionChange={jest.fn()}
  213. onSave={jest.fn()}
  214. setIsPreviewDraggable={jest.fn()}
  215. isOpen
  216. openWidgetTemplates={false}
  217. setOpenWidgetTemplates={jest.fn()}
  218. />
  219. </WidgetBuilderProvider>,
  220. {
  221. organization,
  222. router: RouterFixture({
  223. location: LocationFixture({
  224. query: {
  225. field: [],
  226. yAxis: ['count()'],
  227. dataset: WidgetType.TRANSACTIONS,
  228. displayType: DisplayType.LINE,
  229. title: undefined,
  230. },
  231. }),
  232. }),
  233. }
  234. );
  235. await userEvent.click(await screen.findByText('Add Widget'));
  236. await waitFor(() => {
  237. expect(addErrorMessage).toHaveBeenCalledWith('Unable to save widget');
  238. });
  239. expect(screen.getByText('Create Custom Widget')).toBeInTheDocument();
  240. });
  241. it('should save the widget from the widget builder with loading messages if the widget is valid', async () => {
  242. MockApiClient.addMockResponse({
  243. url: '/organizations/org-slug/dashboards/widgets/',
  244. method: 'POST',
  245. body: {},
  246. statusCode: 200,
  247. });
  248. render(
  249. <WidgetBuilderProvider>
  250. <WidgetBuilderSlideout
  251. dashboard={DashboardFixture([])}
  252. dashboardFilters={{release: undefined}}
  253. isWidgetInvalid
  254. onClose={jest.fn()}
  255. onQueryConditionChange={jest.fn()}
  256. onSave={jest.fn()}
  257. setIsPreviewDraggable={jest.fn()}
  258. isOpen
  259. openWidgetTemplates={false}
  260. setOpenWidgetTemplates={jest.fn()}
  261. />
  262. </WidgetBuilderProvider>,
  263. {
  264. organization,
  265. router: RouterFixture({
  266. location: LocationFixture({
  267. query: {
  268. field: [],
  269. yAxis: ['count()'],
  270. dataset: WidgetType.TRANSACTIONS,
  271. displayType: DisplayType.LINE,
  272. title: 'Widget Title',
  273. },
  274. }),
  275. }),
  276. }
  277. );
  278. await userEvent.click(await screen.findByText('Add Widget'));
  279. await waitFor(() => {
  280. expect(addLoadingMessage).toHaveBeenCalledWith('Saving widget');
  281. });
  282. });
  283. it('clears the alias when dataset changes', async () => {
  284. render(
  285. <WidgetBuilderProvider>
  286. <WidgetBuilderSlideout
  287. dashboard={DashboardFixture([])}
  288. dashboardFilters={{release: undefined}}
  289. isWidgetInvalid
  290. onClose={jest.fn()}
  291. onQueryConditionChange={jest.fn()}
  292. onSave={jest.fn()}
  293. setIsPreviewDraggable={jest.fn()}
  294. isOpen
  295. openWidgetTemplates={false}
  296. setOpenWidgetTemplates={jest.fn()}
  297. />
  298. </WidgetBuilderProvider>,
  299. {
  300. organization,
  301. router: RouterFixture({
  302. location: LocationFixture({
  303. query: {
  304. field: ['count()'],
  305. yAxis: [],
  306. dataset: WidgetType.TRANSACTIONS,
  307. displayType: DisplayType.TABLE,
  308. },
  309. }),
  310. }),
  311. }
  312. );
  313. await userEvent.type(await screen.findByPlaceholderText('Add Alias'), 'test alias');
  314. await userEvent.click(screen.getByText('Errors'));
  315. expect(await screen.findByPlaceholderText('Add Alias')).toHaveValue('');
  316. });
  317. it('clears the alias when display type changes', async () => {
  318. render(
  319. <WidgetBuilderProvider>
  320. <WidgetBuilderSlideout
  321. dashboard={DashboardFixture([])}
  322. dashboardFilters={{release: undefined}}
  323. isWidgetInvalid
  324. onClose={jest.fn()}
  325. onQueryConditionChange={jest.fn()}
  326. onSave={jest.fn()}
  327. setIsPreviewDraggable={jest.fn()}
  328. isOpen
  329. openWidgetTemplates={false}
  330. setOpenWidgetTemplates={jest.fn()}
  331. />
  332. </WidgetBuilderProvider>,
  333. {
  334. organization,
  335. router: RouterFixture({
  336. location: LocationFixture({
  337. query: {
  338. field: ['count()'],
  339. yAxis: [],
  340. dataset: WidgetType.TRANSACTIONS,
  341. displayType: DisplayType.TABLE,
  342. },
  343. }),
  344. }),
  345. }
  346. );
  347. await userEvent.type(
  348. await screen.findByPlaceholderText('Add Alias'),
  349. 'test alias again'
  350. );
  351. await userEvent.click(screen.getByText('Table'));
  352. await userEvent.click(screen.getByText('Area'));
  353. await userEvent.click(screen.getByText('Area'));
  354. await userEvent.click(screen.getByText('Table'));
  355. expect(await screen.findByPlaceholderText('Add Alias')).toHaveValue('');
  356. });
  357. it('only renders thresholds for big number widgets', async () => {
  358. render(
  359. <WidgetBuilderProvider>
  360. <WidgetBuilderSlideout
  361. dashboard={DashboardFixture([])}
  362. dashboardFilters={{release: undefined}}
  363. isWidgetInvalid
  364. onClose={jest.fn()}
  365. onQueryConditionChange={jest.fn()}
  366. onSave={jest.fn()}
  367. setIsPreviewDraggable={jest.fn()}
  368. isOpen
  369. openWidgetTemplates={false}
  370. setOpenWidgetTemplates={jest.fn()}
  371. />
  372. </WidgetBuilderProvider>,
  373. {
  374. organization,
  375. router: RouterFixture({
  376. location: LocationFixture({
  377. query: {
  378. dataset: WidgetType.TRANSACTIONS,
  379. displayType: DisplayType.BIG_NUMBER,
  380. },
  381. }),
  382. }),
  383. }
  384. );
  385. expect(await screen.findByText('Thresholds')).toBeInTheDocument();
  386. });
  387. });