diff --git a/.gitignore b/.gitignore index 93f136199..9f42f5f7b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules npm-debug.log +lib/api.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e159a125..47d95520f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# 2.37.0 + + * Added a `restart()` method to `Application` + * Added support for the full Electron API + * Many custom helpers have been removed in favor of accessing the Electron + APIs directly through the new properties on the `Application` object. + * `app.client.getWindowBounds()` should now be `app.browserWindow.getBounds()` + * `app.client.getClipboardText()` should now be `app.electron.clipboard.readText()` + * See the README or https://github.com/kevinsawicki/spectron/pull/18 for + more details. + * You should now use `app.transferPromiseness` instead of `app.client.transferPromiseness` + to ensure these new properties are correctly transferred to chained promises. + # 1.37.1 * Add the `getAppPath(name)` that maps to the diff --git a/README.md b/README.md index c37d08f63..7b238e979 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Easily test your [Electron](http://electron.atom.io) apps using This minor version of this library tracks the minor version of the Electron versions released. So if you are using Electron `0.37.x` you would want to use -a `spectron` dependency of `~1.37` in your `package.json` file. +a `spectron` dependency of `~2.37` in your `package.json` file. Learn more from [this presentation](https://speakerdeck.com/kevinsawicki/testing-your-electron-apps-with-chromedriver). @@ -57,147 +57,12 @@ describe('application launch', function () { }) ``` -### With Chai As Promised - -WebdriverIO is promise-based and so it pairs really well with the -[Chai as Promised](https://github.com/domenic/chai-as-promised) library that -builds on top of [Chai](http://chaijs.com). +## Application API -Using these together allows you to chain assertions together and have fewer -callback blocks. See below for a simple example: - -```sh -npm install --save-dev chai -npm install --save-dev chai-as-promised -``` - -```js -var Application = require('spectron').Application -var chai = require('chai') -var chaiAsPromised = require('chai-as-promised') -var path = require('path') - -chai.should() -chai.use(chaiAsPromised) +Spectron exports an `Application` class that when configured, can start and +stop your Electron application. -describe('application launch', function () { - beforeEach(function () { - this.app = new Application({ - path: '/Applications/MyApp.app/Contents/MacOS/MyApp' - }) - return this.app.start() - }) - - beforeEach(function () { - chaiAsPromised.transferPromiseness = this.app.client.transferPromiseness - }) - - afterEach(function () { - if (this.app && this.app.isRunning()) { - return this.app.stop() - } - }) - - it('opens a window', function () { - return this.app.client.waitUntilWindowLoaded() - .getWindowCount().should.eventually.equal(1) - .isWindowMinimized().should.eventually.be.false - .isWindowDevToolsOpened().should.eventually.be.false - .isWindowVisible().should.eventually.be.true - .isWindowFocused().should.eventually.be.true - .getWindowWidth().should.eventually.be.above(0) - .getWindowHeight().should.eventually.be.above(0) - }) -}) -``` - -### With AVA - -Spectron works with [AVA](https://github.com/sindresorhus/ava) which allows you -to write your tests in ES2015 without extra support. - -```js -'use strict'; - -import test from 'ava'; -import {Application} from 'spectron'; - -test.beforeEach(t => { - t.context.app = new Application({ - path: '/Applications/MyApp.app/Contents/MacOS/MyApp' - }); - - return t.context.app.start(); -}); - -test.afterEach(t => { - return t.context.app.stop(); -}); - -test(t => { - return t.context.app.client.waitUntilWindowLoaded() - .getWindowCount().then(count => { - t.is(count, 1); - }).isWindowMinimized().then(min => { - t.false(min); - }).isWindowDevToolsOpened().then(opened => { - t.false(opened); - }).isWindowVisible().then(visible => { - t.true(visible); - }).isWindowFocused().then(focused => { - t.true(focused); - }).getWindowWidth().then(width => { - t.ok(width > 0); - }).getWindowHeight().then(height => { - t.ok(height > 0); - }); -}); -``` - -AVA supports ECMAScript advanced features not only promise but also async/await. - -```js -test(async t => { - await t.context.app.client.waitUntilWindowLoaded(); - t.is(1, await app.client.getWindowCount()); - t.false(await app.client.isWindowMinimized()); - t.false(await app.client.isWindowDevToolsOpened()); - t.true(await app.client.isWindowVisible()); - t.true(await app.client.isWindowFocused()); - t.ok(await app.client.getWindowWidth() > 0); - t.ok(await app.client.getWindowHeight() > 0); -}); -``` - -### On Travis CI - -You will want to add the following to your `.travis.yml` file when building on -Linux: - -```yml -before_script: - - "export DISPLAY=:99.0" - - "sh -e /etc/init.d/xvfb start" - - sleep 3 # give xvfb some time to start -``` - -Check out Spectron's [.travis.yml](https://github.com/kevinsawicki/spectron/blob/master/.travis.yml) -file for a production example. - -### On AppVeyor - -You will want to add the following to your `appveyor.yml` file: - -```yml -os: unstable -``` - -Check out Spectron's [appveyor.yml](https://github.com/kevinsawicki/spectron/blob/master/appveyor.yml) -file for a production example. - -### Application - -#### new Application(options) +### new Application(options) Create a new application with the following options: @@ -227,19 +92,9 @@ Create a new application with the following options: `waitUntilTextExists` and `waitUntilWindowLoaded` to complete. Defaults to `5000` milliseconds. +### Properties -#### start() - -Starts the application. Returns a `Promise` that will be resolved when the -application is ready to use. You should always wait for start to complete -before running any commands. - -#### stop() - -Stops the application. Returns a `Promise` that will be resolved once the -application has stopped. - -### Client Commands +#### client Spectron uses [WebdriverIO](http://webdriver.io) and exposes the managed `client` property on the created `Application` instances. @@ -251,47 +106,114 @@ Several additional commands are provided specific to Electron. All the commands return a `Promise`. -#### getAppPath(name) +#### electron + +The `electron` property is your gateway to accessing the full Electron API. + +Each Electron module is exposed as a property on the `electron` property +so you can think of it as an alias for `require('electron')` from within your +app. + +So if you wanted to access the [clipboard](http://electron.atom.io/docs/latest/api/clipboard) +API in your tests you would do: + +```js +app.electron.clipboard.writeText('pasta') + .electron.clipboard.readText().then(function (clipboardText) { + console.log('The clipboard text is ' + clipboardText) + }) +``` + +#### browserWindow + +The `browserWindow` property is an alias for `require('electron').remote.getCurrentWindow()`. + +It provides you access to the current [BrowserWindow](http://electron.atom.io/docs/latest/api/browser-window/) +and contains all the APIs. -Get the path using the `require('electron').app.getPath(name)` API. +So if you wanted to check if the current window is visible in your tests you +would do: ```js -app.client.getAppPath('userData').then(function (userDataPath) { - console.log(userDataPath) +app.browserWindow.isVisible().then(function (visible) { + console.log('window is visible? ' + visible) }) ``` -#### getArgv() +It is named `browserWindow` instead of `window` so that it doesn't collide +with the WebDriver command of that name. -Get the `argv` array from the main process. +#### webContents + +The `browserWindow` property is an alias for `require('electron').remote.getCurrentWebContents()`. + +It provides you access to the [WebContents](http://electron.atom.io/docs/latest/api/web-contents/) +for the current window and contains all the APIs. + +So if you wanted to check if the current window is loading in your tests you +would do: ```js -app.client.getArgv().then(function (argv) { - console.log(argv) +app.webContents.isLoading().then(function (visible) { + console.log('window is loading? ' + visible) }) ``` -#### getClipboardText() +#### mainProcess + +The `mainProcess` property is an alias for `require('electron').remote.process`. + +It provides you access to the main process's [process](https://nodejs.org/api/process.html) +global. -Gets the clipboard text. +So if you wanted to get the `argv` for the main process in your tests you would +do: ```js -app.client.getClipboardText().then(function (clipboardText) { - console.log(clipboardText) +app.mainProcess.argv().then(function (argv) { + console.log('main process args: ' + argv) }) ``` -#### getCwd() +Properties on the `process` are exposed as functions that return promises so +make sure to call `mainProcess.env().then(...)` instead of +`mainProcess.env.then(...)`. -Get the current working directory of the main process. +#### rendererProcess + +The `rendererProcess` property is an alias for `global.process`. + +It provides you access to the renderer process's [process](https://nodejs.org/api/process.html) +global. + +So if you wanted to get the environment variables for the renderer process in +your tests you would do: ```js -app.client.getCwd().then(function (cwd) { - console.log(cwd) +app.rendererProcess.env().then(function (env) { + console.log('main process args: ' + env) }) ``` -#### getMainProcessLogs() +### Methods + +#### start() + +Starts the application. Returns a `Promise` that will be resolved when the +application is ready to use. You should always wait for start to complete +before running any commands. + +#### stop() + +Stops the application. Returns a `Promise` that will be resolved once the +application has stopped. + +#### restart() + +Stops the application and then starts it. Returns a `Promise` that will be +resolved once the application has started again. + +#### client.getMainProcessLogs() Gets the `console` log output from the main process. The logs are cleared after they are returned. @@ -306,17 +228,7 @@ app.client.getMainProcessLogs().then(function (logs) { }) ``` -#### getMainProcessGlobal(globalName) - -Gets a global from the main process by name. - -```js -app.client.getMainProcessGlobal('aGlobal').then(function (globalValue) { - console.log(globalValue) -}) -``` - -#### getRenderProcessLogs() +#### client.getRenderProcessLogs() Gets the `console` log output from the render process. The logs are cleared after they are returned. @@ -333,17 +245,7 @@ app.client.getRenderProcessLogs().then(function (logs) { }) ``` -#### getRepresentedFilename() - -Gets the represented file name. Only supported on Mac OS X. - -```js -app.client.getRepresentedFilename().then(function (filename) { - console.log(filename) -}) -``` - -#### getSelectedText() +#### client.getSelectedText() Get the selected text in the current window. @@ -353,7 +255,7 @@ app.client.getSelectedText().then(function (selectedText) { }) ``` -#### getWindowCount() +#### client.getWindowCount() Gets the number of open windows. @@ -363,189 +265,170 @@ app.client.getWindowCount().then(function (count) { }) ``` -#### getWindowBounds() - -Gets the bounds of the current window. Object returned has -`x`, `y`, `width`, and `height` properties. - -```js -app.client.getWindowBounds().then(function (bounds) { - console.log(bounds.x, bounds.y, bounds.width, bounds.height) -}) -``` - -#### getWindowHeight() - -Get the height of the current window. - -```js -app.client.getWindowHeight().then(function (height) { - console.log(height) -}) -``` - -#### getWindowWidth() +#### client.waitUntilTextExists(selector, text, [timeout]) -Get the width of the current window. - -```js -app.client.getWindowWidth().then(function (width) { - console.log(width) -}) -``` - -#### isDocumentEdited() - -Returns true if the document is edited, false otherwise. Only supported on -Mac OS X. +Waits until the element matching the given selector contains the given +text. Takes an optional timeout in milliseconds that defaults to `5000`. ```js -app.client.isDocumentEdited().then(function (edited) { - console.log(edited) -}) +app.client.waitUntilTextExists('#message', 'Success', 10000) ``` -#### isWindowDevToolsOpened() +#### client.waitUntilWindowLoaded([timeout]) -Returns whether the current window's dev tools are opened. +Wait until the window is no longer loading. Takes an optional timeout +in milliseconds that defaults to `5000`. ```js -app.client.isWindowDevToolsOpened().then(function (devToolsOpened) { - console.log(devToolsOpened) -}) +app.client.waitUntilWindowLoaded(10000) ``` -#### isWindowFocused() +#### client.windowByIndex(index) -Returns whether the current window has focus. +Focus a window using its index from the `windowHandles()` array. ```js -app.client.isWindowFocused().then(function (focused) { - console.log(focused) -}) +app.client.windowByIndex(1) ``` -#### isWindowFullScreen() - -Returns whether the current window is in full screen mode. +## Continuous Integration -```js -app.client.isWindowFullScreen().then(function (fullScreen) { - console.log(fullScreen) -}) -``` - -#### isWindowLoading() +### On Travis CI -Returns whether the current window is loading. +You will want to add the following to your `.travis.yml` file when building on +Linux: -```js -app.client.isWindowLoading().then(function (loading) { - console.log(loading) -}) +```yml +before_script: + - "export DISPLAY=:99.0" + - "sh -e /etc/init.d/xvfb start" + - sleep 3 # give xvfb some time to start ``` -#### isWindowMaximized() - -Returns whether the current window is maximized. - -```js -app.client.isWindowMaximized().then(function (maximized) { - console.log(maximized) -}) -``` +Check out Spectron's [.travis.yml](https://github.com/kevinsawicki/spectron/blob/master/.travis.yml) +file for a production example. -#### isWindowMinimized() +### On AppVeyor -Returns whether the current window is minimized. +You will want to add the following to your `appveyor.yml` file: -```js -app.client.isWindowMinimized().then(function (minimized) { - console.log(minimized) -}) +```yml +os: unstable ``` -#### isWindowVisible() - -Returns whether the current window is visible. - -```js -app.client.isWindowVisible().then(function (visible) { - console.log(visible) -}) -``` +Check out Spectron's [appveyor.yml](https://github.com/kevinsawicki/spectron/blob/master/appveyor.yml) +file for a production example. -#### paste() -Paste the text from the clipboard in the current window. +## Test Library Examples -```js -app.client.paste() -``` +### With Chai As Promised -#### selectAll() +WebdriverIO is promise-based and so it pairs really well with the +[Chai as Promised](https://github.com/domenic/chai-as-promised) library that +builds on top of [Chai](http://chaijs.com). -Select all the text in the current window. +Using these together allows you to chain assertions together and have fewer +callback blocks. See below for a simple example: -```js -app.client.selectAll() +```sh +npm install --save-dev chai +npm install --save-dev chai-as-promised ``` -#### setClipboardText(clipboardText) - -Sets the clipboard text. - ```js -app.client.setClipboardText('pasta') -``` - -#### setDocumentEdited(edited) +var Application = require('spectron').Application +var chai = require('chai') +var chaiAsPromised = require('chai-as-promised') +var path = require('path') -Sets the document edited state. Only supported on Mac OS X. +chai.should() +chai.use(chaiAsPromised) -```js -app.client.setDocumentEdited(true) -``` +describe('application launch', function () { + beforeEach(function () { + this.app = new Application({ + path: '/Applications/MyApp.app/Contents/MacOS/MyApp' + }) + return this.app.start() + }) -#### setRepresentedFilename(filename) + beforeEach(function () { + chaiAsPromised.transferPromiseness = this.app.transferPromiseness + }) -Sets the represented file name. Only supported on Mac OS X. + afterEach(function () { + if (this.app && this.app.isRunning()) { + return this.app.stop() + } + }) -```js -app.client.setRepresentedFilename('/foo.js') + it('opens a window', function () { + return this.app.client.waitUntilWindowLoaded() + .getWindowCount().should.eventually.equal(1) + .browserWindow.isMinimized().should.eventually.be.false + .browserWindow.isDevToolsOpened().should.eventually.be.false + .browserWindow.isVisible().should.eventually.be.true + .browserWindow.isFocused().should.eventually.be.true + .browserWindow.getBounds().should.eventually.have.property('width').and.be.above(0) + .browserWindow.getBounds().should.eventually.have.property('height').and.be.above(0) + }) +}) ``` -#### setWindowBounds(bounds) +### With AVA -Sets the window position and size. The bounds object should have `x`, `y`, -`height`, and `width` keys. +Spectron works with [AVA](https://github.com/sindresorhus/ava) which allows you +to write your tests in ES2015 without extra support. ```js -app.client.setWindowBounds({x: 100, y: 200, width: 50, height: 75}) -``` - -#### waitUntilTextExists(selector, text, [timeout]) +'use strict'; -Waits until the element matching the given selector contains the given -text. Takes an optional timeout in milliseconds that defaults to `5000`. +import test from 'ava'; +import {Application} from 'spectron'; -```js -app.client.waitUntilTextExists('#message', 'Success', 10000) -``` +test.beforeEach(t => { + t.context.app = new Application({ + path: '/Applications/MyApp.app/Contents/MacOS/MyApp' + }); -#### waitUntilWindowLoaded([timeout]) + return t.context.app.start(); +}); -Wait until the window is no longer loading. Takes an optional timeout -in milliseconds that defaults to `5000`. +test.afterEach(t => { + return t.context.app.stop(); +}); -```js -app.client.waitUntilWindowLoaded(10000) +test(t => { + return t.context.app.client.waitUntilWindowLoaded() + .getWindowCount().then(count => { + t.is(count, 1); + }).browserWindow.isMinimized().then(min => { + t.false(min); + }).browserWindow.isDevToolsOpened().then(opened => { + t.false(opened); + }).browserWindow.isVisible().then(visible => { + t.true(visible); + }).browserWindow.isFocused().then(focused => { + t.true(focused); + }).browserWindow.getBounds().then(bounds => { + t.ok(bounds.width > 0); + t.ok(bounds.height > 0); + }); +}); ``` -#### windowByIndex(index) - -Focus a window using its index from the `windowHandles()` array. +AVA supports ECMAScript advanced features not only promise but also async/await. ```js -app.client.windowByIndex(1) +test(async t => { + await t.context.app.client.waitUntilWindowLoaded(); + t.is(1, await app.client.getWindowCount()); + t.false(await app.browserWindow.isMinimized()); + t.false(await app.browserWindow.isDevToolsOpened()); + t.true(await app.browserWindow.isVisible()); + t.true(await app.browserWindow.isFocused()); + t.ok((await app.browserWindow.getBounds()).width > 0); + t.ok((await app.browserWindow.getBounds()).height > 0); +}); ``` diff --git a/lib/api.js b/lib/api.js new file mode 100644 index 000000000..0bdb39e46 --- /dev/null +++ b/lib/api.js @@ -0,0 +1,387 @@ +var apiCache = {} + +function Api (app) { + this.app = app +} + +Api.prototype.initialize = function () { + return this.load().then(this.addApiCommands.bind(this)) +} + +Api.prototype.addApiCommands = function (api) { + this.addRenderProcessApis(api.electron) + this.addMainProcessApis(api.electron.remote) + this.addBrowserWindowApis(api.browserWindow) + this.addWebContentsApis(api.webContents) + this.addProcessApis(api.rendererProcess) + + this.api = { + browserWindow: api.browserWindow, + electron: api.electron, + rendererProcess: api.rendererProcess, + webContents: api.webContents + } + + this.addClientProperties() +} + +Api.prototype.load = function () { + var self = this + return this.getVersion().then(function (version) { + var api = apiCache[version] + if (api) return api + + return self.loadApi().then(function (api) { + apiCache[version] = api + return api + }) + }) +} + +Api.prototype.getVersion = function () { + return this.app.client.execute(function () { + return process.versions.electron + }).then(getResponseValue) +} + +Api.prototype.loadApi = function () { + return this.app.client.execute(function () { + var electron = require('electron') + + var api = { + browserWindow: {}, + electron: {}, + rendererProcess: {}, + webContents: {} + } + + function ignoreModule (moduleName) { + switch (moduleName) { + case 'CallbacksRegistry': + case 'deprecate': + case 'deprecations': + case 'hideInternalModules': + case 'Tray': + return true + } + return false + } + + function isRemoteFunction (name) { + switch (name) { + case 'BrowserWindow': + case 'Menu': + case 'MenuItem': + return false + } + return typeof electron.remote[name] === 'function' + } + + function ignoreApi (apiName) { + switch (apiName) { + case 'prototype': + return true + default: + return apiName[0] === '_' + } + } + + function addModule (parent, parentName, name, api) { + api[name] = {} + for (var key in parent[name]) { + if (ignoreApi(key)) continue + api[name][key] = parentName + '.' + name + '.' + key + } + } + + function addRenderProcessModules () { + Object.getOwnPropertyNames(electron).forEach(function (key) { + if (ignoreModule(key)) return + if (key === 'remote') return + addModule(electron, 'electron', key, api.electron) + }) + } + + function addMainProcessModules () { + api.electron.remote = {} + Object.getOwnPropertyNames(electron.remote).forEach(function (key) { + if (ignoreModule(key)) return + if (isRemoteFunction(key)) { + api.electron.remote[key] = 'electron.remote.' + key + } else { + addModule(electron.remote, 'electron.remote', key, api.electron.remote) + } + }) + addModule(electron.remote, 'electron.remote', 'process', api.electron.remote) + } + + function addBrowserWindow () { + var currentWindow = electron.remote.getCurrentWindow() + for (var name in currentWindow) { + if (ignoreApi(name)) continue + var value = currentWindow[name] + if (typeof value === 'function') { + api.browserWindow[name] = 'browserWindow.' + name + } + } + } + + function addWebContents () { + var webContents = electron.remote.getCurrentWebContents() + for (var name in webContents) { + if (ignoreApi(name)) continue + var value = webContents[name] + if (typeof value === 'function') { + api.webContents[name] = 'webContents.' + name + } + } + } + + function addProcess () { + for (var name in process) { + if (ignoreApi(name)) continue + api.rendererProcess[name] = 'process.' + name + } + } + + addRenderProcessModules() + addMainProcessModules() + addBrowserWindow() + addWebContents() + addProcess() + + return api + }).then(getResponseValue) +} + +Api.prototype.addClientProperty = function (name) { + var self = this + + var clientPrototype = Object.getPrototypeOf(self.app.client) + Object.defineProperty(clientPrototype, name, { + get: function () { + var client = this + return transformObject(self.api[name], {}, function (value) { + return client[value].bind(client) + }) + } + }) +} + +Api.prototype.addClientProperties = function () { + this.addClientProperty('electron') + this.addClientProperty('browserWindow') + this.addClientProperty('webContents') + this.addClientProperty('rendererProcess') + + Object.defineProperty(Object.getPrototypeOf(this.app.client), 'mainProcess', { + get: function () { + return this.electron.remote.process + } + }) +} + +Api.prototype.addRenderProcessApis = function (api) { + var app = this.app + var electron = {} + app.electron = electron + + Object.keys(api).forEach(function (moduleName) { + if (moduleName === 'remote') return + electron[moduleName] = {} + var moduleApi = api[moduleName] + + Object.keys(moduleApi).forEach(function (key) { + var commandName = moduleApi[key] + + app.client.addCommand(commandName, function () { + var args = Array.prototype.slice.call(arguments) + return this.execute(callRenderApi, moduleName, key, args).then(getResponseValue) + }) + + electron[moduleName][key] = function () { + return app.client[commandName].apply(app.client, arguments) + } + }) + }) +} + +Api.prototype.addMainProcessApis = function (api) { + var app = this.app + var remote = {} + app.electron.remote = remote + + Object.keys(api).filter(function (propertyName) { + return typeof api[propertyName] === 'string' + }).forEach(function (name) { + var commandName = api[name] + + app.client.addCommand(commandName, function () { + var args = Array.prototype.slice.call(arguments) + return this.execute(callMainApi, null, name, args).then(getResponseValue) + }) + + remote[name] = function () { + return app.client[commandName].apply(app.client, arguments) + } + }) + + Object.keys(api).filter(function (moduleName) { + return typeof api[moduleName] === 'object' + }).forEach(function (moduleName) { + remote[moduleName] = {} + var moduleApi = api[moduleName] + + Object.keys(moduleApi).forEach(function (key) { + var commandName = moduleApi[key] + + app.client.addCommand(commandName, function () { + var args = Array.prototype.slice.call(arguments) + return this.execute(callMainApi, moduleName, key, args).then(getResponseValue) + }) + + remote[moduleName][key] = function () { + return app.client[commandName].apply(app.client, arguments) + } + }) + }) +} + +Api.prototype.addBrowserWindowApis = function (api) { + var app = this.app + app.browserWindow = {} + + Object.keys(api).forEach(function (name) { + var commandName = api[name] + + app.client.addCommand(commandName, function () { + var args = Array.prototype.slice.call(arguments) + return this.execute(callBrowserWindowApi, name, args).then(getResponseValue) + }) + + app.browserWindow[name] = function () { + return app.client[commandName].apply(app.client, arguments) + } + }) +} + +Api.prototype.addWebContentsApis = function (api) { + var app = this.app + app.webContents = {} + + Object.keys(api).forEach(function (name) { + var commandName = api[name] + + app.client.addCommand(commandName, function () { + var args = Array.prototype.slice.call(arguments) + return this.execute(callWebContentsApi, name, args).then(getResponseValue) + }) + + app.webContents[name] = function () { + return app.client[commandName].apply(app.client, arguments) + } + }) +} + +Api.prototype.addProcessApis = function (api) { + var app = this.app + app.rendererProcess = {} + + Object.keys(api).forEach(function (name) { + var commandName = api[name] + + app.client.addCommand(commandName, function () { + var args = Array.prototype.slice.call(arguments) + return this.execute(callProcessApi, name, args).then(getResponseValue) + }) + + app.rendererProcess[name] = function () { + return app.client[commandName].apply(app.client, arguments) + } + }) + + app.mainProcess = app.electron.remote.process +} + +Api.prototype.transferPromiseness = function (target, promise) { + this.app.client.transferPromiseness(target, promise) + + var addProperties = function (source, target, moduleName) { + var sourceModule = source[moduleName] + if (!sourceModule) return + target[moduleName] = transformObject(sourceModule, {}, function (value, parent) { + return value.bind(parent) + }) + } + + addProperties(promise, target, 'webContents') + addProperties(promise, target, 'browserWindow') + addProperties(promise, target, 'electron') + addProperties(promise, target, 'mainProcess') + addProperties(promise, target, 'rendererProcess') +} + +Api.prototype.logApi = function () { + var fs = require('fs') + var path = require('path') + var json = JSON.stringify(this.api, null, 2) + fs.writeFileSync(path.join(__dirname, 'api.json'), json) +} + +function transformObject (input, output, callback) { + Object.keys(input).forEach(function (name) { + var value = input[name] + if (typeof value === 'object') { + output[name] = {} + transformObject(value, output[name], callback) + } else { + output[name] = callback(value, input) + } + }) + return output +} + +function callRenderApi (moduleName, api, args) { + var module = require('electron')[moduleName] + if (typeof module[api] === 'function') { + return module[api].apply(module, args) + } else { + return module[api] + } +} + +function callMainApi (moduleName, api, args) { + var module = require('electron').remote + if (moduleName) { + module = module[moduleName] + } + if (typeof module[api] === 'function') { + return module[api].apply(module, args) + } else { + return module[api] + } +} + +function callWebContentsApi (name, args) { + var webContents = require('electron').remote.getCurrentWebContents() + return webContents[name].apply(webContents, args) +} + +function callBrowserWindowApi (name, args) { + var window = require('electron').remote.getCurrentWindow() + return window[name].apply(window, args) +} + +function callProcessApi (name, args) { + if (typeof process[name] === 'function') { + return process[name].apply(process, args) + } else { + return process[name] + } +} + +function getResponseValue (response) { + return response.value +} + +module.exports = Api diff --git a/lib/application.js b/lib/application.js index 1aa3e5725..7ee1dc69b 100644 --- a/lib/application.js +++ b/lib/application.js @@ -1,3 +1,4 @@ +var Api = require('./api') var ChromeDriver = require('./chrome-driver') var DevNull = require('dev-null') var fs = require('fs') @@ -19,6 +20,15 @@ function Application (options) { this.args = options.args || [] this.env = options.env || {} this.workingDirectory = options.cwd || process.cwd() + this.api = new Api(this) + this.setupPromiseness() +} + +Application.prototype.setupPromiseness = function () { + var self = this + self.transferPromiseness = function (target, promise) { + self.api.transferPromiseness(target, promise) + } } Application.prototype.start = function () { @@ -26,7 +36,9 @@ Application.prototype.start = function () { return self.exists() .then(function () { return self.startChromeDriver() }) .then(function () { return self.createClient() }) + .then(function () { return self.api.initialize() }) .then(function () { self.running = true }) + .then(function () { return self }) } Application.prototype.stop = function () { @@ -35,18 +47,25 @@ Application.prototype.stop = function () { if (!self.isRunning()) return Promise.reject(Error('Application not running')) return new Promise(function (resolve, reject) { - self.client.windowByIndex(0).quitApplication().then(function () { + self.client.windowByIndex(0).electron.remote.app.quit().then(function () { setTimeout(function () { self.client.end().then(function () { self.chromeDriver.stop() self.running = false - resolve() + resolve(self) }, reject) }, self.quitTimeout) }, reject) }) } +Application.prototype.restart = function () { + var self = this + return self.stop().then(function () { + return self.start() + }) +} + Application.prototype.isRunning = function () { return this.running } @@ -133,32 +152,6 @@ Application.prototype.initializeClient = function (resolve, reject) { } Application.prototype.addCommands = function () { - // TODO Remove in favor of get/setBounds API - this.client.addCommand('getWindowDimensions', function () { - return this.getWindowBounds() - }) - this.client.addCommand('setWindowDimensions', function (x, y, width, height) { - return this.setWindowBounds({x: x, y: y, width: width, height: height}) - }) - - this.client.addCommand('getWindowWidth', function () { - return this.getWindowBounds().then(function (bounds) { - return bounds.width - }) - }) - - this.client.addCommand('getWindowHeight', function () { - return this.getWindowBounds().then(function (bounds) { - return bounds.height - }) - }) - - this.client.addCommand('setWindowBounds', function (bounds) { - return this.execute(function (bounds) { - require('electron').remote.getCurrentWindow().setBounds(bounds) - }, bounds) - }) - this.client.addCommand('waitUntilTextExists', function (selector, text, timeout) { return this.waitUntil(function () { return this.isExisting(selector).getText(selector).then(function (selectorText) { @@ -170,18 +163,6 @@ Application.prototype.addCommands = function () { }) }) - this.client.addCommand('quitApplication', function () { - return this.execute(function () { - require('electron').remote.app.quit() - }) - }) - - this.client.addCommand('getAppPath', function (pathName) { - return this.execute(function (pathName) { - return require('electron').remote.app.getPath(pathName) - }, pathName).then(getResponseValue) - }) - this.client.addCommand('waitUntilWindowLoaded', function (timeout) { return this.waitUntil(function () { return this.isWindowLoading().then(function (loading) { @@ -211,48 +192,6 @@ Application.prototype.addCommands = function () { }).then(getResponseValue) }) - this.client.addCommand('getMainProcessGlobal', function (globalName) { - return this.execute(function (globalName) { - return require('electron').remote.getGlobal(globalName) - }, globalName).then(getResponseValue) - }) - - this.client.addCommand('getArgv', function () { - return this.execute(function () { - return require('electron').remote.getGlobal('process').argv - }).then(getResponseValue) - }) - - this.client.addCommand('getCwd', function () { - return this.execute(function () { - return require('electron').remote.getGlobal('process').cwd() - }).then(getResponseValue) - }) - - this.client.addCommand('getClipboardText', function () { - return this.execute(function () { - return require('electron').clipboard.readText() - }).then(getResponseValue) - }) - - this.client.addCommand('setClipboardText', function (text) { - return this.execute(function (text) { - return require('electron').clipboard.writeText(text) - }, text).then(getResponseValue) - }) - - this.client.addCommand('setDocumentEdited', function (edited) { - return this.execute(function (edited) { - return require('electron').remote.getCurrentWindow().setDocumentEdited(edited) - }, edited).then(getResponseValue) - }) - - this.client.addCommand('setRepresentedFilename', function (filename) { - return this.execute(function (filename) { - return require('electron').remote.getCurrentWindow().setRepresentedFilename(filename) - }, filename).then(getResponseValue) - }) - this.client.addCommand('getRenderProcessLogs', function () { return this.log('browser').then(getResponseValue) }) @@ -263,43 +202,6 @@ Application.prototype.addCommands = function () { self.chromeDriver.clearLogs() return logs }) - - this.client.addCommand('isWindowLoading', function () { - return this.execute(function () { - return require('electron').remote.getCurrentWindow().webContents.isLoading() - }).then(getResponseValue) - }) - - this.addCurrentWindowGetter('getBounds', 'getWindowBounds') - this.addCurrentWindowGetter('isDevToolsOpened', 'isWindowDevToolsOpened') - this.addCurrentWindowGetter('isFocused', 'isWindowFocused') - this.addCurrentWindowGetter('isFullScreen', 'isWindowFullScreen') - this.addCurrentWindowGetter('isMaximized', 'isWindowMaximized') - this.addCurrentWindowGetter('isMinimized', 'isWindowMinimized') - this.addCurrentWindowGetter('isVisible', 'isWindowVisible') - - this.addCurrentWindowGetter('hide', 'hideWindow') - this.addCurrentWindowGetter('maximize', 'maximizeWindow') - this.addCurrentWindowGetter('minimize', 'minimizeWindow') - this.addCurrentWindowGetter('show', 'showWindow') - - this.addCurrentWindowGetter('selectAll', 'selectAll') - this.addCurrentWindowGetter('paste', 'paste') - - this.addCurrentWindowGetter('isDocumentEdited', 'isDocumentEdited') - this.addCurrentWindowGetter('getRepresentedFilename', 'getRepresentedFilename') -} - -Application.prototype.addCurrentWindowGetter = function (methodName, commandName) { - if (!commandName) commandName = methodName - - var currentWindowGetter = function (methodName) { - return require('electron').remote.getCurrentWindow()[methodName]() - } - - this.client.addCommand(commandName, function () { - return this.execute(currentWindowGetter, methodName).then(getResponseValue) - }) } var getResponseValue = function (response) { diff --git a/package.json b/package.json index 1f1097c18..bc0d5deb6 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "devDependencies": { "chai": "^3.3.0", "chai-as-promised": "^5.1.0", - "electron-prebuilt": "^0.37.2", + "electron-prebuilt": "^0.37.6", "mocha": "^2.3.3", "standard": "^5.3.1", "temp": "^0.8.3" diff --git a/test/application-test.js b/test/application-test.js index 9093e11c3..1b805a562 100644 --- a/test/application-test.js +++ b/test/application-test.js @@ -42,7 +42,7 @@ describe('application loading', function () { it('launches the application', function () { return app.client.windowHandles().then(function (response) { assert.equal(response.value.length, 1) - }).getWindowBounds().should.eventually.deep.equal({ + }).browserWindow.getBounds().should.eventually.deep.equal({ x: 25, y: 35, width: 200, @@ -52,26 +52,25 @@ describe('application loading', function () { }) it('passes through args to the launched app', function () { - return app.client.getArgv() + return app.mainProcess.argv() .should.eventually.contain('--foo') .should.eventually.contain('--bar=baz') }) it('passes through env to the launched app', function () { - var getEnv = function () { return process.env } - return app.client.execute(getEnv).then(function (response) { + return app.rendererProcess.env().then(function (env) { if (process.platform === 'win32') { - assert.equal(response.value.foo, 'BAR') - assert.equal(response.value.hello, 'WORLD') + assert.equal(env.foo, 'BAR') + assert.equal(env.hello, 'WORLD') } else { - assert.equal(response.value.FOO, 'BAR') - assert.equal(response.value.HELLO, 'WORLD') + assert.equal(env.FOO, 'BAR') + assert.equal(env.HELLO, 'WORLD') } }) }) it('passes through cwd to the launched app', function () { - return app.client.getCwd().should.eventually.equal(path.join(__dirname, 'fixtures')) + return app.mainProcess.cwd().should.eventually.equal(path.join(__dirname, 'fixtures')) }) describe('start()', function () { @@ -90,8 +89,10 @@ describe('application loading', function () { it('quits the application', function () { var quitPath = path.join(tempPath, 'quit.txt') assert.equal(fs.existsSync(quitPath), false) - return app.stop().then(function () { + return app.stop().then(function (stoppedApp) { + assert.equal(stoppedApp, app) assert.equal(fs.existsSync(quitPath), true) + assert.equal(app.isRunning(), false) }) }) @@ -102,6 +103,24 @@ describe('application loading', function () { }) }) + describe('restart()', function () { + it('restarts the application', function () { + var quitPath = path.join(tempPath, 'quit.txt') + assert.equal(fs.existsSync(quitPath), false) + return app.restart().then(function (restartedApp) { + assert.equal(restartedApp, app) + assert.equal(fs.existsSync(quitPath), true) + assert.equal(app.isRunning(), true) + }) + }) + + it('rejects with an error if the application is not running', function () { + return app.stop().should.be.fulfilled.then(function () { + return app.restart().should.be.rejectedWith(Error) + }) + }) + }) + describe('getRenderProcessLogs', function () { it('gets the render process console logs and clears them', function () { return app.client.waitUntilWindowLoaded() @@ -155,10 +174,9 @@ describe('application loading', function () { }) }) - describe('getMainProcessGlobal', function () { + describe('electron.remote.getGlobal', function () { it('returns the requested global from the main process', function () { - return app.client - .getMainProcessGlobal('mainProcessGlobal').should.eventually.equal('foo') + return app.electron.remote.getGlobal('mainProcessGlobal').should.eventually.equal('foo') }) }) }) diff --git a/test/commands-test.js b/test/commands-test.js index ca3e1d760..ce8cae50e 100644 --- a/test/commands-test.js +++ b/test/commands-test.js @@ -1,3 +1,4 @@ +var fs = require('fs') var helpers = require('./global-setup') var path = require('path') var temp = require('temp').track() @@ -28,9 +29,9 @@ describe('window commands', function () { }) }) - describe('getWindowBounds', function () { + describe('browserWindow.getBounds()', function () { it('gets the window bounds', function () { - return app.client.getWindowBounds().should.eventually.deep.equal({ + return app.browserWindow.getBounds().should.eventually.deep.equal({ x: 25, y: 35, width: 200, @@ -39,159 +40,157 @@ describe('window commands', function () { }) }) - describe('getWindowWidth', function () { - it('gets the window width', function () { - return app.client.getWindowWidth().should.eventually.equal(200) - }) - }) - - describe('getWindowHeight', function () { - it('gets the window height', function () { - return app.client.getWindowHeight().should.eventually.equal(100) + describe('browserWindow.setBounds()', function () { + it('sets the window bounds', function () { + return app.browserWindow.setBounds({ + x: 100, + y: 200, + width: 50, + height: 75 + }) + .pause(1000) + .browserWindow.getBounds().should.eventually.deep.equal({ + x: 100, + y: 200, + width: 50, + height: 75 + }) }) }) - describe('setWindowBounds', function () { - it('sets the window bounds', function () { - return app.client - .setWindowBounds({ - x: 100, - y: 200, - width: 50, - height: 75 - }) - .pause(1000) - .getWindowBounds().should.eventually.deep.equal({ - x: 100, - y: 200, - width: 50, - height: 75 - }) - }) - }) - - describe('isWindowFocused()', function () { + describe('browserWindow.isFocused()', function () { it('returns true when the current window is focused', function () { - return app.client.isWindowFocused().should.eventually.be.true + return app.browserWindow.isFocused().should.eventually.be.true }) }) - describe('isWindowVisible()', function () { + describe('browserWindow.isVisible()', function () { it('returns true when the window is visible, false otherwise', function () { - return app.client - .hideWindow() - .isWindowVisible().should.eventually.be.false - .showWindow() - .isWindowVisible().should.eventually.be.true + return app.browserWindow.hide() + .browserWindow.isVisible().should.eventually.be.false + .browserWindow.show() + .browserWindow.isVisible().should.eventually.be.true }) }) - describe('isWindowDevToolsOpened()', function () { + describe('browserWindow.isDevToolsOpened()', function () { it('returns false when the dev tools are closed', function () { - return app.client.isWindowDevToolsOpened().should.eventually.be.false + return app.browserWindow.isDevToolsOpened().should.eventually.be.false }) }) - describe('isWindowFullScreen()', function () { + describe('browserWindow.isFullScreen()', function () { it('returns false when the window is not in full screen mode', function () { - return app.client.isWindowFullScreen().should.eventually.be.false + return app.client.browserWindow.isFullScreen().should.eventually.be.false }) }) describe('waitUntilWindowLoaded()', function () { it('waits until the current window is loaded', function () { return app.client.waitUntilWindowLoaded() - .isWindowLoading().should.eventually.be.false + .webContents.isLoading().should.eventually.be.false }) }) - describe('isWindowMaximized()', function () { + describe('browserWindow.isMaximized()', function () { it('returns true when the window is maximized, false otherwise', function () { - return app.client.isWindowMaximized().should.eventually.be.false - .maximizeWindow().waitUntil(function () { + return app.browserWindow.isMaximized().should.eventually.be.false + .browserWindow.maximize().waitUntil(function () { // FIXME window maximized state is never true on CI if (process.env.CI) return Promise.resolve(true) - return this.isWindowMaximized() + return this.browserWindow.isMaximized() }, 5000).then(function () { }) }) }) - describe('isWindowMinimized()', function () { + describe('browserWindow.isMinimized()', function () { it('returns true when the window is minimized, false otherwise', function () { - return app.client.isWindowMinimized().should.eventually.be.false - .minimizeWindow().waitUntil(function () { + return app.browserWindow.isMinimized().should.eventually.be.false + .browserWindow.minimize().waitUntil(function () { // FIXME window minimized state is never true on CI if (process.env.CI) return Promise.resolve(true) - return this.isWindowMinimized() + return this.browserWindow.isMinimized() }, 5000).then(function () { }) }) }) - describe('selectAll()', function () { + describe('webContents.selectAll()', function () { it('selects all the text on the page', function () { - return app.client.selectAll() + return app.client.getSelectedText().should.eventually.equal('') + .webContents.selectAll() .getSelectedText().should.eventually.contain('Hello') }) }) - describe('paste()', function () { + describe('webContents.paste()', function () { it('pastes the text into the focused element', function () { return app.client .getText('textarea').should.eventually.equal('') - .setClipboardText('pasta') - .getClipboardText().should.eventually.equal('pasta') + .electron.clipboard.writeText('pasta') + .electron.clipboard.readText().should.eventually.equal('pasta') .click('textarea') - .paste() + .webContents.paste() .waitForValue('textarea', 5000) .getValue('textarea').should.eventually.equal('pasta') }) }) - describe('isDocumentEdited', function () { + describe('browserWindow.isDocumentEdited()', function () { it('returns true when the document is edited', function () { if (process.platform !== 'darwin') return - return app.client - .isDocumentEdited().should.eventually.be.false - .setDocumentEdited(true) - .isDocumentEdited().should.eventually.be.true + return app.browserWindow.isDocumentEdited().should.eventually.be.false + .browserWindow.setDocumentEdited(true) + .browserWindow.isDocumentEdited().should.eventually.be.true }) }) - describe('getRepresentedFilename', function () { + describe('browserWindow.getRepresentedFilename()', function () { it('returns the represented filename', function () { if (process.platform !== 'darwin') return - return app.client - .getRepresentedFilename().should.eventually.equal('') - .setRepresentedFilename('/foo.js') - .getRepresentedFilename().should.eventually.equal('/foo.js') + return app.browserWindow.getRepresentedFilename().should.eventually.equal('') + .browserWindow.setRepresentedFilename('/foo.js') + .browserWindow.getRepresentedFilename().should.eventually.equal('/foo.js') }) }) - describe('getAppPath', function () { + describe('electron.remote.app.getPath()', function () { it('returns the path for the given name', function () { - return app.client.getAppPath('temp').then(function (tempPath) { - return path.resolve(tempPath) - }).should.eventually.equal(temp.dir) - }) - }) - - describe('deprecated APIs', function () { - describe('setWindowDimensions', function () { - it('sets the bounds of the window', function () { - return app.client - .setWindowDimensions(100, 200, 50, 75) - .pause(1000) - .getWindowDimensions().should.eventually.deep.equal({ - x: 100, - y: 200, - width: 50, - height: 75 - }) - }) + var tempDir = fs.realpathSync(temp.dir) + return app.electron.remote.app.setPath('music', tempDir) + .electron.remote.app.getPath('music').should.eventually.equal(tempDir) + }) + }) + + it('exposes properties on constructor APIs', function () { + return app.electron.remote.MenuItem.types().should.eventually.include('normal') + }) + + describe('globalShortcut.isRegistered()', function () { + it('returns false if the shortcut is not registered', function () { + return app.electron.remote.globalShortcut.isRegistered('CommandOrControl+X').should.eventually.be.false + }) + }) + + describe('rendererProcess.versions', function () { + it('includes the Electron version', function () { + return app.rendererProcess.versions().should.eventually.have.property('electron').and.not.be.empty + }) + }) + + describe('electron.screen.getPrimaryDisplay()', function () { + it('returns information about the primary display', function () { + return app.electron.screen.getPrimaryDisplay().should.eventually.have.property('workArea').and.not.be.empty + }) + }) + + describe('electron.webFrame.getZoomFactor()', function () { + it('returns information about the primary display', function () { + return app.electron.webFrame.setZoomFactor(4) + .electron.webFrame.getZoomFactor().should.eventually.equal(4) }) }) }) diff --git a/test/example-test.js b/test/example-test.js index dba135cd8..88dc70176 100644 --- a/test/example-test.js +++ b/test/example-test.js @@ -25,35 +25,35 @@ describe('example application launch', function () { it('opens a window', function () { return app.client.waitUntilWindowLoaded() .getWindowCount().should.eventually.equal(1) - .isWindowMinimized().should.eventually.be.false - .isWindowDevToolsOpened().should.eventually.be.false - .isWindowVisible().should.eventually.be.true - .isWindowFocused().should.eventually.be.true - .getWindowWidth().should.eventually.be.above(0) - .getWindowHeight().should.eventually.be.above(0) + .browserWindow.isMinimized().should.eventually.be.false + .browserWindow.isDevToolsOpened().should.eventually.be.false + .browserWindow.isVisible().should.eventually.be.true + .browserWindow.isFocused().should.eventually.be.true + .browserWindow.getBounds().should.eventually.have.property('width').and.be.above(0) + .browserWindow.getBounds().should.eventually.have.property('height').and.be.above(0) }) describe('when the make larger button is clicked', function () { it('increases the window height and width by 10 pixels', function () { return app.client.waitUntilWindowLoaded() - .getWindowHeight().should.eventually.equal(400) - .getWindowWidth().should.eventually.equal(800) + .browserWindow.getBounds().should.eventually.have.property('width', 800) + .browserWindow.getBounds().should.eventually.have.property('height', 400) .click('.btn-make-bigger') .pause(1000) - .getWindowHeight().should.eventually.equal(410) - .getWindowWidth().should.eventually.equal(810) + .browserWindow.getBounds().should.eventually.have.property('width', 810) + .browserWindow.getBounds().should.eventually.have.property('height', 410) }) }) describe('when the make smaller button is clicked', function () { it('decreases the window height and width by 10 pixels', function () { return app.client.waitUntilWindowLoaded() - .getWindowHeight().should.eventually.equal(400) - .getWindowWidth().should.eventually.equal(800) + .browserWindow.getBounds().should.eventually.have.property('width', 800) + .browserWindow.getBounds().should.eventually.have.property('height', 400) .click('.btn-make-smaller') .pause(1000) - .getWindowHeight().should.eventually.equal(390) - .getWindowWidth().should.eventually.equal(790) + .browserWindow.getBounds().should.eventually.have.property('width', 790) + .browserWindow.getBounds().should.eventually.have.property('height', 390) }) }) }) diff --git a/test/global-setup.js b/test/global-setup.js index 7d9585752..ece844ddb 100644 --- a/test/global-setup.js +++ b/test/global-setup.js @@ -29,7 +29,7 @@ exports.startApplication = function (options) { var app = new Application(options) return app.start().then(function () { assert.equal(app.isRunning(), true) - chaiAsPromised.transferPromiseness = app.client.transferPromiseness + chaiAsPromised.transferPromiseness = app.transferPromiseness return app }) } diff --git a/test/multi-window-test.js b/test/multi-window-test.js index 48469854e..41756d939 100644 --- a/test/multi-window-test.js +++ b/test/multi-window-test.js @@ -25,7 +25,7 @@ describe('multiple windows', function () { return app.client .getWindowCount().should.eventually.equal(2) .windowByIndex(1) - .getWindowBounds().should.eventually.deep.equal({ + .browserWindow.getBounds().should.eventually.deep.equal({ x: 25, y: 35, width: 200, @@ -33,7 +33,7 @@ describe('multiple windows', function () { }) .getTitle().should.eventually.equal('Top') .windowByIndex(0) - .getWindowBounds().should.eventually.deep.equal({ + .browserWindow.getBounds().should.eventually.deep.equal({ x: 25, y: 135, width: 300,