25 |
26 |
27 |
28 |
29 | Here is a screenshot test written with `react-screenshot-test`:
30 |
31 | [](https://github.com/fwouts/react-screenshot-test/tree/master/example/FancyButton.screenshot.jsx)
32 |
33 | All you need is to install `react-screenshot-test` and configure Jest:
34 |
35 | ```js
36 | // jest.screenshot.config.js
37 |
38 | module.exports = {
39 | testEnvironment: "node",
40 | globalSetup: "react-screenshot-test/global-setup",
41 | globalTeardown: "react-screenshot-test/global-teardown",
42 | testMatch: ["**/?(*.)+(screenshot).[jt]s?(x)"],
43 | transform: {
44 | "^.+\\.[t|j]sx?$": "babel-jest", // or ts-jest
45 | "^.+\\.module\\.css$": "react-screenshot-test/css-modules-transform",
46 | "^.+\\.css$": "react-screenshot-test/css-transform",
47 | "^.+\\.scss$": "react-screenshot-test/sass-transform",
48 | "^.+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
49 | "react-screenshot-test/asset-transform"
50 | },
51 | transformIgnorePatterns: ["node_modules/.+\\.js"]
52 | };
53 | ```
54 |
55 | You can then generate screenshots with `jest -c jest.screenshot.config.js -u`,
56 | just like you would with Jest snapshots.
57 |
58 | ## What does it look like?
59 |
60 | Here's a [real example](https://github.com/fwouts/react-screenshot-test/pull/18/files?short_path=9fa0253#diff-9fa0253d6c3a2b1cf8ec498eec18360e) of a pull request where a component was changed:
61 | [](https://github.com/fwouts/react-screenshot-test/pull/18/files?short_path=c1101dd#diff-c1101ddb11729f8ee0750df5e9595b47)
62 |
63 | ## How does it work?
64 |
65 | Under the hood, we start a local server which renders components server-side. Each component is given its own dedicated page (e.g. /render/my-component). Then we use Puppeteer to take a screenshot of that page.
66 |
67 | Curious to learn more? Check out the [internal documentation](./docs/index.md)!
68 |
69 | ## Cross-platform consistency
70 |
71 | If you work on a team where developers use a different OS (e.g. Mac OS and
72 | Linux), or if you develop on Mac OS but use Linux for continuous integration,
73 | you would quickly run into issues where screenshots are inconsistent across
74 | platforms. This is, for better or worse, expected behaviour.
75 |
76 | In order to work around this issue, `react-screenshot-test` will default to
77 | running Puppeteer (i.e. Chrome) inside Docker to take screenshots of your
78 | components. This ensures that generated screenshots are consistent regardless of
79 | which platform you run your tests on.
80 |
81 | You can override this behaviour by setting the `SCREENSHOT_MODE` environment
82 | variable to `local`, which will always use a local browser instead of Docker.
83 |
84 | _Note: On Linux, `react-screenshot-test` will run Docker using host network mode on port 3001_
85 |
86 | ## CSS support
87 |
88 | CSS-in-JS libraries such as Emotion and Styled Components are supported.
89 |
90 | | CSS technique | Supported |
91 | | ------------------------------------------------------ | --------- |
92 | | `
`import "./style.css"` | ✅ |
94 | | Sass stylesheets `import "./style.scss"` | ✅ |
95 | | CSS Modules `import css from "./style.css"` | ✅ |
96 | | [Emotion](https://emotion.sh) | ✅ |
97 | | [Styled Components](https://www.styled-components.com) | ✅ |
98 |
99 | ## Usage with create-react-app
100 |
101 | If you'd like to set up `react-screenshot-test` with a `create-react-app`, [here is everything you need](https://github.com/fwouts/react-screenshot-test-with-create-react-app/compare/original...master).
102 |
103 | ## Storing image snapshots
104 |
105 | We recommend using [Git LFS](https://git-lfs.github.com) to store image
106 | snapshots. This will help prevent your Git repository from becoming bloated over time.
107 |
108 | If you're unfamiliar with Git LFS, you can learn about it with [this short video (2 min)](https://www.youtube.com/watch?v=uLR1RNqJ1Mw) and/or going through [the official tutorial](https://github.com/git-lfs/git-lfs/wiki/Tutorial).
109 |
110 | To set up Git LFS, [install the Git extension](https://git-lfs.github.com/) and add the following to `.gitattributes` in your repository ([source](https://github.com/americanexpress/jest-image-snapshot/issues/92#issuecomment-493582776)):
111 |
112 | ```
113 | **/__screenshots__/*.* binary
114 | **/__screenshots__/*.* filter=lfs diff=lfs merge=lfs -text
115 | ```
116 |
117 | You may also need to set up Git LFS for continuous integration. See [our config](https://github.com/fwouts/react-screenshot-test/blob/master/.circleci/config.yml) for an example with CircleCI.
118 |
119 | ## Usage with Percy
120 |
121 | If you prefer to keep image snapshots out of your repository, you can use a third-party service such as [Percy](https://percy.io):
122 |
123 | - Install `@percy/puppeteer`
124 | - Ensure that `PERCY_TOKEN` is set in your enviroment
125 | - Set up a script to invoke Jest through Percy:
126 |
127 | ```json
128 | {
129 | "screenshot-test-percy": "SCREENSHOT_MODE=percy percy exec -- jest -c jest.screenshot.config.js"
130 | }
131 | ```
132 |
133 | ## TypeScript support
134 |
135 | This library is written in TypeScript. All declarations are included.
136 |
137 | ## Browser support
138 |
139 | At the moment, screenshots are only generated with Chrome. However, the design can be extended to any headless browser. File an issue if you'd like to help make this happen.
140 |
141 | ## Comparison
142 |
143 | | Tool | Visual | Open Source | Price for 100,000 snapshots/month | Jest integration | Review process |
144 | | ---------------------------------------------------------------------------- | ------ | ----------- | --------------------------------- | ---------------- | ---------------------------------------------------------------------- |
145 | | [react-screenshot-test](https://www.npmjs.com/package/react-screenshot-test) | ✅ | ✅ | Free | ✅ | Pull request |
146 | | [Jest snapshots](https://jestjs.io/docs/en/snapshot-testing) | ❌ | ✅ | Free | ✅ | Pull request |
147 | | [Percy](https://percy.io) | ✅ | ❌ | [\$469](https://percy.io/pricing) | ❌ | Separate UI | |
148 | | [storycap](https://github.com/reg-viz/storycap) | ✅ | ✅ | Free | ❌ | Implicit approval with [reg-suit](https://github.com/reg-viz/reg-suit) |
149 |
150 | ## Reporting issues
151 |
152 | If something doesn't work, or if the documentation is unclear, please do not hesitate to [raise an issue](https://github.com/fwouts/react-screenshot-test/issues)!
153 |
154 | If it doesn't work for you, it probably doesn't work for others either 🙃
155 |
--------------------------------------------------------------------------------
/asset-transform/index.js:
--------------------------------------------------------------------------------
1 | // Note: this was forked from
2 | // https://github.com/dferber90/jest-transform-css/blob/master/index.js
3 |
4 | // Note: you must increment this version whenever you update this script or
5 | // anything that it uses.
6 | const TRANSFORM_VERSION = "1";
7 |
8 | const crypto = require("crypto");
9 |
10 | module.exports = {
11 | getCacheKey: (fileData, filename, configString, { instrument }) => {
12 | return crypto
13 | .createHash("md5")
14 | .update(TRANSFORM_VERSION)
15 | .update("\0", "utf8")
16 | .update(fileData)
17 | .update("\0", "utf8")
18 | .update(filename)
19 | .update("\0", "utf8")
20 | .update(configString)
21 | .update("\0", "utf8")
22 | .update(instrument ? "instrument" : "")
23 | .digest("hex");
24 | },
25 |
26 | process: (src, filename) => {
27 | return `
28 | const { recordAsset } = require("react-screenshot-test");
29 | module.exports = recordAsset(${JSON.stringify(filename)});
30 | `;
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/brand/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwouts/react-screenshot-test/97e8f8bb28ef0c62cb357e0c4113f036fb32b5dc/brand/logo.png
--------------------------------------------------------------------------------
/brand/logo.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwouts/react-screenshot-test/97e8f8bb28ef0c62cb357e0c4113f036fb32b5dc/brand/logo.sketch
--------------------------------------------------------------------------------
/brand/social.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwouts/react-screenshot-test/97e8f8bb28ef0c62cb357e0c4113f036fb32b5dc/brand/social.png
--------------------------------------------------------------------------------
/brand/social.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/css-modules-transform/index.js:
--------------------------------------------------------------------------------
1 | // Note: this was forked from
2 | // https://github.com/dferber90/jest-transform-css/blob/master/index.js
3 |
4 | // Note: you must increment this version whenever you update this script or
5 | // anything that it uses.
6 | const TRANSFORM_VERSION = "1";
7 |
8 | const crypto = require("crypto");
9 | const crossSpawn = require("cross-spawn");
10 |
11 | module.exports = {
12 | getCacheKey: (fileData, filename, configString, { instrument }) => {
13 | return (
14 | crypto
15 | .createHash("md5")
16 | .update(TRANSFORM_VERSION)
17 | .update("\0", "utf8")
18 | .update(fileData)
19 | .update("\0", "utf8")
20 | .update(filename)
21 | .update("\0", "utf8")
22 | .update(configString)
23 | // TODO load postcssrc (the config) sync and make it part of the cache
24 | // key
25 | // .update("\0", "utf8")
26 | // .update(getPostCssConfig(filename))
27 | .update("\0", "utf8")
28 | .update(instrument ? "instrument" : "")
29 | .digest("hex")
30 | );
31 | },
32 |
33 | process: (src, filename) => {
34 | // The "process" function of this Jest transform must be sync,
35 | // but postcss is async. So we spawn a sync process to do an sync
36 | // transformation!
37 | // https://twitter.com/kentcdodds/status/1043194634338324480
38 | const postcssRunner = `${__dirname}/postcss-runner.js`;
39 | const result = crossSpawn.sync("node", [
40 | "-e",
41 | `
42 | require("${postcssRunner}")(
43 | ${JSON.stringify({
44 | src,
45 | filename
46 | // config,
47 | // options
48 | })}
49 | )
50 | .then(out => { console.log(JSON.stringify(out)) })
51 | `
52 | ]);
53 |
54 | // check for errors of postcss-runner.js
55 | const error = result.stderr.toString();
56 | if (error) {
57 | throw error;
58 | }
59 |
60 | // read results of postcss-runner.js from stdout
61 | let css;
62 | let tokens;
63 | try {
64 | // we likely logged something to the console from postcss-runner
65 | // in order to debug, and hence the parsing fails!
66 | const parsed = JSON.parse(result.stdout.toString());
67 | css = parsed.css;
68 | tokens = parsed.tokens;
69 | if (Array.isArray(parsed.warnings))
70 | parsed.warnings.forEach(warning => {
71 | console.warn(warning);
72 | });
73 | } catch (e) {
74 | // we forward the logs and return no mappings
75 | console.error(result.stderr.toString());
76 | console.log(result.stdout.toString());
77 | return `
78 | console.error("transform-css: Failed to load '${filename}'");
79 | module.exports = {};
80 | `;
81 | }
82 |
83 | // Finally, inject the styles to the document
84 | return `
85 | const { recordCss } = require("react-screenshot-test");
86 | recordCss(${JSON.stringify(css)});
87 | module.exports = ${JSON.stringify(tokens)};
88 | `;
89 | }
90 | };
91 |
--------------------------------------------------------------------------------
/css-modules-transform/postcss-runner.js:
--------------------------------------------------------------------------------
1 | // Note: this was forked from
2 | // https://github.com/dferber90/jest-transform-css/blob/master/postcss-runner.js
3 |
4 | const postcss = require("postcss");
5 | const postcssrc = require("postcss-load-config");
6 | const cssModules = require("postcss-modules");
7 |
8 | // This script is essentially a PostCSS Runner
9 | // https://github.com/postcss/postcss/blob/master/docs/guidelines/runner.md#postcss-runner-guidelines
10 | module.exports = ({ src, filename }) => {
11 | const ctx = {
12 | // Not sure whether the map is useful or not.
13 | // Disabled for now. We can always enable it once it becomes clear.
14 | map: false,
15 | // To ensure that PostCSS generates source maps and displays better syntax
16 | // errors, runners must specify the from and to options. If your runner does
17 | // not handle writing to disk (for example, a gulp transform), you should
18 | // set both options to point to the same file"
19 | // https://github.com/postcss/postcss/blob/master/docs/guidelines/runner.md#21-set-from-and-to-processing-options
20 | from: filename,
21 | to: filename
22 | };
23 | let tokens = {};
24 | return postcssrc(ctx)
25 | .then(
26 | config => ({ ...config, plugins: config.plugins || [] }),
27 | error => {
28 | // Support running without postcss.config.js
29 | // This is useful in case the webpack setup of the consumer does not
30 | // use PostCSS at all and simply uses css-loader in modules mode.
31 | if (error.message.startsWith("No PostCSS Config found in:")) {
32 | return { plugins: [], options: { from: filename, to: filename } };
33 | }
34 | throw error;
35 | }
36 | )
37 | .then(({ plugins, options }) => {
38 | return postcss([
39 | cssModules({
40 | // Should we read generateScopedName from options?
41 | // Does anybody care about the actual names? This is test-only anyways?
42 | // Should be easy to add in case anybody needs it, just pass it through
43 | // from jest.config.js (we have "config" & "options" in css.js)
44 | generateScopedName: "[path][local]-[hash:base64:10]",
45 | getJSON: (cssFileName, exportedTokens, outputFileName) => {
46 | tokens = exportedTokens;
47 | }
48 | }),
49 | ...plugins
50 | ])
51 | .process(src, options)
52 | .then(
53 | result => ({
54 | css: result.css,
55 | tokens,
56 | // Display result.warnings()
57 | // PostCSS runners must output warnings from result.warnings()
58 | // https://github.com/postcss/postcss/blob/master/docs/guidelines/runner.md#32-display-resultwarnings
59 | warnings: result.warnings().map(warn => warn.toString())
60 | }),
61 | // Don’t show JS stack for CssSyntaxError
62 | // PostCSS runners must not show a stack trace for CSS syntax errors,
63 | // as the runner can be used by developers who are not familiar with
64 | // JavaScript. Instead, handle such errors gracefully:
65 | // https://github.com/postcss/postcss/blob/master/docs/guidelines/runner.md#31-dont-show-js-stack-for-csssyntaxerror
66 | error => {
67 | if (error.name === "CssSyntaxError") {
68 | process.stderr.write(error.message + error.showSourceCode());
69 | } else {
70 | throw error;
71 | }
72 | }
73 | );
74 | });
75 | };
76 |
--------------------------------------------------------------------------------
/css-transform/index.js:
--------------------------------------------------------------------------------
1 | // Note: this was forked from
2 | // https://github.com/dferber90/jest-transform-css/blob/master/index.js
3 |
4 | // Note: you must increment this version whenever you update this script or
5 | // anything that it uses.
6 | const TRANSFORM_VERSION = "1";
7 |
8 | const crypto = require("crypto");
9 |
10 | module.exports = {
11 | getCacheKey: (fileData, filename, configString, { instrument }) => {
12 | return crypto
13 | .createHash("md5")
14 | .update(TRANSFORM_VERSION)
15 | .update("\0", "utf8")
16 | .update(fileData)
17 | .update("\0", "utf8")
18 | .update(filename)
19 | .update("\0", "utf8")
20 | .update(configString)
21 | .update("\0", "utf8")
22 | .update(instrument ? "instrument" : "")
23 | .digest("hex");
24 | },
25 |
26 | process: (src, filename) => {
27 | return `
28 | const { recordCss } = require("react-screenshot-test");
29 | recordCss(${JSON.stringify(src)});
30 | module.exports = {};
31 | `;
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/docs/adding-a-browser.md:
--------------------------------------------------------------------------------
1 | # Adding a browser
2 |
3 | TODO: Write me :)
4 |
--------------------------------------------------------------------------------
/docs/architecture.md:
--------------------------------------------------------------------------------
1 | # Architecture
2 |
3 | There are multiple layers to the library:
4 |
5 | - **[ReactScreenshotTest](https://github.com/fwouts/react-screenshot-test/blob/master/src/lib/react/ReactScreenshotTest.ts) is the library's main entrypoint.**
6 | - It exposes a simple API.
7 | - Internally, it coordinates Jest, the component server and the screenshot renderer.
8 | - **[ReactComponentServer](https://github.com/fwouts/react-screenshot-test/blob/master/src/lib/react/ReactComponentServer.ts) is an HTTP server that renders React components server-side.**
9 | - **[ScreenshotRenderer](https://github.com/fwouts/react-screenshot-test/blob/master/src/lib/screenshot-renderer/api.ts) is an interface wrapping a browser.**
10 | - There are multiple implementations, in particular [**PuppeteerScreenshotRenderer**](https://github.com/fwouts/react-screenshot-test/blob/master/src/lib/screenshot-renderer/PuppeteerScreenshotRenderer.ts) (using Puppeteer) and [**PercyScreenshotRenderer**](https://github.com/fwouts/react-screenshot-test/blob/master/src/lib/screenshot-renderer/PercyScreenshotRenderer.ts) (loading screenshots over HTTP).
11 | - **[ScreenshotServer](https://github.com/fwouts/react-screenshot-test/blob/master/src/lib/screenshot-server/api.ts) is an HTTP server that takes a screenshot of a particular URL.**
12 | - A screenshot server can either be local or it can run within Docker.
13 | - Running it in Docker allows us to take consistent snapshots across platforms.
14 |
--------------------------------------------------------------------------------
/docs/assets/facebook-link-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwouts/react-screenshot-test/97e8f8bb28ef0c62cb357e0c4113f036fb32b5dc/docs/assets/facebook-link-screenshot.png
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # Internal Documentation
2 |
3 | How does React Screenshot Test work? Good question.
4 |
5 | Read on:
6 |
7 | - [The technical journey](./technical-journey.md)
8 | - [Architecture overview](./architecture.md)
9 | - [Supporting static assets (e.g. images)](./supporting-static-assets.md)
10 | - [Supporting CSS](./supporting-css.md)
11 | - [Jest hooks (coming soon)](./jest-hooks.md)
12 | - [Storing snapshots (coming soon)](./storing-snapshots.md)
13 | - [Integration with Percy (coming soon)](./percy.md)
14 | - [Adding a browser (coming soon)](./adding-a-browser.md)
15 |
--------------------------------------------------------------------------------
/docs/jest-hooks.md:
--------------------------------------------------------------------------------
1 | # Jest hooks
2 |
3 | TODO: Write me :)
4 |
--------------------------------------------------------------------------------
/docs/percy.md:
--------------------------------------------------------------------------------
1 | # Jest hooks
2 |
3 | TODO: Write me :)
4 |
--------------------------------------------------------------------------------
/docs/storing-snapshots.md:
--------------------------------------------------------------------------------
1 | # Storing snapshots
2 |
3 | TODO: Write me :)
4 |
--------------------------------------------------------------------------------
/docs/supporting-css.md:
--------------------------------------------------------------------------------
1 | # Supporting CSS
2 |
3 | We've talked about [supporting static assets](./supporting-static-assets.md) such as images. But what about CSS?
4 |
5 | ## Styling components in React
6 |
7 | There are [a lot of ways](https://github.com/MicheleBertoli/css-in-js) to do CSS in React. Here is a quick refresher of the most common approaches, which are all supported by React Screenshot Test.
8 |
9 | ### CSS imports
10 |
11 | This is what I like to call "the old way": import a CSS stylesheet and add class names to components.
12 |
13 | ```tsx
14 | import React from "react";
15 | import "./style.css";
16 |
17 | export const UserProfile = () => (
18 |
19 |
User
20 |
21 | );
22 | ```
23 |
24 | ```css
25 | /* style.css */
26 | .user-profile {
27 | background: black;
28 | }
29 |
30 | .user-profile h1 {
31 | font-size: 14px;
32 | }
33 | ```
34 |
35 | ### CSS Modules
36 |
37 | [CSS Modules](https://create-react-app.dev/docs/adding-a-css-modules-stylesheet/) allow class names to be automatically generated to avoid conflicts between stylesheets.
38 |
39 | ```tsx
40 | import React from "react";
41 | import styles from "./style.module.css";
42 |
43 | // className may end up being "UserProfile_container_ax7yz"
44 | export const UserProfile = () => (
45 |