From f16c3216dfc4b4fb91e10049c8603f6285774f76 Mon Sep 17 00:00:00 2001 From: Jason Leyba Date: Sun, 7 Jan 2018 20:00:23 -0800 Subject: [PATCH] [js] Add support for /session/:sessionId/chromium/send_command (#5159) --- javascript/node/selenium-webdriver/CHANGES.md | 3 + javascript/node/selenium-webdriver/chrome.js | 52 ++++++++++- .../lib/test/data/chrome/download.html | 2 + .../test/chrome/devtools_test.js | 93 +++++++++++++++++++ 4 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 javascript/node/selenium-webdriver/lib/test/data/chrome/download.html create mode 100644 javascript/node/selenium-webdriver/test/chrome/devtools_test.js diff --git a/javascript/node/selenium-webdriver/CHANGES.md b/javascript/node/selenium-webdriver/CHANGES.md index 8ec58e7b64bf1..e4ddc16857842 100644 --- a/javascript/node/selenium-webdriver/CHANGES.md +++ b/javascript/node/selenium-webdriver/CHANGES.md @@ -69,6 +69,9 @@ mode. - Added setChromeService, setEdgeService, & setFirefoxService - Removed setEnableNativeEvents - Removed setScrollBehavior +* Changes to `chrome.Driver` + - Added sendDevToolsCommand + - Added setDownloadPath * Changes to `chrome.Options` - Now extends the `Capabilities` class - Removed from/toCapabilities diff --git a/javascript/node/selenium-webdriver/chrome.js b/javascript/node/selenium-webdriver/chrome.js index 51fe6106a33e4..94ade9fe9c3b5 100644 --- a/javascript/node/selenium-webdriver/chrome.js +++ b/javascript/node/selenium-webdriver/chrome.js @@ -135,6 +135,7 @@ const http = require('./http'); const io = require('./io'); const {Browser, Capabilities, Capability} = require('./lib/capabilities'); const command = require('./lib/command'); +const error = require('./lib/error'); const logging = require('./lib/logging'); const promise = require('./lib/promise'); const Symbols = require('./lib/symbols'); @@ -159,7 +160,8 @@ const CHROMEDRIVER_EXE = const Command = { LAUNCH_APP: 'launchApp', GET_NETWORK_CONDITIONS: 'getNetworkConditions', - SET_NETWORK_CONDITIONS: 'setNetworkConditions' + SET_NETWORK_CONDITIONS: 'setNetworkConditions', + SEND_DEVTOOLS_COMMAND: 'sendDevToolsCommand', }; @@ -193,6 +195,10 @@ function configureExecutor(executor) { Command.SET_NETWORK_CONDITIONS, 'POST', '/session/:sessionId/chromium/network_conditions'); + executor.defineCommand( + Command.SEND_DEVTOOLS_COMMAND, + 'POST', + '/session/:sessionId/chromium/send_command'); } @@ -363,6 +369,12 @@ class Options extends Capabilities { * > in Chrome 60. Users are encouraged to set an initial window size with * > the {@link #windowSize windowSize({width, height})} option. * + * > __NOTE__: For security, Chrome disables downloads by default when + * > in headless mode (to prevent sites from silently downloading files to + * > your machine). After creating a session, you may call + * > {@link ./chrome.Driver#setDownloadPath setDownloadPath} to re-enable + * > downloads, saving files in the specified directory. + * * @return {!Options} A self reference. */ headless() { @@ -751,6 +763,44 @@ class Driver extends webdriver.WebDriver { new command.Command(Command.SET_NETWORK_CONDITIONS) .setParameter('network_conditions', spec)); } + + /** + * Sends an arbitrary devtools command to the browser. + * + * @param {string} cmd The name of the command to send. + * @param {Object=} params The command parameters. + * @return {!Promise} A promise that will be resolved when the command + * has finished. + * @see + */ + sendDevToolsCommand(cmd, params = {}) { + return this.execute( + new command.Command(Command.SEND_DEVTOOLS_COMMAND) + .setParameter('cmd', cmd) + .setParameter('params', params)); + } + + /** + * Sends a DevTools command to change Chrome's download directory. + * + * @param {string} path The desired download directory. + * @return {!Promise} A promise that will be resolved when the command + * has finished. + * @see #sendDevToolsCommand + */ + async setDownloadPath(path) { + if (!path || typeof path !== 'string') { + throw new error.InvalidArgumentError('invalid download path'); + } + const stat = await io.stat(path); + if (!stat.isDirectory()) { + throw new error.InvalidArgumentError('not a directory: ' + path); + } + return this.sendDevToolsCommand('Page.setDownloadBehavior', { + 'behavior': 'allow', + 'downloadPath': path + }); + } } diff --git a/javascript/node/selenium-webdriver/lib/test/data/chrome/download.html b/javascript/node/selenium-webdriver/lib/test/data/chrome/download.html new file mode 100644 index 0000000000000..9fd739e64b507 --- /dev/null +++ b/javascript/node/selenium-webdriver/lib/test/data/chrome/download.html @@ -0,0 +1,2 @@ + +

Hello, world!

diff --git a/javascript/node/selenium-webdriver/test/chrome/devtools_test.js b/javascript/node/selenium-webdriver/test/chrome/devtools_test.js new file mode 100644 index 0000000000000..8256c444ed686 --- /dev/null +++ b/javascript/node/selenium-webdriver/test/chrome/devtools_test.js @@ -0,0 +1,93 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +'use strict'; + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); + +const chrome = require('../../chrome'); +const error = require('../../lib/error'); +const fileServer = require('../../lib/test/fileserver'); +const io = require('../../io'); +const test = require('../../lib/test'); +const webdriver = require('../..'); + +test.suite(function(env) { + let driver; + + before(async function() { + driver = await env.builder() + .setChromeOptions(new chrome.Options().headless()) + .build(); + }); + after(() => driver.quit()); + + it('can send commands to devtools', async function() { + await driver.get(test.Pages.ajaxyPage); + assert.equal(await driver.getCurrentUrl(), test.Pages.ajaxyPage); + + await driver.sendDevToolsCommand( + 'Page.navigate', {url: test.Pages.echoPage}); + assert.equal(await driver.getCurrentUrl(), test.Pages.echoPage); + }); + + describe('setDownloadPath', function() { + it('can enable downloads in headless mode', async function() { + const dir = await io.tmpDir(); + await driver.setDownloadPath(dir); + + const url = fileServer.whereIs('/data/chrome/download.html'); + await driver.get(`data:text/html, +
Go!
`); + + await driver.findElement({css: 'a'}).click(); + + const downloadPath = path.join(dir, 'download.html'); + await driver.wait(() => io.exists(downloadPath), 1000); + + const goldenPath = + path.join(__dirname, '../../lib/test/data/chrome/download.html'); + assert.equal( + fs.readFileSync(downloadPath, 'utf8'), + fs.readFileSync(goldenPath, 'utf8')); + }); + + it('throws if path is not a directory', async function() { + await assertInvalidArgumentError(() => driver.setDownloadPath()); + await assertInvalidArgumentError(() => driver.setDownloadPath(null)); + await assertInvalidArgumentError(() => driver.setDownloadPath('')); + await assertInvalidArgumentError(() => driver.setDownloadPath(1234)); + + const file = await io.tmpFile(); + await assertInvalidArgumentError(() => driver.setDownloadPath(file)); + + async function assertInvalidArgumentError(fn) { + try { + await fn(); + return Promise.reject(Error('should have failed')); + } catch (err) { + if (err instanceof error.InvalidArgumentError) { + return; + } + throw err; + } + } + }); + }); +}, {browsers: ['chrome']});