/* 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,
},
});
});
});