A next-generation tool to create blazing-fast documentation sites
API
created:4/29/2021
updated:4/29/2021

Overview

The cc-cli package automatically creates snapshot tests for your components based on the already created documentation stories.
3 levels from easiest to maintain, to full control and adding custom tests:
  • store-level is one test file for all your stories.
  • document-level is one test file per documentation file and inside the stories' tests are dynamically created.
  • story-level is one test file per documentation file and inside the stories' tests are statically imported and created.
Supports a variety of test renderers:

Install

yarn add @component-controls/cc-cli --dev

Pre-requisites

If you haven't already, you will need to first install jest and related depenencies:

jest

yarn add jest babel-jest --dev

typescript

yarn add ts-jest @types/jest --dev

package.json

"scripts": {
...
"jest": {
// you can use a ts-jest preset
"preset": "ts-jest",
// or transforms for the used files
"transform": {
"^.+\\.(ts|tsx)?$": "ts-jest",
"^.+\\.(js|jsx)$": "babel-jest"
},
"globals": {
// Temporary fix for memory leak in ts-jest, if your project is using typescript.
// https://github.com/kulshekhar/ts-jest/issues/1967.
"ts-jest": {
"isolatedModules": true
}
},
},
},

renderers

Depending on your choice for jest rendering, you will need to install one of the following dependecnies:
react-testing-library
yarn add @testing-library/jest-dom @testing-library/react @types/testing-library__jest-dom --dev
enzyme
yarn add enzyme enzyme-to-json @wojtekmaj/enzyme-adapter-react-17 @types/enzyme @types/testing-library__jest-dom --dev
react-test-renderer
yarn add react-test-renderer @types/react-test-renderer --dev

custom jest loaders

If you are using images, css files etc - you might need to add some custom transforms for your jest tests
example:

package.json

...
"jest": {
"transform": {
...
".+\\.(jpg|jpeg|png|gif|svg)$": "jest-url-loader",
".+\\.(css|styl|less|sass|scss)$": "jest-transform-css"
}
}
}

Examples

The starter projects are set up to demonstrate no-code jest snapshots:

CLI

The command-line interface for cc-cli allows for the quickest, zero-config setup. You can add a test:create script to your package.json file and use it to automatically generate tests for your components based on the existing stories.
quick start: the following command will generate a test file in tests/stories.test.js based on your component-controls configuration files in the .config folder.

package.json

"scripts": {
...
"test:create": "cc-cli"
},

CLI Parameters

ParameterExplanationInput typeDefault value
--config
-c
configuration files folderstring.config
--generate
-g
generate test files for whole store or story/by story files'store' | 'doc' | 'story'store
--renderer
-r
jest framework renderer'rtl' | 'rtr' | 'enzyme'rtl
--output
-o
generated tests output folderstringtests
--test
-t
generated tests file namestring`component-controls.test.(js
--format
-f
generated test files format'cjs' | 'esm' | 'ts'cjs
--overwrite
-w
force ovewrite existing test filesbooleanfalse
--bundle
-b
bundle path, if store loaded from bundlestring
--name
-n
name of the test group/describe sectionstringcomponent-controls generated
--include
-i
array of test file names to include (only for doc/story formats)string[]
--exclude
-x
array of test file names to exclude (only for doc/story formats)string[]
--ally
-y
whether to include axe accessibility testsbooleantrue

Examples

custom configuration folder
yarn cc-cli -c .storybook
custom output folder
yarn cc-cli -c .storybook -o testing
custom test renderer
yarn cc-cli -r enzyme
custom test format (typescript)
yarn cc-cli -f ts

generate levels

store

yarn cc-cli -g store -t component-controls.test.ts -f ts
This will generate one single test file for all your documentation files.
  • This approach is the easiest to maintain.
  • The tests are not associated with a specific component (and its documentation file).
  • The tests can not be extended with custom test conditions.
test file source...

tests/component-controls.test.ts

/* prettier-ignore-start */
import path from 'path';
import {
loadConfigurations,
extractDocuments,
} from '@component-controls/config';
import { renderExample, renderErr } from '@component-controls/test-renderers';
import { render, act } from '@testing-library/react';
import { run, AxeResults } from 'axe-core';
import { reactRunDOM } from '@component-controls/test-renderers';
import '@component-controls/jest-axe-matcher';
describe('component-controls generated', () => {
const configPath = path.resolve(__dirname, '../.config');
const config = loadConfigurations(configPath);
const documents = extractDocuments({ config, configPath });
if (documents) {
documents.forEach((file: string) => {
const exports = require(file);
const doc = exports.default;
const examples = Object.keys(exports)
.filter(key => key !== 'default')
.map(key => exports[key]);
if (examples.length) {
describe(doc.title, () => {
examples.forEach(example => {
describe(example.name, async () => {
let rendered;
act(() => {
rendered = renderExample({
example,
doc,
config,
});
});
if (!rendered) {
renderErr();
return;
}
it('snapshot', () => {
const { asFragment } = render(rendered);
expect(asFragment()).toMatchSnapshot();
});
it('accessibility', async () => {
const axeResults = await reactRunDOM<AxeResults>(rendered, run);
expect(axeResults).toHaveNoAxeViolations();
});
});
});
});
}
});
}
});
/* prettier-ignore-end */

document

yarn cc-cli -g doc -f ts -w
This will generate one test file per document, and inside will dynamically create tests for each story.
  • This approach is a bit harder to maintain - you will need to re-generate the tests when adding a new component or its documentation stories.
  • Each test file is associated with its corresponding component (and its documentation file).
  • The tests are difficult to extend with custom test conditions.
test file source...

src/Header/Header.test.ts

/* prettier-ignore-start */
import path from 'path';
import { run, AxeResults } from 'axe-core';
import { reactRunDOM } from '@component-controls/test-renderers';
import '@component-controls/jest-axe-matcher';
import { loadConfigurations } from '@component-controls/config';
import { renderDocument, renderErr } from '@component-controls/test-renderers';
import { render, act } from '@testing-library/react';
import * as examples from './Header.stories';
describe('Header', () => {
const configPath = path.resolve(__dirname, '../../.config');
const config = loadConfigurations(configPath);
let renderedExamples: ReturnType<typeof renderDocument> = [];
act(() => {
renderedExamples = renderDocument(examples, config);
});
if (!renderedExamples) {
renderErr();
return;
}
renderedExamples.forEach(({ name, rendered }) => {
describe(name, async () => {
it('snapshot', () => {
const { asFragment } = render(rendered);
expect(asFragment()).toMatchSnapshot();
});
it('accessibility', async () => {
const axeResults = await reactRunDOM<AxeResults>(rendered, run);
expect(axeResults).toHaveNoAxeViolations();
});
});
});
});
/* prettier-ignore-end */

story

yarn cc-cli -g story -f ts -w
This will generate one test file per document, and inside will create static tests for each story.
-- This approach is the most difficult to maintain - you will need to re-generate the tests when adding a new component (or its documentation stories) and also when adding new stories to an existing documentation file.
  • Each test file is associated with its corresponding component (and its documentation file).
  • The tests can be extended with custom test conditions.
This approach can lead to outdated test files (for example when you add a new story in an existing document - it will not be included in the tests).
test file source...

src/Header/Header.test.ts

/* prettier-ignore-start */
import path from 'path';
import { run, AxeResults } from 'axe-core';
import { reactRunDOM } from '@component-controls/test-renderers';
import '@component-controls/jest-axe-matcher';
import { loadConfigurations } from '@component-controls/config';
import { renderExample, renderErr } from '@component-controls/test-renderers';
import { render, act } from '@testing-library/react';
import doc, { overview } from './Header.stories';
describe('Header', () => {
const configPath = path.resolve(__dirname, '.config');
const config = loadConfigurations(configPath);
describe('overview', () => {
const example = overview;
let rendered;
act(() => {
rendered = renderExample({
example,
doc,
config,
});
});
if (!rendered) {
renderErr();
return;
}
it('snapshot', () => {
const { asFragment } = render(rendered);
expect(asFragment()).toMatchSnapshot();
});
it('accessibility', async () => {
const axeResults = await reactRunDOM<AxeResults>(rendered, run);
expect(axeResults).toHaveNoAxeViolations();
});
});
});
/* prettier-ignore-end */
Here is an example of extending a test with some custom test conditions (removed the automatically generated snapshot test and replaced with a test for the label content and background style):
test file source...

src/VariantButton/VariantButton.test.ts

/* prettier-ignore-start */
import path from 'path';
import { run, AxeResults } from 'axe-core';
import { reactRunDOM } from '@component-controls/test-renderers';
import '@component-controls/jest-axe-matcher';
import { loadConfigurations } from '@component-controls/config';
import { renderExample, renderErr } from '@component-controls/test-renderers';
import { render, act } from '@testing-library/react';
import doc, { primary } from './VariantButton.stories';
describe('VariantVutton', () => {
const configPath = path.resolve(__dirname, '.config');
const config = loadConfigurations(configPath);
describe('primary', () => {
const example = overview;
let rendered;
act(() => {
rendered = renderExample({
example,
doc,
config,
});
});
if (!rendered) {
renderErr();
return;
}
it('custom test', () => {
const { getByTestId, container } = render(rendered);
// expect(asFragment()).toMatchSnapshot(); - remove auto-snapshots
// add specific custom tests for a label and a style
expect(getByTestId('label')).toHaveTextContent('Primary');
expect(container.children[0]).toHaveStyle('background: lightgrey');
});
it('accessibility', async () => {
const axeResults = await reactRunDOM<AxeResults>(rendered, run);
expect(axeResults).toHaveNoAxeViolations();
});
});
});
/* prettier-ignore-end */