widgetBuilderDataset.spec.tsx 45 KB

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