widgetBuilderDataset.spec.tsx 45 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453
  1. import {urlEncode} from '@sentry/utils';
  2. import {DashboardFixture} from 'sentry-fixture/dashboard';
  3. import {LocationFixture} from 'sentry-fixture/locationFixture';
  4. import {MetricsFieldFixture} from 'sentry-fixture/metrics';
  5. import {SessionsFieldFixture} from 'sentry-fixture/sessions';
  6. import {TagsFixture} from 'sentry-fixture/tags';
  7. import {initializeOrg} from 'sentry-test/initializeOrg';
  8. import {
  9. render,
  10. screen,
  11. userEvent,
  12. waitFor,
  13. within,
  14. } from 'sentry-test/reactTestingLibrary';
  15. import selectEvent from 'sentry-test/selectEvent';
  16. import {resetMockDate, setMockDate} from 'sentry-test/utils';
  17. import ProjectsStore from 'sentry/stores/projectsStore';
  18. import TagStore from 'sentry/stores/tagStore';
  19. import {ERROR_FIELDS, ERRORS_AGGREGATION_FUNCTIONS} from 'sentry/utils/discover/fields';
  20. import type {DashboardDetails, Widget} from 'sentry/views/dashboards/types';
  21. import {
  22. DashboardWidgetSource,
  23. DisplayType,
  24. WidgetType,
  25. } from 'sentry/views/dashboards/types';
  26. import type {WidgetBuilderProps} from 'sentry/views/dashboards/widgetBuilder';
  27. import WidgetBuilder from 'sentry/views/dashboards/widgetBuilder';
  28. import WidgetLegendSelectionState from '../widgetLegendSelectionState';
  29. const defaultOrgFeatures = [
  30. 'performance-view',
  31. 'dashboards-edit',
  32. 'global-views',
  33. 'dashboards-mep',
  34. ];
  35. function mockDashboard(dashboard: Partial<DashboardDetails>): DashboardDetails {
  36. return {
  37. id: '1',
  38. title: 'Dashboard',
  39. createdBy: undefined,
  40. dateCreated: '2020-01-01T00:00:00.000Z',
  41. widgets: [],
  42. projects: [],
  43. filters: {},
  44. ...dashboard,
  45. };
  46. }
  47. function renderTestComponent({
  48. dashboard,
  49. query,
  50. orgFeatures,
  51. onSave,
  52. params,
  53. }: {
  54. dashboard?: WidgetBuilderProps['dashboard'];
  55. onSave?: WidgetBuilderProps['onSave'];
  56. orgFeatures?: string[];
  57. params?: Partial<WidgetBuilderProps['params']>;
  58. query?: Record<string, any>;
  59. } = {}) {
  60. const {organization, projects, router} = initializeOrg({
  61. organization: {
  62. features: orgFeatures ?? defaultOrgFeatures,
  63. },
  64. router: {
  65. location: {
  66. query: {
  67. source: DashboardWidgetSource.DASHBOARDS,
  68. ...query,
  69. },
  70. },
  71. },
  72. });
  73. ProjectsStore.loadInitialData(projects);
  74. const widgetLegendState = new WidgetLegendSelectionState({
  75. location: LocationFixture(),
  76. dashboard: DashboardFixture([], {id: 'new', title: 'Dashboard', ...dashboard}),
  77. organization,
  78. router,
  79. });
  80. render(
  81. <WidgetBuilder
  82. route={{}}
  83. router={router}
  84. routes={router.routes}
  85. routeParams={router.params}
  86. location={router.location}
  87. dashboard={{
  88. id: 'new',
  89. title: 'Dashboard',
  90. createdBy: undefined,
  91. dateCreated: '2020-01-01T00:00:00.000Z',
  92. widgets: [],
  93. projects: [],
  94. filters: {},
  95. ...dashboard,
  96. }}
  97. onSave={onSave ?? jest.fn()}
  98. params={{
  99. orgId: organization.slug,
  100. dashboardId: dashboard?.id ?? 'new',
  101. ...params,
  102. }}
  103. widgetLegendState={widgetLegendState}
  104. />,
  105. {
  106. router,
  107. organization,
  108. }
  109. );
  110. return {router};
  111. }
  112. describe('WidgetBuilder', function () {
  113. const untitledDashboard: DashboardDetails = {
  114. id: '1',
  115. title: 'Untitled Dashboard',
  116. createdBy: undefined,
  117. dateCreated: '2020-01-01T00:00:00.000Z',
  118. widgets: [],
  119. projects: [],
  120. filters: {},
  121. };
  122. const testDashboard: DashboardDetails = {
  123. id: '2',
  124. title: 'Test Dashboard',
  125. createdBy: undefined,
  126. dateCreated: '2020-01-01T00:00:00.000Z',
  127. widgets: [],
  128. projects: [],
  129. filters: {},
  130. };
  131. let eventsMock: jest.Mock | undefined;
  132. let sessionsDataMock: jest.Mock | undefined;
  133. let metricsDataMock: jest.Mock | undefined;
  134. let measurementsMetaMock: jest.Mock | undefined;
  135. beforeEach(function () {
  136. MockApiClient.addMockResponse({
  137. url: '/organizations/org-slug/dashboards/',
  138. body: [
  139. {...untitledDashboard, widgetDisplay: [DisplayType.TABLE]},
  140. {...testDashboard, widgetDisplay: [DisplayType.AREA]},
  141. ],
  142. });
  143. MockApiClient.addMockResponse({
  144. url: '/organizations/org-slug/dashboards/widgets/',
  145. method: 'POST',
  146. statusCode: 200,
  147. body: [],
  148. });
  149. MockApiClient.addMockResponse({
  150. url: '/organizations/org-slug/eventsv2/',
  151. method: 'GET',
  152. statusCode: 200,
  153. body: {
  154. meta: {},
  155. data: [],
  156. },
  157. });
  158. eventsMock = MockApiClient.addMockResponse({
  159. url: '/organizations/org-slug/events/',
  160. method: 'GET',
  161. statusCode: 200,
  162. body: {
  163. meta: {fields: {}},
  164. data: [],
  165. },
  166. });
  167. MockApiClient.addMockResponse({
  168. url: '/organizations/org-slug/projects/',
  169. method: 'GET',
  170. body: [],
  171. });
  172. MockApiClient.addMockResponse({
  173. url: '/organizations/org-slug/recent-searches/',
  174. method: 'GET',
  175. body: [],
  176. });
  177. MockApiClient.addMockResponse({
  178. url: '/organizations/org-slug/recent-searches/',
  179. method: 'POST',
  180. body: [],
  181. });
  182. MockApiClient.addMockResponse({
  183. url: '/organizations/org-slug/issues/',
  184. method: 'GET',
  185. body: [],
  186. });
  187. MockApiClient.addMockResponse({
  188. url: '/organizations/org-slug/events-stats/',
  189. body: [],
  190. });
  191. MockApiClient.addMockResponse({
  192. url: '/organizations/org-slug/tags/event.type/values/',
  193. body: [{count: 2, name: 'Nvidia 1080ti'}],
  194. });
  195. MockApiClient.addMockResponse({
  196. url: '/organizations/org-slug/users/',
  197. body: [],
  198. });
  199. sessionsDataMock = MockApiClient.addMockResponse({
  200. method: 'GET',
  201. url: '/organizations/org-slug/sessions/',
  202. body: SessionsFieldFixture(`sum(session)`),
  203. });
  204. metricsDataMock = MockApiClient.addMockResponse({
  205. method: 'GET',
  206. url: '/organizations/org-slug/metrics/data/',
  207. body: MetricsFieldFixture('session.all'),
  208. });
  209. MockApiClient.addMockResponse({
  210. url: '/organizations/org-slug/tags/',
  211. method: 'GET',
  212. body: TagsFixture(),
  213. });
  214. measurementsMetaMock = MockApiClient.addMockResponse({
  215. url: '/organizations/org-slug/measurements-meta/',
  216. method: 'GET',
  217. body: {},
  218. });
  219. MockApiClient.addMockResponse({
  220. url: '/organizations/org-slug/tags/is/values/',
  221. method: 'GET',
  222. body: [],
  223. });
  224. MockApiClient.addMockResponse({
  225. url: '/organizations/org-slug/metrics-compatibility/',
  226. method: 'GET',
  227. body: {
  228. incompatible_projects: [],
  229. compatible_projects: [1],
  230. },
  231. });
  232. MockApiClient.addMockResponse({
  233. url: '/organizations/org-slug/metrics-compatibility-sums/',
  234. method: 'GET',
  235. body: {
  236. sum: {
  237. metrics: 988803,
  238. metrics_null: 0,
  239. metrics_unparam: 132,
  240. },
  241. },
  242. });
  243. MockApiClient.addMockResponse({
  244. url: '/organizations/org-slug/releases/',
  245. body: [],
  246. });
  247. MockApiClient.addMockResponse({
  248. url: `/organizations/org-slug/spans/fields/`,
  249. body: [],
  250. });
  251. TagStore.reset();
  252. });
  253. afterEach(function () {
  254. MockApiClient.clearMockResponses();
  255. jest.clearAllMocks();
  256. resetMockDate();
  257. });
  258. describe('Release Widgets', function () {
  259. it('shows the Release Health dataset', async function () {
  260. renderTestComponent();
  261. expect(await screen.findByText('Errors and Transactions')).toBeInTheDocument();
  262. expect(screen.getByText('Releases (Sessions, Crash rates)')).toBeInTheDocument();
  263. });
  264. it('maintains the selected dataset when display type is changed', async function () {
  265. renderTestComponent();
  266. expect(
  267. await screen.findByText('Releases (Sessions, Crash rates)')
  268. ).toBeInTheDocument();
  269. expect(screen.getByRole('radio', {name: /Releases/i})).not.toBeChecked();
  270. await userEvent.click(screen.getByRole('radio', {name: /Releases/i}));
  271. await waitFor(() =>
  272. expect(screen.getByRole('radio', {name: /Releases/i})).toBeChecked()
  273. );
  274. await userEvent.click(screen.getByText('Table'));
  275. await userEvent.click(screen.getByText('Line Chart'));
  276. await waitFor(() =>
  277. expect(screen.getByRole('radio', {name: /Releases/i})).toBeChecked()
  278. );
  279. });
  280. it('displays releases tags', async function () {
  281. renderTestComponent();
  282. expect(
  283. await screen.findByText('Releases (Sessions, Crash rates)')
  284. ).toBeInTheDocument();
  285. await userEvent.click(screen.getByRole('radio', {name: /Releases/i}));
  286. expect(screen.getByText('crash_free_rate(…)')).toBeInTheDocument();
  287. expect(screen.getByText('session')).toBeInTheDocument();
  288. await userEvent.click(screen.getByText('crash_free_rate(…)'));
  289. expect(screen.getByText('count_unique(…)')).toBeInTheDocument();
  290. expect(screen.getByText('release')).toBeInTheDocument();
  291. expect(screen.getByText('environment')).toBeInTheDocument();
  292. expect(screen.getByText('session.status')).toBeInTheDocument();
  293. await userEvent.click(screen.getByText('count_unique(…)'));
  294. expect(screen.getByText('user')).toBeInTheDocument();
  295. });
  296. it('does not display tags as params', async function () {
  297. renderTestComponent();
  298. expect(
  299. await screen.findByText('Releases (Sessions, Crash rates)')
  300. ).toBeInTheDocument();
  301. await userEvent.click(screen.getByRole('radio', {name: /Releases/i}));
  302. expect(screen.getByText('crash_free_rate(…)')).toBeInTheDocument();
  303. await selectEvent.select(screen.getByText('crash_free_rate(…)'), 'count_unique(…)');
  304. await userEvent.click(screen.getByText('user'));
  305. expect(screen.queryByText('release')).not.toBeInTheDocument();
  306. expect(screen.queryByText('environment')).not.toBeInTheDocument();
  307. expect(screen.queryByText('session.status')).not.toBeInTheDocument();
  308. });
  309. it('does not allow sort by when session.status is selected', async function () {
  310. renderTestComponent();
  311. expect(
  312. await screen.findByText('Releases (Sessions, Crash rates)')
  313. ).toBeInTheDocument();
  314. await userEvent.click(screen.getByRole('radio', {name: /Releases/i}));
  315. expect(screen.getByText('High to low')).toBeEnabled();
  316. expect(screen.getByText('crash_free_rate(session)')).toBeInTheDocument();
  317. await userEvent.click(screen.getByLabelText('Add a Column'));
  318. await selectEvent.select(screen.getByText('(Required)'), 'session.status');
  319. expect(screen.getByRole('textbox', {name: 'Sort direction'})).toBeDisabled();
  320. expect(screen.getByRole('textbox', {name: 'Sort by'})).toBeDisabled();
  321. });
  322. it('does not allow sort on tags except release', async function () {
  323. setMockDate(new Date('2022-08-02'));
  324. renderTestComponent();
  325. expect(
  326. await screen.findByText('Releases (Sessions, Crash rates)')
  327. ).toBeInTheDocument();
  328. await userEvent.click(screen.getByRole('radio', {name: /Releases/i}), {
  329. delay: null,
  330. });
  331. expect(
  332. within(screen.getByTestId('sort-by-step')).getByText('High to low')
  333. ).toBeEnabled();
  334. expect(
  335. within(screen.getByTestId('sort-by-step')).getByText('crash_free_rate(session)')
  336. ).toBeInTheDocument();
  337. await userEvent.click(screen.getByLabelText('Add a Column'), {delay: null});
  338. await selectEvent.select(screen.getByText('(Required)'), 'release');
  339. await userEvent.click(screen.getByLabelText('Add a Column'), {delay: null});
  340. await selectEvent.select(screen.getByText('(Required)'), 'environment');
  341. expect(await screen.findByText('Sort by a column')).toBeInTheDocument();
  342. // Selector "sortDirection"
  343. expect(screen.getByText('High to low')).toBeInTheDocument();
  344. // Selector "sortBy"
  345. await userEvent.click(screen.getAllByText('crash_free_rate(session)')[1], {
  346. delay: null,
  347. });
  348. // release exists in sort by selector
  349. expect(screen.getAllByText('release')).toHaveLength(3);
  350. // environment does not exist in sort by selector
  351. expect(screen.getAllByText('environment')).toHaveLength(2);
  352. });
  353. it('makes the appropriate sessions call', async function () {
  354. setMockDate(new Date('2022-08-02'));
  355. renderTestComponent();
  356. expect(
  357. await screen.findByText('Releases (Sessions, Crash rates)')
  358. ).toBeInTheDocument();
  359. await userEvent.click(screen.getByRole('radio', {name: /Releases/i}), {
  360. delay: null,
  361. });
  362. await userEvent.click(screen.getByText('Table'), {delay: null});
  363. await userEvent.click(screen.getByText('Line Chart'), {delay: null});
  364. await waitFor(() =>
  365. expect(metricsDataMock).toHaveBeenLastCalledWith(
  366. `/organizations/org-slug/metrics/data/`,
  367. expect.objectContaining({
  368. query: expect.objectContaining({
  369. environment: [],
  370. field: [`session.crash_free_rate`],
  371. groupBy: [],
  372. interval: '5m',
  373. project: [],
  374. statsPeriod: '24h',
  375. }),
  376. })
  377. )
  378. );
  379. });
  380. it('calls the session endpoint with the right limit', async function () {
  381. setMockDate(new Date('2022-08-02'));
  382. renderTestComponent();
  383. expect(
  384. await screen.findByText('Releases (Sessions, Crash rates)')
  385. ).toBeInTheDocument();
  386. await userEvent.click(screen.getByRole('radio', {name: /Releases/i}), {
  387. delay: null,
  388. });
  389. await userEvent.click(screen.getByText('Table'), {delay: null});
  390. await userEvent.click(screen.getByText('Line Chart'), {delay: null});
  391. await selectEvent.select(await screen.findByText('Select group'), 'project');
  392. expect(screen.getByText('Limit to 5 results')).toBeInTheDocument();
  393. await waitFor(() =>
  394. expect(metricsDataMock).toHaveBeenLastCalledWith(
  395. `/organizations/org-slug/metrics/data/`,
  396. expect.objectContaining({
  397. query: expect.objectContaining({
  398. environment: [],
  399. field: ['session.crash_free_rate'],
  400. groupBy: ['project_id'],
  401. interval: '5m',
  402. orderBy: '-session.crash_free_rate',
  403. per_page: 5,
  404. project: [],
  405. statsPeriod: '24h',
  406. }),
  407. })
  408. )
  409. );
  410. });
  411. it('calls sessions api when session.status is selected as a groupby', async function () {
  412. setMockDate(new Date('2022-08-02'));
  413. renderTestComponent();
  414. expect(
  415. await screen.findByText('Releases (Sessions, Crash rates)')
  416. ).toBeInTheDocument();
  417. await userEvent.click(screen.getByRole('radio', {name: /Releases/i}), {
  418. delay: null,
  419. });
  420. await userEvent.click(screen.getByText('Table'), {delay: null});
  421. await userEvent.click(screen.getByText('Line Chart'), {delay: null});
  422. await selectEvent.select(await screen.findByText('Select group'), 'session.status');
  423. expect(screen.getByText('Limit to 5 results')).toBeInTheDocument();
  424. await waitFor(() =>
  425. expect(sessionsDataMock).toHaveBeenLastCalledWith(
  426. `/organizations/org-slug/sessions/`,
  427. expect.objectContaining({
  428. query: expect.objectContaining({
  429. environment: [],
  430. field: ['crash_free_rate(session)'],
  431. groupBy: ['session.status'],
  432. interval: '5m',
  433. project: [],
  434. statsPeriod: '24h',
  435. }),
  436. })
  437. )
  438. );
  439. });
  440. it('displays the correct options for area chart', async function () {
  441. renderTestComponent();
  442. expect(
  443. await screen.findByText('Releases (Sessions, Crash rates)')
  444. ).toBeInTheDocument();
  445. // change dataset to releases
  446. await userEvent.click(screen.getByRole('radio', {name: /Releases/i}));
  447. await userEvent.click(screen.getByText('Table'));
  448. await userEvent.click(screen.getByText('Line Chart'));
  449. expect(screen.getByText('crash_free_rate(…)')).toBeInTheDocument();
  450. expect(screen.getByText(`session`)).toBeInTheDocument();
  451. await userEvent.click(screen.getByText('crash_free_rate(…)'));
  452. expect(screen.getByText('count_unique(…)')).toBeInTheDocument();
  453. await userEvent.click(screen.getByText('count_unique(…)'));
  454. expect(screen.getByText('user')).toBeInTheDocument();
  455. });
  456. it('sets widgetType to release', async function () {
  457. setMockDate(new Date('2022-08-02'));
  458. renderTestComponent();
  459. await userEvent.click(await screen.findByText('Releases (Sessions, Crash rates)'), {
  460. delay: null,
  461. });
  462. expect(metricsDataMock).toHaveBeenCalled();
  463. expect(screen.getByRole('radio', {name: /Releases/i})).toBeChecked();
  464. });
  465. it('does not display "add an equation" button', async function () {
  466. const widget: Widget = {
  467. title: 'Release Widget',
  468. displayType: DisplayType.TABLE,
  469. widgetType: WidgetType.RELEASE,
  470. queries: [
  471. {
  472. name: 'errors',
  473. conditions: '',
  474. fields: ['session.crash_free_rate'],
  475. columns: ['scount_abnormal(session)'],
  476. aggregates: ['session.crash_free_rate'],
  477. orderby: '-session.crash_free_rate',
  478. },
  479. ],
  480. interval: '1d',
  481. id: '1',
  482. };
  483. const dashboard = mockDashboard({widgets: [widget]});
  484. renderTestComponent({
  485. dashboard,
  486. params: {
  487. widgetIndex: '0',
  488. },
  489. });
  490. // Select line chart display
  491. await userEvent.click(await screen.findByText('Table'));
  492. await userEvent.click(screen.getByText('Line Chart'));
  493. await waitFor(() =>
  494. expect(screen.queryByLabelText('Add an Equation')).not.toBeInTheDocument()
  495. );
  496. });
  497. it('suggests release properties for sessions dataset', async function () {
  498. renderTestComponent();
  499. await userEvent.click(
  500. await screen.findByRole('combobox', {name: 'Add a search term'})
  501. );
  502. await userEvent.paste('session.status:');
  503. const row = await screen.findByRole('row', {name: 'session.status:'});
  504. expect(row).toHaveAttribute('aria-invalid', 'true');
  505. await userEvent.click(
  506. screen.getByRole('button', {name: 'Remove filter: session.status'})
  507. );
  508. await userEvent.click(screen.getByText('Releases (Sessions, Crash rates)'));
  509. await userEvent.click(
  510. await screen.findByRole('combobox', {name: 'Add a search term'})
  511. );
  512. expect(await screen.findByRole('button', {name: 'All'})).toBeInTheDocument();
  513. const menu = screen.getByRole('listbox');
  514. const groups = within(menu).getAllByRole('group');
  515. const all = groups[0];
  516. expect(within(all).getByRole('option', {name: 'environment'})).toBeInTheDocument();
  517. expect(within(all).getByRole('option', {name: 'project'})).toBeInTheDocument();
  518. expect(within(all).getByRole('option', {name: 'release'})).toBeInTheDocument();
  519. });
  520. it('adds a function when the only column chosen in a table is a tag', async function () {
  521. setMockDate(new Date('2022-08-02'));
  522. renderTestComponent();
  523. await userEvent.click(await screen.findByText('Releases (Sessions, Crash rates)'), {
  524. delay: null,
  525. });
  526. await selectEvent.select(screen.getByText('crash_free_rate(…)'), 'environment');
  527. // 1 in the table header, 1 in the column selector, and 1 in the sort by
  528. expect(screen.getAllByText(/crash_free_rate/)).toHaveLength(3);
  529. expect(screen.getAllByText('environment')).toHaveLength(2);
  530. });
  531. });
  532. describe('Issue Widgets', function () {
  533. it('sets widgetType to issues', async function () {
  534. const handleSave = jest.fn();
  535. renderTestComponent({onSave: handleSave});
  536. await userEvent.click(
  537. await screen.findByText('Issues (States, Assignment, Time, etc.)')
  538. );
  539. await userEvent.click(screen.getByLabelText('Add Widget'));
  540. await waitFor(() => {
  541. expect(handleSave).toHaveBeenCalledWith([
  542. expect.objectContaining({
  543. title: 'Custom Widget',
  544. displayType: DisplayType.TABLE,
  545. interval: '5m',
  546. widgetType: WidgetType.ISSUE,
  547. queries: [
  548. {
  549. conditions: '',
  550. fields: ['issue', 'assignee', 'title'],
  551. columns: ['issue', 'assignee', 'title'],
  552. aggregates: [],
  553. fieldAliases: [],
  554. name: '',
  555. orderby: 'date',
  556. },
  557. ],
  558. }),
  559. ]);
  560. });
  561. expect(handleSave).toHaveBeenCalledTimes(1);
  562. });
  563. it('render issues dataset disabled when the display type is not set to table', async function () {
  564. renderTestComponent({
  565. query: {
  566. source: DashboardWidgetSource.DISCOVERV2,
  567. },
  568. });
  569. await userEvent.click(await screen.findByText('Table'));
  570. await userEvent.click(screen.getByText('Line Chart'));
  571. expect(
  572. screen.getByRole('radio', {
  573. name: 'Errors and Transactions',
  574. })
  575. ).toBeEnabled();
  576. expect(
  577. screen.getByRole('radio', {
  578. name: 'Issues (States, Assignment, Time, etc.)',
  579. })
  580. ).toBeDisabled();
  581. });
  582. it('renders errors and transactions dataset options', async function () {
  583. renderTestComponent({
  584. query: {
  585. source: DashboardWidgetSource.DISCOVERV2,
  586. },
  587. orgFeatures: [...defaultOrgFeatures, 'performance-discover-dataset-selector'],
  588. });
  589. await userEvent.click(await screen.findByText('Table'));
  590. await userEvent.click(screen.getByText('Line Chart'));
  591. expect(
  592. screen.getByRole('radio', {
  593. name: 'Errors (TypeError, InvalidSearchQuery, etc)',
  594. })
  595. ).toBeEnabled();
  596. expect(
  597. screen.getByRole('radio', {
  598. name: 'Transactions',
  599. })
  600. ).toBeEnabled();
  601. });
  602. it('disables moving and deleting issue column', async function () {
  603. renderTestComponent();
  604. await userEvent.click(
  605. await screen.findByText('Issues (States, Assignment, Time, etc.)')
  606. );
  607. expect(
  608. within(screen.getByTestId('choose-column-step')).getByText('issue')
  609. ).toBeInTheDocument();
  610. expect(
  611. within(screen.getByTestId('choose-column-step')).getByText('assignee')
  612. ).toBeInTheDocument();
  613. expect(
  614. within(screen.getByTestId('choose-column-step')).getByText('title')
  615. ).toBeInTheDocument();
  616. expect(
  617. within(screen.getByTestId('choose-column-step')).getAllByLabelText(
  618. 'Remove column'
  619. )
  620. ).toHaveLength(2);
  621. expect(
  622. within(screen.getByTestId('choose-column-step')).getAllByLabelText(
  623. 'Drag to reorder'
  624. )
  625. ).toHaveLength(3);
  626. await userEvent.click(screen.getAllByLabelText('Remove column')[1]);
  627. await userEvent.click(screen.getAllByLabelText('Remove column')[0]);
  628. expect(
  629. within(screen.getByTestId('choose-column-step')).getByText('issue')
  630. ).toBeInTheDocument();
  631. expect(
  632. within(screen.getByTestId('choose-column-step')).queryByText('assignee')
  633. ).not.toBeInTheDocument();
  634. expect(
  635. within(screen.getByTestId('choose-column-step')).queryByText('title')
  636. ).not.toBeInTheDocument();
  637. expect(
  638. within(screen.getByTestId('choose-column-step')).queryByLabelText('Remove column')
  639. ).not.toBeInTheDocument();
  640. expect(
  641. within(screen.getByTestId('choose-column-step')).queryByLabelText(
  642. 'Drag to reorder'
  643. )
  644. ).not.toBeInTheDocument();
  645. });
  646. it('does not suggest issue filter keys for default dataset', async function () {
  647. renderTestComponent();
  648. await userEvent.click(
  649. await screen.findByRole('combobox', {name: 'Add a search term'})
  650. );
  651. await userEvent.paste('bookmarks');
  652. expect(
  653. await screen.findByRole('option', {
  654. name: '"bookmarks"',
  655. })
  656. ).toBeInTheDocument();
  657. });
  658. it('suggests issue filter keys for issues dataset', async function () {
  659. renderTestComponent();
  660. await userEvent.click(
  661. await screen.findByText('Issues (States, Assignment, Time, etc.)')
  662. );
  663. await userEvent.click(
  664. await screen.findByRole('combobox', {name: 'Add a search term'})
  665. );
  666. await userEvent.paste('ass');
  667. expect(screen.getByLabelText('assigned')).toBeInTheDocument();
  668. });
  669. it('Update table header values (field alias)', async function () {
  670. const handleSave = jest.fn();
  671. renderTestComponent({
  672. onSave: handleSave,
  673. });
  674. await screen.findByText('Table');
  675. await userEvent.click(screen.getByText('Issues (States, Assignment, Time, etc.)'));
  676. await userEvent.type(screen.getAllByPlaceholderText('Alias')[0], 'First Alias');
  677. await userEvent.click(screen.getByText('Add Widget'));
  678. await waitFor(() => {
  679. expect(handleSave).toHaveBeenCalledWith([
  680. expect.objectContaining({
  681. queries: [
  682. expect.objectContaining({
  683. fieldAliases: ['First Alias', '', ''],
  684. }),
  685. ],
  686. }),
  687. ]);
  688. });
  689. });
  690. });
  691. describe('Events Widgets', function () {
  692. describe('Custom Performance Metrics', function () {
  693. it('can choose a custom measurement', async function () {
  694. measurementsMetaMock = MockApiClient.addMockResponse({
  695. url: '/organizations/org-slug/measurements-meta/',
  696. method: 'GET',
  697. body: {'measurements.custom.measurement': {functions: ['p99']}},
  698. });
  699. eventsMock = MockApiClient.addMockResponse({
  700. url: '/organizations/org-slug/events/',
  701. method: 'GET',
  702. statusCode: 200,
  703. body: {
  704. meta: {
  705. fields: {'p99(measurements.total.db.calls)': 'duration'},
  706. isMetricsData: true,
  707. },
  708. data: [{'p99(measurements.total.db.calls)': 10}],
  709. },
  710. });
  711. const {router} = renderTestComponent({
  712. query: {source: DashboardWidgetSource.DISCOVERV2},
  713. dashboard: testDashboard,
  714. orgFeatures: [...defaultOrgFeatures],
  715. });
  716. expect(await screen.findByText('Custom Widget')).toBeInTheDocument();
  717. // 1 in the table header, 1 in the column selector, 1 in the sort field
  718. const countFields = screen.getAllByText('count()');
  719. expect(countFields).toHaveLength(3);
  720. await selectEvent.select(countFields[1], ['p99(…)']);
  721. await selectEvent.select(screen.getByText('transaction.duration'), [
  722. 'measurements.custom.measurement',
  723. ]);
  724. await userEvent.click(screen.getByText('Add Widget'));
  725. await waitFor(() => {
  726. expect(router.push).toHaveBeenCalledWith(
  727. expect.objectContaining({
  728. pathname: '/organizations/org-slug/dashboard/2/',
  729. query: {
  730. displayType: 'table',
  731. interval: '5m',
  732. title: 'Custom Widget',
  733. queryNames: [''],
  734. queryConditions: [''],
  735. queryFields: ['p99(measurements.custom.measurement)'],
  736. queryOrderby: '-p99(measurements.custom.measurement)',
  737. start: null,
  738. end: null,
  739. statsPeriod: '24h',
  740. utc: null,
  741. project: [],
  742. environment: [],
  743. widgetType: 'discover',
  744. },
  745. })
  746. );
  747. });
  748. });
  749. it('raises an alert banner but allows saving widget if widget result is not metrics data and widget is using custom measurements', async function () {
  750. eventsMock = MockApiClient.addMockResponse({
  751. url: '/organizations/org-slug/events/',
  752. method: 'GET',
  753. statusCode: 200,
  754. body: {
  755. meta: {
  756. fields: {'p99(measurements.custom.measurement)': 'duration'},
  757. isMetricsData: false,
  758. },
  759. data: [{'p99(measurements.custom.measurement)': 10}],
  760. },
  761. });
  762. const defaultWidgetQuery = {
  763. name: '',
  764. fields: ['p99(measurements.custom.measurement)'],
  765. columns: [],
  766. aggregates: ['p99(measurements.custom.measurement)'],
  767. conditions: 'user:test.user@sentry.io',
  768. orderby: '',
  769. };
  770. const defaultTableColumns = ['p99(measurements.custom.measurement)'];
  771. renderTestComponent({
  772. query: {
  773. source: DashboardWidgetSource.DISCOVERV2,
  774. defaultWidgetQuery: urlEncode(defaultWidgetQuery),
  775. displayType: DisplayType.TABLE,
  776. defaultTableColumns,
  777. },
  778. orgFeatures: [
  779. ...defaultOrgFeatures,
  780. 'dashboards-mep',
  781. 'dynamic-sampling',
  782. 'mep-rollout-flag',
  783. ],
  784. });
  785. await waitFor(() => {
  786. expect(measurementsMetaMock).toHaveBeenCalled();
  787. });
  788. await waitFor(() => {
  789. expect(eventsMock).toHaveBeenCalled();
  790. });
  791. screen.getByText('Your selection is only applicable to', {exact: false});
  792. expect(screen.getByText('Add Widget').closest('button')).toBeEnabled();
  793. });
  794. it('raises an alert banner if widget result is not metrics data', async function () {
  795. eventsMock = MockApiClient.addMockResponse({
  796. url: '/organizations/org-slug/events/',
  797. method: 'GET',
  798. statusCode: 200,
  799. body: {
  800. meta: {
  801. fields: {'p99(measurements.lcp)': 'duration'},
  802. isMetricsData: false,
  803. },
  804. data: [{'p99(measurements.lcp)': 10}],
  805. },
  806. });
  807. const defaultWidgetQuery = {
  808. name: '',
  809. fields: ['p99(measurements.lcp)'],
  810. columns: [],
  811. aggregates: ['p99(measurements.lcp)'],
  812. conditions: 'user:test.user@sentry.io',
  813. orderby: '',
  814. };
  815. const defaultTableColumns = ['p99(measurements.lcp)'];
  816. renderTestComponent({
  817. query: {
  818. source: DashboardWidgetSource.DISCOVERV2,
  819. defaultWidgetQuery: urlEncode(defaultWidgetQuery),
  820. displayType: DisplayType.TABLE,
  821. defaultTableColumns,
  822. },
  823. orgFeatures: [
  824. ...defaultOrgFeatures,
  825. 'dashboards-mep',
  826. 'dynamic-sampling',
  827. 'mep-rollout-flag',
  828. ],
  829. });
  830. await waitFor(() => {
  831. expect(measurementsMetaMock).toHaveBeenCalled();
  832. });
  833. await waitFor(() => {
  834. expect(eventsMock).toHaveBeenCalled();
  835. });
  836. screen.getByText('Your selection is only applicable to', {exact: false});
  837. });
  838. it('does not raise an alert banner if widget result is not metrics data but widget contains error fields', async function () {
  839. eventsMock = MockApiClient.addMockResponse({
  840. url: '/organizations/org-slug/events/',
  841. method: 'GET',
  842. statusCode: 200,
  843. body: {
  844. meta: {
  845. fields: {'p99(measurements.lcp)': 'duration'},
  846. isMetricsData: false,
  847. },
  848. data: [{'p99(measurements.lcp)': 10}],
  849. },
  850. });
  851. const defaultWidgetQuery = {
  852. name: '',
  853. fields: ['p99(measurements.lcp)'],
  854. columns: ['error.handled'],
  855. aggregates: ['p99(measurements.lcp)'],
  856. conditions: 'user:test.user@sentry.io',
  857. orderby: '',
  858. };
  859. const defaultTableColumns = ['p99(measurements.lcp)'];
  860. renderTestComponent({
  861. query: {
  862. source: DashboardWidgetSource.DISCOVERV2,
  863. defaultWidgetQuery: urlEncode(defaultWidgetQuery),
  864. displayType: DisplayType.TABLE,
  865. defaultTableColumns,
  866. },
  867. orgFeatures: [...defaultOrgFeatures, 'dashboards-mep'],
  868. });
  869. await waitFor(() => {
  870. expect(measurementsMetaMock).toHaveBeenCalled();
  871. });
  872. await waitFor(() => {
  873. expect(eventsMock).toHaveBeenCalled();
  874. });
  875. expect(
  876. screen.queryByText('Your selection is only applicable to', {exact: false})
  877. ).not.toBeInTheDocument();
  878. });
  879. it('only displays custom measurements in supported functions', async function () {
  880. measurementsMetaMock = MockApiClient.addMockResponse({
  881. url: '/organizations/org-slug/measurements-meta/',
  882. method: 'GET',
  883. body: {
  884. 'measurements.custom.measurement': {functions: ['p99']},
  885. 'measurements.another.custom.measurement': {functions: ['p95']},
  886. },
  887. });
  888. renderTestComponent({
  889. query: {source: DashboardWidgetSource.DISCOVERV2},
  890. dashboard: testDashboard,
  891. orgFeatures: [...defaultOrgFeatures],
  892. });
  893. expect(await screen.findByText('Custom Widget')).toBeInTheDocument();
  894. await selectEvent.select(screen.getAllByText('count()')[1], ['p99(…)']);
  895. await userEvent.click(screen.getByText('transaction.duration'));
  896. screen.getByText('measurements.custom.measurement');
  897. expect(
  898. screen.queryByText('measurements.another.custom.measurement')
  899. ).not.toBeInTheDocument();
  900. await selectEvent.select(screen.getAllByText('p99(…)')[0], ['p95(…)']);
  901. await userEvent.click(screen.getByText('transaction.duration'));
  902. screen.getByText('measurements.another.custom.measurement');
  903. expect(
  904. screen.queryByText('measurements.custom.measurement')
  905. ).not.toBeInTheDocument();
  906. });
  907. it('renders custom performance metric using duration units from events meta', async function () {
  908. eventsMock = MockApiClient.addMockResponse({
  909. url: '/organizations/org-slug/events/',
  910. method: 'GET',
  911. statusCode: 200,
  912. body: {
  913. meta: {
  914. fields: {'p99(measurements.custom.measurement)': 'duration'},
  915. isMetricsData: true,
  916. units: {'p99(measurements.custom.measurement)': 'hour'},
  917. },
  918. data: [{'p99(measurements.custom.measurement)': 12}],
  919. },
  920. });
  921. renderTestComponent({
  922. query: {source: DashboardWidgetSource.DISCOVERV2},
  923. dashboard: {
  924. ...testDashboard,
  925. widgets: [
  926. {
  927. title: 'Custom Measurement Widget',
  928. interval: '1d',
  929. id: '1',
  930. widgetType: WidgetType.DISCOVER,
  931. displayType: DisplayType.TABLE,
  932. queries: [
  933. {
  934. conditions: '',
  935. name: '',
  936. fields: ['p99(measurements.custom.measurement)'],
  937. columns: [],
  938. aggregates: ['p99(measurements.custom.measurement)'],
  939. orderby: '-p99(measurements.custom.measurement)',
  940. },
  941. ],
  942. },
  943. ],
  944. },
  945. params: {
  946. widgetIndex: '0',
  947. },
  948. orgFeatures: [...defaultOrgFeatures],
  949. });
  950. await screen.findByText('12.00hr');
  951. });
  952. it('renders custom performance metric using size units from events meta', async function () {
  953. eventsMock = MockApiClient.addMockResponse({
  954. url: '/organizations/org-slug/events/',
  955. method: 'GET',
  956. statusCode: 200,
  957. body: {
  958. meta: {
  959. fields: {'p99(measurements.custom.measurement)': 'size'},
  960. isMetricsData: true,
  961. units: {'p99(measurements.custom.measurement)': 'kibibyte'},
  962. },
  963. data: [{'p99(measurements.custom.measurement)': 12}],
  964. },
  965. });
  966. renderTestComponent({
  967. query: {source: DashboardWidgetSource.DISCOVERV2},
  968. dashboard: {
  969. ...testDashboard,
  970. widgets: [
  971. {
  972. title: 'Custom Measurement Widget',
  973. interval: '1d',
  974. id: '1',
  975. widgetType: WidgetType.DISCOVER,
  976. displayType: DisplayType.TABLE,
  977. queries: [
  978. {
  979. conditions: '',
  980. name: '',
  981. fields: ['p99(measurements.custom.measurement)'],
  982. columns: [],
  983. aggregates: ['p99(measurements.custom.measurement)'],
  984. orderby: '-p99(measurements.custom.measurement)',
  985. },
  986. ],
  987. },
  988. ],
  989. },
  990. params: {
  991. widgetIndex: '0',
  992. },
  993. orgFeatures: [...defaultOrgFeatures],
  994. });
  995. await screen.findByText('12.0 KiB');
  996. });
  997. it('renders custom performance metric using abyte format size units from events meta', async function () {
  998. eventsMock = MockApiClient.addMockResponse({
  999. url: '/organizations/org-slug/events/',
  1000. method: 'GET',
  1001. statusCode: 200,
  1002. body: {
  1003. meta: {
  1004. fields: {'p99(measurements.custom.measurement)': 'size'},
  1005. isMetricsData: true,
  1006. units: {'p99(measurements.custom.measurement)': 'kilobyte'},
  1007. },
  1008. data: [{'p99(measurements.custom.measurement)': 12000}],
  1009. },
  1010. });
  1011. renderTestComponent({
  1012. query: {source: DashboardWidgetSource.DISCOVERV2},
  1013. dashboard: {
  1014. ...testDashboard,
  1015. widgets: [
  1016. {
  1017. title: 'Custom Measurement Widget',
  1018. interval: '1d',
  1019. id: '1',
  1020. widgetType: WidgetType.DISCOVER,
  1021. displayType: DisplayType.TABLE,
  1022. queries: [
  1023. {
  1024. conditions: '',
  1025. name: '',
  1026. fields: ['p99(measurements.custom.measurement)'],
  1027. columns: [],
  1028. aggregates: ['p99(measurements.custom.measurement)'],
  1029. orderby: '-p99(measurements.custom.measurement)',
  1030. },
  1031. ],
  1032. },
  1033. ],
  1034. },
  1035. params: {
  1036. widgetIndex: '0',
  1037. },
  1038. orgFeatures: [...defaultOrgFeatures],
  1039. });
  1040. await screen.findByText('12 MB');
  1041. });
  1042. it('displays saved custom performance metric in column select', async function () {
  1043. renderTestComponent({
  1044. query: {source: DashboardWidgetSource.DISCOVERV2},
  1045. dashboard: {
  1046. ...testDashboard,
  1047. widgets: [
  1048. {
  1049. title: 'Custom Measurement Widget',
  1050. interval: '1d',
  1051. id: '1',
  1052. widgetType: WidgetType.DISCOVER,
  1053. displayType: DisplayType.TABLE,
  1054. queries: [
  1055. {
  1056. conditions: '',
  1057. name: '',
  1058. fields: ['p99(measurements.custom.measurement)'],
  1059. columns: [],
  1060. aggregates: ['p99(measurements.custom.measurement)'],
  1061. orderby: '-p99(measurements.custom.measurement)',
  1062. },
  1063. ],
  1064. },
  1065. ],
  1066. },
  1067. params: {
  1068. widgetIndex: '0',
  1069. },
  1070. orgFeatures: [...defaultOrgFeatures],
  1071. });
  1072. await screen.findByText('measurements.custom.measurement');
  1073. });
  1074. it('displays custom performance metric in column select dropdown', async function () {
  1075. measurementsMetaMock = MockApiClient.addMockResponse({
  1076. url: '/organizations/org-slug/measurements-meta/',
  1077. method: 'GET',
  1078. body: {'measurements.custom.measurement': {functions: ['p99']}},
  1079. });
  1080. renderTestComponent({
  1081. query: {source: DashboardWidgetSource.DISCOVERV2},
  1082. dashboard: {
  1083. ...testDashboard,
  1084. widgets: [
  1085. {
  1086. title: 'Custom Measurement Widget',
  1087. interval: '1d',
  1088. id: '1',
  1089. widgetType: WidgetType.DISCOVER,
  1090. displayType: DisplayType.TABLE,
  1091. queries: [
  1092. {
  1093. conditions: '',
  1094. name: '',
  1095. fields: ['transaction', 'count()'],
  1096. columns: ['transaction'],
  1097. aggregates: ['count()'],
  1098. orderby: '-count()',
  1099. },
  1100. ],
  1101. },
  1102. ],
  1103. },
  1104. params: {
  1105. widgetIndex: '0',
  1106. },
  1107. orgFeatures: [...defaultOrgFeatures],
  1108. });
  1109. await screen.findByText('transaction');
  1110. await userEvent.click(screen.getAllByText('count()')[1]);
  1111. expect(
  1112. await screen.findByText('measurements.custom.measurement')
  1113. ).toBeInTheDocument();
  1114. });
  1115. it('does not default to sorting by transaction when columns change', async function () {
  1116. renderTestComponent({
  1117. query: {source: DashboardWidgetSource.DISCOVERV2},
  1118. dashboard: {
  1119. ...testDashboard,
  1120. widgets: [
  1121. {
  1122. title: 'Custom Measurement Widget',
  1123. interval: '1d',
  1124. id: '1',
  1125. widgetType: WidgetType.DISCOVER,
  1126. displayType: DisplayType.TABLE,
  1127. queries: [
  1128. {
  1129. conditions: '',
  1130. name: '',
  1131. fields: [
  1132. 'p99(measurements.custom.measurement)',
  1133. 'transaction',
  1134. 'count()',
  1135. ],
  1136. columns: ['transaction'],
  1137. aggregates: ['p99(measurements.custom.measurement)', 'count()'],
  1138. orderby: '-p99(measurements.custom.measurement)',
  1139. },
  1140. ],
  1141. },
  1142. ],
  1143. },
  1144. params: {
  1145. widgetIndex: '0',
  1146. },
  1147. orgFeatures: [...defaultOrgFeatures],
  1148. });
  1149. expect(
  1150. await screen.findByText('p99(measurements.custom.measurement)')
  1151. ).toBeInTheDocument();
  1152. // Delete p99(measurements.custom.measurement) column
  1153. await userEvent.click(screen.getAllByLabelText('Remove column')[0]);
  1154. expect(
  1155. screen.queryByText('p99(measurements.custom.measurement)')
  1156. ).not.toBeInTheDocument();
  1157. expect(
  1158. within(screen.getByTestId('sort-by-step')).queryByText('transaction')
  1159. ).not.toBeInTheDocument();
  1160. expect(
  1161. within(screen.getByTestId('sort-by-step')).getByText('count()')
  1162. ).toBeInTheDocument();
  1163. });
  1164. });
  1165. });
  1166. describe('Errors dataset', function () {
  1167. it('only shows the correct aggregates for timeseries charts', async function () {
  1168. renderTestComponent({
  1169. dashboard: {
  1170. ...testDashboard,
  1171. widgets: [
  1172. {
  1173. title: 'Errors Widget',
  1174. interval: '1d',
  1175. id: '1',
  1176. widgetType: WidgetType.ERRORS,
  1177. displayType: DisplayType.LINE,
  1178. queries: [
  1179. {
  1180. conditions: '',
  1181. name: '',
  1182. fields: ['count()'],
  1183. columns: [],
  1184. aggregates: ['count()'],
  1185. orderby: '-count()',
  1186. },
  1187. ],
  1188. },
  1189. ],
  1190. },
  1191. params: {
  1192. widgetIndex: '0',
  1193. },
  1194. orgFeatures: [...defaultOrgFeatures, 'performance-discover-dataset-selector'],
  1195. });
  1196. // Open the y-axis options dropdown
  1197. const yAxisStep = screen
  1198. .getByRole('heading', {name: /choose what to plot in the y-axis/i})
  1199. .closest('li');
  1200. await userEvent.click(within(yAxisStep!).getByText('count()'));
  1201. // Verify the error aggregates are present
  1202. expect(screen.getAllByRole('menuitemradio')).toHaveLength(
  1203. ERRORS_AGGREGATION_FUNCTIONS.length
  1204. );
  1205. ERRORS_AGGREGATION_FUNCTIONS.forEach(aggregation => {
  1206. expect(
  1207. screen.getByRole('menuitemradio', {name: new RegExp(`${aggregation}\\(…?\\)`)})
  1208. ).toBeInTheDocument();
  1209. });
  1210. });
  1211. it('only shows the correct aggregate params for timeseries charts', async function () {
  1212. MockApiClient.addMockResponse({
  1213. url: '/organizations/org-slug/tags/',
  1214. method: 'GET',
  1215. body: [],
  1216. });
  1217. renderTestComponent({
  1218. dashboard: {
  1219. ...testDashboard,
  1220. widgets: [
  1221. {
  1222. title: 'Errors Widget',
  1223. interval: '1d',
  1224. id: '1',
  1225. widgetType: WidgetType.ERRORS,
  1226. displayType: DisplayType.LINE,
  1227. queries: [
  1228. {
  1229. conditions: '',
  1230. name: '',
  1231. fields: ['count_unique(user)'],
  1232. columns: [],
  1233. aggregates: ['count_unique(user)'],
  1234. orderby: '-count_unique(user)',
  1235. },
  1236. ],
  1237. },
  1238. ],
  1239. },
  1240. params: {
  1241. widgetIndex: '0',
  1242. },
  1243. orgFeatures: [...defaultOrgFeatures, 'performance-discover-dataset-selector'],
  1244. });
  1245. expect(await screen.findByText('Select group')).toBeInTheDocument();
  1246. // Open the aggregate parameter dropdown
  1247. const yAxisStep = screen
  1248. .getByRole('heading', {name: /choose what to plot in the y-axis/i})
  1249. .closest('li');
  1250. await userEvent.click(within(yAxisStep!).getByText('user'));
  1251. // Verify the error aggregate params are present
  1252. expect(screen.getAllByTestId('menu-list-item-label')).toHaveLength(
  1253. ERROR_FIELDS.length
  1254. );
  1255. ERROR_FIELDS.forEach(field => {
  1256. expect(screen.getByRole('menuitemradio', {name: field})).toBeInTheDocument();
  1257. });
  1258. });
  1259. });
  1260. });