widgetBuilder.spec.tsx 64 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091
  1. import selectEvent from 'react-select-event';
  2. import {urlEncode} from '@sentry/utils';
  3. import {initializeOrg} from 'sentry-test/initializeOrg';
  4. import {mountGlobalModal} from 'sentry-test/modal';
  5. import {act, render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  6. import * as indicators from 'sentry/actionCreators/indicator';
  7. import * as modals from 'sentry/actionCreators/modal';
  8. import TagStore from 'sentry/stores/tagStore';
  9. import {TOP_N} from 'sentry/utils/discover/types';
  10. import {
  11. DashboardDetails,
  12. DashboardWidgetSource,
  13. DisplayType,
  14. Widget,
  15. WidgetType,
  16. } from 'sentry/views/dashboardsV2/types';
  17. import WidgetBuilder, {WidgetBuilderProps} from 'sentry/views/dashboardsV2/widgetBuilder';
  18. const defaultOrgFeatures = [
  19. 'performance-view',
  20. 'new-widget-builder-experience-design',
  21. 'dashboards-edit',
  22. 'global-views',
  23. 'dashboards-mep',
  24. ];
  25. // Mocking worldMapChart to avoid act warnings
  26. jest.mock('sentry/components/charts/worldMapChart');
  27. function mockDashboard(dashboard: Partial<DashboardDetails>): DashboardDetails {
  28. return {
  29. id: '1',
  30. title: 'Dashboard',
  31. createdBy: undefined,
  32. dateCreated: '2020-01-01T00:00:00.000Z',
  33. widgets: [],
  34. projects: [],
  35. filters: {},
  36. ...dashboard,
  37. };
  38. }
  39. function renderTestComponent({
  40. dashboard,
  41. query,
  42. orgFeatures,
  43. onSave,
  44. params,
  45. }: {
  46. dashboard?: WidgetBuilderProps['dashboard'];
  47. onSave?: WidgetBuilderProps['onSave'];
  48. orgFeatures?: string[];
  49. params?: Partial<WidgetBuilderProps['params']>;
  50. query?: Record<string, any>;
  51. } = {}) {
  52. const {organization, router, routerContext} = initializeOrg({
  53. ...initializeOrg(),
  54. organization: {
  55. features: orgFeatures ?? defaultOrgFeatures,
  56. },
  57. router: {
  58. location: {
  59. query: {
  60. source: DashboardWidgetSource.DASHBOARDS,
  61. ...query,
  62. },
  63. },
  64. },
  65. });
  66. render(
  67. <WidgetBuilder
  68. route={{}}
  69. router={router}
  70. routes={router.routes}
  71. routeParams={router.params}
  72. location={router.location}
  73. dashboard={{
  74. id: 'new',
  75. title: 'Dashboard',
  76. createdBy: undefined,
  77. dateCreated: '2020-01-01T00:00:00.000Z',
  78. widgets: [],
  79. projects: [],
  80. filters: {},
  81. ...dashboard,
  82. }}
  83. onSave={onSave ?? jest.fn()}
  84. params={{
  85. orgId: organization.slug,
  86. dashboardId: dashboard?.id ?? 'new',
  87. ...params,
  88. }}
  89. />,
  90. {
  91. context: routerContext,
  92. organization,
  93. }
  94. );
  95. return {router};
  96. }
  97. /**
  98. * This test suite contains tests that test the generic interactions
  99. * between most components in the WidgetBuilder. Tests for the
  100. * SortBy step can be found in (and should be added to)
  101. * ./widgetBuilderSortBy.spec.tsx and tests for specific dataset
  102. * behaviour can be found (and should be added to) ./widgetBuilderDataset.spec.tsx.
  103. * The test files are broken up to allow better parallelization
  104. * in CI (we currently parallelize files).
  105. */
  106. describe('WidgetBuilder', function () {
  107. const untitledDashboard: DashboardDetails = {
  108. id: '1',
  109. title: 'Untitled Dashboard',
  110. createdBy: undefined,
  111. dateCreated: '2020-01-01T00:00:00.000Z',
  112. widgets: [],
  113. projects: [],
  114. filters: {},
  115. };
  116. const testDashboard: DashboardDetails = {
  117. id: '2',
  118. title: 'Test Dashboard',
  119. createdBy: undefined,
  120. dateCreated: '2020-01-01T00:00:00.000Z',
  121. widgets: [],
  122. projects: [],
  123. filters: {},
  124. };
  125. let eventsStatsMock: jest.Mock | undefined;
  126. let eventsv2Mock: jest.Mock | undefined;
  127. let eventsMock: jest.Mock | undefined;
  128. let tagsMock: jest.Mock | undefined;
  129. beforeEach(function () {
  130. MockApiClient.addMockResponse({
  131. url: '/organizations/org-slug/dashboards/',
  132. body: [
  133. {...untitledDashboard, widgetDisplay: [DisplayType.TABLE]},
  134. {...testDashboard, widgetDisplay: [DisplayType.AREA]},
  135. ],
  136. });
  137. MockApiClient.addMockResponse({
  138. url: '/organizations/org-slug/dashboards/widgets/',
  139. method: 'POST',
  140. statusCode: 200,
  141. body: [],
  142. });
  143. eventsv2Mock = MockApiClient.addMockResponse({
  144. url: '/organizations/org-slug/eventsv2/',
  145. method: 'GET',
  146. statusCode: 200,
  147. body: {
  148. meta: {},
  149. data: [],
  150. },
  151. });
  152. eventsMock = MockApiClient.addMockResponse({
  153. url: '/organizations/org-slug/events/',
  154. method: 'GET',
  155. statusCode: 200,
  156. body: {
  157. meta: {fields: {}},
  158. data: [],
  159. },
  160. });
  161. MockApiClient.addMockResponse({
  162. url: '/organizations/org-slug/projects/',
  163. method: 'GET',
  164. body: [],
  165. });
  166. MockApiClient.addMockResponse({
  167. url: '/organizations/org-slug/recent-searches/',
  168. method: 'GET',
  169. body: [],
  170. });
  171. MockApiClient.addMockResponse({
  172. url: '/organizations/org-slug/recent-searches/',
  173. method: 'POST',
  174. body: [],
  175. });
  176. MockApiClient.addMockResponse({
  177. url: '/organizations/org-slug/issues/',
  178. method: 'GET',
  179. body: [],
  180. });
  181. eventsStatsMock = MockApiClient.addMockResponse({
  182. url: '/organizations/org-slug/events-stats/',
  183. body: [],
  184. });
  185. MockApiClient.addMockResponse({
  186. url: '/organizations/org-slug/tags/event.type/values/',
  187. body: [{count: 2, name: 'Nvidia 1080ti'}],
  188. });
  189. MockApiClient.addMockResponse({
  190. url: '/organizations/org-slug/events-geo/',
  191. body: {data: [], meta: {}},
  192. });
  193. MockApiClient.addMockResponse({
  194. url: '/organizations/org-slug/users/',
  195. body: [],
  196. });
  197. MockApiClient.addMockResponse({
  198. method: 'GET',
  199. url: '/organizations/org-slug/sessions/',
  200. body: TestStubs.SessionsField({
  201. field: `sum(session)`,
  202. }),
  203. });
  204. MockApiClient.addMockResponse({
  205. method: 'GET',
  206. url: '/organizations/org-slug/metrics/data/',
  207. body: TestStubs.MetricsField({
  208. field: 'sum(sentry.sessions.session)',
  209. }),
  210. });
  211. tagsMock = MockApiClient.addMockResponse({
  212. url: '/organizations/org-slug/tags/',
  213. method: 'GET',
  214. body: TestStubs.Tags(),
  215. });
  216. MockApiClient.addMockResponse({
  217. url: '/organizations/org-slug/measurements-meta/',
  218. method: 'GET',
  219. body: {},
  220. });
  221. MockApiClient.addMockResponse({
  222. url: '/organizations/org-slug/tags/is/values/',
  223. method: 'GET',
  224. body: [],
  225. });
  226. TagStore.reset();
  227. });
  228. afterEach(function () {
  229. MockApiClient.clearMockResponses();
  230. jest.clearAllMocks();
  231. jest.useRealTimers();
  232. });
  233. describe('with eventsv2', function () {
  234. it('no feature access', function () {
  235. renderTestComponent({orgFeatures: []});
  236. expect(
  237. screen.getByText("You don't have access to this feature")
  238. ).toBeInTheDocument();
  239. });
  240. it('widget not found', function () {
  241. const widget: Widget = {
  242. displayType: DisplayType.AREA,
  243. interval: '1d',
  244. queries: [
  245. {
  246. name: 'Known Users',
  247. fields: [],
  248. columns: [],
  249. aggregates: [],
  250. conditions: '',
  251. orderby: '-time',
  252. },
  253. {
  254. name: 'Anonymous Users',
  255. fields: [],
  256. columns: [],
  257. aggregates: [],
  258. conditions: '',
  259. orderby: '-time',
  260. },
  261. ],
  262. title: 'Transactions',
  263. id: '1',
  264. };
  265. const dashboard = mockDashboard({widgets: [widget]});
  266. renderTestComponent({
  267. dashboard,
  268. orgFeatures: ['new-widget-builder-experience-design', 'dashboards-edit'],
  269. params: {
  270. widgetIndex: '2', // Out of bounds, only one widget
  271. },
  272. });
  273. expect(
  274. screen.getByText('The widget you want to edit was not found.')
  275. ).toBeInTheDocument();
  276. });
  277. it('renders a widget not found message if the widget index url is not an integer', function () {
  278. const widget: Widget = {
  279. displayType: DisplayType.AREA,
  280. interval: '1d',
  281. queries: [
  282. {
  283. name: 'Known Users',
  284. fields: [],
  285. columns: [],
  286. aggregates: [],
  287. conditions: '',
  288. orderby: '-time',
  289. },
  290. ],
  291. title: 'Transactions',
  292. id: '1',
  293. };
  294. const dashboard = mockDashboard({widgets: [widget]});
  295. renderTestComponent({
  296. dashboard,
  297. orgFeatures: ['new-widget-builder-experience-design', 'dashboards-edit'],
  298. params: {
  299. widgetIndex: '0.5', // Invalid index
  300. },
  301. });
  302. expect(
  303. screen.getByText('The widget you want to edit was not found.')
  304. ).toBeInTheDocument();
  305. });
  306. it('renders', async function () {
  307. renderTestComponent();
  308. // Header - Breadcrumbs
  309. expect(await screen.findByRole('link', {name: 'Dashboards'})).toHaveAttribute(
  310. 'href',
  311. '/organizations/org-slug/dashboards/'
  312. );
  313. expect(screen.getByRole('link', {name: 'Dashboard'})).toHaveAttribute(
  314. 'href',
  315. '/organizations/org-slug/dashboards/new/'
  316. );
  317. expect(screen.getByText('Widget Builder')).toBeInTheDocument();
  318. // Header - Widget Title
  319. expect(screen.getByRole('heading', {name: 'Custom Widget'})).toBeInTheDocument();
  320. // Footer - Actions
  321. expect(screen.getByLabelText('Cancel')).toBeInTheDocument();
  322. expect(screen.getByLabelText('Add Widget')).toBeInTheDocument();
  323. // Content - Step 1
  324. expect(
  325. screen.getByRole('heading', {name: 'Choose your dataset'})
  326. ).toBeInTheDocument();
  327. expect(screen.getByLabelText('Select Errors and Transactions')).toBeChecked();
  328. // Content - Step 2
  329. expect(
  330. screen.getByRole('heading', {name: 'Choose your visualization'})
  331. ).toBeInTheDocument();
  332. // Content - Step 3
  333. expect(
  334. screen.getByRole('heading', {name: 'Choose your columns'})
  335. ).toBeInTheDocument();
  336. // Content - Step 4
  337. expect(
  338. screen.getByRole('heading', {name: 'Filter your results'})
  339. ).toBeInTheDocument();
  340. // Content - Step 5
  341. expect(screen.getByRole('heading', {name: 'Sort by a column'})).toBeInTheDocument();
  342. });
  343. it('has links back to the new dashboard if creating', async function () {
  344. // Dashboard has undefined dashboardId when creating from a new dashboard
  345. // because of route setup
  346. renderTestComponent({params: {dashboardId: undefined}});
  347. expect(await screen.findByRole('link', {name: 'Dashboard'})).toHaveAttribute(
  348. 'href',
  349. '/organizations/org-slug/dashboards/new/'
  350. );
  351. expect(screen.getByLabelText('Cancel')).toHaveAttribute(
  352. 'href',
  353. '/organizations/org-slug/dashboards/new/'
  354. );
  355. });
  356. it('renders new design', async function () {
  357. renderTestComponent({
  358. orgFeatures: [...defaultOrgFeatures, 'new-widget-builder-experience-design'],
  359. });
  360. // Switch to line chart for time series
  361. userEvent.click(await screen.findByText('Table'));
  362. userEvent.click(screen.getByText('Line Chart'));
  363. // Header - Breadcrumbs
  364. expect(await screen.findByRole('link', {name: 'Dashboards'})).toHaveAttribute(
  365. 'href',
  366. '/organizations/org-slug/dashboards/'
  367. );
  368. expect(screen.getByRole('link', {name: 'Dashboard'})).toHaveAttribute(
  369. 'href',
  370. '/organizations/org-slug/dashboards/new/'
  371. );
  372. expect(screen.getByText('Widget Builder')).toBeInTheDocument();
  373. // Header - Widget Title
  374. expect(screen.getByRole('heading', {name: 'Custom Widget'})).toBeInTheDocument();
  375. // Footer - Actions
  376. expect(screen.getByLabelText('Cancel')).toBeInTheDocument();
  377. expect(screen.getByLabelText('Add Widget')).toBeInTheDocument();
  378. // Content - Step 1
  379. expect(
  380. screen.getByRole('heading', {name: 'Choose your dataset'})
  381. ).toBeInTheDocument();
  382. expect(screen.getByLabelText('Select Errors and Transactions')).toBeChecked();
  383. // Content - Step 2
  384. expect(
  385. screen.getByRole('heading', {name: 'Choose your visualization'})
  386. ).toBeInTheDocument();
  387. // Content - Step 3
  388. expect(
  389. screen.getByRole('heading', {name: 'Choose what to plot in the y-axis'})
  390. ).toBeInTheDocument();
  391. // Content - Step 4
  392. expect(
  393. screen.getByRole('heading', {name: 'Filter your results'})
  394. ).toBeInTheDocument();
  395. // Content - Step 5
  396. expect(
  397. screen.getByRole('heading', {name: 'Group your results'})
  398. ).toBeInTheDocument();
  399. });
  400. it('can update the title', async function () {
  401. renderTestComponent({
  402. query: {source: DashboardWidgetSource.DISCOVERV2},
  403. });
  404. const customWidgetLabels = await screen.findAllByText('Custom Widget');
  405. // EditableText and chart title
  406. expect(customWidgetLabels).toHaveLength(2);
  407. userEvent.click(customWidgetLabels[0]);
  408. userEvent.clear(screen.getByRole('textbox', {name: 'Widget title'}));
  409. userEvent.paste(
  410. screen.getByRole('textbox', {name: 'Widget title'}),
  411. 'Unique Users'
  412. );
  413. userEvent.keyboard('{enter}');
  414. expect(screen.queryByText('Custom Widget')).not.toBeInTheDocument();
  415. expect(screen.getAllByText('Unique Users')).toHaveLength(2);
  416. });
  417. it('can add query conditions', async function () {
  418. const {router} = renderTestComponent({
  419. query: {source: DashboardWidgetSource.DISCOVERV2},
  420. dashboard: testDashboard,
  421. });
  422. userEvent.type(
  423. await screen.findByRole('textbox', {name: 'Search events'}),
  424. 'color:blue{enter}'
  425. );
  426. userEvent.click(screen.getByText('Add Widget'));
  427. await waitFor(() => {
  428. expect(router.push).toHaveBeenCalledWith(
  429. expect.objectContaining({
  430. pathname: '/organizations/org-slug/dashboard/2/',
  431. query: {
  432. displayType: 'table',
  433. interval: '5m',
  434. title: 'Custom Widget',
  435. queryNames: [''],
  436. queryConditions: ['color:blue'],
  437. queryFields: ['count()'],
  438. queryOrderby: '-count()',
  439. start: null,
  440. end: null,
  441. statsPeriod: '24h',
  442. utc: null,
  443. project: [],
  444. environment: [],
  445. },
  446. })
  447. );
  448. });
  449. });
  450. it('can choose a field', async function () {
  451. const {router} = renderTestComponent({
  452. query: {source: DashboardWidgetSource.DISCOVERV2},
  453. dashboard: testDashboard,
  454. });
  455. expect(await screen.findAllByText('Custom Widget')).toHaveLength(2);
  456. // No delete button as there is only one query.
  457. expect(screen.queryByLabelText('Remove query')).not.toBeInTheDocument();
  458. // 1 in the table header, 1 in the column selector, 1 in the sort field
  459. const countFields = screen.getAllByText('count()');
  460. expect(countFields).toHaveLength(3);
  461. await selectEvent.select(countFields[1], ['last_seen()']);
  462. userEvent.click(screen.getByText('Add Widget'));
  463. await waitFor(() => {
  464. expect(router.push).toHaveBeenCalledWith(
  465. expect.objectContaining({
  466. pathname: '/organizations/org-slug/dashboard/2/',
  467. query: {
  468. displayType: 'table',
  469. interval: '5m',
  470. title: 'Custom Widget',
  471. queryNames: [''],
  472. queryConditions: [''],
  473. queryFields: ['last_seen()'],
  474. queryOrderby: '-last_seen()',
  475. start: null,
  476. end: null,
  477. statsPeriod: '24h',
  478. utc: null,
  479. project: [],
  480. environment: [],
  481. },
  482. })
  483. );
  484. });
  485. });
  486. it('can add additional fields', async function () {
  487. const handleSave = jest.fn();
  488. renderTestComponent({onSave: handleSave});
  489. userEvent.click(await screen.findByText('Table'));
  490. // Select line chart display
  491. userEvent.click(screen.getByText('Line Chart'));
  492. // Click the add overlay button
  493. userEvent.click(screen.getByLabelText('Add Overlay'));
  494. await selectEvent.select(screen.getByText('(Required)'), ['count_unique(…)']);
  495. userEvent.click(screen.getByLabelText('Add Widget'));
  496. await waitFor(() => {
  497. expect(handleSave).toHaveBeenCalledWith([
  498. expect.objectContaining({
  499. title: 'Custom Widget',
  500. displayType: DisplayType.LINE,
  501. interval: '5m',
  502. widgetType: WidgetType.DISCOVER,
  503. queries: [
  504. {
  505. conditions: '',
  506. fields: ['count()', 'count_unique(user)'],
  507. aggregates: ['count()', 'count_unique(user)'],
  508. fieldAliases: [],
  509. columns: [],
  510. orderby: '',
  511. name: '',
  512. },
  513. ],
  514. }),
  515. ]);
  516. });
  517. expect(handleSave).toHaveBeenCalledTimes(1);
  518. });
  519. it('can add equation fields', async function () {
  520. const handleSave = jest.fn();
  521. renderTestComponent({onSave: handleSave});
  522. userEvent.click(await screen.findByText('Table'));
  523. // Select line chart display
  524. userEvent.click(screen.getByText('Line Chart'));
  525. // Click the add an equation button
  526. userEvent.click(screen.getByLabelText('Add an Equation'));
  527. expect(screen.getByPlaceholderText('Equation')).toBeInTheDocument();
  528. userEvent.paste(screen.getByPlaceholderText('Equation'), 'count() + 100');
  529. userEvent.click(screen.getByLabelText('Add Widget'));
  530. await waitFor(() => {
  531. expect(handleSave).toHaveBeenCalledWith([
  532. expect.objectContaining({
  533. title: 'Custom Widget',
  534. displayType: DisplayType.LINE,
  535. interval: '5m',
  536. widgetType: WidgetType.DISCOVER,
  537. queries: [
  538. {
  539. name: '',
  540. fields: ['count()', 'equation|count() + 100'],
  541. aggregates: ['count()', 'equation|count() + 100'],
  542. columns: [],
  543. fieldAliases: [],
  544. conditions: '',
  545. orderby: '',
  546. },
  547. ],
  548. }),
  549. ]);
  550. });
  551. expect(handleSave).toHaveBeenCalledTimes(1);
  552. });
  553. it('can respond to validation feedback', async function () {
  554. jest.spyOn(indicators, 'addErrorMessage');
  555. renderTestComponent();
  556. userEvent.click(await screen.findByText('Table'));
  557. const customWidgetLabels = await screen.findAllByText('Custom Widget');
  558. // EditableText and chart title
  559. expect(customWidgetLabels).toHaveLength(2);
  560. userEvent.click(customWidgetLabels[0]);
  561. userEvent.clear(screen.getByRole('textbox', {name: 'Widget title'}));
  562. userEvent.keyboard('{enter}');
  563. expect(indicators.addErrorMessage).toHaveBeenCalledWith('Widget title is required');
  564. });
  565. it('sets up widget data in edit correctly', async function () {
  566. const widget: Widget = {
  567. id: '1',
  568. title: 'Errors over time',
  569. interval: '5m',
  570. displayType: DisplayType.LINE,
  571. queries: [
  572. {
  573. name: 'errors',
  574. conditions: 'event.type:error',
  575. fields: ['count()', 'count_unique(id)'],
  576. aggregates: ['count()', 'count_unique(id)'],
  577. columns: [],
  578. orderby: '',
  579. },
  580. {
  581. name: 'csp',
  582. conditions: 'event.type:csp',
  583. fields: ['count()', 'count_unique(id)'],
  584. aggregates: ['count()', 'count_unique(id)'],
  585. columns: [],
  586. orderby: '',
  587. },
  588. ],
  589. };
  590. const dashboard = mockDashboard({widgets: [widget]});
  591. renderTestComponent({dashboard, params: {widgetIndex: '0'}});
  592. await screen.findByText('Line Chart');
  593. // Should be in edit 'mode'
  594. expect(await screen.findByText('Update Widget')).toBeInTheDocument();
  595. // Should set widget data up.
  596. expect(screen.getByText('Update Widget')).toBeInTheDocument();
  597. // Filters
  598. expect(
  599. screen.getAllByPlaceholderText('Search for events, users, tags, and more')
  600. ).toHaveLength(2);
  601. expect(screen.getByText('event.type:csp')).toBeInTheDocument();
  602. expect(screen.getByText('event.type:error')).toBeInTheDocument();
  603. // Y-axis
  604. expect(screen.getAllByRole('button', {name: 'Remove query'})).toHaveLength(2);
  605. expect(screen.getByText('count()')).toBeInTheDocument();
  606. expect(screen.getByText('count_unique(…)')).toBeInTheDocument();
  607. expect(screen.getByText('id')).toBeInTheDocument();
  608. // Expect events-stats endpoint to be called for each search conditions with
  609. // the same y-axis parameters
  610. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  611. 1,
  612. '/organizations/org-slug/events-stats/',
  613. expect.objectContaining({
  614. query: expect.objectContaining({
  615. query: 'event.type:error',
  616. yAxis: ['count()', 'count_unique(id)'],
  617. }),
  618. })
  619. );
  620. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  621. 2,
  622. '/organizations/org-slug/events-stats/',
  623. expect.objectContaining({
  624. query: expect.objectContaining({
  625. query: 'event.type:csp',
  626. yAxis: ['count()', 'count_unique(id)'],
  627. }),
  628. })
  629. );
  630. });
  631. it('can edit a widget', async function () {
  632. const widget: Widget = {
  633. id: '1',
  634. title: 'Errors over time',
  635. interval: '5m',
  636. displayType: DisplayType.LINE,
  637. queries: [
  638. {
  639. name: 'errors',
  640. conditions: 'event.type:error',
  641. fields: ['count()', 'count_unique(id)'],
  642. aggregates: ['count()', 'count_unique(id)'],
  643. columns: [],
  644. orderby: '',
  645. },
  646. {
  647. name: 'csp',
  648. conditions: 'event.type:csp',
  649. fields: ['count()', 'count_unique(id)'],
  650. aggregates: ['count()', 'count_unique(id)'],
  651. columns: [],
  652. orderby: '',
  653. },
  654. ],
  655. };
  656. const dashboard = mockDashboard({widgets: [widget]});
  657. const handleSave = jest.fn();
  658. renderTestComponent({onSave: handleSave, dashboard, params: {widgetIndex: '0'}});
  659. await screen.findByText('Line Chart');
  660. // Should be in edit 'mode'
  661. expect(await screen.findByText('Update Widget')).toBeInTheDocument();
  662. const customWidgetLabels = await screen.findAllByText(widget.title);
  663. // EditableText and chart title
  664. expect(customWidgetLabels).toHaveLength(2);
  665. userEvent.click(customWidgetLabels[0]);
  666. userEvent.clear(screen.getByRole('textbox', {name: 'Widget title'}));
  667. userEvent.paste(screen.getByRole('textbox', {name: 'Widget title'}), 'New Title');
  668. userEvent.click(screen.getByRole('button', {name: 'Update Widget'}));
  669. await waitFor(() => {
  670. expect(handleSave).toHaveBeenCalledWith([
  671. expect.objectContaining({
  672. ...widget,
  673. title: 'New Title',
  674. }),
  675. ]);
  676. });
  677. expect(handleSave).toHaveBeenCalledTimes(1);
  678. });
  679. it('renders column inputs for table widgets', async function () {
  680. const widget: Widget = {
  681. id: '0',
  682. title: 'sdk usage',
  683. interval: '5m',
  684. displayType: DisplayType.TABLE,
  685. queries: [
  686. {
  687. name: 'errors',
  688. conditions: 'event.type:error',
  689. fields: ['sdk.name', 'count()'],
  690. columns: ['sdk.name'],
  691. aggregates: ['count()'],
  692. orderby: '',
  693. },
  694. ],
  695. };
  696. const dashboard = mockDashboard({widgets: [widget]});
  697. renderTestComponent({dashboard, params: {widgetIndex: '0'}});
  698. // Should be in edit 'mode'
  699. expect(await screen.findByText('Update Widget')).toBeInTheDocument();
  700. // Should set widget data up.
  701. expect(screen.getByRole('heading', {name: widget.title})).toBeInTheDocument();
  702. expect(screen.getByText('Table')).toBeInTheDocument();
  703. expect(screen.getByLabelText('Search events')).toBeInTheDocument();
  704. // Should have an orderby select
  705. expect(screen.getByText('Sort by a column')).toBeInTheDocument();
  706. // Add a column, and choose a value,
  707. expect(screen.getByLabelText('Add a Column')).toBeInTheDocument();
  708. });
  709. it('can save table widgets', async function () {
  710. const widget: Widget = {
  711. id: '0',
  712. title: 'sdk usage',
  713. interval: '5m',
  714. displayType: DisplayType.TABLE,
  715. queries: [
  716. {
  717. name: 'errors',
  718. conditions: 'event.type:error',
  719. fields: ['sdk.name', 'count()'],
  720. columns: ['sdk.name'],
  721. aggregates: ['count()'],
  722. orderby: '-count()',
  723. },
  724. ],
  725. };
  726. const dashboard = mockDashboard({widgets: [widget]});
  727. const handleSave = jest.fn();
  728. renderTestComponent({dashboard, onSave: handleSave, params: {widgetIndex: '0'}});
  729. // Should be in edit 'mode'
  730. expect(await screen.findByText('Update Widget')).toBeInTheDocument();
  731. // Add a column, and choose a value,
  732. userEvent.click(screen.getByLabelText('Add a Column'));
  733. await selectEvent.select(screen.getByText('(Required)'), 'trace');
  734. // Save widget
  735. userEvent.click(screen.getByLabelText('Update Widget'));
  736. await waitFor(() => {
  737. expect(handleSave).toHaveBeenCalledWith([
  738. expect.objectContaining({
  739. id: '0',
  740. title: 'sdk usage',
  741. displayType: DisplayType.TABLE,
  742. interval: '5m',
  743. queries: [
  744. {
  745. name: 'errors',
  746. conditions: 'event.type:error',
  747. fields: ['sdk.name', 'count()', 'trace'],
  748. aggregates: ['count()'],
  749. columns: ['sdk.name', 'trace'],
  750. orderby: '-count()',
  751. fieldAliases: ['', '', ''],
  752. },
  753. ],
  754. widgetType: WidgetType.DISCOVER,
  755. }),
  756. ]);
  757. });
  758. expect(handleSave).toHaveBeenCalledTimes(1);
  759. });
  760. it('should properly query for table fields', async function () {
  761. const defaultWidgetQuery = {
  762. name: '',
  763. fields: ['title', 'count()'],
  764. columns: ['title'],
  765. aggregates: ['count()'],
  766. conditions: '',
  767. orderby: '',
  768. };
  769. const defaultTableColumns = ['title', 'count()', 'count_unique(user)', 'epm()'];
  770. renderTestComponent({
  771. query: {
  772. source: DashboardWidgetSource.DISCOVERV2,
  773. defaultWidgetQuery: urlEncode(defaultWidgetQuery),
  774. displayType: DisplayType.LINE,
  775. defaultTableColumns,
  776. },
  777. });
  778. expect(await screen.findByText('Line Chart')).toBeInTheDocument();
  779. userEvent.click(screen.getByText('Line Chart'));
  780. userEvent.click(screen.getByText('Table'));
  781. await waitFor(() => {
  782. expect(eventsv2Mock).toHaveBeenLastCalledWith(
  783. '/organizations/org-slug/eventsv2/',
  784. expect.objectContaining({
  785. query: expect.objectContaining({
  786. field: defaultTableColumns,
  787. }),
  788. })
  789. );
  790. });
  791. });
  792. it('should use defaultWidgetQuery Y-Axis and Conditions if given a defaultWidgetQuery', async function () {
  793. const defaultWidgetQuery = {
  794. name: '',
  795. fields: ['count()', 'failure_count()', 'count_unique(user)'],
  796. columns: [],
  797. aggregates: ['count()', 'failure_count()', 'count_unique(user)'],
  798. conditions: 'tag:value',
  799. orderby: '',
  800. };
  801. renderTestComponent({
  802. query: {
  803. source: DashboardWidgetSource.DISCOVERV2,
  804. defaultWidgetQuery: urlEncode(defaultWidgetQuery),
  805. },
  806. });
  807. expect(await screen.findByText('tag:value')).toBeInTheDocument();
  808. // Table display, column, and sort field
  809. expect(screen.getAllByText('count()')).toHaveLength(3);
  810. // Table display and column
  811. expect(screen.getAllByText('failure_count()')).toHaveLength(2);
  812. // Table display
  813. expect(screen.getByText('count_unique(user)')).toBeInTheDocument();
  814. // Column
  815. expect(screen.getByText('count_unique(…)')).toBeInTheDocument();
  816. // Column
  817. expect(screen.getByText('user')).toBeInTheDocument();
  818. });
  819. it('uses displayType if given a displayType', async function () {
  820. renderTestComponent({
  821. query: {
  822. displayType: DisplayType.BAR,
  823. },
  824. });
  825. expect(await screen.findByText('Bar Chart')).toBeInTheDocument();
  826. });
  827. it('deletes the widget when the modal is confirmed', async () => {
  828. const handleSave = jest.fn();
  829. const widget: Widget = {
  830. id: '1',
  831. title: 'Errors over time',
  832. interval: '5m',
  833. displayType: DisplayType.LINE,
  834. queries: [
  835. {
  836. name: 'errors',
  837. conditions: 'event.type:error',
  838. fields: ['count()', 'count_unique(id)'],
  839. aggregates: ['count()', 'count_unique(id)'],
  840. columns: [],
  841. orderby: '',
  842. },
  843. {
  844. name: 'csp',
  845. conditions: 'event.type:csp',
  846. fields: ['count()', 'count_unique(id)'],
  847. aggregates: ['count()', 'count_unique(id)'],
  848. columns: [],
  849. orderby: '',
  850. },
  851. ],
  852. };
  853. const dashboard = mockDashboard({widgets: [widget]});
  854. renderTestComponent({onSave: handleSave, dashboard, params: {widgetIndex: '0'}});
  855. userEvent.click(await screen.findByText('Delete'));
  856. await mountGlobalModal();
  857. userEvent.click(await screen.findByText('Confirm'));
  858. await waitFor(() => {
  859. // The only widget was deleted
  860. expect(handleSave).toHaveBeenCalledWith([]);
  861. });
  862. expect(handleSave).toHaveBeenCalledTimes(1);
  863. });
  864. it('persists the page filter period when updating a widget', async () => {
  865. const widget: Widget = {
  866. id: '1',
  867. title: 'Errors over time',
  868. interval: '5m',
  869. displayType: DisplayType.LINE,
  870. queries: [
  871. {
  872. name: 'errors',
  873. conditions: 'event.type:error',
  874. fields: ['count()', 'count_unique(id)'],
  875. aggregates: ['count()', 'count_unique(id)'],
  876. columns: [],
  877. orderby: '',
  878. },
  879. ],
  880. };
  881. const dashboard = mockDashboard({widgets: [widget]});
  882. const {router} = renderTestComponent({
  883. dashboard,
  884. params: {orgId: 'org-slug', widgetIndex: '0'},
  885. query: {statsPeriod: '90d'},
  886. });
  887. await screen.findByText('Update Widget');
  888. await screen.findByText('90D');
  889. expect(screen.getByTestId('page-filter-timerange-selector')).toBeEnabled();
  890. userEvent.click(screen.getByText('Update Widget'));
  891. await waitFor(() => {
  892. expect(router.push).toHaveBeenLastCalledWith(
  893. expect.objectContaining({
  894. pathname: '/organizations/org-slug/dashboard/1/',
  895. query: expect.objectContaining({
  896. statsPeriod: '90d',
  897. }),
  898. })
  899. );
  900. });
  901. });
  902. it('renders page filters in the filter step', async () => {
  903. const mockReleases = MockApiClient.addMockResponse({
  904. url: '/organizations/org-slug/releases/',
  905. body: [TestStubs.Release()],
  906. });
  907. renderTestComponent({
  908. params: {orgId: 'org-slug'},
  909. query: {statsPeriod: '90d'},
  910. orgFeatures: [...defaultOrgFeatures, 'dashboards-top-level-filter'],
  911. });
  912. await screen.findByText('90D');
  913. expect(screen.getByTestId('page-filter-timerange-selector')).toBeDisabled();
  914. expect(screen.getByTestId('page-filter-environment-selector')).toBeDisabled();
  915. expect(screen.getByTestId('page-filter-project-selector-loading')).toBeDisabled();
  916. await waitFor(() => {
  917. expect(mockReleases).toHaveBeenCalled();
  918. });
  919. expect(screen.getByRole('button', {name: /all releases/i})).toBeDisabled();
  920. });
  921. it('appends dashboard filters to widget builder fetch data request', async () => {
  922. MockApiClient.addMockResponse({
  923. url: '/organizations/org-slug/releases/',
  924. body: [TestStubs.Release()],
  925. });
  926. const mock = MockApiClient.addMockResponse({
  927. url: '/organizations/org-slug/eventsv2/',
  928. body: [],
  929. });
  930. renderTestComponent({
  931. dashboard: {
  932. id: 'new',
  933. title: 'Dashboard',
  934. createdBy: undefined,
  935. dateCreated: '2020-01-01T00:00:00.000Z',
  936. widgets: [],
  937. projects: [],
  938. filters: {release: ['abc@1.2.0']},
  939. },
  940. params: {orgId: 'org-slug'},
  941. query: {statsPeriod: '90d'},
  942. orgFeatures: [...defaultOrgFeatures, 'dashboards-top-level-filter'],
  943. });
  944. await waitFor(() => {
  945. expect(mock).toHaveBeenCalledWith(
  946. '/organizations/org-slug/eventsv2/',
  947. expect.objectContaining({
  948. query: expect.objectContaining({
  949. query: ' release:abc@1.2.0 ',
  950. }),
  951. })
  952. );
  953. });
  954. });
  955. it('does not error when query conditions field is blurred', async function () {
  956. jest.useFakeTimers();
  957. const widget: Widget = {
  958. id: '0',
  959. title: 'sdk usage',
  960. interval: '5m',
  961. displayType: DisplayType.BAR,
  962. queries: [
  963. {
  964. name: 'filled in',
  965. conditions: 'event.type:error',
  966. fields: ['count()', 'count_unique(id)'],
  967. aggregates: ['count()', 'count_unique(id)'],
  968. columns: [],
  969. orderby: '-count()',
  970. },
  971. ],
  972. };
  973. const dashboard = mockDashboard({widgets: [widget]});
  974. const handleSave = jest.fn();
  975. renderTestComponent({dashboard, onSave: handleSave, params: {widgetIndex: '0'}});
  976. userEvent.click(await screen.findByLabelText('Add Query'));
  977. // Triggering the onBlur of the new field should not error
  978. userEvent.click(
  979. screen.getAllByPlaceholderText('Search for events, users, tags, and more')[1]
  980. );
  981. userEvent.keyboard('{esc}');
  982. act(() => {
  983. // Run all timers because the handleBlur contains a setTimeout
  984. jest.runAllTimers();
  985. });
  986. });
  987. it('does not wipe column changes when filters are modified', async function () {
  988. jest.useFakeTimers();
  989. // widgetIndex: undefined means creating a new widget
  990. renderTestComponent({params: {widgetIndex: undefined}});
  991. userEvent.click(await screen.findByLabelText('Add a Column'));
  992. await selectEvent.select(screen.getByText('(Required)'), /project/);
  993. // Triggering the onBlur of the filter should not error
  994. userEvent.click(
  995. screen.getByPlaceholderText('Search for events, users, tags, and more')
  996. );
  997. userEvent.keyboard('{enter}');
  998. act(() => {
  999. // Run all timers because the handleBlur contains a setTimeout
  1000. jest.runAllTimers();
  1001. });
  1002. expect(await screen.findAllByText('project')).toHaveLength(2);
  1003. });
  1004. it('renders fields with commas properly', async () => {
  1005. const defaultWidgetQuery = {
  1006. conditions: '',
  1007. fields: ['equation|count_if(transaction.duration,equals,300)*2'],
  1008. aggregates: ['equation|count_if(transaction.duration,equals,300)*2'],
  1009. columns: [],
  1010. orderby: '',
  1011. name: '',
  1012. };
  1013. const defaultTableColumns = [
  1014. 'count_if(transaction.duration,equals,300)',
  1015. 'equation|count_if(transaction.duration,equals,300)*2',
  1016. ];
  1017. renderTestComponent({
  1018. query: {
  1019. source: DashboardWidgetSource.DISCOVERV2,
  1020. defaultWidgetQuery: urlEncode(defaultWidgetQuery),
  1021. defaultTableColumns,
  1022. yAxis: ['equation|count_if(transaction.duration,equals,300)*2'],
  1023. },
  1024. });
  1025. expect(
  1026. await screen.findByText('count_if(transaction.duration,equals,300)*2')
  1027. ).toBeInTheDocument();
  1028. });
  1029. it('sets the correct fields for a top n widget', async () => {
  1030. renderTestComponent({
  1031. orgFeatures: [...defaultOrgFeatures, 'performance-view'],
  1032. query: {
  1033. displayType: DisplayType.TOP_N,
  1034. },
  1035. });
  1036. // Top N now opens as Area Chart
  1037. await screen.findByText('Area Chart');
  1038. // Add a group by
  1039. userEvent.click(screen.getByText('Add Overlay'));
  1040. await selectEvent.select(screen.getByText('Select group'), /project/);
  1041. // Change the y-axis
  1042. await selectEvent.select(screen.getAllByText('count()')[0], 'eps()');
  1043. await waitFor(() => {
  1044. expect(eventsStatsMock).toHaveBeenLastCalledWith(
  1045. '/organizations/org-slug/events-stats/',
  1046. expect.objectContaining({
  1047. query: expect.objectContaining({
  1048. query: '',
  1049. yAxis: ['eps()'],
  1050. field: ['project', 'eps()'],
  1051. topEvents: TOP_N,
  1052. orderby: '-eps()',
  1053. }),
  1054. })
  1055. );
  1056. });
  1057. });
  1058. it('fetches tags when tag store is empty', function () {
  1059. renderTestComponent();
  1060. expect(tagsMock).toHaveBeenCalled();
  1061. });
  1062. it('does not fetch tags when tag store is not empty', function () {
  1063. TagStore.loadTagsSuccess(TestStubs.Tags());
  1064. renderTestComponent();
  1065. expect(tagsMock).not.toHaveBeenCalled();
  1066. });
  1067. it('excludes the Other series when grouping and using multiple y-axes', async function () {
  1068. renderTestComponent({
  1069. orgFeatures: [...defaultOrgFeatures, 'new-widget-builder-experience-design'],
  1070. query: {
  1071. displayType: DisplayType.LINE,
  1072. },
  1073. });
  1074. await selectEvent.select(await screen.findByText('Select group'), 'project');
  1075. userEvent.click(screen.getByText('Add Overlay'));
  1076. await selectEvent.select(screen.getByText('(Required)'), /count_unique/);
  1077. await waitFor(() => {
  1078. expect(eventsStatsMock).toBeCalledWith(
  1079. '/organizations/org-slug/events-stats/',
  1080. expect.objectContaining({
  1081. query: expect.objectContaining({excludeOther: '1'}),
  1082. })
  1083. );
  1084. });
  1085. });
  1086. it('excludes the Other series when grouping and using multiple queries', async function () {
  1087. renderTestComponent({
  1088. orgFeatures: [...defaultOrgFeatures, 'new-widget-builder-experience-design'],
  1089. query: {
  1090. displayType: DisplayType.LINE,
  1091. },
  1092. });
  1093. await selectEvent.select(await screen.findByText('Select group'), 'project');
  1094. userEvent.click(screen.getByText('Add Query'));
  1095. await waitFor(() => {
  1096. expect(eventsStatsMock).toBeCalledWith(
  1097. '/organizations/org-slug/events-stats/',
  1098. expect.objectContaining({
  1099. query: expect.objectContaining({excludeOther: '1'}),
  1100. })
  1101. );
  1102. });
  1103. });
  1104. it('includes Other series when there is only one query and one y-axis', async function () {
  1105. renderTestComponent({
  1106. orgFeatures: [...defaultOrgFeatures, 'new-widget-builder-experience-design'],
  1107. query: {
  1108. displayType: DisplayType.LINE,
  1109. },
  1110. });
  1111. await selectEvent.select(await screen.findByText('Select group'), 'project');
  1112. await waitFor(() => {
  1113. expect(eventsStatsMock).toBeCalledWith(
  1114. '/organizations/org-slug/events-stats/',
  1115. expect.objectContaining({
  1116. query: expect.not.objectContaining({excludeOther: '1'}),
  1117. })
  1118. );
  1119. });
  1120. });
  1121. it('decreases the limit when more y-axes and queries are added', async function () {
  1122. renderTestComponent({
  1123. orgFeatures: [...defaultOrgFeatures, 'new-widget-builder-experience-design'],
  1124. query: {
  1125. displayType: DisplayType.LINE,
  1126. },
  1127. });
  1128. await selectEvent.select(await screen.findByText('Select group'), 'project');
  1129. screen.getByText('Limit to 5 results');
  1130. userEvent.click(screen.getByText('Add Query'));
  1131. userEvent.click(screen.getByText('Add Overlay'));
  1132. await screen.findByText('Limit to 2 results');
  1133. });
  1134. it('alerts the user if there are unsaved changes', async function () {
  1135. const {router} = renderTestComponent();
  1136. const alertMock = jest.fn();
  1137. const setRouteLeaveHookMock = jest.spyOn(router, 'setRouteLeaveHook');
  1138. setRouteLeaveHookMock.mockImplementationOnce((_route, _callback) => {
  1139. alertMock();
  1140. });
  1141. const customWidgetLabels = await screen.findAllByText('Custom Widget');
  1142. // EditableText and chart title
  1143. expect(customWidgetLabels).toHaveLength(2);
  1144. // Change title text
  1145. userEvent.click(customWidgetLabels[0]);
  1146. userEvent.clear(screen.getByRole('textbox', {name: 'Widget title'}));
  1147. userEvent.paste(
  1148. screen.getByRole('textbox', {name: 'Widget title'}),
  1149. 'Unique Users'
  1150. );
  1151. userEvent.keyboard('{enter}');
  1152. // Click Cancel
  1153. userEvent.click(screen.getByText('Cancel'));
  1154. // Assert an alert was triggered
  1155. expect(alertMock).toHaveBeenCalled();
  1156. });
  1157. it('does not trigger alert dialog if no changes', async function () {
  1158. const {router} = renderTestComponent();
  1159. const alertMock = jest.fn();
  1160. const setRouteLeaveHookMock = jest.spyOn(router, 'setRouteLeaveHook');
  1161. setRouteLeaveHookMock.mockImplementationOnce((_route, _callback) => {
  1162. alertMock();
  1163. });
  1164. await screen.findAllByText('Custom Widget');
  1165. // Click Cancel
  1166. userEvent.click(screen.getByText('Cancel'));
  1167. // Assert an alert was triggered
  1168. expect(alertMock).not.toHaveBeenCalled();
  1169. });
  1170. describe('Widget creation coming from other verticals', function () {
  1171. it('redirects correctly when creating a new dashboard', async function () {
  1172. const {router} = renderTestComponent({
  1173. query: {source: DashboardWidgetSource.DISCOVERV2},
  1174. });
  1175. userEvent.click(await screen.findByText('Add Widget'));
  1176. await waitFor(() => {
  1177. expect(router.push).toHaveBeenCalledWith(
  1178. expect.objectContaining({
  1179. pathname: '/organizations/org-slug/dashboards/new/',
  1180. query: {
  1181. displayType: 'table',
  1182. interval: '5m',
  1183. title: 'Custom Widget',
  1184. queryNames: [''],
  1185. queryConditions: [''],
  1186. queryFields: ['count()'],
  1187. queryOrderby: '-count()',
  1188. start: null,
  1189. end: null,
  1190. statsPeriod: '24h',
  1191. utc: null,
  1192. project: [],
  1193. environment: [],
  1194. },
  1195. })
  1196. );
  1197. });
  1198. });
  1199. it('redirects correctly when choosing an existing dashboard', async function () {
  1200. const {router} = renderTestComponent({
  1201. query: {source: DashboardWidgetSource.DISCOVERV2},
  1202. dashboard: testDashboard,
  1203. });
  1204. userEvent.click(await screen.findByText('Add Widget'));
  1205. await waitFor(() => {
  1206. expect(router.push).toHaveBeenCalledWith(
  1207. expect.objectContaining({
  1208. pathname: '/organizations/org-slug/dashboard/2/',
  1209. query: {
  1210. displayType: 'table',
  1211. interval: '5m',
  1212. title: 'Custom Widget',
  1213. queryNames: [''],
  1214. queryConditions: [''],
  1215. queryFields: ['count()'],
  1216. queryOrderby: '-count()',
  1217. start: null,
  1218. end: null,
  1219. statsPeriod: '24h',
  1220. utc: null,
  1221. project: [],
  1222. environment: [],
  1223. },
  1224. })
  1225. );
  1226. });
  1227. });
  1228. it('shows the correct orderby when switching from a line chart to table', async function () {
  1229. const defaultWidgetQuery = {
  1230. name: '',
  1231. fields: ['count_unique(user)'],
  1232. columns: [],
  1233. aggregates: ['count_unique(user)'],
  1234. conditions: '',
  1235. orderby: 'count_unique(user)',
  1236. };
  1237. const defaultTableColumns = ['title', 'count_unique(user)'];
  1238. renderTestComponent({
  1239. orgFeatures: [...defaultOrgFeatures, 'new-widget-builder-experience-design'],
  1240. query: {
  1241. source: DashboardWidgetSource.DISCOVERV2,
  1242. defaultWidgetQuery: urlEncode(defaultWidgetQuery),
  1243. displayType: DisplayType.LINE,
  1244. defaultTableColumns,
  1245. },
  1246. });
  1247. userEvent.click(await screen.findByText('Line Chart'));
  1248. userEvent.click(screen.getByText('Table'));
  1249. expect(screen.getByText('count_unique(user)')).toBeInTheDocument();
  1250. await waitFor(() => {
  1251. expect(eventsv2Mock).toHaveBeenLastCalledWith(
  1252. '/organizations/org-slug/eventsv2/',
  1253. expect.objectContaining({
  1254. query: expect.objectContaining({
  1255. field: defaultTableColumns,
  1256. sort: ['count_unique(user)'],
  1257. }),
  1258. })
  1259. );
  1260. });
  1261. });
  1262. it('does not send request with orderby if a timeseries chart without grouping', async function () {
  1263. const defaultWidgetQuery = {
  1264. name: '',
  1265. fields: ['count_unique(user)'],
  1266. columns: [],
  1267. aggregates: ['count_unique(user)'],
  1268. conditions: '',
  1269. orderby: 'count_unique(user)',
  1270. };
  1271. const defaultTableColumns = ['title', 'count_unique(user)'];
  1272. renderTestComponent({
  1273. orgFeatures: [...defaultOrgFeatures, 'new-widget-builder-experience-design'],
  1274. query: {
  1275. source: DashboardWidgetSource.DISCOVERV2,
  1276. defaultWidgetQuery: urlEncode(defaultWidgetQuery),
  1277. displayType: DisplayType.LINE,
  1278. defaultTableColumns,
  1279. },
  1280. });
  1281. await waitFor(() => {
  1282. expect(eventsStatsMock).toHaveBeenLastCalledWith(
  1283. '/organizations/org-slug/events-stats/',
  1284. expect.objectContaining({
  1285. query: expect.objectContaining({
  1286. orderby: '',
  1287. }),
  1288. })
  1289. );
  1290. });
  1291. });
  1292. });
  1293. it('opens top-N widgets as area display', async function () {
  1294. const widget: Widget = {
  1295. id: '1',
  1296. title: 'Errors over time',
  1297. interval: '5m',
  1298. displayType: DisplayType.TOP_N,
  1299. queries: [
  1300. {
  1301. name: '',
  1302. conditions: '',
  1303. fields: ['count()', 'count_unique(id)'],
  1304. aggregates: ['count()', 'count_unique(id)'],
  1305. columns: [],
  1306. orderby: '-count()',
  1307. },
  1308. ],
  1309. };
  1310. const dashboard = mockDashboard({widgets: [widget]});
  1311. renderTestComponent({
  1312. orgFeatures: [...defaultOrgFeatures, 'new-widget-builder-experience-design'],
  1313. dashboard,
  1314. params: {
  1315. widgetIndex: '0',
  1316. },
  1317. });
  1318. expect(await screen.findByText('Area Chart')).toBeInTheDocument();
  1319. });
  1320. it('Update table header values (field alias)', async function () {
  1321. const handleSave = jest.fn();
  1322. renderTestComponent({
  1323. onSave: handleSave,
  1324. orgFeatures: [...defaultOrgFeatures, 'new-widget-builder-experience-design'],
  1325. });
  1326. await screen.findByText('Table');
  1327. userEvent.paste(screen.getByPlaceholderText('Alias'), 'First Alias');
  1328. userEvent.click(screen.getByLabelText('Add a Column'));
  1329. userEvent.paste(screen.getAllByPlaceholderText('Alias')[1], 'Second Alias');
  1330. userEvent.click(screen.getByText('Add Widget'));
  1331. await waitFor(() => {
  1332. expect(handleSave).toHaveBeenCalledWith([
  1333. expect.objectContaining({
  1334. queries: [
  1335. expect.objectContaining({fieldAliases: ['First Alias', 'Second Alias']}),
  1336. ],
  1337. }),
  1338. ]);
  1339. });
  1340. });
  1341. it('does not wipe equation aliases when a column alias is updated', async function () {
  1342. renderTestComponent({
  1343. orgFeatures: [...defaultOrgFeatures, 'new-widget-builder-experience-design'],
  1344. });
  1345. await screen.findByText('Table');
  1346. userEvent.click(screen.getByText('Add an Equation'));
  1347. userEvent.paste(screen.getAllByPlaceholderText('Alias')[1], 'This should persist');
  1348. userEvent.type(screen.getAllByPlaceholderText('Alias')[0], 'A');
  1349. expect(await screen.findByText('This should persist')).toBeInTheDocument();
  1350. });
  1351. it('does not wipe equation aliases when a column selection is made', async function () {
  1352. renderTestComponent({
  1353. orgFeatures: [...defaultOrgFeatures, 'new-widget-builder-experience-design'],
  1354. });
  1355. await screen.findByText('Table');
  1356. userEvent.click(screen.getByText('Add an Equation'));
  1357. userEvent.paste(screen.getAllByPlaceholderText('Alias')[1], 'This should persist');
  1358. // 1 for the table, 1 for the the column selector, 1 for the sort
  1359. await waitFor(() => expect(screen.getAllByText('count()')).toHaveLength(3));
  1360. await selectEvent.select(screen.getAllByText('count()')[1], /count_unique/);
  1361. expect(screen.getByText('This should persist')).toBeInTheDocument();
  1362. });
  1363. it('copies over the orderby from the previous query if adding another', async function () {
  1364. renderTestComponent({
  1365. orgFeatures: [...defaultOrgFeatures, 'new-widget-builder-experience-design'],
  1366. });
  1367. userEvent.click(await screen.findByText('Table'));
  1368. userEvent.click(screen.getByText('Line Chart'));
  1369. await selectEvent.select(screen.getByText('Select group'), 'project');
  1370. await selectEvent.select(screen.getAllByText('count()')[1], 'count_unique(…)');
  1371. MockApiClient.clearMockResponses();
  1372. eventsStatsMock = MockApiClient.addMockResponse({
  1373. url: '/organizations/org-slug/events-stats/',
  1374. body: [],
  1375. });
  1376. userEvent.click(screen.getByText('Add Query'));
  1377. // Assert on two calls, one for each query
  1378. const expectedArgs = expect.objectContaining({
  1379. query: expect.objectContaining({
  1380. orderby: '-count_unique(user)',
  1381. }),
  1382. });
  1383. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  1384. 1,
  1385. '/organizations/org-slug/events-stats/',
  1386. expectedArgs
  1387. );
  1388. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  1389. 2,
  1390. '/organizations/org-slug/events-stats/',
  1391. expectedArgs
  1392. );
  1393. });
  1394. describe('Widget Library', function () {
  1395. it('renders', async function () {
  1396. renderTestComponent();
  1397. expect(await screen.findByText('Widget Library')).toBeInTheDocument();
  1398. });
  1399. it('only opens the modal when the query data is changed', async function () {
  1400. const mockModal = jest.spyOn(modals, 'openWidgetBuilderOverwriteModal');
  1401. renderTestComponent();
  1402. await screen.findByText('Widget Library');
  1403. userEvent.click(screen.getByText('Duration Distribution'));
  1404. // Widget Library, Builder title, and Chart title
  1405. expect(await screen.findAllByText('Duration Distribution')).toHaveLength(3);
  1406. // Confirm modal doesn't open because no changes were made
  1407. expect(mockModal).not.toHaveBeenCalled();
  1408. userEvent.click(screen.getAllByLabelText('Remove this Y-Axis')[0]);
  1409. userEvent.click(screen.getByText('High Throughput Transactions'));
  1410. // Should not have overwritten widget data, and confirm modal should open
  1411. expect(await screen.findAllByText('Duration Distribution')).toHaveLength(3);
  1412. expect(mockModal).toHaveBeenCalled();
  1413. });
  1414. });
  1415. describe('group by field', function () {
  1416. it('does not contain functions as options', async function () {
  1417. renderTestComponent({
  1418. query: {displayType: 'line'},
  1419. orgFeatures: [...defaultOrgFeatures, 'new-widget-builder-experience-design'],
  1420. });
  1421. await screen.findByText('Group your results');
  1422. expect(screen.getByText('Select group')).toBeInTheDocument();
  1423. userEvent.click(screen.getByText('Select group'));
  1424. // Only one f(x) field set in the y-axis selector
  1425. expect(screen.getByText('f(x)')).toBeInTheDocument();
  1426. });
  1427. it('adds more fields when Add Group is clicked', async function () {
  1428. renderTestComponent({
  1429. query: {displayType: 'line'},
  1430. orgFeatures: [...defaultOrgFeatures, 'new-widget-builder-experience-design'],
  1431. });
  1432. await screen.findByText('Group your results');
  1433. userEvent.click(screen.getByText('Add Group'));
  1434. expect(await screen.findAllByText('Select group')).toHaveLength(2);
  1435. });
  1436. it("doesn't reset group by when changing y-axis", async function () {
  1437. renderTestComponent({
  1438. query: {displayType: 'line'},
  1439. orgFeatures: [...defaultOrgFeatures, 'new-widget-builder-experience-design'],
  1440. });
  1441. await selectEvent.select(await screen.findByText('Select group'), 'project');
  1442. userEvent.click(screen.getAllByText('count()')[0], undefined, {skipHover: true});
  1443. userEvent.click(screen.getByText(/count_unique/), undefined, {skipHover: true});
  1444. expect(await screen.findByText('project')).toBeInTheDocument();
  1445. });
  1446. it("doesn't erase the selection when switching to another time series", async function () {
  1447. renderTestComponent({
  1448. query: {displayType: 'line'},
  1449. orgFeatures: [...defaultOrgFeatures, 'new-widget-builder-experience-design'],
  1450. });
  1451. await selectEvent.select(await screen.findByText('Select group'), 'project');
  1452. userEvent.click(screen.getByText('Line Chart'));
  1453. userEvent.click(screen.getByText('Area Chart'));
  1454. expect(await screen.findByText('project')).toBeInTheDocument();
  1455. });
  1456. it('sends a top N request when a grouping is selected', async function () {
  1457. renderTestComponent({
  1458. query: {displayType: 'line'},
  1459. orgFeatures: [...defaultOrgFeatures, 'new-widget-builder-experience-design'],
  1460. });
  1461. userEvent.click(await screen.findByText('Group your results'));
  1462. userEvent.type(screen.getByText('Select group'), 'project{enter}');
  1463. await waitFor(() =>
  1464. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  1465. 2,
  1466. '/organizations/org-slug/events-stats/',
  1467. expect.objectContaining({
  1468. query: expect.objectContaining({
  1469. query: '',
  1470. yAxis: ['count()'],
  1471. field: ['project', 'count()'],
  1472. topEvents: TOP_N,
  1473. orderby: '-count()',
  1474. }),
  1475. })
  1476. )
  1477. );
  1478. });
  1479. it('allows deleting groups until there is one left', async function () {
  1480. renderTestComponent({
  1481. query: {displayType: 'line'},
  1482. orgFeatures: [...defaultOrgFeatures, 'new-widget-builder-experience-design'],
  1483. });
  1484. await screen.findByText('Group your results');
  1485. userEvent.click(screen.getByText('Add Group'));
  1486. expect(screen.getAllByLabelText('Remove group')).toHaveLength(2);
  1487. userEvent.click(screen.getAllByLabelText('Remove group')[1]);
  1488. await waitFor(() =>
  1489. expect(screen.queryByLabelText('Remove group')).not.toBeInTheDocument()
  1490. );
  1491. });
  1492. it("display 'remove' and 'drag to reorder' buttons", async function () {
  1493. renderTestComponent({
  1494. query: {displayType: 'line'},
  1495. orgFeatures: [...defaultOrgFeatures, 'new-widget-builder-experience-design'],
  1496. });
  1497. await screen.findByText('Select group');
  1498. expect(screen.queryByLabelText('Remove group')).not.toBeInTheDocument();
  1499. await selectEvent.select(screen.getByText('Select group'), 'project');
  1500. expect(screen.getByLabelText('Remove group')).toBeInTheDocument();
  1501. expect(screen.queryByLabelText('Drag to reorder')).not.toBeInTheDocument();
  1502. userEvent.click(screen.getByText('Add Group'));
  1503. expect(screen.getAllByLabelText('Remove group')).toHaveLength(2);
  1504. expect(screen.getAllByLabelText('Drag to reorder')).toHaveLength(2);
  1505. });
  1506. it.todo(
  1507. 'Since simulate drag and drop with RTL is not recommended because of browser layout, remember to create acceptance test for this'
  1508. );
  1509. });
  1510. describe('limit field', function () {
  1511. it('renders if groupBy value is present', async function () {
  1512. const handleSave = jest.fn();
  1513. renderTestComponent({
  1514. query: {displayType: 'line'},
  1515. orgFeatures: [...defaultOrgFeatures, 'new-widget-builder-experience-design'],
  1516. onSave: handleSave,
  1517. });
  1518. await selectEvent.select(await screen.findByText('Select group'), 'project');
  1519. expect(screen.getByText('Limit to 5 results')).toBeInTheDocument();
  1520. userEvent.click(screen.getByText('Add Widget'));
  1521. await waitFor(() =>
  1522. expect(handleSave).toHaveBeenCalledWith([
  1523. expect.objectContaining({
  1524. limit: 5,
  1525. }),
  1526. ])
  1527. );
  1528. });
  1529. it('update value', async function () {
  1530. renderTestComponent({
  1531. query: {displayType: 'line'},
  1532. orgFeatures: [...defaultOrgFeatures, 'new-widget-builder-experience-design'],
  1533. });
  1534. await selectEvent.select(await screen.findByText('Select group'), 'project');
  1535. userEvent.click(screen.getByText('Limit to 5 results'));
  1536. userEvent.click(screen.getByText('Limit to 2 results'));
  1537. await waitFor(() =>
  1538. expect(eventsStatsMock).toHaveBeenCalledWith(
  1539. '/organizations/org-slug/events-stats/',
  1540. expect.objectContaining({
  1541. query: expect.objectContaining({
  1542. query: '',
  1543. yAxis: ['count()'],
  1544. field: ['project', 'count()'],
  1545. topEvents: 2,
  1546. orderby: '-count()',
  1547. }),
  1548. })
  1549. )
  1550. );
  1551. });
  1552. it('gets removed if no groupBy value', async function () {
  1553. renderTestComponent({
  1554. query: {displayType: 'line'},
  1555. orgFeatures: [...defaultOrgFeatures, 'new-widget-builder-experience-design'],
  1556. });
  1557. await selectEvent.select(await screen.findByText('Select group'), 'project');
  1558. expect(screen.getByText('Limit to 5 results')).toBeInTheDocument();
  1559. userEvent.click(screen.getByLabelText('Remove group'));
  1560. await waitFor(() =>
  1561. expect(screen.queryByText('Limit to 5 results')).not.toBeInTheDocument()
  1562. );
  1563. });
  1564. it('applies a limit when switching from a table to timeseries chart with grouping', async function () {
  1565. const widget: Widget = {
  1566. displayType: DisplayType.TABLE,
  1567. interval: '1d',
  1568. queries: [
  1569. {
  1570. name: 'Test Widget',
  1571. fields: ['count()', 'count_unique(user)', 'epm()', 'project'],
  1572. columns: ['project'],
  1573. aggregates: ['count()', 'count_unique(user)', 'epm()'],
  1574. conditions: '',
  1575. orderby: '',
  1576. },
  1577. ],
  1578. title: 'Transactions',
  1579. id: '1',
  1580. };
  1581. const dashboard = mockDashboard({widgets: [widget]});
  1582. renderTestComponent({
  1583. dashboard,
  1584. orgFeatures: [...defaultOrgFeatures, 'new-widget-builder-experience-design'],
  1585. params: {
  1586. widgetIndex: '0',
  1587. },
  1588. });
  1589. userEvent.click(await screen.findByText('Table'));
  1590. userEvent.click(screen.getByText('Line Chart'));
  1591. expect(screen.getByText('Limit to 3 results')).toBeInTheDocument();
  1592. expect(eventsStatsMock).toHaveBeenCalledWith(
  1593. '/organizations/org-slug/events-stats/',
  1594. expect.objectContaining({
  1595. query: expect.objectContaining({
  1596. topEvents: 3,
  1597. }),
  1598. })
  1599. );
  1600. });
  1601. it('persists the limit when switching between timeseries charts', async function () {
  1602. const widget: Widget = {
  1603. displayType: DisplayType.AREA,
  1604. interval: '1d',
  1605. queries: [
  1606. {
  1607. name: 'Test Widget',
  1608. fields: ['count()', 'count_unique(user)', 'epm()', 'project'],
  1609. columns: ['project'],
  1610. aggregates: ['count()', 'count_unique(user)', 'epm()'],
  1611. conditions: '',
  1612. orderby: '',
  1613. },
  1614. ],
  1615. title: 'Transactions',
  1616. id: '1',
  1617. limit: 1,
  1618. };
  1619. const dashboard = mockDashboard({widgets: [widget]});
  1620. renderTestComponent({
  1621. dashboard,
  1622. orgFeatures: [...defaultOrgFeatures, 'new-widget-builder-experience-design'],
  1623. params: {
  1624. widgetIndex: '0',
  1625. },
  1626. });
  1627. userEvent.click(await screen.findByText('Area Chart'));
  1628. userEvent.click(screen.getByText('Line Chart'));
  1629. expect(screen.getByText('Limit to 1 result')).toBeInTheDocument();
  1630. expect(eventsStatsMock).toHaveBeenCalledWith(
  1631. '/organizations/org-slug/events-stats/',
  1632. expect.objectContaining({
  1633. query: expect.objectContaining({
  1634. topEvents: 1,
  1635. }),
  1636. })
  1637. );
  1638. });
  1639. it('unsets the limit when going from timeseries to table', async function () {
  1640. const widget: Widget = {
  1641. displayType: DisplayType.AREA,
  1642. interval: '1d',
  1643. queries: [
  1644. {
  1645. name: 'Test Widget',
  1646. fields: ['count()', 'count_unique(user)', 'epm()', 'project'],
  1647. columns: ['project'],
  1648. aggregates: ['count()', 'count_unique(user)', 'epm()'],
  1649. conditions: '',
  1650. orderby: '',
  1651. },
  1652. ],
  1653. title: 'Transactions',
  1654. id: '1',
  1655. limit: 1,
  1656. };
  1657. const dashboard = mockDashboard({widgets: [widget]});
  1658. renderTestComponent({
  1659. dashboard,
  1660. orgFeatures: [...defaultOrgFeatures, 'new-widget-builder-experience-design'],
  1661. params: {
  1662. widgetIndex: '0',
  1663. },
  1664. });
  1665. userEvent.click(await screen.findByText('Area Chart'));
  1666. userEvent.click(screen.getByText('Table'));
  1667. expect(screen.queryByText('Limit to 1 result')).not.toBeInTheDocument();
  1668. expect(eventsv2Mock).toHaveBeenCalledWith(
  1669. '/organizations/org-slug/eventsv2/',
  1670. expect.objectContaining({
  1671. query: expect.objectContaining({
  1672. topEvents: undefined,
  1673. }),
  1674. })
  1675. );
  1676. });
  1677. });
  1678. });
  1679. describe('with events', function () {
  1680. it('should properly query for table fields', async function () {
  1681. const defaultWidgetQuery = {
  1682. name: '',
  1683. fields: ['title', 'count()'],
  1684. columns: ['title'],
  1685. aggregates: ['count()'],
  1686. conditions: '',
  1687. orderby: '',
  1688. };
  1689. const defaultTableColumns = ['title', 'count()', 'count_unique(user)', 'epm()'];
  1690. renderTestComponent({
  1691. query: {
  1692. source: DashboardWidgetSource.DISCOVERV2,
  1693. defaultWidgetQuery: urlEncode(defaultWidgetQuery),
  1694. displayType: DisplayType.LINE,
  1695. defaultTableColumns,
  1696. },
  1697. orgFeatures: [...defaultOrgFeatures, 'discover-frontend-use-events-endpoint'],
  1698. });
  1699. expect(await screen.findByText('Line Chart')).toBeInTheDocument();
  1700. userEvent.click(screen.getByText('Line Chart'));
  1701. userEvent.click(screen.getByText('Table'));
  1702. await waitFor(() => {
  1703. expect(eventsMock).toHaveBeenLastCalledWith(
  1704. '/organizations/org-slug/events/',
  1705. expect.objectContaining({
  1706. query: expect.objectContaining({
  1707. field: defaultTableColumns,
  1708. }),
  1709. })
  1710. );
  1711. });
  1712. });
  1713. describe('Widget creation coming from other verticals', function () {
  1714. it('shows the correct orderby when switching from a line chart to table', async function () {
  1715. const defaultWidgetQuery = {
  1716. name: '',
  1717. fields: ['count_unique(user)'],
  1718. columns: [],
  1719. aggregates: ['count_unique(user)'],
  1720. conditions: '',
  1721. orderby: 'count_unique(user)',
  1722. };
  1723. const defaultTableColumns = ['title', 'count_unique(user)'];
  1724. renderTestComponent({
  1725. orgFeatures: [
  1726. ...defaultOrgFeatures,
  1727. 'new-widget-builder-experience-design',
  1728. 'discover-frontend-use-events-endpoint',
  1729. ],
  1730. query: {
  1731. source: DashboardWidgetSource.DISCOVERV2,
  1732. defaultWidgetQuery: urlEncode(defaultWidgetQuery),
  1733. displayType: DisplayType.LINE,
  1734. defaultTableColumns,
  1735. },
  1736. });
  1737. userEvent.click(await screen.findByText('Line Chart'));
  1738. userEvent.click(screen.getByText('Table'));
  1739. expect(screen.getByText('count_unique(user)')).toBeInTheDocument();
  1740. await waitFor(() => {
  1741. expect(eventsMock).toHaveBeenLastCalledWith(
  1742. '/organizations/org-slug/events/',
  1743. expect.objectContaining({
  1744. query: expect.objectContaining({
  1745. field: defaultTableColumns,
  1746. sort: ['count_unique(user)'],
  1747. }),
  1748. })
  1749. );
  1750. });
  1751. });
  1752. });
  1753. });
  1754. });