SuperChart.test.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. /*
  2. * Licensed to the Apache Software Foundation (ASF) under one
  3. * or more contributor license agreements. See the NOTICE file
  4. * distributed with this work for additional information
  5. * regarding copyright ownership. The ASF licenses this file
  6. * to you under the Apache License, Version 2.0 (the
  7. * "License"); you may not use this file except in compliance
  8. * with the License. You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing,
  13. * software distributed under the License is distributed on an
  14. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  15. * KIND, either express or implied. See the License for the
  16. * specific language governing permissions and limitations
  17. * under the License.
  18. */
  19. import '@testing-library/jest-dom';
  20. import { render, screen } from '@superset-ui/core/spec';
  21. import mockConsole, { RestoreConsole } from 'jest-mock-console';
  22. import { triggerResizeObserver } from 'resize-observer-polyfill';
  23. import { ErrorBoundary } from 'react-error-boundary';
  24. import { promiseTimeout, SuperChart } from '@superset-ui/core';
  25. import { WrapperProps } from '../../../src/chart/components/SuperChart';
  26. import {
  27. ChartKeys,
  28. DiligentChartPlugin,
  29. BuggyChartPlugin,
  30. } from './MockChartPlugins';
  31. import { isMatrixifyEnabled } from '../../../src/chart/types/matrixify';
  32. import MatrixifyGridRenderer from '../../../src/chart/components/Matrixify/MatrixifyGridRenderer';
  33. // Mock Matrixify imports
  34. jest.mock('../../../src/chart/types/matrixify', () => ({
  35. isMatrixifyEnabled: jest.fn(() => false),
  36. getMatrixifyConfig: jest.fn(() => null),
  37. }));
  38. jest.mock(
  39. '../../../src/chart/components/Matrixify/MatrixifyGridRenderer',
  40. () => ({
  41. __esModule: true,
  42. default: jest.fn(() => null),
  43. }),
  44. );
  45. const DEFAULT_QUERY_DATA = { data: ['foo', 'bar'] };
  46. const DEFAULT_QUERIES_DATA = [
  47. { data: ['foo', 'bar'] },
  48. { data: ['foo2', 'bar2'] },
  49. ];
  50. // Fix for expect outside test block - move expectDimension into a test utility
  51. // Replace expectDimension function with a non-expect version
  52. function getDimensionText(container: HTMLElement) {
  53. const dimensionEl = container.querySelector('.dimension');
  54. return dimensionEl?.textContent || '';
  55. }
  56. describe('SuperChart', () => {
  57. jest.setTimeout(5000);
  58. let restoreConsole: RestoreConsole;
  59. const plugins = [
  60. new DiligentChartPlugin().configure({ key: ChartKeys.DILIGENT }),
  61. new BuggyChartPlugin().configure({ key: ChartKeys.BUGGY }),
  62. ];
  63. beforeAll(() => {
  64. plugins.forEach(p => {
  65. p.unregister().register();
  66. });
  67. });
  68. beforeEach(() => {
  69. restoreConsole = mockConsole();
  70. triggerResizeObserver([]); // Reset any pending resize observers
  71. });
  72. afterEach(() => {
  73. restoreConsole();
  74. });
  75. describe('includes ErrorBoundary', () => {
  76. let expectedErrors = 0;
  77. let actualErrors = 0;
  78. function onError(e: Event) {
  79. e.preventDefault();
  80. actualErrors += 1;
  81. }
  82. beforeEach(() => {
  83. expectedErrors = 0;
  84. actualErrors = 0;
  85. window.addEventListener('error', onError);
  86. });
  87. afterEach(() => {
  88. window.removeEventListener('error', onError);
  89. });
  90. it('should have correct number of errors', () => {
  91. expect(actualErrors).toBe(expectedErrors);
  92. expectedErrors = 0;
  93. });
  94. it('renders default FallbackComponent', async () => {
  95. expectedErrors = 1;
  96. render(
  97. <SuperChart
  98. chartType={ChartKeys.BUGGY}
  99. queriesData={[DEFAULT_QUERY_DATA]}
  100. width="200"
  101. height="200"
  102. />,
  103. );
  104. expect(
  105. await screen.findByText('Oops! An error occurred!'),
  106. ).toBeInTheDocument();
  107. });
  108. it('renders custom FallbackComponent', async () => {
  109. expectedErrors = 1;
  110. const CustomFallbackComponent = jest.fn(() => (
  111. <div>Custom Fallback!</div>
  112. ));
  113. render(
  114. <SuperChart
  115. chartType={ChartKeys.BUGGY}
  116. queriesData={[DEFAULT_QUERY_DATA]}
  117. width="200"
  118. height="200"
  119. FallbackComponent={CustomFallbackComponent}
  120. />,
  121. );
  122. expect(await screen.findByText('Custom Fallback!')).toBeInTheDocument();
  123. expect(CustomFallbackComponent).toHaveBeenCalledTimes(1);
  124. });
  125. it('call onErrorBoundary', async () => {
  126. expectedErrors = 1;
  127. const handleError = jest.fn();
  128. render(
  129. <SuperChart
  130. chartType={ChartKeys.BUGGY}
  131. queriesData={[DEFAULT_QUERY_DATA]}
  132. width="200"
  133. height="200"
  134. onErrorBoundary={handleError}
  135. />,
  136. );
  137. await screen.findByText('Oops! An error occurred!');
  138. expect(handleError).toHaveBeenCalledTimes(1);
  139. });
  140. // Update the test cases
  141. it('does not include ErrorBoundary if told so', async () => {
  142. expectedErrors = 1;
  143. const inactiveErrorHandler = jest.fn();
  144. const activeErrorHandler = jest.fn();
  145. render(
  146. <ErrorBoundary
  147. fallbackRender={() => <div>Error!</div>}
  148. onError={activeErrorHandler}
  149. >
  150. <SuperChart
  151. disableErrorBoundary
  152. chartType={ChartKeys.BUGGY}
  153. queriesData={[DEFAULT_QUERY_DATA]}
  154. width="200"
  155. height="200"
  156. onErrorBoundary={inactiveErrorHandler}
  157. />
  158. </ErrorBoundary>,
  159. );
  160. await screen.findByText('Error!');
  161. expect(activeErrorHandler).toHaveBeenCalledTimes(1);
  162. expect(inactiveErrorHandler).not.toHaveBeenCalled();
  163. });
  164. });
  165. // Helper function to find elements by class name
  166. const findByClassName = (container: HTMLElement, className: string) =>
  167. container.querySelector(`.${className}`);
  168. // Update test cases
  169. // Update timeout for all async tests
  170. jest.setTimeout(10000);
  171. // Update the props test to wait for component to render
  172. it('passes the props to renderer correctly', async () => {
  173. const { container } = render(
  174. <SuperChart
  175. chartType={ChartKeys.DILIGENT}
  176. queriesData={[DEFAULT_QUERY_DATA]}
  177. width={101}
  178. height={118}
  179. formData={{ abc: 1 }}
  180. />,
  181. );
  182. await promiseTimeout(() => {
  183. const testComponent = findByClassName(container, 'test-component');
  184. expect(testComponent).not.toBeNull();
  185. expect(testComponent).toBeInTheDocument();
  186. expect(getDimensionText(container)).toBe('101x118');
  187. });
  188. });
  189. // Helper function to create a sized wrapper
  190. const createSizedWrapper = () => {
  191. const wrapper = document.createElement('div');
  192. wrapper.style.width = '300px';
  193. wrapper.style.height = '300px';
  194. wrapper.style.position = 'relative';
  195. wrapper.style.display = 'block';
  196. return wrapper;
  197. };
  198. // Update dimension tests to wait for resize observer
  199. // First, increase the timeout for all tests
  200. jest.setTimeout(20000);
  201. // Update the waitForDimensions helper to include a retry mechanism
  202. // Update waitForDimensions to avoid await in loop
  203. const waitForDimensions = async (
  204. container: HTMLElement,
  205. expectedWidth: number,
  206. expectedHeight: number,
  207. ) => {
  208. const maxAttempts = 5;
  209. const interval = 100;
  210. return new Promise<void>((resolve, reject) => {
  211. let attempts = 0;
  212. const checkDimension = () => {
  213. const testComponent = container.querySelector('.test-component');
  214. const dimensionEl = container.querySelector('.dimension');
  215. if (!testComponent || !dimensionEl) {
  216. if (attempts >= maxAttempts) {
  217. reject(new Error('Elements not found'));
  218. return;
  219. }
  220. attempts += 1;
  221. setTimeout(checkDimension, interval);
  222. return;
  223. }
  224. if (dimensionEl.textContent !== `${expectedWidth}x${expectedHeight}`) {
  225. if (attempts >= maxAttempts) {
  226. reject(new Error('Dimension mismatch'));
  227. return;
  228. }
  229. attempts += 1;
  230. setTimeout(checkDimension, interval);
  231. return;
  232. }
  233. resolve();
  234. };
  235. checkDimension();
  236. });
  237. };
  238. // Update the resize observer trigger to ensure it's called after component mount
  239. it.skip('works when width and height are percent', async () => {
  240. const { container } = render(
  241. <SuperChart
  242. chartType={ChartKeys.DILIGENT}
  243. queriesData={[DEFAULT_QUERY_DATA]}
  244. debounceTime={1}
  245. width="100%"
  246. height="100%"
  247. />,
  248. );
  249. // Wait for initial render
  250. await new Promise(resolve => setTimeout(resolve, 50));
  251. triggerResizeObserver([
  252. {
  253. contentRect: {
  254. width: 300,
  255. height: 300,
  256. top: 0,
  257. left: 0,
  258. right: 300,
  259. bottom: 300,
  260. x: 0,
  261. y: 0,
  262. toJSON() {
  263. return {
  264. width: this.width,
  265. height: this.height,
  266. top: this.top,
  267. left: this.left,
  268. right: this.right,
  269. bottom: this.bottom,
  270. x: this.x,
  271. y: this.y,
  272. };
  273. },
  274. },
  275. borderBoxSize: [{ blockSize: 300, inlineSize: 300 }],
  276. contentBoxSize: [{ blockSize: 300, inlineSize: 300 }],
  277. devicePixelContentBoxSize: [{ blockSize: 300, inlineSize: 300 }],
  278. target: document.createElement('div'),
  279. },
  280. ]);
  281. await waitForDimensions(container, 300, 300);
  282. });
  283. it('passes the props with multiple queries to renderer correctly', async () => {
  284. const { container } = render(
  285. <SuperChart
  286. chartType={ChartKeys.DILIGENT}
  287. queriesData={DEFAULT_QUERIES_DATA}
  288. width={101}
  289. height={118}
  290. formData={{ abc: 1 }}
  291. />,
  292. );
  293. await promiseTimeout(() => {
  294. const testComponent = container.querySelector('.test-component');
  295. expect(testComponent).not.toBeNull();
  296. expect(testComponent).toBeInTheDocument();
  297. expect(getDimensionText(container)).toBe('101x118');
  298. });
  299. });
  300. describe('supports NoResultsComponent', () => {
  301. it('renders NoResultsComponent when queriesData is missing', () => {
  302. render(
  303. <SuperChart chartType={ChartKeys.DILIGENT} width="200" height="200" />,
  304. );
  305. expect(screen.getByText('No Results')).toBeInTheDocument();
  306. });
  307. it('renders NoResultsComponent when queriesData data is null', () => {
  308. render(
  309. <SuperChart
  310. chartType={ChartKeys.DILIGENT}
  311. queriesData={[{ data: null }]}
  312. width="200"
  313. height="200"
  314. />,
  315. );
  316. expect(screen.getByText('No Results')).toBeInTheDocument();
  317. });
  318. });
  319. describe('supports dynamic width and/or height', () => {
  320. // Add MyWrapper component definition
  321. function MyWrapper({ width, height, children }: WrapperProps) {
  322. return (
  323. <div>
  324. <div className="wrapper-insert">
  325. {width}x{height}
  326. </div>
  327. {children}
  328. </div>
  329. );
  330. }
  331. it('works with width and height that are numbers', async () => {
  332. const { container } = render(
  333. <SuperChart
  334. chartType={ChartKeys.DILIGENT}
  335. queriesData={[DEFAULT_QUERY_DATA]}
  336. width={100}
  337. height={100}
  338. />,
  339. );
  340. await promiseTimeout(() => {
  341. const testComponent = container.querySelector('.test-component');
  342. expect(testComponent).not.toBeNull();
  343. expect(testComponent).toBeInTheDocument();
  344. expect(getDimensionText(container)).toBe('100x100');
  345. });
  346. });
  347. it.skip('works when width and height are percent', async () => {
  348. const wrapper = createSizedWrapper();
  349. document.body.appendChild(wrapper);
  350. const { container } = render(
  351. <div style={{ width: '100%', height: '100%', position: 'absolute' }}>
  352. <SuperChart
  353. chartType={ChartKeys.DILIGENT}
  354. queriesData={[DEFAULT_QUERY_DATA]}
  355. debounceTime={1}
  356. width="100%"
  357. height="100%"
  358. Wrapper={MyWrapper}
  359. />
  360. </div>,
  361. );
  362. wrapper.appendChild(container);
  363. // Wait for initial render
  364. await new Promise(resolve => setTimeout(resolve, 100));
  365. // Trigger resize
  366. triggerResizeObserver([
  367. {
  368. contentRect: {
  369. width: 300,
  370. height: 300,
  371. top: 0,
  372. left: 0,
  373. right: 300,
  374. bottom: 300,
  375. x: 0,
  376. y: 0,
  377. toJSON() {
  378. return this;
  379. },
  380. },
  381. borderBoxSize: [{ blockSize: 300, inlineSize: 300 }],
  382. contentBoxSize: [{ blockSize: 300, inlineSize: 300 }],
  383. devicePixelContentBoxSize: [{ blockSize: 300, inlineSize: 300 }],
  384. target: wrapper,
  385. },
  386. ]);
  387. // Wait for resize to be processed
  388. await new Promise(resolve => setTimeout(resolve, 200));
  389. // Check dimensions
  390. const wrapperInsert = container.querySelector('.wrapper-insert');
  391. expect(wrapperInsert).not.toBeNull();
  392. expect(wrapperInsert).toBeInTheDocument();
  393. expect(wrapperInsert).toHaveTextContent('300x300');
  394. await waitForDimensions(container, 300, 300);
  395. document.body.removeChild(wrapper);
  396. }, 30000);
  397. });
  398. it('should render MatrixifyGridRenderer when matrixify is enabled with empty data', () => {
  399. const mockIsMatrixifyEnabled = isMatrixifyEnabled as jest.MockedFunction<
  400. typeof isMatrixifyEnabled
  401. >;
  402. const mockMatrixifyGridRenderer =
  403. MatrixifyGridRenderer as jest.MockedFunction<
  404. typeof MatrixifyGridRenderer
  405. >;
  406. mockIsMatrixifyEnabled.mockReturnValue(true);
  407. render(
  408. <SuperChart
  409. chartType={ChartKeys.DILIGENT}
  410. width="200"
  411. height="200"
  412. queriesData={[{ data: [] }]}
  413. enableNoResults
  414. />,
  415. );
  416. expect(mockMatrixifyGridRenderer).toHaveBeenCalled();
  417. expect(screen.queryByText('No Results')).not.toBeInTheDocument();
  418. });
  419. it('should render MatrixifyGridRenderer when matrixify is enabled with null data', () => {
  420. const mockIsMatrixifyEnabled = isMatrixifyEnabled as jest.MockedFunction<
  421. typeof isMatrixifyEnabled
  422. >;
  423. const mockMatrixifyGridRenderer =
  424. MatrixifyGridRenderer as jest.MockedFunction<
  425. typeof MatrixifyGridRenderer
  426. >;
  427. mockIsMatrixifyEnabled.mockReturnValue(true);
  428. render(
  429. <SuperChart
  430. chartType={ChartKeys.DILIGENT}
  431. width="200"
  432. height="200"
  433. queriesData={[{ data: null }]}
  434. enableNoResults
  435. />,
  436. );
  437. expect(mockMatrixifyGridRenderer).toHaveBeenCalled();
  438. expect(screen.queryByText('No Results')).not.toBeInTheDocument();
  439. });
  440. it('should ignore custom noResults component when matrixify is enabled', () => {
  441. const mockIsMatrixifyEnabled = isMatrixifyEnabled as jest.MockedFunction<
  442. typeof isMatrixifyEnabled
  443. >;
  444. const mockMatrixifyGridRenderer =
  445. MatrixifyGridRenderer as jest.MockedFunction<
  446. typeof MatrixifyGridRenderer
  447. >;
  448. mockIsMatrixifyEnabled.mockReturnValue(true);
  449. const CustomNoResults = () => <div>Custom No Data Message</div>;
  450. render(
  451. <SuperChart
  452. chartType={ChartKeys.DILIGENT}
  453. width="200"
  454. height="200"
  455. queriesData={[{ data: [] }]}
  456. enableNoResults
  457. noResults={<CustomNoResults />}
  458. />,
  459. );
  460. expect(mockMatrixifyGridRenderer).toHaveBeenCalled();
  461. expect(
  462. screen.queryByText('Custom No Data Message'),
  463. ).not.toBeInTheDocument();
  464. });
  465. it('should apply error boundary to matrixify grid renderer', () => {
  466. const mockIsMatrixifyEnabled = isMatrixifyEnabled as jest.MockedFunction<
  467. typeof isMatrixifyEnabled
  468. >;
  469. const mockMatrixifyGridRenderer =
  470. MatrixifyGridRenderer as jest.MockedFunction<
  471. typeof MatrixifyGridRenderer
  472. >;
  473. mockIsMatrixifyEnabled.mockReturnValue(true);
  474. const onErrorBoundary = jest.fn();
  475. render(
  476. <SuperChart
  477. chartType={ChartKeys.DILIGENT}
  478. width="200"
  479. height="200"
  480. queriesData={[{ data: [] }]}
  481. enableNoResults
  482. onErrorBoundary={onErrorBoundary}
  483. />,
  484. );
  485. expect(mockMatrixifyGridRenderer).toHaveBeenCalled();
  486. expect(onErrorBoundary).not.toHaveBeenCalled();
  487. });
  488. });