widgetBuilderDataset.spec.tsx 44 KB

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