addDashboardWidgetModal.spec.jsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705
  1. import {mountWithTheme} from 'sentry-test/enzyme';
  2. import {initializeOrg} from 'sentry-test/initializeOrg';
  3. import {getOptionByLabel, selectByLabel} from 'sentry-test/select-new';
  4. import AddDashboardWidgetModal from 'app/components/modals/addDashboardWidgetModal';
  5. import TagStore from 'app/stores/tagStore';
  6. const stubEl = props => <div>{props.children}</div>;
  7. function mountModal({initialData, onAddWidget, onUpdateWidget, widget}) {
  8. return mountWithTheme(
  9. <AddDashboardWidgetModal
  10. Header={stubEl}
  11. Footer={stubEl}
  12. Body={stubEl}
  13. organization={initialData.organization}
  14. onAddWidget={onAddWidget}
  15. onUpdateWidget={onUpdateWidget}
  16. widget={widget}
  17. closeModal={() => void 0}
  18. />,
  19. initialData.routerContext
  20. );
  21. }
  22. async function clickSubmit(wrapper) {
  23. // Click on submit.
  24. const button = wrapper.find('Button[data-test-id="add-widget"] button');
  25. button.simulate('click');
  26. // Wait for xhr to complete.
  27. return tick();
  28. }
  29. function getDisplayType(wrapper) {
  30. return wrapper.find('input[name="displayType"]');
  31. }
  32. async function setSearchConditions(el, query) {
  33. el.find('textarea')
  34. .simulate('change', {target: {value: query}})
  35. .getDOMNode()
  36. .setSelectionRange(query.length, query.length);
  37. await tick();
  38. await el.update();
  39. el.find('textarea').simulate('keydown', {key: 'Enter'});
  40. }
  41. describe('Modals -> AddDashboardWidgetModal', function () {
  42. const initialData = initializeOrg({
  43. organization: {
  44. features: ['performance-view', 'discover-query'],
  45. apdexThreshold: 400,
  46. },
  47. });
  48. const tags = [
  49. {name: 'browser.name', key: 'browser.name'},
  50. {name: 'custom-field', key: 'custom-field'},
  51. ];
  52. let eventsStatsMock;
  53. beforeEach(function () {
  54. TagStore.onLoadTagsSuccess(tags);
  55. MockApiClient.addMockResponse({
  56. url: '/organizations/org-slug/dashboards/widgets/',
  57. method: 'POST',
  58. statusCode: 200,
  59. body: [],
  60. });
  61. eventsStatsMock = MockApiClient.addMockResponse({
  62. url: '/organizations/org-slug/events-stats/',
  63. body: [],
  64. });
  65. MockApiClient.addMockResponse({
  66. url: '/organizations/org-slug/eventsv2/',
  67. body: {data: [{'event.type': 'error'}], meta: {'event.type': 'string'}},
  68. });
  69. MockApiClient.addMockResponse({
  70. url: '/organizations/org-slug/events-geo/',
  71. body: {data: [], meta: {}},
  72. });
  73. MockApiClient.addMockResponse({
  74. url: '/organizations/org-slug/recent-searches/',
  75. body: [],
  76. });
  77. });
  78. afterEach(() => {
  79. MockApiClient.clearMockResponses();
  80. });
  81. it('can update the title', async function () {
  82. let widget = undefined;
  83. const wrapper = mountModal({
  84. initialData,
  85. onAddWidget: data => (widget = data),
  86. });
  87. const input = wrapper.find('Input[name="title"] input');
  88. input.simulate('change', {target: {value: 'Unique Users'}});
  89. await clickSubmit(wrapper);
  90. expect(widget.title).toEqual('Unique Users');
  91. wrapper.unmount();
  92. });
  93. it('can add conditions', async function () {
  94. jest.useFakeTimers();
  95. let widget = undefined;
  96. const wrapper = mountModal({
  97. initialData,
  98. onAddWidget: data => (widget = data),
  99. });
  100. // Change the search text on the first query.
  101. const input = wrapper.find('#smart-search-input').first();
  102. input.simulate('change', {target: {value: 'color:blue'}}).simulate('blur');
  103. jest.runAllTimers();
  104. jest.useRealTimers();
  105. await clickSubmit(wrapper);
  106. expect(widget.queries).toHaveLength(1);
  107. expect(widget.queries[0].conditions).toEqual('color:blue');
  108. wrapper.unmount();
  109. });
  110. it('can choose a field', async function () {
  111. let widget = undefined;
  112. const wrapper = mountModal({
  113. initialData,
  114. onAddWidget: data => (widget = data),
  115. });
  116. // No delete button as there is only one field.
  117. expect(wrapper.find('IconDelete')).toHaveLength(0);
  118. selectByLabel(wrapper, 'p95(\u2026)', {name: 'field', at: 0, control: true});
  119. await clickSubmit(wrapper);
  120. expect(widget.queries).toHaveLength(1);
  121. expect(widget.queries[0].fields).toEqual(['p95(transaction.duration)']);
  122. wrapper.unmount();
  123. });
  124. it('can add additional fields', async function () {
  125. let widget = undefined;
  126. const wrapper = mountModal({
  127. initialData,
  128. onAddWidget: data => (widget = data),
  129. });
  130. // Click the add button
  131. const add = wrapper.find('button[aria-label="Add Overlay"]');
  132. add.simulate('click');
  133. wrapper.update();
  134. // Should be another field input.
  135. expect(wrapper.find('QueryField')).toHaveLength(2);
  136. selectByLabel(wrapper, 'p95(\u2026)', {name: 'field', at: 1, control: true});
  137. await clickSubmit(wrapper);
  138. expect(widget.queries).toHaveLength(1);
  139. expect(widget.queries[0].fields).toEqual(['count()', 'p95(transaction.duration)']);
  140. wrapper.unmount();
  141. });
  142. it('can add and delete additional queries', async function () {
  143. MockApiClient.addMockResponse({
  144. url: '/organizations/org-slug/tags/event.type/values/',
  145. body: [{count: 2, name: 'Nvidia 1080ti'}],
  146. });
  147. MockApiClient.addMockResponse({
  148. url: '/organizations/org-slug/recent-searches/',
  149. method: 'POST',
  150. body: [],
  151. });
  152. let widget = undefined;
  153. const wrapper = mountModal({
  154. initialData,
  155. onAddWidget: data => (widget = data),
  156. });
  157. // Set first query search conditions
  158. await setSearchConditions(
  159. wrapper.find('SearchConditionsWrapper StyledSearchBar'),
  160. 'event.type:transaction'
  161. );
  162. // Set first query legend alias
  163. wrapper
  164. .find('SearchConditionsWrapper input[placeholder="Legend Alias"]')
  165. .simulate('change', {target: {value: 'Transactions'}});
  166. // Click the "Add Query" button twice
  167. const addQuery = wrapper.find('button[aria-label="Add Query"]');
  168. addQuery.simulate('click');
  169. wrapper.update();
  170. addQuery.simulate('click');
  171. wrapper.update();
  172. // Expect three search bars
  173. expect(wrapper.find('StyledSearchBar')).toHaveLength(3);
  174. // Expect "Add Query" button to be hidden since we're limited to at most 3 search conditions
  175. expect(wrapper.find('button[aria-label="Add Query"]')).toHaveLength(0);
  176. // Delete second query
  177. expect(wrapper.find('button[aria-label="Remove query"]')).toHaveLength(3);
  178. wrapper.find('button[aria-label="Remove query"]').at(1).simulate('click');
  179. wrapper.update();
  180. // Expect "Add Query" button to be shown again
  181. expect(wrapper.find('button[aria-label="Add Query"]')).toHaveLength(1);
  182. // Set second query search conditions
  183. const secondSearchBar = wrapper.find('SearchConditionsWrapper StyledSearchBar').at(1);
  184. await setSearchConditions(secondSearchBar, 'event.type:error');
  185. // Set second query legend alias
  186. wrapper
  187. .find('SearchConditionsWrapper input[placeholder="Legend Alias"]')
  188. .at(1)
  189. .simulate('change', {target: {value: 'Errors'}});
  190. // Save widget
  191. await clickSubmit(wrapper);
  192. expect(widget.queries).toHaveLength(2);
  193. expect(widget.queries[0]).toMatchObject({
  194. name: 'Transactions',
  195. conditions: 'event.type:transaction',
  196. fields: ['count()'],
  197. });
  198. expect(widget.queries[1]).toMatchObject({
  199. name: 'Errors',
  200. conditions: 'event.type:error',
  201. fields: ['count()'],
  202. });
  203. wrapper.unmount();
  204. });
  205. it('can respond to validation feedback', async function () {
  206. MockApiClient.addMockResponse({
  207. url: '/organizations/org-slug/dashboards/widgets/',
  208. method: 'POST',
  209. statusCode: 400,
  210. body: {
  211. title: ['This field is required'],
  212. queries: [{conditions: ['Invalid value']}],
  213. },
  214. });
  215. let widget = undefined;
  216. const wrapper = mountModal({
  217. initialData,
  218. onAddWidget: data => (widget = data),
  219. });
  220. await clickSubmit(wrapper);
  221. await wrapper.update();
  222. // API request should fail and not add widget.
  223. expect(widget).toBeUndefined();
  224. const errors = wrapper.find('FieldErrorReason');
  225. expect(errors).toHaveLength(2);
  226. // Nested object error should display
  227. const conditionError = wrapper.find('WidgetQueriesForm FieldErrorReason');
  228. expect(conditionError).toHaveLength(1);
  229. wrapper.unmount();
  230. });
  231. it('can edit a widget', async function () {
  232. let widget = {
  233. id: '9',
  234. title: 'Errors over time',
  235. interval: '5m',
  236. displayType: 'line',
  237. queries: [
  238. {
  239. id: '9',
  240. name: 'errors',
  241. conditions: 'event.type:error',
  242. fields: ['count()', 'count_unique(id)'],
  243. },
  244. {
  245. id: '9',
  246. name: 'csp',
  247. conditions: 'event.type:csp',
  248. fields: ['count()', 'count_unique(id)'],
  249. },
  250. ],
  251. };
  252. const onAdd = jest.fn();
  253. const wrapper = mountModal({
  254. initialData,
  255. widget,
  256. onAddWidget: onAdd,
  257. onUpdateWidget: data => {
  258. widget = data;
  259. },
  260. });
  261. // Should be in edit 'mode'
  262. const heading = wrapper.find('h4');
  263. expect(heading.text()).toContain('Edit');
  264. // Should set widget data up.
  265. const title = wrapper.find('Input[name="title"]');
  266. expect(title.props().value).toEqual(widget.title);
  267. expect(wrapper.find('input[name="displayType"]').props().value).toEqual(
  268. widget.displayType
  269. );
  270. expect(wrapper.find('WidgetQueriesForm')).toHaveLength(1);
  271. expect(wrapper.find('StyledSearchBar')).toHaveLength(2);
  272. expect(wrapper.find('QueryField')).toHaveLength(2);
  273. // Expect events-stats endpoint to be called for each search conditions with
  274. // the same y-axis parameters
  275. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  276. 1,
  277. '/organizations/org-slug/events-stats/',
  278. expect.objectContaining({
  279. query: expect.objectContaining({
  280. query: 'event.type:error',
  281. yAxis: ['count()', 'count_unique(id)'],
  282. }),
  283. })
  284. );
  285. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  286. 2,
  287. '/organizations/org-slug/events-stats/',
  288. expect.objectContaining({
  289. query: expect.objectContaining({
  290. query: 'event.type:csp',
  291. yAxis: ['count()', 'count_unique(id)'],
  292. }),
  293. })
  294. );
  295. title.simulate('change', {target: {value: 'New title'}});
  296. await clickSubmit(wrapper);
  297. expect(onAdd).not.toHaveBeenCalled();
  298. expect(widget.title).toEqual('New title');
  299. expect(eventsStatsMock).toHaveBeenCalledTimes(2);
  300. wrapper.unmount();
  301. });
  302. it('renders column inputs for table widgets', async function () {
  303. MockApiClient.addMockResponse({
  304. url: '/organizations/org-slug/eventsv2/',
  305. method: 'GET',
  306. statusCode: 200,
  307. body: {
  308. meta: {},
  309. data: [],
  310. },
  311. });
  312. let widget = {
  313. id: '9',
  314. title: 'sdk usage',
  315. interval: '5m',
  316. displayType: 'table',
  317. queries: [
  318. {
  319. id: '9',
  320. name: 'errors',
  321. conditions: 'event.type:error',
  322. fields: ['sdk.name', 'count()'],
  323. },
  324. ],
  325. };
  326. const wrapper = mountModal({
  327. initialData,
  328. widget,
  329. onAddWidget: jest.fn(),
  330. onUpdateWidget: data => {
  331. widget = data;
  332. },
  333. });
  334. // Should be in edit 'mode'
  335. const heading = wrapper.find('h4').first();
  336. expect(heading.text()).toContain('Edit');
  337. // Should set widget data up.
  338. const title = wrapper.find('Input[name="title"]');
  339. expect(title.props().value).toEqual(widget.title);
  340. expect(wrapper.find('input[name="displayType"]').props().value).toEqual(
  341. widget.displayType
  342. );
  343. expect(wrapper.find('WidgetQueriesForm')).toHaveLength(1);
  344. // Should have an orderby select
  345. expect(wrapper.find('WidgetQueriesForm SelectControl[name="orderby"]')).toHaveLength(
  346. 1
  347. );
  348. // Add a column, and choose a value,
  349. wrapper.find('button[aria-label="Add a Column"]').simulate('click');
  350. await wrapper.update();
  351. selectByLabel(wrapper, 'trace', {name: 'field', at: 2, control: true});
  352. await wrapper.update();
  353. await clickSubmit(wrapper);
  354. // A new field should be added.
  355. expect(widget.queries[0].fields).toHaveLength(3);
  356. expect(widget.queries[0].fields[2]).toEqual('trace');
  357. wrapper.unmount();
  358. });
  359. it('uses count() columns if there are no aggregate fields remaining when switching from table to chart', async function () {
  360. let widget = undefined;
  361. const wrapper = mountModal({
  362. initialData,
  363. onAddWidget: data => (widget = data),
  364. });
  365. // No delete button as there is only one field.
  366. expect(wrapper.find('IconDelete')).toHaveLength(0);
  367. // Select Table display
  368. selectByLabel(wrapper, 'Table', {name: 'displayType', at: 0, control: true});
  369. expect(getDisplayType(wrapper).props().value).toEqual('table');
  370. // Add field column
  371. selectByLabel(wrapper, 'event.type', {name: 'field', at: 0, control: true});
  372. let fieldColumn = wrapper.find('input[name="field"]');
  373. expect(fieldColumn.props().value).toEqual({
  374. kind: 'field',
  375. meta: {dataType: 'string', name: 'event.type'},
  376. });
  377. // Select Line chart display
  378. selectByLabel(wrapper, 'Line Chart', {name: 'displayType', at: 0, control: true});
  379. expect(getDisplayType(wrapper).props().value).toEqual('line');
  380. // Expect event.type field to be converted to count()
  381. fieldColumn = wrapper.find('input[name="field"]');
  382. expect(fieldColumn.props().value).toEqual({
  383. kind: 'function',
  384. meta: {name: 'count', parameters: []},
  385. });
  386. await clickSubmit(wrapper);
  387. expect(widget.queries).toHaveLength(1);
  388. expect(widget.queries[0].fields).toEqual(['count()']);
  389. wrapper.unmount();
  390. });
  391. it('should filter out non-aggregate fields when switching from table to chart', async function () {
  392. let widget = undefined;
  393. const wrapper = mountModal({
  394. initialData,
  395. onAddWidget: data => (widget = data),
  396. });
  397. // No delete button as there is only one field.
  398. expect(wrapper.find('IconDelete')).toHaveLength(0);
  399. // Select Table display
  400. selectByLabel(wrapper, 'Table', {name: 'displayType', at: 0, control: true});
  401. expect(getDisplayType(wrapper).props().value).toEqual('table');
  402. // Click the add button
  403. const add = wrapper.find('button[aria-label="Add a Column"]');
  404. add.simulate('click');
  405. wrapper.update();
  406. // Add columns
  407. selectByLabel(wrapper, 'event.type', {name: 'field', at: 0, control: true});
  408. let fieldColumn = wrapper.find('input[name="field"]').at(0);
  409. expect(fieldColumn.props().value).toEqual({
  410. kind: 'field',
  411. meta: {dataType: 'string', name: 'event.type'},
  412. });
  413. selectByLabel(wrapper, 'p95(\u2026)', {name: 'field', at: 1, control: true});
  414. fieldColumn = wrapper.find('input[name="field"]').at(1);
  415. expect(fieldColumn.props().value).toMatchObject({
  416. kind: 'function',
  417. meta: {
  418. name: 'p95',
  419. parameters: [{defaultValue: 'transaction.duration', kind: 'column'}],
  420. },
  421. });
  422. // Select Line chart display
  423. selectByLabel(wrapper, 'Line Chart', {name: 'displayType', at: 0, control: true});
  424. expect(getDisplayType(wrapper).props().value).toEqual('line');
  425. // Expect event.type field to be converted to count()
  426. fieldColumn = wrapper.find('input[name="field"]');
  427. expect(fieldColumn.length).toEqual(1);
  428. expect(fieldColumn.props().value).toMatchObject({
  429. kind: 'function',
  430. meta: {
  431. name: 'p95',
  432. parameters: [{defaultValue: 'transaction.duration', kind: 'column'}],
  433. },
  434. });
  435. await clickSubmit(wrapper);
  436. expect(widget.queries).toHaveLength(1);
  437. expect(widget.queries[0].fields).toEqual(['p95(transaction.duration)']);
  438. wrapper.unmount();
  439. });
  440. it('should filter non-legal y-axis choices for timeseries widget charts', async function () {
  441. let widget = undefined;
  442. const wrapper = mountModal({
  443. initialData,
  444. onAddWidget: data => (widget = data),
  445. });
  446. // No delete button as there is only one field.
  447. expect(wrapper.find('IconDelete')).toHaveLength(0);
  448. selectByLabel(wrapper, 'any(\u2026)', {
  449. name: 'field',
  450. at: 0,
  451. control: true,
  452. });
  453. // Expect user.display to not be an available parameter option for any()
  454. // for line (timeseries) widget charts
  455. const option = getOptionByLabel(wrapper, 'user.display', {
  456. name: 'parameter',
  457. at: 0,
  458. control: true,
  459. });
  460. expect(option.exists()).toEqual(false);
  461. // Be able to choose a numeric-like option for any()
  462. selectByLabel(wrapper, 'measurements.lcp', {
  463. name: 'parameter',
  464. at: 0,
  465. control: true,
  466. });
  467. await clickSubmit(wrapper);
  468. expect(widget.displayType).toEqual('line');
  469. expect(widget.queries).toHaveLength(1);
  470. expect(widget.queries[0].fields).toEqual(['any(measurements.lcp)']);
  471. wrapper.unmount();
  472. });
  473. it('should not filter y-axis choices for big number widget charts', async function () {
  474. let widget = undefined;
  475. const wrapper = mountModal({
  476. initialData,
  477. onAddWidget: data => (widget = data),
  478. });
  479. // No delete button as there is only one field.
  480. expect(wrapper.find('IconDelete')).toHaveLength(0);
  481. // Select Big number display
  482. selectByLabel(wrapper, 'Big Number', {name: 'displayType', at: 0, control: true});
  483. expect(getDisplayType(wrapper).props().value).toEqual('big_number');
  484. selectByLabel(wrapper, 'count_unique(\u2026)', {
  485. name: 'field',
  486. at: 0,
  487. control: true,
  488. });
  489. // Be able to choose a non numeric-like option for count_unique()
  490. selectByLabel(wrapper, 'user.display', {
  491. name: 'parameter',
  492. at: 0,
  493. control: true,
  494. });
  495. await clickSubmit(wrapper);
  496. expect(widget.displayType).toEqual('big_number');
  497. expect(widget.queries).toHaveLength(1);
  498. expect(widget.queries[0].fields).toEqual(['count_unique(user.display)']);
  499. wrapper.unmount();
  500. });
  501. it('should filter y-axis choices for world map widget charts', async function () {
  502. let widget = undefined;
  503. const wrapper = mountModal({
  504. initialData,
  505. onAddWidget: data => (widget = data),
  506. });
  507. // No delete button as there is only one field.
  508. expect(wrapper.find('IconDelete')).toHaveLength(0);
  509. // Select World Map display
  510. selectByLabel(wrapper, 'World Map', {name: 'displayType', at: 0, control: true});
  511. expect(getDisplayType(wrapper).props().value).toEqual('world_map');
  512. // Choose any()
  513. selectByLabel(wrapper, 'any(\u2026)', {
  514. name: 'field',
  515. at: 0,
  516. control: true,
  517. });
  518. // user.display should be filtered out for any()
  519. const option = getOptionByLabel(wrapper, 'user.display', {
  520. name: 'parameter',
  521. at: 0,
  522. control: true,
  523. });
  524. expect(option.exists()).toEqual(false);
  525. selectByLabel(wrapper, 'measurements.lcp', {
  526. name: 'parameter',
  527. at: 0,
  528. control: true,
  529. });
  530. // Choose count_unique()
  531. selectByLabel(wrapper, 'count_unique(\u2026)', {
  532. name: 'field',
  533. at: 0,
  534. control: true,
  535. });
  536. // user.display not should be filtered out for count_unique()
  537. selectByLabel(wrapper, 'user.display', {
  538. name: 'parameter',
  539. at: 0,
  540. control: true,
  541. });
  542. // Be able to choose a numeric-like option
  543. selectByLabel(wrapper, 'measurements.lcp', {
  544. name: 'parameter',
  545. at: 0,
  546. control: true,
  547. });
  548. await clickSubmit(wrapper);
  549. expect(widget.displayType).toEqual('world_map');
  550. expect(widget.queries).toHaveLength(1);
  551. expect(widget.queries[0].fields).toEqual(['count_unique(measurements.lcp)']);
  552. wrapper.unmount();
  553. });
  554. it('should filter y-axis choices by output type when switching from big number to line chart', async function () {
  555. let widget = undefined;
  556. const wrapper = mountModal({
  557. initialData,
  558. onAddWidget: data => (widget = data),
  559. });
  560. // No delete button as there is only one field.
  561. expect(wrapper.find('IconDelete')).toHaveLength(0);
  562. // Select Big Number display
  563. selectByLabel(wrapper, 'Big Number', {name: 'displayType', at: 0, control: true});
  564. expect(getDisplayType(wrapper).props().value).toEqual('big_number');
  565. // Choose any()
  566. selectByLabel(wrapper, 'any(\u2026)', {
  567. name: 'field',
  568. at: 0,
  569. control: true,
  570. });
  571. selectByLabel(wrapper, 'id', {
  572. name: 'parameter',
  573. at: 0,
  574. control: true,
  575. });
  576. // Select Line chart display
  577. selectByLabel(wrapper, 'Line Chart', {name: 'displayType', at: 0, control: true});
  578. expect(getDisplayType(wrapper).props().value).toEqual('line');
  579. // Expect event.type field to be converted to count()
  580. const fieldColumn = wrapper.find('input[name="field"]');
  581. expect(fieldColumn.length).toEqual(1);
  582. expect(fieldColumn.props().value).toMatchObject({
  583. kind: 'function',
  584. meta: {
  585. name: 'count',
  586. parameters: [],
  587. },
  588. });
  589. await clickSubmit(wrapper);
  590. expect(widget.displayType).toEqual('line');
  591. expect(widget.queries).toHaveLength(1);
  592. expect(widget.queries[0].fields).toEqual(['count()']);
  593. wrapper.unmount();
  594. });
  595. });