Skip to content

Commit cbbdbda

Browse files
committed
Add test environment and demo project files
1 parent 1b38734 commit cbbdbda

25 files changed

+10618
-0
lines changed

.babelrc

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"presets": [
3+
"@babel/preset-env",
4+
"@babel/preset-react"
5+
],
6+
"env": {
7+
"test": {
8+
"plugins": ["require-context-hook"]
9+
}
10+
}
11+
}

.storybook/config.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { configure } from '@storybook/react';
2+
3+
// pick all stories.js files within the src/ folder
4+
const req = require.context('../src', true, /stories\.js$/);
5+
6+
function loadStories() {
7+
req.keys().forEach(filename => req(filename));
8+
}
9+
10+
configure(loadStories, module);

.storybook/preview-head.html

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover" />

README.md

+38
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,40 @@
11
# visual-testing
22
Example repo with storyshots-puppeteer
3+
4+
Install dependencies:
5+
```
6+
yarn
7+
```
8+
9+
### Run visual tests
10+
11+
Build with storybook + run ui snapshot tests:
12+
```
13+
npm test
14+
```
15+
16+
This will place images currently in `src/__image_snapshots__` based on the stories described in storybook stories.
17+
18+
If any of the files are changed (e.g. related styles in `styles.css`) the related test may break (depending on the threshold of the difference check) and there will be an additional folder with the visual diff of the base and the new images in `src/__image_snapshots__/__diff_output__`.
19+
20+
### For dev
21+
22+
Webpack dev server (with live reload)
23+
```
24+
npm start
25+
```
26+
27+
Storybook (with live reload)
28+
```
29+
npm run storybook
30+
```
31+
32+
### Notes
33+
34+
`test/jest.setup.js` runs only on desktop (via puppeteer) by default, device emulation (apple and android devices) can be added if the commented part at the bottom is uncommented.
35+
After this device emulation is added the device related image snapshots will contain the suffix of the device name.
36+
This way e.g. media-queries can be checked or device related functionality (code based on UA, etc).
37+
38+
Because I wanted to keep it light everything is very basic and currently only `.css` files are supported.
39+
40+
`styleMock.js` is needed to mock styles

dist/index.html

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>The Minimal React Webpack Babel Setup</title>
5+
</head>
6+
<body>
7+
<div id="app"></div>
8+
<script src="/bundle.js"></script>
9+
</body>
10+
</html>

package.json

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"name": "visual-testing",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"scripts": {
7+
"build-storybook": "build-storybook -c .storybook -o ./public",
8+
"start": "webpack-dev-server --config ./webpack.config.js --mode development",
9+
"test": "npm run build-storybook && npm run test:snapshot",
10+
"test:snapshot": "jest --config ./test/jest.config.json",
11+
"test:snapshot:watch": "npm run test:snapshot -- --watch",
12+
"storybook": "start-storybook -p 9001 -c .storybook"
13+
},
14+
"keywords": [],
15+
"author": "",
16+
"license": "ISC",
17+
"dependencies": {
18+
"@babel/core": "^7.6.4",
19+
"@babel/preset-env": "^7.6.3",
20+
"@babel/preset-react": "^7.6.3",
21+
"@storybook/addon-storyshots": "^5.2.4",
22+
"@storybook/addon-storyshots-puppeteer": "^5.2.4",
23+
"@storybook/react": "^5.2.4",
24+
"babel-loader": "^8.0.6",
25+
"babel-plugin-require-context-hook": "^1.0.0",
26+
"classnames": "^2.2.6",
27+
"css-loader": "^3.2.0",
28+
"jest": "^24.9.0",
29+
"puppeteer": "^1.20.0",
30+
"react": "^16.10.2",
31+
"react-dom": "^16.10.2",
32+
"react-hot-loader": "^4.12.15",
33+
"react-test-renderer": "^16.10.2",
34+
"style-loader": "^1.0.0",
35+
"webpack": "^4.41.2",
36+
"webpack-cli": "^3.3.9",
37+
"webpack-dev-server": "^3.8.2"
38+
}
39+
}

src/app.js

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React from 'react';
2+
3+
import Header from './components/header';
4+
import Article from './components/article';
5+
import Footer from './components/footer';
6+
7+
import './styles.css';
8+
9+
const App = ({ isLoggedIn, isFirstArticleHighlighted }) => (
10+
<div className="app">
11+
<Header isLoggedIn={isLoggedIn}/>
12+
<main>
13+
<Article isHighlighted={isFirstArticleHighlighted}/>
14+
<Article />
15+
<Article />
16+
</main>
17+
<Footer isLoggedIn={isLoggedIn}/>
18+
</div>
19+
);
20+
21+
export default App;

src/app.snapshot.js

Whitespace-only changes.

src/components/article/index.js

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from 'react';
2+
import cn from 'classnames';
3+
4+
import './styles.css';
5+
6+
const Article = ({ isHighlighted }) => (
7+
<article className={cn({highlighted: isHighlighted})}>
8+
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
9+
</article>
10+
)
11+
12+
export default Article;

src/components/article/stories.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import React from 'react';
2+
import { storiesOf } from '@storybook/react';
3+
import Article from './';
4+
5+
storiesOf('Article', module)
6+
.add('normal', () => <Article />)
7+
.add('highlighted', () => <Article isHighlighted={true} />);

src/components/article/styles.css

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
article {
2+
padding: 20px;
3+
}
4+
5+
article.highlighted {
6+
background: cadetblue;
7+
color: #fff;
8+
}

src/components/footer/index.js

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from 'react';
2+
3+
import './styles.css';
4+
5+
const Footer = ({ isLoggedIn }) => (
6+
<footer>
7+
<span>&copy; Copyright 2019</span>
8+
{isLoggedIn && <button>Log out</button>}
9+
</footer>
10+
)
11+
12+
export default Footer;

src/components/footer/stories.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import React from 'react';
2+
import { storiesOf } from '@storybook/react';
3+
import Footer from './';
4+
5+
storiesOf('Footer', module)
6+
.add('normal', () => <Footer />)
7+
.add('logged in', () => <Footer isLoggedIn={true} />);

src/components/footer/styles.css

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
footer button {
2+
background: salmon;
3+
border: none;
4+
}
5+
6+
footer span + button {
7+
margin-left: 10px;
8+
}

src/components/header/index.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import React from 'react';
2+
3+
import './styles.css';
4+
5+
const Header = ({ isLoggedIn }) => (
6+
<header>
7+
<button>MenuItem 1</button>
8+
<button>MenuItem 2</button>
9+
<button>MenuItem 3</button>
10+
{isLoggedIn && <button>Log out</button>}
11+
</header>
12+
)
13+
14+
export default Header;

src/components/header/stories.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import React from 'react';
2+
import { storiesOf } from '@storybook/react';
3+
import Header from './';
4+
5+
storiesOf('Header', module)
6+
.add('normal', () => <Header />)
7+
.add('logged in', () => <Header isLoggedIn={true} />);

src/components/header/styles.css

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
header button {
2+
background: aqua;
3+
border: none;
4+
}

src/index.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import React from 'react';
2+
import ReactDOM from 'react-dom';
3+
4+
import App from './app';
5+
6+
ReactDOM.render(
7+
<App />,
8+
document.getElementById('app')
9+
);
10+
11+
module.hot.accept();

src/stories.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from 'react';
2+
import { storiesOf } from '@storybook/react';
3+
import App from './app';
4+
5+
storiesOf('App', module)
6+
.add('normal', () => <App />)
7+
.add('one article highlighted', () => <App isFirstArticleHighlighted={true}/>)
8+
.add('logged in', () => <App isLoggedIn={true}/>)
9+
.add('logged in and one article highlighted', () => <App isLoggedIn={true} isFirstArticleHighlighted={true}/>);

src/styles.css

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
html {
2+
font-size: 62.5%;
3+
}
4+
5+
body {
6+
font-size: 1.6rem;
7+
}

styleMock.js

Whitespace-only changes.

test/jest.config.json

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"moduleNameMapper": {
3+
"\\.(css|scss)$": "<rootDir>/styleMock.js"
4+
},
5+
"testRegex": "((\\.|/*.)(snapshot))\\.js?$",
6+
"rootDir": "..",
7+
"setupFilesAfterEnv": ["<rootDir>/test/jest.setup.js"]
8+
}

test/jest.setup.js

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import registerRequireContextHook from 'babel-plugin-require-context-hook/register';
2+
import initStoryshots from '@storybook/addon-storyshots';
3+
import { imageSnapshot } from '@storybook/addon-storyshots-puppeteer';
4+
const path = require('path');
5+
const devices = require('puppeteer/DeviceDescriptors');
6+
7+
const STORYBOOK_OUTPUT_DIRECTORY = `file://${path.resolve(__dirname, '../public')}`;
8+
const OUTPUT_DIRECTORY = path.resolve(__dirname, '../src/__image_snapshots__');
9+
const DEVICE_DESKTOP = 'desktop';
10+
11+
const DEVICES_TO_TEST = [
12+
'iPad Pro',
13+
'iPad Pro landscape',
14+
'iPhone 6',
15+
'iPhone 6 landscape',
16+
'iPhone X',
17+
'iPhone X landscape',
18+
'Pixel 2',
19+
'Pixel 2 landscape'
20+
];
21+
22+
// configure Jest to work with Webpack's require.context() - storyshot is running under Jest so need to polyfill it
23+
registerRequireContextHook();
24+
25+
const run = device => {
26+
initStoryshots({
27+
suite: 'Storyshots',
28+
test: imageSnapshot({
29+
// could be a running one as well, e.g. storybookUrl: 'http://localhost:9001',
30+
storybookUrl: STORYBOOK_OUTPUT_DIRECTORY,
31+
32+
// set page for puppeteer
33+
customizePage: page => device !== DEVICE_DESKTOP ? page.emulate(devices[device]) : page,
34+
35+
// jest-image-snapshot configuration
36+
getMatchOptions: ({ context: { kind, story }}) => ({
37+
failureThreshold: 0.01,
38+
failureThresholdType: 'percent',
39+
customSnapshotsDir: OUTPUT_DIRECTORY,
40+
41+
// use custom file name. Extension will be added by jest-image-snapshot
42+
customSnapshotIdentifier:
43+
`${kind}-${story}--${device}`
44+
.replace(/[^a-z0-9]/gi, '_') // replace anything other than basic letters or numbers with '_'
45+
.replace(/_+/g, '_') // replace any number of sequential underscores with a single underscore
46+
.toLowerCase()
47+
})
48+
}),
49+
});
50+
}
51+
52+
// uncomment this to run on multiple devices
53+
// for (const device of DEVICES_TO_TEST) {
54+
// run(device);
55+
// }
56+
57+
// running on desktop only
58+
run(DEVICE_DESKTOP);

webpack.config.js

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
const webpack = require('webpack');
2+
3+
module.exports = {
4+
entry: [
5+
'react-hot-loader/patch',
6+
'./src/index.js',
7+
],
8+
module: {
9+
rules: [
10+
{
11+
test: /\.(js|jsx)$/,
12+
exclude: /node_modules/,
13+
use: ['babel-loader'],
14+
},
15+
{
16+
test: /\.css$/i,
17+
use: ['style-loader', 'css-loader'],
18+
},
19+
],
20+
},
21+
resolve: {
22+
extensions: ['*', '.js', '.jsx'],
23+
},
24+
output: {
25+
path: __dirname + '/dist',
26+
publicPath: '/',
27+
filename: 'bundle.js',
28+
},
29+
plugins: [
30+
new webpack.HotModuleReplacementPlugin(),
31+
],
32+
devServer: {
33+
contentBase: './dist',
34+
hot: true,
35+
},
36+
};

0 commit comments

Comments
 (0)