widgetBuilderDataset.spec.tsx 44 KB

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