Skip to content

Commit

Permalink
Make getFixtures API compatible with Jest (#1507)
Browse files Browse the repository at this point in the history
* Make getFixtures API compatible with Jest

* Document getFixtures() caveats
  • Loading branch information
ovidiuch committed May 26, 2023
1 parent 3619136 commit 057fb98
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 53 deletions.
2 changes: 1 addition & 1 deletion docs/MIGRATION_V6.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ React Cosmos 6 also comes with a brand new Vite plugin. To set up a Vite codebas

- `react-cosmos/fixture` exports moved to `react-cosmos/client` (eg. `import { useValue } from 'react-cosmos/client'`).
- `NativeFixtureLoader` component moved from `react-cosmos/native` to new `react-cosmos-native` package. Install `react-cosmos-native@next` as well for a React Native setup.
- `getFixtures2()` refactored to `async getFixtures()`.
- `getFixtures2()` renamed to `getFixtures()`.
- `getCosmosConfigAtPath()` is now async. To replicate the old sync behavior, require() the config module manually and pass it to `createCosmosConfig()`.
- For visual regression testing you may need to make Jest transpile Cosmos modules by adding `"/node_modules/react-cosmos"` to `transformIgnorePatterns` in your Jest config.

Expand Down
23 changes: 19 additions & 4 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,9 @@ Get all your fixtures programatically. A ton of information is provided for each
```js
import { getFixtures } from 'react-cosmos';

const fixtures = await getFixtures(cosmosConfig);
const fixtures = getFixtures(cosmosConfig, {
rendererUrl: 'http://localhost:5000/renderer.html',
});

console.log(fixtures);
// [
Expand All @@ -484,16 +486,29 @@ console.log(fixtures);
// "parents": ["pages", "Error"]
// "playgroundUrl": "http://localhost:5000/?fixtureId=%7B%22path%22%3A%22components%2Fpages%2FError%2F__fixtures__%2Fnot-found.js%22%2C%22name%22%3Anull%7D",
// "relativeFilePath": "components/pages/Error/__fixtures__/not-found.js",
// "rendererUrl": "http://localhost:5000/static/renderer.html?fixtureId=%7B%22path%22%3A%22components%2Fpages%2FError%2F__fixtures__%2Fnot-found.js%22%2C%22name%22%3Anull%7D",
// "rendererUrl": "http://localhost:5000/renderer.html?fixtureId=%7B%22path%22%3A%22components%2Fpages%2FError%2F__fixtures__%2Fnot-found.js%22%2C%22name%22%3Anull%7D",
// "treePath": ["pages", "Error", "not-found"]
// },
// ...
```

> See a more complete output example [here](../examples/webpack/tests/fixtures.test.ts).
Aside from the fixture information showcased above, each fixture object returned also contains a `getElement` function property, which takes no arguments. `getElement` allows you to render fixtures in your own time, in environments like jsdom. Just as in the React Cosmos UI, the fixture element will include any decorators you've defined for your fixtures. `getElement` can be used for Jest snapshot testing.

#### Caveats

The `getFixtures()` API is tricky to work with.

To create URLs for each fixture, fixture modules are imported in order to retrieve the fixture names of _multi fixtures_. Fixture modules are non-standard (JSX or TypeScript files) and often expect a DOM environment. Thus calling `getFixtures()` in a Node environment isn't straightforward and Jest with `"jsdom"` [testEnvironment](https://jestjs.io/docs/configuration#testenvironment-string) is the de facto way of using this API.

Jest brings its own array of problems due to its limitations:

1. [ESM support is unfinished.](https://github.com/jestjs/jest/issues/9430)
2. [You can't create test cases asynchronously.](https://github.com/jestjs/jest/issues/2235#issuecomment-584387443) Using an async `globalSetup` [could work](https://github.com/jestjs/jest/issues/2235#issuecomment-584387443), but it can't import ESM and we're back to square one.

For the reasons above `getFixtures()` is a synchronous API. It uses CommonJS `require()` to import user modules.

Another limitation due to the lack of ESM support in Jest is the fact that `getFixtures()` doesn't run server plugins. The config hooks of server plugins usually auto-set the `rendererUrl` option in the user's Cosmos config. The Vite and Webpack plugins do this. With `getFixtures()`, however, we pass the renderer URL as a separate option after the Cosmos config.

## Create React App

- Add `react-cosmos-plugin-webpack` plugin.
Expand Down
11 changes: 9 additions & 2 deletions packages/react-cosmos/src/getFixtures/getFixtures.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ beforeEach(() => {

it('renders fixture elements', async () => {
const cosmosConfig = createCosmosConfig(rootDir);
const fixures = await getFixtures(cosmosConfig);

const fixures = getFixtures(cosmosConfig, {
rendererUrl: 'http://localhost:5000/renderer.html',
});

function testFixtureElement(relPath: string, name: string | null = null) {
const match = fixures.find(
Expand All @@ -49,7 +52,11 @@ it('renders fixture elements', async () => {

it('returns fixture info', async () => {
const cosmosConfig = createCosmosConfig(rootDir);
const fixtures = await getFixtures(cosmosConfig);

const fixtures = getFixtures(cosmosConfig, {
rendererUrl: 'http://localhost:5000/renderer.html',
});

expect(fixtures).toEqual([
{
absoluteFilePath: path.join(rootDir, 'src/__fixtures__/Controls.tsx'),
Expand Down
38 changes: 6 additions & 32 deletions packages/react-cosmos/src/getFixtures/getFixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,19 @@ import { ReactElement } from 'react';
import {
buildPlaygroundQueryString,
ByPath,
CosmosCommand,
createFixtureTree,
createRendererUrl,
FixtureId,
flattenFixtureTree,
getFixtureFromExport,
getFixtureListFromExports,
getSortedDecoratorsForFixturePath,
pickRendererUrl,
ReactDecorator,
ReactFixture,
ReactFixtureExport,
} from 'react-cosmos-core';
import { createFixtureNode, decorateFixture } from 'react-cosmos-renderer';
import { CosmosConfig } from '../cosmosConfig/types.js';
import { getPluginConfigs } from '../cosmosPlugin/pluginConfigs.js';
import { CosmosPlatform } from '../cosmosPlugin/types.js';
import { applyServerConfigPlugins } from '../shared/applyServerConfigPlugins.js';
import { getServerPlugins } from '../shared/getServerPlugins.js';
import { getPlaygroundUrl } from '../shared/playgroundUrl.js';
import { importUserModules } from './importUserModules.js';

Expand All @@ -39,34 +33,12 @@ export type FixtureApi = {
};

type Options = {
command?: CosmosCommand;
platform?: CosmosPlatform;
rendererUrl?: string;
};
export async function getFixtures(
cosmosConfig: CosmosConfig,
{ command = 'dev', platform = 'web' }: Options = {}
) {
const pluginConfigs = await getPluginConfigs({
cosmosConfig,
relativePaths: false,
});

const serverPlugins = await getServerPlugins(
pluginConfigs,
cosmosConfig.rootDir
);

cosmosConfig = await applyServerConfigPlugins({
cosmosConfig,
serverPlugins,
command,
platform,
});

const { fixtures, decorators } = await importUserModules(cosmosConfig);
export function getFixtures(cosmosConfig: CosmosConfig, options: Options = {}) {
const { fixtures, decorators } = importUserModules(cosmosConfig);
const fixtureExports = mapValues(fixtures, f => f.default);
const decoratorExports = mapValues(decorators, f => f.default);
const rendererUrl = pickRendererUrl(cosmosConfig.rendererUrl, command);
const result: FixtureApi[] = [];

getFlatFixtureTree(cosmosConfig, fixtureExports).forEach(
Expand All @@ -93,7 +65,9 @@ export async function getFixtures(
parents,
playgroundUrl: getPlaygroundFixtureUrl(cosmosConfig, fixtureId),
relativeFilePath: fixtureId.path,
rendererUrl: rendererUrl && createRendererUrl(rendererUrl, fixtureId),
rendererUrl: options.rendererUrl
? createRendererUrl(options.rendererUrl, fixtureId)
: null,
treePath,
});
}
Expand Down
26 changes: 12 additions & 14 deletions packages/react-cosmos/src/getFixtures/importUserModules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,32 @@ type UserModules = {
decorators: ByPath<ReactDecoratorModule>;
};

export async function importUserModules({
export function importUserModules({
rootDir,
fixturesDir,
fixtureFileSuffix,
ignore,
}: CosmosConfig): Promise<UserModules> {
}: CosmosConfig): UserModules {
const { fixturePaths, decoratorPaths } = findUserModulePaths({
rootDir,
fixturesDir,
fixtureFileSuffix,
ignore,
});
return {
fixtures: await importModules(fixturePaths, rootDir),
decorators: await importModules(decoratorPaths, rootDir),
fixtures: importModules(fixturePaths, rootDir),
decorators: importModules(decoratorPaths, rootDir),
};
}

async function importModules<T>(paths: string[], rootDir: string) {
const modules = await Promise.all(
paths.map(async p => {
// Converting to forward slashes on Windows is important because the
// slashes are used for generating a sorted list of fixtures and
// decorators.
const relPath = slash(path.relative(rootDir, p));
return { relPath, module: await import(p) };
})
);
function importModules<T>(paths: string[], rootDir: string) {
const modules = paths.map(p => {
// Converting to forward slashes on Windows is important because the
// slashes are used for generating a sorted list of fixtures and
// decorators.
const relPath = slash(path.relative(rootDir, p));
return { relPath, module: require(p) };
});

return modules.reduce(
(acc: Record<string, T>, { relPath, module }) => ({
Expand Down

0 comments on commit 057fb98

Please sign in to comment.