/* eslint-disable react/jsx-props-no-spreading */ import * as React from 'react'; import { render, waitForElementToBeRemoved } from '@testing-library/react'; import * as debug from '@atlassian/clientside-extensions-debug'; import { AsyncPanelHandler } from './AsyncPanelHandler'; import type { AsyncPanelRenderExtension, AsyncPanelHandlerProps } from './AsyncPanelHandler'; debug.setLoggingEnabled(true); debug.setDebugEnabled(true); // avoid spamming the console with error/warn messages // eslint-disable-next-line no-underscore-dangle debug._deregisterDefaultLogger(); const onMountCallbackSpy = jest.fn(); const onUnmountCallbackSpy = jest.fn(); const moduleSpy = jest.fn(); // onAction method of an Extension const createFakeRenderProvider: () => AsyncPanelRenderExtension = (): AsyncPanelRenderExtension => () => // fake async module Promise.resolve({ __esModule: true, default: moduleSpy, }); const LOCATION = 'test-location'; const EXTENSION_KEY = 'test-extension'; const TestComponent = (props: AsyncPanelHandlerProps) => { return ; }; TestComponent.defaultProps = { renderProvider: createFakeRenderProvider(), fallback:

loading

, location: LOCATION, extensionKey: EXTENSION_KEY, } as Partial; const getFirstCallArguments = (onDebugSpy: jest.Mock) => onDebugSpy.mock.calls[0][0]; describe('AsyncPanelHandler', () => { const onDebugSpy = jest.fn(); debug.observeLogger(onDebugSpy); beforeEach(() => { onMountCallbackSpy.mockReset(); onUnmountCallbackSpy.mockReset(); moduleSpy.mockReset(); onDebugSpy.mockClear(); moduleSpy.mockImplementation((panelAPI /* context, */ /* data */) => { panelAPI.onMount(onMountCallbackSpy); panelAPI.onUnmount(onUnmountCallbackSpy); }); }); it('should load and render the panel content asynchronously', async () => { const CUSTOM_CONTENT = `custom content`; onMountCallbackSpy.mockImplementationOnce((container: HTMLElement) => { container.innerHTML = CUSTOM_CONTENT; }); const { getByText, findByText, queryByTestId } = render(); expect(queryByTestId('loading')).toBeTruthy(); expect(onMountCallbackSpy).toHaveBeenCalledTimes(0); // wait for element to be on the screen await findByText(CUSTOM_CONTENT); expect(getByText(CUSTOM_CONTENT)).toBeTruthy(); expect(queryByTestId('loading')).toBeFalsy(); expect(onMountCallbackSpy).toHaveBeenCalledTimes(1); }); it('should provide an onMount and onUnmount API', async () => { const CUSTOM_CONTENT_1 = `custom content 1`; const CUSTOM_CONTENT_2 = `custom content 2`; const firstRenderProvider = createFakeRenderProvider(); const secondRenderProvider = createFakeRenderProvider(); onMountCallbackSpy .mockImplementationOnce((container: HTMLElement) => { container.innerHTML = CUSTOM_CONTENT_1; }) .mockImplementationOnce((container: HTMLElement) => { container.innerHTML = CUSTOM_CONTENT_2; }); const { rerender, findByText, queryByTestId, queryByText } = render(); expect(queryByTestId('loading')).toBeTruthy(); expect(onMountCallbackSpy).toHaveBeenCalledTimes(0); expect(onUnmountCallbackSpy).toHaveBeenCalledTimes(0); // wait for element to be on the screen await findByText(CUSTOM_CONTENT_1); expect(queryByText(CUSTOM_CONTENT_1)).toBeTruthy(); expect(queryByTestId('loading')).toBeFalsy(); expect(onMountCallbackSpy).toHaveBeenCalledTimes(1); expect(onUnmountCallbackSpy).toHaveBeenCalledTimes(0); rerender(); // wait for element to be on the screen await findByText(CUSTOM_CONTENT_2); expect(queryByText(CUSTOM_CONTENT_2)).toBeTruthy(); expect(queryByText(CUSTOM_CONTENT_1)).toBeFalsy(); expect(queryByTestId('loading')).toBeFalsy(); expect(onMountCallbackSpy).toHaveBeenCalledTimes(2); expect(onUnmountCallbackSpy).toHaveBeenCalledTimes(1); }); it('should catch the error if the async module has a wrong shape', async () => { const expectedError = { level: debug.LogLevel.error, message: `The provided dynamic import of extension "${EXTENSION_KEY}" at location "${LOCATION}" does not look like a module that can be asynchronously loaded.`, components: ['AsyncPanelHandler'], meta: { location: LOCATION, extension: EXTENSION_KEY, }, }; expect(onDebugSpy).toHaveBeenCalledTimes(0); // @ts-expect-error - ignoring to force assigment of an empty object as a resolved module const { getByText } = render( Promise.resolve({})} />); await waitForElementToBeRemoved(() => getByText('loading')); expect(onDebugSpy).toHaveBeenCalledTimes(1); expect(getFirstCallArguments(onDebugSpy)).toMatchObject(expectedError); }); it('should catch any error thrown by the render provider', async () => { const expectedError = { level: debug.LogLevel.error, message: `Failed trying to execute the dynamic import from extension "${EXTENSION_KEY}" for the async panel at location "${LOCATION}"`, components: ['AsyncPanelHandler'], meta: { location: LOCATION, extension: EXTENSION_KEY, error: new Error('test render error'), }, }; expect(onDebugSpy).toHaveBeenCalledTimes(0); const { getByText } = render( { throw new Error('test render error'); }} />, ); await waitForElementToBeRemoved(() => getByText('loading')); expect(onDebugSpy).toHaveBeenCalledTimes(1); expect(getFirstCallArguments(onDebugSpy)).toMatchObject(expectedError); }); it('should pass the context to the async module when the module was loaded', async () => { // given const contextValue = { myCounter: 1234, }; const contextProvider = () => contextValue; onMountCallbackSpy.mockImplementation((container: HTMLElement) => { const contextArgPos = 1; const lastContext = moduleSpy.mock.calls[moduleSpy.mock.calls.length - 1][contextArgPos]; container.innerHTML = `

My counter is: ${lastContext.myCounter}

`; }); // when const { findByTestId, getByTestId } = render(); await findByTestId('counter'); // then expect(getByTestId('label').textContent).toEqual('My counter is: 1234'); expect(moduleSpy).toHaveBeenLastCalledWith(expect.any(Object), contextValue); expect(onDebugSpy).toHaveBeenCalledTimes(0); }); it('should propagate the latest value of the context to the async module', async () => { // given const contextValue = { myCounter: 1234, }; const contextProvider = () => contextValue; // when const { getByText, rerender } = render(); await waitForElementToBeRemoved(() => getByText('loading')); // then expect(moduleSpy).toHaveBeenLastCalledWith(expect.any(Object), contextValue); expect(onDebugSpy).toHaveBeenCalledTimes(0); // New context value const newContextValue = 9876; const newContextProvider = () => newContextValue; rerender(); expect(moduleSpy).toHaveBeenLastCalledWith(expect.any(Object), newContextValue); expect(onDebugSpy).toHaveBeenCalledTimes(0); }); it('should log an error when failed invoking context provider', async () => { // given const contextProvider = () => { throw new SyntaxError(); }; // when const { getByText } = render(); await waitForElementToBeRemoved(() => getByText('loading')); // then expect(onDebugSpy).toHaveBeenCalledTimes(1); expect(getFirstCallArguments(onDebugSpy)).toMatchObject({ level: debug.LogLevel.error, message: `Failed to invoke "contextProvider" function for extension "${EXTENSION_KEY}" at location "${LOCATION}".\nError: SyntaxError`, components: ['AsyncPanelHandler'], meta: { location: LOCATION, extension: EXTENSION_KEY, }, }); }); });