├── .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( 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 [![Build Status](https://travis-ci.com/Hapag-Lloyd/jest-puppeteer-react.svg?branch=master)](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 | --------------------------------------------------------------------------------