This is an automated email from the ASF dual-hosted git repository. rusackas pushed a commit to branch replace-jest-enzyme in repository https://gitbox.apache.org/repos/asf/superset.git
commit 45b950e5448caf2c7cd10e3ce6d77517bac58e7d Author: Evan Rusackas <[email protected]> AuthorDate: Fri Feb 7 18:43:32 2025 -0700 ChartDataProvider -> RTL --- .../chart/components/ChartDataProvider.test.tsx | 432 ++++++++++----------- 1 file changed, 216 insertions(+), 216 deletions(-) diff --git a/superset-frontend/packages/superset-ui-core/test/chart/components/ChartDataProvider.test.tsx b/superset-frontend/packages/superset-ui-core/test/chart/components/ChartDataProvider.test.tsx index 390263fd11..c75ac957fb 100644 --- a/superset-frontend/packages/superset-ui-core/test/chart/components/ChartDataProvider.test.tsx +++ b/superset-frontend/packages/superset-ui-core/test/chart/components/ChartDataProvider.test.tsx @@ -16,16 +16,15 @@ * specific language governing permissions and limitations * under the License. */ - -import { ReactNode } from 'react'; -import { shallow } from 'enzyme'; +import '@testing-library/jest-dom'; +import { render, screen, act } from '@testing-library/react'; import ChartClient from '../../../src/chart/clients/ChartClient'; import ChartDataProvider, { ChartDataProviderProps, } from '../../../src/chart/components/ChartDataProvider'; import { bigNumberFormData } from '../fixtures/formData'; -// Note: the mock implementation of these function directly affects the expected results below +// Keep existing mock setup const defaultMockLoadFormData = jest.fn(({ formData }: { formData: unknown }) => Promise.resolve(formData), ); @@ -50,7 +49,6 @@ const mockLoadQueryData = jest.fn<Promise<unknown>, unknown[]>( ); const actual = jest.requireActual('../../../src/chart/clients/ChartClient'); -// ChartClient is now a mock jest.spyOn(actual, 'default').mockImplementation(() => ({ loadDatasource: mockLoadDatasource, loadFormData: mockLoadFormData, @@ -62,7 +60,6 @@ const ChartClientMock = ChartClient as jest.Mock<ChartClient>; describe('ChartDataProvider', () => { beforeEach(() => { ChartClientMock.mockClear(); - mockLoadFormData = defaultMockLoadFormData; mockLoadFormData.mockClear(); mockLoadDatasource.mockClear(); @@ -71,11 +68,17 @@ describe('ChartDataProvider', () => { const props: ChartDataProviderProps = { formData: { ...bigNumberFormData }, - children: () => <div />, + children: ({ loading, payload, error }) => ( + <div> + {loading && <span role="status">Loading...</span>} + {payload && <pre role="contentinfo">{JSON.stringify(payload)}</pre>} + {error && <div role="alert">{error.message}</div>} + </div> + ), }; function setup(overrideProps?: Partial<ChartDataProviderProps>) { - return shallow(<ChartDataProvider {...props} {...overrideProps} />); + return render(<ChartDataProvider {...props} {...overrideProps} />); } it('instantiates a new ChartClient()', () => { @@ -86,7 +89,7 @@ describe('ChartDataProvider', () => { describe('ChartClient.loadFormData', () => { it('calls method on mount', () => { setup(); - expect(mockLoadFormData.mock.calls).toHaveLength(1); + expect(mockLoadFormData).toHaveBeenCalledTimes(1); expect(mockLoadFormData.mock.calls[0][0]).toEqual({ sliceId: props.sliceId, formData: props.formData, @@ -96,234 +99,231 @@ describe('ChartDataProvider', () => { it('should pass formDataRequestOptions to ChartClient.loadFormData', () => { const options = { host: 'override' }; setup({ formDataRequestOptions: options }); - expect(mockLoadFormData.mock.calls).toHaveLength(1); + expect(mockLoadFormData).toHaveBeenCalledTimes(1); expect(mockLoadFormData.mock.calls[0][1]).toEqual(options); }); - it('calls ChartClient.loadFormData when formData or sliceId change', () => { - const wrapper = setup(); + it('calls ChartClient.loadFormData when formData or sliceId change', async () => { + const { rerender } = setup(); const newProps = { sliceId: 123, formData: undefined }; - expect(mockLoadFormData.mock.calls).toHaveLength(1); + expect(mockLoadFormData).toHaveBeenCalledTimes(1); - wrapper.setProps(newProps); - expect(mockLoadFormData.mock.calls).toHaveLength(2); + rerender(<ChartDataProvider {...props} {...newProps} />); + expect(mockLoadFormData).toHaveBeenCalledTimes(2); expect(mockLoadFormData.mock.calls[1][0]).toEqual(newProps); }); }); describe('ChartClient.loadDatasource', () => { - it('does not method if loadDatasource is false', () => - new Promise(done => { - expect.assertions(1); - setup({ loadDatasource: false }); - setTimeout(() => { - expect(mockLoadDatasource.mock.calls).toHaveLength(0); - done(undefined); - }, 0); - })); - - it('calls method on mount if loadDatasource is true', () => - new Promise(done => { - expect.assertions(2); - setup({ loadDatasource: true }); - setTimeout(() => { - expect(mockLoadDatasource.mock.calls).toHaveLength(1); - expect(mockLoadDatasource.mock.calls[0][0]).toEqual( - props.formData.datasource, - ); - done(undefined); - }, 0); - })); - - it('should pass datasourceRequestOptions to ChartClient.loadDatasource', () => - new Promise(done => { - expect.assertions(2); - const options = { host: 'override' }; - setup({ loadDatasource: true, datasourceRequestOptions: options }); - setTimeout(() => { - expect(mockLoadDatasource.mock.calls).toHaveLength(1); - expect(mockLoadDatasource.mock.calls[0][1]).toEqual(options); - done(undefined); - }, 0); - })); - - it('calls ChartClient.loadDatasource if loadDatasource is true and formData or sliceId change', () => - new Promise(done => { - expect.assertions(3); - const newDatasource = 'test'; - const wrapper = setup({ loadDatasource: true }); - wrapper.setProps({ - formData: { datasource: newDatasource }, - sliceId: undefined, - }); - - setTimeout(() => { - expect(mockLoadDatasource.mock.calls).toHaveLength(2); - expect(mockLoadDatasource.mock.calls[0][0]).toEqual( - props.formData.datasource, - ); - expect(mockLoadDatasource.mock.calls[1][0]).toEqual(newDatasource); - done(undefined); - }, 0); - })); + it('does not call method if loadDatasource is false', async () => { + setup({ loadDatasource: false }); + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + }); + expect(mockLoadDatasource).not.toHaveBeenCalled(); + }); + + it('calls method on mount if loadDatasource is true', async () => { + setup({ loadDatasource: true }); + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + }); + expect(mockLoadDatasource).toHaveBeenCalledTimes(1); + expect(mockLoadDatasource.mock.calls[0]).toEqual([ + props.formData.datasource, + undefined, + ]); + }); + + it('should pass datasourceRequestOptions to ChartClient.loadDatasource', async () => { + const options = { host: 'override' }; + setup({ loadDatasource: true, datasourceRequestOptions: options }); + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + }); + expect(mockLoadDatasource).toHaveBeenCalledTimes(1); + expect(mockLoadDatasource.mock.calls[0][1]).toEqual(options); + }); + + it('calls ChartClient.loadDatasource if loadDatasource is true and formData or sliceId change', async () => { + const { rerender } = setup({ loadDatasource: true }); + const newDatasource = 'test'; + + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + }); + + await act(async () => { + rerender( + <ChartDataProvider + {...props} + formData={{ ...props.formData, datasource: newDatasource }} + loadDatasource={true} + />, + ); + await new Promise(resolve => setTimeout(resolve, 0)); + }); + + expect(mockLoadDatasource).toHaveBeenCalledTimes(2); + expect(mockLoadDatasource.mock.calls[0]).toEqual([ + props.formData.datasource, + undefined, + ]); + expect(mockLoadDatasource.mock.calls[1]).toEqual([ + newDatasource, + undefined, + ]); + }); }); describe('ChartClient.loadQueryData', () => { - it('calls method on mount', () => - new Promise(done => { - expect.assertions(2); - setup(); - setTimeout(() => { - expect(mockLoadQueryData.mock.calls).toHaveLength(1); - expect(mockLoadQueryData.mock.calls[0][0]).toEqual(props.formData); - done(undefined); - }, 0); - })); - - it('should pass queryDataRequestOptions to ChartClient.loadQueryData', () => - new Promise(done => { - expect.assertions(2); - const options = { host: 'override' }; - setup({ queryRequestOptions: options }); - setTimeout(() => { - expect(mockLoadQueryData.mock.calls).toHaveLength(1); - expect(mockLoadQueryData.mock.calls[0][1]).toEqual(options); - done(undefined); - }, 0); - })); - - it('calls ChartClient.loadQueryData when formData or sliceId change', () => - new Promise(done => { - expect.assertions(3); - const newFormData = { key: 'test' }; - const wrapper = setup(); - wrapper.setProps({ formData: newFormData, sliceId: undefined }); - - setTimeout(() => { - expect(mockLoadQueryData.mock.calls).toHaveLength(2); - expect(mockLoadQueryData.mock.calls[0][0]).toEqual(props.formData); - expect(mockLoadQueryData.mock.calls[1][0]).toEqual(newFormData); - done(undefined); - }, 0); - })); + it('calls method on mount', async () => { + setup(); + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + }); + expect(mockLoadQueryData).toHaveBeenCalledTimes(1); + expect(mockLoadQueryData.mock.calls[0]).toEqual([ + props.formData, + undefined, + ]); + }); + + it('should pass queryDataRequestOptions to ChartClient.loadQueryData', async () => { + const options = { host: 'override' }; + setup({ queryRequestOptions: options }); + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + }); + expect(mockLoadQueryData).toHaveBeenCalledTimes(1); + expect(mockLoadQueryData).toHaveBeenCalledWith( + expect.anything(), + options, + ); + }); + + it('calls ChartClient.loadQueryData when formData or sliceId change', async () => { + const { rerender } = setup(); + const newFormData = { key: 'test' }; + + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + }); + + await act(async () => { + rerender(<ChartDataProvider {...props} formData={newFormData} />); + await new Promise(resolve => setTimeout(resolve, 0)); + }); + + expect(mockLoadQueryData).toHaveBeenCalledTimes(2); + expect(mockLoadQueryData.mock.calls[0]).toEqual([ + props.formData, + undefined, + ]); + expect(mockLoadQueryData.mock.calls[1]).toEqual([newFormData, undefined]); + }); }); describe('children', () => { - it('calls children({ loading: true }) when loading', () => { - const children = jest.fn<ReactNode, unknown[]>(); - setup({ children }); + it('shows loading state initially', async () => { + mockLoadFormData.mockImplementation(() => new Promise(() => {})); + mockLoadQueryData.mockImplementation(() => new Promise(() => {})); + mockLoadDatasource.mockImplementation(() => new Promise(() => {})); + + setup(); + await screen.findByRole('status'); + }); - // during the first tick (before more promises resolve) loading is true - expect(children.mock.calls).toHaveLength(1); - expect(children.mock.calls[0][0]).toEqual({ loading: true }); + it('shows payload when loaded', async () => { + mockLoadFormData.mockResolvedValue(props.formData); + mockLoadQueryData.mockResolvedValue([props.formData]); + mockLoadDatasource.mockResolvedValue(props.formData.datasource); + + setup({ loadDatasource: true }); + + const payloadElement = await screen.findByRole('contentinfo'); + const actualPayload = JSON.parse(payloadElement.textContent || ''); + + expect(actualPayload).toEqual({ + formData: props.formData, + datasource: props.formData.datasource, + queriesData: [props.formData], + }); + }); + + it('shows error message upon request error', async () => { + const errorMessage = 'error'; + mockLoadFormData.mockRejectedValue(new Error(errorMessage)); + + setup(); + + const errorElement = await screen.findByRole('alert'); + expect(errorElement).toHaveAttribute('role', 'alert'); + expect(errorElement.textContent).toBe(errorMessage); }); - it('calls children({ payload }) when loaded', () => - new Promise(done => { - expect.assertions(2); - const children = jest.fn<ReactNode, unknown[]>(); - setup({ children, loadDatasource: true }); - - setTimeout(() => { - expect(children.mock.calls).toHaveLength(2); - expect(children.mock.calls[1][0]).toEqual({ - payload: { - formData: props.formData, - datasource: props.formData.datasource, - queriesData: [props.formData], - }, - }); - done(undefined); - }, 0); - })); - - it('calls children({ error }) upon request error', () => - new Promise(done => { - expect.assertions(2); - const children = jest.fn<ReactNode, unknown[]>(); - mockLoadFormData = jest.fn(() => Promise.reject(new Error('error'))); - - setup({ children }); - - setTimeout(() => { - expect(children.mock.calls).toHaveLength(2); // loading + error - expect(children.mock.calls[1][0]).toEqual({ - error: new Error('error'), - }); - done(undefined); - }, 0); - })); - - it('calls children({ error }) upon JS error', () => - new Promise(done => { - expect.assertions(2); - const children = jest.fn<ReactNode, unknown[]>(); - - mockLoadFormData = jest.fn(() => { - throw new Error('non-async error'); - }); - - setup({ children }); - - setTimeout(() => { - expect(children.mock.calls).toHaveLength(2); // loading + error - expect(children.mock.calls[1][0]).toEqual({ - error: new Error('non-async error'), - }); - done(undefined); - }, 0); - })); + it('shows error message upon JS error', async () => { + mockLoadFormData.mockImplementation(() => { + throw new Error('non-async error'); + }); + + setup(); + + const errorElement = await screen.findByRole('alert'); + expect(errorElement).toHaveAttribute('role', 'alert'); + expect(errorElement.textContent).toBe('non-async error'); + }); }); describe('callbacks', () => { - it('calls onLoad(payload) when loaded', () => - new Promise(done => { - expect.assertions(2); - const onLoaded = jest.fn<void, unknown[]>(); - setup({ onLoaded, loadDatasource: true }); - - setTimeout(() => { - expect(onLoaded.mock.calls).toHaveLength(1); - expect(onLoaded.mock.calls[0][0]).toEqual({ - formData: props.formData, - datasource: props.formData.datasource, - queriesData: [props.formData], - }); - done(undefined); - }, 0); - })); - - it('calls onError(error) upon request error', () => - new Promise(done => { - expect.assertions(2); - const onError = jest.fn<void, unknown[]>(); - mockLoadFormData = jest.fn(() => Promise.reject(new Error('error'))); - - setup({ onError }); - setTimeout(() => { - expect(onError.mock.calls).toHaveLength(1); - expect(onError.mock.calls[0][0]).toEqual(new Error('error')); - done(undefined); - }, 0); - })); - - it('calls onError(error) upon JS error', () => - new Promise(done => { - expect.assertions(2); - const onError = jest.fn<void, unknown[]>(); - - mockLoadFormData = jest.fn(() => { - throw new Error('non-async error'); - }); - - setup({ onError }); - setTimeout(() => { - expect(onError.mock.calls).toHaveLength(1); - expect(onError.mock.calls[0][0]).toEqual( - new Error('non-async error'), - ); - done(undefined); - }, 0); - })); + it('calls onLoaded when loaded', async () => { + const onLoaded = jest.fn(); + mockLoadFormData.mockResolvedValue(props.formData); + mockLoadQueryData.mockResolvedValue([props.formData]); + mockLoadDatasource.mockResolvedValue(props.formData.datasource); + + setup({ onLoaded, loadDatasource: true }); + + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + }); + + expect(onLoaded).toHaveBeenCalledTimes(1); + expect(onLoaded).toHaveBeenCalledWith({ + formData: props.formData, + datasource: props.formData.datasource, + queriesData: [props.formData], + }); + }); + + it('calls onError upon request error', async () => { + const onError = jest.fn(); + mockLoadFormData.mockRejectedValue(new Error('error')); + + setup({ onError }); + + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + }); + + expect(onError).toHaveBeenCalledTimes(1); + expect(onError).toHaveBeenCalledWith(new Error('error')); + }); + + it('calls onError upon JS error', async () => { + const onError = jest.fn(); + mockLoadFormData.mockImplementation(() => { + throw new Error('non-async error'); + }); + + setup({ onError }); + + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + }); + + expect(onError).toHaveBeenCalledTimes(1); + expect(onError).toHaveBeenCalledWith(new Error('non-async error')); + }); }); });
