Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

doc: add testing page to ima docs #580

Merged
merged 1 commit into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 209 additions & 0 deletions docs/basic-features/testing.md
Original file line number Diff line number Diff line change
@@ -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<ReturnType<typeof render> & { 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 <div>{localize('my.translation.key')} {children}</div>;
}

test('renders learn react link', async () => {
const { getByText } = await renderWithContext(<Component>My Text</Component>);
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<ContextValue>
```

`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(<Component>My Text</Component>, {
contextValue, // Provide the custom context value
});
const textElement = getByText(/My Text/i);

expect(textElement).toBeInTheDocument();
});
```

### initImaApp

```javascript
async function initImaApp(): Promise<ImaApp>
```

`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(<Component>My Text</Component>, {
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(<Component>My Text</Component>);
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(<Component>My Text</Component>);
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).*
162 changes: 5 additions & 157 deletions packages/testing-library/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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(<Component>My Text</Component>);
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(<Component>My Text</Component>, { 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(<Component>My Text</Component>, { 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(<Component>My Text</Component>);
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(<Component>My Text</Component>);
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.
1 change: 1 addition & 0 deletions website/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const sidebars = {
'basic-features/dictionary',
'basic-features/error-handling',
'basic-features/typescript',
'basic-features/testing',
],
},
{
Expand Down
Loading