/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import '@testing-library/jest-dom'; import { render, screen } from '@superset-ui/core/spec'; import mockConsole, { RestoreConsole } from 'jest-mock-console'; import { triggerResizeObserver } from 'resize-observer-polyfill'; import { ErrorBoundary } from 'react-error-boundary'; import { promiseTimeout, SuperChart } from '@superset-ui/core'; import { WrapperProps } from '../../../src/chart/components/SuperChart'; import { ChartKeys, DiligentChartPlugin, BuggyChartPlugin, } from './MockChartPlugins'; import { isMatrixifyEnabled } from '../../../src/chart/types/matrixify'; import MatrixifyGridRenderer from '../../../src/chart/components/Matrixify/MatrixifyGridRenderer'; // Mock Matrixify imports jest.mock('../../../src/chart/types/matrixify', () => ({ isMatrixifyEnabled: jest.fn(() => false), getMatrixifyConfig: jest.fn(() => null), })); jest.mock( '../../../src/chart/components/Matrixify/MatrixifyGridRenderer', () => ({ __esModule: true, default: jest.fn(() => null), }), ); const DEFAULT_QUERY_DATA = { data: ['foo', 'bar'] }; const DEFAULT_QUERIES_DATA = [ { data: ['foo', 'bar'] }, { data: ['foo2', 'bar2'] }, ]; // Fix for expect outside test block - move expectDimension into a test utility // Replace expectDimension function with a non-expect version function getDimensionText(container: HTMLElement) { const dimensionEl = container.querySelector('.dimension'); return dimensionEl?.textContent || ''; } describe('SuperChart', () => { jest.setTimeout(5000); let restoreConsole: RestoreConsole; const plugins = [ new DiligentChartPlugin().configure({ key: ChartKeys.DILIGENT }), new BuggyChartPlugin().configure({ key: ChartKeys.BUGGY }), ]; beforeAll(() => { plugins.forEach(p => { p.unregister().register(); }); }); beforeEach(() => { restoreConsole = mockConsole(); triggerResizeObserver([]); // Reset any pending resize observers }); afterEach(() => { restoreConsole(); }); describe('includes ErrorBoundary', () => { let expectedErrors = 0; let actualErrors = 0; function onError(e: Event) { e.preventDefault(); actualErrors += 1; } beforeEach(() => { expectedErrors = 0; actualErrors = 0; window.addEventListener('error', onError); }); afterEach(() => { window.removeEventListener('error', onError); }); it('should have correct number of errors', () => { expect(actualErrors).toBe(expectedErrors); expectedErrors = 0; }); it('renders default FallbackComponent', async () => { expectedErrors = 1; render( , ); expect( await screen.findByText('Oops! An error occurred!'), ).toBeInTheDocument(); }); it('renders custom FallbackComponent', async () => { expectedErrors = 1; const CustomFallbackComponent = jest.fn(() => (
Custom Fallback!
)); render( , ); expect(await screen.findByText('Custom Fallback!')).toBeInTheDocument(); expect(CustomFallbackComponent).toHaveBeenCalledTimes(1); }); it('call onErrorBoundary', async () => { expectedErrors = 1; const handleError = jest.fn(); render( , ); await screen.findByText('Oops! An error occurred!'); expect(handleError).toHaveBeenCalledTimes(1); }); // Update the test cases it('does not include ErrorBoundary if told so', async () => { expectedErrors = 1; const inactiveErrorHandler = jest.fn(); const activeErrorHandler = jest.fn(); render(
Error!
} onError={activeErrorHandler} >
, ); await screen.findByText('Error!'); expect(activeErrorHandler).toHaveBeenCalledTimes(1); expect(inactiveErrorHandler).not.toHaveBeenCalled(); }); }); // Helper function to find elements by class name const findByClassName = (container: HTMLElement, className: string) => container.querySelector(`.${className}`); // Update test cases // Update timeout for all async tests jest.setTimeout(10000); // Update the props test to wait for component to render it('passes the props to renderer correctly', async () => { const { container } = render( , ); await promiseTimeout(() => { const testComponent = findByClassName(container, 'test-component'); expect(testComponent).not.toBeNull(); expect(testComponent).toBeInTheDocument(); expect(getDimensionText(container)).toBe('101x118'); }); }); // Helper function to create a sized wrapper const createSizedWrapper = () => { const wrapper = document.createElement('div'); wrapper.style.width = '300px'; wrapper.style.height = '300px'; wrapper.style.position = 'relative'; wrapper.style.display = 'block'; return wrapper; }; // Update dimension tests to wait for resize observer // First, increase the timeout for all tests jest.setTimeout(20000); // Update the waitForDimensions helper to include a retry mechanism // Update waitForDimensions to avoid await in loop const waitForDimensions = async ( container: HTMLElement, expectedWidth: number, expectedHeight: number, ) => { const maxAttempts = 5; const interval = 100; return new Promise((resolve, reject) => { let attempts = 0; const checkDimension = () => { const testComponent = container.querySelector('.test-component'); const dimensionEl = container.querySelector('.dimension'); if (!testComponent || !dimensionEl) { if (attempts >= maxAttempts) { reject(new Error('Elements not found')); return; } attempts += 1; setTimeout(checkDimension, interval); return; } if (dimensionEl.textContent !== `${expectedWidth}x${expectedHeight}`) { if (attempts >= maxAttempts) { reject(new Error('Dimension mismatch')); return; } attempts += 1; setTimeout(checkDimension, interval); return; } resolve(); }; checkDimension(); }); }; // Update the resize observer trigger to ensure it's called after component mount it.skip('works when width and height are percent', async () => { const { container } = render( , ); // Wait for initial render await new Promise(resolve => setTimeout(resolve, 50)); triggerResizeObserver([ { contentRect: { width: 300, height: 300, top: 0, left: 0, right: 300, bottom: 300, x: 0, y: 0, toJSON() { return { width: this.width, height: this.height, top: this.top, left: this.left, right: this.right, bottom: this.bottom, x: this.x, y: this.y, }; }, }, borderBoxSize: [{ blockSize: 300, inlineSize: 300 }], contentBoxSize: [{ blockSize: 300, inlineSize: 300 }], devicePixelContentBoxSize: [{ blockSize: 300, inlineSize: 300 }], target: document.createElement('div'), }, ]); await waitForDimensions(container, 300, 300); }); it('passes the props with multiple queries to renderer correctly', async () => { const { container } = render( , ); await promiseTimeout(() => { const testComponent = container.querySelector('.test-component'); expect(testComponent).not.toBeNull(); expect(testComponent).toBeInTheDocument(); expect(getDimensionText(container)).toBe('101x118'); }); }); describe('supports NoResultsComponent', () => { it('renders NoResultsComponent when queriesData is missing', () => { render( , ); expect(screen.getByText('No Results')).toBeInTheDocument(); }); it('renders NoResultsComponent when queriesData data is null', () => { render( , ); expect(screen.getByText('No Results')).toBeInTheDocument(); }); }); describe('supports dynamic width and/or height', () => { // Add MyWrapper component definition function MyWrapper({ width, height, children }: WrapperProps) { return (
{width}x{height}
{children}
); } it('works with width and height that are numbers', async () => { const { container } = render( , ); await promiseTimeout(() => { const testComponent = container.querySelector('.test-component'); expect(testComponent).not.toBeNull(); expect(testComponent).toBeInTheDocument(); expect(getDimensionText(container)).toBe('100x100'); }); }); it.skip('works when width and height are percent', async () => { const wrapper = createSizedWrapper(); document.body.appendChild(wrapper); const { container } = render(
, ); wrapper.appendChild(container); // Wait for initial render await new Promise(resolve => setTimeout(resolve, 100)); // Trigger resize triggerResizeObserver([ { contentRect: { width: 300, height: 300, top: 0, left: 0, right: 300, bottom: 300, x: 0, y: 0, toJSON() { return this; }, }, borderBoxSize: [{ blockSize: 300, inlineSize: 300 }], contentBoxSize: [{ blockSize: 300, inlineSize: 300 }], devicePixelContentBoxSize: [{ blockSize: 300, inlineSize: 300 }], target: wrapper, }, ]); // Wait for resize to be processed await new Promise(resolve => setTimeout(resolve, 200)); // Check dimensions const wrapperInsert = container.querySelector('.wrapper-insert'); expect(wrapperInsert).not.toBeNull(); expect(wrapperInsert).toBeInTheDocument(); expect(wrapperInsert).toHaveTextContent('300x300'); await waitForDimensions(container, 300, 300); document.body.removeChild(wrapper); }, 30000); }); it('should render MatrixifyGridRenderer when matrixify is enabled with empty data', () => { const mockIsMatrixifyEnabled = isMatrixifyEnabled as jest.MockedFunction< typeof isMatrixifyEnabled >; const mockMatrixifyGridRenderer = MatrixifyGridRenderer as jest.MockedFunction< typeof MatrixifyGridRenderer >; mockIsMatrixifyEnabled.mockReturnValue(true); render( , ); expect(mockMatrixifyGridRenderer).toHaveBeenCalled(); expect(screen.queryByText('No Results')).not.toBeInTheDocument(); }); it('should render MatrixifyGridRenderer when matrixify is enabled with null data', () => { const mockIsMatrixifyEnabled = isMatrixifyEnabled as jest.MockedFunction< typeof isMatrixifyEnabled >; const mockMatrixifyGridRenderer = MatrixifyGridRenderer as jest.MockedFunction< typeof MatrixifyGridRenderer >; mockIsMatrixifyEnabled.mockReturnValue(true); render( , ); expect(mockMatrixifyGridRenderer).toHaveBeenCalled(); expect(screen.queryByText('No Results')).not.toBeInTheDocument(); }); it('should ignore custom noResults component when matrixify is enabled', () => { const mockIsMatrixifyEnabled = isMatrixifyEnabled as jest.MockedFunction< typeof isMatrixifyEnabled >; const mockMatrixifyGridRenderer = MatrixifyGridRenderer as jest.MockedFunction< typeof MatrixifyGridRenderer >; mockIsMatrixifyEnabled.mockReturnValue(true); const CustomNoResults = () =>
Custom No Data Message
; render( } />, ); expect(mockMatrixifyGridRenderer).toHaveBeenCalled(); expect( screen.queryByText('Custom No Data Message'), ).not.toBeInTheDocument(); }); it('should apply error boundary to matrixify grid renderer', () => { const mockIsMatrixifyEnabled = isMatrixifyEnabled as jest.MockedFunction< typeof isMatrixifyEnabled >; const mockMatrixifyGridRenderer = MatrixifyGridRenderer as jest.MockedFunction< typeof MatrixifyGridRenderer >; mockIsMatrixifyEnabled.mockReturnValue(true); const onErrorBoundary = jest.fn(); render( , ); expect(mockMatrixifyGridRenderer).toHaveBeenCalled(); expect(onErrorBoundary).not.toHaveBeenCalled(); }); });