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. ];
  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. widgetType: 'discover',
  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. });