widgetCard.spec.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. import {initializeOrg} from 'sentry-test/initializeOrg';
  2. import {mountGlobalModal} from 'sentry-test/modal';
  3. import {mountWithTheme, screen, userEvent} from 'sentry-test/reactTestingLibrary';
  4. import * as modal from 'sentry/actionCreators/modal';
  5. import {Client} from 'sentry/api';
  6. import {DisplayType, Widget, WidgetType} from 'sentry/views/dashboardsV2/types';
  7. import WidgetCard from 'sentry/views/dashboardsV2/widgetCard';
  8. describe('Dashboards > WidgetCard', function () {
  9. const initialData = initializeOrg({
  10. organization: TestStubs.Organization({
  11. features: ['connect-discover-and-dashboards', 'dashboards-edit', 'discover-basic'],
  12. projects: [TestStubs.Project()],
  13. }),
  14. router: {orgId: 'orgId'},
  15. } as Parameters<typeof initializeOrg>[0]);
  16. const multipleQueryWidget: Widget = {
  17. title: 'Errors',
  18. interval: '5m',
  19. displayType: DisplayType.LINE,
  20. widgetType: WidgetType.DISCOVER,
  21. queries: [
  22. {
  23. conditions: 'event.type:error',
  24. fields: ['count()', 'failure_count()'],
  25. name: 'errors',
  26. orderby: '',
  27. },
  28. {
  29. conditions: 'event.type:default',
  30. fields: ['count()', 'failure_count()'],
  31. name: 'default',
  32. orderby: '',
  33. },
  34. ],
  35. };
  36. const selection = {
  37. projects: [1],
  38. environments: ['prod'],
  39. datetime: {
  40. period: '14d',
  41. start: null,
  42. end: null,
  43. utc: false,
  44. },
  45. };
  46. const api = new Client();
  47. let eventsMock;
  48. beforeEach(function () {
  49. MockApiClient.addMockResponse({
  50. url: '/organizations/org-slug/events-stats/',
  51. body: [],
  52. });
  53. MockApiClient.addMockResponse({
  54. url: '/organizations/org-slug/events-geo/',
  55. body: [],
  56. });
  57. eventsMock = MockApiClient.addMockResponse({
  58. url: '/organizations/org-slug/eventsv2/',
  59. body: {
  60. meta: {title: 'string'},
  61. data: [{title: 'title'}],
  62. },
  63. });
  64. });
  65. afterEach(function () {
  66. MockApiClient.clearMockResponses();
  67. });
  68. it('renders with Open in Discover button and opens the Query Selector Modal when clicked', async function () {
  69. const spy = jest.spyOn(modal, 'openDashboardWidgetQuerySelectorModal');
  70. mountWithTheme(
  71. <WidgetCard
  72. api={api}
  73. organization={initialData.organization}
  74. widget={multipleQueryWidget}
  75. selection={selection}
  76. isEditing={false}
  77. onDelete={() => undefined}
  78. onEdit={() => undefined}
  79. onDuplicate={() => undefined}
  80. renderErrorMessage={() => undefined}
  81. isSorting={false}
  82. currentWidgetDragging={false}
  83. showContextMenu
  84. widgetLimitReached={false}
  85. />
  86. );
  87. await tick();
  88. userEvent.click(screen.getByTestId('context-menu'));
  89. expect(screen.getByText('Open in Discover')).toBeInTheDocument();
  90. userEvent.click(screen.getByText('Open in Discover'));
  91. expect(spy).toHaveBeenCalledWith({
  92. organization: initialData.organization,
  93. widget: multipleQueryWidget,
  94. });
  95. });
  96. it('renders with Open in Discover button and opens in Discover when clicked', async function () {
  97. mountWithTheme(
  98. <WidgetCard
  99. api={api}
  100. organization={initialData.organization}
  101. widget={{...multipleQueryWidget, queries: [multipleQueryWidget.queries[0]]}}
  102. selection={selection}
  103. isEditing={false}
  104. onDelete={() => undefined}
  105. onEdit={() => undefined}
  106. onDuplicate={() => undefined}
  107. renderErrorMessage={() => undefined}
  108. isSorting={false}
  109. currentWidgetDragging={false}
  110. showContextMenu
  111. widgetLimitReached={false}
  112. />
  113. );
  114. await tick();
  115. userEvent.click(screen.getByTestId('context-menu'));
  116. expect(screen.getByText('Open in Discover')).toBeInTheDocument();
  117. expect(screen.getByText('Open in Discover').closest('a')).toHaveAttribute(
  118. 'href',
  119. '/organizations/org-slug/discover/results/?environment=prod&field=count%28%29&field=failure_count%28%29&name=Errors&project=1&query=event.type%3Aerror&statsPeriod=14d&yAxis=count%28%29&yAxis=failure_count%28%29'
  120. );
  121. });
  122. it('Opens in Discover with World Map', async function () {
  123. mountWithTheme(
  124. <WidgetCard
  125. api={api}
  126. organization={initialData.organization}
  127. widget={{
  128. ...multipleQueryWidget,
  129. displayType: DisplayType.WORLD_MAP,
  130. queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
  131. }}
  132. selection={selection}
  133. isEditing={false}
  134. onDelete={() => undefined}
  135. onEdit={() => undefined}
  136. onDuplicate={() => undefined}
  137. renderErrorMessage={() => undefined}
  138. isSorting={false}
  139. currentWidgetDragging={false}
  140. showContextMenu
  141. widgetLimitReached={false}
  142. />
  143. );
  144. await tick();
  145. userEvent.click(screen.getByTestId('context-menu'));
  146. expect(screen.getByText('Open in Discover')).toBeInTheDocument();
  147. expect(screen.getByText('Open in Discover').closest('a')).toHaveAttribute(
  148. 'href',
  149. '/organizations/org-slug/discover/results/?display=worldmap&environment=prod&field=geo.country_code&field=count%28%29&name=Errors&project=1&query=event.type%3Aerror%20has%3Ageo.country_code&statsPeriod=14d&yAxis=count%28%29'
  150. );
  151. });
  152. it('Opens in Discover with prepended fields pulled from equations', async function () {
  153. mountWithTheme(
  154. <WidgetCard
  155. api={api}
  156. organization={initialData.organization}
  157. widget={{
  158. ...multipleQueryWidget,
  159. queries: [
  160. {
  161. ...multipleQueryWidget.queries[0],
  162. fields: [
  163. 'equation|(count() + failure_count()) / count_if(transaction.duration,equals,300)',
  164. ],
  165. },
  166. ],
  167. }}
  168. selection={selection}
  169. isEditing={false}
  170. onDelete={() => undefined}
  171. onEdit={() => undefined}
  172. onDuplicate={() => undefined}
  173. renderErrorMessage={() => undefined}
  174. isSorting={false}
  175. currentWidgetDragging={false}
  176. showContextMenu
  177. widgetLimitReached={false}
  178. />
  179. );
  180. await tick();
  181. userEvent.click(screen.getByTestId('context-menu'));
  182. expect(screen.getByText('Open in Discover')).toBeInTheDocument();
  183. expect(screen.getByText('Open in Discover').closest('a')).toHaveAttribute(
  184. 'href',
  185. '/organizations/org-slug/discover/results/?environment=prod&field=count_if%28transaction.duration%2Cequals%2C300%29&field=failure_count%28%29&field=count%28%29&field=equation%7C%28count%28%29%20%2B%20failure_count%28%29%29%20%2F%20count_if%28transaction.duration%2Cequals%2C300%29&name=Errors&project=1&query=event.type%3Aerror&statsPeriod=14d&yAxis=equation%7C%28count%28%29%20%2B%20failure_count%28%29%29%20%2F%20count_if%28transaction.duration%2Cequals%2C300%29'
  186. );
  187. });
  188. it('Opens in Discover with Top N', async function () {
  189. mountWithTheme(
  190. <WidgetCard
  191. api={api}
  192. organization={initialData.organization}
  193. widget={{
  194. ...multipleQueryWidget,
  195. displayType: DisplayType.TOP_N,
  196. queries: [
  197. {...multipleQueryWidget.queries[0], fields: ['transaction', 'count()']},
  198. ],
  199. }}
  200. selection={selection}
  201. isEditing={false}
  202. onDelete={() => undefined}
  203. onEdit={() => undefined}
  204. onDuplicate={() => undefined}
  205. renderErrorMessage={() => undefined}
  206. isSorting={false}
  207. currentWidgetDragging={false}
  208. showContextMenu
  209. widgetLimitReached={false}
  210. />
  211. );
  212. await tick();
  213. userEvent.click(screen.getByTestId('context-menu'));
  214. expect(screen.getByText('Open in Discover')).toBeInTheDocument();
  215. expect(screen.getByText('Open in Discover').closest('a')).toHaveAttribute(
  216. 'href',
  217. '/organizations/org-slug/discover/results/?display=top5&environment=prod&field=transaction&name=Errors&project=1&query=event.type%3Aerror&statsPeriod=14d&yAxis=count%28%29'
  218. );
  219. });
  220. it('calls onDuplicate when Duplicate Widget is clicked', async function () {
  221. const mock = jest.fn();
  222. mountWithTheme(
  223. <WidgetCard
  224. api={api}
  225. organization={initialData.organization}
  226. widget={{
  227. ...multipleQueryWidget,
  228. displayType: DisplayType.WORLD_MAP,
  229. queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
  230. }}
  231. selection={selection}
  232. isEditing={false}
  233. onDelete={() => undefined}
  234. onEdit={() => undefined}
  235. onDuplicate={mock}
  236. renderErrorMessage={() => undefined}
  237. isSorting={false}
  238. currentWidgetDragging={false}
  239. showContextMenu
  240. widgetLimitReached={false}
  241. />
  242. );
  243. await tick();
  244. userEvent.click(screen.getByTestId('context-menu'));
  245. expect(screen.getByText('Duplicate Widget')).toBeInTheDocument();
  246. userEvent.click(screen.getByText('Duplicate Widget'));
  247. expect(mock).toHaveBeenCalledTimes(1);
  248. });
  249. it('does not add duplicate widgets if max widget is reached', async function () {
  250. const mock = jest.fn();
  251. mountWithTheme(
  252. <WidgetCard
  253. api={api}
  254. organization={initialData.organization}
  255. widget={{
  256. ...multipleQueryWidget,
  257. displayType: DisplayType.WORLD_MAP,
  258. queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
  259. }}
  260. selection={selection}
  261. isEditing={false}
  262. onDelete={() => undefined}
  263. onEdit={() => undefined}
  264. onDuplicate={mock}
  265. renderErrorMessage={() => undefined}
  266. isSorting={false}
  267. currentWidgetDragging={false}
  268. showContextMenu
  269. widgetLimitReached
  270. />
  271. );
  272. await tick();
  273. userEvent.click(screen.getByTestId('context-menu'));
  274. expect(screen.getByText('Duplicate Widget')).toBeInTheDocument();
  275. userEvent.click(screen.getByText('Duplicate Widget'));
  276. expect(mock).toHaveBeenCalledTimes(0);
  277. });
  278. it('calls onEdit when Edit Widget is clicked', async function () {
  279. const mock = jest.fn();
  280. mountWithTheme(
  281. <WidgetCard
  282. api={api}
  283. organization={initialData.organization}
  284. widget={{
  285. ...multipleQueryWidget,
  286. displayType: DisplayType.WORLD_MAP,
  287. queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
  288. }}
  289. selection={selection}
  290. isEditing={false}
  291. onDelete={() => undefined}
  292. onEdit={mock}
  293. onDuplicate={() => undefined}
  294. renderErrorMessage={() => undefined}
  295. isSorting={false}
  296. currentWidgetDragging={false}
  297. showContextMenu
  298. widgetLimitReached={false}
  299. />
  300. );
  301. await tick();
  302. userEvent.click(screen.getByTestId('context-menu'));
  303. expect(screen.getByText('Edit Widget')).toBeInTheDocument();
  304. userEvent.click(screen.getByText('Edit Widget'));
  305. expect(mock).toHaveBeenCalledTimes(1);
  306. });
  307. it('renders delete widget option', async function () {
  308. const mock = jest.fn();
  309. mountWithTheme(
  310. <WidgetCard
  311. api={api}
  312. organization={initialData.organization}
  313. widget={{
  314. ...multipleQueryWidget,
  315. displayType: DisplayType.WORLD_MAP,
  316. queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
  317. }}
  318. selection={selection}
  319. isEditing={false}
  320. onDelete={mock}
  321. onEdit={() => undefined}
  322. onDuplicate={() => undefined}
  323. renderErrorMessage={() => undefined}
  324. isSorting={false}
  325. currentWidgetDragging={false}
  326. showContextMenu
  327. widgetLimitReached={false}
  328. />
  329. );
  330. await tick();
  331. userEvent.click(screen.getByTestId('context-menu'));
  332. expect(screen.getByText('Delete Widget')).toBeInTheDocument();
  333. userEvent.click(screen.getByText('Delete Widget'));
  334. // Confirm Modal
  335. mountGlobalModal();
  336. await screen.findByRole('dialog');
  337. userEvent.click(screen.getByTestId('confirm-button'));
  338. expect(mock).toHaveBeenCalled();
  339. });
  340. it('calls eventsV2 with a limit of 20 items', async function () {
  341. const mock = jest.fn();
  342. mountWithTheme(
  343. <WidgetCard
  344. api={api}
  345. organization={initialData.organization}
  346. widget={{
  347. ...multipleQueryWidget,
  348. displayType: DisplayType.TABLE,
  349. queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
  350. }}
  351. selection={selection}
  352. isEditing={false}
  353. onDelete={mock}
  354. onEdit={() => undefined}
  355. onDuplicate={() => undefined}
  356. renderErrorMessage={() => undefined}
  357. isSorting={false}
  358. currentWidgetDragging={false}
  359. showContextMenu
  360. widgetLimitReached={false}
  361. tableItemLimit={20}
  362. />
  363. );
  364. await tick();
  365. expect(eventsMock).toHaveBeenCalledWith(
  366. '/organizations/org-slug/eventsv2/',
  367. expect.objectContaining({
  368. query: expect.objectContaining({
  369. per_page: 20,
  370. }),
  371. })
  372. );
  373. });
  374. it('calls eventsV2 with a default limit of 5 items', async function () {
  375. const mock = jest.fn();
  376. mountWithTheme(
  377. <WidgetCard
  378. api={api}
  379. organization={initialData.organization}
  380. widget={{
  381. ...multipleQueryWidget,
  382. displayType: DisplayType.TABLE,
  383. queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
  384. }}
  385. selection={selection}
  386. isEditing={false}
  387. onDelete={mock}
  388. onEdit={() => undefined}
  389. onDuplicate={() => undefined}
  390. renderErrorMessage={() => undefined}
  391. isSorting={false}
  392. currentWidgetDragging={false}
  393. showContextMenu
  394. widgetLimitReached={false}
  395. />
  396. );
  397. await tick();
  398. expect(eventsMock).toHaveBeenCalledWith(
  399. '/organizations/org-slug/eventsv2/',
  400. expect.objectContaining({
  401. query: expect.objectContaining({
  402. per_page: 5,
  403. }),
  404. })
  405. );
  406. });
  407. });