├── .gitignore ├── packages ├── jest-electron-runner │ ├── runner.html │ ├── electron-runner.js │ ├── package.json │ ├── electron-transport.js │ └── index.js └── jest-environment-electron │ ├── index.js │ └── package.json ├── .flowconfig ├── lerna.json ├── assets ├── jest-electron-full.gif └── jest-electron-full.mp4 ├── .editorconfig ├── example ├── __tests__ │ ├── __snapshots__ │ │ ├── index.test.js.snap │ │ └── index.test.ts.snap │ ├── index.test.ts │ └── index.test.js ├── index.js ├── package.json └── tsconfig.json ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | -------------------------------------------------------------------------------- /packages/jest-electron-runner/runner.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [lints] 8 | 9 | [options] 10 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.0.0", 3 | "packages": ["packages/*"], 4 | "version": "independent" 5 | } 6 | -------------------------------------------------------------------------------- /assets/jest-electron-full.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d4rkr00t/jest-electron-runner/HEAD/assets/jest-electron-full.gif -------------------------------------------------------------------------------- /assets/jest-electron-full.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d4rkr00t/jest-electron-runner/HEAD/assets/jest-electron-full.mp4 -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /example/__tests__/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Some test #add test with snapshot 1`] = ` 4 | Object { 5 | "a": 3, 6 | "b": 7, 7 | } 8 | `; 9 | -------------------------------------------------------------------------------- /example/__tests__/__snapshots__/index.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Some test #add test with snapshot 1`] = ` 4 | Object { 5 | "a": 3, 6 | "b": 7, 7 | } 8 | `; 9 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | exports.add = function add(a, b) { 2 | return a + b; 3 | }; 4 | 5 | exports.useDOMApi = function useDOMApi() { 6 | const range = document.createRange(); 7 | range.setStart(document.body, 0); 8 | range.setEnd(document.body, 0); 9 | }; 10 | 11 | exports.printMe = function printMe(arg) { 12 | console.info(arg); 13 | return arg; 14 | }; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest-electron-runner", 3 | "private": true, 4 | "version": "0.0.0", 5 | "devDependencies": { 6 | "lerna": "^2.1.2" 7 | }, 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/d4rkr00t/jest-electron-runner.git" 14 | }, 15 | "keywords": [], 16 | "author": "Stanislav Sysoev <@d4rkr00t>", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/d4rkr00t/jest-electron-runner/issues" 20 | }, 21 | "homepage": "https://github.com/d4rkr00t/jest-electron-runner#readme" 22 | } 23 | -------------------------------------------------------------------------------- /packages/jest-environment-electron/index.js: -------------------------------------------------------------------------------- 1 | const { FakeTimers, installCommonGlobals } = require("jest-util"); 2 | const mock = require("jest-mock"); 3 | 4 | class ElectronEnvironment { 5 | constructor(config) { 6 | const global = (this.global = window); 7 | installCommonGlobals(global, config.globals); 8 | this.moduleMocker = new mock.ModuleMocker(global); 9 | this.fakeTimers = new FakeTimers(global, this.moduleMocker, config); 10 | } 11 | 12 | dispose() { 13 | if (this.fakeTimers) { 14 | this.fakeTimers.dispose(); 15 | } 16 | this.fakeTimers = null; 17 | } 18 | 19 | runScript(script) { 20 | return script.runInThisContext(); 21 | } 22 | } 23 | 24 | module.exports = ElectronEnvironment; 25 | -------------------------------------------------------------------------------- /packages/jest-environment-electron/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest-environment-electron", 3 | "version": "0.0.1", 4 | "description": "Jest environment for Electron", 5 | "main": "index.js", 6 | "dependencies": { 7 | "jest-mock": "^21.0.2", 8 | "jest-util": "^21.0.2" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/d4rkr00t/jest-electron-runner.git" 13 | }, 14 | "keywords": [ 15 | "jest", 16 | "environment", 17 | "jest environment", 18 | "electron" 19 | ], 20 | "author": "Stanislav Sysoev <@d4rkr00t>", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/d4rkr00t/jest-electron-runner/issues" 24 | }, 25 | "homepage": "https://github.com/d4rkr00t/jest-electron-runner#readme" 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jest-electron-runner 2 | 3 | Jest test runner that executes tests in Electron's BrowserWindow environment, which gives you access to all browser APIs 4 | available there. 5 | 6 | > EXPERIMENTAL. NOT READY FOR ANY SERIOUS USE !!!! 7 | 8 | ![jest electron runner](/assets/jest-electron-full.gif) 9 | 10 | ## How to use 11 | 12 | Install required packages: 13 | 14 | ```sh 15 | npm install jest jest-electron-runner jest-environment-electron electron 16 | ``` 17 | 18 | Configure jest: 19 | ```js 20 | "jest": { 21 | "runner": "jest-electron-runner", 22 | // Environment is important, otherwise jest will use jsdom by default. 23 | "testEnvironment": "jest-environment-electron" 24 | } 25 | ``` 26 | 27 | ## TODO: 28 | - [ ] Coverage 29 | - [ ] Investigate performance with a lot of test suits 30 | -------------------------------------------------------------------------------- /example/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { add, useDOMApi, printMe } from "../index"; 2 | 3 | describe("Some test", () => { 4 | describe("#add", () => { 5 | test("should add 1 + 2", () => { 6 | expect(add(1, 2)).toBe(3); 7 | }); 8 | 9 | test("should add 1 + 1", () => { 10 | expect(add(1, 1)).toBe(3); 11 | }); 12 | 13 | test("test with snapshot", () => { 14 | expect({ a: add(1, 2), b: add(3, 4) }).toMatchSnapshot(); 15 | }); 16 | }); 17 | 18 | describe("#useDOMApi", () => { 19 | test("should work", () => { 20 | expect(() => useDOMApi()).not.toThrow(); 21 | }); 22 | }); 23 | 24 | describe("#printMe", () => { 25 | test("shouldn't break transport", () => { 26 | const msg = "BREAK TRANSPORT!"; 27 | expect(printMe(msg)).toBe(msg); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /example/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | const { add, useDOMApi, printMe } = require("../index"); 2 | 3 | describe("Some test", () => { 4 | describe("#add", () => { 5 | test("should add 1 + 2", () => { 6 | expect(add(1, 2)).toBe(3); 7 | }); 8 | 9 | test("should add 1 + 1", () => { 10 | expect(add(1, 1)).toBe(3); 11 | }); 12 | 13 | test("test with snapshot", () => { 14 | expect({ a: add(1, 2), b: add(3, 4) }).toMatchSnapshot(); 15 | }); 16 | }); 17 | 18 | describe("#useDOMApi", () => { 19 | test("should work", () => { 20 | expect(() => useDOMApi()).not.toThrow(); 21 | }); 22 | }); 23 | 24 | describe("#printMe", () => { 25 | test("shouldn't break transport", () => { 26 | const msg = "BREAK TRANSPORT!"; 27 | expect(printMe(msg)).toBe(msg); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/jest-electron-runner/electron-runner.js: -------------------------------------------------------------------------------- 1 | const os = require("os"); 2 | const { ipcRenderer, remote } = require("electron"); 3 | const Runtime = require("jest-runtime"); 4 | const runTest = require("jest-runner/build/run_test"); 5 | 6 | ipcRenderer.on("run", (evt, { file, config, globalConfig }) => { 7 | Runtime.createHasteMap(config, { 8 | maxWorkers: os.cpus().length - 1, 9 | watchman: globalConfig.watchman 10 | }) 11 | .build() 12 | .then(hasteMap => { 13 | const resolver = Runtime.createResolver(config, hasteMap.moduleMap); 14 | runTest(file, globalConfig, config, resolver) 15 | .then(results => { 16 | ipcRenderer.send("test-results", results); 17 | window.close(); 18 | }) 19 | .catch(e => { 20 | ipcRenderer.send( 21 | "error", 22 | e instanceof Error ? e.message : e.toString() 23 | ); 24 | window.close(); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/jest-electron-runner/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest-electron-runner", 3 | "version": "0.0.2", 4 | "description": "Jest TestRunner to run tests in Electron environment", 5 | "main": "index.js", 6 | "dependencies": { 7 | "jest-runner": "^21.0.2", 8 | "minimist": "^1.2.0", 9 | "projector-spawn": "^1.0.1", 10 | "supports-color": "^4.4.0", 11 | "throat": "^4.1.0" 12 | }, 13 | "peerDependencies": { 14 | "electron": ">= 1.7.5", 15 | "jest-environment-electron": "*" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/d4rkr00t/jest-electron-runner.git" 20 | }, 21 | "keywords": [ 22 | "jest", 23 | "test runner", 24 | "electron" 25 | ], 26 | "author": "Stanislav Sysoev <@d4rkr00t>", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/d4rkr00t/jest-electron-runner/issues" 30 | }, 31 | "homepage": "https://github.com/d4rkr00t/jest-electron-runner#readme" 32 | } 33 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "private": true, 4 | "version": "0.0.0", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest" 8 | }, 9 | "devDependencies": { 10 | "@types/jest": "^20.0.8", 11 | "electron": "^1.7.6", 12 | "jest": "^21.0.1", 13 | "ts-jest": "^21.0.0", 14 | "typescript": "^2.5.2" 15 | }, 16 | "keywords": [], 17 | "author": "Stanislav Sysoev <@d4rkr00t>", 18 | "license": "MIT", 19 | "jest": { 20 | "runner": "/../packages/jest-electron-runner/index.js", 21 | "transform": { 22 | "^.+\\.tsx?$": "/node_modules/ts-jest/preprocessor.js" 23 | }, 24 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", 25 | "moduleFileExtensions": [ 26 | "ts", 27 | "tsx", 28 | "js", 29 | "jsx", 30 | "json" 31 | ], 32 | "collectCoverageFrom": [ 33 | "./example/*.js" 34 | ], 35 | "testEnvironment": "./../packages/jest-environment-electron/index.js" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/jest-electron-runner/electron-transport.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow, ipcMain } = require("electron"); 2 | const minimist = require("minimist"); 3 | 4 | const args = minimist(process.argv.slice(2)); 5 | 6 | function run() { 7 | let runner = new BrowserWindow({ 8 | title: "Jest", 9 | show: false, 10 | contextIsolation: true 11 | }); 12 | 13 | ipcMain.on("test-results", (evt, result) => { 14 | try { 15 | process.send({ type: "result", data: result }); 16 | } catch (e) {} 17 | }); 18 | 19 | ipcMain.on("error", (evt, error) => { 20 | try { 21 | process.send({ type: "error", data: error }); 22 | } catch (e) {} 23 | }); 24 | 25 | runner.loadURL(`file://${__dirname}/runner.html`); 26 | 27 | process.on("message", msg => { 28 | runner.webContents.on("did-finish-load", () => { 29 | runner.webContents.send("run", { 30 | file: msg.file, 31 | globalConfig: msg.globalConfig, 32 | config: msg.config 33 | }); 34 | }); 35 | }); 36 | 37 | process.send({ type: "ready" }); 38 | } 39 | 40 | app.on("ready", run); 41 | 42 | app.on("window-all-closed", function() { 43 | app.quit(); 44 | }); 45 | 46 | if (process.platform === "darwin") { 47 | app.dock.hide(); 48 | } 49 | -------------------------------------------------------------------------------- /packages/jest-electron-runner/index.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const spawn = require("child_process").spawn; 3 | const electron = require("electron"); 4 | const throat = require("throat"); 5 | const supportsColor = require("supports-color"); 6 | 7 | class ElectronTestRunner { 8 | constructor(globalConfig) { 9 | this._globalConfig = globalConfig; 10 | } 11 | 12 | runTests(tests, watcher, onStart, onResult, onFailure, options) { 13 | const mutex = throat(this._globalConfig.maxWorkers); 14 | return Promise.all( 15 | tests.map(test => 16 | mutex( 17 | () => 18 | new Promise((resolve, reject) => { 19 | if (watcher.isInterrupted()) { 20 | throw new CancelRun(); 21 | } 22 | 23 | const env = Object.assign({}, process.env, { 24 | FORCE_COLOR: supportsColor.level 25 | }); 26 | 27 | onStart(test) 28 | .then(() => 29 | spawn( 30 | electron, 31 | [path.join(__dirname, "electron-transport.js")], 32 | { stdio: ["ipc"], env } 33 | ) 34 | ) 35 | .then(electron => { 36 | process.on("exit", code => { 37 | electron.kill(); 38 | }); 39 | 40 | electron.on("message", message => { 41 | if (message.type === "ready") { 42 | return electron.send({ 43 | file: test.path, 44 | globalConfig: this._globalConfig, 45 | config: test.context.config 46 | }); 47 | } 48 | 49 | if (message.type === "error") { 50 | return reject(new Error(message.data)); 51 | } 52 | 53 | resolve(message.data); 54 | }); 55 | }) 56 | .catch(e => { 57 | reject(e); 58 | }); 59 | }) 60 | ) 61 | .then(result => onResult(test, result)) 62 | .catch(e => onFailure(test, e)) 63 | ) 64 | ); 65 | } 66 | } 67 | 68 | class CancelRun extends Error {} 69 | 70 | module.exports = ElectronTestRunner; 71 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": 5 | "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */, 6 | "module": 7 | "commonjs" /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 8 | // "lib": [], /* Specify library files to be included in the compilation: */ 9 | "allowJs": true /* Allow javascript files to be compiled. */, 10 | // "checkJs": true, /* Report errors in .js files. */ 11 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 12 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | // "outDir": "./", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "removeComments": true, /* Do not emit comments to output. */ 18 | // "noEmit": true, /* Do not emit outputs. */ 19 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 21 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 22 | 23 | /* Strict Type-Checking Options */ 24 | "strict": true /* Enable all strict type-checking options. */ 25 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 26 | // "strictNullChecks": true, /* Enable strict null checks. */ 27 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 28 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 29 | 30 | /* Additional Checks */ 31 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 32 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 33 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 34 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 35 | 36 | /* Module Resolution Options */ 37 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 38 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 39 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 40 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 41 | // "typeRoots": [], /* List of folders to include type definitions from. */ 42 | // "types": [], /* Type declaration files to be included in compilation. */ 43 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 44 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 45 | 46 | /* Source Map Options */ 47 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 48 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 49 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 50 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 51 | 52 | /* Experimental Options */ 53 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 54 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 55 | } 56 | } 57 | --------------------------------------------------------------------------------