├── .appveyor.yml ├── .editorconfig ├── .gitignore ├── .travis.yml ├── README.md ├── index.d.ts ├── index.js ├── lib ├── flags.js ├── runner.js └── util.js ├── package.json └── test ├── runner.test.js └── util.test.js /.appveyor.yml: -------------------------------------------------------------------------------- 1 | # Test against the latest version of this Node.js version 2 | environment: 3 | nodejs_version: "7" 4 | 5 | platform: 6 | - x86 7 | - x64 8 | 9 | # Install scripts. (runs after repo cloning) 10 | install: 11 | # Get the latest stable version of Node.js or io.js 12 | - ps: Install-Product node $env:nodejs_version 13 | # install modules 14 | - npm install 15 | 16 | # Post-install test scripts. 17 | test_script: 18 | # Output useful info for debugging. 19 | - node --version 20 | - npm --version 21 | # run tests 22 | - npm test 23 | 24 | # Don't actually build. 25 | build: off 26 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | indent_style = space 5 | indent_size = 2 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .idea/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | - osx 4 | - windows 5 | dist: trusty 6 | sudo: false 7 | language: node_js 8 | node_js: 9 | - "7" 10 | addons: 11 | chrome: stable 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Npm Package](https://img.shields.io/npm/v/chrome-runner.svg?style=flat-square)](https://www.npmjs.com/package/chrome-runner) 2 | [![Build Status](https://img.shields.io/travis/gwuhaolin/chrome-runner.svg?style=flat-square)](https://travis-ci.org/gwuhaolin/chrome-runner) 3 | [![Build Status](https://img.shields.io/appveyor/ci/gwuhaolin/chrome-runner.svg?style=flat-square)](https://ci.appveyor.com/project/gwuhaolin/chrome-runner) 4 | [![Dependency Status](https://david-dm.org/gwuhaolin/chrome-runner.svg?style=flat-square)](https://npmjs.org/package/chrome-runner) 5 | [![Npm Downloads](http://img.shields.io/npm/dm/chrome-runner.svg?style=flat-square)](https://www.npmjs.com/package/chrome-runner) 6 | 7 | # chrome-runner 8 | Run chrome with ease from node. 9 | 10 | - Support OSX Linux Windows system 11 | - Handle chrome unexpected exit and restart it 12 | - Opens up the browser's `remote-debugging-port` on an available port 13 | - Automatic locates a Chrome binary to launch 14 | - Uses a fresh Chrome profile for each launch, and cleans itself up on `kill()` 15 | - Support typescript 16 | 17 | ## Use 18 | ```js 19 | const {Runner,launch,launchWithoutNoise,launchWithHeadless} = require('chrome-runner'); 20 | // launch a chrome, launch return a Runner instance 21 | const runner = await launch(); 22 | // read chrome remote debugging port 23 | runner.port; 24 | // kill this chrome 25 | await runner.kill(); 26 | ``` 27 | 28 | ### Options 29 | `launch()` method can pass options by `launch({name:value})`. Include: 30 | - `port`: {number} launch chrome listen on debug port, default will random a free port to use 31 | - `chromePath`: {string} chrome executable full path, default will automatic find a path according to your system. If no executable chrome find, will use env CHROME_PATH as executable full path. If all of the above way can't get a path a Error('no chrome installations found') will throw 32 | - `chromeFlags`: {Array} flags pass to chrome when start chrome, all flags can be find [here](http://peter.sh/experiments/chromium-command-line-switches/) 33 | - `startupPage`: {string} open page when chrome start, default is `about:blank` 34 | - `shouldRestartChrome`: {boole} logger to handle log from chrome-runner, interface like console, default use console 35 | - `monitorInterval`: {number} in ms, monitor chrome is alive interval, default is 500ms 36 | - `chromeDataDir`: {string} chrome data dir, default will create one in system tmp 37 | - `disableLogging`: {boolean} Controls if Chome stdout and stderr is logged to file, default is `true`. 38 | 39 | ### Runner API 40 | - `runner.port`: get chrome remove debug port 41 | - `runner.kill()`: kill chrome and release all resource and remove temp files 42 | 43 | #### Events 44 | Runner extends EventEmitter, it will emit some events in it's lifecycle, Include: 45 | - `chromeAlive(port)`: when monitor detect chrome is alive 46 | - `chromeDead(code, signal)`: after monitor detect chrome is not alive 47 | - `chromeRestarted()`: after chrome unexpected exited then runner restart it 48 | - `chromeDataDirPrepared(chromeDataDir)`: after runner create data dir for chrome 49 | - `chromeDataDirRemoved(chromeDataDir)`: after remove successful create data dir for chrome 50 | 51 | ### launchWithoutNoise 52 | `launchWithoutNoise` same with `launch` but [disables many chrome services](https://github.com/gwuhaolin/chrome-runner/blob/master/lib/flags.js) that add noise to automated scenarios. 53 | 54 | ### launchWithHeadless 55 | `launchWithHeadless` same with `launch` but [run chrome in headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome) and without noise. 56 | 57 | **more use case see [unit test](./test/runner.test.js), API detail see [d.ts](./index.d.ts)** 58 | 59 | ### Chrome log files 60 | After chrome launched, chrome's log and pid file will be pipe to file in `chromeDataDir`, Include: 61 | - `chrome-out.log` chrome info log 62 | - `chrome-err.log` chrome error log 63 | - `chrome.pid` chrome pid file 64 | 65 | ## Install chrome on linux server 66 | chrome-runner required chrome installed on your system, it easy to install on OSX and Windows, Linux server see [How to install Chrome browser properly via command line?](https://askubuntu.com/questions/79280/how-to-install-chrome-browser-properly-via-command-line) 67 | 68 | ## Use Case 69 | chrome-runner has been used in many project, e.g: 70 | - [chrome-render](https://github.com/gwuhaolin/chrome-render) general server render base on chrome 71 | - [chrome-pool](https://github.com/gwuhaolin/chrome-pool) headless chrome tabs manage pool 72 | - [koa-seo](https://github.com/gwuhaolin/koa-seo) SEO middleware for koa base on chrome-render, a substitute for prerender 73 | - [chrome-tester](https://github.com/gwuhaolin/chrome-tester) web page automatic tester 74 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from "events"; 2 | import {ChildProcess} from "child_process"; 3 | 4 | interface RunnerOptions { 5 | /** 6 | * launch chrome listen on debug port, default will random a free port to use 7 | */ 8 | port?: number, 9 | /** 10 | * chrome executable full path, default will automatic find a path according to your system. If no executable chrome find, a Error('no chrome installations found') will throw 11 | */ 12 | chromePath?: string, 13 | /** 14 | * flags pass to chrome when start chrome, all flags can be find [here](http://peter.sh/experiments/chromium-command-line-switches/) 15 | */ 16 | chromeFlags?: [string], 17 | /** 18 | * open page when chrome start, default is about:blank 19 | */ 20 | startupPage?: string, 21 | /** 22 | * logger to handle log from chrome-runner, interface like console, default use console 23 | */ 24 | shouldRestartChrome?: boolean, 25 | /** 26 | * in ms, monitor chrome is alive interval, default is 500ms 27 | */ 28 | monitorInterval?: number, 29 | /** 30 | * chrome data dir, default will create one in system tmp 31 | */ 32 | chromeDataDir?: string, 33 | } 34 | 35 | export class Runner extends EventEmitter { 36 | port: number; 37 | flags: [string]; 38 | chromeProcess: ChildProcess; 39 | chromeDataDir: string; 40 | chromeOutFd: number; 41 | chromeErrorFd: number; 42 | pidFd: number; 43 | 44 | launch(): Promise; 45 | 46 | kill(): Promise; 47 | } 48 | 49 | export function launch(opts: RunnerOptions): Promise; 50 | export function launchWithoutNoise(opts: RunnerOptions): Promise; 51 | export function launchWithHeadless(opts: RunnerOptions): Promise; 52 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { NOISE_FLAGS, HEADLESS_FLAGS } = require('./lib/flags'); 2 | const Runner = require('./lib/runner'); 3 | 4 | async function launch(runnerOptions = {}) { 5 | const runner = new Runner(runnerOptions); 6 | await runner.launch(); 7 | return runner; 8 | } 9 | 10 | async function launchWithoutNoise(runnerOptions = {}) { 11 | let chromeFlags = NOISE_FLAGS; 12 | if (Array.isArray(runnerOptions.chromeFlags)) { 13 | chromeFlags = chromeFlags.concat(runnerOptions.chromeFlags); 14 | } 15 | return launch(Object.assign(runnerOptions, { 16 | chromeFlags, 17 | })); 18 | } 19 | 20 | async function launchWithHeadless(runnerOptions = {}) { 21 | let chromeFlags = NOISE_FLAGS.concat(HEADLESS_FLAGS); 22 | if (Array.isArray(runnerOptions.chromeFlags)) { 23 | chromeFlags = chromeFlags.concat(runnerOptions.chromeFlags); 24 | } 25 | return launch(Object.assign(runnerOptions, { 26 | chromeFlags, 27 | })); 28 | } 29 | 30 | module.exports = { 31 | Runner, 32 | launch, 33 | launchWithoutNoise, 34 | launchWithHeadless 35 | }; 36 | -------------------------------------------------------------------------------- /lib/flags.js: -------------------------------------------------------------------------------- 1 | // See all flags here: http://peter.sh/experiments/chromium-command-line-switches/ 2 | module.exports = { 3 | DEFAULT_FLAGS: [ 4 | // Skip first run wizards 5 | '--no-first-run', 6 | ], 7 | NOISE_FLAGS: [ 8 | // Disable built-in Google Translate service 9 | '--disable-translate', 10 | // Disable all chrome extensions entirely 11 | '--disable-extensions', 12 | // Disable various background network services, including extension updating, 13 | // safe browsing service, upgrade detector, translate, UMA 14 | '--disable-background-networking', 15 | // Disable fetching safebrowsing lists, likely redundant due to disable-background-networking 16 | '--safebrowsing-disable-auto-update', 17 | // Disable syncing to a Google account 18 | '--disable-sync', 19 | // Disable reporting to UMA, but allows for collection 20 | '--metrics-recording-only', 21 | // Disable installation of default apps on first run 22 | '--disable-default-apps', 23 | ], 24 | HEADLESS_FLAGS: [ 25 | // https://developers.google.com/web/updates/2017/04/headless-chrome 26 | '--headless', 27 | '--disable-gpu' 28 | ], 29 | } 30 | -------------------------------------------------------------------------------- /lib/runner.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const fs = require('fs'); 3 | const EventEmitter = require('events'); 4 | const childProcess = require('child_process'); 5 | const rimraf = require('rimraf'); 6 | const findChrome = require('chrome-finder'); 7 | const onDeath = require('death'); 8 | const { DEFAULT_FLAGS } = require('./flags'); 9 | const { getRandomPort, makeTmpDir, isPortOpen } = require('./util'); 10 | 11 | class Runner extends EventEmitter { 12 | 13 | /** 14 | * a Runner to launch a chrome 15 | * @param opts 16 | * port: {number} launch chrome listen on debug port, default will random a free port to use 17 | * chromePath: {string} chrome executable full path, default will automatic find a path according to your system. If no executable chrome find, a Error('no chrome installations found') will throw 18 | * chromeFlags: {Array} flags pass to chrome when start chrome, all flags can be find [here](http://peter.sh/experiments/chromium-command-line-switches/) 19 | * startupPage: {string} open page when chrome start, default is about:blank 20 | * shouldRestartChrome: {boole} logger to handle log from chrome-runner, interface like console, default use console 21 | * monitorInterval: {number} in ms, monitor chrome is alive interval, default is 500ms 22 | * chromeDataDir: {string} chrome data dir, default will create one in system tmp 23 | * 24 | * a Runner will emit some events in it's lifecycle: 25 | * chromeAlive(port): when monitor detect chrome is alive 26 | * chromeDead(code, signal): after monitor detect chrome is not alive 27 | * chromeRestarted(): after chrome unexpected exited then runner restart it 28 | * chromeDataDirPrepared(chromeDataDir): after runner create data dir for chrome 29 | * chromeDataDirRemoved(chromeDataDir): after remove successful create data dir for chrome 30 | */ 31 | constructor(opts) { 32 | super(); 33 | const { 34 | port, 35 | chromePath = findChrome(), 36 | chromeFlags = [], 37 | startupPage = 'about:blank', 38 | shouldRestartChrome = true, 39 | monitorInterval = 500, 40 | chromeDataDir = makeTmpDir(), 41 | disableLogging = false, 42 | } = opts; 43 | this.port = port; 44 | this.chromePath = chromePath; 45 | this.chromeFlags = chromeFlags; 46 | this.startupPage = startupPage; 47 | this.shouldRestartChrome = shouldRestartChrome; 48 | this.monitorInterval = monitorInterval; 49 | this.chromeDataDir = chromeDataDir; 50 | this.disableLogging = disableLogging; 51 | 52 | this.chromeProcess = undefined; 53 | this.chromeOutFd = undefined; 54 | this.chromeErrorFd = undefined; 55 | this.pidFd = undefined; 56 | } 57 | 58 | async launch() { 59 | // pidFd is last file create by prepareChromeDataDir() 60 | if (!this.pidFd) { 61 | this.prepareChromeDataDir(); 62 | } 63 | await this.spawn(); 64 | return this; 65 | } 66 | 67 | kill() { 68 | return new Promise((resolve, reject) => { 69 | if (this.chromeProcess) { 70 | this.chromeProcess.on('close', () => { 71 | this.destroyChromeDataDir(); 72 | resolve(); 73 | }); 74 | 75 | try { 76 | this.shouldRestartChrome = false; 77 | this.chromeProcess.kill(); 78 | delete this.chromeProcess; 79 | clearInterval(this.monitorTimmer); 80 | delete this.monitorTimmer; 81 | } catch (err) { 82 | reject(err); 83 | } 84 | } else { 85 | // fail silently as we did not start chrome 86 | resolve(); 87 | } 88 | }); 89 | } 90 | 91 | get flags() { 92 | const flags = DEFAULT_FLAGS.concat([ 93 | `--remote-debugging-port=${this.port}`, 94 | `--user-data-dir=${this.chromeDataDir}` 95 | ]); 96 | 97 | if (process.platform === 'linux') { 98 | flags.push('--disable-setuid-sandbox'); 99 | // https://chromium.googlesource.com/chromium/src/+/master/docs/linux_suid_sandbox_development.md 100 | // https://github.com/gwuhaolin/chrome-pool/issues/4 101 | flags.push('--no-sandbox'); 102 | } 103 | 104 | flags.push(...this.chromeFlags); 105 | flags.push(this.startupPage); 106 | 107 | return flags; 108 | } 109 | 110 | // exec chrome, resolves when debugger is ready 111 | async spawn() { 112 | if (!this.port) { 113 | this.port = await getRandomPort(); 114 | } 115 | const chromeProcess = childProcess.spawn(this.chromePath, this.flags, { 116 | detached: true, 117 | stdio: this.disableLogging ? ['ignore', 'ignore', 'ignore'] : ['ignore', this.chromeOutFd, this.chromeErrorFd] 118 | }); 119 | this.chromeProcess = chromeProcess; 120 | this.monitorChromeIsAlive(); 121 | this.handleProcessEvent(); 122 | fs.writeFileSync(this.pidFd, chromeProcess.pid.toString()); 123 | return new Promise((resolve) => { 124 | this.once('chromeAlive', resolve); 125 | }); 126 | } 127 | 128 | monitorChromeIsAlive() { 129 | clearInterval(this.monitorTimmer); 130 | this.monitorTimmer = setInterval(async () => { 131 | const alive = await isPortOpen(this.port); 132 | if (alive) { 133 | this.emit('chromeAlive', this.port); 134 | } else { 135 | this.emit('chromeDead'); 136 | if (this.shouldRestartChrome) { 137 | if (this.chromeProcess) { 138 | this.chromeProcess.kill(); 139 | } 140 | await this.spawn(); 141 | this.emit('chromeRestarted'); 142 | } 143 | } 144 | }, this.monitorInterval); 145 | } 146 | 147 | handleProcessEvent() { 148 | if (typeof this.offDeath === 'function') { 149 | this.offDeath(); 150 | } 151 | // kill chrome and release resource on main app exit 152 | this.offDeath = onDeath(async () => { 153 | await this.kill(); 154 | process.exit(); 155 | }); 156 | } 157 | 158 | prepareChromeDataDir() { 159 | // support set chromeDataDir option 160 | if (!fs.existsSync(this.chromeDataDir)) { 161 | fs.mkdirSync(this.chromeDataDir); 162 | } 163 | if (!this.disableLogging) { 164 | this.chromeOutFd = fs.openSync(`${this.chromeDataDir}/chrome-out.log`, 'a'); 165 | this.chromeErrorFd = fs.openSync(`${this.chromeDataDir}/chrome-err.log`, 'a'); 166 | } 167 | this.pidFd = fs.openSync(`${this.chromeDataDir}/chrome.pid`, 'w'); 168 | this.emit('chromeDataDirPrepared', this.chromeDataDir); 169 | } 170 | 171 | destroyChromeDataDir() { 172 | try { 173 | if (this.chromeOutFd) { 174 | fs.closeSync(this.chromeOutFd); 175 | delete this.chromeOutFd; 176 | } 177 | 178 | if (this.chromeErrorFd) { 179 | fs.closeSync(this.chromeErrorFd); 180 | delete this.chromeErrorFd; 181 | } 182 | 183 | if (this.pidFd) { 184 | fs.closeSync(this.pidFd); 185 | delete this.pidFd; 186 | } 187 | 188 | if (this.chromeDataDir) { 189 | rimraf.sync(this.chromeDataDir); 190 | this.emit('chromeDataDirRemoved', this.chromeDataDir); 191 | } 192 | } catch (err) { 193 | // sometimes windows rimraf failed 194 | } 195 | } 196 | } 197 | 198 | module.exports = Runner; 199 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require('path'); 3 | const os = require('os'); 4 | const fs = require('fs'); 5 | const { createServer } = require('http'); 6 | const net = require('net'); 7 | 8 | /** 9 | * Return a random, unused port in system. 10 | */ 11 | function getRandomPort() { 12 | return new Promise((resolve, reject) => { 13 | const server = createServer(); 14 | server.listen(0); 15 | server.once('listening', () => { 16 | const port = server.address().port; 17 | server.close(() => resolve(port)); 18 | }); 19 | server.once('error', reject); 20 | }); 21 | } 22 | 23 | /** 24 | * make a tmp dir in OS tmp dir and return it's path 25 | * @returns {string} tmp dir path 26 | */ 27 | function makeTmpDir() { 28 | const now = new Date(); 29 | const tmpDirPath = path.resolve(os.tmpdir(), `chrome_${now.getFullYear()}_${now.getMonth() + 1}_${now.getDate()}__${now.getHours()}_${now.getMinutes()}_${now.getSeconds()}__${String(Math.random()).substring(2)}`); 30 | fs.mkdirSync(tmpDirPath); 31 | return tmpDirPath; 32 | } 33 | 34 | /** 35 | * check port is listening 36 | * @param port 37 | * @returns {Promise} 38 | */ 39 | function isPortOpen(port) { 40 | 41 | const cleanupNetClient = function (client) { 42 | if (client) { 43 | client.end(); 44 | client.destroy(); 45 | client.unref(); 46 | } 47 | } 48 | 49 | return new Promise((resolve) => { 50 | const client = net.createConnection(port); 51 | client.once('error', () => { 52 | cleanupNetClient(client); 53 | resolve(false); 54 | }); 55 | client.once('connect', () => { 56 | cleanupNetClient(client); 57 | resolve(true); 58 | }); 59 | }); 60 | } 61 | 62 | module.exports = { 63 | makeTmpDir, 64 | getRandomPort, 65 | isPortOpen, 66 | } 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chrome-runner", 3 | "version": "1.3.5", 4 | "description": "headless chrome runner launcher", 5 | "keywords": [ 6 | "headless", 7 | "chrome", 8 | "runner", 9 | "launcher" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "git@github.com:gwuhaolin/chrome-runner.git" 14 | }, 15 | "main": "index.js", 16 | "scripts": { 17 | "test": "mocha test" 18 | }, 19 | "author": "gwuhaolin", 20 | "engines": { 21 | "node": ">= 7.0.0" 22 | }, 23 | "dependencies": { 24 | "@types/node": "^8.0.7", 25 | "chrome-finder": "^1.0.1", 26 | "death": "^1.1.0", 27 | "rimraf": "^2.6.1" 28 | }, 29 | "devDependencies": { 30 | "mocha": "^5.0.5" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/runner.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const assert = require('assert'); 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const { Runner, launch, launchWithoutNoise, launchWithHeadless } = require('../index'); 6 | 7 | process.on('unhandledRejection', console.trace); 8 | 9 | async function delay(time) { 10 | return new Promise(resolve => setTimeout(resolve, time)); 11 | } 12 | 13 | describe('Runner', function () { 14 | this.timeout(5000); 15 | 16 | it('set and get port', async function () { 17 | this.timeout(10000); 18 | const runner = await launchWithHeadless({ 19 | port: 4577, 20 | }); 21 | assert.notEqual(runner.chromeProcess, null); 22 | assert.equal(runner.port, 4577); 23 | return await runner.kill(); 24 | }); 25 | 26 | it('use Runner to launch() then kill() and ensure chromeDataDirPrepared event emit', async function () { 27 | const runner = new Runner({ 28 | chromeFlags: ['--headless', '--disable-gpu'] 29 | }); 30 | runner.once('chromeDataDirPrepared', console.log); 31 | await runner.launch(); 32 | return await runner.kill(); 33 | }); 34 | 35 | it.skip('launch() then kill()', async function () { 36 | const runner = await launch(); 37 | assert.notEqual(runner.chromeProcess, null); 38 | return await runner.kill(); 39 | }); 40 | 41 | it.skip('launchWithoutNoise() then kill()', async function () { 42 | const runner = await launchWithoutNoise(); 43 | assert.notEqual(runner.chromeProcess, null); 44 | assert.notEqual(runner.port, null); 45 | return await runner.kill(); 46 | }); 47 | 48 | it('launchWithHeadless() then kill()', async function () { 49 | const runner = await launchWithHeadless(); 50 | assert.notEqual(runner.chromeProcess, null); 51 | assert.notEqual(runner.port, null); 52 | return await runner.kill(); 53 | }); 54 | 55 | it('set chromeDataDir option', async function () { 56 | const chromeDataDir = path.resolve(__dirname, '../chrome_runner_test'); 57 | const runner = await launchWithHeadless({ 58 | chromeDataDir, 59 | }); 60 | assert.equal(runner.chromeDataDir, chromeDataDir); 61 | return await runner.kill(); 62 | }); 63 | 64 | it('restart chrome when chrome exit unexpected', async function () { 65 | this.timeout(8000); 66 | const runner = await launchWithHeadless(); 67 | process.kill(runner.chromeProcess.pid); 68 | await delay(2000); 69 | process.kill(runner.chromeProcess.pid); 70 | await delay(2000); 71 | process.kill(runner.chromeProcess.pid); 72 | await delay(2000); 73 | await runner.kill(); 74 | }); 75 | 76 | 77 | it('after kill() all tmp file should be removed', async function () { 78 | const runner = await launchWithHeadless(); 79 | const chromeDataDir = runner.chromeDataDir; 80 | assert.notEqual(runner.chromeProcess, null); 81 | await runner.kill(); 82 | 83 | // TODO windows removed tmp dir failed 84 | if (process.platform !== 'win32') { 85 | assert.equal(fs.existsSync(chromeDataDir), false, `tmp dir ${chromeDataDir} should be removed`); 86 | } 87 | }); 88 | 89 | it('emit chromeDataDirRemoved after kill', function (done) { 90 | launchWithHeadless().then(async (runner) => { 91 | 92 | // TODO windows removed tmp dir failed 93 | if (process.platform !== 'win32') { 94 | runner.once('chromeDataDirRemoved', (chromeDataDir) => { 95 | console.log(chromeDataDir); 96 | done(); 97 | }); 98 | } else { 99 | done(); 100 | } 101 | await runner.kill(); 102 | }); 103 | }); 104 | 105 | it('should emit chromeAlive event then chromeRestarted event when chrome exit unexpected', function (done) { 106 | launchWithHeadless().then((runner) => { 107 | let chromeDead; 108 | runner.once('chromeDead', async () => { 109 | chromeDead = true; 110 | }); 111 | runner.once('chromeRestarted', async () => { 112 | if (chromeDead) { 113 | await runner.kill(); 114 | done(); 115 | } 116 | }); 117 | process.kill(runner.chromeProcess.pid); 118 | }); 119 | }); 120 | 121 | it('set monitorInterval should emit chromeAlive event interval', function (done) { 122 | this.timeout(7000); 123 | launchWithHeadless({ 124 | monitorInterval: 1000, 125 | }).then((runner) => { 126 | let times = 0; 127 | runner.on('chromeAlive', async () => { 128 | times++; 129 | if (times >= 5) { 130 | await runner.kill(); 131 | done(); 132 | } 133 | }); 134 | }); 135 | }); 136 | 137 | it('set disableLogging option', async function () { 138 | const runner = await launchWithHeadless({ 139 | disableLogging: true, 140 | }); 141 | assert.equal(fs.existsSync(path.join(runner.chromeDataDir, 'chrome-err.log')), false); 142 | assert.equal(fs.existsSync(path.join(runner.chromeDataDir, 'chrome-out.log')), false); 143 | return await runner.kill(); 144 | }); 145 | 146 | }); 147 | -------------------------------------------------------------------------------- /test/util.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const assert = require('assert'); 3 | const fs = require('fs'); 4 | const rimraf = require('rimraf'); 5 | const { getRandomPort, isPortOpen, makeTmpDir } = require('../lib/util'); 6 | 7 | describe('util', () => { 8 | 9 | it('getRandomPort', async () => { 10 | const port = await getRandomPort(); 11 | assert.ok(Number.isInteger(port) && port > 0 && port <= 0xFFFF, 'Verify generated port number is valid integer'); 12 | }); 13 | 14 | it('isPortOpen', async () => { 15 | const open = await isPortOpen(1111); 16 | assert.equal(open, false); 17 | }); 18 | 19 | it('makeTmpDir', () => { 20 | const tmpDirPath = makeTmpDir(); 21 | assert.equal(fs.existsSync(tmpDirPath), true); 22 | fs.accessSync(tmpDirPath, fs.constants.W_OK); 23 | rimraf.sync(tmpDirPath); 24 | assert.equal(fs.existsSync(tmpDirPath), false); 25 | }); 26 | }); 27 | --------------------------------------------------------------------------------