widgetBuilderDataset.spec.tsx 45 KB

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