├── .node-version
├── .eslintignore
├── .prettierignore
├── index.js
├── .gitignore
├── test
├── fixtures
│ ├── require-name
│ │ ├── preload.js
│ │ ├── package.json
│ │ ├── index.html
│ │ └── main.js
│ ├── app
│ │ ├── package.json
│ │ ├── index.html
│ │ └── main.js
│ ├── example
│ │ ├── package.json
│ │ ├── main.js
│ │ └── index.html
│ ├── multi-window
│ │ ├── package.json
│ │ ├── index-top.html
│ │ ├── index-bottom.html
│ │ └── main.js
│ ├── slow
│ │ ├── package.json
│ │ ├── index.html
│ │ └── main.js
│ ├── web-view
│ │ ├── package.json
│ │ ├── web-view.html
│ │ ├── index.html
│ │ └── main.js
│ ├── accessible
│ │ ├── package.json
│ │ ├── index.html
│ │ └── main.js
│ ├── no-node-integration
│ │ ├── package.json
│ │ ├── index.html
│ │ └── main.js
│ └── not-accessible
│ │ ├── package.json
│ │ ├── web-view.html
│ │ ├── index.html
│ │ └── main.js
├── slow-page-test.js
├── global-setup.js
├── web-view-test.js
├── require-name-test.js
├── many-args.js
├── no-node-integration-test.js
├── multi-window-test.js
├── example-test.js
├── accessibility-test.js
├── commands-test.js
└── application-test.js
├── .prettierrc
├── lib
├── launcher.bat
├── rpath-fix.js
├── launcher.js
├── accessibility.js
├── chrome-driver.js
├── application.js
├── spectron.d.ts
└── api.js
├── tsconfig.json
├── .npmignore
├── .eslintrc.json
├── LICENSE.md
├── .github
└── workflows
│ └── ci.yml
├── package.json
├── CHANGELOG.md
└── README.md
/.node-version:
--------------------------------------------------------------------------------
1 | v8.11.1
2 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | vendor/axs_testing.js
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | vendor/axs_testing.js
2 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | exports.Application = require('./lib/application');
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | lib/api.json
4 | .npmrc
5 |
--------------------------------------------------------------------------------
/test/fixtures/require-name/preload.js:
--------------------------------------------------------------------------------
1 | window.electronRequire = require;
2 |
--------------------------------------------------------------------------------
/test/fixtures/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Test",
3 | "version": "1.0.0",
4 | "main": "main.js"
5 | }
6 |
--------------------------------------------------------------------------------
/test/fixtures/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Example",
3 | "version": "1.0.0",
4 | "main": "main.js"
5 | }
6 |
--------------------------------------------------------------------------------
/test/fixtures/multi-window/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Test",
3 | "version": "1.0.0",
4 | "main": "main.js"
5 | }
6 |
--------------------------------------------------------------------------------
/test/fixtures/require-name/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Test",
3 | "version": "1.0.0",
4 | "main": "main.js"
5 | }
6 |
--------------------------------------------------------------------------------
/test/fixtures/slow/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Slow Page",
3 | "version": "1.0.0",
4 | "main": "main.js"
5 | }
6 |
--------------------------------------------------------------------------------
/test/fixtures/web-view/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Example",
3 | "version": "1.0.0",
4 | "main": "main.js"
5 | }
6 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "none",
3 | "tabWidth": 2,
4 | "singleQuote": true,
5 | "endOfLine": "lf"
6 | }
7 |
--------------------------------------------------------------------------------
/lib/launcher.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | "%SPECTRON_NODE_PATH%" "%SPECTRON_LAUNCHER_PATH%" %*
3 | if ERRORLEVEL 1 exit /b 1
4 | exit /b 0
--------------------------------------------------------------------------------
/test/fixtures/accessible/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Accessibility",
3 | "version": "1.0.0",
4 | "main": "main.js"
5 | }
6 |
--------------------------------------------------------------------------------
/test/fixtures/no-node-integration/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Test",
3 | "version": "1.0.0",
4 | "main": "main.js"
5 | }
6 |
--------------------------------------------------------------------------------
/test/fixtures/not-accessible/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Accessibility",
3 | "version": "1.0.0",
4 | "main": "main.js"
5 | }
6 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "types": ["node", "webdriverio/async"]
4 | },
5 | "files": [
6 | "lib/spectron.d.ts"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | test
2 | .travis.yml
3 | appveyor.yml
4 | .npmignore
5 | script
6 | npm-debug.log
7 | lib/api.json
8 | lib/launcher.go
9 | .node-version
10 | .npmrc
--------------------------------------------------------------------------------
/test/fixtures/multi-window/index-top.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Top
6 |
7 |
8 | Top
9 |
10 |
11 |
--------------------------------------------------------------------------------
/test/fixtures/multi-window/index-bottom.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Bottom
6 |
7 |
8 | Bottom
9 |
10 |
11 |
--------------------------------------------------------------------------------
/test/fixtures/web-view/web-view.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Web View
6 |
7 |
8 | web view
9 |
10 |
11 |
--------------------------------------------------------------------------------
/test/fixtures/no-node-integration/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | no node integration
6 |
7 |
8 | no node integration
9 |
10 |
11 |
--------------------------------------------------------------------------------
/test/fixtures/not-accessible/web-view.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Web View
6 |
7 |
8 | bad aria role
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/test/fixtures/slow/index.html:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/test/fixtures/require-name/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | require name
6 |
10 |
11 |
12 | custom require name
13 |
14 |
15 |
--------------------------------------------------------------------------------
/test/fixtures/accessible/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Accessibility
6 |
12 |
13 |
14 | readable
15 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/test/fixtures/web-view/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
8 |
9 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/test/fixtures/not-accessible/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Accessibility
6 |
12 |
13 |
14 | unreadable
15 |
16 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/test/fixtures/accessible/main.js:
--------------------------------------------------------------------------------
1 | const { app, BrowserWindow } = require('electron');
2 | require('@electron/remote/main').initialize();
3 |
4 | let mainWindow = null;
5 |
6 | app.on('ready', function () {
7 | mainWindow = new BrowserWindow({
8 | center: true,
9 | width: 800,
10 | height: 600,
11 | webPreferences: {
12 | nodeIntegration: true,
13 | enableRemoteModule: true,
14 | contextIsolation: false
15 | }
16 | });
17 | require('@electron/remote/main').enable(mainWindow.webContents);
18 | mainWindow.loadFile('index.html');
19 | mainWindow.on('closed', function () {
20 | mainWindow = null;
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/test/fixtures/slow/main.js:
--------------------------------------------------------------------------------
1 | const { app, BrowserWindow } = require('electron');
2 | require('@electron/remote/main').initialize();
3 |
4 | let mainWindow = null;
5 |
6 | app.on('ready', function () {
7 | mainWindow = new BrowserWindow({
8 | x: 25,
9 | y: 35,
10 | width: 200,
11 | height: 100,
12 | webPreferences: {
13 | nodeIntegration: true,
14 | enableRemoteModule: true,
15 | contextIsolation: false
16 | }
17 | });
18 | require('@electron/remote/main').enable(mainWindow.webContents);
19 | mainWindow.loadFile('index.html');
20 | mainWindow.on('closed', function () {
21 | mainWindow = null;
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/lib/rpath-fix.js:
--------------------------------------------------------------------------------
1 | if (process.platform !== 'darwin') process.exit(0);
2 |
3 | const cp = require('child_process');
4 | const path = require('path');
5 |
6 | const pathToChromedriver = require.resolve(
7 | 'electron-chromedriver/chromedriver'
8 | );
9 | const pathToChromedriverBin = path.resolve(
10 | pathToChromedriver,
11 | '..',
12 | 'bin',
13 | 'chromedriver'
14 | );
15 |
16 | const result = cp.spawnSync(
17 | 'install_name_tool',
18 | ['-add_rpath', '@executable_path/.', pathToChromedriverBin],
19 | {}
20 | );
21 | if (result.status !== 0) {
22 | if (result.stderr.includes('file already has LC_RPATH')) process.exit(0);
23 | process.exit(result.status);
24 | }
25 |
--------------------------------------------------------------------------------
/test/fixtures/no-node-integration/main.js:
--------------------------------------------------------------------------------
1 | const { app, BrowserWindow } = require('electron');
2 | require('@electron/remote/main').initialize();
3 |
4 | let mainWindow = null;
5 |
6 | app.on('ready', function () {
7 | mainWindow = new BrowserWindow({
8 | x: 25,
9 | y: 35,
10 | width: 200,
11 | height: 100,
12 | webPreferences: {
13 | nodeIntegration: false,
14 | enableRemoteModule: true,
15 | contextIsolation: false
16 | }
17 | });
18 | require('@electron/remote/main').enable(mainWindow.webContents);
19 | mainWindow.loadFile('index.html');
20 | mainWindow.on('closed', function () {
21 | mainWindow = null;
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/test/fixtures/not-accessible/main.js:
--------------------------------------------------------------------------------
1 | const { app, BrowserWindow } = require('electron');
2 | require('@electron/remote/main').initialize();
3 |
4 | let mainWindow = null;
5 |
6 | app.on('ready', function () {
7 | mainWindow = new BrowserWindow({
8 | center: true,
9 | width: 800,
10 | height: 600,
11 | webPreferences: {
12 | nodeIntegration: true,
13 | contextIsolation: false,
14 | enableRemoteModule: true,
15 | webviewTag: true
16 | }
17 | });
18 | require('@electron/remote/main').enable(mainWindow.webContents);
19 | mainWindow.loadFile('index.html');
20 | mainWindow.on('closed', function () {
21 | mainWindow = null;
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/test/fixtures/example/main.js:
--------------------------------------------------------------------------------
1 | const { app, BrowserWindow } = require('electron');
2 | require('@electron/remote/main').initialize();
3 |
4 | let mainWindow = null;
5 |
6 | app.on('ready', function () {
7 | mainWindow = new BrowserWindow({
8 | center: true,
9 | width: 800,
10 | height: 400,
11 | minHeight: 100,
12 | minWidth: 100,
13 | webPreferences: {
14 | nodeIntegration: true,
15 | enableRemoteModule: true,
16 | contextIsolation: false
17 | }
18 | });
19 | require('@electron/remote/main').enable(mainWindow.webContents);
20 | mainWindow.loadFile('index.html');
21 | mainWindow.on('closed', function () {
22 | mainWindow = null;
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/test/fixtures/web-view/main.js:
--------------------------------------------------------------------------------
1 | const { app, BrowserWindow } = require('electron');
2 | require('@electron/remote/main').initialize();
3 |
4 | let mainWindow = null;
5 |
6 | app.on('ready', function () {
7 | mainWindow = new BrowserWindow({
8 | center: true,
9 | width: 800,
10 | height: 400,
11 | minHeight: 100,
12 | minWidth: 100,
13 | webPreferences: {
14 | nodeIntegration: true,
15 | enableRemoteModule: true,
16 | contextIsolation: false,
17 | webviewTag: true
18 | }
19 | });
20 | require('@electron/remote/main').enable(mainWindow.webContents);
21 | mainWindow.loadFile('index.html');
22 | mainWindow.on('closed', function () {
23 | mainWindow = null;
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/test/fixtures/require-name/main.js:
--------------------------------------------------------------------------------
1 | const { app, BrowserWindow } = require('electron');
2 | require('@electron/remote/main').initialize();
3 | const path = require('path');
4 |
5 | let mainWindow = null;
6 |
7 | app.on('ready', function () {
8 | mainWindow = new BrowserWindow({
9 | x: 25,
10 | y: 35,
11 | width: 200,
12 | height: 100,
13 | webPreferences: {
14 | nodeIntegration: false,
15 | preload: path.join(__dirname, 'preload.js'),
16 | enableRemoteModule: true,
17 | contextIsolation: false
18 | }
19 | });
20 | require('@electron/remote/main').enable(mainWindow.webContents);
21 | mainWindow.loadFile('index.html');
22 | mainWindow.on('closed', function () {
23 | mainWindow = null;
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/test/fixtures/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Test
6 |
11 |
12 |
13 |
14 | Hello
15 | word1 word2
16 | word3 word4
17 | word5 word6
18 |
19 |
20 |
21 |
22 |
27 |
28 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "standard",
3 | "parser": "@typescript-eslint/parser",
4 | "plugins": ["@typescript-eslint"],
5 | "env": {
6 | "browser": true
7 | },
8 | "rules": {
9 | "semi": ["error", "always"],
10 | "no-var": "error",
11 | "no-unused-vars": 0,
12 | "no-global-assign": 0,
13 | "guard-for-in": 2,
14 | "space-before-function-paren": 0,
15 | "@typescript-eslint/no-unused-vars": ["error", {
16 | "vars": "all",
17 | "args": "after-used",
18 | "ignoreRestSiblings": false
19 | }],
20 | "prefer-const": ["error", {
21 | "destructuring": "all"
22 | }],
23 | "standard/no-callback-literal": "off",
24 | "node/no-deprecated-api": 0
25 | },
26 | "parserOptions": {
27 | "ecmaVersion": 6,
28 | "sourceType": "module"
29 | },
30 | "overrides": [
31 | {
32 | "files": "*.d.ts",
33 | "rules": {
34 | "no-useless-constructor": "off",
35 | "no-undef": "off"
36 | }
37 | }
38 | ]
39 | }
40 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016 GitHub Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/test/fixtures/multi-window/main.js:
--------------------------------------------------------------------------------
1 | const { app, BrowserWindow } = require('electron');
2 | require('@electron/remote/main').initialize();
3 |
4 | let topWindow = null;
5 | let bottomWindow = null;
6 |
7 | app.on('ready', function () {
8 | topWindow = new BrowserWindow({
9 | x: 25,
10 | y: 35,
11 | width: 200,
12 | height: 100,
13 | webPreferences: {
14 | nodeIntegration: true,
15 | enableRemoteModule: true,
16 | contextIsolation: false
17 | }
18 | });
19 | require('@electron/remote/main').enable(topWindow.webContents);
20 | topWindow.loadFile('index-top.html');
21 | topWindow.on('closed', function () {
22 | topWindow = null;
23 | });
24 |
25 | bottomWindow = new BrowserWindow({
26 | x: 25,
27 | y: 135,
28 | width: 300,
29 | height: 50,
30 | webPreferences: {
31 | nodeIntegration: true,
32 | enableRemoteModule: true,
33 | contextIsolation: false
34 | }
35 | });
36 | require('@electron/remote/main').enable(bottomWindow.webContents);
37 | bottomWindow.loadFile('index-bottom.html');
38 | bottomWindow.on('closed', function () {
39 | bottomWindow = null;
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | tags:
8 | - v[0-9]+.[0-9]+.[0-9]+*
9 | pull_request:
10 |
11 | jobs:
12 | build:
13 | runs-on: ${{ matrix.os }}
14 | strategy:
15 | matrix:
16 | os: [ubuntu-latest, windows-latest, macOS-latest]
17 | node-version: [12.x]
18 |
19 | steps:
20 | - name: Fix git checkout line endings
21 | run: git config --global core.autocrlf input
22 | - uses: actions/checkout@v2
23 | - name: Setup Node.js
24 | uses: actions/setup-node@v1
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 | - name: Get npm cache directory
28 | id: npm-cache
29 | run: |
30 | echo "::set-output name=dir::$(npm config get cache)"
31 | - uses: actions/cache@v1
32 | with:
33 | path: ${{ steps.npm-cache.outputs.dir }}
34 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
35 | restore-keys: |
36 | ${{ runner.os }}-node-
37 | - name: Install
38 | run: npm ci
39 | - name: Test
40 | run: npm test
41 |
--------------------------------------------------------------------------------
/lib/launcher.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const ChildProcess = require('child_process');
4 |
5 | let executablePath = null;
6 | const appArgs = [];
7 | const chromeArgs = [];
8 |
9 | process.argv.slice(2).forEach(function (arg) {
10 | const indexOfEqualSign = arg.indexOf('=');
11 | if (indexOfEqualSign === -1) {
12 | chromeArgs.push(arg);
13 | return;
14 | }
15 |
16 | const name = arg.substring(0, indexOfEqualSign);
17 | const value = arg.substring(indexOfEqualSign + 1);
18 | if (name === '--spectron-path') {
19 | executablePath = value;
20 | } else if (name.indexOf('--spectron-arg') === 0) {
21 | appArgs[Number(name.substring(14))] = value;
22 | } else if (name.indexOf('--spectron-env') === 0) {
23 | process.env[name.substring(15)] = value;
24 | } else if (name.indexOf('--spectron-') !== 0) {
25 | chromeArgs.push(arg);
26 | }
27 | });
28 |
29 | const args = appArgs.concat(chromeArgs);
30 | const appProcess = ChildProcess.spawn(executablePath, args);
31 | appProcess.on('exit', function (code) {
32 | process.exit(code);
33 | });
34 | appProcess.stderr.pipe(process.stdout);
35 | appProcess.stdout.pipe(process.stdout);
36 | appProcess.stdin.pipe(process.stdin);
37 |
--------------------------------------------------------------------------------
/test/slow-page-test.js:
--------------------------------------------------------------------------------
1 | const helpers = require('./global-setup');
2 | const path = require('path');
3 | const expect = require('chai').expect;
4 |
5 | const describe = global.describe;
6 | const it = global.it;
7 | const before = global.before;
8 | const after = global.after;
9 |
10 | describe('Slow loading page', function () {
11 | helpers.setupTimeout(this);
12 |
13 | let app = null;
14 |
15 | before(function () {
16 | return helpers
17 | .startApplication({
18 | args: [path.join(__dirname, 'fixtures', 'slow')]
19 | })
20 | .then(function (startedApp) {
21 | app = startedApp;
22 | });
23 | });
24 |
25 | after(function () {
26 | return helpers.stopApplication(app);
27 | });
28 |
29 | describe('webContents.isLoading()', function () {
30 | it('resolves to true', function () {
31 | return app.webContents.isLoading().should.eventually.be.true;
32 | });
33 | });
34 |
35 | describe('waitUntilWindowLoaded(timeout)', function () {
36 | it('rejects with an error when the timeout is hit', async function () {
37 | await expect(app.client.waitUntilWindowLoaded(100)).to.be.rejectedWith(
38 | Error
39 | );
40 | });
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/test/fixtures/app/main.js:
--------------------------------------------------------------------------------
1 | const { app, BrowserWindow, ipcMain } = require('electron');
2 | require('@electron/remote/main').initialize();
3 | const fs = require('fs');
4 | const path = require('path');
5 |
6 | let mainWindow = null;
7 | app.allowRendererProcessReuse = true;
8 |
9 | app.on('ready', function () {
10 | console.log('main log');
11 | console.warn('main warn');
12 | console.error('main error');
13 |
14 | global.mainProcessGlobal = 'foo';
15 | global.ipcEventCount = 0;
16 |
17 | mainWindow = new BrowserWindow({
18 | x: 25,
19 | y: 35,
20 | width: 200,
21 | height: 100,
22 | webPreferences: {
23 | enableRemoteModule: true,
24 | nodeIntegration: true,
25 | contextIsolation: false
26 | }
27 | });
28 | require('@electron/remote/main').enable(mainWindow.webContents);
29 | mainWindow.loadFile('index.html');
30 | mainWindow.on('closed', function () {
31 | mainWindow = null;
32 | });
33 | });
34 |
35 | app.on('will-quit', function () {
36 | if (fs.existsSync(process.env.SPECTRON_TEMP_DIR)) {
37 | fs.writeFileSync(path.join(process.env.SPECTRON_TEMP_DIR, 'quit.txt'), '');
38 | }
39 | });
40 |
41 | ipcMain.on('ipc-event', function (event, count) {
42 | global.ipcEventCount += count;
43 | });
44 |
--------------------------------------------------------------------------------
/test/global-setup.js:
--------------------------------------------------------------------------------
1 | const Application = require('..').Application;
2 | const assert = require('assert');
3 | const chai = require('chai');
4 | const chaiAsPromised = require('chai-as-promised');
5 | const chaiRoughly = require('chai-roughly');
6 |
7 | const path = require('path');
8 |
9 | global.before(function () {
10 | chai.should();
11 | chai.use(chaiAsPromised);
12 | chai.use(chaiRoughly);
13 | });
14 |
15 | exports.getElectronPath = function () {
16 | let electronPath = path.join(
17 | __dirname,
18 | '..',
19 | 'node_modules',
20 | '.bin',
21 | 'electron'
22 | );
23 | if (process.platform === 'win32') electronPath += '.cmd';
24 | return electronPath;
25 | };
26 |
27 | exports.setupTimeout = function (test) {
28 | if (process.env.CI) {
29 | test.timeout(30000);
30 | } else {
31 | test.timeout(10000);
32 | }
33 | };
34 |
35 | exports.startApplication = function (options) {
36 | options.path = exports.getElectronPath();
37 | if (process.env.CI) options.startTimeout = 30000;
38 |
39 | const app = new Application(options);
40 | return app.start().then(function () {
41 | assert.strictEqual(app.isRunning(), true);
42 | chaiAsPromised.transferPromiseness = app.transferPromiseness;
43 | return app;
44 | });
45 | };
46 |
47 | exports.stopApplication = async function (app) {
48 | if (!app || !app.isRunning()) return;
49 |
50 | await app.stop();
51 | assert.strictEqual(app.isRunning(), false);
52 | };
53 |
--------------------------------------------------------------------------------
/test/web-view-test.js:
--------------------------------------------------------------------------------
1 | const helpers = require('./global-setup');
2 | const path = require('path');
3 | const { expect } = require('chai');
4 |
5 | const describe = global.describe;
6 | const it = global.it;
7 | const beforeEach = global.beforeEach;
8 | const afterEach = global.afterEach;
9 |
10 | describe(' tags', function () {
11 | helpers.setupTimeout(this);
12 |
13 | let app = null;
14 |
15 | beforeEach(function () {
16 | return helpers
17 | .startApplication({
18 | args: [path.join(__dirname, 'fixtures', 'web-view')]
19 | })
20 | .then(function (startedApp) {
21 | app = startedApp;
22 | });
23 | });
24 |
25 | afterEach(function () {
26 | return helpers.stopApplication(app);
27 | });
28 |
29 | it('allows the web view to be accessed', async function () {
30 | // waiting for windowHandles ensures waitUntilWindowLoaded doesn't access a nil webContents.
31 | // TODO: this issue should be fixed by waitUntilWindowLoaded instead of this workaround.
32 | await app.client.getWindowHandles();
33 | await app.client.waitUntilWindowLoaded();
34 | const count = await app.client.getWindowCount();
35 | expect(count).to.equal(2);
36 | await app.client.windowByIndex(1);
37 | const elem = await app.client.$('body');
38 | const text = await elem.getText();
39 | expect(text).to.equal('web view');
40 | await app.webContents.getTitle().should.eventually.equal('Web View');
41 | await app.client.windowByIndex(0);
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/test/require-name-test.js:
--------------------------------------------------------------------------------
1 | const helpers = require('./global-setup');
2 | const path = require('path');
3 | const { expect } = require('chai');
4 |
5 | const describe = global.describe;
6 | const it = global.it;
7 | const beforeEach = global.beforeEach;
8 | const afterEach = global.afterEach;
9 |
10 | describe('requireName option to Application', function () {
11 | helpers.setupTimeout(this);
12 |
13 | let app = null;
14 |
15 | beforeEach(function () {
16 | return helpers
17 | .startApplication({
18 | args: [path.join(__dirname, 'fixtures', 'require-name')],
19 | requireName: 'electronRequire'
20 | })
21 | .then(function (startedApp) {
22 | app = startedApp;
23 | });
24 | });
25 |
26 | afterEach(function () {
27 | return helpers.stopApplication(app);
28 | });
29 |
30 | it('uses the custom require name to load the electron module', async function () {
31 | await app.client.waitUntilWindowLoaded();
32 | await app.browserWindow
33 | .getBounds()
34 | .should.eventually.roughly(5)
35 | .deep.equal({
36 | x: 25,
37 | y: 35,
38 | width: 200,
39 | height: 100
40 | });
41 | await app.webContents.getTitle().should.eventually.equal('require name');
42 | const emptyArgs = await app.electron.remote.process.execArgv();
43 | const elem = await app.client.$('body');
44 | const text = await elem.getText();
45 | expect(text).to.equal('custom require name');
46 | await app.webContents.getTitle().should.eventually.equal('require name');
47 | return expect(emptyArgs).to.be.empty;
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/test/many-args.js:
--------------------------------------------------------------------------------
1 | const helpers = require('./global-setup');
2 | const path = require('path');
3 | const temp = require('temp').track();
4 |
5 | const describe = global.describe;
6 | const it = global.it;
7 | const beforeEach = global.beforeEach;
8 | const afterEach = global.afterEach;
9 | const expect = require('chai').expect;
10 |
11 | describe('application loading', function () {
12 | helpers.setupTimeout(this);
13 |
14 | let app = null;
15 | let tempPath = null;
16 |
17 | beforeEach(function () {
18 | tempPath = temp.mkdirSync('spectron-temp-dir-');
19 |
20 | return helpers
21 | .startApplication({
22 | cwd: path.join(__dirname, 'fixtures'),
23 | args: [
24 | path.join(__dirname, 'fixtures', 'app'),
25 | '--bar1=baz1',
26 | '--bar2=baz2',
27 | '--bar3=baz3',
28 | '--bar4=baz4',
29 | '--bar5=baz5',
30 | '--bar6=baz6',
31 | '--bar7=baz7',
32 | '--bar8=baz8',
33 | '--bar9=baz9',
34 | '--bar10=baz10',
35 | '--bar11=baz11',
36 | '--bar12=baz12',
37 | '--bar13=baz13'
38 | ],
39 | env: {
40 | FOO: 'BAR',
41 | HELLO: 'WORLD',
42 | SPECTRON_TEMP_DIR: tempPath
43 | }
44 | })
45 | .then(function (startedApp) {
46 | app = startedApp;
47 | });
48 | });
49 |
50 | afterEach(function () {
51 | return helpers.stopApplication(app);
52 | });
53 |
54 | it('passes through args to the launched app', async function () {
55 | const argv = await app.mainProcess.argv();
56 | expect(argv[2]).to.equal('--bar1=baz1');
57 | expect(argv[9]).to.equal('--bar8=baz8');
58 | expect(argv[12]).to.equal('--bar11=baz11');
59 | });
60 | });
61 |
--------------------------------------------------------------------------------
/test/no-node-integration-test.js:
--------------------------------------------------------------------------------
1 | const helpers = require('./global-setup');
2 | const path = require('path');
3 | const assert = require('assert');
4 | const { expect } = require('chai');
5 |
6 | const describe = global.describe;
7 | const it = global.it;
8 | const before = global.before;
9 | const after = global.after;
10 |
11 | const skipIfWindows = process.platform === 'win32' ? describe.skip : describe;
12 |
13 | skipIfWindows('when nodeIntegration is set to false', function () {
14 | helpers.setupTimeout(this);
15 |
16 | let app = null;
17 |
18 | before(function () {
19 | return helpers
20 | .startApplication({
21 | args: [path.join(__dirname, 'fixtures', 'no-node-integration')]
22 | })
23 | .then(function (startedApp) {
24 | app = startedApp;
25 | });
26 | });
27 |
28 | after(function () {
29 | return helpers.stopApplication(app);
30 | });
31 |
32 | it('does not throw an error', async function () {
33 | await app.client.getTitle().should.eventually.equal('no node integration');
34 | const elem = await app.client.$('body');
35 | const text = await elem.getText();
36 | expect(text).to.equal('no node integration');
37 | });
38 |
39 | it('does not add Electron API helper methods', function () {
40 | assert.strictEqual(typeof app.electron, 'undefined');
41 | assert.strictEqual(typeof app.browserWindow, 'undefined');
42 | assert.strictEqual(typeof app.webContents, 'undefined');
43 | assert.strictEqual(typeof app.mainProcess, 'undefined');
44 | assert.strictEqual(typeof app.rendererProcess, 'undefined');
45 |
46 | assert.strictEqual(typeof app.client.electron, 'undefined');
47 | assert.strictEqual(typeof app.client.browserWindow, 'undefined');
48 | assert.strictEqual(typeof app.client.webContents, 'undefined');
49 | assert.strictEqual(typeof app.client.mainProcess, 'undefined');
50 | assert.strictEqual(typeof app.client.rendererProcess, 'undefined');
51 | });
52 | });
53 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "spectron",
3 | "version": "19.0.0",
4 | "description": "Easily test your Electron apps using ChromeDriver and WebdriverIO.",
5 | "main": "index.js",
6 | "types": "./lib/spectron.d.ts",
7 | "scripts": {
8 | "lint": "eslint \"**/*.js\" \"**/*.ts\" && prettier --check \"**/*.js\" \"**/*.ts\"",
9 | "prettier:write": "prettier --write \"**/*.js\" \"**/*.ts\"",
10 | "prepare": "check-for-leaks",
11 | "prepush": "check-for-leaks",
12 | "pretest": "tsc",
13 | "test": "npm run lint && xvfb-maybe --server-args=\"-screen 0 1024x768x24\" -- mocha",
14 | "postinstall": "node lib/rpath-fix.js"
15 | },
16 | "engines": {
17 | "node": ">=12.20.0"
18 | },
19 | "repository": "https://github.com/electron/spectron",
20 | "keywords": [
21 | "electron",
22 | "chromedriver",
23 | "webdriverio",
24 | "selenium"
25 | ],
26 | "author": "Kevin Sawicki",
27 | "license": "MIT",
28 | "dependencies": {
29 | "@electron/remote": "2.0.4",
30 | "dev-null": "^0.1.1",
31 | "electron-chromedriver": "17.0.0",
32 | "got": "^11.8.0",
33 | "split": "^1.0.1",
34 | "webdriverio": "7.16.13"
35 | },
36 | "devDependencies": {
37 | "@typescript-eslint/eslint-plugin": "^4.8.2",
38 | "@typescript-eslint/parser": "^4.8.2",
39 | "chai": "^4.2.0",
40 | "chai-as-promised": "^7.1.1",
41 | "chai-roughly": "^1.0.0",
42 | "check-for-leaks": "^1.2.1",
43 | "electron": "^17.0.0",
44 | "eslint": "^7.14.0",
45 | "eslint-config-standard": "^16.0.2",
46 | "eslint-plugin-import": "^2.22.1",
47 | "eslint-plugin-node": "^11.1.0",
48 | "eslint-plugin-promise": "^4.2.1",
49 | "husky": "^4.3.0",
50 | "mocha": "^9.0.2",
51 | "prettier": "^2.2.0",
52 | "standard": "^16.0.3",
53 | "temp": "^0.9.4",
54 | "ts-node": "^10.3.0",
55 | "typescript": "^4.4.4",
56 | "xvfb-maybe": "^0.2.1"
57 | },
58 | "resolutions": {
59 | "@types/node": "^12.0.0"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/test/fixtures/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Example
6 |
53 |
70 |
71 |
72 |
73 | make larger
74 |
75 | make smaller
76 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/lib/accessibility.js:
--------------------------------------------------------------------------------
1 | const axsPath = require.resolve('../vendor/axs_testing');
2 |
3 | exports.addCommand = function (client, requireName) {
4 | client.addCommand('auditAccessibility', function (options) {
5 | return this.execute(
6 | function (axsPath, requireName, options) {
7 | options = options || {};
8 | const ignoreWarnings = options.ignoreWarnings || false;
9 | const ignoreRules = Array.isArray(options.ignoreRules)
10 | ? options.ignoreRules
11 | : [];
12 |
13 | const axs = window[requireName](axsPath);
14 | const audit = axs.Audit.run(
15 | new axs.AuditConfiguration({
16 | showUnsupportedRulesWarning: false
17 | })
18 | );
19 |
20 | let failures = audit.filter(function (result) {
21 | return result.result === 'FAIL';
22 | });
23 |
24 | if (ignoreWarnings) {
25 | failures = failures.filter(function (result) {
26 | return result.rule.severity !== 'Warning';
27 | });
28 | }
29 |
30 | failures = failures.filter(function (result) {
31 | return ignoreRules.indexOf(result.rule.code) === -1;
32 | });
33 |
34 | if (failures.length > 0) {
35 | let message = 'Accessibilty audit failed\n\n';
36 | message += failures
37 | .map(function (result) {
38 | return axs.Audit.accessibilityErrorMessage(result);
39 | })
40 | .join('\n\n');
41 |
42 | return {
43 | message: message,
44 | failed: true,
45 | results: failures.map(function (result) {
46 | return {
47 | code: result.rule.code,
48 | elements: result.elements.map(function (element) {
49 | return axs.utils.getQuerySelectorText(element);
50 | }),
51 | message: result.rule.heading,
52 | severity: result.rule.severity,
53 | url: result.rule.url
54 | };
55 | })
56 | };
57 | } else {
58 | return {
59 | message: 'Accessibilty audit passed',
60 | results: [],
61 | failed: false
62 | };
63 | }
64 | },
65 | axsPath,
66 | requireName,
67 | options
68 | ).then(function (response) {
69 | return response;
70 | });
71 | });
72 | };
73 |
--------------------------------------------------------------------------------
/test/multi-window-test.js:
--------------------------------------------------------------------------------
1 | const helpers = require('./global-setup');
2 | const path = require('path');
3 |
4 | const describe = global.describe;
5 | const it = global.it;
6 | const beforeEach = global.beforeEach;
7 | const afterEach = global.afterEach;
8 |
9 | describe('multiple windows', function () {
10 | helpers.setupTimeout(this);
11 |
12 | let app = null;
13 |
14 | beforeEach(function () {
15 | return helpers
16 | .startApplication({
17 | args: [path.join(__dirname, 'fixtures', 'multi-window')]
18 | })
19 | .then(function (startedApp) {
20 | app = startedApp;
21 | });
22 | });
23 |
24 | afterEach(function () {
25 | return helpers.stopApplication(app);
26 | });
27 |
28 | // TODO: This is failing across platforms
29 | it.skip('should switch focus thanks to windowByIndex', async function () {
30 | const windowCount = await app.client.getWindowCount();
31 | windowCount.should.equal(2);
32 |
33 | const windowsData = {};
34 |
35 | await app.client.windowByIndex(0);
36 | const window0Title = await app.browserWindow.getTitle();
37 | const window0Bounds = await app.browserWindow.getBounds();
38 | windowsData[window0Title] = window0Bounds;
39 |
40 | await app.client.windowByIndex(1);
41 | const window1Title = await app.browserWindow.getTitle();
42 | const window1Bounds = await app.browserWindow.getBounds();
43 | windowsData[window1Title] = window1Bounds;
44 |
45 | windowsData.Top.should.roughly(5).deep.equal({
46 | x: 25,
47 | y: 35,
48 | width: 200,
49 | height: 100
50 | });
51 | windowsData.Bottom.should.roughly(5).deep.equal({
52 | x: 25,
53 | y: 135,
54 | width: 300,
55 | height: 50
56 | });
57 | });
58 |
59 | it('should switch focus thanks to switchWindow', async function () {
60 | const windowCount = await app.client.getWindowCount();
61 | windowCount.should.equal(2);
62 | await app.client.switchWindow('Top');
63 | await app.client.getTitle().should.eventually.equal('Top');
64 | await app.client.switchWindow('Bottom');
65 | await app.client.getTitle().should.eventually.equal('Bottom');
66 | await app.client.switchWindow('index-top.html');
67 | await app.client.getTitle().should.eventually.equal('Top');
68 | await app.client.switchWindow('index-bottom.html');
69 | await app.client.getTitle().should.eventually.equal('Bottom');
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/test/example-test.js:
--------------------------------------------------------------------------------
1 | // Test for examples included in README.md
2 | const helpers = require('./global-setup');
3 | const path = require('path');
4 | const { expect } = require('chai');
5 |
6 | const describe = global.describe;
7 | const it = global.it;
8 | const beforeEach = global.beforeEach;
9 | const afterEach = global.afterEach;
10 |
11 | describe('example application launch', function () {
12 | helpers.setupTimeout(this);
13 |
14 | let app = null;
15 |
16 | beforeEach(function () {
17 | return helpers
18 | .startApplication({
19 | args: [path.join(__dirname, 'fixtures', 'example')]
20 | })
21 | .then(function (startedApp) {
22 | app = startedApp;
23 | });
24 | });
25 |
26 | afterEach(function () {
27 | return helpers.stopApplication(app);
28 | });
29 |
30 | it('opens a window', async function () {
31 | await app.client.waitUntilWindowLoaded();
32 | app.browserWindow.focus();
33 | const windowCount = await app.client.getWindowCount();
34 | expect(windowCount).to.equal(1);
35 | const isMinimized = await app.browserWindow.isMinimized();
36 | expect(isMinimized).to.equal(false);
37 | const isDevOpen = await app.browserWindow.isDevToolsOpened();
38 | expect(isDevOpen).to.equal(false);
39 | const isVisible = await app.browserWindow.isVisible();
40 | expect(isVisible).to.equal(true);
41 | const isFocused = await app.browserWindow.isFocused();
42 | expect(isFocused).to.equal(true);
43 | await app.browserWindow
44 | .getBounds()
45 | .should.eventually.have.property('width')
46 | .and.be.above(0);
47 | await app.browserWindow
48 | .getBounds()
49 | .should.eventually.have.property('height')
50 | .and.be.above(0);
51 | });
52 |
53 | describe('when the make larger button is clicked', function () {
54 | it('increases the window height and width by 10 pixels', async function () {
55 | await app.client.waitUntilWindowLoaded();
56 | await app.browserWindow
57 | .getBounds()
58 | .should.eventually.have.property('width', 800);
59 | await app.browserWindow
60 | .getBounds()
61 | .should.eventually.have.property('height', 400);
62 | const elem = await app.client.$('.btn-make-bigger');
63 | await elem.click();
64 | const bounds = await app.browserWindow.getBounds();
65 | bounds.should.have.property('width', 810);
66 | bounds.should.have.property('height', 410);
67 | });
68 | });
69 |
70 | describe('when the make smaller button is clicked', function () {
71 | it('decreases the window height and width by 10 pixels', async function () {
72 | await app.client.waitUntilWindowLoaded();
73 | await app.browserWindow
74 | .getBounds()
75 | .should.eventually.have.property('width', 800);
76 | await app.browserWindow
77 | .getBounds()
78 | .should.eventually.have.property('height', 400);
79 | const elem = await app.client.$('.btn-make-smaller');
80 | await elem.click();
81 | const bounds = await app.browserWindow.getBounds();
82 | bounds.should.have.property('width', 790);
83 | bounds.should.have.property('height', 390);
84 | });
85 | });
86 | });
87 |
--------------------------------------------------------------------------------
/lib/chrome-driver.js:
--------------------------------------------------------------------------------
1 | const ChildProcess = require('child_process');
2 | const path = require('path');
3 | const { default: got } = require('got');
4 | const split = require('split');
5 |
6 | function ChromeDriver(
7 | host,
8 | port,
9 | nodePath,
10 | startTimeout,
11 | workingDirectory,
12 | chromeDriverLogPath
13 | ) {
14 | this.host = host;
15 | this.port = port;
16 | this.nodePath = nodePath;
17 | this.startTimeout = startTimeout;
18 | this.workingDirectory = workingDirectory;
19 | this.chromeDriverLogPath = chromeDriverLogPath;
20 |
21 | this.path = require.resolve('electron-chromedriver/chromedriver');
22 | this.urlBase = '/';
23 | this.statusUrl =
24 | 'http://' + this.host + ':' + this.port + this.urlBase + 'status';
25 | this.logLines = [];
26 | }
27 |
28 | ChromeDriver.prototype.start = function () {
29 | if (this.process) throw new Error('ChromeDriver already started');
30 |
31 | const args = [this.path, '--port=' + this.port, '--url-base=' + this.urlBase];
32 |
33 | if (this.chromeDriverLogPath) {
34 | args.push('--verbose');
35 | args.push('--log-path=' + this.chromeDriverLogPath);
36 | }
37 | const options = {
38 | cwd: this.workingDirectory,
39 | env: this.getEnvironment()
40 | };
41 | this.process = ChildProcess.spawn(this.nodePath, args, options);
42 |
43 | const self = this;
44 | this.exitHandler = function () {
45 | self.stop();
46 | };
47 | global.process.on('exit', this.exitHandler);
48 |
49 | this.setupLogs();
50 | return this.waitUntilRunning();
51 | };
52 |
53 | ChromeDriver.prototype.waitUntilRunning = function () {
54 | const self = this;
55 | return new Promise(function (resolve, reject) {
56 | const startTime = Date.now();
57 | const checkIfRunning = function () {
58 | self.isRunning(function (running) {
59 | if (!self.process) {
60 | return reject(Error('ChromeDriver has been stopped'));
61 | }
62 |
63 | if (running) {
64 | return resolve();
65 | }
66 |
67 | const elapsedTime = Date.now() - startTime;
68 | if (elapsedTime > self.startTimeout) {
69 | return reject(
70 | Error(
71 | 'ChromeDriver did not start within ' + self.startTimeout + 'ms'
72 | )
73 | );
74 | }
75 |
76 | global.setTimeout(checkIfRunning, 100);
77 | });
78 | };
79 | checkIfRunning();
80 | });
81 | };
82 |
83 | ChromeDriver.prototype.setupLogs = function () {
84 | const linesToIgnore = 2; // First two lines are ChromeDriver specific
85 | let lineCount = 0;
86 |
87 | this.logLines = [];
88 |
89 | const self = this;
90 | this.process.stdout.pipe(split()).on('data', function (line) {
91 | if (lineCount < linesToIgnore) {
92 | lineCount++;
93 | return;
94 | }
95 | self.logLines.push(line);
96 | });
97 | };
98 |
99 | ChromeDriver.prototype.getEnvironment = function () {
100 | const env = {};
101 | Object.keys(process.env).forEach(function (key) {
102 | env[key] = process.env[key];
103 | });
104 |
105 | if (process.platform === 'win32') {
106 | env.SPECTRON_NODE_PATH = process.execPath;
107 | env.SPECTRON_LAUNCHER_PATH = path.join(__dirname, 'launcher.js');
108 | }
109 |
110 | return env;
111 | };
112 |
113 | ChromeDriver.prototype.stop = function () {
114 | if (this.exitHandler) global.process.removeListener('exit', this.exitHandler);
115 | this.exitHandler = null;
116 |
117 | if (this.process) this.process.kill();
118 | this.process = null;
119 |
120 | this.clearLogs();
121 | };
122 |
123 | ChromeDriver.prototype.isRunning = function (callback) {
124 | const cb = false;
125 | got(this.statusUrl)
126 | .json()
127 | .then(({ value }) => callback(value && value.ready))
128 | .catch(() => callback(cb));
129 | };
130 |
131 | ChromeDriver.prototype.getLogs = function () {
132 | return this.logLines.slice();
133 | };
134 |
135 | ChromeDriver.prototype.clearLogs = function () {
136 | this.logLines = [];
137 | };
138 |
139 | module.exports = ChromeDriver;
140 |
--------------------------------------------------------------------------------
/test/accessibility-test.js:
--------------------------------------------------------------------------------
1 | const helpers = require('./global-setup');
2 | const path = require('path');
3 | const { expect } = require('chai');
4 | const assert = require('assert');
5 |
6 | const describe = global.describe;
7 | const it = global.it;
8 | const beforeEach = global.beforeEach;
9 | const afterEach = global.afterEach;
10 |
11 | describe('app.client.auditAccessibility()', function () {
12 | helpers.setupTimeout(this);
13 |
14 | let app = null;
15 |
16 | afterEach(function () {
17 | return helpers.stopApplication(app);
18 | });
19 |
20 | describe('when the audit passes', function () {
21 | beforeEach(function () {
22 | return helpers
23 | .startApplication({
24 | args: [path.join(__dirname, 'fixtures', 'accessible')]
25 | })
26 | .then(function (startedApp) {
27 | app = startedApp;
28 | });
29 | });
30 |
31 | it('resolves to an audit object with no results', async function () {
32 | await app.client.waitUntilWindowLoaded();
33 | const audit = await app.client.auditAccessibility();
34 | assert.strictEqual(audit.failed, false);
35 | expect(audit.results).to.have.length(0);
36 | expect(audit.message).to.equal('Accessibilty audit passed');
37 | });
38 | });
39 |
40 | describe('when the audit fails', function () {
41 | beforeEach(function () {
42 | return helpers
43 | .startApplication({
44 | args: [path.join(__dirname, 'fixtures', 'not-accessible')]
45 | })
46 | .then(function (startedApp) {
47 | app = startedApp;
48 | });
49 | });
50 |
51 | it('resolves to an audit object with the results', async function () {
52 | await app.client.waitUntilWindowLoaded();
53 | await app.client.windowByIndex(0);
54 | let audit = await app.client.auditAccessibility();
55 | assert.strictEqual(audit.failed, true);
56 | expect(audit.results).to.have.length(3);
57 |
58 | expect(audit.results[0].code).to.equal('AX_TEXT_01');
59 | expect(audit.results[0].elements).to.deep.equal(['INPUT']);
60 | expect(audit.results[0].severity).to.equal('Severe');
61 |
62 | expect(audit.results[1].code).to.equal('AX_HTML_01');
63 | expect(audit.results[1].elements).to.deep.equal(['html']);
64 | expect(audit.results[1].severity).to.equal('Warning');
65 |
66 | expect(audit.results[2].code).to.equal('AX_COLOR_01');
67 | expect(audit.results[2].elements).to.deep.equal(['DIV']);
68 | expect(audit.results[2].severity).to.equal('Warning');
69 | await app.client.windowByIndex(1);
70 | audit = await app.client.auditAccessibility();
71 | assert.strictEqual(audit.failed, true);
72 | expect(audit.results).to.have.length(1);
73 |
74 | expect(audit.results[0].code).to.equal('AX_ARIA_01');
75 | expect(audit.results[0].elements).to.deep.equal(['DIV']);
76 | expect(audit.results[0].severity).to.equal('Severe');
77 | await app.client.windowByIndex(0);
78 | });
79 |
80 | it('ignores warnings when ignoreWarnings is specified', async function () {
81 | await app.client.waitUntilWindowLoaded();
82 | const audit = await app.client.auditAccessibility({
83 | ignoreWarnings: true
84 | });
85 | assert.strictEqual(audit.failed, true);
86 | expect(audit.results).to.have.length(1);
87 |
88 | expect(audit.results[0].code).to.equal('AX_TEXT_01');
89 | expect(audit.results[0].elements).to.deep.equal(['INPUT']);
90 | expect(audit.results[0].severity).to.equal('Severe');
91 | });
92 |
93 | it('ignores rules when ignoreRules is specified', async function () {
94 | await app.client.waitUntilWindowLoaded();
95 | const audit = await app.client.auditAccessibility({
96 | ignoreRules: ['AX_TEXT_01', 'AX_HTML_01']
97 | });
98 | assert.strictEqual(audit.failed, true);
99 | expect(audit.results).to.have.length(1);
100 |
101 | expect(audit.results[0].code).to.equal('AX_COLOR_01');
102 | expect(audit.results[0].elements).to.deep.equal(['DIV']);
103 | expect(audit.results[0].severity).to.equal('Warning');
104 | });
105 | });
106 | });
107 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 5.0.0
2 |
3 | * Supports Electron `3.x.y` releases.
4 |
5 | # 4.0.0
6 |
7 | * Supports Electron `2.0.x` releases.
8 |
9 | # 3.8.0
10 |
11 | * Supports Electron `1.8.x` releases.
12 |
13 | # 3.7.3
14 |
15 | * Updated README with better examples and fixed typos.
16 | * Use launcher.bat instead of launcher.exe for Windows
17 | * Added `webdriverOptions` to allow specifying additional webdriver options.
18 |
19 | # 3.7.2
20 |
21 | * Added `webdriverLogPath` to the Spectron typescript definition.
22 | * Fixed typescript definition reference to the Electron API.
23 |
24 | # 3.7.1
25 |
26 | * Added a `chromeDriverArgs` option to the `Application` constructor that
27 | can be used to pass arguments directly to `chromedriver`.
28 |
29 | # 3.7.0
30 |
31 | * Supports Electron `1.7.x` releases.
32 |
33 | # 3.6.5
34 |
35 | * Updated typescript definition for WebDriver logging support.
36 | * Enhanced waitUntilTextExists() to find substrings in case the selector
37 | matches multiple elements.
38 |
39 | # 3.6.4
40 |
41 | * Added a `chromeDriverArgs` option to the `Application` constructor that
42 | can be used to pass arguments directly to `chromedriver`.
43 |
44 | # 3.6.3
45 |
46 | * Added a `spectron.d.ts` file referenced from the `types` field in the
47 | `package.json`.
48 | * Switched to using the WebdriverIO `timeouts` API instead of the
49 | deprecated `timeoutsAsyncScript` API.
50 |
51 | # 3.6.2
52 | * Reverted the console warning about incompatible minor versions since it
53 | caused launch issues.
54 |
55 | # 3.6.1
56 |
57 | * Display a warning in the console when the installed minor versions of
58 | Electron and Spectron do not match.
59 |
60 | # 3.6.0
61 |
62 | * Supports Electron `1.6.x` releases.
63 |
64 | # 3.5.0
65 |
66 | * Supports Electron `1.5.x` releases.
67 |
68 | # 3.4.1
69 |
70 | * Fixed an issue where an error would be thrown when the `process` global
71 | was set to `null`.
72 |
73 | # 3.4.0
74 |
75 | * Supports Electron `1.4.x` releases.
76 | * The `Application.isRunning()` API is now public.
77 | * Added an `Application.getSettings()` API to access the settings specified to
78 | the `Application` constructor.
79 | * Fixed an issue where `waitUntilWindowLoaded()` did not properly resolve when
80 | the `webContents` reported as loaded.
81 | * Fixed an issue where `waitUntilTextExists()` did not properly reject when
82 | the element did not exist or a timeout occurred.
83 | * Fixed an issue where a broken pipe error could occur in certain apps.
84 |
85 | # 3.3.0
86 |
87 | * Supports Electron `1.3.x` releases.
88 |
89 | # 3.2.6
90 | * Add `ignoreRules` option to `app.client.auditAccessibility(options)`. See
91 | README for usage details.
92 |
93 | # 3.2.5
94 |
95 | * Add `app.client.auditAccessibility` API that audits the window for
96 | accessibility issues. See README for usage details.
97 |
98 | # 3.2.3
99 |
100 | * Add `chromeDriverLogPath` option to `Application`. See README for usage
101 | details.
102 |
103 | # 3.2.2
104 |
105 | * Add `debuggerAddress` option to `Application`. See README for usage details.
106 |
107 | # 3.2.0
108 |
109 | * Supports Electron `1.2.x` releases.
110 |
111 | # 3.1.3
112 |
113 | * Improve detection of Node integration inside application and gracefully
114 | handle disabled Node integration.
115 |
116 | # 3.1.2
117 |
118 | * Add support for the async `WebContents.savePage` API. See the README
119 | for usage details.
120 |
121 | # 3.1.1
122 |
123 | * Add support for the async `BrowserWindow.capturePage` API. See the README
124 | for usage details.
125 |
126 | # 3.1.0
127 |
128 | * Supports Electron `1.1.x` releases.
129 |
130 | # 3.0.1
131 |
132 | * Added a new `requireName` option to `Application` for if your app is
133 | re-assigning the `require` function to a different name on `window`.
134 | * Fixed an issue where applications setting `nodeIntegration` to `false` could
135 | not be tested.
136 |
137 | # 3.0.0
138 |
139 | * Spectron now runs with `electron-chromedriver` 1.0 and is targeted at
140 | apps using Electron 1.0 or later.
141 | * No API changes.
142 |
143 | # 2.37.0
144 |
145 | * Added a `restart()` method to `Application`
146 | * Added support for the full Electron API
147 | * Many custom helpers have been removed in favor of accessing the Electron
148 | APIs directly through the new properties on the `Application` object.
149 | * `app.client.getWindowBounds()` should now be `app.browserWindow.getBounds()`
150 | * `app.client.getClipboardText()` should now be `app.electron.clipboard.readText()`
151 | * See the README or https://github.com/kevinsawicki/spectron/pull/18 for
152 | more details.
153 | * You should now use `app.transferPromiseness` instead of `app.client.transferPromiseness`
154 | to ensure these new properties are correctly transferred to chained promises.
155 |
156 | # 1.37.1
157 |
158 | * Add the `getAppPath(name)` that maps to the
159 | `require('electron').app.getPath(name)` API.
160 |
161 | # 1.37.0
162 |
163 | * Upgraded to WebdriverIO 4.0.4
164 | * Added a `connectionRetryCount` config option to `Application` that sets the
165 | default number of connection retries to make to ChromeDriver.
166 | * Added a `connectionRetryTimeout` config option to `Application` that sets
167 | the default number of milliseconds to wait when connecting to ChromeDriver.
168 |
169 | # 0.36.1
170 |
171 | * Added a `cwd` config option to `Application` that sets the working
172 | directory of the launched application. This option defaults to
173 | `process.cwd()`.
174 | * Added a `getCwd` command helper to get the current working directory of the
175 | main process.
176 |
177 | # 0.35.5
178 |
179 | * Added a `startTimeout` config option to `Application` that sets the default
180 | millisecond timeout to wait for ChromeDriver to start up. This option
181 | defaults to 5 seconds.
182 | * Added a `nodePath` config option to `Application` that sets the path to a
183 | `node` executable that will be used to launch the ChromeDriver startup
184 | script.
185 |
186 | # 0.35.4
187 |
188 | * Added `getMainProcessGlobal` command helper to get a global from the main
189 | process.
190 |
191 | # 0.35.2
192 |
193 | * Remove use of deprecated Electron APIs.
194 |
195 | # 0.35.1
196 |
197 | * Added `getMainProcessLogs` command helpers to get main process logs.
198 | * Added `getRenderProcessLogs` command helpers to get render process logs.
199 |
200 | # 0.34.1
201 |
202 | * Added a `waitTimeout` config option to `Application` that sets the default
203 | millisecond timeout for all wait-based command helpers like `waitUntil`,
204 | `waitUntilWindowLoaded`, etc. This option defaults to 5 seconds.
205 | * Added a `windowByIndex(index)` command helper that focuses a window by
206 | index in the `windowHandles()` array order.
207 | * Added `setRepresentedFilename` and `getRepresentedFilename` command helpers.
208 | * Added `isDocumentEdited` and `setDocumentEdited` command helpers.
209 | * `setWindowDimensions` was renamed to `setWindowBounds` to mirror the new
210 | Electron `BrowserWindow.setBounds` API. It also takes a `bounds` object
211 | argument instead of multiple arguments for size and position. See the
212 | `README` for an example
213 | * `getWindowDimensions` was renamed to `getWindowBounds` to mirror the new
214 | Electron `BrowserWindow.getBounds` API. See the `README` for an example.
215 |
--------------------------------------------------------------------------------
/lib/application.js:
--------------------------------------------------------------------------------
1 | const Accessibility = require('./accessibility');
2 | const Api = require('./api');
3 | const ChromeDriver = require('./chrome-driver');
4 | const DevNull = require('dev-null');
5 | const fs = require('fs');
6 | const path = require('path');
7 | const WebDriver = require('webdriverio');
8 |
9 | function Application(options) {
10 | options = options || {};
11 | this.host = options.host || '127.0.0.1';
12 | this.port = parseInt(options.port, 10) || 9515;
13 |
14 | this.quitTimeout = parseInt(options.quitTimeout, 10) || 1000;
15 | this.startTimeout = parseInt(options.startTimeout, 10) || 5000;
16 | this.waitTimeout = parseInt(options.waitTimeout, 10) || 5000;
17 |
18 | this.connectionRetryCount = parseInt(options.connectionRetryCount, 10) || 10;
19 | this.connectionRetryTimeout =
20 | parseInt(options.connectionRetryTimeout, 10) || 30000;
21 |
22 | this.nodePath = options.nodePath || process.execPath;
23 | this.path = options.path;
24 |
25 | this.args = options.args || [];
26 | this.chromeDriverArgs = options.chromeDriverArgs || [];
27 | this.env = options.env || {};
28 | this.workingDirectory = options.cwd || process.cwd();
29 | this.debuggerAddress = options.debuggerAddress;
30 | this.chromeDriverLogPath = options.chromeDriverLogPath;
31 | this.webdriverLogPath = options.webdriverLogPath;
32 | this.webdriverOptions = options.webdriverOptions || {};
33 | this.requireName = options.requireName || 'require';
34 |
35 | this.api = new Api(this, this.requireName);
36 | this.setupPromiseness();
37 | }
38 |
39 | Application.prototype.setupPromiseness = function () {
40 | const self = this;
41 | self.transferPromiseness = function (target, promise) {
42 | self.api.transferPromiseness(target, promise);
43 | };
44 | };
45 |
46 | Application.prototype.start = function () {
47 | const self = this;
48 | return self
49 | .exists()
50 | .then(function () {
51 | return self.startChromeDriver();
52 | })
53 | .then(function () {
54 | return self.createClient();
55 | })
56 | .then(function () {
57 | return self.api.initialize();
58 | })
59 | .then(function () {
60 | return self.client.setTimeouts(
61 | self.waitTimeout,
62 | self.waitTimeout,
63 | self.waitTimeout
64 | );
65 | })
66 | .then(function () {
67 | self.running = true;
68 | })
69 | .then(function () {
70 | return self;
71 | });
72 | };
73 |
74 | Application.prototype.stop = function () {
75 | const self = this;
76 |
77 | if (!self.isRunning()) {
78 | return Promise.reject(Error('Application not running'));
79 | }
80 |
81 | return new Promise(function (resolve, reject) {
82 | const endClient = function () {
83 | setTimeout(function () {
84 | self.chromeDriver.stop();
85 | self.running = false;
86 | resolve(self);
87 | }, self.quitTimeout);
88 | };
89 |
90 | if (self.api.nodeIntegration) {
91 | self.client.electron.remote.app.quit().then(endClient, reject);
92 | } else {
93 | self.client
94 | .windowByIndex(0)
95 | .then(function () {
96 | self.client.closeWindow();
97 | })
98 | .then(endClient, reject);
99 | }
100 | });
101 | };
102 |
103 | Application.prototype.restart = function () {
104 | const self = this;
105 | return self.stop().then(function () {
106 | return self.start();
107 | });
108 | };
109 |
110 | Application.prototype.isRunning = function () {
111 | return this.running;
112 | };
113 |
114 | Application.prototype.getSettings = function () {
115 | return {
116 | host: this.host,
117 | port: this.port,
118 | quitTimeout: this.quitTimeout,
119 | startTimeout: this.startTimeout,
120 | waitTimeout: this.waitTimeout,
121 | connectionRetryCount: this.connectionRetryCount,
122 | connectionRetryTimeout: this.connectionRetryTimeout,
123 | nodePath: this.nodePath,
124 | path: this.path,
125 | args: this.args,
126 | chromeDriverArgs: this.chromeDriverArgs,
127 | env: this.env,
128 | workingDirectory: this.workingDirectory,
129 | debuggerAddress: this.debuggerAddress,
130 | chromeDriverLogPath: this.chromeDriverLogPath,
131 | webdriverLogPath: this.webdriverLogPath,
132 | webdriverOptions: this.webdriverOptions,
133 | requireName: this.requireName
134 | };
135 | };
136 |
137 | Application.prototype.exists = function () {
138 | const self = this;
139 | return new Promise(function (resolve, reject) {
140 | // Binary path is ignored by ChromeDriver if debuggerAddress is set
141 | if (self.debuggerAddress) return resolve();
142 |
143 | if (typeof self.path !== 'string') {
144 | return reject(Error('Application path must be a string'));
145 | }
146 |
147 | fs.stat(self.path, function (error, stat) {
148 | if (error) return reject(error);
149 | if (stat.isFile()) return resolve();
150 | reject(Error('Application path specified is not a file: ' + self.path));
151 | });
152 | });
153 | };
154 |
155 | Application.prototype.startChromeDriver = function () {
156 | this.chromeDriver = new ChromeDriver(
157 | this.host,
158 | this.port,
159 | this.nodePath,
160 | this.startTimeout,
161 | this.workingDirectory,
162 | this.chromeDriverLogPath
163 | );
164 | return this.chromeDriver.start();
165 | };
166 |
167 | Application.prototype.createClient = function () {
168 | const self = this;
169 | return new Promise(function (resolve, reject) {
170 | const args = [];
171 | args.push('spectron-path=' + self.path);
172 | self.args.forEach(function (arg, index) {
173 | args.push('spectron-arg' + index + '=' + arg);
174 | });
175 |
176 | for (const name in self.env) {
177 | if (Object.prototype.hasOwnProperty.call(self.env, name)) {
178 | args.push('spectron-env-' + name + '=' + self.env[name]);
179 | }
180 | }
181 |
182 | self.chromeDriverArgs.forEach(function (arg) {
183 | args.push(arg);
184 | });
185 |
186 | const isWin = process.platform === 'win32';
187 | const launcherPath = path.join(
188 | __dirname,
189 | isWin ? 'launcher.bat' : 'launcher.js'
190 | );
191 |
192 | if (process.env.APPVEYOR) {
193 | args.push('no-sandbox');
194 | }
195 |
196 | const options = {
197 | hostname: self.host,
198 | port: self.port,
199 | waitforTimeout: self.waitTimeout,
200 | connectionRetryCount: self.connectionRetryCount,
201 | connectionRetryTimeout: self.connectionRetryTimeout,
202 | logLevel: 'silent',
203 | capabilities: {
204 | 'goog:chromeOptions': {
205 | binary: launcherPath,
206 | args: args,
207 | debuggerAddress: self.debuggerAddress,
208 | windowTypes: ['app', 'webview']
209 | }
210 | },
211 | logOutput: DevNull()
212 | };
213 |
214 | if (self.webdriverLogPath) {
215 | options.outputDir = self.webdriverLogPath;
216 | options.logLevel = 'trace';
217 | }
218 |
219 | Object.assign(options, self.webdriverOptions);
220 |
221 | self.client = WebDriver.remote(options).then(function (remote) {
222 | self.client = remote;
223 | self.addCommands();
224 | resolve();
225 | }, reject);
226 | });
227 | };
228 |
229 | Application.prototype.addCommands = function () {
230 | this.client.addCommand(
231 | 'waitUntilTextExists',
232 | function (selector, text, timeout) {
233 | const self = this;
234 | return self
235 | .waitUntil(async function () {
236 | const elem = await self.$(selector);
237 | const exists = await elem.isExisting();
238 | if (!exists) {
239 | return false;
240 | }
241 |
242 | const selectorText = await elem.getText();
243 | return Array.isArray(selectorText)
244 | ? selectorText.some((s) => s.includes(text))
245 | : selectorText.includes(text);
246 | }, timeout)
247 | .then(
248 | function () {},
249 | function (error) {
250 | error.message = 'waitUntilTextExists ' + error.message;
251 | throw error;
252 | }
253 | );
254 | }
255 | );
256 |
257 | this.client.addCommand('waitUntilWindowLoaded', function (timeout) {
258 | const self = this;
259 | return self
260 | .waitUntil(function () {
261 | return self.webContents.isLoading().then(function (loading) {
262 | return !loading;
263 | });
264 | }, timeout)
265 | .then(
266 | function () {},
267 | function (error) {
268 | error.message = 'waitUntilWindowLoaded ' + error.message;
269 | throw error;
270 | }
271 | );
272 | });
273 |
274 | this.client.addCommand('getWindowCount', function () {
275 | return this.getWindowHandles().then(function (handles) {
276 | return handles.length;
277 | });
278 | });
279 |
280 | this.client.addCommand('windowByIndex', function (index) {
281 | const self = this;
282 | return self.getWindowHandles().then(function (handles) {
283 | return self.switchToWindow(handles[index]);
284 | });
285 | });
286 |
287 | this.client.addCommand('getSelectedText', function () {
288 | return this.execute(function () {
289 | return window.getSelection().toString();
290 | });
291 | });
292 |
293 | this.client.addCommand('getRenderProcessLogs', function () {
294 | return this.getLogs('browser');
295 | });
296 |
297 | const self = this;
298 | this.client.addCommand('getMainProcessLogs', function () {
299 | const logs = self.chromeDriver.getLogs();
300 | self.chromeDriver.clearLogs();
301 | return logs;
302 | });
303 |
304 | Accessibility.addCommand(this.client, this.requireName);
305 | };
306 |
307 | module.exports = Application;
308 |
--------------------------------------------------------------------------------
/test/commands-test.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const helpers = require('./global-setup');
3 | const path = require('path');
4 | const { expect } = require('chai');
5 | const temp = require('temp').track();
6 |
7 | const describe = global.describe;
8 | const it = global.it;
9 | const before = global.before;
10 | const after = global.after;
11 |
12 | describe('window commands', function () {
13 | helpers.setupTimeout(this);
14 |
15 | let app = null;
16 |
17 | before(function () {
18 | return helpers
19 | .startApplication({
20 | args: [path.join(__dirname, 'fixtures', 'app')]
21 | })
22 | .then(function (startedApp) {
23 | app = startedApp;
24 | });
25 | });
26 |
27 | after(function () {
28 | return helpers.stopApplication(app);
29 | });
30 |
31 | describe('getWindowCount', function () {
32 | it('retrieves the window count', function () {
33 | return app.client.getWindowCount().should.eventually.equal(1);
34 | });
35 | });
36 |
37 | describe('waitUntilTextExists', function () {
38 | it('resolves if the element (single occurrence) contains the given text - full text', async function () {
39 | await app.client.waitUntilTextExists('.occurrences-1', 'word1 word2');
40 | });
41 |
42 | it('resolves if the element (single occurrence) contains the given text - partial text', async function () {
43 | await app.client.waitUntilTextExists('.occurrences-1', 'word1');
44 | });
45 |
46 | it('resolves if the element (multiple occurrences) contains the given text - full text', async function () {
47 | await app.client.waitUntilTextExists('.occurrences-2', 'word3 word4');
48 | });
49 |
50 | it('resolves if the element (multiple occurrences) contains the given text - partial text', async function () {
51 | await app.client.waitUntilTextExists('.occurrences-2', 'word3');
52 | });
53 |
54 | it('rejects if the element is missing', async function () {
55 | await expect(
56 | app.client.waitUntilTextExists('#not-in-page', 'Hello', 50)
57 | ).to.be.rejectedWith(Error);
58 | });
59 |
60 | it('rejects if the element never contains the text', async function () {
61 | await expect(
62 | app.client.waitUntilTextExists('html', 'not on page', 50)
63 | ).to.be.rejectedWith(Error);
64 | });
65 | });
66 |
67 | describe('browserWindow.getBounds()', function () {
68 | it('gets the window bounds', function () {
69 | return app.browserWindow
70 | .getBounds()
71 | .should.eventually.roughly(5)
72 | .deep.equal({
73 | x: 25,
74 | y: 35,
75 | width: 200,
76 | height: 100
77 | });
78 | });
79 | });
80 |
81 | describe('browserWindow.setBounds()', function () {
82 | it('sets the window bounds', async function () {
83 | await app.browserWindow.setBounds({
84 | x: 100,
85 | y: 200,
86 | width: 150, // Windows minimum is ~100px
87 | height: 130
88 | });
89 | await app.browserWindow
90 | .getBounds()
91 | .should.eventually.roughly(5)
92 | .deep.equal({
93 | x: 100,
94 | y: 200,
95 | width: 150,
96 | height: 130
97 | });
98 | });
99 | });
100 |
101 | describe('browserWindow.isFocused()', function () {
102 | it('returns true when the current window is focused', async function () {
103 | await app.browserWindow.show();
104 | const focused = await app.browserWindow.isFocused();
105 | return expect(focused).to.be.true;
106 | });
107 | });
108 |
109 | describe('browserWindow.isVisible()', function () {
110 | it('returns true when the window is visible, false otherwise', async function () {
111 | await app.browserWindow.hide();
112 | const isInvisible = await app.browserWindow.isVisible();
113 | await app.browserWindow.show();
114 | const isVisible = await app.browserWindow.isVisible();
115 | return expect(isVisible).to.be.true && expect(isInvisible).to.be.false;
116 | });
117 | });
118 |
119 | describe('browserWindow.isDevToolsOpened()', function () {
120 | it('returns false when the dev tools are closed', function () {
121 | return app.browserWindow.isDevToolsOpened().should.eventually.be.false;
122 | });
123 | });
124 |
125 | describe('browserWindow.isFullScreen()', function () {
126 | it('returns false when the window is not in full screen mode', function () {
127 | return app.client.browserWindow.isFullScreen().should.eventually.be.false;
128 | });
129 | });
130 |
131 | describe('waitUntilWindowLoaded()', function () {
132 | it('waits until the current window is loaded', async function () {
133 | await app.client.waitUntilWindowLoaded();
134 | return app.webContents.isLoading().should.eventually.be.false;
135 | });
136 | });
137 |
138 | describe('browserWindow.isMaximized()', function () {
139 | it('returns true when the window is maximized, false otherwise', async function () {
140 | const notMaximized = await app.browserWindow.isMaximized();
141 | expect(notMaximized).to.equal(false);
142 | await app.browserWindow.maximize();
143 | let maximized = await app.browserWindow.isMaximized();
144 | if (process.env.CI) {
145 | // FIXME window maximized state is never true on CI
146 | maximized = true;
147 | }
148 | expect(maximized).to.equal(true);
149 | });
150 | });
151 |
152 | describe('browserWindow.isMinimized()', function () {
153 | it('returns true when the window is minimized, false otherwise', async function () {
154 | expect(await app.browserWindow.isMinimized()).to.equal(false);
155 |
156 | await app.browserWindow.minimize();
157 | if (!process.env.CI) {
158 | await app.client.waitUntil(() => app.browserWindow.isMinimized(), {
159 | timeout: 2000
160 | });
161 | }
162 | });
163 | });
164 |
165 | describe('webContents.selectAll()', function () {
166 | it('selects all the text on the page', async function () {
167 | await app.client.waitUntilTextExists('html', 'Hello');
168 | let text = await app.client.getSelectedText();
169 | expect(text).to.equal('');
170 | app.client.webContents.selectAll();
171 | text = await app.client.getSelectedText();
172 | expect(text).to.contain('Hello');
173 | });
174 | });
175 |
176 | describe('webContents.paste()', function () {
177 | it('pastes the text into the focused element', async function () {
178 | const elem = await app.client.$('textarea');
179 | const text = await elem.getText();
180 | expect(text).to.equal('');
181 | app.electron.clipboard.writeText('pasta');
182 | await app.electron.clipboard.readText().should.eventually.equal('pasta');
183 | await elem.click();
184 | await app.webContents.paste();
185 | const value = await elem.getValue();
186 | return expect(value).to.equal('pasta');
187 | });
188 | });
189 |
190 | describe('browserWindow.isDocumentEdited()', function () {
191 | it('returns true when the document is edited', async function () {
192 | if (process.platform !== 'darwin') return;
193 |
194 | const notEdited = await app.browserWindow.isDocumentEdited();
195 | expect(notEdited).to.equal(false);
196 | app.browserWindow.setDocumentEdited(true);
197 | return app.browserWindow.isDocumentEdited().should.eventually.be.true;
198 | });
199 | });
200 |
201 | describe('browserWindow.getRepresentedFilename()', function () {
202 | it('returns the represented filename', async function () {
203 | if (process.platform !== 'darwin') return;
204 |
205 | let filename = await app.browserWindow.getRepresentedFilename();
206 | expect(filename).to.equal('');
207 | await app.browserWindow.setRepresentedFilename('/foo.js');
208 | filename = await app.browserWindow.getRepresentedFilename();
209 | return expect(filename).to.equal('/foo.js');
210 | });
211 | });
212 |
213 | describe('electron.remote.app.getPath()', function () {
214 | it('returns the path for the given name', async function () {
215 | const tempDir = fs.realpathSync(temp.dir);
216 | await app.electron.remote.app.setPath('music', tempDir);
217 | return app.electron.remote.app
218 | .getPath('music')
219 | .should.eventually.equal(tempDir);
220 | });
221 | });
222 |
223 | it('exposes properties on constructor APIs', async function () {
224 | await app.electron.remote.MenuItem.types().should.eventually.include(
225 | 'normal'
226 | );
227 | });
228 |
229 | describe('globalShortcut.isRegistered()', function () {
230 | it('returns false if the shortcut is not registered', function () {
231 | return app.electron.remote.globalShortcut.isRegistered(
232 | 'CommandOrControl+X'
233 | ).should.eventually.be.false;
234 | });
235 | });
236 |
237 | describe('rendererProcess.versions', function () {
238 | it('includes the Electron version', function () {
239 | return app.rendererProcess
240 | .versions()
241 | .should.eventually.have.property('electron').and.not.be.empty;
242 | });
243 | });
244 |
245 | describe('electron.screen.getPrimaryDisplay()', function () {
246 | it('returns information about the primary display', function () {
247 | return app.electron.remote.screen
248 | .getPrimaryDisplay()
249 | .should.eventually.have.property('workArea').and.not.be.empty;
250 | });
251 | });
252 |
253 | describe('electron.webFrame.getZoomFactor()', function () {
254 | it('returns information about the primary display', async function () {
255 | await app.electron.webFrame.setZoomFactor(4);
256 | return app.electron.webFrame
257 | .getZoomFactor()
258 | .should.eventually.be.closeTo(4, 0.1);
259 | });
260 | });
261 | });
262 |
--------------------------------------------------------------------------------
/test/application-test.js:
--------------------------------------------------------------------------------
1 | const Application = require('..').Application;
2 | const assert = require('assert');
3 | const fs = require('fs');
4 | const helpers = require('./global-setup');
5 | const path = require('path');
6 | const temp = require('temp').track();
7 |
8 | const describe = global.describe;
9 | const it = global.it;
10 | const beforeEach = global.beforeEach;
11 | const afterEach = global.afterEach;
12 | const expect = require('chai').expect;
13 |
14 | describe('application loading', function () {
15 | helpers.setupTimeout(this);
16 |
17 | let app = null;
18 | let tempPath = null;
19 |
20 | beforeEach(function () {
21 | tempPath = temp.mkdirSync('spectron-temp-dir-');
22 |
23 | return helpers
24 | .startApplication({
25 | cwd: path.join(__dirname, 'fixtures'),
26 | args: [path.join(__dirname, 'fixtures', 'app'), '--foo', '--bar=baz'],
27 | env: {
28 | FOO: 'BAR',
29 | HELLO: 'WORLD',
30 | SPECTRON_TEMP_DIR: tempPath
31 | }
32 | })
33 | .then(function (startedApp) {
34 | app = startedApp;
35 | });
36 | });
37 |
38 | afterEach(function () {
39 | return helpers.stopApplication(app);
40 | });
41 |
42 | it('launches the application', async function () {
43 | const response = await app.client.getWindowHandles();
44 | assert.strictEqual(response.length, 1);
45 |
46 | await app.browserWindow
47 | .getBounds()
48 | .should.eventually.roughly(5)
49 | .deep.equal({
50 | x: 25,
51 | y: 35,
52 | width: 200,
53 | height: 100
54 | });
55 | await app.client.waitUntilTextExists('html', 'Hello');
56 | await app.client.getTitle().should.eventually.equal('Test');
57 | });
58 |
59 | it('passes through args to the launched app', async function () {
60 | const arvg = app.mainProcess.argv();
61 | await arvg.should.eventually.contain('--foo');
62 | await arvg.should.eventually.contain('--bar=baz');
63 | });
64 |
65 | it('passes through env to the launched app', async function () {
66 | const env = await app.rendererProcess.env();
67 | if (process.platform === 'win32') {
68 | assert.strictEqual(env.foo, 'BAR');
69 | assert.strictEqual(env.hello, 'WORLD');
70 | } else {
71 | assert.strictEqual(env.FOO, 'BAR');
72 | assert.strictEqual(env.HELLO, 'WORLD');
73 | }
74 | });
75 |
76 | it('passes through cwd to the launched app', async function () {
77 | const cwd = app.mainProcess.cwd();
78 | await cwd.should.eventually.equal(path.join(__dirname, 'fixtures'));
79 | });
80 |
81 | it('throws an error when no path is specified', function () {
82 | if (process.platform === 'win32') return;
83 | return new Application()
84 | .start()
85 | .should.be.rejectedWith(Error, 'Application path must be a string');
86 | });
87 |
88 | describe('start()', function () {
89 | it('rejects with an error if the application does not exist', function () {
90 | return new Application({ path: path.join(__dirname, 'invalid') })
91 | .start()
92 | .should.be.rejectedWith(Error);
93 | });
94 |
95 | it('rejects with an error if ChromeDriver does not start within the specified timeout', function () {
96 | return new Application({
97 | path: helpers.getElectronPath(),
98 | host: 'bad.host',
99 | startTimeout: 150
100 | })
101 | .start()
102 | .should.be.rejectedWith(
103 | Error,
104 | 'ChromeDriver did not start within 150ms'
105 | );
106 | });
107 | });
108 |
109 | describe('stop()', function () {
110 | it('quits the application', async function () {
111 | const quitPath = path.join(tempPath, 'quit.txt');
112 | assert.strictEqual(fs.existsSync(quitPath), false);
113 | const stoppedApp = await app.stop();
114 | assert.strictEqual(stoppedApp, app);
115 | assert.strictEqual(fs.existsSync(quitPath), true);
116 | assert.strictEqual(app.isRunning(), false);
117 | });
118 |
119 | it('rejects with an error if the application is not running', async function () {
120 | await app.stop();
121 | await expect(app.stop()).to.be.rejectedWith(Error);
122 | });
123 | });
124 |
125 | describe('restart()', function () {
126 | it('restarts the application', async function () {
127 | const quitPath = path.join(tempPath, 'quit.txt');
128 | assert.strictEqual(fs.existsSync(quitPath), false);
129 | const restartedApp = await app.restart();
130 | assert.strictEqual(restartedApp, app);
131 | assert.strictEqual(fs.existsSync(quitPath), true);
132 | assert.strictEqual(app.isRunning(), true);
133 | });
134 |
135 | it('rejects with an error if the application is not running', async function () {
136 | await app.stop();
137 | await expect(app.restart()).to.be.rejectedWith(Error);
138 | });
139 | });
140 |
141 | describe('getSettings()', function () {
142 | it('returns an object with all the configured options', function () {
143 | expect(app.getSettings().port).to.equal(9515);
144 | expect(app.getSettings().quitTimeout).to.equal(1000);
145 | expect(app.getSettings().env.SPECTRON_TEMP_DIR).to.equal(tempPath);
146 | });
147 | });
148 |
149 | describe('getRenderProcessLogs', function () {
150 | it('gets the render process console logs and clears them', async function () {
151 | await app.client.waitUntilWindowLoaded();
152 | let logs = await app.client.getRenderProcessLogs();
153 | expect(logs.length).to.equal(2);
154 | expect(logs[0].message).to.contain('7:14 "render warn"');
155 | expect(logs[0].source).to.equal('console-api');
156 | expect(logs[0].level).to.equal('WARNING');
157 |
158 | expect(logs[1].message).to.contain('8:14 "render error"');
159 | expect(logs[1].source).to.equal('console-api');
160 | expect(logs[1].level).to.equal('SEVERE');
161 | logs = await app.client.getRenderProcessLogs();
162 | expect(logs.length).to.equal(0);
163 | });
164 | });
165 |
166 | describe('getMainProcessLogs', function () {
167 | it('gets the main process console logs and clears them', async function () {
168 | await app.client.waitUntilWindowLoaded();
169 | let logs = await app.client.getMainProcessLogs();
170 | expect(logs).to.contain('main log');
171 | expect(logs).to.contain('main warn');
172 | expect(logs).to.contain('main error');
173 | logs = await app.client.getMainProcessLogs();
174 | expect(logs.length).to.equal(0);
175 | });
176 |
177 | // TODO (jkleinsc) - enable this test once spectron is rewritten to not use remote
178 | it.skip('does not include any deprecation warnings', async function () {
179 | await app.client.waitUntilWindowLoaded();
180 | const logs = await app.client.getMainProcessLogs();
181 | logs.forEach(function (log) {
182 | expect(log).not.to.contain('(electron)');
183 | });
184 | });
185 |
186 | it('clears the logs when the application is stopped', async function () {
187 | await app.stop();
188 | expect(app.chromeDriver.getLogs().length).to.equal(0);
189 | });
190 | });
191 |
192 | describe('electron.remote.getGlobal', function () {
193 | it('returns the requested global from the main process', async function () {
194 | const val = await app.electron.remote.getGlobal('mainProcessGlobal');
195 | val.should.equal('foo');
196 | });
197 | });
198 |
199 | describe('browserWindow.capturePage', function () {
200 | it('returns a Buffer screenshot of the given rectangle', async function () {
201 | const buffer = await app.browserWindow.capturePage({
202 | x: 0,
203 | y: 0,
204 | width: 10,
205 | height: 10
206 | });
207 | expect(buffer).to.be.an.instanceof(Buffer);
208 | expect(buffer.length).to.be.above(0);
209 | });
210 |
211 | it('returns a Buffer screenshot of the entire page when no rectangle is specified', async function () {
212 | const buffer = await app.browserWindow.capturePage();
213 | expect(buffer).to.be.an.instanceof(Buffer);
214 | expect(buffer.length).to.be.above(0);
215 | });
216 | });
217 |
218 | describe('webContents.savePage', function () {
219 | it('saves the page to the specified path', function () {
220 | const filePath = path.join(tempPath, 'page.html');
221 | return app.webContents
222 | .savePage(filePath, 'HTMLComplete')
223 | .then(function () {
224 | const html = fs.readFileSync(filePath, 'utf8');
225 | expect(html).to.contain('Test');
226 | expect(html).to.contain('Hello');
227 | });
228 | });
229 |
230 | it('throws an error when the specified path is invalid', async function () {
231 | await expect(
232 | app.webContents.savePage(tempPath, 'MHTMLfds')
233 | ).to.be.rejectedWith(Error);
234 | });
235 | });
236 |
237 | describe('webContents.executeJavaScript', function () {
238 | it('executes the given script and returns the result of its last statement (sync)', async function () {
239 | const result = await app.webContents.executeJavaScript('1 + 2');
240 | expect(result).to.equal(3);
241 | });
242 |
243 | it('executes the given script and returns the result of its last statement (async)', async function () {
244 | const result = await app.webContents.executeJavaScript(`
245 | new Promise(function(resolve){
246 | setTimeout(function(){
247 | resolve("ok")
248 | }, 1000)
249 | })`);
250 | expect(result).to.equal('ok');
251 | });
252 | });
253 |
254 | describe('electron.ipcRenderer.send', function () {
255 | it('sends the message to the main process', async function () {
256 | let ipcCount = await app.electron.remote.getGlobal('ipcEventCount');
257 | expect(ipcCount).to.equal(0);
258 | await app.electron.ipcRenderer.send('ipc-event', 123);
259 | ipcCount = await app.electron.remote.getGlobal('ipcEventCount');
260 | expect(ipcCount).to.equal(123);
261 | await app.electron.ipcRenderer.send('ipc-event', 456);
262 | ipcCount = await app.electron.remote.getGlobal('ipcEventCount');
263 | expect(ipcCount).to.equal(579);
264 | });
265 | });
266 |
267 | describe('webContents.sendInputEvent', function () {
268 | it('triggers a keypress DOM event', async function () {
269 | await app.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'A' });
270 | const elem = await app.client.$('.keypress-count');
271 | let text = await elem.getText();
272 | expect(text).to.equal('A');
273 | await app.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'B' });
274 | text = await elem.getText();
275 | expect(text).to.equal('B');
276 | });
277 | });
278 | });
279 |
--------------------------------------------------------------------------------
/lib/spectron.d.ts:
--------------------------------------------------------------------------------
1 | // Type definitions for spectron v3.6.0
2 | // Project: https://github.com/electron/spectron
3 | // Definitions by: deerares
4 |
5 | ///
6 |
7 | declare module 'spectron' {
8 | import * as Electron from 'electron';
9 | import * as WebdriverIO from 'webdriverio';
10 |
11 | interface AccessibilityAuditOptions {
12 | /**
13 | * true to ignore failures with a severity of 'Warning' and only
14 | * include failures with a severity of 'Severe'. Defaults to false.
15 | */
16 | ignoreWarnings?: boolean;
17 |
18 | /**
19 | * Rule code values such as AX_COLOR_01 to ignore failures for.
20 | */
21 | ignoreRules?: string[];
22 | }
23 |
24 | interface AccessibilityAuditResult {
25 | /**
26 | * False when the audit has failures
27 | */
28 | failed: boolean;
29 |
30 | /**
31 | * A detailed message about the results
32 | */
33 | message: string;
34 |
35 | /**
36 | * An array of detail objects for each failed rule
37 | */
38 | results: {
39 | /**
40 | * A unique accessibility rule identifier
41 | */
42 | code: string;
43 |
44 | /**
45 | * Selector path of each HTML element that failed the rule
46 | */
47 | elements: string[];
48 |
49 | /**
50 | * A String message about the failed rule
51 | */
52 | message: string;
53 |
54 | /**
55 | * 'Warning' or 'Severe'
56 | */
57 | severity: 'Warning' | 'Severe';
58 |
59 | /**
60 | * URL providing more details about the failed rule
61 | */
62 | url: string;
63 | }[];
64 | }
65 |
66 | export interface SpectronClient extends WebdriverIO.Browser<'async'> {
67 | /**
68 | * Wait until the window is no longer loading.
69 | * Takes an optional timeout in milliseconds that defaults to 5000.
70 | */
71 | waitUntilWindowLoaded(timeout?: number): Promise;
72 |
73 | /**
74 | * Wait until the element matching the given selector contains the given text.
75 | * Takes an optional timeout in milliseconds that defaults to 5000.
76 | */
77 | waitUntilTextExists(
78 | selector: string,
79 | text: string,
80 | timeout?: number
81 | ): Promise;
82 |
83 | /**
84 | * Gets the number of open windows. tags are also counted as separate windows.
85 | */
86 | getWindowCount(): Promise;
87 | /**
88 | * Focus a window using its index from the windowHandles() array.
89 | * tags can also be focused as a separate window.
90 | */
91 | windowByIndex(index: number): Promise;
92 | /**
93 | * Get the selected text in the current window.
94 | */
95 | getSelectedText(): Promise;
96 | /**
97 | * Gets the console log output from the render process.
98 | * The logs are cleared after they are returned.
99 | */
100 | getRenderProcessLogs(): Promise