diff --git a/docs/latest/README.md b/docs/latest/README.md index 8418c360e..f5275f947 100644 --- a/docs/latest/README.md +++ b/docs/latest/README.md @@ -66,10 +66,9 @@ an issue: * [Testing and Debugging](latest/tutorial/application-debugging.md) * [Debugging the Main Process](latest/tutorial/debugging-main-process.md) * [Debugging with Visual Studio Code](latest/tutorial/debugging-vscode.md) - * [Using Selenium and WebDriver](latest/tutorial/using-selenium-and-webdriver.md) * [Testing on Headless CI Systems (Travis, Jenkins)](latest/tutorial/testing-on-headless-ci.md) * [DevTools Extension](latest/tutorial/devtools-extension.md) - * [Automated Testing with a Custom Driver](latest/tutorial/automated-testing-with-a-custom-driver.md) + * [Automated Testing](latest/tutorial/automated-testing.md) * [REPL](latest/tutorial/repl.md) * [Distribution](latest/tutorial/application-distribution.md) * [Supported Platforms](latest/tutorial/support.md#supported-platforms) diff --git a/docs/latest/api/app.md b/docs/latest/api/app.md index 2ac919dbf..e55e369db 100644 --- a/docs/latest/api/app.md +++ b/docs/latest/api/app.md @@ -43,10 +43,10 @@ Returns: * `launchInfo` Recordselection

') +const hasFormat = clipboard.has('public/utf8-plain-text') console.log(hasFormat) // 'true' or 'false' ``` diff --git a/docs/latest/tutorial/accessibility.md b/docs/latest/tutorial/accessibility.md index 685d5b899..6b56906ea 100644 --- a/docs/latest/tutorial/accessibility.md +++ b/docs/latest/tutorial/accessibility.md @@ -1,55 +1,14 @@ --- title: "Accessibility" -description: "Making accessible applications is important and we're happy to provide functionality to Devtron and Spectron that gives developers the opportunity to make their apps better for everyone." +description: "Accessibility concerns in Electron applications are similar to those of websites because they're both ultimately HTML." slug: accessibility hide_title: false --- # Accessibility -Making accessible applications is important and we're happy to provide -functionality to [Devtron][devtron] and [Spectron][spectron] that gives -developers the opportunity to make their apps better for everyone. - ---- - Accessibility concerns in Electron applications are similar to those of -websites because they're both ultimately HTML. With Electron apps, however, -you can't use the online resources for accessibility audits because your app -doesn't have a URL to point the auditor to. - -These features bring those auditing tools to your Electron app. You can -choose to add audits to your tests with Spectron or use them within DevTools -with Devtron. Read on for a summary of the tools. - -## Spectron - -In the testing framework Spectron, you can now audit each window and `` -tag in your application. For example: - -```javascript -app.client.auditAccessibility().then(function (audit) { - if (audit.failed) { - console.error(audit.message) - } -}) -``` - -You can read more about this feature in [Spectron's documentation][spectron-a11y]. - -## Devtron - -In Devtron, there is an accessibility tab which will allow you to audit a -page in your app, sort and filter the results. - -![devtron screenshot][devtron-screenshot] - -Both of these tools are using the [Accessibility Developer Tools][a11y-devtools] -library built by Google for Chrome. You can learn more about the accessibility -audit rules this library uses on that [repository's wiki][a11y-devtools-wiki]. - -If you know of other great accessibility tools for Electron, add them to the -accessibility documentation with a pull request. +websites because they're both ultimately HTML. ## Manually enabling accessibility features @@ -91,10 +50,6 @@ CFStringRef kAXManualAccessibility = CFSTR("AXManualAccessibility"); } ``` -[devtron]: https://electronjs.org/devtron -[devtron-screenshot]: https://cloud.githubusercontent.com/assets/1305617/17156618/9f9bcd72-533f-11e6-880d-389115f40a2a.png -[spectron]: https://electronjs.org/spectron -[spectron-a11y]: https://github.com/electron/spectron#accessibility-testing [a11y-docs]: https://www.chromium.org/developers/design-documents/accessibility#TOC-How-Chrome-detects-the-presence-of-Assistive-Technology [a11y-devtools]: https://github.com/GoogleChrome/accessibility-developer-tools [a11y-devtools-wiki]: https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules diff --git a/docs/latest/tutorial/automated-testing-with-a-custom-driver.md b/docs/latest/tutorial/automated-testing-with-a-custom-driver.md deleted file mode 100644 index ea5263dc8..000000000 --- a/docs/latest/tutorial/automated-testing-with-a-custom-driver.md +++ /dev/null @@ -1,142 +0,0 @@ ---- -title: "Automated Testing with a Custom Driver" -description: "To write automated tests for your Electron app, you will need a way to \"drive\" your application. Spectron is a commonly-used solution which lets you emulate user actions via WebDriver. However, it's also possible to write your own custom driver using node's builtin IPC-over-STDIO. The benefit of a custom driver is that it tends to require less overhead than Spectron, and lets you expose custom methods to your test suite." -slug: automated-testing-with-a-custom-driver -hide_title: false ---- - -# Automated Testing with a Custom Driver - -To write automated tests for your Electron app, you will need a way to "drive" your application. [Spectron](https://electronjs.org/spectron) is a commonly-used solution which lets you emulate user actions via [WebDriver](https://webdriver.io/). However, it's also possible to write your own custom driver using node's builtin IPC-over-STDIO. The benefit of a custom driver is that it tends to require less overhead than Spectron, and lets you expose custom methods to your test suite. - -To create a custom driver, we'll use Node.js' [child_process](https://nodejs.org/api/child_process.html) API. The test suite will spawn the Electron process, then establish a simple messaging protocol: - -```js -const childProcess = require('child_process') -const electronPath = require('electron') - -// spawn the process -const env = { /* ... */ } -const stdio = ['inherit', 'inherit', 'inherit', 'ipc'] -const appProcess = childProcess.spawn(electronPath, ['./app'], { stdio, env }) - -// listen for IPC messages from the app -appProcess.on('message', (msg) => { - // ... -}) - -// send an IPC message to the app -appProcess.send({ my: 'message' }) -``` - -From within the Electron app, you can listen for messages and send replies using the Node.js [process](https://nodejs.org/api/process.html) API: - -```js -// listen for IPC messages from the test suite -process.on('message', (msg) => { - // ... -}) - -// send an IPC message to the test suite -process.send({ my: 'message' }) -``` - -We can now communicate from the test suite to the Electron app using the `appProcess` object. - -For convenience, you may want to wrap `appProcess` in a driver object that provides more high-level functions. Here is an example of how you can do this: - -```js -class TestDriver { - constructor ({ path, args, env }) { - this.rpcCalls = [] - - // start child process - env.APP_TEST_DRIVER = 1 // let the app know it should listen for messages - this.process = childProcess.spawn(path, args, { stdio: ['inherit', 'inherit', 'inherit', 'ipc'], env }) - - // handle rpc responses - this.process.on('message', (message) => { - // pop the handler - const rpcCall = this.rpcCalls[message.msgId] - if (!rpcCall) return - this.rpcCalls[message.msgId] = null - // reject/resolve - if (message.reject) rpcCall.reject(message.reject) - else rpcCall.resolve(message.resolve) - }) - - // wait for ready - this.isReady = this.rpc('isReady').catch((err) => { - console.error('Application failed to start', err) - this.stop() - process.exit(1) - }) - } - - // simple RPC call - // to use: driver.rpc('method', 1, 2, 3).then(...) - async rpc (cmd, ...args) { - // send rpc request - const msgId = this.rpcCalls.length - this.process.send({ msgId, cmd, args }) - return new Promise((resolve, reject) => this.rpcCalls.push({ resolve, reject })) - } - - stop () { - this.process.kill() - } -} -``` - -In the app, you'd need to write a simple handler for the RPC calls: - -```js -if (process.env.APP_TEST_DRIVER) { - process.on('message', onMessage) -} - -async function onMessage ({ msgId, cmd, args }) { - let method = METHODS[cmd] - if (!method) method = () => new Error('Invalid method: ' + cmd) - try { - const resolve = await method(...args) - process.send({ msgId, resolve }) - } catch (err) { - const reject = { - message: err.message, - stack: err.stack, - name: err.name - } - process.send({ msgId, reject }) - } -} - -const METHODS = { - isReady () { - // do any setup needed - return true - } - // define your RPC-able methods here -} -``` - -Then, in your test suite, you can use your test-driver as follows: - -```js -const test = require('ava') -const electronPath = require('electron') - -const app = new TestDriver({ - path: electronPath, - args: ['./app'], - env: { - NODE_ENV: 'test' - } -}) -test.before(async t => { - await app.isReady -}) -test.after.always('cleanup', async t => { - await app.stop() -}) -``` diff --git a/docs/latest/tutorial/automated-testing.md b/docs/latest/tutorial/automated-testing.md new file mode 100644 index 000000000..1d6cfc6a5 --- /dev/null +++ b/docs/latest/tutorial/automated-testing.md @@ -0,0 +1,272 @@ +--- +title: "Automated Testing" +description: "Test automation is an efficient way of validating that your application code works as intended. While Electron doesn't actively maintain its own testing solution, this guide will go over a couple ways you can run end-to-end automated tests on your Electron app." +slug: automated-testing +hide_title: false +--- + +# Automated Testing + +Test automation is an efficient way of validating that your application code works as intended. +While Electron doesn't actively maintain its own testing solution, this guide will go over a couple +ways you can run end-to-end automated tests on your Electron app. + +## Using the WebDriver interface + +From [ChromeDriver - WebDriver for Chrome][chrome-driver]: + +> WebDriver is an open source tool for automated testing of web apps across many +> browsers. It provides capabilities for navigating to web pages, user input, +> JavaScript execution, and more. ChromeDriver is a standalone server which +> implements WebDriver's wire protocol for Chromium. It is being developed by +> members of the Chromium and WebDriver teams. + +There are a few ways that you can set up testing using WebDriver. + +### With WebdriverIO + +[WebdriverIO](https://webdriver.io/) (WDIO) is a test automation framework that provides a +Node.js package for testing with WebDriver. Its ecosystem also includes various plugins +(e.g. reporter and services) that can help you put together your test setup. + +#### Install the testrunner + +First you need to run the WebdriverIO starter toolkit in your project root directory: + +```sh npm2yarn +npx wdio . --yes +``` + +This installs all necessary packages for you and generates a `wdio.conf.js` configuration file. + +#### Connect WDIO to your Electron app + +Update the capabilities in your configuration file to point to your Electron app binary: + +```javascript title='wdio.conf.js' +export.config = { + // ... + capabilities: [{ + browserName: 'chrome', + 'goog:chromeOptions': { + binary: '/path/to/your/electron/binary', // Path to your Electron binary. + args: [/* cli arguments */] // Optional, perhaps 'app=' + /path/to/your/app/ + } + }] + // ... +} +``` + +#### Run your tests + +To run your tests: + +```sh +$ npx wdio run wdio.conf.js +``` + +[chrome-driver]: https://sites.google.com/chromium.org/driver/ + +### With Selenium + +[Selenium](https://www.selenium.dev/) is a web automation framework that +exposes bindings to WebDriver APIs in many languages. Their Node.js bindings +are available under the `selenium-webdriver` package on NPM. + +#### Run a ChromeDriver server + +In order to use Selenium with Electron, you need to download the `electron-chromedriver` +binary, and run it: + +```sh npm2yarn +npm install --save-dev electron-chromedriver +./node_modules/.bin/chromedriver +Starting ChromeDriver (v2.10.291558) on port 9515 +Only local connections are allowed. +``` + +Remember the port number `9515`, which will be used later. + +#### Connect Selenium to ChromeDriver + +Next, install Selenium into your project: + +```sh npm2yarn +npm install --save-dev selenium-webdriver +``` + +Usage of `selenium-webdriver` with Electron is the same as with +normal websites, except that you have to manually specify how to connect +ChromeDriver and where to find the binary of your Electron app: + +```js title='test.js' +const webdriver = require('selenium-webdriver') +const driver = new webdriver.Builder() + // The "9515" is the port opened by ChromeDriver. + .usingServer('http://localhost:9515') + .withCapabilities({ + 'goog:chromeOptions': { + // Here is the path to your Electron binary. + binary: '/Path-to-Your-App.app/Contents/MacOS/Electron' + } + }) + .forBrowser('chrome') // note: use .forBrowser('electron') for selenium-webdriver <= 3.6.0 + .build() +driver.get('http://www.google.com') +driver.findElement(webdriver.By.name('q')).sendKeys('webdriver') +driver.findElement(webdriver.By.name('btnG')).click() +driver.wait(() => { + return driver.getTitle().then((title) => { + return title === 'webdriver - Google Search' + }) +}, 1000) +driver.quit() +``` + +## Using a custom test driver + +It's also possible to write your own custom driver using Node.js' built-in IPC-over-STDIO. +Custom test drivers require you to write additional app code, but have lower overhead and let you +expose custom methods to your test suite. + +To create a custom driver, we'll use Node.js' [`child_process`](https://nodejs.org/api/child_process.html) API. +The test suite will spawn the Electron process, then establish a simple messaging protocol: + +```js title='testDriver.js' +const childProcess = require('child_process') +const electronPath = require('electron') + +// spawn the process +const env = { /* ... */ } +const stdio = ['inherit', 'inherit', 'inherit', 'ipc'] +const appProcess = childProcess.spawn(electronPath, ['./app'], { stdio, env }) + +// listen for IPC messages from the app +appProcess.on('message', (msg) => { + // ... +}) + +// send an IPC message to the app +appProcess.send({ my: 'message' }) +``` + +From within the Electron app, you can listen for messages and send replies using the Node.js +[`process`](https://nodejs.org/api/process.html) API: + +```js title='main.js' +// listen for messages from the test suite +process.on('message', (msg) => { + // ... +}) + +// send a message to the test suite +process.send({ my: 'message' }) +``` + +We can now communicate from the test suite to the Electron app using the `appProcess` object. + +For convenience, you may want to wrap `appProcess` in a driver object that provides more +high-level functions. Here is an example of how you can do this. Let's start by creating +a `TestDriver` class: + +```js title='testDriver.js' +class TestDriver { + constructor ({ path, args, env }) { + this.rpcCalls = [] + + // start child process + env.APP_TEST_DRIVER = 1 // let the app know it should listen for messages + this.process = childProcess.spawn(path, args, { stdio: ['inherit', 'inherit', 'inherit', 'ipc'], env }) + + // handle rpc responses + this.process.on('message', (message) => { + // pop the handler + const rpcCall = this.rpcCalls[message.msgId] + if (!rpcCall) return + this.rpcCalls[message.msgId] = null + // reject/resolve + if (message.reject) rpcCall.reject(message.reject) + else rpcCall.resolve(message.resolve) + }) + + // wait for ready + this.isReady = this.rpc('isReady').catch((err) => { + console.error('Application failed to start', err) + this.stop() + process.exit(1) + }) + } + + // simple RPC call + // to use: driver.rpc('method', 1, 2, 3).then(...) + async rpc (cmd, ...args) { + // send rpc request + const msgId = this.rpcCalls.length + this.process.send({ msgId, cmd, args }) + return new Promise((resolve, reject) => this.rpcCalls.push({ resolve, reject })) + } + + stop () { + this.process.kill() + } +} + +module.exports = { TestDriver }; +``` + +In your app code, can then write a simple handler to receive RPC calls: + +```js title='main.js' +const METHODS = { + isReady () { + // do any setup needed + return true + } + // define your RPC-able methods here +} + +const onMessage = async ({ msgId, cmd, args }) => { + let method = METHODS[cmd] + if (!method) method = () => new Error('Invalid method: ' + cmd) + try { + const resolve = await method(...args) + process.send({ msgId, resolve }) + } catch (err) { + const reject = { + message: err.message, + stack: err.stack, + name: err.name + } + process.send({ msgId, reject }) + } +} + +if (process.env.APP_TEST_DRIVER) { + process.on('message', onMessage) +} +``` + +Then, in your test suite, you can use your `TestDriver` class with the test automation +framework of your choosing. The following example uses +[`ava`](https://www.npmjs.com/package/ava), but other popular choices like Jest +or Mocha would work as well: + +```js title='test.js' +const test = require('ava') +const electronPath = require('electron') +const { TestDriver } = require('./testDriver') + +const app = new TestDriver({ + path: electronPath, + args: ['./app'], + env: { + NODE_ENV: 'test' + } +}) +test.before(async t => { + await app.isReady +}) +test.after.always('cleanup', async t => { + await app.stop() +}) +``` diff --git a/docs/latest/tutorial/using-selenium-and-webdriver.md b/docs/latest/tutorial/using-selenium-and-webdriver.md deleted file mode 100644 index 721209ad6..000000000 --- a/docs/latest/tutorial/using-selenium-and-webdriver.md +++ /dev/null @@ -1,180 +0,0 @@ ---- -title: "Selenium and WebDriver" -description: "From ChromeDriver - WebDriver for Chrome:" -slug: using-selenium-and-webdriver -hide_title: false ---- - -# Selenium and WebDriver - -From [ChromeDriver - WebDriver for Chrome][chrome-driver]: - -> WebDriver is an open source tool for automated testing of web apps across many -> browsers. It provides capabilities for navigating to web pages, user input, -> JavaScript execution, and more. ChromeDriver is a standalone server which -> implements WebDriver's wire protocol for Chromium. It is being developed by -> members of the Chromium and WebDriver teams. - -## Setting up Spectron - -[Spectron][spectron] is the officially supported ChromeDriver testing framework -for Electron. It is built on top of [WebdriverIO](https://webdriver.io/) and -has helpers to access Electron APIs in your tests and bundles ChromeDriver. - -```sh -$ npm install --save-dev spectron -``` - -```javascript -// A simple test to verify a visible window is opened with a title -const Application = require('spectron').Application -const assert = require('assert') - -const myApp = new Application({ - path: '/Applications/MyApp.app/Contents/MacOS/MyApp' -}) - -const verifyWindowIsVisibleWithTitle = async (app) => { - await app.start() - try { - // Check if the window is visible - const isVisible = await app.browserWindow.isVisible() - // Verify the window is visible - assert.strictEqual(isVisible, true) - // Get the window's title - const title = await app.client.getTitle() - // Verify the window's title - assert.strictEqual(title, 'My App') - } catch (error) { - // Log any failures - console.error('Test failed', error.message) - } - // Stop the application - await app.stop() -} - -verifyWindowIsVisibleWithTitle(myApp) -``` - -## Setting up with WebDriverJs - -[WebDriverJs](https://www.selenium.dev/selenium/docs/api/javascript/index.html) provides -a Node package for testing with web driver, we will use it as an example. - -### 1. Start ChromeDriver - -First you need to download the `chromedriver` binary, and run it: - -```sh -$ npm install electron-chromedriver -$ ./node_modules/.bin/chromedriver -Starting ChromeDriver (v2.10.291558) on port 9515 -Only local connections are allowed. -``` - -Remember the port number `9515`, which will be used later - -### 2. Install WebDriverJS - -```sh -$ npm install selenium-webdriver -``` - -### 3. Connect to ChromeDriver - -The usage of `selenium-webdriver` with Electron is the same with -upstream, except that you have to manually specify how to connect -chrome driver and where to find Electron's binary: - -```javascript -const webdriver = require('selenium-webdriver') - -const driver = new webdriver.Builder() - // The "9515" is the port opened by chrome driver. - .usingServer('http://localhost:9515') - .withCapabilities({ - 'goog:chromeOptions': { - // Here is the path to your Electron binary. - binary: '/Path-to-Your-App.app/Contents/MacOS/Electron' - } - }) - .forBrowser('chrome') // note: use .forBrowser('electron') for selenium-webdriver <= 3.6.0 - .build() - -driver.get('http://www.google.com') -driver.findElement(webdriver.By.name('q')).sendKeys('webdriver') -driver.findElement(webdriver.By.name('btnG')).click() -driver.wait(() => { - return driver.getTitle().then((title) => { - return title === 'webdriver - Google Search' - }) -}, 1000) - -driver.quit() -``` - -## Setting up with WebdriverIO - -[WebdriverIO](https://webdriver.io/) provides a Node package for testing with web -driver. - -### 1. Start ChromeDriver - -First you need to download the `chromedriver` binary, and run it: - -```sh -$ npm install electron-chromedriver -$ ./node_modules/.bin/chromedriver --url-base=wd/hub --port=9515 -Starting ChromeDriver (v2.10.291558) on port 9515 -Only local connections are allowed. -``` - -Remember the port number `9515`, which will be used later - -### 2. Install WebdriverIO - -```sh -$ npm install webdriverio -``` - -### 3. Connect to chrome driver - -```javascript -const webdriverio = require('webdriverio') -const options = { - host: 'localhost', // Use localhost as chrome driver server - port: 9515, // "9515" is the port opened by chrome driver. - desiredCapabilities: { - browserName: 'chrome', - 'goog:chromeOptions': { - binary: '/Path-to-Your-App/electron', // Path to your Electron binary. - args: [/* cli arguments */] // Optional, perhaps 'app=' + /path/to/your/app/ - } - } -} - -const client = webdriverio.remote(options) - -client - .init() - .url('http://google.com') - .setValue('#q', 'webdriverio') - .click('#btnG') - .getTitle().then((title) => { - console.log('Title was: ' + title) - }) - .end() -``` - -## Workflow - -To test your application without rebuilding Electron, -[place](latest/tutorial/application-distribution.md) -your app source into Electron's resource directory. - -Alternatively, pass an argument to run with your Electron binary that points to -your app's folder. This eliminates the need to copy-paste your app into -Electron's resource directory. - -[chrome-driver]: https://sites.google.com/a/chromium.org/chromedriver/ -[spectron]: https://electronjs.org/spectron diff --git a/package.json b/package.json index bb566ed20..b46e6e8d7 100644 --- a/package.json +++ b/package.json @@ -77,5 +77,5 @@ "tar-stream": "^2.2.0", "unist-util-visit-parents": "^3.1.1" }, - "sha": "e9446f0dc425dee443a0d279af44cb686f3b9db4" + "sha": "15038974ad1a10b1c3d74d350c545723b0ed639c" } diff --git a/sidebars.js b/sidebars.js index dfd3b4264..ea43954bc 100644 --- a/sidebars.js +++ b/sidebars.js @@ -37,7 +37,7 @@ module.exports = { 'latest/tutorial/windows-arm', 'latest/tutorial/windows-taskbar', 'latest/tutorial/tray', - 'latest/tutorial/window-customization' + 'latest/tutorial/window-customization', ], }, { @@ -74,13 +74,12 @@ module.exports = { type: 'category', label: 'Testing And Debugging', items: [ - 'latest/tutorial/using-selenium-and-webdriver', + 'latest/tutorial/automated-testing', 'latest/tutorial/debugging-main-process', 'latest/tutorial/debugging-vscode', 'latest/tutorial/repl', 'latest/tutorial/devtools-extension', 'latest/tutorial/application-debugging', - 'latest/tutorial/automated-testing-with-a-custom-driver', 'latest/tutorial/testing-on-headless-ci', 'latest/tutorial/testing-widevine-cdm', ],