diff --git a/docs/basic-features/testing.md b/docs/basic-features/testing.md new file mode 100644 index 0000000000..b1aceeb21c --- /dev/null +++ b/docs/basic-features/testing.md @@ -0,0 +1,209 @@ +--- +title: Testing +description: Basic features > Testing +--- + +The `@ima/testing-library` contains utilities for testing IMA.js applications. It provides integration with [Jest](https://jestjs.io), [React Testing Library](https://testing-library.com/docs/react-testing-library/intro) and [Testing Library Jest DOM](https://testing-library.com/docs/ecosystem-jest-dom). If you initialized your project via `create-ima-app`, the testing setup is already included in your project. If not, check `@ima/testing-library` README for more information about how to setup testing in your project. + +## API + +IMA Testing Library is re-exporting everything from `@testing-library/react`. You should always import React Testing Library functions from `@ima/testing-library` as we might add some additional functionality / wrappers in the future. As such, it provides the same API as `@testing-library/react` with some additional features. + +### renderWithContext + +```javascript +async function renderWithContext( + ui: ReactElement, + options?: RenderOptions & { contextValue?: ContextValue; app?: ImaApp } +): Promise & { app: ImaApp | null; contextValue: ContextValue; }> +``` + +`renderWithContext` is a wrapper around [`render` from `@testing-library/react`](https://testing-library.com/docs/react-testing-library/api#render). It sets `wrapper` option in `render` method to a real IMA.js context wrapper. It can take additional optional IMA specific options: +- `contextValue` - the result of `getContextValue` +- `app` - the result of `initImaApp` (if you provide `contextValue`, it does not make any sense to provide `app` as the `app` is only used to generate the `contextValue`) + +If any of the options is not provided, it will be generated automatically. + +Example usage: + +```javascript +import { useLocalize } from '@ima/react-page-renderer'; +import { renderWithContext } from '@ima/testing-library'; + +function Component({ children }) { + const localize = useLocalize(); // Get localize function from IMA.js context + + return
{localize('my.translation.key')} {children}
; +} + +test('renders learn react link', async () => { + const { getByText } = await renderWithContext(My Text); + const textElement = getByText(/My Text/i); + + expect(textElement).toBeInTheDocument(); +}); +``` + +If you had used `render` from `@testing-library/react` directly, the test would have failed because the `useLocalize` hook would be missing the context. The `renderWithContext` function provides the necessary context and making it easier to test components that depend on the IMA.js context. + +### getContextValue + +```javascript +async function getContextValue(app?: ImaApp): Promise +``` + +`getContextValue` is a helper function that returns the context value from the IMA.js app. It can take an optional `app` parameter, which is the result of `initImaApp`. + +Example usage: + +```javascript +test('renders learn react link with custom context value', async () => { + const contextValue = await getContextValue(); // Generate default context value + + contextValue.$Utils.$Foo = jest.fn(() => 'bar'); // Mock some part of the context + + const { getByText } = await renderWithContext(My Text, { + contextValue, // Provide the custom context value + }); + const textElement = getByText(/My Text/i); + + expect(textElement).toBeInTheDocument(); +}); +``` + +### initImaApp + +```javascript +async function initImaApp(): Promise +``` + +`initImaApp` is a helper function that initializes the IMA.js app. + +```javascript +test('renders learn react link with custom app configuration', async () => { + const app = await initImaApp(); // Initialize the app + + app.oc.get('$Utils').$Foo = jest.fn(() => 'bar'); // Mock some part of the app + + const { getByText } = await renderWithContext(My Text, { + app, // Provide the custom app + }); + const textElement = getByText(/My Text/i); + + expect(textElement).toBeInTheDocument(); +}); +``` + +## Extending IMA boot config methods + +You can extend IMA boot config by using [IMA `pluginLoader.register`](https://imajs.io/api/classes/ima_core.PluginLoader/#register) method. Use the same approach as in IMA plugins. + +You can either register a plugin loader for all tests by setting it up in a setup file. + +```javascript +// jestSetup.js +import { pluginLoader } from '@ima/core'; + +// If you don't care, if this plugin loader is registered first, or last +pluginLoader.register('jestSetup.js', () => { + return { + initSettings: () => { + return { + prod: { + customSetting: 'customValue' + } + } + } + }; +}); + +// If you need to register the plugin loader after all other plugin loaders +beforeAll(() => { + pluginLoader.register('jestSetup.js', () => { + return { + initSettings: () => { + return { + prod: { + customSetting: 'customValue' + } + } + } + }; + }); +}); + +// jest.config.js +module.exports = { + // Add this line to your jest config + setupFilesAfterEnv: ['./jestSetup.js'] +}; +``` + +Or you can register a plugin loader for a specific test file. + +```javascript +// mySpec.js +import { pluginLoader } from '@ima/core'; + +beforeAll(() => { + pluginLoader.register('mySpec', () => { + return { + initSettings: () => { + return { + prod: { + customSetting: 'customValue' + } + } + } + }; + }); +}); + +test('renders learn react link with custom app configuration', async () => { + const { getByText } = await renderWithContext(My Text); + const textElement = getByText(/My Text/i); + + expect(textElement).toBeInTheDocument(); +}); +``` + +Or you can register a plugin loader for a test file, but make the boot config methods dynamic so you can change them for each test. + +```javascript +// mySpec.js +import { pluginLoader } from '@ima/core'; + +// We create a placeholder for the plugin loader, so we can change it later +let initSettings = () => {}; + +beforeAll(() => { + pluginLoader.register('mySpec', (...args) => { + return { + initSettings: (...args) => { + return initSettings(...args); // Here we call our overridable function + } + }; + }); +}); + +afterEach(() => { + initSettings = () => {}; // Reset the plugin loader so it is not called for other tests +}); + +test('renders learn react link with custom app configuration', async () => { + initSettings = () => { + return { + prod: { + customSetting: 'customValue' + } + } + }; + + const { getByText } = await renderWithContext(My Text); + const textElement = getByText(/My Text/i); + + expect(textElement).toBeInTheDocument(); +}); +``` + +*Note, that the plugin loader register method evaluates the second argument right away, but the specific boot config methods are evaluated during `renderWithContext` (or `initImaApp` if you are using it directly).* diff --git a/packages/testing-library/README.md b/packages/testing-library/README.md index 15c5f8aa76..c17eb98448 100644 --- a/packages/testing-library/README.md +++ b/packages/testing-library/README.md @@ -58,6 +58,8 @@ module.exports = { }; ``` +See [src/server/configuration.ts](https://github.com/seznam/ima/blob/master/packages/testing-library/src/server/configuration.ts) for the full list of available options. + **Client Configuration** This configuration should be evaluated in the setup files, or directly in the test files. It's config values are used to initialize the IMA.js application and provide the context for the tests. @@ -71,162 +73,8 @@ setImaTestingLibraryClientConfig({ }); ``` -## Usage - -IMA Testing Library is re-exporting everything from `@testing-library/react`. You should always import React Testing Library functions from `@ima/testing-library` as we might add some additional functionality / wrappers in the future. - -IMA Testing Library exports async function `renderWithContext`. It adds default context to the `render` function from RTL. The context is created from the IMA.js application and it contains the same values as the real application context. - -```javascript -import { renderWithContext } from '@ima/testing-library'; - -test('renders learn react link', async () => { - const { getByText } = await renderWithContext(My Text); - const textElement = getByText(/My Text/i); - - expect(textElement).toBeInTheDocument(); -}); -``` - -You might need to specify custom additions to the context, or mock some parts of the IMA application. You can do this by providing a custom context wrapper and using the `@ima/testing-library` specific utilities. - -```javascript -import { renderWithContext, getContextValue, initImaApp } from '@ima/testing-library'; - -test('renders learn react link with custom app configuration', async () => { - const app = await initImaApp(); - - app.oc.get('$Utils').$Foo = jest.fn(() => 'bar'); - - const { getByText } = await renderWithContext(My Text, { app }); - const textElement = getByText(/My Text/i); - - expect(textElement).toBeInTheDocument(); -}); - -test('renders learn react link with custom context value', async () => { - const app = await initImaApp(); - const contextValue = await getContextValue(app); - - contextValue.$Utils.$Foo = jest.fn(() => 'bar'); - - const { getByText } = await renderWithContext(My Text, { contextValue }); - const textElement = getByText(/My Text/i); - - expect(textElement).toBeInTheDocument(); -}); -``` - -### Extending IMA boot config methods +See [src/client/configuration.ts](https://github.com/seznam/ima/blob/master/packages/testing-library/src/client/configuration.ts) for the full list of available options. -You can extend IMA boot config by using [IMA `pluginLoader.register`](https://imajs.io/api/classes/ima_core.PluginLoader/#register) method. Use the same approach as in IMA plugins. - -You can either register a plugin loader for all tests by setting it up in a setup file. - -```javascript -// jestSetup.js -import { pluginLoader } from '@ima/core'; - -// If you don't care, if this plugin loader is registered first, or last -pluginLoader.register('jestSetup.js', () => { - return { - initSettings: () => { - return { - prod: { - customSetting: 'customValue' - } - } - } - }; -}); - -// If you need to register the plugin loader as the last one -beforeAll(() => { - pluginLoader.register('jestSetup.js', () => { - return { - initSettings: () => { - return { - prod: { - customSetting: 'customValue' - } - } - } - }; - }); -}); - -// jest.config.js -module.exports = { - // Add this line to your jest config - setupFilesAfterEnv: ['./jestSetup.js'] -}; -``` - -Or you can register a plugin loader for a specific test file. - -```javascript -// mySpec.js -import { pluginLoader } from '@ima/core'; - -beforeAll(() => { - pluginLoader.register('mySpec', () => { - return { - initSettings: () => { - return { - prod: { - customSetting: 'customValue' - } - } - } - }; - }); -}); - -test('renders learn react link with custom app configuration', async () => { - const { getByText } = await renderWithContext(My Text); - const textElement = getByText(/My Text/i); - - expect(textElement).toBeInTheDocument(); -}); -``` - -Or you can register a plugin loader for a test file, but make the boot config methods dynamic so you can change them for each test. - -```javascript -// mySpec.js -import { pluginLoader } from '@ima/core'; - -// We create a placeholder for the plugin loader, so we can change it later -let initSettings = () => {}; - -beforeAll(() => { - pluginLoader.register('mySpec', (...args) => { - return { - initSettings: (...args) => { - return initSettings(...args); // Here we call our overridable function - } - }; - }); -}); - -afterEach(() => { - initSettings = () => {}; // Reset the plugin loader so it is not called for other tests -}); - -test('renders learn react link with custom app configuration', async () => { - initSettings = () => { - return { - prod: { - customSetting: 'customValue' - } - } - }; - - const { getByText } = await renderWithContext(My Text); - const textElement = getByText(/My Text/i); - - expect(textElement).toBeInTheDocument(); -}); -``` +## Usage -*Note, that the plugin loader register method evaluates the second argument right away, but the specific boot config methods are evaluated during `renderWithContext` (or `initImaApp` if you are using it directly).* +See [documentation](https://imajs.io/basic-features/testing) for more information about how to use the IMA Testing Library. diff --git a/website/sidebars.js b/website/sidebars.js index 3786a1510e..5b19ede4b0 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -60,6 +60,7 @@ const sidebars = { 'basic-features/dictionary', 'basic-features/error-handling', 'basic-features/typescript', + 'basic-features/testing', ], }, {