From b1b40ab1a24b0f31d788a004880c311bf52b8ea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B3n=20Molleda?= Date: Thu, 4 Nov 2021 10:37:44 -0700 Subject: [PATCH] chore: fix sidebars --- docs/latest/README.md | 3 +- docs/latest/api/app.md | 8 +- docs/latest/api/browser-window.md | 2 +- docs/latest/api/clipboard.md | 2 +- docs/latest/tutorial/accessibility.md | 49 +++- .../automated-testing-with-a-custom-driver.md | 142 +++++++++ docs/latest/tutorial/automated-testing.md | 272 ------------------ .../tutorial/using-selenium-and-webdriver.md | 180 ++++++++++++ scripts/tasks/update-versions-info.js | 5 +- scripts/utils/git-commands.js | 2 +- sidebars.js | 3 +- versions-info.json | 7 +- 12 files changed, 389 insertions(+), 286 deletions(-) create mode 100644 docs/latest/tutorial/automated-testing-with-a-custom-driver.md delete mode 100644 docs/latest/tutorial/automated-testing.md create mode 100644 docs/latest/tutorial/using-selenium-and-webdriver.md diff --git a/docs/latest/README.md b/docs/latest/README.md index f5275f947..8418c360e 100644 --- a/docs/latest/README.md +++ b/docs/latest/README.md @@ -66,9 +66,10 @@ 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](latest/tutorial/automated-testing.md) + * [Automated Testing with a Custom Driver](latest/tutorial/automated-testing-with-a-custom-driver.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 e55e369db..2ac919dbf 100644 --- a/docs/latest/api/app.md +++ b/docs/latest/api/app.md @@ -43,10 +43,10 @@ Returns: * `launchInfo` Recordselection

') console.log(hasFormat) // 'true' or 'false' ``` diff --git a/docs/latest/tutorial/accessibility.md b/docs/latest/tutorial/accessibility.md index 6b56906ea..685d5b899 100644 --- a/docs/latest/tutorial/accessibility.md +++ b/docs/latest/tutorial/accessibility.md @@ -1,14 +1,55 @@ --- title: "Accessibility" -description: "Accessibility concerns in Electron applications are similar to those of websites because they're both ultimately HTML." +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." 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. +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. ## Manually enabling accessibility features @@ -50,6 +91,10 @@ 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 new file mode 100644 index 000000000..ea5263dc8 --- /dev/null +++ b/docs/latest/tutorial/automated-testing-with-a-custom-driver.md @@ -0,0 +1,142 @@ +--- +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 deleted file mode 100644 index 1d6cfc6a5..000000000 --- a/docs/latest/tutorial/automated-testing.md +++ /dev/null @@ -1,272 +0,0 @@ ---- -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 new file mode 100644 index 000000000..721209ad6 --- /dev/null +++ b/docs/latest/tutorial/using-selenium-and-webdriver.md @@ -0,0 +1,180 @@ +--- +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/scripts/tasks/update-versions-info.js b/scripts/tasks/update-versions-info.js index be3acc799..a6933c824 100644 --- a/scripts/tasks/update-versions-info.js +++ b/scripts/tasks/update-versions-info.js @@ -24,18 +24,21 @@ const createVersionEntry = (options) => { */ const updateVersionsInfo = async (latest) => { const current = await getCurrentBranchName(); - const versions = [createVersionEntry({ label: latest, version: 'latest' })]; + const versions = []; if (!/v\d+-x-y/.test(current)) { const branches = await getRemoteBranches(); const tracked = branches .map((branch) => branch.split('/').pop()) + .reverse() .filter((branch) => /v\d+-x-y/.test(branch)) .map((version) => createVersionEntry({ version })); versions.push(...tracked); } + versions.unshift(createVersionEntry({ label: latest, version: 'latest' })); + await fs.writeFile(VERSIONS_INFO, JSON.stringify(versions, null, 2), 'utf-8'); }; diff --git a/scripts/utils/git-commands.js b/scripts/utils/git-commands.js index 6c21176e2..43e0b593b 100644 --- a/scripts/utils/git-commands.js +++ b/scripts/utils/git-commands.js @@ -127,7 +127,7 @@ const getRemoteBranches = async () => { const branches = stdout .split('\n') .map((line) => line.trim()) - .filter((line) => !line.includes('->')); + .filter((line) => !line.includes('->') && line.startsWith('origin')); return branches; }; diff --git a/sidebars.js b/sidebars.js index 891e0b787..dfd3b4264 100644 --- a/sidebars.js +++ b/sidebars.js @@ -37,8 +37,7 @@ module.exports = { 'latest/tutorial/windows-arm', 'latest/tutorial/windows-taskbar', 'latest/tutorial/tray', - 'latest/tutorial/window-customization', - 'latest/tutorial/automated-testing', + 'latest/tutorial/window-customization' ], }, { diff --git a/versions-info.json b/versions-info.json index e7239537d..148b3736c 100644 --- a/versions-info.json +++ b/versions-info.json @@ -1,7 +1,12 @@ [ { - "label": "15-x-y", + "label": "Latest", "href": "https://electronjs.org/docs/latest", "target": "_blank" + }, + { + "label": "v14-x-y", + "href": "https://electronjs.org/docs/v14-x-y", + "target": "_blank" } ] \ No newline at end of file