├── .yarn
└── versions
│ └── 3a2fbd38.yml
├── .prettierignore
├── example
├── .yarnrc.yml
├── test-lib
│ ├── mocks
│ │ └── fileMock.js
│ ├── setup
│ │ └── setup-test-framework.js
│ └── screenshot
│ │ └── index.ejs
├── src
│ ├── __tests__
│ │ ├── __image_snapshots__
│ │ │ ├── .gitignore
│ │ │ ├── button-browser-js-button-should-render-correctly-1-snap.png
│ │ │ ├── button-browser-js-button-should-render-correctly-test-1-1-snap.png
│ │ │ ├── button-browser-js-button-should-render-correctly-test-2-1-snap.png
│ │ │ ├── button-browser-js-button-with-describe-should-render-correctly-1-snap.png
│ │ │ ├── button-browser-js-button-with-describe-1-should-render-correctly-1-snap.png
│ │ │ ├── button-browser-js-button-with-describe-2-should-render-correctly-1-snap.png
│ │ │ ├── button-browser-js-button-with-describe-1-should-render-correctly-test-1-1-snap.png
│ │ │ ├── button-browser-js-button-with-describe-1-should-render-correctly-test-2-1-snap.png
│ │ │ ├── button-browser-js-button-with-describe-2-should-render-correctly-test-1-1-snap.png
│ │ │ └── button-browser-js-button-with-describe-2-should-render-correctly-test-2-1-snap.png
│ │ └── Button.browser.js
│ └── Button.js
├── jest-puppeteer.config.js
├── .gitignore
├── index.ejs
├── jest.config.json
├── babel.config.js
├── package.json
└── jest-puppeteer-react.config.js
├── setup.js
├── src
├── index.js
├── index.d.ts
├── webpack
│ ├── render.browser.js
│ ├── globals.browser.js
│ └── entry.browser.js
├── docker
│ ├── entrypoint.sh
│ ├── Dockerfile
│ └── index.js
├── PuppeteerReactEnvironment.js
├── render.js
└── global.js
├── teardown.js
├── environment.js
├── bin
└── debug.js
├── .npmignore
├── .yarnrc.yml
├── .gitignore
├── jest-preset.json
├── expect-puppeteer-react.js
├── .editorconfig
├── .github
└── workflows
│ └── node.js.yml
├── LICENSE
├── package.json
└── README.md
/.yarn/versions/3a2fbd38.yml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | example/node_modules
--------------------------------------------------------------------------------
/example/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | yarnPath: .yarn/releases/yarn-4.1.1.cjs
2 |
--------------------------------------------------------------------------------
/example/test-lib/mocks/fileMock.js:
--------------------------------------------------------------------------------
1 | module.exports = {};
2 |
--------------------------------------------------------------------------------
/setup.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./src/global').setup;
2 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | module.exports.render = require('./render');
2 |
--------------------------------------------------------------------------------
/example/src/__tests__/__image_snapshots__/.gitignore:
--------------------------------------------------------------------------------
1 | __diff_output__
--------------------------------------------------------------------------------
/teardown.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./src/global').teardown;
2 |
--------------------------------------------------------------------------------
/environment.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./src/PuppeteerReactEnvironment');
2 |
--------------------------------------------------------------------------------
/example/test-lib/setup/setup-test-framework.js:
--------------------------------------------------------------------------------
1 | require('jest-puppeteer-react/expect-puppeteer-react');
2 |
--------------------------------------------------------------------------------
/bin/debug.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | require('../setup.js')({
4 | noInfo: false,
5 | debugOnly: true,
6 | rootDir: process.cwd(),
7 | });
8 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | example/
2 | node_modules/
3 | .idea/
4 | yarn-error.log
5 | __tests__
6 | .travis.yml
7 | .prettierignore
8 | .gitignore
9 | .editorconfig
10 | yarn.lock
11 |
--------------------------------------------------------------------------------
/example/jest-puppeteer.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | launch: {
3 | // dumpio: true,
4 | // headless: false,
5 | // slowMo: 1000,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | enableImmutableInstalls: false
2 |
3 | enableTelemetry: false
4 |
5 | nmMode: classic
6 |
7 | nodeLinker: node-modules
8 |
9 | yarnPath: .yarn/releases/yarn-4.1.1.cjs
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | lib/
2 | node_modules/
3 | .idea/
4 |
5 | # yarn
6 | .pnp.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/sdks
12 | !.yarn/versions
13 | yarn-error.log
14 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | coverage
2 | build
3 | node_modules
4 | .yalc
5 | yalc.lock
6 |
7 | # yarn
8 | .pnp.*
9 | .yarn/*
10 | !.yarn/patches
11 | !.yarn/plugins
12 | !.yarn/releases
13 | !.yarn/sdks
14 | !.yarn/versions
15 | yarn-error.log
16 |
--------------------------------------------------------------------------------
/example/src/__tests__/__image_snapshots__/button-browser-js-button-should-render-correctly-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hapag-Lloyd/jest-puppeteer-react/HEAD/example/src/__tests__/__image_snapshots__/button-browser-js-button-should-render-correctly-1-snap.png
--------------------------------------------------------------------------------
/example/src/__tests__/__image_snapshots__/button-browser-js-button-should-render-correctly-test-1-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hapag-Lloyd/jest-puppeteer-react/HEAD/example/src/__tests__/__image_snapshots__/button-browser-js-button-should-render-correctly-test-1-1-snap.png
--------------------------------------------------------------------------------
/example/src/__tests__/__image_snapshots__/button-browser-js-button-should-render-correctly-test-2-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hapag-Lloyd/jest-puppeteer-react/HEAD/example/src/__tests__/__image_snapshots__/button-browser-js-button-should-render-correctly-test-2-1-snap.png
--------------------------------------------------------------------------------
/jest-preset.json:
--------------------------------------------------------------------------------
1 | {
2 | "globalSetup": "jest-puppeteer-react/setup",
3 | "globalTeardown": "jest-puppeteer-react/teardown",
4 | "testEnvironment": "jest-puppeteer-react/environment",
5 | "setupFilesAfterEnv":
6 | ["jest-puppeteer-react/expect-puppeteer-react"]
7 | }
8 |
--------------------------------------------------------------------------------
/example/src/__tests__/__image_snapshots__/button-browser-js-button-with-describe-should-render-correctly-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hapag-Lloyd/jest-puppeteer-react/HEAD/example/src/__tests__/__image_snapshots__/button-browser-js-button-with-describe-should-render-correctly-1-snap.png
--------------------------------------------------------------------------------
/example/src/__tests__/__image_snapshots__/button-browser-js-button-with-describe-1-should-render-correctly-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hapag-Lloyd/jest-puppeteer-react/HEAD/example/src/__tests__/__image_snapshots__/button-browser-js-button-with-describe-1-should-render-correctly-1-snap.png
--------------------------------------------------------------------------------
/example/src/__tests__/__image_snapshots__/button-browser-js-button-with-describe-2-should-render-correctly-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hapag-Lloyd/jest-puppeteer-react/HEAD/example/src/__tests__/__image_snapshots__/button-browser-js-button-with-describe-2-should-render-correctly-1-snap.png
--------------------------------------------------------------------------------
/example/src/__tests__/__image_snapshots__/button-browser-js-button-with-describe-1-should-render-correctly-test-1-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hapag-Lloyd/jest-puppeteer-react/HEAD/example/src/__tests__/__image_snapshots__/button-browser-js-button-with-describe-1-should-render-correctly-test-1-1-snap.png
--------------------------------------------------------------------------------
/example/src/__tests__/__image_snapshots__/button-browser-js-button-with-describe-1-should-render-correctly-test-2-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hapag-Lloyd/jest-puppeteer-react/HEAD/example/src/__tests__/__image_snapshots__/button-browser-js-button-with-describe-1-should-render-correctly-test-2-1-snap.png
--------------------------------------------------------------------------------
/example/src/__tests__/__image_snapshots__/button-browser-js-button-with-describe-2-should-render-correctly-test-1-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hapag-Lloyd/jest-puppeteer-react/HEAD/example/src/__tests__/__image_snapshots__/button-browser-js-button-with-describe-2-should-render-correctly-test-1-1-snap.png
--------------------------------------------------------------------------------
/example/src/__tests__/__image_snapshots__/button-browser-js-button-with-describe-2-should-render-correctly-test-2-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hapag-Lloyd/jest-puppeteer-react/HEAD/example/src/__tests__/__image_snapshots__/button-browser-js-button-with-describe-2-should-render-correctly-test-2-1-snap.png
--------------------------------------------------------------------------------
/example/src/Button.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | export default class Button extends Component {
5 | static propTypes = {
6 | label: PropTypes.string,
7 | };
8 |
9 | render() {
10 | return ;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/example/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Test
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/expect-puppeteer-react.js:
--------------------------------------------------------------------------------
1 | require('expect-puppeteer');
2 |
3 | const { toMatchImageSnapshot } = require('jest-image-snapshot');
4 |
5 | // maybe extend the matcher to understand page objects and making a screenshot on its own
6 | // async matchers aren't supported yet: https://github.com/facebook/jest/pull/5919
7 | expect.extend({ toMatchImageSnapshot });
8 |
--------------------------------------------------------------------------------
/example/test-lib/screenshot/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Test
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/index.d.ts:
--------------------------------------------------------------------------------
1 | import { Page, Viewport, DirectNavigationOptions } from "puppeteer";
2 |
3 | export interface JestPuppeteerReactRenderOptions extends DirectNavigationOptions {
4 | dumpConsole?: boolean,
5 | before?(page: Page): any,
6 | after?(page: Page): any,
7 | viewport?: Viewport,
8 | }
9 |
10 | export function render(
11 | component: JSX.Element,
12 | options?: JestPuppeteerReactRenderOptions
13 | ): Promise;
14 |
--------------------------------------------------------------------------------
/example/jest.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleNameMapper": {
3 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/test-lib/mocks/fileMock.js"
4 | },
5 | "setupFilesAfterEnv": ["/test-lib/setup/setup-test-framework.js"],
6 | "globalSetup": "/../setup",
7 | "globalTeardown": "/../teardown",
8 | "transform": {
9 | "^.+\\.js?$": "babel-jest"
10 | },
11 | "testEnvironment": "jsdom"
12 | }
13 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 | #
5 | # Some of these options are also respected by Prettier
6 |
7 | root = true
8 |
9 | [*]
10 | indent_style = space
11 | indent_size = 4
12 | end_of_line = lf
13 | charset = utf-8
14 | trim_trailing_whitespace = true
15 | insert_final_newline = true
16 |
17 | [{*.json,*.yml}]
18 | indent_style = space
19 | indent_size = 4
20 |
21 | [*.md]
22 | trim_trailing_whitespace = false
23 |
--------------------------------------------------------------------------------
/src/webpack/render.browser.js:
--------------------------------------------------------------------------------
1 | export const render = async (reactNode, options) => {
2 | const testName = window.__path.join(' ');
3 |
4 | if (testName in window.__tests) {
5 | throw new Error(`Test name collision detected for "${testName}". Please use describe() or rename tests.`);
6 | }
7 |
8 | // console.log('browser renderer', reactNode, testName);
9 | window.__tests[testName] = Object.assign({}, options, {
10 | reactNode,
11 | path: [...window.__path],
12 | });
13 |
14 | return window.page;
15 | };
16 |
--------------------------------------------------------------------------------
/src/docker/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | nohup google-chrome \
4 | --no-sandbox \
5 | --disable-background-networking \
6 | --disable-default-apps \
7 | --disable-extensions \
8 | --disable-sync \
9 | --disable-translate \
10 | --headless \
11 | --hide-scrollbars \
12 | --metrics-recording-only \
13 | --mute-audio \
14 | --no-first-run \
15 | --safebrowsing-disable-auto-update \
16 | --ignore-certificate-errors \
17 | --ignore-ssl-errors \
18 | --ignore-certificate-errors-spki-list \
19 | --user-data-dir=/tmp \
20 | --remote-debugging-port=9222 \
21 | --remote-debugging-address=0.0.0.0
--------------------------------------------------------------------------------
/src/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM debian:stable-slim
2 |
3 | EXPOSE 9222
4 |
5 | # Install dependencies
6 | RUN apt-get update && \
7 | apt-get -y upgrade && \
8 | apt-get install -yq curl gnupg
9 |
10 | # Install Google Chrome
11 | RUN curl https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \
12 | echo 'deb [arch=amd64] http://dl.google.com/linux/chrome/deb stable main' >> /etc/apt/sources.list.d/google-chrome.list && \
13 | apt-get update && \
14 | apt-get install -y google-chrome-unstable --no-install-recommends && \
15 | rm -fr /var/lib/apt/lists/* && \
16 | apt-get purge --auto-remove -y curl && \
17 | rm -fr /src/*.deb
18 |
19 | COPY entrypoint.sh .
20 | RUN chmod +x ./entrypoint.sh
21 |
22 | ENTRYPOINT ["./entrypoint.sh"]
23 |
--------------------------------------------------------------------------------
/.github/workflows/node.js.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Node.js CI
5 |
6 | on:
7 | push:
8 | branches-ignore:
9 | - $default-branch
10 | - master
11 | pull_request:
12 | branches-ignore:
13 | - $default-branch
14 | - master
15 |
16 | jobs:
17 | build:
18 |
19 | runs-on: ubuntu-latest
20 |
21 | steps:
22 | - uses: actions/checkout@v3
23 | - uses: actions/setup-node@v3
24 | with:
25 | node-version: '20'
26 | - run: yarn install
27 | - run: yarn pretest
28 | - run: yarn test
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Hapag-Lloyd AG, Ballindamm 25, 20095 Hamburg/Germany and individual contributors.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/PuppeteerReactEnvironment.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { TestEnvironment } = require('jest-environment-puppeteer');
3 | const { promisify } = require('util');
4 | const fs = require('fs');
5 |
6 | class PuppeteerReactEnvironment extends TestEnvironment {
7 | async setup() {
8 | await super.setup();
9 |
10 | const rootPath = process.cwd(); // of your project under test
11 |
12 | const configName = 'jest-puppeteer-react.config';
13 | const statPromisified = promisify(fs.stat);
14 |
15 | let configExt = '.cjs';
16 |
17 | try {
18 | await statPromisified(path.join(process.cwd(), `${configName}${configExt}`));
19 | } catch (e) {
20 | // Fallback extension if CommonJS module not exist
21 | configExt = '.js';
22 | }
23 |
24 | const config = require(path.join(rootPath, `${configName}${configExt}`));
25 |
26 | if (!config.port) {
27 | config.port = 1111;
28 | }
29 | const { renderOptions } = config;
30 | if (renderOptions.viewport) {
31 | // ensure width and height are set if the viewport was set
32 | if (!renderOptions.viewport.width) renderOptions.viewport.width = 600;
33 | if (!renderOptions.viewport.height) renderOptions.viewport.height = 800;
34 | }
35 |
36 | this.global._jest_puppeteer_react_default_config = config;
37 | }
38 |
39 | async teardown() {
40 | delete this.global._jest_puppeteer_react_default_config;
41 |
42 | await super.teardown();
43 | }
44 | }
45 |
46 | module.exports = PuppeteerReactEnvironment;
47 |
--------------------------------------------------------------------------------
/example/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = (api) => {
2 | api.cache.never();
3 |
4 | const presetEnvOptions = {
5 | modules: 'auto',
6 | useBuiltIns: 'entry',
7 | corejs: 2,
8 | debug: false,
9 | shippedProposals: true,
10 | targets: {
11 | node: 'current',
12 | chrome: '110',
13 | },
14 | };
15 |
16 | return {
17 | presets: ['@babel/preset-react', ['@babel/preset-env', presetEnvOptions]],
18 | plugins: [
19 | '@babel/plugin-syntax-dynamic-import',
20 | '@babel/plugin-syntax-import-meta',
21 | '@babel/plugin-proposal-class-properties',
22 | '@babel/plugin-proposal-json-strings',
23 | [
24 | '@babel/plugin-proposal-decorators',
25 | {
26 | legacy: true,
27 | },
28 | ],
29 | '@babel/plugin-proposal-function-sent',
30 | '@babel/plugin-proposal-export-namespace-from',
31 | '@babel/plugin-proposal-numeric-separator',
32 | '@babel/plugin-proposal-throw-expressions',
33 | '@babel/plugin-proposal-export-default-from',
34 | '@babel/plugin-proposal-logical-assignment-operators',
35 | '@babel/plugin-proposal-optional-chaining',
36 | [
37 | '@babel/plugin-proposal-pipeline-operator',
38 | {
39 | proposal: 'minimal',
40 | },
41 | ],
42 | '@babel/plugin-proposal-nullish-coalescing-operator',
43 | '@babel/plugin-proposal-do-expressions',
44 | '@babel/plugin-proposal-function-bind',
45 | ],
46 | };
47 | };
48 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "jest --config jest.config.json",
8 | "test:clearCache": "jest --clearCache --config jest.config.json"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "devDependencies": {
13 | "@babel/core": "^7.24.4",
14 | "@babel/plugin-proposal-class-properties": "^7.18.6",
15 | "@babel/plugin-proposal-decorators": "^7.24.1",
16 | "@babel/plugin-proposal-do-expressions": "^7.24.1",
17 | "@babel/plugin-proposal-export-default-from": "^7.24.1",
18 | "@babel/plugin-proposal-export-namespace-from": "^7.18.9",
19 | "@babel/plugin-proposal-function-bind": "^7.24.1",
20 | "@babel/plugin-proposal-function-sent": "^7.24.1",
21 | "@babel/plugin-proposal-json-strings": "^7.18.6",
22 | "@babel/plugin-proposal-logical-assignment-operators": "^7.20.7",
23 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
24 | "@babel/plugin-proposal-numeric-separator": "^7.18.6",
25 | "@babel/plugin-proposal-optional-chaining": "^7.21.0",
26 | "@babel/plugin-proposal-pipeline-operator": "^7.24.1",
27 | "@babel/plugin-proposal-throw-expressions": "^7.24.1",
28 | "@babel/plugin-syntax-dynamic-import": "^7.8.3",
29 | "@babel/plugin-syntax-import-meta": "^7.10.4",
30 | "@babel/preset-env": "^7.24.4",
31 | "@babel/preset-react": "^7.24.1",
32 | "babel-core": "^7.0.0-bridge.0",
33 | "babel-jest": "^29.7.0",
34 | "babel-loader": "^9.1.2",
35 | "html-webpack-plugin": "^5.6.0",
36 | "jest": "^29.7.0",
37 | "jest-environment-jsdom": "^29.7.0"
38 | },
39 | "dependencies": {
40 | "@babel/polyfill": "^7.12.1",
41 | "format-util": "^1.0.5",
42 | "jest-puppeteer-react": "file:.yalc/jest-puppeteer-react",
43 | "prop-types": "^15.8.1",
44 | "react": "^18.2.0",
45 | "react-dom": "^18.2.0"
46 | },
47 | "packageManager": "yarn@4.1.1"
48 | }
49 |
--------------------------------------------------------------------------------
/example/src/__tests__/Button.browser.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment jest-puppeteer-react/environment
3 | */
4 | import React from 'react';
5 | import { render } from 'jest-puppeteer-react';
6 |
7 | import Button from '../Button';
8 |
9 | describe('Button', () => {
10 | jest.setTimeout(60000);
11 | test('should render correctly', async () => {
12 | await render(, {
13 | viewport: { width: 200, height: 100, deviceScaleFactor: 1 },
14 | });
15 |
16 | const screenshot = await page.screenshot();
17 | expect(screenshot).toMatchImageSnapshot();
18 | });
19 |
20 | test.each(['test 1', 'test 2'])('should render correctly %s', async (t) => {
21 | await render(, {
22 | viewport: { width: 100, height: 100, deviceScaleFactor: 1 },
23 | });
24 |
25 | const screenshot = await page.screenshot();
26 | expect(screenshot).toMatchImageSnapshot();
27 | });
28 | describe('with describe', () => {
29 | test('should render correctly', async () => {
30 | await render(, {
31 | viewport: { width: 100, height: 100, deviceScaleFactor: 1 },
32 | });
33 |
34 | const screenshot = await page.screenshot();
35 | expect(screenshot).toMatchImageSnapshot();
36 | });
37 | });
38 |
39 | describe.each(['describe 1', 'describe 2'])('with %s', (d) => {
40 | test('should render correctly', async () => {
41 | await render(, {
42 | viewport: { width: 100, height: 100, deviceScaleFactor: 1 },
43 | });
44 |
45 | const screenshot = await page.screenshot();
46 | expect(screenshot).toMatchImageSnapshot();
47 | });
48 | });
49 |
50 | describe.each(['describe 1', 'describe 2'])('with %s', (d) => {
51 | test.each(['test 1', 'test 2'])('should render correctly %s', async (t) => {
52 | await render(, {
53 | viewport: { width: 100, height: 100, deviceScaleFactor: 1 },
54 | });
55 |
56 | const screenshot = await page.screenshot();
57 | expect(screenshot).toMatchImageSnapshot();
58 | });
59 | });
60 | });
61 |
--------------------------------------------------------------------------------
/src/render.js:
--------------------------------------------------------------------------------
1 | const merge = require('lodash/merge');
2 | const debug = require('debug')('jest-puppeteer-react');
3 |
4 | async function render(reactNode, options) {
5 | const { currentTestName } = expect.getState();
6 | debug('rendering for testname ' + currentTestName);
7 | const config = global._jest_puppeteer_react_default_config;
8 | const opts = merge({}, config.renderOptions, options);
9 | const protocol = config.useHttps ? 'https' : 'http';
10 | const host = config.useDocker ? config.dockerHost : 'localhost';
11 |
12 | const url = `${protocol}://${host}:${config.port}?test=${encodeURIComponent(currentTestName)}`;
13 |
14 | const pageConfig = {};
15 | if (opts.timeout) {
16 | pageConfig.timeout = opts.timeout;
17 | }
18 | if (opts.waitUntil) {
19 | pageConfig.waitUntil = opts.waitUntil;
20 | }
21 |
22 | // jest-puppeteer reuses page (browser tab) beetween tests
23 | // `__jestReactPuppeteerEventsSubscription` flag needs to avoid subscription duplication
24 | if (opts.dumpConsole && !page.__jestReactPuppeteerEventsSubscription) {
25 | page.on('console', (msg) => {
26 | console.log(`event "console" from "${currentTestName}"`);
27 |
28 | const msgType = msg.type();
29 | const msgText = msg.text();
30 | const consoleLogType = msgType === 'warning' ? 'warn' : msgType;
31 | if (typeof console[consoleLogType] === 'function') {
32 | console[consoleLogType](msgText);
33 | } else {
34 | console.log('Unexpected message type', consoleLogType);
35 | console.log(msgText);
36 | }
37 | });
38 | page.on('error', (msg) => {
39 | console.error(`event "error" from "${currentTestName}"`, msg);
40 | });
41 | page.on('pageerror', (msg) => {
42 | console.error(`event "pageerror" from "${currentTestName}"`, msg);
43 | });
44 | page.__jestReactPuppeteerEventsSubscription = true;
45 | }
46 |
47 | if (opts.before) {
48 | await opts.before(page);
49 | }
50 |
51 | if (opts.viewport) {
52 | debug('setting a viewport from options');
53 | await page.setViewport(opts.viewport);
54 | }
55 |
56 | debug('page.goto ' + url);
57 | await page.goto(url, pageConfig);
58 |
59 | if (opts.after) {
60 | await opts.after(page);
61 | }
62 |
63 | return page;
64 | }
65 |
66 | module.exports = render;
67 |
--------------------------------------------------------------------------------
/example/jest-puppeteer-react.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 | const path = require('path');
4 | const debug = require('debug')('jest-puppeteer-react');
5 |
6 | const isMac = process.platform === 'darwin';
7 | const isWin32 = process.platform === 'win32';
8 |
9 | const dockerHost = () => {
10 | if (isMac) {
11 | return 'docker.for.mac.host.internal';
12 | }
13 | if (isWin32) {
14 | return 'host.docker.internal';
15 | }
16 | return '172.17.0.1';
17 | };
18 |
19 | function getIPAddress() {
20 | const interfaces = require('os').networkInterfaces();
21 | for (let devName in interfaces) {
22 | const iface = interfaces[devName];
23 |
24 | for (let i = 0; i < iface.length; i++) {
25 | const alias = iface[i];
26 | if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) return alias.address;
27 | }
28 | }
29 |
30 | return '0.0.0.0';
31 | }
32 |
33 | debug(`get ip address: ${getIPAddress()}`);
34 |
35 | module.exports = {
36 | generateWebpackConfig: function generateWebpackConfig(entryFiles, aliasObject) {
37 | return {
38 | mode: 'development',
39 | entry: { test: ['@babel/polyfill', ...entryFiles] },
40 | devtool: 'eval-source-map',
41 | output: {
42 | path: path.resolve(__dirname, 'build'),
43 | filename: '[name].js',
44 | },
45 | devServer: {
46 | server: 'https',
47 | },
48 | resolve: {
49 | alias: aliasObject,
50 | },
51 | plugins: [
52 | new HtmlWebpackPlugin({
53 | template: path.resolve(__dirname, 'index.ejs'),
54 | }),
55 | ],
56 | module: {
57 | rules: [
58 | {
59 | test: /\.m?js$/,
60 | exclude: /(node_modules)/,
61 | use: {
62 | loader: 'babel-loader',
63 | },
64 | },
65 | ],
66 | },
67 | };
68 | },
69 | port: 1111,
70 | renderOptions: {
71 | viewport: { deviceScaleFactor: 1 },
72 | dumpConsole: false,
73 | },
74 | useHttps: true,
75 | useDocker: true,
76 | dockerHost: dockerHost(),
77 | dockerEntrypoint: '""', // overwrites the default entrypoint of the image
78 | dockerRunOptions: '--ipc=host',
79 | dockerCommand:
80 | 'google-chrome ' +
81 | '--no-sandbox ' +
82 | '--disable-background-networking ' +
83 | '--disable-default-apps ' +
84 | '--disable-extensions ' +
85 | '--disable-sync ' +
86 | '--disable-translate ' +
87 | '--headless ' +
88 | '--hide-scrollbars ' +
89 | '--metrics-recording-only ' +
90 | '--mute-audio ' +
91 | '--no-first-run ' +
92 | '--safebrowsing-disable-auto-update ' +
93 | '--ignore-certificate-errors ' +
94 | '--ignore-ssl-errors ' +
95 | '--ignore-certificate-errors-spki-list ' +
96 | '--user-data-dir=/tmp ' +
97 | '--remote-debugging-port=9222 ' +
98 | '--remote-debugging-address=0.0.0.0',
99 | };
100 |
--------------------------------------------------------------------------------
/src/webpack/globals.browser.js:
--------------------------------------------------------------------------------
1 | import format from 'format-util';
2 | import pretty from 'pretty-format';
3 |
4 | // some snippet of the code inspired/copied by https://github.com/facebook/jest/blob/master/packages/jest-each/src/bind.js
5 |
6 | if (!window.Proxy) throw new Error('The environment needs to support window.Proxy!');
7 |
8 | const makeShrugger = () => {
9 | const functionMock = () => {};
10 | return new Proxy(functionMock, {
11 | apply: () => makeShrugger(), // if called as function
12 | get: (target, name) => {
13 | // if trying to get property
14 | if (name in target) {
15 | return target[name];
16 | }
17 | return makeShrugger();
18 | },
19 | });
20 | };
21 |
22 | // ¯\_(ツ)_/¯ .. allows you to call anything on him and just ignores it
23 | const shrugger = makeShrugger();
24 | window.jest = shrugger;
25 | window.page = shrugger;
26 | window.expect = shrugger;
27 |
28 | const notImplementedYet = (name) => () => {
29 | throw new Error(`${name} is not supported yet in jest-puppeteer-react`);
30 | };
31 |
32 | window.beforeAll = notImplementedYet('beforeAll');
33 | window.afterAll = notImplementedYet('afterAll');
34 | window.beforeEach = notImplementedYet('beforeEach');
35 | window.afterEach = notImplementedYet('afterEach');
36 |
37 | window.__path = []; // current test path
38 | window.__tests = {}; // 'Button should render':
39 |
40 | const SUPPORTED_PLACEHOLDERS = /%[sdifjoOp%]/g;
41 | const PRETTY_PLACEHOLDER = '%p';
42 |
43 | const applyRestParams = (params, test) => {
44 | // if (params.length < test.length) return done => test(...params, done);
45 |
46 | return () => test(...params);
47 | };
48 |
49 | const getPrettyIndexes = (placeholders) =>
50 | placeholders.reduce((indexes, placeholder, index) => (placeholder === PRETTY_PLACEHOLDER ? indexes.concat(index) : indexes), []);
51 |
52 | const arrayFormat = (title, ...args) => {
53 | const placeholders = title.match(SUPPORTED_PLACEHOLDERS) || [];
54 | const prettyIndexes = getPrettyIndexes(placeholders);
55 |
56 | const { title: prettyTitle, args: remainingArgs } = args.reduce(
57 | (acc, arg, index) => {
58 | if (prettyIndexes.indexOf(index) !== -1) {
59 | return {
60 | args: acc.args,
61 | title: acc.title.replace(PRETTY_PLACEHOLDER, pretty(arg, { maxDepth: 1, min: true })),
62 | };
63 | }
64 |
65 | return {
66 | args: acc.args.concat([arg]),
67 | title: acc.title,
68 | };
69 | },
70 | { args: [], title }
71 | );
72 |
73 | return format(prettyTitle, ...remainingArgs.slice(0, placeholders.length - prettyIndexes.length));
74 | };
75 |
76 | const each =
77 | (cb) =>
78 | (...args) => {
79 | return (title, testFun) => {
80 | const table = args[0].every(Array.isArray) ? args[0] : args[0].map((entry) => [entry]);
81 | return table.forEach((row) => cb(arrayFormat(title, ...row), applyRestParams(row, testFun)));
82 | };
83 | };
84 |
85 | window.describe = (name, fun) => {
86 | window.__path.push(name);
87 | // console.log('describe', window.__path);
88 | fun();
89 | window.__path.pop();
90 | };
91 | window.describe.each = each(window.describe);
92 | window.describe.only = window.describe;
93 | window.describe.skip = () => {};
94 |
95 | window.test = (name, fun) => {
96 | window.__path.push(name);
97 | // console.log('test', window.__path);
98 | fun();
99 | window.__path.pop();
100 | };
101 | window.test.each = each(window.test);
102 | window.test.only = window.test;
103 | window.test.skip = () => {};
104 | window.test.todo = () => {};
105 |
106 | window.it = window.test;
107 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jest-puppeteer-react",
3 | "version": "12.0.1",
4 | "description": "screenshot tests for your react components in chromium using puppeteer & jest",
5 | "main": "src/index.js",
6 | "types": "src/index.d.ts",
7 | "bin": {
8 | "jestPuppeteerReactDebug": "./bin/debug.js"
9 | },
10 | "engines": {
11 | "node": ">= 16.1.0"
12 | },
13 | "packageManager": "yarn@4.1.1",
14 | "scripts": {
15 | "pretest": "yalc publish && cd example && yalc add jest-puppeteer-react && yarn install",
16 | "test": "cd example && yarn test",
17 | "precommit": "pretty-quick --staged",
18 | "prettier": "prettier --write \"./**/*.js\""
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "git+https://github.com/hapag-lloyd/jest-puppeteer-react.git"
23 | },
24 | "keywords": [
25 | "jest",
26 | "react",
27 | "puppeteer",
28 | "test",
29 | "testing"
30 | ],
31 | "author": "Ansgar Mertens ",
32 | "contributors": [
33 | "Jan Rosczak ",
34 | "Stefan Schult ",
35 | "Albino Tonnina ",
36 | "Timo Koenig ",
37 | "Aleksei Androsov ",
38 | "Mihkel Eidast "
39 | ],
40 | "license": "MIT",
41 | "bugs": {
42 | "url": "https://github.com/hapag-lloyd/jest-puppeteer-react/issues"
43 | },
44 | "homepage": "https://github.com/hapag-lloyd/jest-puppeteer-react#readme",
45 | "dependencies": {
46 | "debug": "^4.3.4",
47 | "docker-cli-js": "^2.10.0",
48 | "expect-puppeteer": "^10.0.0",
49 | "format-util": "^1.0.5",
50 | "glob": "^10.3.12",
51 | "jest-each": "^29.7.0",
52 | "jest-environment-node": "^29.7.0",
53 | "jest-environment-puppeteer": "^10.0.1",
54 | "jest-image-snapshot": "^6.4.0",
55 | "jest-puppeteer": "^10.0.1",
56 | "lodash": "^4.17.21",
57 | "ora": "5.4.1",
58 | "pretty-format": "^29.7.0"
59 | },
60 | "peerDependencies": {
61 | "jest": "29.x",
62 | "puppeteer": "*",
63 | "react": "18.x",
64 | "react-dom": "18.x",
65 | "webpack": "5.x",
66 | "webpack-dev-server": "4.x"
67 | },
68 | "devDependencies": {
69 | "husky": "^9.0.11",
70 | "mkdirp": "^3.0.1",
71 | "prettier": "^3.2.5",
72 | "pretty-quick": "^4.0.0",
73 | "puppeteer": "^22.6.3",
74 | "react": "^18.2.0",
75 | "react-dom": "^18.2.0",
76 | "webpack": "^5.91.0",
77 | "webpack-dev-server": "^5.0.4",
78 | "yalc": "^1.0.0-pre.53"
79 | },
80 | "prettier": {
81 | "tabWidth": 4,
82 | "trailingComma": "es5",
83 | "singleQuote": true,
84 | "printWidth": 140,
85 | "overrides": [
86 | {
87 | "files": "*.json",
88 | "options": {
89 | "parser": "json"
90 | }
91 | },
92 | {
93 | "files": "*.md",
94 | "options": {
95 | "parser": "markdown"
96 | }
97 | },
98 | {
99 | "files": "*.ts",
100 | "options": {
101 | "parser": "typescript"
102 | }
103 | },
104 | {
105 | "files": "*.tsx",
106 | "options": {
107 | "parser": "typescript"
108 | }
109 | },
110 | {
111 | "files": ".prettierrc",
112 | "options": {
113 | "parser": "json"
114 | }
115 | },
116 | {
117 | "files": "*.html",
118 | "options": {
119 | "parser": "html"
120 | }
121 | },
122 | {
123 | "files": "*.css",
124 | "options": {
125 | "parser": "css"
126 | }
127 | }
128 | ]
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # jest-puppeteer-react [](https://travis-ci.com/Hapag-Lloyd/jest-puppeteer-react)
2 |
3 | ```
4 | yarn add jest-puppeteer-react
5 | ```
6 |
7 | This lib combines `jest-puppeteer` with webpack and webpack-dev-server to render your React components so you don't have to setup a server and navigate to it. It also includes `jest-image-snapshot` which adds the `toMatchImageSnapshot` matcher to expect.
8 |
9 | ## Setup
10 |
11 | 1. Use the preset in your jest configuration:
12 |
13 | ```
14 | {
15 | "preset": "jest-puppeteer-react"
16 | }
17 | ```
18 |
19 | Or require / include the needed scripts.
20 |
21 | 2. Add a config file which contains a function to return a webpack config which is used to render:
22 |
23 | ```
24 | const webpack = require('webpack');
25 | const path = require('path');
26 | const buildDevWebpackConfig = require('./packages/core/dev/webpack/dev');
27 |
28 | module.exports = {
29 | generateWebpackConfig: function generateWebpackConfig(entryFiles, aliasObject) {
30 | const webpackConfig = buildDevWebpackConfig('test', {
31 | root: __dirname,
32 | app: 'x',
33 | }, {
34 | template: path.join(__dirname, './packages/dev-test-lib/screenshot/index.ejs'),
35 | }, webpack);
36 |
37 | webpackConfig.entry = { test: entryFiles };
38 | webpackConfig.resolve.alias = aliasObject;
39 |
40 | return webpackConfig;
41 | },
42 | port: 1111,
43 | };
44 | ```
45 |
46 | ## Usage
47 |
48 | Then use the specified render in your tests:
49 |
50 | ```ecmascript 6
51 | import React from 'react';
52 | import { render } from 'jest-puppeteer-react';
53 | import Button from '../Button';
54 |
55 | describe('Button', () => {
56 | test('should render a button', async () => {
57 | await render(
58 | ,
59 | { viewport: { width: 100, height: 100 } }
60 | );
61 |
62 | const screenshot = await page.screenshot();
63 | expect(screenshot).toMatchImageSnapshot();
64 | });
65 | });
66 | ```
67 |
68 | ## Options
69 |
70 | The second argument of render takes some options to make things easier. You can also supply a default for this via the config.
71 |
72 | ```
73 | {
74 | timeout: 60000, // 60 seconds
75 | viewport: {
76 | width: 100,
77 | height: 100,
78 | deviceScaleFactor: 2 // Retina Resolution
79 | }
80 | }
81 | ```
82 |
83 | ### Viewport
84 |
85 | Automatically calls `page.setViewport()` for you.
86 | See [puppeteer](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagesetviewportviewport) docs for options.
87 |
88 | ## Configuration
89 |
90 | You can put a `jest-puppeteer-react.config.js` file in your project root which gets automatically detected by jest-puppeteer-react.
91 |
92 | Example:
93 |
94 | ```
95 | const webpack = require('webpack');
96 | const path = require('path');
97 | const buildDevWebpackConfig = require('./packages/core/dev/webpack/dev');
98 |
99 | module.exports = {
100 | generateWebpackConfig: function generateWebpackConfig(entryFiles, aliasObject) {
101 | const webpackConfig = buildDevWebpackConfig('test', {
102 | root: __dirname,
103 | app: 'x',
104 | }, {
105 | template: path.join(__dirname, './packages/dev-test-lib/screenshot/index.ejs'),
106 | }, webpack);
107 |
108 | webpackConfig.entry = { test: entryFiles };
109 | webpackConfig.resolve.alias = aliasObject;
110 |
111 | return webpackConfig;
112 | },
113 | port: 1111,
114 | renderOptions: {
115 | viewport: { deviceScaleFactor: 2 },
116 | dumpConsole: false, // set to true to dump console.* from puppeteer
117 |
118 | // function calls before page.goto()
119 | before: async (page) => {
120 | // for example, disable cache
121 | await page.setCacheEnabled(false);
122 | },
123 |
124 | // function calls after page.goto()
125 | after: (page) => {},
126 | },
127 | };
128 | ```
129 |
130 | ### Configure Puppeteer
131 |
132 | You can put a `jest-puppeteer.config.js` file in your root to configure puppeteer. This is a feature of the jest-puppeteer lib. See their readme for documentation: [jest-puppeteer](https://github.com/smooth-code/jest-puppeteer/tree/6cd3050e472c9a8bcdb18e2635a40ad674c4b795#configure-puppeteer).
133 |
134 | ### Configure ESLint
135 |
136 | If you want to use the page object directly (without using the return value of render), you can set it as a global for eslint. See [jest-puppeteer](https://github.com/smooth-code/jest-puppeteer/tree/6cd3050e472c9a8bcdb18e2635a40ad674c4b795#configure-eslint) for an example.
137 |
138 | ## Limitations
139 |
140 | To be able to render the components in the browser, the test cases are required via webpack and the structure functions such as describe and test are evaluated. However, special behavior implemented in jest may be missing. For example mocks and timers are not supported currently.
141 | Furthermore at the moment hooks are not supported aswell. But they could be implemented quite easily.
142 |
--------------------------------------------------------------------------------
/src/docker/index.js:
--------------------------------------------------------------------------------
1 | const { dockerCommand } = require('docker-cli-js');
2 | const debug = require('debug')('jest-puppeteer-react');
3 | const { exec } = require('child_process');
4 | const http = require('http');
5 |
6 | const options = {
7 | machineName: null, // use local docker
8 | currentWorkingDirectory: __dirname, // use current working directory
9 | echo: false, // echo command output to stdout/stderr
10 | env: process.env, // use process.env variables
11 | };
12 |
13 | const DEFAULT_DOCKER_IMAGE_NAME = 'ghcr.io/puppeteer/puppeteer:latest';
14 |
15 | async function getAvailableBrowserURL(containerId) {
16 | const inspectResponse = await dockerCommand(
17 | `inspect --format=\\""{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}"\\" ${containerId}`,
18 | options
19 | );
20 | const containerIp = inspectResponse.object;
21 |
22 | debug(`Found container IP: ${containerIp}`);
23 |
24 | let availableUrl;
25 | const urlsToCheck = ['http://localhost:9222', `http://${containerIp}:9222`];
26 | for (let i = 0; i < urlsToCheck.length; i++) {
27 | const urlToCheck = urlsToCheck[i];
28 | try {
29 | await checkUrlAvailability(urlToCheck);
30 | debug(`url available: ${urlToCheck}`);
31 | availableUrl = urlToCheck;
32 | break;
33 | } catch (e) {
34 | debug(`url unavailable: ${urlToCheck}`);
35 | }
36 | }
37 |
38 | if (availableUrl) {
39 | return availableUrl;
40 | }
41 |
42 | throw new Error('could not find available browserURL');
43 | }
44 |
45 | async function checkUrlAvailability(url) {
46 | return new Promise((resolve, reject) => {
47 | const request = http.get(url, () => {
48 | request.end();
49 | resolve();
50 | });
51 | request.setTimeout(500);
52 | request.on('error', (error) => {
53 | request.destroy(error);
54 | reject(error);
55 | });
56 | request.on('timeout', (error) => {
57 | request.destroy(error);
58 | reject(error);
59 | });
60 | });
61 | }
62 |
63 | async function getRunningContainerIds(dockerImageName) {
64 | const { containerList } = await dockerCommand('ps', options);
65 | debug('getRunningContainerIds', { containerList });
66 | return containerList.filter(({ image }) => image === dockerImageName).map((container) => container['container id']);
67 | }
68 |
69 | /**
70 | * @returns {Promise} resolves to the browserURL of the started chrome instance
71 | */
72 | async function start(config) {
73 | const dockerImageName = config.dockerImageName || DEFAULT_DOCKER_IMAGE_NAME;
74 | const customEntryPoint = config.dockerEntrypoint ? `--entrypoint=${config.dockerEntrypoint}` : '';
75 | const customRunOptions = config.dockerRunOptions || '';
76 | const customCommand = config.dockerCommand || '';
77 |
78 | const containerIds = await getRunningContainerIds(dockerImageName);
79 | let containerId = null;
80 |
81 | if (containerIds.length > 0) {
82 | debug('docker container is already running');
83 | containerId = containerIds[0];
84 | } else {
85 | const runCommand = `run -p 9222:9222 ${customEntryPoint} -d ${customRunOptions} ${dockerImageName} ${customCommand}`;
86 | debug(`executing: docker ${runCommand}`);
87 | const data2 = await dockerCommand(runCommand, options);
88 | debug('docker run result:', data2);
89 | containerId = data2.containerId;
90 | }
91 |
92 | // wait a moment for Chrome to start
93 | await new Promise((resolve) => setTimeout(resolve, 500));
94 |
95 | let retriesLeft = 10;
96 | let browserURL = null;
97 | while (!browserURL) {
98 | try {
99 | // when running container with chrome inside another container we haven't access to 0.0.0.0:9222
100 | // so we have to try to access :9222 to find valid one
101 | browserURL = await getAvailableBrowserURL(containerId);
102 | } catch (e) {
103 | if (retriesLeft > 0) {
104 | retriesLeft--;
105 | debug('waiting 5 seconds for Chrome');
106 | await new Promise((resolve) => setTimeout(resolve, 5000));
107 | } else {
108 | throw e;
109 | }
110 | }
111 | }
112 |
113 | debug(`Found browserURL: ${browserURL}`);
114 |
115 | return browserURL;
116 | }
117 |
118 | async function stop(config) {
119 | debug('stopping any running docker containers');
120 | // check if running
121 | const dockerImageName = config.dockerImageName || DEFAULT_DOCKER_IMAGE_NAME;
122 | const ours = await getRunningContainerIds(dockerImageName);
123 |
124 | if (ours.length > 0) {
125 | console.log(`Stopping ${ours.length} Docker container(s)`);
126 | for (let i = 0; i < ours.length; i++) {
127 | const containerId = ours[i];
128 |
129 | const result = await dockerCommand(`stop ${containerId}`, options);
130 | debug('stopped container with id ' + containerId + ' result:', result);
131 | }
132 | } else {
133 | debug('no containers to stop');
134 | }
135 | }
136 |
137 | module.exports.start = start;
138 | module.exports.stop = stop;
139 |
--------------------------------------------------------------------------------
/src/global.js:
--------------------------------------------------------------------------------
1 | const setupPuppeteer = require('jest-environment-puppeteer/setup');
2 | const teardownPuppeteer = require('jest-environment-puppeteer/teardown');
3 | const ora = require('ora');
4 | const debug = require('debug')('jest-puppeteer-react');
5 | const webpack = require('webpack');
6 | const WebpackDevServer = require('webpack-dev-server');
7 | const { promisify } = require('util');
8 | const path = require('path');
9 | const fs = require('fs');
10 | const os = require('os');
11 | const { glob } = require('glob');
12 | const docker = require('./docker');
13 |
14 | const DIR = path.join(os.tmpdir(), 'jest_puppeteer_react_global_setup');
15 |
16 | let webpackDevServer;
17 |
18 | const getConfig = async () => {
19 | const configName = 'jest-puppeteer-react.config';
20 | const statPromisified = promisify(fs.stat);
21 |
22 | let configExt = '.cjs';
23 |
24 | try {
25 | await statPromisified(path.join(process.cwd(), `${configName}${configExt}`));
26 | } catch (e) {
27 | // Fallback extension if CommonJS module not exist
28 | configExt = '.js';
29 | }
30 |
31 | return require(path.join(process.cwd(), `${configName}${configExt}`));
32 | };
33 |
34 | module.exports.setup = async function setup(jestConfig) {
35 | const { noInfo = true, rootDir, testPathPattern, debugOnly = false } = jestConfig;
36 |
37 | // build only files matching testPathPattern
38 | const testPathPatterRe = new RegExp(testPathPattern, 'i');
39 | const testFiles = (await glob(`${rootDir}/**/*.browser.@(js|jsx|ts|tsx)`, { dotRelative: true, posix: true })).filter((file) => {
40 | if (file.includes('node_modules')) {
41 | return false;
42 | }
43 | return testPathPatterRe.test(fs.realpathSync(file));
44 | });
45 |
46 | const config = await getConfig();
47 |
48 | const entryFiles = [
49 | path.resolve(__dirname, 'webpack/globals.browser.js'),
50 | ...testFiles,
51 | path.resolve(__dirname, 'webpack/entry.browser.js'),
52 | ];
53 | const aliasObject = {
54 | 'jest-puppeteer-react': path.resolve(__dirname, 'webpack/render.browser.js'),
55 | };
56 |
57 | const webpackConfig = config.generateWebpackConfig(entryFiles, aliasObject);
58 |
59 | const spinner = ora({ color: 'yellow', stream: process.stdout });
60 |
61 | const compiler = webpack({
62 | infrastructureLogging: {
63 | level: 'warn',
64 | },
65 | ...webpackConfig,
66 | });
67 |
68 | const compilerHooks = new Promise((resolve, reject) => {
69 | compiler.hooks.watchRun.tapAsync('jest-puppeeter-react', (_, callback) => {
70 | spinner.start('Waiting for webpack build to succeed...');
71 | callback();
72 | });
73 | compiler.hooks.done.tapAsync('jest-puppeeter-react', (stats, callback) => {
74 | if (stats.hasErrors()) {
75 | spinner.fail('Webpack build failed');
76 | reject(stats);
77 | } else {
78 | spinner.succeed('Webpack build finished');
79 | resolve(stats);
80 | }
81 | callback();
82 | });
83 | });
84 |
85 | const port = config.port || 1111;
86 | debug('starting webpack-dev-server on port ' + port);
87 | webpackDevServer = new WebpackDevServer(
88 | {
89 | allowedHosts: 'all',
90 | devMiddleware: {
91 | stats: 'minimal',
92 | },
93 | port,
94 | ...(webpackConfig.devServer || {}),
95 | },
96 | compiler
97 | );
98 |
99 | try {
100 | await webpackDevServer.start();
101 | await compilerHooks;
102 | } catch (e) {
103 | console.error(e);
104 | return;
105 | }
106 |
107 | if (config.useDocker && !debugOnly) {
108 | try {
109 | spinner.start('Starting Docker for screenshots...');
110 | debug('calling docker.start()');
111 | const browserURL = await docker.start(config);
112 | debug(`browserURL is ${browserURL}`);
113 | process.env.JEST_PUPPETEER_CONFIG = path.join(DIR, 'config.json');
114 | fs.mkdirSync(DIR, { recursive: true });
115 | fs.writeFileSync(
116 | process.env.JEST_PUPPETEER_CONFIG,
117 | JSON.stringify({
118 | connect: {
119 | browserURL: browserURL,
120 | },
121 | })
122 | );
123 | spinner.succeed('Docker started');
124 | } catch (e) {
125 | console.error(e);
126 | throw new Error('Failed to start docker for screenshots');
127 | }
128 | }
129 |
130 | debug('setup jest-puppeteer');
131 | await setupPuppeteer(jestConfig);
132 | };
133 |
134 | module.exports.teardown = async function teardown(jestConfig) {
135 | debug('stopping webpack-dev-server');
136 | try {
137 | await webpackDevServer.stop();
138 | } catch (e) {
139 | console.error(e);
140 | }
141 |
142 | const config = getConfig();
143 | try {
144 | if (config.useDocker) {
145 | debug('stopping docker');
146 | await docker.stop(config);
147 | }
148 | } catch (e) {
149 | console.error(e);
150 | }
151 |
152 | debug('teardown jest-puppeteer');
153 | await teardownPuppeteer(jestConfig);
154 | };
155 |
--------------------------------------------------------------------------------
/src/webpack/entry.browser.js:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client';
2 |
3 | const search = window.location.search;
4 | const urlParams = new URLSearchParams(window.location.search);
5 | let currentTest = urlParams.get('testPreview') || urlParams.get('test');
6 |
7 | if (urlParams.has('test')) {
8 | const testName = decodeURIComponent(search.match(/\?test=([^&]+)/i)[1]);
9 |
10 | const component =
11 | (window.__tests[testName] || {}).reactNode || React.createElement('div', null, `no component found for test "${testName}"`);
12 |
13 | const container = document.getElementById('main');
14 | const root = createRoot(container);
15 | root.render(component);
16 | } else {
17 | const wrapper = document.getElementById('main') || document.createElement('div');
18 | wrapper.style.setProperty('display', 'grid');
19 | wrapper.style.setProperty('grid-template-columns', '300px auto');
20 | wrapper.style.setProperty('grid-gap', '.16em');
21 | wrapper.style.setProperty('height', '100vh');
22 | wrapper.style.setProperty('background-color', '#808080');
23 |
24 | const containerWrapper = document.createElement('div');
25 | // frame to render the test preview
26 | const container = document.createElement('iframe');
27 | container.style.setProperty('border', 'none');
28 | container.style.setProperty('background-color', document.body.style.backgroundColor || 'white');
29 |
30 | containerWrapper.appendChild(container);
31 |
32 | const applyTest = ({ path, reactNode, viewport }, position = 0, result = {}) => {
33 | const pathEntry = path[position];
34 |
35 | if (!result[pathEntry] && position < path.length) {
36 | result[pathEntry] = {};
37 | }
38 |
39 | if (position < path.length) {
40 | result[pathEntry] = Object.assign(
41 | {},
42 | result[pathEntry],
43 | applyTest({ path, reactNode, viewport }, position + 1, result[pathEntry])
44 | );
45 |
46 | return result;
47 | }
48 |
49 | return {
50 | __reactNode: reactNode,
51 | viewport,
52 | description: path.join(' '),
53 | };
54 | };
55 |
56 | const updateContainer = (reactNode, viewport, title) => {
57 | container.style.setProperty('height', viewport.height + 'px');
58 | container.style.setProperty('width', viewport.width + 'px');
59 | container.src = [location.origin, '?test=', title].join('');
60 | document.title = 'Preview: ' + title;
61 | };
62 |
63 | const detailsBlockEntries = (element, values) => {
64 | Object.entries(values)
65 | .sort(([a], [b]) => a.localeCompare(b))
66 | .map(createDetailsBlock)
67 | .forEach((entry) => element.appendChild(entry));
68 | };
69 |
70 | const createDetailsBlock = ([key, values]) => {
71 | let details;
72 | if (!values.__reactNode) {
73 | details = document.createElement('details');
74 | details.style.setProperty('padding-left', '1em');
75 | details.style.setProperty('margin', '.32em');
76 | details.open = true;
77 | const summary = document.createElement('summary');
78 | summary.style.setProperty('margin-left', '-1em');
79 | summary.textContent = key;
80 | details.appendChild(summary);
81 | detailsBlockEntries(details, values);
82 | } else {
83 | const description = values.description;
84 | const a = document.createElement('a');
85 | a.style.setProperty('display', 'block');
86 | a.style.setProperty('padding', '.1em');
87 |
88 | const url = `${document.location.protocol}//${document.location.hostname}:${
89 | document.location.port
90 | }?testPreview=${encodeURIComponent(description)}`;
91 |
92 | a.href = url;
93 |
94 | a.onclick = (e) => {
95 | e.stopPropagation();
96 | window.history.pushState({}, description, url);
97 | updateContainer(values.__reactNode, values.viewport, description);
98 | return false;
99 | };
100 |
101 | a.onmouseenter = (e) => {
102 | e.target.style.setProperty('background-color', '#ddd');
103 | };
104 | a.onmouseleave = (e) => {
105 | e.target.style.removeProperty('background-color');
106 | };
107 | a.text = key;
108 |
109 | if (description === currentTest) {
110 | updateContainer(values.__reactNode, values.viewport, description);
111 | }
112 | details = a;
113 | }
114 | return details;
115 | };
116 |
117 | // build an object to easily create a hierarchical structure
118 | const tests = Object.entries(__tests)
119 | .map(([, t]) => t)
120 | .reduce((acc, { path, reactNode, viewport }, i, tests) => {
121 | return applyTest({ path, reactNode, viewport }, 0, acc);
122 | }, {});
123 |
124 | const detailsBlock = document.createElement('div');
125 | detailsBlock.style.setProperty('overflow', 'auto');
126 | detailsBlock.style.setProperty('position', 'relative');
127 | detailsBlock.style.setProperty('max-height', '100%');
128 | detailsBlock.style.setProperty('background-color', 'white');
129 | detailsBlockEntries(detailsBlock, tests);
130 |
131 | wrapper.appendChild(detailsBlock);
132 | wrapper.appendChild(containerWrapper);
133 | document.body.appendChild(wrapper);
134 | }
135 |
--------------------------------------------------------------------------------