widgetBuilder.spec.tsx 82 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715
  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 {ReleaseFixture} from 'sentry-fixture/release';
  6. import {SessionsFieldFixture} from 'sentry-fixture/sessions';
  7. import {TagsFixture} from 'sentry-fixture/tags';
  8. import {WidgetFixture} from 'sentry-fixture/widget';
  9. import {initializeOrg} from 'sentry-test/initializeOrg';
  10. import {
  11. act,
  12. render,
  13. renderGlobalModal,
  14. screen,
  15. userEvent,
  16. waitFor,
  17. } from 'sentry-test/reactTestingLibrary';
  18. import selectEvent from 'sentry-test/selectEvent';
  19. import * as modals from 'sentry/actionCreators/modal';
  20. import ProjectsStore from 'sentry/stores/projectsStore';
  21. import TagStore from 'sentry/stores/tagStore';
  22. import {DatasetSource, TOP_N} from 'sentry/utils/discover/types';
  23. import type {DashboardDetails, Widget} from 'sentry/views/dashboards/types';
  24. import {
  25. DashboardWidgetSource,
  26. DisplayType,
  27. WidgetType,
  28. } from 'sentry/views/dashboards/types';
  29. import type {WidgetBuilderProps} from 'sentry/views/dashboards/widgetBuilder';
  30. import WidgetBuilder from 'sentry/views/dashboards/widgetBuilder';
  31. import WidgetLegendSelectionState from '../widgetLegendSelectionState';
  32. const defaultOrgFeatures = [
  33. 'performance-view',
  34. 'dashboards-edit',
  35. 'global-views',
  36. 'dashboards-mep',
  37. ];
  38. function mockDashboard(dashboard: Partial<DashboardDetails>): DashboardDetails {
  39. return {
  40. id: '1',
  41. title: 'Dashboard',
  42. createdBy: undefined,
  43. dateCreated: '2020-01-01T00:00:00.000Z',
  44. widgets: [],
  45. projects: [],
  46. filters: {},
  47. ...dashboard,
  48. };
  49. }
  50. function renderTestComponent({
  51. dashboard,
  52. query,
  53. orgFeatures,
  54. onSave,
  55. params,
  56. updateDashboardSplitDecision,
  57. }: {
  58. dashboard?: WidgetBuilderProps['dashboard'];
  59. onSave?: WidgetBuilderProps['onSave'];
  60. orgFeatures?: string[];
  61. params?: Partial<WidgetBuilderProps['params']>;
  62. query?: Record<string, any>;
  63. updateDashboardSplitDecision?: WidgetBuilderProps['updateDashboardSplitDecision'];
  64. } = {}) {
  65. const {organization, projects, router} = initializeOrg({
  66. organization: {
  67. features: orgFeatures ?? defaultOrgFeatures,
  68. },
  69. router: {
  70. location: {
  71. query: {
  72. source: DashboardWidgetSource.DASHBOARDS,
  73. ...query,
  74. },
  75. },
  76. },
  77. });
  78. ProjectsStore.loadInitialData(projects);
  79. const widgetLegendState = new WidgetLegendSelectionState({
  80. location: LocationFixture(),
  81. dashboard: DashboardFixture([], {id: 'new', title: 'Dashboard', ...dashboard}),
  82. organization,
  83. router,
  84. });
  85. render(
  86. <WidgetBuilder
  87. route={{}}
  88. router={router}
  89. routes={router.routes}
  90. routeParams={router.params}
  91. location={router.location}
  92. dashboard={{
  93. id: 'new',
  94. title: 'Dashboard',
  95. createdBy: undefined,
  96. dateCreated: '2020-01-01T00:00:00.000Z',
  97. widgets: [],
  98. projects: [],
  99. filters: {},
  100. ...dashboard,
  101. }}
  102. onSave={onSave ?? jest.fn()}
  103. params={{
  104. orgId: organization.slug,
  105. dashboardId: dashboard?.id ?? 'new',
  106. ...params,
  107. }}
  108. updateDashboardSplitDecision={updateDashboardSplitDecision}
  109. widgetLegendState={widgetLegendState}
  110. />,
  111. {
  112. router,
  113. organization,
  114. }
  115. );
  116. return {router};
  117. }
  118. /**
  119. * This test suite contains tests that test the generic interactions
  120. * between most components in the WidgetBuilder. Tests for the
  121. * SortBy step can be found in (and should be added to)
  122. * ./widgetBuilderSortBy.spec.tsx and tests for specific dataset
  123. * behaviour can be found (and should be added to) ./widgetBuilderDataset.spec.tsx.
  124. * The test files are broken up to allow better parallelization
  125. * in CI (we currently parallelize files).
  126. */
  127. describe('WidgetBuilder', function () {
  128. const untitledDashboard: DashboardDetails = {
  129. id: '1',
  130. title: 'Untitled Dashboard',
  131. createdBy: undefined,
  132. dateCreated: '2020-01-01T00:00:00.000Z',
  133. widgets: [],
  134. projects: [],
  135. filters: {},
  136. };
  137. const testDashboard: DashboardDetails = {
  138. id: '2',
  139. title: 'Test Dashboard',
  140. createdBy: undefined,
  141. dateCreated: '2020-01-01T00:00:00.000Z',
  142. widgets: [],
  143. projects: [],
  144. filters: {},
  145. };
  146. let eventsStatsMock: jest.Mock | undefined;
  147. let eventsMock: jest.Mock | undefined;
  148. let tagsMock: jest.Mock | undefined;
  149. beforeEach(function () {
  150. MockApiClient.addMockResponse({
  151. url: '/organizations/org-slug/dashboards/',
  152. body: [
  153. {...untitledDashboard, widgetDisplay: [DisplayType.TABLE]},
  154. {...testDashboard, widgetDisplay: [DisplayType.AREA]},
  155. ],
  156. });
  157. MockApiClient.addMockResponse({
  158. url: '/organizations/org-slug/dashboards/widgets/',
  159. method: 'POST',
  160. statusCode: 200,
  161. body: [],
  162. });
  163. eventsMock = MockApiClient.addMockResponse({
  164. url: '/organizations/org-slug/events/',
  165. method: 'GET',
  166. statusCode: 200,
  167. body: {
  168. meta: {fields: {}},
  169. data: [],
  170. },
  171. });
  172. MockApiClient.addMockResponse({
  173. url: '/organizations/org-slug/projects/',
  174. method: 'GET',
  175. body: [],
  176. });
  177. MockApiClient.addMockResponse({
  178. url: '/organizations/org-slug/recent-searches/',
  179. method: 'GET',
  180. body: [],
  181. });
  182. MockApiClient.addMockResponse({
  183. url: '/organizations/org-slug/recent-searches/',
  184. method: 'POST',
  185. body: [],
  186. });
  187. MockApiClient.addMockResponse({
  188. url: '/organizations/org-slug/issues/',
  189. method: 'GET',
  190. body: [],
  191. });
  192. eventsStatsMock = MockApiClient.addMockResponse({
  193. url: '/organizations/org-slug/events-stats/',
  194. body: [],
  195. });
  196. MockApiClient.addMockResponse({
  197. url: '/organizations/org-slug/tags/event.type/values/',
  198. body: [{count: 2, name: 'Nvidia 1080ti'}],
  199. });
  200. MockApiClient.addMockResponse({
  201. url: '/organizations/org-slug/users/',
  202. body: [],
  203. });
  204. MockApiClient.addMockResponse({
  205. method: 'GET',
  206. url: '/organizations/org-slug/sessions/',
  207. body: SessionsFieldFixture(`sum(session)`),
  208. });
  209. MockApiClient.addMockResponse({
  210. method: 'GET',
  211. url: '/organizations/org-slug/metrics/data/',
  212. body: MetricsFieldFixture('session.all'),
  213. });
  214. tagsMock = MockApiClient.addMockResponse({
  215. url: '/organizations/org-slug/tags/',
  216. method: 'GET',
  217. body: TagsFixture(),
  218. });
  219. MockApiClient.addMockResponse({
  220. url: '/organizations/org-slug/measurements-meta/',
  221. method: 'GET',
  222. body: {},
  223. });
  224. MockApiClient.addMockResponse({
  225. url: '/organizations/org-slug/tags/is/values/',
  226. method: 'GET',
  227. body: [],
  228. });
  229. MockApiClient.addMockResponse({
  230. url: '/organizations/org-slug/tags/transaction.duration/values/',
  231. method: 'GET',
  232. body: [],
  233. });
  234. MockApiClient.addMockResponse({
  235. url: '/organizations/org-slug/releases/',
  236. body: [],
  237. });
  238. MockApiClient.addMockResponse({
  239. url: '/organizations/org-slug/releases/stats/',
  240. body: [],
  241. });
  242. MockApiClient.addMockResponse({
  243. url: `/organizations/org-slug/spans/fields/`,
  244. body: [],
  245. });
  246. TagStore.reset();
  247. });
  248. afterEach(function () {
  249. MockApiClient.clearMockResponses();
  250. jest.clearAllMocks();
  251. });
  252. it('no feature access', function () {
  253. renderTestComponent({orgFeatures: []});
  254. expect(screen.getByText("You don't have access to this feature")).toBeInTheDocument();
  255. });
  256. it('widget not found', async function () {
  257. const widget: Widget = {
  258. displayType: DisplayType.AREA,
  259. interval: '1d',
  260. queries: [
  261. {
  262. name: 'Known Users',
  263. fields: [],
  264. columns: [],
  265. aggregates: [],
  266. conditions: '',
  267. orderby: '-time',
  268. },
  269. {
  270. name: 'Anonymous Users',
  271. fields: [],
  272. columns: [],
  273. aggregates: [],
  274. conditions: '',
  275. orderby: '-time',
  276. },
  277. ],
  278. title: 'Transactions',
  279. id: '1',
  280. };
  281. const dashboard = mockDashboard({widgets: [widget]});
  282. renderTestComponent({
  283. dashboard,
  284. orgFeatures: ['dashboards-edit'],
  285. params: {
  286. widgetIndex: '2', // Out of bounds, only one widget
  287. },
  288. });
  289. expect(
  290. await screen.findByText('The widget you want to edit was not found.')
  291. ).toBeInTheDocument();
  292. });
  293. it('renders a widget not found message if the widget index url is not an integer', async function () {
  294. const widget: Widget = {
  295. displayType: DisplayType.AREA,
  296. interval: '1d',
  297. queries: [
  298. {
  299. name: 'Known Users',
  300. fields: [],
  301. columns: [],
  302. aggregates: [],
  303. conditions: '',
  304. orderby: '-time',
  305. },
  306. ],
  307. title: 'Transactions',
  308. id: '1',
  309. };
  310. const dashboard = mockDashboard({widgets: [widget]});
  311. renderTestComponent({
  312. dashboard,
  313. orgFeatures: ['dashboards-edit'],
  314. params: {
  315. widgetIndex: '0.5', // Invalid index
  316. },
  317. });
  318. expect(
  319. await screen.findByText('The widget you want to edit was not found.')
  320. ).toBeInTheDocument();
  321. });
  322. it('renders', async function () {
  323. renderTestComponent();
  324. // Header - Breadcrumbs
  325. expect(await screen.findByRole('link', {name: 'Dashboards'})).toHaveAttribute(
  326. 'href',
  327. '/organizations/org-slug/dashboards/'
  328. );
  329. expect(screen.getByRole('link', {name: 'Dashboard'})).toHaveAttribute(
  330. 'href',
  331. '/organizations/org-slug/dashboards/new/'
  332. );
  333. expect(screen.getByText('Widget Builder')).toBeInTheDocument();
  334. // Header - Widget Title
  335. expect(screen.getByText('Custom Widget')).toBeInTheDocument();
  336. // Footer - Actions
  337. expect(screen.getByLabelText('Cancel')).toBeInTheDocument();
  338. expect(screen.getByLabelText('Add Widget')).toBeInTheDocument();
  339. // Content - Step 1
  340. expect(
  341. screen.getByRole('heading', {name: 'Choose your dataset'})
  342. ).toBeInTheDocument();
  343. expect(screen.getByLabelText('Errors and Transactions')).toBeChecked();
  344. // Content - Step 2
  345. expect(
  346. screen.getByRole('heading', {name: 'Choose your visualization'})
  347. ).toBeInTheDocument();
  348. // Content - Step 3
  349. expect(
  350. screen.getByRole('heading', {name: 'Choose your columns'})
  351. ).toBeInTheDocument();
  352. // Content - Step 4
  353. expect(
  354. screen.getByRole('heading', {name: 'Filter your results'})
  355. ).toBeInTheDocument();
  356. // Content - Step 5
  357. expect(screen.getByRole('heading', {name: 'Sort by a column'})).toBeInTheDocument();
  358. });
  359. it('has links back to the new dashboard if creating', async function () {
  360. // Dashboard has undefined dashboardId when creating from a new dashboard
  361. // because of route setup
  362. renderTestComponent({params: {dashboardId: undefined}});
  363. expect(await screen.findByLabelText('Cancel')).toHaveAttribute(
  364. 'href',
  365. '/organizations/org-slug/dashboards/new/'
  366. );
  367. });
  368. it('renders new design', async function () {
  369. renderTestComponent({
  370. orgFeatures: [...defaultOrgFeatures],
  371. });
  372. // Switch to line chart for time series
  373. await userEvent.click(screen.getByText('Table'));
  374. await userEvent.click(screen.getByText('Line Chart'));
  375. // Header - Breadcrumbs
  376. expect(await screen.findByRole('link', {name: 'Dashboards'})).toHaveAttribute(
  377. 'href',
  378. '/organizations/org-slug/dashboards/'
  379. );
  380. expect(screen.getByRole('link', {name: 'Dashboard'})).toHaveAttribute(
  381. 'href',
  382. '/organizations/org-slug/dashboards/new/'
  383. );
  384. expect(screen.getByText('Widget Builder')).toBeInTheDocument();
  385. // Header - Widget Title
  386. expect(screen.getByText('Custom Widget')).toBeInTheDocument();
  387. // Footer - Actions
  388. expect(screen.getByLabelText('Cancel')).toBeInTheDocument();
  389. expect(screen.getByLabelText('Add Widget')).toBeInTheDocument();
  390. // Content - Step 1
  391. expect(
  392. screen.getByRole('heading', {name: 'Choose your dataset'})
  393. ).toBeInTheDocument();
  394. expect(screen.getByLabelText('Errors and Transactions')).toBeChecked();
  395. // Content - Step 2
  396. expect(
  397. screen.getByRole('heading', {name: 'Choose your visualization'})
  398. ).toBeInTheDocument();
  399. // Content - Step 3
  400. expect(
  401. screen.getByRole('heading', {name: 'Choose what to plot in the y-axis'})
  402. ).toBeInTheDocument();
  403. // Content - Step 4
  404. expect(
  405. screen.getByRole('heading', {name: 'Filter your results'})
  406. ).toBeInTheDocument();
  407. // Content - Step 5
  408. expect(screen.getByRole('heading', {name: 'Group your results'})).toBeInTheDocument();
  409. });
  410. it('can update the title', async function () {
  411. renderTestComponent({
  412. query: {source: DashboardWidgetSource.DISCOVERV2},
  413. });
  414. const customWidgetLabels = await screen.findByText('Custom Widget');
  415. // EditableText and chart title
  416. expect(customWidgetLabels).toBeInTheDocument();
  417. await userEvent.clear(screen.getByRole('textbox', {name: 'Widget title'}));
  418. await userEvent.click(screen.getByRole('textbox', {name: 'Widget title'}));
  419. await userEvent.paste('Unique Users');
  420. await userEvent.keyboard('{enter}');
  421. expect(screen.queryByText('Custom Widget')).not.toBeInTheDocument();
  422. expect(screen.getByText('Unique Users')).toBeInTheDocument();
  423. });
  424. it('can add query conditions', async function () {
  425. const {router} = renderTestComponent({
  426. query: {source: DashboardWidgetSource.DISCOVERV2},
  427. dashboard: testDashboard,
  428. });
  429. await userEvent.click(
  430. await screen.findByRole('combobox', {name: 'Add a search term'})
  431. );
  432. await userEvent.paste('color:blue');
  433. await userEvent.keyboard('{enter}');
  434. await userEvent.click(screen.getByText('Add Widget'));
  435. await waitFor(() => {
  436. expect(router.push).toHaveBeenCalledWith(
  437. expect.objectContaining({
  438. pathname: '/organizations/org-slug/dashboard/2/',
  439. query: {
  440. displayType: 'table',
  441. interval: '5m',
  442. title: 'Custom Widget',
  443. queryNames: [''],
  444. queryConditions: ['color:blue'],
  445. queryFields: ['count()'],
  446. queryOrderby: '-count()',
  447. start: null,
  448. end: null,
  449. statsPeriod: '24h',
  450. utc: null,
  451. project: [],
  452. environment: [],
  453. widgetType: 'discover',
  454. },
  455. })
  456. );
  457. });
  458. });
  459. it('can choose a field', async function () {
  460. const {router} = renderTestComponent({
  461. query: {source: DashboardWidgetSource.DISCOVERV2},
  462. dashboard: testDashboard,
  463. });
  464. expect(await screen.findByText('Custom Widget')).toBeInTheDocument();
  465. // No delete button as there is only one query.
  466. expect(screen.queryByLabelText('Remove query')).not.toBeInTheDocument();
  467. // 1 in the table header, 1 in the column selector, 1 in the sort field
  468. const countFields = screen.getAllByText('count()');
  469. expect(countFields).toHaveLength(3);
  470. await selectEvent.select(countFields[1], ['last_seen()']);
  471. await userEvent.click(screen.getByText('Add Widget'));
  472. await waitFor(() => {
  473. expect(router.push).toHaveBeenCalledWith(
  474. expect.objectContaining({
  475. pathname: '/organizations/org-slug/dashboard/2/',
  476. query: {
  477. displayType: 'table',
  478. interval: '5m',
  479. title: 'Custom Widget',
  480. queryNames: [''],
  481. queryConditions: [''],
  482. queryFields: ['last_seen()'],
  483. queryOrderby: '-last_seen()',
  484. start: null,
  485. end: null,
  486. statsPeriod: '24h',
  487. utc: null,
  488. project: [],
  489. environment: [],
  490. widgetType: 'discover',
  491. },
  492. })
  493. );
  494. });
  495. });
  496. it('can add additional fields', async function () {
  497. const handleSave = jest.fn();
  498. renderTestComponent({onSave: handleSave});
  499. await userEvent.click(await screen.findByText('Table'));
  500. // Select line chart display
  501. await userEvent.click(screen.getByText('Line Chart'));
  502. // Click the Add Series button
  503. await userEvent.click(screen.getByLabelText('Add Series'));
  504. await selectEvent.select(screen.getByText('(Required)'), ['count_unique(…)']);
  505. await userEvent.click(screen.getByLabelText('Add Widget'));
  506. await waitFor(() => {
  507. expect(handleSave).toHaveBeenCalledWith([
  508. expect.objectContaining({
  509. title: 'Custom Widget',
  510. displayType: DisplayType.LINE,
  511. interval: '5m',
  512. widgetType: WidgetType.DISCOVER,
  513. queries: [
  514. {
  515. conditions: '',
  516. fields: ['count()', 'count_unique(user)'],
  517. aggregates: ['count()', 'count_unique(user)'],
  518. fieldAliases: [],
  519. columns: [],
  520. orderby: '',
  521. name: '',
  522. },
  523. ],
  524. }),
  525. ]);
  526. });
  527. expect(handleSave).toHaveBeenCalledTimes(1);
  528. });
  529. it('can add additional fields and equation for Big Number with selection', async function () {
  530. renderTestComponent({
  531. query: {
  532. displayType: DisplayType.BIG_NUMBER,
  533. },
  534. orgFeatures: [...defaultOrgFeatures, 'dashboards-bignumber-equations'],
  535. });
  536. // Add new field
  537. await userEvent.click(screen.getByLabelText('Add Field'));
  538. expect(screen.getByText('(Required)')).toBeInTheDocument();
  539. await selectEvent.select(screen.getByText('(Required)'), ['count_unique(…)']);
  540. expect(screen.getByRole('radio', {name: 'field1'})).toBeChecked();
  541. // Add another new field
  542. await userEvent.click(screen.getByLabelText('Add Field'));
  543. expect(screen.getByText('(Required)')).toBeInTheDocument();
  544. await selectEvent.select(screen.getByText('(Required)'), ['eps()']);
  545. expect(screen.getByRole('radio', {name: 'field2'})).toBeChecked();
  546. // Add an equation
  547. await userEvent.click(screen.getByLabelText('Add an Equation'));
  548. expect(screen.getByPlaceholderText('Equation')).toBeInTheDocument();
  549. expect(screen.getByRole('radio', {name: 'field3'})).toBeChecked();
  550. await userEvent.click(screen.getByPlaceholderText('Equation'));
  551. await userEvent.paste('eps() + 100');
  552. // Check if right value is displayed from equation
  553. await userEvent.click(screen.getByPlaceholderText('Equation'));
  554. await userEvent.paste('2 * 100');
  555. expect(screen.getByText('200')).toBeInTheDocument();
  556. });
  557. it('can add equation fields', async function () {
  558. const handleSave = jest.fn();
  559. renderTestComponent({onSave: handleSave});
  560. await userEvent.click(await screen.findByText('Table'));
  561. // Select line chart display
  562. await userEvent.click(screen.getByText('Line Chart'));
  563. // Click the add an equation button
  564. await userEvent.click(screen.getByLabelText('Add an Equation'));
  565. expect(screen.getByPlaceholderText('Equation')).toBeInTheDocument();
  566. await userEvent.click(screen.getByPlaceholderText('Equation'));
  567. await userEvent.paste('count() + 100');
  568. await userEvent.click(screen.getByLabelText('Add Widget'));
  569. await waitFor(() => {
  570. expect(handleSave).toHaveBeenCalledWith([
  571. expect.objectContaining({
  572. title: 'Custom Widget',
  573. displayType: DisplayType.LINE,
  574. interval: '5m',
  575. widgetType: WidgetType.DISCOVER,
  576. queries: [
  577. {
  578. name: '',
  579. fields: ['count()', 'equation|count() + 100'],
  580. aggregates: ['count()', 'equation|count() + 100'],
  581. columns: [],
  582. fieldAliases: [],
  583. conditions: '',
  584. orderby: '',
  585. },
  586. ],
  587. }),
  588. ]);
  589. });
  590. expect(handleSave).toHaveBeenCalledTimes(1);
  591. });
  592. it('can respond to validation feedback', async function () {
  593. MockApiClient.addMockResponse({
  594. url: '/organizations/org-slug/dashboards/widgets/',
  595. method: 'POST',
  596. statusCode: 400,
  597. body: {
  598. title: ['This field may not be blank.'],
  599. },
  600. });
  601. renderTestComponent();
  602. await userEvent.click(await screen.findByText('Table'));
  603. const customWidgetLabels = await screen.findByText('Custom Widget');
  604. // EditableText and chart title
  605. expect(customWidgetLabels).toBeInTheDocument();
  606. await userEvent.clear(screen.getByRole('textbox', {name: 'Widget title'}));
  607. await userEvent.click(screen.getByText('Add Widget'));
  608. await screen.findByText('This field may not be blank.');
  609. });
  610. it('sets up widget data in edit correctly', async function () {
  611. const widget: Widget = {
  612. id: '1',
  613. title: 'Errors over time',
  614. interval: '5m',
  615. displayType: DisplayType.LINE,
  616. queries: [
  617. {
  618. name: 'errors',
  619. conditions: 'event.type:error',
  620. fields: ['count()', 'count_unique(id)'],
  621. aggregates: ['count()', 'count_unique(id)'],
  622. columns: [],
  623. orderby: '',
  624. },
  625. {
  626. name: 'csp',
  627. conditions: 'event.type:csp',
  628. fields: ['count()', 'count_unique(id)'],
  629. aggregates: ['count()', 'count_unique(id)'],
  630. columns: [],
  631. orderby: '',
  632. },
  633. ],
  634. };
  635. const dashboard = mockDashboard({widgets: [widget]});
  636. renderTestComponent({dashboard, params: {widgetIndex: '0'}});
  637. await screen.findByText('Line Chart');
  638. // Should be in edit 'mode'
  639. expect(await screen.findByText('Update Widget')).toBeInTheDocument();
  640. // Should set widget data up.
  641. expect(screen.getByText('Update Widget')).toBeInTheDocument();
  642. // Filters
  643. expect(
  644. await screen.findAllByRole('grid', {name: 'Create a search query'})
  645. ).toHaveLength(2);
  646. expect(screen.getByRole('row', {name: 'event.type:csp'})).toBeInTheDocument();
  647. expect(screen.getByRole('row', {name: 'event.type:error'})).toBeInTheDocument();
  648. // Y-axis
  649. expect(screen.getAllByRole('button', {name: 'Remove query'})).toHaveLength(2);
  650. expect(screen.getByText('count()')).toBeInTheDocument();
  651. expect(screen.getByText('count_unique(…)')).toBeInTheDocument();
  652. expect(screen.getByText('id')).toBeInTheDocument();
  653. // Expect events-stats endpoint to be called for each search conditions with
  654. // the same y-axis parameters
  655. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  656. 1,
  657. '/organizations/org-slug/events-stats/',
  658. expect.objectContaining({
  659. query: expect.objectContaining({
  660. query: 'event.type:error',
  661. yAxis: ['count()', 'count_unique(id)'],
  662. }),
  663. })
  664. );
  665. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  666. 2,
  667. '/organizations/org-slug/events-stats/',
  668. expect.objectContaining({
  669. query: expect.objectContaining({
  670. query: 'event.type:csp',
  671. yAxis: ['count()', 'count_unique(id)'],
  672. }),
  673. })
  674. );
  675. });
  676. it('can edit a widget', async function () {
  677. const widget: Widget = {
  678. id: '1',
  679. title: 'Errors over time',
  680. interval: '5m',
  681. displayType: DisplayType.LINE,
  682. queries: [
  683. {
  684. name: 'errors',
  685. conditions: 'event.type:error',
  686. fields: ['count()', 'count_unique(id)'],
  687. aggregates: ['count()', 'count_unique(id)'],
  688. columns: [],
  689. orderby: '',
  690. },
  691. {
  692. name: 'csp',
  693. conditions: 'event.type:csp',
  694. fields: ['count()', 'count_unique(id)'],
  695. aggregates: ['count()', 'count_unique(id)'],
  696. columns: [],
  697. orderby: '',
  698. },
  699. ],
  700. };
  701. const dashboard = mockDashboard({widgets: [widget]});
  702. const handleSave = jest.fn();
  703. renderTestComponent({onSave: handleSave, dashboard, params: {widgetIndex: '0'}});
  704. await screen.findByText('Line Chart');
  705. // Should be in edit 'mode'
  706. expect(screen.getByText('Update Widget')).toBeInTheDocument();
  707. const customWidgetLabels = screen.getByText(widget.title);
  708. // EditableText and chart title
  709. expect(customWidgetLabels).toBeInTheDocument();
  710. await userEvent.clear(screen.getByRole('textbox', {name: 'Widget title'}));
  711. await userEvent.click(screen.getByRole('textbox', {name: 'Widget title'}));
  712. await userEvent.paste('New Title');
  713. await userEvent.click(screen.getByRole('button', {name: 'Update Widget'}));
  714. await waitFor(() => {
  715. expect(handleSave).toHaveBeenCalledWith([
  716. expect.objectContaining({
  717. ...widget,
  718. title: 'New Title',
  719. }),
  720. ]);
  721. });
  722. expect(handleSave).toHaveBeenCalledTimes(1);
  723. });
  724. it('renders column inputs for table widgets', async function () {
  725. const widget: Widget = {
  726. id: '0',
  727. title: 'sdk usage',
  728. interval: '5m',
  729. displayType: DisplayType.TABLE,
  730. queries: [
  731. {
  732. name: 'errors',
  733. conditions: 'event.type:error',
  734. fields: ['sdk.name', 'count()'],
  735. columns: ['sdk.name'],
  736. aggregates: ['count()'],
  737. orderby: '',
  738. },
  739. ],
  740. };
  741. const dashboard = mockDashboard({widgets: [widget]});
  742. renderTestComponent({dashboard, params: {widgetIndex: '0'}});
  743. // Should be in edit 'mode'
  744. expect(await screen.findByText('Update Widget')).toBeInTheDocument();
  745. // Should set widget data up.
  746. expect(screen.getByText(widget.title)).toBeInTheDocument();
  747. expect(screen.getByText('Table')).toBeInTheDocument();
  748. await screen.findByRole('grid', {name: 'Create a search query'});
  749. // Should have an orderby select
  750. expect(screen.getByText('Sort by a column')).toBeInTheDocument();
  751. // Add a column, and choose a value,
  752. expect(screen.getByLabelText('Add a Column')).toBeInTheDocument();
  753. });
  754. it('can save table widgets', async function () {
  755. const widget: Widget = {
  756. id: '0',
  757. title: 'sdk usage',
  758. interval: '5m',
  759. displayType: DisplayType.TABLE,
  760. queries: [
  761. {
  762. name: 'errors',
  763. conditions: 'event.type:error',
  764. fields: ['sdk.name', 'count()'],
  765. columns: ['sdk.name'],
  766. aggregates: ['count()'],
  767. orderby: '-count()',
  768. },
  769. ],
  770. };
  771. const dashboard = mockDashboard({widgets: [widget]});
  772. const handleSave = jest.fn();
  773. renderTestComponent({dashboard, onSave: handleSave, params: {widgetIndex: '0'}});
  774. // Should be in edit 'mode'
  775. expect(await screen.findByText('Update Widget')).toBeInTheDocument();
  776. // Add a column, and choose a value,
  777. await userEvent.click(screen.getByLabelText('Add a Column'));
  778. await selectEvent.select(screen.getByText('(Required)'), 'trace');
  779. // Save widget
  780. await userEvent.click(screen.getByLabelText('Update Widget'));
  781. await waitFor(() => {
  782. expect(handleSave).toHaveBeenCalledWith([
  783. expect.objectContaining({
  784. id: '0',
  785. title: 'sdk usage',
  786. displayType: DisplayType.TABLE,
  787. interval: '5m',
  788. queries: [
  789. {
  790. name: 'errors',
  791. conditions: 'event.type:error',
  792. fields: ['sdk.name', 'count()', 'trace'],
  793. aggregates: ['count()'],
  794. columns: ['sdk.name', 'trace'],
  795. orderby: '-count()',
  796. fieldAliases: ['', '', ''],
  797. },
  798. ],
  799. widgetType: WidgetType.DISCOVER,
  800. }),
  801. ]);
  802. });
  803. expect(handleSave).toHaveBeenCalledTimes(1);
  804. });
  805. it('should properly query for table fields', async function () {
  806. const defaultWidgetQuery = {
  807. name: '',
  808. fields: ['title', 'count()'],
  809. columns: ['title'],
  810. aggregates: ['count()'],
  811. conditions: '',
  812. orderby: '',
  813. };
  814. const defaultTableColumns = ['title', 'count()', 'count_unique(user)', 'epm()'];
  815. renderTestComponent({
  816. query: {
  817. source: DashboardWidgetSource.DISCOVERV2,
  818. defaultWidgetQuery: urlEncode(defaultWidgetQuery),
  819. displayType: DisplayType.LINE,
  820. defaultTableColumns,
  821. },
  822. });
  823. expect(await screen.findByText('Line Chart')).toBeInTheDocument();
  824. await userEvent.click(screen.getByText('Line Chart'));
  825. await userEvent.click(screen.getByText('Table'));
  826. await waitFor(() => {
  827. expect(eventsMock).toHaveBeenLastCalledWith(
  828. '/organizations/org-slug/events/',
  829. expect.objectContaining({
  830. query: expect.objectContaining({
  831. field: defaultTableColumns,
  832. }),
  833. })
  834. );
  835. });
  836. });
  837. it('should use defaultWidgetQuery Y-Axis and Conditions if given a defaultWidgetQuery', async function () {
  838. const defaultWidgetQuery = {
  839. name: '',
  840. fields: ['count()', 'failure_count()', 'count_unique(user)'],
  841. columns: [],
  842. aggregates: ['count()', 'failure_count()', 'count_unique(user)'],
  843. conditions: 'tag:value',
  844. orderby: '',
  845. };
  846. renderTestComponent({
  847. query: {
  848. source: DashboardWidgetSource.DISCOVERV2,
  849. defaultWidgetQuery: urlEncode(defaultWidgetQuery),
  850. },
  851. });
  852. expect(await screen.findByRole('row', {name: 'tag:value'})).toBeInTheDocument();
  853. // Table display, column, and sort field
  854. await waitFor(() => {
  855. expect(screen.getAllByText('count()')).toHaveLength(3);
  856. });
  857. // Table display and column
  858. expect(screen.getAllByText('failure_count()')).toHaveLength(2);
  859. // Table display
  860. expect(screen.getByText('count_unique(user)')).toBeInTheDocument();
  861. // Column
  862. expect(screen.getByText('count_unique(…)')).toBeInTheDocument();
  863. // Column
  864. expect(screen.getByText('user')).toBeInTheDocument();
  865. });
  866. it('uses displayType if given a displayType', async function () {
  867. renderTestComponent({
  868. query: {
  869. displayType: DisplayType.BAR,
  870. },
  871. });
  872. expect(await screen.findByText('Bar Chart')).toBeInTheDocument();
  873. });
  874. it('deletes the widget when the modal is confirmed', async () => {
  875. const handleSave = jest.fn();
  876. const widget: Widget = {
  877. id: '1',
  878. title: 'Errors over time',
  879. interval: '5m',
  880. displayType: DisplayType.LINE,
  881. queries: [
  882. {
  883. name: 'errors',
  884. conditions: 'event.type:error',
  885. fields: ['count()', 'count_unique(id)'],
  886. aggregates: ['count()', 'count_unique(id)'],
  887. columns: [],
  888. orderby: '',
  889. },
  890. {
  891. name: 'csp',
  892. conditions: 'event.type:csp',
  893. fields: ['count()', 'count_unique(id)'],
  894. aggregates: ['count()', 'count_unique(id)'],
  895. columns: [],
  896. orderby: '',
  897. },
  898. ],
  899. };
  900. const dashboard = mockDashboard({widgets: [widget]});
  901. renderTestComponent({onSave: handleSave, dashboard, params: {widgetIndex: '0'}});
  902. await userEvent.click(await screen.findByText('Delete'));
  903. renderGlobalModal();
  904. await userEvent.click(await screen.findByText('Confirm'));
  905. await waitFor(() => {
  906. // The only widget was deleted
  907. expect(handleSave).toHaveBeenCalledWith([]);
  908. });
  909. expect(handleSave).toHaveBeenCalledTimes(1);
  910. });
  911. it('persists the page filter period when updating a widget', async () => {
  912. const widget: Widget = {
  913. id: '1',
  914. title: 'Errors over time',
  915. interval: '5m',
  916. displayType: DisplayType.LINE,
  917. queries: [
  918. {
  919. name: 'errors',
  920. conditions: 'event.type:error',
  921. fields: ['count()', 'count_unique(id)'],
  922. aggregates: ['count()', 'count_unique(id)'],
  923. columns: [],
  924. orderby: '',
  925. },
  926. ],
  927. };
  928. const dashboard = mockDashboard({widgets: [widget]});
  929. const {router} = renderTestComponent({
  930. dashboard,
  931. params: {orgId: 'org-slug', widgetIndex: '0'},
  932. query: {statsPeriod: '90d'},
  933. });
  934. await userEvent.click(screen.getByText('Update Widget'));
  935. await waitFor(() => {
  936. expect(router.push).toHaveBeenLastCalledWith(
  937. expect.objectContaining({
  938. pathname: '/organizations/org-slug/dashboard/1/',
  939. query: expect.objectContaining({
  940. statsPeriod: '90d',
  941. }),
  942. })
  943. );
  944. });
  945. });
  946. it('renders page filters in the filter step', async () => {
  947. const mockReleases = MockApiClient.addMockResponse({
  948. url: '/organizations/org-slug/releases/',
  949. body: [ReleaseFixture()],
  950. });
  951. renderTestComponent({
  952. params: {orgId: 'org-slug'},
  953. query: {statsPeriod: '90d'},
  954. orgFeatures: defaultOrgFeatures,
  955. });
  956. expect(await screen.findByTestId('page-filter-timerange-selector')).toBeDisabled();
  957. expect(screen.getByTestId('page-filter-environment-selector')).toBeDisabled();
  958. expect(screen.getByTestId('page-filter-project-selector')).toBeDisabled();
  959. expect(mockReleases).toHaveBeenCalled();
  960. expect(screen.getByRole('button', {name: /all releases/i})).toBeDisabled();
  961. });
  962. it('appends dashboard filters to widget builder fetch data request', async () => {
  963. MockApiClient.addMockResponse({
  964. url: '/organizations/org-slug/releases/',
  965. body: [ReleaseFixture()],
  966. });
  967. const mock = MockApiClient.addMockResponse({
  968. url: '/organizations/org-slug/events/',
  969. body: [],
  970. });
  971. renderTestComponent({
  972. dashboard: {
  973. id: 'new',
  974. title: 'Dashboard',
  975. createdBy: undefined,
  976. dateCreated: '2020-01-01T00:00:00.000Z',
  977. widgets: [],
  978. projects: [],
  979. filters: {release: ['abc@1.2.0']},
  980. },
  981. params: {orgId: 'org-slug'},
  982. query: {statsPeriod: '90d'},
  983. orgFeatures: defaultOrgFeatures,
  984. });
  985. await waitFor(() => {
  986. expect(mock).toHaveBeenCalledWith(
  987. '/organizations/org-slug/events/',
  988. expect.objectContaining({
  989. query: expect.objectContaining({
  990. query: ' release:"abc@1.2.0" ',
  991. }),
  992. })
  993. );
  994. });
  995. });
  996. it('does not error when query conditions field is blurred', async function () {
  997. const widget: Widget = {
  998. id: '0',
  999. title: 'sdk usage',
  1000. interval: '5m',
  1001. displayType: DisplayType.BAR,
  1002. queries: [
  1003. {
  1004. name: 'filled in',
  1005. conditions: 'event.type:error',
  1006. fields: ['count()', 'count_unique(id)'],
  1007. aggregates: ['count()', 'count_unique(id)'],
  1008. columns: [],
  1009. orderby: '-count()',
  1010. },
  1011. ],
  1012. };
  1013. const dashboard = mockDashboard({widgets: [widget]});
  1014. const handleSave = jest.fn();
  1015. renderTestComponent({dashboard, onSave: handleSave, params: {widgetIndex: '0'}});
  1016. await userEvent.click(await screen.findByLabelText('Add Query'), {delay: null});
  1017. // Triggering the onBlur of the new field should not error
  1018. await userEvent.click(
  1019. screen.getAllByPlaceholderText('Search for events, users, tags, and more')[1],
  1020. {delay: null}
  1021. );
  1022. await userEvent.keyboard('{Escape}', {delay: null});
  1023. // Run all timers because the handleBlur contains a setTimeout
  1024. await act(tick);
  1025. });
  1026. it('does not wipe column changes when filters are modified', async function () {
  1027. // widgetIndex: undefined means creating a new widget
  1028. renderTestComponent({params: {widgetIndex: undefined}});
  1029. await userEvent.click(await screen.findByLabelText('Add a Column'), {delay: null});
  1030. await selectEvent.select(screen.getByText('(Required)'), /project/);
  1031. // Triggering the onBlur of the filter should not error
  1032. await userEvent.click(
  1033. screen.getByPlaceholderText('Search for events, users, tags, and more'),
  1034. {delay: null}
  1035. );
  1036. await userEvent.keyboard('{enter}', {delay: null});
  1037. expect(await screen.findAllByText('project')).toHaveLength(2);
  1038. });
  1039. it('renders fields with commas properly', async () => {
  1040. const defaultWidgetQuery = {
  1041. conditions: '',
  1042. fields: ['equation|count_if(transaction.duration,equals,300)*2'],
  1043. aggregates: ['equation|count_if(transaction.duration,equals,300)*2'],
  1044. columns: [],
  1045. orderby: '',
  1046. name: '',
  1047. };
  1048. const defaultTableColumns = [
  1049. 'count_if(transaction.duration,equals,300)',
  1050. 'equation|count_if(transaction.duration,equals,300)*2',
  1051. ];
  1052. renderTestComponent({
  1053. query: {
  1054. source: DashboardWidgetSource.DISCOVERV2,
  1055. defaultWidgetQuery: urlEncode(defaultWidgetQuery),
  1056. defaultTableColumns,
  1057. yAxis: ['equation|count_if(transaction.duration,equals,300)*2'],
  1058. },
  1059. });
  1060. expect(
  1061. await screen.findByText('count_if(transaction.duration,equals,300)*2')
  1062. ).toBeInTheDocument();
  1063. });
  1064. it('sets the correct fields for a top n widget', async () => {
  1065. renderTestComponent({
  1066. orgFeatures: [...defaultOrgFeatures, 'performance-view'],
  1067. query: {
  1068. displayType: DisplayType.TOP_N,
  1069. },
  1070. });
  1071. // Top N now opens as Area Chart
  1072. expect(await screen.findByText('Area Chart')).toBeInTheDocument();
  1073. // Add a group by
  1074. await userEvent.click(screen.getByText('Add Series'));
  1075. await selectEvent.select(screen.getByText('Select group'), /project/);
  1076. // Change the y-axis
  1077. await selectEvent.select(screen.getAllByText('count()')[0], 'eps()');
  1078. await waitFor(() => {
  1079. expect(eventsStatsMock).toHaveBeenLastCalledWith(
  1080. '/organizations/org-slug/events-stats/',
  1081. expect.objectContaining({
  1082. query: expect.objectContaining({
  1083. query: '',
  1084. yAxis: ['eps()'],
  1085. field: ['project', 'eps()'],
  1086. topEvents: TOP_N,
  1087. orderby: '-eps()',
  1088. }),
  1089. })
  1090. );
  1091. });
  1092. });
  1093. it('fetches tags when tag store is empty', async function () {
  1094. renderTestComponent();
  1095. await waitFor(() => {
  1096. expect(tagsMock).toHaveBeenCalled();
  1097. });
  1098. });
  1099. it('does not fetch tags when tag store is not empty', async function () {
  1100. await act(async () => {
  1101. TagStore.loadTagsSuccess(TagsFixture());
  1102. renderTestComponent();
  1103. await tick();
  1104. });
  1105. expect(tagsMock).not.toHaveBeenCalled();
  1106. });
  1107. it('excludes the Other series when grouping and using multiple y-axes', async function () {
  1108. renderTestComponent({
  1109. orgFeatures: [...defaultOrgFeatures],
  1110. query: {
  1111. displayType: DisplayType.LINE,
  1112. },
  1113. });
  1114. await selectEvent.select(await screen.findByText('Select group'), 'project');
  1115. await userEvent.click(screen.getByText('Add Series'));
  1116. await selectEvent.select(screen.getByText('(Required)'), /count_unique/);
  1117. await waitFor(() => {
  1118. expect(eventsStatsMock).toHaveBeenCalledWith(
  1119. '/organizations/org-slug/events-stats/',
  1120. expect.objectContaining({
  1121. query: expect.objectContaining({excludeOther: '1'}),
  1122. })
  1123. );
  1124. });
  1125. });
  1126. it('excludes the Other series when grouping and using multiple queries', async function () {
  1127. renderTestComponent({
  1128. orgFeatures: [...defaultOrgFeatures],
  1129. query: {
  1130. displayType: DisplayType.LINE,
  1131. },
  1132. });
  1133. await selectEvent.select(await screen.findByText('Select group'), 'project');
  1134. await userEvent.click(screen.getByText('Add Query'));
  1135. await waitFor(() => {
  1136. expect(eventsStatsMock).toHaveBeenCalledWith(
  1137. '/organizations/org-slug/events-stats/',
  1138. expect.objectContaining({
  1139. query: expect.objectContaining({excludeOther: '1'}),
  1140. })
  1141. );
  1142. });
  1143. });
  1144. it('includes Other series when there is only one query and one y-axis', async function () {
  1145. renderTestComponent({
  1146. orgFeatures: [...defaultOrgFeatures],
  1147. query: {
  1148. displayType: DisplayType.LINE,
  1149. },
  1150. });
  1151. await selectEvent.select(await screen.findByText('Select group'), 'project');
  1152. await waitFor(() => {
  1153. expect(eventsStatsMock).toHaveBeenCalledWith(
  1154. '/organizations/org-slug/events-stats/',
  1155. expect.objectContaining({
  1156. query: expect.not.objectContaining({excludeOther: '1'}),
  1157. })
  1158. );
  1159. });
  1160. });
  1161. it('decreases the limit when more y-axes and queries are added', async function () {
  1162. renderTestComponent({
  1163. orgFeatures: [...defaultOrgFeatures],
  1164. query: {
  1165. displayType: DisplayType.LINE,
  1166. },
  1167. });
  1168. await selectEvent.select(await screen.findByText('Select group'), 'project');
  1169. screen.getByText('Limit to 5 results');
  1170. await userEvent.click(screen.getByText('Add Query'));
  1171. await userEvent.click(screen.getByText('Add Series'));
  1172. expect(screen.getByText('Limit to 2 results')).toBeInTheDocument();
  1173. });
  1174. it('alerts the user if there are unsaved title changes', async function () {
  1175. renderTestComponent();
  1176. window.confirm = jest.fn();
  1177. const customWidgetLabels = await screen.findByText('Custom Widget');
  1178. // EditableText and chart title
  1179. expect(customWidgetLabels).toBeInTheDocument();
  1180. // Change title text
  1181. await userEvent.clear(screen.getByRole('textbox', {name: 'Widget title'}));
  1182. await userEvent.click(screen.getByRole('textbox', {name: 'Widget title'}));
  1183. await userEvent.paste('Unique Users');
  1184. await userEvent.keyboard('{Enter}');
  1185. // Click Cancel
  1186. await userEvent.click(screen.getByText('Cancel'));
  1187. // Assert an alert was triggered
  1188. expect(window.confirm).toHaveBeenCalled();
  1189. });
  1190. it('alerts the user if there are unsaved description changes', async function () {
  1191. renderTestComponent();
  1192. window.confirm = jest.fn();
  1193. const descriptionTextArea = await screen.findByRole('textbox', {
  1194. name: 'Widget Description',
  1195. });
  1196. expect(descriptionTextArea).toBeInTheDocument();
  1197. expect(descriptionTextArea).toHaveAttribute(
  1198. 'placeholder',
  1199. 'Enter description (Optional)'
  1200. );
  1201. // Change description text
  1202. await userEvent.clear(descriptionTextArea);
  1203. await userEvent.click(descriptionTextArea);
  1204. await userEvent.paste('This is a description');
  1205. await userEvent.keyboard('{Enter}');
  1206. // Click Cancel
  1207. await userEvent.click(screen.getByText('Cancel'));
  1208. // Assert an alert was triggered
  1209. expect(window.confirm).toHaveBeenCalled();
  1210. });
  1211. it('does not trigger alert dialog if no changes', async function () {
  1212. renderTestComponent();
  1213. const alertMock = jest.spyOn(window, 'confirm');
  1214. await userEvent.click(await screen.findByText('Cancel'));
  1215. expect(alertMock).not.toHaveBeenCalled();
  1216. });
  1217. describe('Widget creation coming from other verticals', function () {
  1218. it('redirects correctly when creating a new dashboard', async function () {
  1219. const {router} = renderTestComponent({
  1220. query: {source: DashboardWidgetSource.DISCOVERV2},
  1221. });
  1222. await userEvent.click(await screen.findByText('Add Widget'));
  1223. await waitFor(() => {
  1224. expect(router.push).toHaveBeenCalledWith(
  1225. expect.objectContaining({
  1226. pathname: '/organizations/org-slug/dashboards/new/',
  1227. query: {
  1228. displayType: 'table',
  1229. interval: '5m',
  1230. title: 'Custom Widget',
  1231. queryNames: [''],
  1232. queryConditions: [''],
  1233. queryFields: ['count()'],
  1234. queryOrderby: '-count()',
  1235. start: null,
  1236. end: null,
  1237. statsPeriod: '24h',
  1238. utc: null,
  1239. project: [],
  1240. environment: [],
  1241. widgetType: 'discover',
  1242. },
  1243. })
  1244. );
  1245. });
  1246. });
  1247. it('redirects correctly when choosing an existing dashboard', async function () {
  1248. const {router} = renderTestComponent({
  1249. query: {source: DashboardWidgetSource.DISCOVERV2},
  1250. dashboard: testDashboard,
  1251. });
  1252. await userEvent.click(await screen.findByText('Add Widget'));
  1253. await waitFor(() => {
  1254. expect(router.push).toHaveBeenCalledWith(
  1255. expect.objectContaining({
  1256. pathname: '/organizations/org-slug/dashboard/2/',
  1257. query: {
  1258. displayType: 'table',
  1259. interval: '5m',
  1260. title: 'Custom Widget',
  1261. queryNames: [''],
  1262. queryConditions: [''],
  1263. queryFields: ['count()'],
  1264. queryOrderby: '-count()',
  1265. start: null,
  1266. end: null,
  1267. statsPeriod: '24h',
  1268. utc: null,
  1269. project: [],
  1270. environment: [],
  1271. widgetType: 'discover',
  1272. },
  1273. })
  1274. );
  1275. });
  1276. });
  1277. it('shows the correct orderby when switching from a line chart to table', async function () {
  1278. const defaultWidgetQuery = {
  1279. name: '',
  1280. fields: ['count_unique(user)'],
  1281. columns: [],
  1282. aggregates: ['count_unique(user)'],
  1283. conditions: '',
  1284. orderby: 'count_unique(user)',
  1285. };
  1286. const defaultTableColumns = ['title', 'count_unique(user)'];
  1287. renderTestComponent({
  1288. orgFeatures: [...defaultOrgFeatures],
  1289. query: {
  1290. source: DashboardWidgetSource.DISCOVERV2,
  1291. defaultWidgetQuery: urlEncode(defaultWidgetQuery),
  1292. displayType: DisplayType.LINE,
  1293. defaultTableColumns,
  1294. },
  1295. });
  1296. await userEvent.click(await screen.findByText('Line Chart'));
  1297. await userEvent.click(screen.getByText('Table'));
  1298. expect(screen.getAllByText('count_unique(user)')[0]).toBeInTheDocument();
  1299. await waitFor(() => {
  1300. expect(eventsMock).toHaveBeenLastCalledWith(
  1301. '/organizations/org-slug/events/',
  1302. expect.objectContaining({
  1303. query: expect.objectContaining({
  1304. field: defaultTableColumns,
  1305. sort: ['count_unique(user)'],
  1306. }),
  1307. })
  1308. );
  1309. });
  1310. });
  1311. it('does not send request with orderby if a timeseries chart without grouping', async function () {
  1312. const defaultWidgetQuery = {
  1313. name: '',
  1314. fields: ['count_unique(user)'],
  1315. columns: [],
  1316. aggregates: ['count_unique(user)'],
  1317. conditions: '',
  1318. orderby: 'count_unique(user)',
  1319. };
  1320. const defaultTableColumns = ['title', 'count_unique(user)'];
  1321. renderTestComponent({
  1322. orgFeatures: [...defaultOrgFeatures],
  1323. query: {
  1324. source: DashboardWidgetSource.DISCOVERV2,
  1325. defaultWidgetQuery: urlEncode(defaultWidgetQuery),
  1326. displayType: DisplayType.LINE,
  1327. defaultTableColumns,
  1328. },
  1329. });
  1330. await waitFor(() => {
  1331. expect(eventsStatsMock).toHaveBeenLastCalledWith(
  1332. '/organizations/org-slug/events-stats/',
  1333. expect.objectContaining({
  1334. query: expect.objectContaining({
  1335. orderby: '',
  1336. }),
  1337. })
  1338. );
  1339. });
  1340. });
  1341. });
  1342. it('opens top-N widgets as area display', async function () {
  1343. const widget: Widget = {
  1344. id: '1',
  1345. title: 'Errors over time',
  1346. interval: '5m',
  1347. displayType: DisplayType.TOP_N,
  1348. queries: [
  1349. {
  1350. name: '',
  1351. conditions: '',
  1352. fields: ['count()', 'count_unique(id)'],
  1353. aggregates: ['count()', 'count_unique(id)'],
  1354. columns: [],
  1355. orderby: '-count()',
  1356. },
  1357. ],
  1358. };
  1359. const dashboard = mockDashboard({widgets: [widget]});
  1360. renderTestComponent({
  1361. orgFeatures: [...defaultOrgFeatures],
  1362. dashboard,
  1363. params: {
  1364. widgetIndex: '0',
  1365. },
  1366. });
  1367. expect(await screen.findByText('Area Chart')).toBeInTheDocument();
  1368. });
  1369. it('Update table header values (field alias)', async function () {
  1370. const handleSave = jest.fn();
  1371. renderTestComponent({
  1372. onSave: handleSave,
  1373. orgFeatures: [...defaultOrgFeatures],
  1374. });
  1375. await userEvent.click(screen.getByPlaceholderText('Alias'));
  1376. await userEvent.paste('First Alias');
  1377. await userEvent.click(screen.getByLabelText('Add a Column'));
  1378. await userEvent.click(screen.getAllByPlaceholderText('Alias')[1]);
  1379. await userEvent.paste('Second Alias');
  1380. await userEvent.click(screen.getByText('Add Widget'));
  1381. await waitFor(() => {
  1382. expect(handleSave).toHaveBeenCalledWith([
  1383. expect.objectContaining({
  1384. queries: [
  1385. expect.objectContaining({fieldAliases: ['First Alias', 'Second Alias']}),
  1386. ],
  1387. }),
  1388. ]);
  1389. });
  1390. });
  1391. it('does not wipe equation aliases when a column alias is updated', async function () {
  1392. renderTestComponent({
  1393. orgFeatures: [...defaultOrgFeatures],
  1394. });
  1395. await userEvent.click(screen.getByText('Add an Equation'));
  1396. await userEvent.click(screen.getAllByPlaceholderText('Alias')[1]);
  1397. await userEvent.paste('This should persist');
  1398. await userEvent.type(screen.getAllByPlaceholderText('Alias')[0], 'A');
  1399. expect(await screen.findByText('This should persist')).toBeInTheDocument();
  1400. });
  1401. it('does not wipe equation aliases when a column selection is made', async function () {
  1402. renderTestComponent({
  1403. orgFeatures: [...defaultOrgFeatures],
  1404. });
  1405. await userEvent.click(screen.getByText('Add an Equation'));
  1406. await userEvent.click(screen.getAllByPlaceholderText('Alias')[1]);
  1407. await userEvent.paste('This should persist');
  1408. // 1 for the table, 1 for the column selector, 1 for the sort
  1409. await waitFor(() => expect(screen.getAllByText('count()')).toHaveLength(3));
  1410. await selectEvent.select(screen.getAllByText('count()')[1], /count_unique/);
  1411. expect(screen.getByText('This should persist')).toBeInTheDocument();
  1412. });
  1413. it('copies over the orderby from the previous query if adding another', async function () {
  1414. renderTestComponent({
  1415. orgFeatures: [...defaultOrgFeatures],
  1416. });
  1417. await userEvent.click(await screen.findByText('Table'));
  1418. await userEvent.click(screen.getByText('Line Chart'));
  1419. await selectEvent.select(screen.getByText('Select group'), 'project');
  1420. await selectEvent.select(screen.getAllByText('count()')[1], 'count_unique(…)');
  1421. MockApiClient.clearMockResponses();
  1422. eventsStatsMock = MockApiClient.addMockResponse({
  1423. url: '/organizations/org-slug/events-stats/',
  1424. body: [],
  1425. });
  1426. await userEvent.click(screen.getByText('Add Query'));
  1427. // Assert on two calls, one for each query
  1428. const expectedArgs = expect.objectContaining({
  1429. query: expect.objectContaining({
  1430. orderby: '-count_unique(user)',
  1431. }),
  1432. });
  1433. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  1434. 1,
  1435. '/organizations/org-slug/events-stats/',
  1436. expectedArgs
  1437. );
  1438. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  1439. 2,
  1440. '/organizations/org-slug/events-stats/',
  1441. expectedArgs
  1442. );
  1443. });
  1444. it('disables add widget button and prevents widget previewing from firing widget query if widget query condition is invalid', async function () {
  1445. renderTestComponent({
  1446. orgFeatures: [...defaultOrgFeatures],
  1447. });
  1448. await userEvent.click(await screen.findByText('Table'));
  1449. await userEvent.click(screen.getByText('Line Chart'));
  1450. expect(eventsStatsMock).toHaveBeenCalledTimes(1);
  1451. await userEvent.click(
  1452. await screen.findByRole('combobox', {name: 'Add a search term'})
  1453. );
  1454. await userEvent.paste('transaction.duration:123a');
  1455. // Unfocus input
  1456. await userEvent.click(screen.getByText('Filter your results'));
  1457. expect(screen.getByText('Add Widget').closest('button')).toBeDisabled();
  1458. expect(screen.getByText('Widget query condition is invalid.')).toBeInTheDocument();
  1459. expect(eventsStatsMock).toHaveBeenCalledTimes(1);
  1460. });
  1461. describe('discover dataset split', function () {
  1462. let widget, dashboard;
  1463. describe('events', function () {
  1464. beforeEach(function () {
  1465. widget = {
  1466. displayType: DisplayType.TABLE,
  1467. interval: '1d',
  1468. queries: [
  1469. {
  1470. name: 'Test Widget',
  1471. fields: ['count()', 'count_unique(user)', 'epm()', 'project'],
  1472. columns: ['project'],
  1473. aggregates: ['count()', 'count_unique(user)', 'epm()'],
  1474. conditions: '',
  1475. orderby: '',
  1476. },
  1477. ],
  1478. title: 'Transactions',
  1479. id: '1',
  1480. };
  1481. dashboard = mockDashboard({widgets: [widget]});
  1482. });
  1483. it('selects the error discover split type as the dataset when the events request completes', async function () {
  1484. eventsMock = MockApiClient.addMockResponse({
  1485. url: '/organizations/org-slug/events/',
  1486. method: 'GET',
  1487. statusCode: 200,
  1488. body: {
  1489. meta: {discoverSplitDecision: WidgetType.ERRORS},
  1490. data: [],
  1491. },
  1492. });
  1493. const mockUpdateDashboardSplitDecision = jest.fn();
  1494. renderTestComponent({
  1495. orgFeatures: [...defaultOrgFeatures, 'performance-discover-dataset-selector'],
  1496. dashboard,
  1497. params: {
  1498. widgetIndex: '0',
  1499. },
  1500. updateDashboardSplitDecision: mockUpdateDashboardSplitDecision,
  1501. });
  1502. await waitFor(() => {
  1503. expect(eventsMock).toHaveBeenCalled();
  1504. });
  1505. expect(screen.getByRole('radio', {name: /errors/i})).toBeChecked();
  1506. expect(mockUpdateDashboardSplitDecision).toHaveBeenCalledWith(
  1507. '1',
  1508. WidgetType.ERRORS
  1509. );
  1510. expect(
  1511. await screen.findByText(
  1512. "We're splitting our datasets up to make it a bit easier to digest. We defaulted this widget to Errors. Edit as you see fit."
  1513. )
  1514. ).toBeInTheDocument();
  1515. });
  1516. it('selects the transaction discover split type as the dataset when the events request completes', async function () {
  1517. eventsMock = MockApiClient.addMockResponse({
  1518. url: '/organizations/org-slug/events/',
  1519. method: 'GET',
  1520. statusCode: 200,
  1521. body: {
  1522. meta: {discoverSplitDecision: WidgetType.TRANSACTIONS},
  1523. data: [],
  1524. },
  1525. });
  1526. const mockUpdateDashboardSplitDecision = jest.fn();
  1527. renderTestComponent({
  1528. orgFeatures: [...defaultOrgFeatures, 'performance-discover-dataset-selector'],
  1529. dashboard,
  1530. params: {
  1531. widgetIndex: '0',
  1532. },
  1533. updateDashboardSplitDecision: mockUpdateDashboardSplitDecision,
  1534. });
  1535. await waitFor(() => {
  1536. expect(eventsMock).toHaveBeenCalled();
  1537. });
  1538. expect(screen.getByRole('radio', {name: /transactions/i})).toBeChecked();
  1539. expect(mockUpdateDashboardSplitDecision).toHaveBeenCalledWith(
  1540. '1',
  1541. WidgetType.TRANSACTIONS
  1542. );
  1543. expect(
  1544. await screen.findByText(
  1545. "We're splitting our datasets up to make it a bit easier to digest. We defaulted this widget to Transactions. Edit as you see fit."
  1546. )
  1547. ).toBeInTheDocument();
  1548. });
  1549. it('persists the query state for tables when switching between errors and transactions', async function () {
  1550. dashboard = mockDashboard({
  1551. widgets: [
  1552. WidgetFixture({
  1553. displayType: DisplayType.TABLE,
  1554. widgetType: WidgetType.TRANSACTIONS,
  1555. queries: [
  1556. {
  1557. name: 'Test Widget',
  1558. fields: ['p99(transaction.duration)'],
  1559. columns: [],
  1560. aggregates: ['p99(transaction.duration)'],
  1561. conditions: 'testFilter:value',
  1562. orderby: '',
  1563. },
  1564. ],
  1565. }),
  1566. ],
  1567. });
  1568. renderTestComponent({
  1569. orgFeatures: [...defaultOrgFeatures, 'performance-discover-dataset-selector'],
  1570. dashboard,
  1571. params: {
  1572. widgetIndex: '0',
  1573. },
  1574. });
  1575. expect(await screen.findByText(/p99\(…\)/i)).toBeInTheDocument();
  1576. expect(screen.getByText('transaction.duration')).toBeInTheDocument();
  1577. expect(screen.getByRole('row', {name: 'testFilter:value'})).toBeInTheDocument();
  1578. expect(screen.getByRole('radio', {name: /transactions/i})).toBeChecked();
  1579. // Switch to errors
  1580. await userEvent.click(screen.getByRole('radio', {name: /errors/i}));
  1581. expect(screen.getByRole('radio', {name: /transactions/i})).not.toBeChecked();
  1582. expect(screen.getByRole('radio', {name: /errors/i})).toBeChecked();
  1583. // The state is still the same
  1584. expect(await screen.findByText(/p99\(…\)/i)).toBeInTheDocument();
  1585. expect(screen.getByText('transaction.duration')).toBeInTheDocument();
  1586. expect(screen.getByRole('row', {name: 'testFilter:value'})).toBeInTheDocument();
  1587. });
  1588. it('sets the correct default count_if parameters for the errors dataset', async function () {
  1589. dashboard = mockDashboard({
  1590. widgets: [
  1591. WidgetFixture({
  1592. displayType: DisplayType.TABLE,
  1593. widgetType: WidgetType.ERRORS,
  1594. queries: [
  1595. {
  1596. name: 'Test Widget',
  1597. fields: ['count()'],
  1598. columns: [],
  1599. aggregates: ['count()'],
  1600. conditions: '',
  1601. orderby: '',
  1602. },
  1603. ],
  1604. }),
  1605. ],
  1606. });
  1607. renderTestComponent({
  1608. orgFeatures: [...defaultOrgFeatures, 'performance-discover-dataset-selector'],
  1609. dashboard,
  1610. params: {
  1611. widgetIndex: '0',
  1612. },
  1613. });
  1614. await userEvent.click(await screen.findByTestId('label'));
  1615. await userEvent.click(screen.getByText(/count_if/));
  1616. const fieldLabels = screen.getAllByTestId('label');
  1617. expect(fieldLabels[0]).toHaveTextContent(/count_if/);
  1618. expect(fieldLabels[1]).toHaveTextContent('event.type');
  1619. expect(screen.getByDisplayValue('error')).toBeInTheDocument();
  1620. });
  1621. it('sets the correct default count_if parameters for the transactions dataset', async function () {
  1622. dashboard = mockDashboard({
  1623. widgets: [
  1624. WidgetFixture({
  1625. displayType: DisplayType.TABLE,
  1626. widgetType: WidgetType.TRANSACTIONS,
  1627. queries: [
  1628. {
  1629. name: 'Test Widget',
  1630. fields: ['count()'],
  1631. columns: [],
  1632. aggregates: ['count()'],
  1633. conditions: '',
  1634. orderby: '',
  1635. },
  1636. ],
  1637. }),
  1638. ],
  1639. });
  1640. renderTestComponent({
  1641. orgFeatures: [...defaultOrgFeatures, 'performance-discover-dataset-selector'],
  1642. dashboard,
  1643. params: {
  1644. widgetIndex: '0',
  1645. },
  1646. });
  1647. await userEvent.click(await screen.findByTestId('label'));
  1648. await userEvent.click(screen.getByText(/count_if/));
  1649. const fieldLabels = screen.getAllByTestId('label');
  1650. expect(fieldLabels[0]).toHaveTextContent(/count_if/);
  1651. expect(fieldLabels[1]).toHaveTextContent('transaction.duration');
  1652. expect(screen.getByDisplayValue('300')).toBeInTheDocument();
  1653. });
  1654. });
  1655. describe('events-stats', function () {
  1656. beforeEach(function () {
  1657. widget = {
  1658. displayType: DisplayType.LINE,
  1659. interval: '1d',
  1660. queries: [
  1661. {
  1662. name: 'Test Widget',
  1663. fields: ['count()', 'count_unique(user)', 'epm()', 'project'],
  1664. columns: ['project'],
  1665. aggregates: ['count()', 'count_unique(user)', 'epm()'],
  1666. conditions: '',
  1667. orderby: '',
  1668. },
  1669. ],
  1670. title: 'Transactions',
  1671. id: '1',
  1672. };
  1673. dashboard = mockDashboard({widgets: [widget]});
  1674. });
  1675. it('selects the error discover split type as the dataset when the request completes', async function () {
  1676. eventsStatsMock = MockApiClient.addMockResponse({
  1677. url: '/organizations/org-slug/events-stats/',
  1678. method: 'GET',
  1679. statusCode: 200,
  1680. body: {
  1681. meta: {discoverSplitDecision: WidgetType.ERRORS},
  1682. data: [],
  1683. },
  1684. });
  1685. const mockUpdateDashboardSplitDecision = jest.fn();
  1686. renderTestComponent({
  1687. orgFeatures: [...defaultOrgFeatures, 'performance-discover-dataset-selector'],
  1688. dashboard,
  1689. params: {
  1690. widgetIndex: '0',
  1691. },
  1692. updateDashboardSplitDecision: mockUpdateDashboardSplitDecision,
  1693. });
  1694. await waitFor(() => {
  1695. expect(eventsStatsMock).toHaveBeenCalled();
  1696. });
  1697. expect(screen.getByRole('radio', {name: /errors/i})).toBeChecked();
  1698. expect(mockUpdateDashboardSplitDecision).toHaveBeenCalledWith(
  1699. '1',
  1700. WidgetType.ERRORS
  1701. );
  1702. expect(
  1703. await screen.findByText(
  1704. "We're splitting our datasets up to make it a bit easier to digest. We defaulted this widget to Errors. Edit as you see fit."
  1705. )
  1706. ).toBeInTheDocument();
  1707. });
  1708. it('selects the transaction discover split type as the dataset when the request completes', async function () {
  1709. eventsStatsMock = MockApiClient.addMockResponse({
  1710. url: '/organizations/org-slug/events-stats/',
  1711. method: 'GET',
  1712. statusCode: 200,
  1713. body: {
  1714. meta: {discoverSplitDecision: WidgetType.TRANSACTIONS},
  1715. data: [],
  1716. },
  1717. });
  1718. const mockUpdateDashboardSplitDecision = jest.fn();
  1719. renderTestComponent({
  1720. orgFeatures: [...defaultOrgFeatures, 'performance-discover-dataset-selector'],
  1721. dashboard,
  1722. params: {
  1723. widgetIndex: '0',
  1724. },
  1725. updateDashboardSplitDecision: mockUpdateDashboardSplitDecision,
  1726. });
  1727. await waitFor(() => {
  1728. expect(eventsStatsMock).toHaveBeenCalled();
  1729. });
  1730. expect(screen.getByRole('radio', {name: /transactions/i})).toBeChecked();
  1731. expect(mockUpdateDashboardSplitDecision).toHaveBeenCalledWith(
  1732. '1',
  1733. WidgetType.TRANSACTIONS
  1734. );
  1735. expect(
  1736. await screen.findByText(
  1737. "We're splitting our datasets up to make it a bit easier to digest. We defaulted this widget to Transactions. Edit as you see fit."
  1738. )
  1739. ).toBeInTheDocument();
  1740. });
  1741. it('persists the query state for timeseries when switching between errors and transactions', async function () {
  1742. dashboard = mockDashboard({
  1743. widgets: [
  1744. WidgetFixture({
  1745. displayType: DisplayType.LINE,
  1746. widgetType: WidgetType.TRANSACTIONS,
  1747. queries: [
  1748. {
  1749. name: 'Test Widget',
  1750. fields: ['p99(transaction.duration)'],
  1751. columns: [],
  1752. aggregates: ['p99(transaction.duration)'],
  1753. conditions: 'testFilter:value',
  1754. orderby: '',
  1755. },
  1756. ],
  1757. }),
  1758. ],
  1759. });
  1760. renderTestComponent({
  1761. orgFeatures: [...defaultOrgFeatures, 'performance-discover-dataset-selector'],
  1762. dashboard,
  1763. params: {
  1764. widgetIndex: '0',
  1765. },
  1766. });
  1767. expect(await screen.findByText(/p99\(…\)/i)).toBeInTheDocument();
  1768. expect(screen.getByText('transaction.duration')).toBeInTheDocument();
  1769. expect(screen.getByRole('row', {name: 'testFilter:value'})).toBeInTheDocument(); // Check for query builder token
  1770. expect(screen.getByRole('radio', {name: /transactions/i})).toBeChecked();
  1771. // Switch to errors
  1772. await userEvent.click(screen.getByRole('radio', {name: /errors/i}));
  1773. expect(screen.getByRole('radio', {name: /transactions/i})).not.toBeChecked();
  1774. expect(screen.getByRole('radio', {name: /errors/i})).toBeChecked();
  1775. // The state is still the same
  1776. expect(await screen.findByText(/p99\(…\)/i)).toBeInTheDocument();
  1777. expect(screen.getByText('transaction.duration')).toBeInTheDocument();
  1778. expect(screen.getByRole('row', {name: 'testFilter:value'})).toBeInTheDocument();
  1779. });
  1780. it('sets the correct default count_if parameters for the errors dataset', async function () {
  1781. dashboard = mockDashboard({
  1782. widgets: [
  1783. WidgetFixture({
  1784. displayType: DisplayType.LINE,
  1785. widgetType: WidgetType.ERRORS,
  1786. queries: [
  1787. {
  1788. name: 'Test Widget',
  1789. fields: ['count()'],
  1790. columns: [],
  1791. aggregates: ['count()'],
  1792. conditions: '',
  1793. orderby: '',
  1794. },
  1795. ],
  1796. }),
  1797. ],
  1798. });
  1799. renderTestComponent({
  1800. orgFeatures: [...defaultOrgFeatures, 'performance-discover-dataset-selector'],
  1801. dashboard,
  1802. params: {
  1803. widgetIndex: '0',
  1804. },
  1805. });
  1806. await userEvent.click(await screen.findByTestId('label'));
  1807. await userEvent.click(screen.getByText(/count_if/));
  1808. const fieldLabels = screen.getAllByTestId('label');
  1809. expect(fieldLabels[0]).toHaveTextContent(/count_if/);
  1810. expect(fieldLabels[1]).toHaveTextContent('event.type');
  1811. expect(screen.getByDisplayValue('error')).toBeInTheDocument();
  1812. });
  1813. it('sets the correct default count_if parameters for the transactions dataset', async function () {
  1814. dashboard = mockDashboard({
  1815. widgets: [
  1816. WidgetFixture({
  1817. displayType: DisplayType.LINE,
  1818. widgetType: WidgetType.TRANSACTIONS,
  1819. queries: [
  1820. {
  1821. name: 'Test Widget',
  1822. fields: ['count()'],
  1823. columns: [],
  1824. aggregates: ['count()'],
  1825. conditions: '',
  1826. orderby: '',
  1827. },
  1828. ],
  1829. }),
  1830. ],
  1831. });
  1832. renderTestComponent({
  1833. orgFeatures: [...defaultOrgFeatures, 'performance-discover-dataset-selector'],
  1834. dashboard,
  1835. params: {
  1836. widgetIndex: '0',
  1837. },
  1838. });
  1839. await userEvent.click(await screen.findByTestId('label'));
  1840. await userEvent.click(screen.getByText(/count_if/));
  1841. const fieldLabels = screen.getAllByTestId('label');
  1842. expect(fieldLabels[0]).toHaveTextContent(/count_if/);
  1843. expect(fieldLabels[1]).toHaveTextContent('transaction.duration');
  1844. expect(screen.getByDisplayValue('300')).toBeInTheDocument();
  1845. });
  1846. });
  1847. describe('discover split warning', function () {
  1848. it('does not show the alert if the widget type is already split', async function () {
  1849. dashboard = mockDashboard({
  1850. widgets: [WidgetFixture({widgetType: WidgetType.TRANSACTIONS})],
  1851. });
  1852. eventsStatsMock = MockApiClient.addMockResponse({
  1853. url: '/organizations/org-slug/events-stats/',
  1854. method: 'GET',
  1855. statusCode: 200,
  1856. body: {
  1857. meta: {},
  1858. data: [],
  1859. },
  1860. });
  1861. renderTestComponent({
  1862. orgFeatures: [...defaultOrgFeatures, 'performance-discover-dataset-selector'],
  1863. dashboard,
  1864. params: {
  1865. widgetIndex: '0',
  1866. },
  1867. });
  1868. await waitFor(() => {
  1869. expect(screen.getByRole('radio', {name: /transactions/i})).toBeChecked();
  1870. });
  1871. expect(
  1872. screen.queryByText(/we're splitting our datasets/i)
  1873. ).not.toBeInTheDocument();
  1874. });
  1875. it('shows the alert if the widget is split but the decision is forced', async function () {
  1876. dashboard = mockDashboard({
  1877. widgets: [
  1878. WidgetFixture({
  1879. widgetType: WidgetType.ERRORS,
  1880. datasetSource: DatasetSource.FORCED,
  1881. }),
  1882. ],
  1883. });
  1884. eventsStatsMock = MockApiClient.addMockResponse({
  1885. url: '/organizations/org-slug/events-stats/',
  1886. method: 'GET',
  1887. statusCode: 200,
  1888. body: {
  1889. meta: {},
  1890. data: [],
  1891. },
  1892. });
  1893. renderTestComponent({
  1894. orgFeatures: [...defaultOrgFeatures, 'performance-discover-dataset-selector'],
  1895. dashboard,
  1896. params: {
  1897. widgetIndex: '0',
  1898. },
  1899. });
  1900. await waitFor(() => {
  1901. expect(screen.getByRole('radio', {name: /errors/i})).toBeChecked();
  1902. });
  1903. expect(
  1904. await screen.findByText(
  1905. "We're splitting our datasets up to make it a bit easier to digest. We defaulted this widget to Errors. Edit as you see fit."
  1906. )
  1907. ).toBeInTheDocument();
  1908. });
  1909. it('is dismissable', async function () {
  1910. dashboard = mockDashboard({
  1911. widgets: [
  1912. WidgetFixture({
  1913. widgetType: WidgetType.ERRORS,
  1914. datasetSource: DatasetSource.FORCED,
  1915. }),
  1916. ],
  1917. });
  1918. eventsStatsMock = MockApiClient.addMockResponse({
  1919. url: '/organizations/org-slug/events-stats/',
  1920. method: 'GET',
  1921. statusCode: 200,
  1922. body: {
  1923. meta: {},
  1924. data: [],
  1925. },
  1926. });
  1927. renderTestComponent({
  1928. orgFeatures: [...defaultOrgFeatures, 'performance-discover-dataset-selector'],
  1929. dashboard,
  1930. params: {
  1931. widgetIndex: '0',
  1932. },
  1933. });
  1934. expect(
  1935. await screen.findByText(
  1936. "We're splitting our datasets up to make it a bit easier to digest. We defaulted this widget to Errors. Edit as you see fit."
  1937. )
  1938. ).toBeInTheDocument();
  1939. await userEvent.click(screen.getByRole('button', {name: 'Close'}));
  1940. expect(
  1941. screen.queryByText(/we're splitting our datasets/i)
  1942. ).not.toBeInTheDocument();
  1943. });
  1944. });
  1945. });
  1946. describe('Widget Library', function () {
  1947. it('only opens the modal when the query data is changed', async function () {
  1948. const mockModal = jest.spyOn(modals, 'openWidgetBuilderOverwriteModal');
  1949. renderTestComponent();
  1950. await screen.findByText('Widget Library');
  1951. await userEvent.click(screen.getByText('Duration Distribution'));
  1952. // Widget Library, Builder title, and Chart title
  1953. expect(screen.getAllByText('Duration Distribution')).toHaveLength(2);
  1954. // Confirm modal doesn't open because no changes were made
  1955. expect(mockModal).not.toHaveBeenCalled();
  1956. await userEvent.click(screen.getAllByLabelText('Remove this Y-Axis')[0]);
  1957. await userEvent.click(screen.getByText('High Throughput Transactions'));
  1958. // Should not have overwritten widget data, and confirm modal should open
  1959. expect(screen.getAllByText('Duration Distribution')).toHaveLength(2);
  1960. expect(mockModal).toHaveBeenCalled();
  1961. });
  1962. });
  1963. describe('group by field', function () {
  1964. it('does not contain functions as options', async function () {
  1965. renderTestComponent({
  1966. query: {displayType: 'line'},
  1967. orgFeatures: [...defaultOrgFeatures],
  1968. });
  1969. expect(await screen.findByText('Select group')).toBeInTheDocument();
  1970. await userEvent.click(screen.getByText('Select group'));
  1971. // Only one f(x) field set in the y-axis selector
  1972. expect(screen.getByText('f(x)')).toBeInTheDocument();
  1973. });
  1974. it('adds more fields when Add Group is clicked', async function () {
  1975. renderTestComponent({
  1976. query: {displayType: 'line'},
  1977. orgFeatures: [...defaultOrgFeatures],
  1978. });
  1979. await userEvent.click(await screen.findByText('Add Group'));
  1980. expect(await screen.findAllByText('Select group')).toHaveLength(2);
  1981. });
  1982. it("doesn't reset group by when changing y-axis", async function () {
  1983. renderTestComponent({
  1984. query: {displayType: 'line'},
  1985. orgFeatures: [...defaultOrgFeatures],
  1986. });
  1987. await selectEvent.select(await screen.findByText('Select group'), 'project');
  1988. await userEvent.click(screen.getAllByText('count()')[0], {
  1989. skipHover: true,
  1990. });
  1991. await userEvent.click(screen.getByText(/count_unique/), {
  1992. skipHover: true,
  1993. });
  1994. expect(await screen.findByText('project')).toBeInTheDocument();
  1995. });
  1996. it("doesn't erase the selection when switching to another time series", async function () {
  1997. renderTestComponent({
  1998. query: {displayType: 'line'},
  1999. orgFeatures: [...defaultOrgFeatures],
  2000. });
  2001. await selectEvent.select(await screen.findByText('Select group'), 'project');
  2002. await userEvent.click(screen.getByText('Line Chart'));
  2003. await userEvent.click(screen.getByText('Area Chart'));
  2004. expect(await screen.findByText('project')).toBeInTheDocument();
  2005. });
  2006. it('sends a top N request when a grouping is selected', async function () {
  2007. renderTestComponent({
  2008. query: {displayType: 'line'},
  2009. orgFeatures: [...defaultOrgFeatures],
  2010. });
  2011. await userEvent.click(await screen.findByText('Group your results'));
  2012. await userEvent.type(screen.getByText('Select group'), 'project{enter}');
  2013. await waitFor(() =>
  2014. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  2015. 2,
  2016. '/organizations/org-slug/events-stats/',
  2017. expect.objectContaining({
  2018. query: expect.objectContaining({
  2019. query: '',
  2020. yAxis: ['count()'],
  2021. field: ['project', 'count()'],
  2022. topEvents: TOP_N,
  2023. orderby: '-count()',
  2024. }),
  2025. })
  2026. )
  2027. );
  2028. });
  2029. it('allows deleting groups until there is one left', async function () {
  2030. renderTestComponent({
  2031. query: {displayType: 'line'},
  2032. orgFeatures: [...defaultOrgFeatures],
  2033. });
  2034. await userEvent.click(await screen.findByText('Add Group'));
  2035. expect(screen.getAllByLabelText('Remove group')).toHaveLength(2);
  2036. await userEvent.click(screen.getAllByLabelText('Remove group')[1]);
  2037. await waitFor(() =>
  2038. expect(screen.queryByLabelText('Remove group')).not.toBeInTheDocument()
  2039. );
  2040. });
  2041. it("display 'remove' and 'drag to reorder' buttons", async function () {
  2042. renderTestComponent({
  2043. query: {displayType: 'line'},
  2044. orgFeatures: [...defaultOrgFeatures],
  2045. });
  2046. expect(screen.queryByLabelText('Remove group')).not.toBeInTheDocument();
  2047. await selectEvent.select(screen.getByText('Select group'), 'project');
  2048. expect(screen.getByLabelText('Remove group')).toBeInTheDocument();
  2049. expect(screen.queryByLabelText('Drag to reorder')).not.toBeInTheDocument();
  2050. await userEvent.click(screen.getByText('Add Group'));
  2051. expect(screen.getAllByLabelText('Remove group')).toHaveLength(2);
  2052. expect(screen.getAllByLabelText('Drag to reorder')).toHaveLength(2);
  2053. });
  2054. it.todo(
  2055. 'Since simulate drag and drop with RTL is not recommended because of browser layout, remember to create acceptance test for this'
  2056. );
  2057. });
  2058. describe('limit field', function () {
  2059. it('renders if groupBy value is present', async function () {
  2060. const handleSave = jest.fn();
  2061. renderTestComponent({
  2062. query: {displayType: 'line'},
  2063. orgFeatures: [...defaultOrgFeatures],
  2064. onSave: handleSave,
  2065. });
  2066. await selectEvent.select(await screen.findByText('Select group'), 'project');
  2067. expect(screen.getByText('Limit to 5 results')).toBeInTheDocument();
  2068. await userEvent.click(screen.getByText('Add Widget'));
  2069. await waitFor(() =>
  2070. expect(handleSave).toHaveBeenCalledWith([
  2071. expect.objectContaining({
  2072. limit: 5,
  2073. }),
  2074. ])
  2075. );
  2076. });
  2077. it('update value', async function () {
  2078. renderTestComponent({
  2079. query: {displayType: 'line'},
  2080. orgFeatures: [...defaultOrgFeatures],
  2081. });
  2082. await selectEvent.select(await screen.findByText('Select group'), 'project');
  2083. await userEvent.click(screen.getByText('Limit to 5 results'));
  2084. await userEvent.click(screen.getByText('Limit to 2 results'));
  2085. await waitFor(() =>
  2086. expect(eventsStatsMock).toHaveBeenCalledWith(
  2087. '/organizations/org-slug/events-stats/',
  2088. expect.objectContaining({
  2089. query: expect.objectContaining({
  2090. query: '',
  2091. yAxis: ['count()'],
  2092. field: ['project', 'count()'],
  2093. topEvents: 2,
  2094. orderby: '-count()',
  2095. }),
  2096. })
  2097. )
  2098. );
  2099. });
  2100. it('gets removed if no groupBy value', async function () {
  2101. renderTestComponent({
  2102. query: {displayType: 'line'},
  2103. orgFeatures: [...defaultOrgFeatures],
  2104. });
  2105. await selectEvent.select(await screen.findByText('Select group'), 'project');
  2106. expect(screen.getByText('Limit to 5 results')).toBeInTheDocument();
  2107. await userEvent.click(screen.getByLabelText('Remove group'));
  2108. await waitFor(() =>
  2109. expect(screen.queryByText('Limit to 5 results')).not.toBeInTheDocument()
  2110. );
  2111. });
  2112. it('applies a limit when switching from a table to timeseries chart with grouping', async function () {
  2113. const widget: Widget = {
  2114. displayType: DisplayType.TABLE,
  2115. interval: '1d',
  2116. queries: [
  2117. {
  2118. name: 'Test Widget',
  2119. fields: ['count()', 'count_unique(user)', 'epm()', 'project'],
  2120. columns: ['project'],
  2121. aggregates: ['count()', 'count_unique(user)', 'epm()'],
  2122. conditions: '',
  2123. orderby: '',
  2124. },
  2125. ],
  2126. title: 'Transactions',
  2127. id: '1',
  2128. };
  2129. const dashboard = mockDashboard({widgets: [widget]});
  2130. renderTestComponent({
  2131. dashboard,
  2132. orgFeatures: [...defaultOrgFeatures],
  2133. params: {
  2134. widgetIndex: '0',
  2135. },
  2136. });
  2137. await userEvent.click(await screen.findByText('Table'));
  2138. await userEvent.click(screen.getByText('Line Chart'));
  2139. expect(screen.getByText('Limit to 3 results')).toBeInTheDocument();
  2140. expect(eventsStatsMock).toHaveBeenCalledWith(
  2141. '/organizations/org-slug/events-stats/',
  2142. expect.objectContaining({
  2143. query: expect.objectContaining({
  2144. topEvents: 3,
  2145. }),
  2146. })
  2147. );
  2148. });
  2149. it('persists the limit when switching between timeseries charts', async function () {
  2150. const widget: Widget = {
  2151. displayType: DisplayType.AREA,
  2152. interval: '1d',
  2153. queries: [
  2154. {
  2155. name: 'Test Widget',
  2156. fields: ['count()', 'count_unique(user)', 'epm()', 'project'],
  2157. columns: ['project'],
  2158. aggregates: ['count()', 'count_unique(user)', 'epm()'],
  2159. conditions: '',
  2160. orderby: '',
  2161. },
  2162. ],
  2163. title: 'Transactions',
  2164. id: '1',
  2165. limit: 1,
  2166. };
  2167. const dashboard = mockDashboard({widgets: [widget]});
  2168. renderTestComponent({
  2169. dashboard,
  2170. orgFeatures: [...defaultOrgFeatures],
  2171. params: {
  2172. widgetIndex: '0',
  2173. },
  2174. });
  2175. await userEvent.click(await screen.findByText('Area Chart'));
  2176. await userEvent.click(screen.getByText('Line Chart'));
  2177. expect(screen.getByText('Limit to 1 result')).toBeInTheDocument();
  2178. expect(eventsStatsMock).toHaveBeenCalledWith(
  2179. '/organizations/org-slug/events-stats/',
  2180. expect.objectContaining({
  2181. query: expect.objectContaining({
  2182. topEvents: 1,
  2183. }),
  2184. })
  2185. );
  2186. });
  2187. it('unsets the limit when going from timeseries to table', async function () {
  2188. const widget: Widget = {
  2189. displayType: DisplayType.AREA,
  2190. interval: '1d',
  2191. queries: [
  2192. {
  2193. name: 'Test Widget',
  2194. fields: ['count()', 'count_unique(user)', 'epm()', 'project'],
  2195. columns: ['project'],
  2196. aggregates: ['count()', 'count_unique(user)', 'epm()'],
  2197. conditions: '',
  2198. orderby: '',
  2199. },
  2200. ],
  2201. title: 'Transactions',
  2202. id: '1',
  2203. limit: 1,
  2204. };
  2205. const dashboard = mockDashboard({widgets: [widget]});
  2206. renderTestComponent({
  2207. dashboard,
  2208. orgFeatures: [...defaultOrgFeatures],
  2209. params: {
  2210. widgetIndex: '0',
  2211. },
  2212. });
  2213. await userEvent.click(await screen.findByText('Area Chart'));
  2214. await userEvent.click(screen.getByText('Table'));
  2215. expect(screen.queryByText('Limit to 1 result')).not.toBeInTheDocument();
  2216. expect(eventsMock).toHaveBeenCalledWith(
  2217. '/organizations/org-slug/events/',
  2218. expect.objectContaining({
  2219. query: expect.objectContaining({
  2220. topEvents: undefined,
  2221. }),
  2222. })
  2223. );
  2224. });
  2225. });
  2226. describe('Spans Dataset', () => {
  2227. it('queries for span tags and returns the correct data', async () => {
  2228. MockApiClient.addMockResponse({
  2229. url: `/organizations/org-slug/spans/fields/`,
  2230. body: [
  2231. {
  2232. key: 'plan',
  2233. name: 'plan',
  2234. },
  2235. ],
  2236. match: [
  2237. function (_url: string, options: Record<string, any>) {
  2238. return options.query.type === 'string';
  2239. },
  2240. ],
  2241. });
  2242. MockApiClient.addMockResponse({
  2243. url: `/organizations/org-slug/spans/fields/`,
  2244. body: [
  2245. {
  2246. key: 'tags[lcp.size,number]',
  2247. name: 'lcp.size',
  2248. },
  2249. {
  2250. key: 'tags[something.else,number]',
  2251. name: 'something.else',
  2252. },
  2253. ],
  2254. match: [
  2255. function (_url: string, options: Record<string, any>) {
  2256. return options.query.type === 'number';
  2257. },
  2258. ],
  2259. });
  2260. const dashboard = mockDashboard({
  2261. widgets: [
  2262. WidgetFixture({
  2263. widgetType: WidgetType.SPANS,
  2264. displayType: DisplayType.TABLE,
  2265. queries: [
  2266. {
  2267. name: 'Test Widget',
  2268. fields: ['count(tags[lcp.size,number])'],
  2269. columns: [],
  2270. aggregates: ['count(tags[lcp.size,number])'],
  2271. conditions: '',
  2272. orderby: '',
  2273. },
  2274. ],
  2275. }),
  2276. ],
  2277. });
  2278. renderTestComponent({
  2279. dashboard,
  2280. orgFeatures: [...defaultOrgFeatures, 'dashboards-eap'],
  2281. params: {
  2282. widgetIndex: '0',
  2283. },
  2284. });
  2285. // Click the argument to the count() function
  2286. expect(await screen.findByText('lcp.size')).toBeInTheDocument();
  2287. await userEvent.click(screen.getByText('lcp.size'));
  2288. // The option now appears in the aggregate property dropdown
  2289. expect(screen.queryAllByText('lcp.size')).toHaveLength(2);
  2290. expect(screen.getByText('something.else')).toBeInTheDocument();
  2291. // Click count() to verify the string tag is in the dropdown
  2292. expect(screen.queryByText('plan')).not.toBeInTheDocument();
  2293. await userEvent.click(screen.getByText(`count(…)`));
  2294. expect(screen.getByText('plan')).toBeInTheDocument();
  2295. });
  2296. it('does not show the Add Query button', async function () {
  2297. const dashboard = mockDashboard({
  2298. widgets: [
  2299. WidgetFixture({
  2300. widgetType: WidgetType.SPANS,
  2301. // Add Query is only available for timeseries charts
  2302. displayType: DisplayType.LINE,
  2303. queries: [
  2304. {
  2305. name: 'Test Widget',
  2306. fields: ['count(span.duration)'],
  2307. columns: [],
  2308. conditions: '',
  2309. orderby: '',
  2310. aggregates: ['count(span.duration)'],
  2311. },
  2312. ],
  2313. }),
  2314. ],
  2315. });
  2316. renderTestComponent({
  2317. dashboard,
  2318. orgFeatures: [...defaultOrgFeatures],
  2319. params: {
  2320. widgetIndex: '0',
  2321. },
  2322. });
  2323. await screen.findByText('Line Chart');
  2324. expect(screen.queryByText('Add Query')).not.toBeInTheDocument();
  2325. });
  2326. });
  2327. });