├── .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; 101 | /** 102 | * Gets the console log output from the main process. 103 | * The logs are cleared after they are returned. 104 | */ 105 | getMainProcessLogs(): Promise; 106 | 107 | /** 108 | * Run an accessibility audit in the focused window with the specified options. 109 | */ 110 | auditAccessibility( 111 | options?: AccessibilityAuditOptions 112 | ): Promise; 113 | } 114 | 115 | export type SpectronWindow = { 116 | [P in keyof Electron.BrowserWindow]: Electron.BrowserWindow[P] extends ( 117 | ...args: infer A 118 | ) => infer R 119 | ? (...args: A) => Promise 120 | : undefined; 121 | }; 122 | 123 | export interface SpectronWebContents extends Electron.WebContents { 124 | savePage( 125 | fullPath: string, 126 | saveType: 'HTMLOnly' | 'HTMLComplete' | 'MHTML', 127 | callback?: (error: Error) => void 128 | ): boolean; 129 | savePage( 130 | fullPath: string, 131 | saveType: 'HTMLOnly' | 'HTMLComplete' | 'MHTML' 132 | ): Promise; 133 | savePage( 134 | fullPath: string, 135 | saveType: 'HTMLOnly' | 'HTMLComplete' | 'MHTML' 136 | ): any; 137 | executeJavaScript(code: string, userGesture?: boolean): Promise; 138 | } 139 | 140 | type BasicAppSettings = { 141 | /** 142 | * String host name of the launched chromedriver process. Defaults to 'localhost'. 143 | */ 144 | host?: string; 145 | /** 146 | * Number port of the launched chromedriver process. Defaults to 9515. 147 | */ 148 | port?: number; 149 | /** 150 | * Number in milliseconds to wait for application quitting. 151 | * Defaults to 1000 milliseconds. 152 | */ 153 | quitTimeout?: number; 154 | /** 155 | * Number in milliseconds to wait for ChromeDriver to start. 156 | * Defaults to 5000 milliseconds. 157 | */ 158 | startTimeout?: number; 159 | /** 160 | * Number in milliseconds to wait for calls like waitUntilTextExists and 161 | * waitUntilWindowLoaded to complete. 162 | * Defaults to 5000 milliseconds. 163 | */ 164 | waitTimeout?: number; 165 | /** 166 | * Number of retry attempts to make when connecting to ChromeDriver. 167 | * Defaults to 10 attempts. 168 | */ 169 | connectionRetryCount?: number; 170 | /** 171 | * Number in milliseconds to wait for connections to ChromeDriver to be made. 172 | * Defaults to 30000 milliseconds. 173 | */ 174 | connectionRetryTimeout?: number; 175 | /** 176 | * String path to a node executable to launch ChromeDriver with. 177 | * Defaults to process.execPath. 178 | */ 179 | nodePath?: string; 180 | /** String path to the Electron application executable to launch. 181 | * Note: If you want to invoke electron directly with your app's main script then you should 182 | * specify path as electron via electron-prebuilt and specify your app's main script path as 183 | * the first argument in the args array. 184 | */ 185 | path: string; 186 | /** 187 | * Array of arguments to pass to the Electron application. 188 | */ 189 | args?: string[]; 190 | /** 191 | * Array of arguments to pass to ChromeDriver. 192 | * See here (https://sites.google.com/chromium.org/driver/capabilities) for details 193 | * on the Chrome arguments. 194 | */ 195 | chromeDriverArgs?: string[]; 196 | /** 197 | * Object of additional environment variables to set in the launched application. 198 | */ 199 | env?: object; 200 | /** 201 | * String address of a Chrome debugger server to connect to. 202 | */ 203 | debuggerAddress?: string; 204 | /** 205 | * String path to file to store ChromeDriver logs in. 206 | * Setting this option enables --verbose logging when starting ChromeDriver. 207 | */ 208 | chromeDriverLogPath?: string; 209 | /** 210 | * String path to a directory where Webdriver will write logs to. 211 | * Setting this option enables verbose logging from Webdriver. 212 | */ 213 | webdriverLogPath?: string; 214 | /** 215 | * Extra Webdriver options 216 | */ 217 | webdriverOptions?: object; 218 | /** 219 | * Custom property name to use when requiring modules. 220 | * Defaults to require. 221 | * This should only be used if your application deletes the main window.require function 222 | * and assigns it to another property name on window. 223 | */ 224 | requireName?: string; 225 | }; 226 | type AppConstructorOptions = BasicAppSettings & { 227 | /** 228 | * String path to the working directory to use for the launched application. 229 | * Defaults to process.cwd(). 230 | */ 231 | cwd?: string; 232 | }; 233 | export type ApplicationSettings = BasicAppSettings & { 234 | /** 235 | * String path to the working directory to use for the launched application. 236 | * Defaults to process.cwd(). 237 | */ 238 | workingDirectory?: string; 239 | }; 240 | 241 | /** 242 | * Start and stop your Electron application. 243 | */ 244 | export class Application { 245 | /** 246 | * Spectron uses WebdriverIO and exposes the managed client property on the created 247 | * Application instances. 248 | * The full client API provided by WebdriverIO can be found here 249 | * http://webdriver.io/api.html 250 | * Several additional commands are provided specific to Electron. 251 | */ 252 | client: SpectronClient; 253 | /** 254 | * The electron property is your gateway to accessing the full Electron API. 255 | * Each Electron module is exposed as a property on the electron property so you can 256 | * think of it as an alias for require('electron') from within your app. 257 | */ 258 | electron: typeof Electron; 259 | /** 260 | * The browserWindow property is an alias for require('electron').remote.getCurrentWindow(). 261 | * It provides you access to the current BrowserWindow and contains all the APIs. 262 | * https://electron.atom.io/docs/api/browser-window/ 263 | */ 264 | browserWindow: SpectronWindow; 265 | /** 266 | * The webContents property is an alias for 267 | * require('electron').remote.getCurrentWebContents(). 268 | * It provides you access to the WebContents for the current window 269 | * and contains all the APIs. 270 | * https://electron.atom.io/docs/api/web-contents/ 271 | */ 272 | webContents: SpectronWebContents; 273 | /** 274 | * The mainProcess property is an alias for require('electron').remote.process. 275 | * It provides you access to the main process's process global. 276 | * https://nodejs.org/api/process.html 277 | */ 278 | mainProcess: NodeJS.Process; 279 | /** 280 | *The rendererProcess property is an alias for global.process. 281 | * It provides you access to the main process's process global. 282 | * https://nodejs.org/api/process.html 283 | */ 284 | rendererProcess: NodeJS.Process; 285 | 286 | constructor(options: AppConstructorOptions); 287 | 288 | /** 289 | * Starts the application. 290 | * Returns a Promise that will be resolved when the application is ready to use. 291 | * You should always wait for start to complete before running any commands. 292 | */ 293 | start(): Promise; 294 | 295 | /** 296 | * Stops the application. 297 | * Returns a Promise that will be resolved once the application has stopped. 298 | */ 299 | stop(): Promise; 300 | 301 | /** 302 | * Stops the application and then starts it. 303 | * Returns a Promise that will be resolved once the application has started again. 304 | */ 305 | restart(): Promise; 306 | 307 | /** 308 | * Checks to determine if the application is running or not. 309 | */ 310 | isRunning(): boolean; 311 | 312 | /** 313 | * Get all the configured options passed to the new Application() constructor. 314 | * This will include the default options values currently being used. 315 | */ 316 | getSettings(): ApplicationSettings; 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /lib/api.js: -------------------------------------------------------------------------------- 1 | const apiCache = {}; 2 | 3 | function Api(app, requireName) { 4 | this.app = app; 5 | this.requireName = requireName; 6 | } 7 | 8 | Api.prototype.initialize = function () { 9 | const self = this; 10 | return self.load().then(self.addApiCommands.bind(self)); 11 | }; 12 | 13 | Api.prototype.addApiCommands = function (api) { 14 | if (!this.nodeIntegration) return; 15 | 16 | this.addRenderProcessApis(api.electron); 17 | this.addMainProcessApis(api.electron.remote); 18 | this.addBrowserWindowApis(api.browserWindow); 19 | this.addWebContentsApis(api.webContents); 20 | this.addProcessApis(api.rendererProcess); 21 | 22 | this.api = { 23 | browserWindow: api.browserWindow, 24 | electron: api.electron, 25 | rendererProcess: api.rendererProcess, 26 | webContents: api.webContents 27 | }; 28 | 29 | this.addClientProperties(); 30 | }; 31 | 32 | Api.prototype.load = function () { 33 | const self = this; 34 | 35 | return this.isNodeIntegrationEnabled().then(function (nodeIntegration) { 36 | self.nodeIntegration = nodeIntegration; 37 | if (!nodeIntegration) { 38 | return { 39 | electron: { remote: {} }, 40 | browserWindow: {}, 41 | webContents: {}, 42 | rendererProcess: {} 43 | }; 44 | } 45 | 46 | return self.getVersion().then(function (version) { 47 | const api = apiCache[version]; 48 | if (api) return api; 49 | 50 | return self.loadApi().then(function (api) { 51 | if (version) apiCache[version] = api; 52 | return api; 53 | }); 54 | }); 55 | }); 56 | }; 57 | 58 | Api.prototype.isNodeIntegrationEnabled = function () { 59 | const self = this; 60 | return self.app.client.execute(function (requireName) { 61 | return typeof window[requireName] === 'function'; 62 | }, self.requireName); 63 | }; 64 | 65 | Api.prototype.getVersion = function () { 66 | return this.app.client.execute(function (requireName) { 67 | const process = window[requireName]('process'); 68 | if (process != null && process.versions != null) { 69 | return process.versions.electron; 70 | } 71 | }, this.requireName); 72 | }; 73 | 74 | Api.prototype.loadApi = function () { 75 | return this.app.client.execute(function (requireName) { 76 | if (typeof window[requireName] !== 'function') { 77 | throw new Error( 78 | 'Could not find global require method with name: ' + requireName 79 | ); 80 | } 81 | const electron = window[requireName]('electron'); 82 | electron.remote = window[requireName]('@electron/remote'); 83 | const process = window[requireName]('process'); 84 | 85 | const api = { 86 | browserWindow: {}, 87 | electron: {}, 88 | rendererProcess: {}, 89 | webContents: {} 90 | }; 91 | 92 | function ignoreModule(moduleName) { 93 | switch (moduleName) { 94 | case 'CallbacksRegistry': 95 | case 'deprecate': 96 | case 'deprecations': 97 | case 'hideInternalModules': 98 | case 'Tray': 99 | return true; 100 | case 'inAppPurchase': 101 | return process.platform !== 'darwin'; 102 | } 103 | return false; 104 | } 105 | 106 | function isRemoteFunction(name) { 107 | switch (name) { 108 | case 'BrowserWindow': 109 | case 'Menu': 110 | case 'MenuItem': 111 | return false; 112 | } 113 | return typeof electron.remote[name] === 'function'; 114 | } 115 | 116 | function ignoreApi(apiName) { 117 | switch (apiName) { 118 | case 'prototype': 119 | return true; 120 | default: 121 | return apiName[0] === '_'; 122 | } 123 | } 124 | 125 | function addModule(parent, parentName, name, api) { 126 | api[name] = {}; 127 | for (const key in parent[name]) { 128 | if (ignoreApi(key)) continue; 129 | api[name][key] = parentName + '.' + name + '.' + key; 130 | } 131 | } 132 | 133 | function addRenderProcessModules() { 134 | Object.getOwnPropertyNames(electron).forEach(function (key) { 135 | if (ignoreModule(key)) return; 136 | if (key === 'remote') return; 137 | addModule(electron, 'electron', key, api.electron); 138 | }); 139 | } 140 | 141 | function addMainProcessModules() { 142 | api.electron.remote = {}; 143 | Object.getOwnPropertyNames(electron.remote).forEach(function (key) { 144 | if (ignoreModule(key)) return; 145 | if (isRemoteFunction(key)) { 146 | api.electron.remote[key] = 'electron.remote.' + key; 147 | } else { 148 | addModule( 149 | electron.remote, 150 | 'electron.remote', 151 | key, 152 | api.electron.remote 153 | ); 154 | } 155 | }); 156 | addModule( 157 | electron.remote, 158 | 'electron.remote', 159 | 'process', 160 | api.electron.remote 161 | ); 162 | } 163 | 164 | function addBrowserWindow() { 165 | const currentWindow = electron.remote.getCurrentWindow(); 166 | for (const name in currentWindow) { 167 | if (ignoreApi(name)) continue; 168 | const value = currentWindow[name]; 169 | if (typeof value === 'function') { 170 | api.browserWindow[name] = 'browserWindow.' + name; 171 | } 172 | } 173 | } 174 | 175 | function addWebContents() { 176 | const webContents = electron.remote.getCurrentWebContents(); 177 | for (const name in webContents) { 178 | if (ignoreApi(name)) continue; 179 | const value = webContents[name]; 180 | if (typeof value === 'function') { 181 | api.webContents[name] = 'webContents.' + name; 182 | } 183 | } 184 | } 185 | 186 | function addProcess() { 187 | if (typeof process !== 'object') return; 188 | 189 | for (const name in process) { 190 | if (ignoreApi(name)) continue; 191 | api.rendererProcess[name] = 'process.' + name; 192 | } 193 | } 194 | 195 | addRenderProcessModules(); 196 | addMainProcessModules(); 197 | addBrowserWindow(); 198 | addWebContents(); 199 | addProcess(); 200 | 201 | return api; 202 | }, this.requireName); 203 | }; 204 | 205 | Api.prototype.addClientProperty = function (name) { 206 | const self = this; 207 | 208 | const clientPrototype = Object.getPrototypeOf(self.app.client); 209 | Object.defineProperty(clientPrototype, name, { 210 | get: function () { 211 | const client = this; 212 | return transformObject(self.api[name], {}, function (value) { 213 | return client[value].bind(client); 214 | }); 215 | } 216 | }); 217 | }; 218 | 219 | Api.prototype.addClientProperties = function () { 220 | this.addClientProperty('electron'); 221 | this.addClientProperty('browserWindow'); 222 | this.addClientProperty('webContents'); 223 | this.addClientProperty('rendererProcess'); 224 | 225 | Object.defineProperty(Object.getPrototypeOf(this.app.client), 'mainProcess', { 226 | get: function () { 227 | return this.electron.remote.process; 228 | } 229 | }); 230 | }; 231 | 232 | Api.prototype.addRenderProcessApis = function (api) { 233 | const app = this.app; 234 | const self = this; 235 | const electron = {}; 236 | app.electron = electron; 237 | 238 | Object.keys(api).forEach(function (moduleName) { 239 | if (moduleName === 'remote') return; 240 | electron[moduleName] = {}; 241 | const moduleApi = api[moduleName]; 242 | 243 | Object.keys(moduleApi).forEach(function (key) { 244 | const commandName = moduleApi[key]; 245 | 246 | app.client.addCommand(commandName, function () { 247 | const args = Array.prototype.slice.call(arguments); 248 | return this.execute( 249 | callRenderApi, 250 | moduleName, 251 | key, 252 | args, 253 | self.requireName 254 | ); 255 | }); 256 | 257 | electron[moduleName][key] = function () { 258 | return app.client[commandName].apply(app.client, arguments); 259 | }; 260 | }); 261 | }); 262 | }; 263 | 264 | Api.prototype.addMainProcessApis = function (api) { 265 | const app = this.app; 266 | const self = this; 267 | const remote = {}; 268 | app.electron.remote = remote; 269 | 270 | Object.keys(api) 271 | .filter(function (propertyName) { 272 | return typeof api[propertyName] === 'string'; 273 | }) 274 | .forEach(function (name) { 275 | const commandName = api[name]; 276 | 277 | app.client.addCommand(commandName, function () { 278 | const args = Array.prototype.slice.call(arguments); 279 | return this.execute(callMainApi, '', name, args, self.requireName); 280 | }); 281 | 282 | remote[name] = function () { 283 | return app.client[commandName].apply(app.client, arguments); 284 | }; 285 | }); 286 | 287 | Object.keys(api) 288 | .filter(function (moduleName) { 289 | return typeof api[moduleName] === 'object'; 290 | }) 291 | .forEach(function (moduleName) { 292 | remote[moduleName] = {}; 293 | const moduleApi = api[moduleName]; 294 | 295 | Object.keys(moduleApi).forEach(function (key) { 296 | const commandName = moduleApi[key]; 297 | 298 | app.client.addCommand(commandName, function () { 299 | const args = Array.prototype.slice.call(arguments); 300 | return this.execute( 301 | callMainApi, 302 | moduleName, 303 | key, 304 | args, 305 | self.requireName 306 | ); 307 | }); 308 | 309 | remote[moduleName][key] = function () { 310 | return app.client[commandName].apply(app.client, arguments); 311 | }; 312 | }); 313 | }); 314 | }; 315 | 316 | Api.prototype.addBrowserWindowApis = function (api) { 317 | const app = this.app; 318 | const self = this; 319 | app.browserWindow = {}; 320 | 321 | const asyncApis = { 322 | 'browserWindow.capturePage': true 323 | }; 324 | 325 | Object.keys(api) 326 | .filter(function (name) { 327 | return !Object.prototype.hasOwnProperty.call(asyncApis, api[name]); 328 | }) 329 | .forEach(function (name) { 330 | const commandName = api[name]; 331 | 332 | app.client.addCommand(commandName, function () { 333 | const args = Array.prototype.slice.call(arguments); 334 | return this.execute(callBrowserWindowApi, name, args, self.requireName); 335 | }); 336 | 337 | app.browserWindow[name] = function () { 338 | return app.client[commandName].apply(app.client, arguments); 339 | }; 340 | }); 341 | 342 | this.addCapturePageSupport(); 343 | }; 344 | 345 | Api.prototype.addCapturePageSupport = function () { 346 | const app = this.app; 347 | const self = this; 348 | 349 | app.client.addCommand('browserWindow.capturePage', function (rect) { 350 | return this.executeAsync( 351 | async function (rect, requireName, done) { 352 | const args = []; 353 | if (rect != null) args.push(rect); 354 | const browserWindow = 355 | window[requireName]('@electron/remote').getCurrentWindow(); 356 | const image = await browserWindow.capturePage.apply( 357 | browserWindow, 358 | args 359 | ); 360 | if (image != null) { 361 | done(image.toPNG().toString('base64')); 362 | } else { 363 | done(image); 364 | } 365 | }, 366 | rect, 367 | self.requireName 368 | ).then(function (image) { 369 | return Buffer.from(image, 'base64'); 370 | }); 371 | }); 372 | 373 | app.browserWindow.capturePage = function () { 374 | return app.client['browserWindow.capturePage'].apply(app.client, arguments); 375 | }; 376 | }; 377 | 378 | Api.prototype.addWebContentsApis = function (api) { 379 | const app = this.app; 380 | const self = this; 381 | app.webContents = {}; 382 | 383 | const asyncApis = { 384 | 'webContents.savePage': true, 385 | 'webContents.executeJavaScript': true 386 | }; 387 | 388 | Object.keys(api) 389 | .filter(function (name) { 390 | return !Object.prototype.hasOwnProperty.call(asyncApis, api[name]); 391 | }) 392 | .forEach(function (name) { 393 | const commandName = api[name]; 394 | 395 | app.client.addCommand(commandName, function () { 396 | const args = Array.prototype.slice.call(arguments); 397 | return this.execute(callWebContentsApi, name, args, self.requireName); 398 | }); 399 | 400 | app.webContents[name] = function () { 401 | return app.client[commandName].apply(app.client, arguments); 402 | }; 403 | }); 404 | 405 | this.addSavePageSupport(); 406 | this.addExecuteJavaScriptSupport(); 407 | }; 408 | 409 | Api.prototype.addSavePageSupport = function () { 410 | const app = this.app; 411 | const self = this; 412 | 413 | app.client.addCommand('webContents.savePage', function (fullPath, saveType) { 414 | return this.executeAsync( 415 | async function (fullPath, saveType, requireName, done) { 416 | const webContents = 417 | window[requireName]('@electron/remote').getCurrentWebContents(); 418 | await webContents.savePage(fullPath, saveType); 419 | done(); 420 | }, 421 | fullPath, 422 | saveType, 423 | self.requireName 424 | ).then(function (rawError) { 425 | if (rawError) { 426 | const error = new Error(rawError.message); 427 | if (rawError.name) error.name = rawError.name; 428 | throw error; 429 | } 430 | }); 431 | }); 432 | 433 | app.webContents.savePage = function () { 434 | return app.client['webContents.savePage'].apply(app.client, arguments); 435 | }; 436 | }; 437 | 438 | Api.prototype.addExecuteJavaScriptSupport = function () { 439 | const app = this.app; 440 | const self = this; 441 | 442 | app.client.addCommand( 443 | 'webContents.executeJavaScript', 444 | function (code, useGesture) { 445 | return this.executeAsync( 446 | async function (code, useGesture, requireName, done) { 447 | const webContents = 448 | window[requireName]('@electron/remote').getCurrentWebContents(); 449 | const result = await webContents.executeJavaScript(code, useGesture); 450 | done(result); 451 | }, 452 | code, 453 | useGesture, 454 | self.requireName 455 | ); 456 | } 457 | ); 458 | 459 | app.webContents.executeJavaScript = function () { 460 | return app.client['webContents.executeJavaScript'].apply( 461 | app.client, 462 | arguments 463 | ); 464 | }; 465 | }; 466 | 467 | Api.prototype.addProcessApis = function (api) { 468 | const app = this.app; 469 | app.rendererProcess = {}; 470 | 471 | Object.keys(api).forEach(function (name) { 472 | const commandName = api[name]; 473 | 474 | app.client.addCommand(commandName, function () { 475 | const args = Array.prototype.slice.call(arguments); 476 | return this.execute(callProcessApi, name, args); 477 | }); 478 | 479 | app.rendererProcess[name] = function () { 480 | return app.client[commandName].apply(app.client, arguments); 481 | }; 482 | }); 483 | 484 | app.mainProcess = app.electron.remote.process; 485 | }; 486 | 487 | Api.prototype.transferPromiseness = function (target, promise) { 488 | if (!this.nodeIntegration) return; 489 | 490 | const addProperties = function (source, target, moduleName) { 491 | const sourceModule = source[moduleName]; 492 | if (!sourceModule) return; 493 | target[moduleName] = transformObject( 494 | sourceModule, 495 | {}, 496 | function (value, parent) { 497 | return value.bind(parent); 498 | } 499 | ); 500 | }; 501 | 502 | addProperties(promise, target, 'webContents'); 503 | addProperties(promise, target, 'browserWindow'); 504 | addProperties(promise, target, 'electron'); 505 | addProperties(promise, target, 'mainProcess'); 506 | addProperties(promise, target, 'rendererProcess'); 507 | }; 508 | 509 | Api.prototype.logApi = function () { 510 | const fs = require('fs'); 511 | const path = require('path'); 512 | const json = JSON.stringify(this.api, null, 2); 513 | fs.writeFileSync(path.join(__dirname, 'api.json'), json); 514 | }; 515 | 516 | function transformObject(input, output, callback) { 517 | Object.keys(input).forEach(function (name) { 518 | const value = input[name]; 519 | if (typeof value === 'object') { 520 | output[name] = {}; 521 | transformObject(value, output[name], callback); 522 | } else { 523 | output[name] = callback(value, input); 524 | } 525 | }); 526 | return output; 527 | } 528 | 529 | function callRenderApi(moduleName, api, args, requireName) { 530 | const module = window[requireName]('electron')[moduleName]; 531 | if (typeof module[api] === 'function') { 532 | return module[api].apply(module, args); 533 | } else { 534 | return module[api]; 535 | } 536 | } 537 | 538 | function callMainApi(moduleName, api, args, requireName) { 539 | let module = window[requireName]('@electron/remote'); 540 | if (moduleName) { 541 | module = module[moduleName]; 542 | } 543 | if (typeof module[api] === 'function') { 544 | return module[api].apply(module, args); 545 | } else { 546 | return module[api]; 547 | } 548 | } 549 | 550 | function callWebContentsApi(name, args, requireName) { 551 | const webContents = 552 | window[requireName]('@electron/remote').getCurrentWebContents(); 553 | return webContents[name].apply(webContents, args); 554 | } 555 | 556 | function callBrowserWindowApi(name, args, requireName) { 557 | const browserWindow = 558 | window[requireName]('@electron/remote').getCurrentWindow(); 559 | return browserWindow[name].apply(browserWindow, args); 560 | } 561 | 562 | function callProcessApi(name, args) { 563 | if (typeof process[name] === 'function') { 564 | return process[name].apply(process, args); 565 | } else { 566 | return process[name]; 567 | } 568 | } 569 | 570 | module.exports = Api; 571 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spectron icon Spectron 2 | 3 | [![CI](https://github.com/electron-userland/spectron/workflows/CI/badge.svg)](https://github.com/electron-userland/spectron/actions) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/) 4 | [![dependencies](https://img.shields.io/david/electron/spectron.svg)](https://david-dm.org/electron/spectron) [![license:mit](https://img.shields.io/badge/license-mit-blue.svg)](https://opensource.org/licenses/MIT) [![npm:](https://img.shields.io/npm/v/spectron.svg)](https://www.npmjs.com/package/spectron) [![downloads](https://img.shields.io/npm/dm/spectron.svg)](https://www.npmjs.com/package/spectron) 5 | 6 | ### 🚨 Spectron is officially deprecated as of February 1, 2022. 7 | 8 | Easily test your [Electron](http://electron.atom.io) apps using 9 | [ChromeDriver](https://sites.google.com/chromium.org/driver) and 10 | [WebdriverIO](http://webdriver.io). 11 | 12 | ## Version Map 13 | 14 | For given versions of Electron you must depend on a very specific version range of Spectron. Below is a version mapping table between Spectron version and Electron version. 15 | 16 | | Electron Version | Spectron Version | 17 | |------------------|------------------| 18 | | `~1.0.0` | `~3.0.0` | 19 | | `~1.1.0` | `~3.1.0` | 20 | | `~1.2.0` | `~3.2.0` | 21 | | `~1.3.0` | `~3.3.0` | 22 | | `~1.4.0` | `~3.4.0` | 23 | | `~1.5.0` | `~3.5.0` | 24 | | `~1.6.0` | `~3.6.0` | 25 | | `~1.7.0` | `~3.7.0` | 26 | | `~1.8.0` | `~3.8.0` | 27 | | `^2.0.0` | `^4.0.0` | 28 | | `^3.0.0` | `^5.0.0` | 29 | | `^4.0.0` | `^6.0.0` | 30 | | `^5.0.0` | `^7.0.0` | 31 | | `^6.0.0` | `^8.0.0` | 32 | | `^7.0.0` | `^9.0.0` | 33 | | `^8.0.0` | `^10.0.0`| 34 | | `^9.0.0` | `^11.0.0`| 35 | | `^10.0.0` | `^12.0.0`| 36 | | `^11.0.0` | `^13.0.0`| 37 | | `^12.0.0` | `^14.0.0`| 38 | | `^13.0.0` | `^15.0.0`| 39 | | `^14.0.0` | `^16.0.0`| 40 | | `^15.0.0` | `^17.0.0`| 41 | | `^16.0.0` | `^18.0.0`| 42 | | `^17.0.0` | `^19.0.0`| 43 | 44 | Learn more from [this presentation](https://speakerdeck.com/kevinsawicki/testing-your-electron-apps-with-chromedriver). 45 | 46 | :rotating_light: Upgrading from `1.x` to `2.x`/`3.x`? Read the [changelog](https://github.com/electron/spectron/blob/master/CHANGELOG.md). 47 | 48 | ## Installation 49 | 50 | ```sh 51 | npm install --save-dev spectron 52 | ``` 53 | 54 | ## Usage 55 | 56 | Spectron works with any testing framework but the following example uses 57 | [mocha](https://mochajs.org): 58 | 59 | To get up and running from your command line: 60 | ```sh 61 | # Install mocha locally as a dev dependency. 62 | npm i mocha -D 63 | 64 | # From the project root, create a folder called test, in that directory, create a file called 'spec.js' 65 | touch test/spec.js 66 | 67 | # Change directory to test 68 | cd test 69 | ``` 70 | 71 | Then simply include the following in your first `spec.js`. 72 | 73 | ```js 74 | const { Application } = require('spectron') 75 | const assert = require('assert') 76 | const electronPath = require('electron') // Require Electron from the binaries included in node_modules. 77 | const path = require('path') 78 | 79 | describe('Application launch', function () { 80 | this.timeout(10000) 81 | 82 | beforeEach(async function () { 83 | this.app = new Application({ 84 | // Your electron path can be any binary 85 | // i.e for OSX an example path could be '/Applications/MyApp.app/Contents/MacOS/MyApp' 86 | // But for the sake of the example we fetch it from our node_modules. 87 | path: electronPath, 88 | 89 | // Assuming you have the following directory structure 90 | 91 | // |__ my project 92 | // |__ ... 93 | // |__ main.js 94 | // |__ package.json 95 | // |__ index.html 96 | // |__ ... 97 | // |__ test 98 | // |__ spec.js <- You are here! ~ Well you should be. 99 | 100 | // The following line tells spectron to look and use the main.js file 101 | // and the package.json located 1 level above. 102 | args: [path.join(__dirname, '..')] 103 | }) 104 | await this.app.start() 105 | }) 106 | 107 | afterEach(async function () { 108 | if (this.app && this.app.isRunning()) { 109 | await this.app.stop() 110 | } 111 | }) 112 | 113 | it('shows an initial window', async function () { 114 | const count = await this.app.client.getWindowCount() 115 | assert.equal(count, 1) 116 | // Please note that getWindowCount() will return 2 if `dev tools` are opened. 117 | // assert.equal(count, 2) 118 | }) 119 | }) 120 | ``` 121 | 122 | Create an npm task in your package.json file 123 | ```sh 124 | "scripts": { 125 | "test": "mocha" 126 | } 127 | ``` 128 | 129 | And from the root of your project, in your command-line simply run: 130 | ```sh 131 | npm test 132 | ``` 133 | 134 | By default, mocha searches for a folder with the name `test` ( which we created before ). 135 | For more information on how to configure mocha, please visit [mocha](https://mochajs.org). 136 | 137 | #### Limitations 138 | 139 | As stated in [issue #19](https://github.com/electron/spectron/issues/19), Spectron will not be able to start if your Electron app is launched using the `remote-debugging-port` command-line switch (i.e. `app.commandLine.appendSwitch('remote-debugging-port', );`). Please make sure to include the necessary logic in your app's code to disable the switch during tests. 140 | 141 | As mentioned in [issue #202](https://github.com/electron-userland/spectron/issues/202#issuecomment-632223955), 142 | `app.start()` promise won't resolve if the electron application calls 143 | `setPath('userData', path)`. Webdriver places a port file into the `userData` 144 | directory and needs to know where to look for it. The workaround is to pass 145 | `chromeDriverArgs: ['user-data-dir=/custom/userData/path']` to the `Application` 146 | constructor. 147 | 148 | ## Application API 149 | 150 | Spectron exports an `Application` class that when configured, can start and 151 | stop your Electron application. 152 | 153 | ### new Application(options) 154 | 155 | Create a new application with the following options: 156 | 157 | * `path` - **Required.** String path to the Electron application executable to 158 | launch. 159 | **Note:** If you want to invoke `electron` directly with your app's main 160 | script then you should specify `path` as `electron` via `electron-prebuilt` 161 | and specify your app's main script path as the first argument in the `args` 162 | array. 163 | * `args` - Array of arguments to pass to the Electron application. 164 | * `chromeDriverArgs` - Array of arguments to pass to ChromeDriver. 165 | See [here](https://sites.google.com/chromium.org/driver/capabilities) for details on the Chrome arguments. 166 | * `cwd`- String path to the working directory to use for the launched 167 | application. Defaults to `process.cwd()`. 168 | * `env` - Object of additional environment variables to set in the launched 169 | application. 170 | * `host` - String host name of the launched `chromedriver` process. 171 | Defaults to `'localhost'`. 172 | * `port` - Number port of the launched `chromedriver` process. 173 | Defaults to `9515`. 174 | * `nodePath` - String path to a `node` executable to launch ChromeDriver with. 175 | Defaults to `process.execPath`. 176 | * `connectionRetryCount` - Number of retry attempts to make when connecting 177 | to ChromeDriver. Defaults to `10` attempts. 178 | * `connectionRetryTimeout` - Number in milliseconds to wait for connections 179 | to ChromeDriver to be made. Defaults to `30000` milliseconds. 180 | * `quitTimeout` - Number in milliseconds to wait for application quitting. 181 | Defaults to `1000` milliseconds. 182 | * `requireName` - Custom property name to use when requiring modules. Defaults 183 | to `require`. This should only be used if your application deletes the main 184 | `window.require` function and assigns it to another property name on `window`. 185 | * `startTimeout` - Number in milliseconds to wait for ChromeDriver to start. 186 | Defaults to `5000` milliseconds. 187 | * `waitTimeout` - Number in milliseconds to wait for calls like 188 | `waitUntilTextExists` and `waitUntilWindowLoaded` to complete. 189 | Defaults to `5000` milliseconds. 190 | * `debuggerAddress` - String address of a Chrome debugger server to connect to. 191 | * `chromeDriverLogPath` - String path to file to store ChromeDriver logs in. 192 | Setting this option enables `--verbose` logging when starting ChromeDriver. 193 | * `webdriverLogPath` - String path to a directory where Webdriver will write 194 | logs to. Setting this option enables `verbose` logging from Webdriver. 195 | * `webdriverOptions` - Object of additional options for Webdriver 196 | 197 | ### Node Integration 198 | 199 | The Electron helpers provided by Spectron require accessing the core Electron 200 | APIs in the renderer processes of your application. So, either your Electron 201 | application has `nodeIntegration` set to `true` or you'll need to expose a 202 | `require` window global to Spectron so it can access the core Electron APIs. 203 | 204 | You can do this by adding a [`preload`][preload] script that does the following: 205 | 206 | ```js 207 | if (process.env.NODE_ENV === 'test') { 208 | window.electronRequire = require 209 | } 210 | ``` 211 | 212 | Then create the Spectron `Application` with the `requireName` option set to 213 | `'electronRequire'` and then runs your tests via `NODE_ENV=test npm test`. 214 | 215 | **Note:** This is only required if your tests are accessing any Electron APIs. 216 | You don't need to do this if you are only accessing the helpers on the `client` 217 | property which do not require Node integration. 218 | 219 | ### Properties 220 | 221 | #### client 222 | 223 | Spectron uses [WebdriverIO](https://webdriver.io) and exposes the managed 224 | `client` property on the created `Application` instances. 225 | 226 | The `client` API is WebdriverIO's `browser` object. Documentation can be found 227 | [here](https://webdriver.io/docs/browserobject/). 228 | 229 | Several additional commands are provided specific to Electron. 230 | 231 | All the commands return a `Promise`. 232 | 233 | So if you wanted to get the text of an element you would do: 234 | 235 | ```js 236 | const element = await app.client.$('#error-alert') 237 | const errorText = await element.getText() 238 | console.log('The #error-alert text content is ' + errorText) 239 | ``` 240 | 241 | #### electron 242 | 243 | The `electron` property is your gateway to accessing the full Electron API. 244 | 245 | Each Electron module is exposed as a property on the `electron` property 246 | so you can think of it as an alias for `require('electron')` from within your 247 | app. 248 | 249 | So if you wanted to access the [clipboard](http://electron.atom.io/docs/latest/api/clipboard) 250 | API in your tests you would do: 251 | 252 | ```js 253 | app.electron.clipboard.writeText('pasta') 254 | const clipboardText = app.electron.clipboard.readText() 255 | console.log('The clipboard text is ' + clipboardText) 256 | ``` 257 | 258 | #### browserWindow 259 | 260 | The `browserWindow` property is an alias for `require('electron').remote.getCurrentWindow()`. 261 | 262 | It provides you access to the current [BrowserWindow](http://electron.atom.io/docs/latest/api/browser-window/) 263 | and contains all the APIs. 264 | 265 | So if you wanted to check if the current window is visible in your tests you 266 | would do: 267 | 268 | ```js 269 | const visible = await app.browserWindow.isVisible() 270 | console.log('window is visible? ' + visible) 271 | ``` 272 | 273 | It is named `browserWindow` instead of `window` so that it doesn't collide 274 | with the WebDriver command of that name. 275 | 276 | ##### capturePage 277 | 278 | The async `capturePage` API is supported but instead of taking a callback it 279 | returns a `Promise` that resolves to a `Buffer` that is the image data of 280 | screenshot. 281 | 282 | ```js 283 | const imageBuffer = await app.browserWindow.capturePage() 284 | fs.writeFile('page.png', imageBuffer) 285 | ``` 286 | 287 | #### webContents 288 | 289 | The `webContents` property is an alias for `require('electron').remote.getCurrentWebContents()`. 290 | 291 | It provides you access to the [WebContents](http://electron.atom.io/docs/latest/api/web-contents/) 292 | for the current window and contains all the APIs. 293 | 294 | So if you wanted to check if the current window is loading in your tests you 295 | would do: 296 | 297 | ```js 298 | app.webContents.isLoading().then(function (visible) { 299 | console.log('window is loading? ' + visible) 300 | }) 301 | ``` 302 | 303 | ##### savePage 304 | 305 | The async `savePage` API is supported but instead of taking a callback it 306 | returns a `Promise` that will raise any errors and resolve to `undefined` when 307 | complete. 308 | 309 | ```js 310 | try { 311 | await app.webContents.savePage('/Users/kevin/page.html', 'HTMLComplete') 312 | console.log('page saved') 313 | catch (error) { 314 | console.error('saving page failed', error.message) 315 | } 316 | ``` 317 | 318 | ##### executeJavaScript 319 | The async `executeJavaScript` API is supported but instead of taking a callback it 320 | returns a `Promise` that will resolve with the result of the last statement of the 321 | script. 322 | 323 | ```js 324 | const result = await app.webContents.executeJavaScript('1 + 2') 325 | console.log(result) // prints 3 326 | ``` 327 | 328 | #### mainProcess 329 | 330 | The `mainProcess` property is an alias for `require('electron').remote.process`. 331 | 332 | It provides you access to the main process's [process](https://nodejs.org/api/process.html) 333 | global. 334 | 335 | So if you wanted to get the `argv` for the main process in your tests you would 336 | do: 337 | 338 | ```js 339 | const argv = await app.mainProcess.argv() 340 | console.log('main process args: ' + argv) 341 | ``` 342 | 343 | Properties on the `process` are exposed as functions that return promises so 344 | make sure to call `mainProcess.env().then(...)` instead of 345 | `mainProcess.env.then(...)`. 346 | 347 | #### rendererProcess 348 | 349 | The `rendererProcess` property is an alias for `global.process`. 350 | 351 | It provides you access to the renderer process's [process](https://nodejs.org/api/process.html) 352 | global. 353 | 354 | So if you wanted to get the environment variables for the renderer process in 355 | your tests you would do: 356 | 357 | ```js 358 | const env = await app.rendererProcess.env() 359 | console.log('renderer process env variables: ' + env) 360 | ``` 361 | 362 | ### Methods 363 | 364 | #### start() 365 | 366 | Starts the application. Returns a `Promise` that will be resolved when the 367 | application is ready to use. You should always wait for start to complete 368 | before running any commands. 369 | 370 | #### stop() 371 | 372 | Stops the application. Returns a `Promise` that will be resolved once the 373 | application has stopped. 374 | 375 | #### restart() 376 | 377 | Stops the application and then starts it. Returns a `Promise` that will be 378 | resolved once the application has started again. 379 | 380 | #### isRunning() 381 | 382 | Checks to determine if the application is running or not. 383 | 384 | Returns a `Boolean`. 385 | 386 | #### getSettings() 387 | 388 | Get all the configured options passed to the `new Application()` constructor. 389 | This will include the default options values currently being used. 390 | 391 | Returns an `Object`. 392 | 393 | #### client.getMainProcessLogs() 394 | 395 | Gets the `console` log output from the main process. The logs are cleared 396 | after they are returned. 397 | 398 | Returns a `Promise` that resolves to an array of string log messages 399 | 400 | ```js 401 | const logs = await app.client.getMainProcessLogs() 402 | logs.forEach(function (log) { 403 | console.log(log) 404 | }) 405 | ``` 406 | 407 | #### client.getRenderProcessLogs() 408 | 409 | Gets the `console` log output from the render process. The logs are cleared 410 | after they are returned. 411 | 412 | Returns a `Promise` that resolves to an array of log objects. 413 | 414 | ```js 415 | const logs = await app.client.getRenderProcessLogs() 416 | logs.forEach(function (log) { 417 | console.log(log.message) 418 | console.log(log.source) 419 | console.log(log.level) 420 | }) 421 | ``` 422 | 423 | #### client.getSelectedText() 424 | 425 | Get the selected text in the current window. 426 | 427 | ```js 428 | const selectedText = await app.client.getSelectedText() 429 | console.log(selectedText) 430 | ``` 431 | 432 | #### client.getWindowCount() 433 | 434 | Gets the number of open windows. 435 | `` tags are also counted as separate windows. 436 | 437 | ```js 438 | const count = await app.client.getWindowCount() 439 | console.log(count) 440 | ``` 441 | 442 | #### client.waitUntilTextExists(selector, text, [timeout]) 443 | 444 | Waits until the element matching the given selector contains the given 445 | text. Takes an optional timeout in milliseconds that defaults to `5000`. 446 | 447 | ```js 448 | app.client.waitUntilTextExists('#message', 'Success', 10000) 449 | ``` 450 | 451 | #### client.waitUntilWindowLoaded([timeout]) 452 | 453 | Wait until the window is no longer loading. Takes an optional timeout 454 | in milliseconds that defaults to `5000`. 455 | 456 | ```js 457 | app.client.waitUntilWindowLoaded(10000) 458 | ``` 459 | 460 | #### client.windowByIndex(index) 461 | 462 | Focus a window using its index from the `windowHandles()` array. 463 | `` tags can also be focused as a separate window. 464 | 465 | ```js 466 | app.client.windowByIndex(1) 467 | ``` 468 | 469 | #### client.switchWindow(urlOrTitleToMatch) 470 | 471 | Focus a window using its URL or title. 472 | 473 | ```js 474 | // switch via url match 475 | app.client.switchWindow('google.com') 476 | 477 | // switch via title match 478 | app.client.switchWindow('Next-gen WebDriver test framework') 479 | ``` 480 | 481 | ### Accessibility Testing 482 | 483 | Spectron bundles the [Accessibility Developer Tools](https://github.com/GoogleChrome/accessibility-developer-tools) 484 | provided by Google and adds support for auditing each window and `` 485 | tag in your application. 486 | 487 | #### client.auditAccessibility(options) 488 | 489 | Run an accessibility audit in the focused window with the specified options. 490 | 491 | * `options` - An optional Object with the following keys: 492 | * `ignoreWarnings` - `true` to ignore failures with a severity of `'Warning'` 493 | and only include failures with a severity of `'Severe'`. Defaults to `false`. 494 | * `ignoreRules` - Array of String rule code values such as `AX_COLOR_01` to 495 | ignore failures for. The full list is available [here](https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules). 496 | 497 | Returns an `audit` Object with the following properties: 498 | 499 | * `message` - A detailed String message about the results 500 | * `failed` - A Boolean, `false` when the audit has failures 501 | * `results` - An array of detail objects for each failed rule. Each object 502 | in the array has the following properties: 503 | * `code` - A unique String accessibility rule identifier 504 | * `elements` - An Array of Strings representing the selector path of each 505 | HTML element that failed the rule 506 | * `message` - A String message about the failed rule 507 | * `severity` - `'Warning'` or `'Severe'` 508 | * `url` - A String URL providing more details about the failed rule 509 | 510 | ```js 511 | const audit = await app.client.auditAccessibility() 512 | if (audit.failed) { 513 | console.error(audit.message) 514 | } 515 | ``` 516 | 517 | See https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules 518 | for more details about the audit rules. 519 | 520 | If you are using a `` tag in your app and want to audit both the outer 521 | page and the ``'s page then you will need to do the following: 522 | 523 | ```js 524 | // Focus main page and audit it 525 | await app.client.windowByIndex(0) 526 | const audit = await app.client.auditAccessibility() 527 | if (audit.failed) { 528 | console.error('Main page failed audit') 529 | console.error(audit.message) 530 | } 531 | 532 | //Focus tag and audit it 533 | await app.client.windowByIndex(1) 534 | const audit = await app.client.auditAccessibility() 535 | if (audit.failed) { 536 | console.error(' page failed audit') 537 | console.error(audit.message) 538 | } 539 | ``` 540 | 541 | ## Continuous Integration 542 | 543 | ### On Travis CI 544 | 545 | You will want to add the following to your `.travis.yml` file when building on 546 | Linux: 547 | 548 | ```yml 549 | before_script: 550 | - "export DISPLAY=:99.0" 551 | - "sh -e /etc/init.d/xvfb start" 552 | - sleep 3 # give xvfb some time to start 553 | ``` 554 | 555 | Check out Spectron's [.travis.yml](https://github.com/electron/spectron/blob/master/.travis.yml) 556 | file for a production example. 557 | 558 | ### On AppVeyor 559 | 560 | You will want to add the following to your `appveyor.yml` file: 561 | 562 | ```yml 563 | os: unstable 564 | ``` 565 | 566 | Check out Spectron's [appveyor.yml](https://github.com/electron/spectron/blob/master/appveyor.yml) 567 | file for a production example. 568 | 569 | 570 | ## Test Library Examples 571 | 572 | ### With Chai As Promised 573 | 574 | WebdriverIO is promise-based and so it pairs really well with the 575 | [Chai as Promised](https://github.com/domenic/chai-as-promised) library that 576 | builds on top of [Chai](http://chaijs.com). 577 | 578 | Using these together allows you to chain assertions together and have fewer 579 | callback blocks. See below for a simple example: 580 | 581 | ```sh 582 | npm install --save-dev chai 583 | npm install --save-dev chai-as-promised 584 | ``` 585 | 586 | ```js 587 | const Application = require('spectron').Application 588 | const chai = require('chai') 589 | const chaiAsPromised = require('chai-as-promised') 590 | const electronPath = require('electron') 591 | const path = require('path') 592 | 593 | chai.should() 594 | chai.use(chaiAsPromised) 595 | 596 | describe('Application launch', function () { 597 | this.timeout(10000); 598 | 599 | beforeEach(function () { 600 | this.app = new Application({ 601 | path: electronPath, 602 | args: [path.join(__dirname, '..')] 603 | }) 604 | return this.app.start() 605 | }) 606 | 607 | beforeEach(function () { 608 | chaiAsPromised.transferPromiseness = this.app.transferPromiseness 609 | }) 610 | 611 | afterEach(function () { 612 | if (this.app && this.app.isRunning()) { 613 | return this.app.stop() 614 | } 615 | }) 616 | 617 | it('opens a window', function () { 618 | return this.app.client.waitUntilWindowLoaded() 619 | .getWindowCount().should.eventually.have.at.least(1) 620 | .browserWindow.isMinimized().should.eventually.be.false 621 | .browserWindow.isVisible().should.eventually.be.true 622 | .browserWindow.isFocused().should.eventually.be.true 623 | .browserWindow.getBounds().should.eventually.have.property('width').and.be.above(0) 624 | .browserWindow.getBounds().should.eventually.have.property('height').and.be.above(0) 625 | }) 626 | }) 627 | ``` 628 | 629 | ### With AVA 630 | 631 | Spectron works with [AVA](https://github.com/avajs/ava), which allows you 632 | to write your tests in ES2015+ without doing any extra work. 633 | 634 | ```js 635 | import test from 'ava'; 636 | import {Application} from 'spectron'; 637 | 638 | test.beforeEach(t => { 639 | t.context.app = new Application({ 640 | path: '/Applications/MyApp.app/Contents/MacOS/MyApp' 641 | }); 642 | 643 | return t.context.app.start(); 644 | }); 645 | 646 | test.afterEach(t => { 647 | return t.context.app.stop(); 648 | }); 649 | 650 | test('opens a window', t => { 651 | return t.context.app.client.waitUntilWindowLoaded() 652 | .getWindowCount().then(count => { 653 | t.is(count, 1); 654 | }).browserWindow.isMinimized().then(min => { 655 | t.false(min); 656 | }).browserWindow.isDevToolsOpened().then(opened => { 657 | t.false(opened); 658 | }).browserWindow.isVisible().then(visible => { 659 | t.true(visible); 660 | }).browserWindow.isFocused().then(focused => { 661 | t.true(focused); 662 | }).browserWindow.getBounds().then(bounds => { 663 | t.true(bounds.width > 0); 664 | t.true(bounds.height > 0); 665 | }); 666 | }); 667 | ``` 668 | 669 | AVA has built-in support for [async functions](https://github.com/avajs/ava#async-function-support), which simplifies async operations: 670 | 671 | ```js 672 | import test from 'ava'; 673 | import {Application} from 'spectron'; 674 | 675 | test.beforeEach(async t => { 676 | t.context.app = new Application({ 677 | path: '/Applications/MyApp.app/Contents/MacOS/MyApp' 678 | }); 679 | 680 | await t.context.app.start(); 681 | }); 682 | 683 | test.afterEach.always(async t => { 684 | await t.context.app.stop(); 685 | }); 686 | 687 | test('example', async t => { 688 | const app = t.context.app; 689 | await app.client.waitUntilWindowLoaded(); 690 | 691 | const win = app.browserWindow; 692 | t.is(await app.client.getWindowCount(), 1); 693 | t.false(await win.isMinimized()); 694 | t.false(await win.isDevToolsOpened()); 695 | t.true(await win.isVisible()); 696 | t.true(await win.isFocused()); 697 | 698 | const {width, height} = await win.getBounds(); 699 | t.true(width > 0); 700 | t.true(height > 0); 701 | }); 702 | ``` 703 | 704 | [preload]: http://electron.atom.io/docs/api/browser-window/#new-browserwindowoptions 705 | --------------------------------------------------------------------------------