useWidgetBuilderState.spec.tsx 29 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027
  1. import {LocationFixture} from 'sentry-fixture/locationFixture';
  2. import {act, renderHook} from 'sentry-test/reactTestingLibrary';
  3. import type {Column} from 'sentry/utils/discover/fields';
  4. import {useLocation} from 'sentry/utils/useLocation';
  5. import {useNavigate} from 'sentry/utils/useNavigate';
  6. import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
  7. import {WidgetBuilderProvider} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
  8. import useWidgetBuilderState, {
  9. BuilderStateAction,
  10. serializeFields,
  11. } from 'sentry/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState';
  12. import {FieldValueKind} from 'sentry/views/discover/table/types';
  13. jest.mock('sentry/utils/useLocation');
  14. jest.mock('sentry/utils/useNavigate');
  15. const mockedUsedLocation = jest.mocked(useLocation);
  16. const mockedUseNavigate = jest.mocked(useNavigate);
  17. describe('useWidgetBuilderState', () => {
  18. let mockNavigate!: jest.Mock;
  19. beforeEach(() => {
  20. mockNavigate = jest.fn();
  21. mockedUseNavigate.mockReturnValue(mockNavigate);
  22. jest.useFakeTimers();
  23. });
  24. afterEach(() => {
  25. jest.useRealTimers();
  26. jest.clearAllMocks();
  27. });
  28. it('returns the widget builder state from the query params', () => {
  29. mockedUsedLocation.mockReturnValue(
  30. LocationFixture({
  31. query: {
  32. title: 'test',
  33. description: 'lalala this is a description',
  34. },
  35. })
  36. );
  37. const {result} = renderHook(() => useWidgetBuilderState(), {
  38. wrapper: WidgetBuilderProvider,
  39. });
  40. expect(result.current.state.title).toBe('test');
  41. expect(result.current.state.description).toBe('lalala this is a description');
  42. });
  43. it('sets the new title and description in the query params', () => {
  44. const {result} = renderHook(() => useWidgetBuilderState(), {
  45. wrapper: WidgetBuilderProvider,
  46. });
  47. act(() => {
  48. result.current.dispatch({
  49. type: BuilderStateAction.SET_TITLE,
  50. payload: 'new title',
  51. });
  52. });
  53. act(() => {
  54. result.current.dispatch({
  55. type: BuilderStateAction.SET_DESCRIPTION,
  56. payload: 'new description',
  57. });
  58. });
  59. jest.runAllTimers();
  60. expect(mockNavigate).toHaveBeenCalledWith(
  61. expect.objectContaining({query: expect.objectContaining({title: 'new title'})})
  62. );
  63. expect(mockNavigate).toHaveBeenCalledWith(
  64. expect.objectContaining({
  65. query: expect.objectContaining({description: 'new description'}),
  66. })
  67. );
  68. });
  69. describe('display type', () => {
  70. it('returns the display type from the query params', () => {
  71. mockedUsedLocation.mockReturnValue(
  72. LocationFixture({
  73. query: {displayType: DisplayType.AREA},
  74. })
  75. );
  76. const {result} = renderHook(() => useWidgetBuilderState(), {
  77. wrapper: WidgetBuilderProvider,
  78. });
  79. expect(result.current.state.displayType).toBe(DisplayType.AREA);
  80. });
  81. it('returns a default display type from the query params when the display type is not valid', () => {
  82. mockedUsedLocation.mockReturnValue(
  83. LocationFixture({
  84. query: {displayType: 'invalid'},
  85. })
  86. );
  87. const {result} = renderHook(() => useWidgetBuilderState(), {
  88. wrapper: WidgetBuilderProvider,
  89. });
  90. expect(result.current.state.displayType).toBe(DisplayType.TABLE);
  91. });
  92. it('sets the display type in the query params', () => {
  93. const {result} = renderHook(() => useWidgetBuilderState(), {
  94. wrapper: WidgetBuilderProvider,
  95. });
  96. act(() => {
  97. result.current.dispatch({
  98. type: BuilderStateAction.SET_DISPLAY_TYPE,
  99. payload: DisplayType.AREA,
  100. });
  101. });
  102. jest.runAllTimers();
  103. expect(mockNavigate).toHaveBeenCalledWith(
  104. expect.objectContaining({
  105. query: expect.objectContaining({displayType: DisplayType.AREA}),
  106. })
  107. );
  108. });
  109. it('persists the values when going from timeseries to timeseries', () => {
  110. mockedUsedLocation.mockReturnValue(
  111. LocationFixture({
  112. query: {
  113. displayType: DisplayType.LINE,
  114. field: ['event.type'],
  115. yAxis: ['count()', 'count_unique(user)'],
  116. },
  117. })
  118. );
  119. const {result} = renderHook(() => useWidgetBuilderState(), {
  120. wrapper: WidgetBuilderProvider,
  121. });
  122. expect(result.current.state.displayType).toBe(DisplayType.LINE);
  123. expect(result.current.state.fields).toEqual([
  124. {field: 'event.type', alias: undefined, kind: 'field'},
  125. ]);
  126. expect(result.current.state.yAxis).toEqual([
  127. {
  128. function: ['count', '', undefined, undefined],
  129. alias: undefined,
  130. kind: 'function',
  131. },
  132. {
  133. function: ['count_unique', 'user', undefined, undefined],
  134. alias: undefined,
  135. kind: 'function',
  136. },
  137. ]);
  138. act(() => {
  139. result.current.dispatch({
  140. type: BuilderStateAction.SET_DISPLAY_TYPE,
  141. payload: DisplayType.AREA,
  142. });
  143. });
  144. expect(result.current.state.displayType).toBe(DisplayType.AREA);
  145. expect(result.current.state.fields).toEqual([
  146. {field: 'event.type', alias: undefined, kind: 'field'},
  147. ]);
  148. expect(result.current.state.yAxis).toEqual([
  149. {
  150. function: ['count', '', undefined, undefined],
  151. alias: undefined,
  152. kind: 'function',
  153. },
  154. {
  155. function: ['count_unique', 'user', undefined, undefined],
  156. alias: undefined,
  157. kind: 'function',
  158. },
  159. ]);
  160. });
  161. it('concatenates the values when going from timeseries to table', () => {
  162. mockedUsedLocation.mockReturnValue(
  163. LocationFixture({
  164. query: {
  165. displayType: DisplayType.LINE,
  166. field: ['event.type'],
  167. yAxis: ['count()', 'count_unique(user)'],
  168. },
  169. })
  170. );
  171. const {result} = renderHook(() => useWidgetBuilderState(), {
  172. wrapper: WidgetBuilderProvider,
  173. });
  174. expect(result.current.state.displayType).toBe(DisplayType.LINE);
  175. expect(result.current.state.fields).toEqual([
  176. {field: 'event.type', alias: undefined, kind: 'field'},
  177. ]);
  178. expect(result.current.state.yAxis).toEqual([
  179. {
  180. function: ['count', '', undefined, undefined],
  181. alias: undefined,
  182. kind: 'function',
  183. },
  184. {
  185. function: ['count_unique', 'user', undefined, undefined],
  186. alias: undefined,
  187. kind: 'function',
  188. },
  189. ]);
  190. act(() => {
  191. result.current.dispatch({
  192. type: BuilderStateAction.SET_DISPLAY_TYPE,
  193. payload: DisplayType.TABLE,
  194. });
  195. });
  196. expect(result.current.state.displayType).toBe(DisplayType.TABLE);
  197. expect(result.current.state.fields).toEqual([
  198. {field: 'event.type', alias: undefined, kind: 'field'},
  199. {
  200. function: ['count', '', undefined, undefined],
  201. alias: undefined,
  202. kind: 'function',
  203. },
  204. {
  205. function: ['count_unique', 'user', undefined, undefined],
  206. alias: undefined,
  207. kind: 'function',
  208. },
  209. ]);
  210. });
  211. it('separates the values when going from table to timeseries', () => {
  212. // remember, this takes up to 3 yAxes
  213. mockedUsedLocation.mockReturnValue(
  214. LocationFixture({
  215. query: {
  216. displayType: DisplayType.TABLE,
  217. field: [
  218. 'event.type',
  219. 'potato',
  220. 'count()',
  221. 'count_unique(user)',
  222. 'count_unique(potato)',
  223. 'count_unique(thisIsRemoved)',
  224. ],
  225. },
  226. })
  227. );
  228. const {result} = renderHook(() => useWidgetBuilderState(), {
  229. wrapper: WidgetBuilderProvider,
  230. });
  231. expect(result.current.state.displayType).toBe(DisplayType.TABLE);
  232. expect(result.current.state.fields).toEqual([
  233. {field: 'event.type', alias: undefined, kind: 'field'},
  234. {field: 'potato', alias: undefined, kind: 'field'},
  235. {
  236. function: ['count', '', undefined, undefined],
  237. alias: undefined,
  238. kind: 'function',
  239. },
  240. {
  241. function: ['count_unique', 'user', undefined, undefined],
  242. alias: undefined,
  243. kind: 'function',
  244. },
  245. {
  246. function: ['count_unique', 'potato', undefined, undefined],
  247. alias: undefined,
  248. kind: 'function',
  249. },
  250. {
  251. function: ['count_unique', 'thisIsRemoved', undefined, undefined],
  252. alias: undefined,
  253. kind: 'function',
  254. },
  255. ]);
  256. act(() => {
  257. result.current.dispatch({
  258. type: BuilderStateAction.SET_DISPLAY_TYPE,
  259. payload: DisplayType.LINE,
  260. });
  261. });
  262. expect(result.current.state.displayType).toBe(DisplayType.LINE);
  263. expect(result.current.state.fields).toEqual([
  264. {field: 'event.type', alias: undefined, kind: 'field'},
  265. {field: 'potato', alias: undefined, kind: 'field'},
  266. ]);
  267. expect(result.current.state.yAxis).toEqual([
  268. {
  269. function: ['count', '', undefined, undefined],
  270. alias: undefined,
  271. kind: 'function',
  272. },
  273. {
  274. function: ['count_unique', 'user', undefined, undefined],
  275. alias: undefined,
  276. kind: 'function',
  277. },
  278. {
  279. function: ['count_unique', 'potato', undefined, undefined],
  280. alias: undefined,
  281. kind: 'function',
  282. },
  283. ]);
  284. });
  285. it('does not duplicate fields when switching dataset in line chart then display type to table', () => {
  286. mockedUsedLocation.mockReturnValue(
  287. LocationFixture({
  288. query: {
  289. displayType: DisplayType.LINE,
  290. dataset: WidgetType.ERRORS,
  291. yAxis: ['count()'],
  292. },
  293. })
  294. );
  295. const {result} = renderHook(() => useWidgetBuilderState(), {
  296. wrapper: WidgetBuilderProvider,
  297. });
  298. expect(result.current.state.yAxis).toEqual([
  299. {
  300. function: ['count', '', undefined, undefined],
  301. alias: undefined,
  302. kind: 'function',
  303. },
  304. ]);
  305. act(() => {
  306. result.current.dispatch({
  307. type: BuilderStateAction.SET_DATASET,
  308. payload: WidgetType.TRANSACTIONS,
  309. });
  310. });
  311. expect(result.current.state.yAxis).toEqual([
  312. {
  313. function: ['count', '', undefined, undefined],
  314. alias: undefined,
  315. kind: 'function',
  316. },
  317. ]);
  318. expect(result.current.state.fields).toEqual([]);
  319. act(() => {
  320. result.current.dispatch({
  321. type: BuilderStateAction.SET_DISPLAY_TYPE,
  322. payload: DisplayType.TABLE,
  323. });
  324. });
  325. expect(result.current.state.fields).toEqual([
  326. {
  327. function: ['count', '', undefined, undefined],
  328. alias: undefined,
  329. kind: 'function',
  330. },
  331. ]);
  332. });
  333. it('does not duplicate fields when changing display from table to chart', () => {
  334. mockedUsedLocation.mockReturnValue(
  335. LocationFixture({
  336. query: {
  337. displayType: DisplayType.TABLE,
  338. dataset: WidgetType.ERRORS,
  339. field: ['count()'],
  340. },
  341. })
  342. );
  343. const {result} = renderHook(() => useWidgetBuilderState(), {
  344. wrapper: WidgetBuilderProvider,
  345. });
  346. expect(result.current.state.fields).toEqual([
  347. {
  348. function: ['count', '', undefined, undefined],
  349. alias: undefined,
  350. kind: 'function',
  351. },
  352. ]);
  353. act(() => {
  354. result.current.dispatch({
  355. type: BuilderStateAction.SET_DATASET,
  356. payload: WidgetType.SPANS,
  357. });
  358. });
  359. expect(result.current.state.fields).toEqual([
  360. {
  361. function: ['count', 'span.duration', undefined, undefined],
  362. alias: undefined,
  363. kind: 'function',
  364. },
  365. ]);
  366. expect(result.current.state.yAxis).toEqual([]);
  367. act(() => {
  368. result.current.dispatch({
  369. type: BuilderStateAction.SET_DISPLAY_TYPE,
  370. payload: DisplayType.LINE,
  371. });
  372. });
  373. expect(result.current.state.yAxis).toEqual([
  374. {
  375. function: ['count', 'span.duration', undefined, undefined],
  376. alias: undefined,
  377. kind: 'function',
  378. },
  379. ]);
  380. });
  381. it('does not duplicate fields when switching dataset in big number then display type to table', () => {
  382. mockedUsedLocation.mockReturnValue(
  383. LocationFixture({
  384. query: {
  385. displayType: DisplayType.BIG_NUMBER,
  386. dataset: WidgetType.ERRORS,
  387. field: ['count()'],
  388. },
  389. })
  390. );
  391. const {result} = renderHook(() => useWidgetBuilderState(), {
  392. wrapper: WidgetBuilderProvider,
  393. });
  394. expect(result.current.state.fields).toEqual([
  395. {
  396. function: ['count', '', undefined, undefined],
  397. alias: undefined,
  398. kind: 'function',
  399. },
  400. ]);
  401. act(() => {
  402. result.current.dispatch({
  403. type: BuilderStateAction.SET_DATASET,
  404. payload: WidgetType.TRANSACTIONS,
  405. });
  406. });
  407. expect(result.current.state.fields).toEqual([
  408. {
  409. function: ['count', '', undefined, undefined],
  410. alias: undefined,
  411. kind: 'function',
  412. },
  413. ]);
  414. expect(result.current.state.yAxis).toEqual([]);
  415. act(() => {
  416. result.current.dispatch({
  417. type: BuilderStateAction.SET_DISPLAY_TYPE,
  418. payload: DisplayType.TABLE,
  419. });
  420. });
  421. expect(result.current.state.fields).toEqual([
  422. {
  423. function: ['count', '', undefined, undefined],
  424. alias: undefined,
  425. kind: 'function',
  426. },
  427. ]);
  428. });
  429. it('sets the aggregate as fields when switching to big number', () => {
  430. mockedUsedLocation.mockReturnValue(
  431. LocationFixture({
  432. query: {
  433. displayType: DisplayType.TABLE,
  434. field: ['event.type', 'count()'],
  435. sort: ['-count()'],
  436. },
  437. })
  438. );
  439. const {result} = renderHook(() => useWidgetBuilderState(), {
  440. wrapper: WidgetBuilderProvider,
  441. });
  442. expect(result.current.state.fields).toEqual([
  443. {field: 'event.type', alias: undefined, kind: 'field'},
  444. {
  445. function: ['count', '', undefined, undefined],
  446. alias: undefined,
  447. kind: 'function',
  448. },
  449. ]);
  450. act(() => {
  451. result.current.dispatch({
  452. type: BuilderStateAction.SET_DISPLAY_TYPE,
  453. payload: DisplayType.BIG_NUMBER,
  454. });
  455. });
  456. expect(result.current.state.fields).toEqual([
  457. {
  458. function: ['count', '', undefined, undefined],
  459. alias: undefined,
  460. kind: 'function',
  461. },
  462. ]);
  463. expect(result.current.state.sort).toEqual([]);
  464. });
  465. it('selects the first filter when switching to big number', () => {
  466. mockedUsedLocation.mockReturnValue(
  467. LocationFixture({
  468. query: {
  469. field: ['event.type', 'count()', 'count_unique(user)'],
  470. query: ['event.type:test', 'event.type:test2'],
  471. },
  472. })
  473. );
  474. const {result} = renderHook(() => useWidgetBuilderState(), {
  475. wrapper: WidgetBuilderProvider,
  476. });
  477. expect(result.current.state.query).toEqual(['event.type:test', 'event.type:test2']);
  478. act(() => {
  479. result.current.dispatch({
  480. type: BuilderStateAction.SET_DISPLAY_TYPE,
  481. payload: DisplayType.BIG_NUMBER,
  482. });
  483. });
  484. expect(result.current.state.query).toEqual(['event.type:test']);
  485. });
  486. });
  487. describe('dataset', () => {
  488. it('returns the dataset from the query params', () => {
  489. mockedUsedLocation.mockReturnValue(
  490. LocationFixture({query: {dataset: WidgetType.ISSUE}})
  491. );
  492. const {result} = renderHook(() => useWidgetBuilderState(), {
  493. wrapper: WidgetBuilderProvider,
  494. });
  495. expect(result.current.state.dataset).toBe(WidgetType.ISSUE);
  496. });
  497. it('sets the dataset in the query params', () => {
  498. const {result} = renderHook(() => useWidgetBuilderState(), {
  499. wrapper: WidgetBuilderProvider,
  500. });
  501. act(() => {
  502. result.current.dispatch({
  503. type: BuilderStateAction.SET_DATASET,
  504. payload: WidgetType.METRICS,
  505. });
  506. });
  507. jest.runAllTimers();
  508. expect(mockNavigate).toHaveBeenCalledWith(
  509. expect.objectContaining({
  510. query: expect.objectContaining({dataset: WidgetType.METRICS}),
  511. })
  512. );
  513. });
  514. it('returns errors as the default dataset', () => {
  515. mockedUsedLocation.mockReturnValue(LocationFixture({query: {dataset: 'invalid'}}));
  516. const {result} = renderHook(() => useWidgetBuilderState(), {
  517. wrapper: WidgetBuilderProvider,
  518. });
  519. expect(result.current.state.dataset).toBe(WidgetType.ERRORS);
  520. });
  521. it('resets the display type to table when the dataset is switched to issues', () => {
  522. mockedUsedLocation.mockReturnValue(
  523. LocationFixture({
  524. query: {dataset: WidgetType.TRANSACTIONS, displayType: DisplayType.LINE},
  525. })
  526. );
  527. const {result} = renderHook(() => useWidgetBuilderState(), {
  528. wrapper: WidgetBuilderProvider,
  529. });
  530. expect(result.current.state.displayType).toBe(DisplayType.LINE);
  531. act(() => {
  532. result.current.dispatch({
  533. type: BuilderStateAction.SET_DATASET,
  534. payload: WidgetType.ISSUE,
  535. });
  536. });
  537. expect(result.current.state.displayType).toBe(DisplayType.TABLE);
  538. });
  539. it('resets the fields, yAxis, query, and sort when the dataset is switched', () => {
  540. mockedUsedLocation.mockReturnValue(
  541. LocationFixture({
  542. query: {
  543. title: 'This title should persist',
  544. description: 'This description should persist',
  545. dataset: WidgetType.TRANSACTIONS,
  546. field: ['event.type', 'potato', 'count()'],
  547. yAxis: ['count()', 'count_unique(user)'],
  548. query: ['event.type = "test"'],
  549. sort: ['-testField'],
  550. },
  551. })
  552. );
  553. const {result} = renderHook(() => useWidgetBuilderState(), {
  554. wrapper: WidgetBuilderProvider,
  555. });
  556. act(() => {
  557. result.current.dispatch({
  558. type: BuilderStateAction.SET_DATASET,
  559. payload: WidgetType.SPANS,
  560. });
  561. });
  562. expect(result.current.state.title).toBe('This title should persist');
  563. expect(result.current.state.description).toBe('This description should persist');
  564. expect(result.current.state.fields).toEqual([
  565. {
  566. function: ['count', 'span.duration', undefined, undefined],
  567. alias: undefined,
  568. kind: 'function',
  569. },
  570. ]);
  571. expect(result.current.state.yAxis).toEqual([]);
  572. expect(result.current.state.query).toEqual(['']);
  573. expect(result.current.state.sort).toEqual([
  574. {
  575. field: 'count(span.duration)',
  576. kind: 'desc',
  577. },
  578. ]);
  579. });
  580. it('resets the yAxis when the dataset is switched from anything to issues', () => {
  581. mockedUsedLocation.mockReturnValue(
  582. LocationFixture({
  583. query: {
  584. dataset: WidgetType.TRANSACTIONS,
  585. yAxis: ['count()', 'count_unique(user)'],
  586. displayType: DisplayType.LINE,
  587. },
  588. })
  589. );
  590. const {result} = renderHook(() => useWidgetBuilderState(), {
  591. wrapper: WidgetBuilderProvider,
  592. });
  593. expect(result.current.state.yAxis).toEqual([
  594. {
  595. function: ['count', '', undefined, undefined],
  596. alias: undefined,
  597. kind: 'function',
  598. },
  599. {
  600. function: ['count_unique', 'user', undefined, undefined],
  601. alias: undefined,
  602. kind: 'function',
  603. },
  604. ]);
  605. act(() => {
  606. result.current.dispatch({
  607. type: BuilderStateAction.SET_DATASET,
  608. payload: WidgetType.ISSUE,
  609. });
  610. });
  611. expect(result.current.state.yAxis).toEqual([]);
  612. });
  613. it('resets the sort when the display type is switched and the sort is not in the new fields', () => {
  614. mockedUsedLocation.mockReturnValue(
  615. LocationFixture({
  616. query: {
  617. displayType: DisplayType.LINE,
  618. field: ['testField', 'testField2'],
  619. sort: ['-project.name'],
  620. },
  621. })
  622. );
  623. const {result} = renderHook(() => useWidgetBuilderState(), {
  624. wrapper: WidgetBuilderProvider,
  625. });
  626. expect(result.current.state.sort).toEqual([{field: 'project.name', kind: 'desc'}]);
  627. act(() => {
  628. result.current.dispatch({
  629. type: BuilderStateAction.SET_DISPLAY_TYPE,
  630. payload: DisplayType.TABLE,
  631. });
  632. });
  633. expect(result.current.state.sort).toEqual([
  634. {
  635. field: 'testField',
  636. kind: 'desc',
  637. },
  638. ]);
  639. });
  640. it('keeps sort when the sort is in the new fields', () => {
  641. mockedUsedLocation.mockReturnValue(
  642. LocationFixture({
  643. query: {
  644. displayType: DisplayType.LINE,
  645. field: ['testField', 'testField2'],
  646. sort: ['-testField'],
  647. },
  648. })
  649. );
  650. const {result} = renderHook(() => useWidgetBuilderState(), {
  651. wrapper: WidgetBuilderProvider,
  652. });
  653. expect(result.current.state.sort).toEqual([{field: 'testField', kind: 'desc'}]);
  654. act(() => {
  655. result.current.dispatch({
  656. type: BuilderStateAction.SET_DISPLAY_TYPE,
  657. payload: DisplayType.TABLE,
  658. });
  659. });
  660. expect(result.current.state.sort).toEqual([
  661. {
  662. field: 'testField',
  663. kind: 'desc',
  664. },
  665. ]);
  666. });
  667. });
  668. describe('fields', () => {
  669. it('returns the fields from the query params', () => {
  670. mockedUsedLocation.mockReturnValue(
  671. LocationFixture({query: {field: ['event.type', 'potato', 'count()']}})
  672. );
  673. const {result} = renderHook(() => useWidgetBuilderState(), {
  674. wrapper: WidgetBuilderProvider,
  675. });
  676. expect(result.current.state.fields).toEqual([
  677. {field: 'event.type', alias: undefined, kind: 'field'},
  678. {field: 'potato', alias: undefined, kind: 'field'},
  679. {
  680. alias: undefined,
  681. kind: 'function',
  682. function: ['count', '', undefined, undefined],
  683. },
  684. ]);
  685. });
  686. it('decodes both JSON formatted fields and non-JSON formatted fields', () => {
  687. mockedUsedLocation.mockReturnValue(
  688. LocationFixture({
  689. query: {
  690. field: [
  691. '{"field": "event.type", "alias": "test"}',
  692. 'p90(transaction.duration)',
  693. ],
  694. },
  695. })
  696. );
  697. const {result} = renderHook(() => useWidgetBuilderState(), {
  698. wrapper: WidgetBuilderProvider,
  699. });
  700. expect(result.current.state.fields).toEqual([
  701. {field: 'event.type', alias: 'test', kind: 'field'},
  702. {
  703. function: ['p90', 'transaction.duration', undefined, undefined],
  704. alias: undefined,
  705. kind: 'function',
  706. },
  707. ]);
  708. });
  709. it('encodes fields to JSON when they have aliases', () => {
  710. const fields = [
  711. {field: 'event.type', alias: 'test', kind: FieldValueKind.FIELD},
  712. {field: 'event.type', alias: undefined, kind: FieldValueKind.FIELD},
  713. ] as Column[];
  714. const encodedFields = serializeFields(fields);
  715. expect(encodedFields).toEqual([
  716. '{"field":"event.type","alias":"test"}',
  717. 'event.type',
  718. ]);
  719. });
  720. it('resets the sort when the field that is being sorted is removed', () => {
  721. mockedUsedLocation.mockReturnValue(
  722. LocationFixture({
  723. query: {field: ['testField'], sort: ['-testField']},
  724. })
  725. );
  726. const {result} = renderHook(() => useWidgetBuilderState(), {
  727. wrapper: WidgetBuilderProvider,
  728. });
  729. expect(result.current.state.sort).toEqual([{field: 'testField', kind: 'desc'}]);
  730. act(() => {
  731. result.current.dispatch({
  732. type: BuilderStateAction.SET_FIELDS,
  733. payload: [{field: 'testField2', kind: FieldValueKind.FIELD}],
  734. });
  735. });
  736. expect(result.current.state.sort).toEqual([{field: 'testField2', kind: 'desc'}]);
  737. });
  738. it('modifies the sort when the field that is being sorted is modified', () => {
  739. mockedUsedLocation.mockReturnValue(
  740. LocationFixture({
  741. query: {field: ['testField', 'sortField'], sort: ['-sortField']},
  742. })
  743. );
  744. const {result} = renderHook(() => useWidgetBuilderState(), {
  745. wrapper: WidgetBuilderProvider,
  746. });
  747. expect(result.current.state.sort).toEqual([{field: 'sortField', kind: 'desc'}]);
  748. act(() => {
  749. result.current.dispatch({
  750. type: BuilderStateAction.SET_FIELDS,
  751. payload: [
  752. {field: 'testField', kind: FieldValueKind.FIELD},
  753. {field: 'newSortField', kind: FieldValueKind.FIELD},
  754. ],
  755. });
  756. });
  757. expect(result.current.state.sort).toEqual([{field: 'newSortField', kind: 'desc'}]);
  758. });
  759. it('does not reset the table sort for issue widgets', () => {
  760. mockedUsedLocation.mockReturnValue(
  761. LocationFixture({
  762. query: {
  763. dataset: WidgetType.ISSUE,
  764. field: ['testField'],
  765. sort: ['-notInFields'],
  766. },
  767. })
  768. );
  769. const {result} = renderHook(() => useWidgetBuilderState(), {
  770. wrapper: WidgetBuilderProvider,
  771. });
  772. expect(result.current.state.sort).toEqual([{field: 'notInFields', kind: 'desc'}]);
  773. act(() => {
  774. result.current.dispatch({
  775. type: BuilderStateAction.SET_FIELDS,
  776. payload: [{field: 'testField', kind: FieldValueKind.FIELD}],
  777. });
  778. });
  779. expect(result.current.state.sort).toEqual([{field: 'notInFields', kind: 'desc'}]);
  780. });
  781. });
  782. describe('yAxis', () => {
  783. it('does not conflict with fields when setting the state', () => {
  784. mockedUsedLocation.mockReturnValue(
  785. LocationFixture({
  786. query: {
  787. field: ['event.type', 'potato', 'count()'],
  788. yAxis: ['count()', 'count_unique(user)'],
  789. },
  790. })
  791. );
  792. const {result} = renderHook(() => useWidgetBuilderState(), {
  793. wrapper: WidgetBuilderProvider,
  794. });
  795. expect(result.current.state.fields).toEqual([
  796. {field: 'event.type', alias: undefined, kind: 'field'},
  797. {field: 'potato', alias: undefined, kind: 'field'},
  798. {
  799. function: ['count', '', undefined, undefined],
  800. alias: undefined,
  801. kind: 'function',
  802. },
  803. ]);
  804. expect(result.current.state.yAxis).toEqual([
  805. {
  806. function: ['count', '', undefined, undefined],
  807. alias: undefined,
  808. kind: 'function',
  809. },
  810. {
  811. function: ['count_unique', 'user', undefined, undefined],
  812. alias: undefined,
  813. kind: 'function',
  814. },
  815. ]);
  816. });
  817. });
  818. describe('sort', () => {
  819. it('can decode and update sorts', () => {
  820. mockedUsedLocation.mockReturnValue(
  821. LocationFixture({
  822. query: {
  823. sort: ['-testField'],
  824. },
  825. })
  826. );
  827. const {result} = renderHook(() => useWidgetBuilderState(), {
  828. wrapper: WidgetBuilderProvider,
  829. });
  830. expect(result.current.state.sort).toEqual([{field: 'testField', kind: 'desc'}]);
  831. act(() => {
  832. result.current.dispatch({
  833. type: BuilderStateAction.SET_SORT,
  834. payload: [{field: 'testField', kind: 'asc'}],
  835. });
  836. });
  837. expect(result.current.state.sort).toEqual([{field: 'testField', kind: 'asc'}]);
  838. });
  839. });
  840. describe('limit', () => {
  841. it('can decode and update limit', () => {
  842. mockedUsedLocation.mockReturnValue(
  843. LocationFixture({
  844. query: {
  845. limit: '4',
  846. },
  847. })
  848. );
  849. const {result} = renderHook(() => useWidgetBuilderState(), {
  850. wrapper: WidgetBuilderProvider,
  851. });
  852. expect(result.current.state.limit).toBe(4);
  853. act(() => {
  854. result.current.dispatch({
  855. type: BuilderStateAction.SET_LIMIT,
  856. payload: 10,
  857. });
  858. });
  859. expect(result.current.state.limit).toBe(10);
  860. });
  861. });
  862. describe('legendAlias', () => {
  863. it('can decode and update legendAlias', () => {
  864. mockedUsedLocation.mockReturnValue(
  865. LocationFixture({
  866. query: {
  867. legendAlias: ['test', 'test2'],
  868. },
  869. })
  870. );
  871. const {result} = renderHook(() => useWidgetBuilderState(), {
  872. wrapper: WidgetBuilderProvider,
  873. });
  874. expect(result.current.state.legendAlias).toEqual(['test', 'test2']);
  875. act(() => {
  876. result.current.dispatch({
  877. type: BuilderStateAction.SET_LEGEND_ALIAS,
  878. payload: ['test3', 'test4'],
  879. });
  880. });
  881. expect(result.current.state.legendAlias).toEqual(['test3', 'test4']);
  882. });
  883. });
  884. });