From 6728cfa23b604b5ce9303a63eec46623c969f023 Mon Sep 17 00:00:00 2001 From: Chris Breiding Date: Mon, 11 Jan 2021 09:10:29 -0500 Subject: [PATCH] feat: Allow downloading files without prompts (#14431) Co-authored-by: Jennifer Shehane --- packages/server/lib/browsers/chrome.ts | 10 +++++++ packages/server/lib/browsers/electron.js | 10 +++++++ packages/server/lib/browsers/firefox.ts | 19 +++++++++++++ packages/server/lib/open_project.js | 2 ++ packages/server/package.json | 1 + packages/server/test/e2e/4_downloads_spec.ts | 14 ++++++++++ .../server/test/integration/cypress_spec.js | 1 + .../fixtures/projects/downloads/cypress.json | 5 ++++ .../downloads/cypress/fixtures/downloads.html | 15 ++++++++++ .../downloads/cypress/fixtures/files.zip | Bin 0 -> 401 bytes .../downloads/cypress/fixtures/people.xlsx | Bin 0 -> 3946 bytes .../downloads/cypress/fixtures/records.csv | 4 +++ .../cypress/integration/downloads_spec.ts | 26 ++++++++++++++++++ .../server/test/unit/browsers/chrome_spec.js | 5 ++-- .../server/test/unit/open_project_spec.js | 1 + yarn.lock | 5 ++++ 16 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 packages/server/test/e2e/4_downloads_spec.ts create mode 100644 packages/server/test/support/fixtures/projects/downloads/cypress.json create mode 100644 packages/server/test/support/fixtures/projects/downloads/cypress/fixtures/downloads.html create mode 100644 packages/server/test/support/fixtures/projects/downloads/cypress/fixtures/files.zip create mode 100644 packages/server/test/support/fixtures/projects/downloads/cypress/fixtures/people.xlsx create mode 100644 packages/server/test/support/fixtures/projects/downloads/cypress/fixtures/records.csv create mode 100644 packages/server/test/support/fixtures/projects/downloads/cypress/integration/downloads_spec.ts diff --git a/packages/server/lib/browsers/chrome.ts b/packages/server/lib/browsers/chrome.ts index d4ad82ff54..01ed9c6bee 100644 --- a/packages/server/lib/browsers/chrome.ts +++ b/packages/server/lib/browsers/chrome.ts @@ -294,6 +294,13 @@ const _navigateUsingCRI = async function (client, url) { await client.send('Page.navigate', { url }) } +const _setDownloadsDir = async function (client, dir) { + await client.send('Page.setDownloadBehavior', { + behavior: 'allow', + downloadPath: dir, + }) +} + const _setAutomation = (client, automation) => { return automation.use( CdpAutomation(client.send), @@ -317,6 +324,8 @@ export = { _navigateUsingCRI, + _setDownloadsDir, + _setAutomation, _getChromePreferences, @@ -485,6 +494,7 @@ export = { await this._maybeRecordVideo(criClient, options) await this._navigateUsingCRI(criClient, url) + await this._setDownloadsDir(criClient, options.downloadsFolder) // return the launched browser process // with additional method to close the remote connection diff --git a/packages/server/lib/browsers/electron.js b/packages/server/lib/browsers/electron.js index 937d7d1ec5..f764fb41a3 100644 --- a/packages/server/lib/browsers/electron.js +++ b/packages/server/lib/browsers/electron.js @@ -233,6 +233,9 @@ module.exports = { // enabling can only happen once the window has loaded return this._enableDebugger(win.webContents) }) + .then(() => { + return this._setDownloadsDir(win.webContents, options.downloadsFolder) + }) .return(win) }, @@ -287,6 +290,13 @@ module.exports = { return webContents.debugger.sendCommand('Console.enable') }, + _setDownloadsDir (webContents, dir) { + return webContents.debugger.sendCommand('Page.setDownloadBehavior', { + behavior: 'allow', + downloadPath: dir, + }) + }, + _getPartition (options) { if (options.isTextTerminal) { // create dynamic persisted run diff --git a/packages/server/lib/browsers/firefox.ts b/packages/server/lib/browsers/firefox.ts index a57bc2bf42..c8821bd10a 100644 --- a/packages/server/lib/browsers/firefox.ts +++ b/packages/server/lib/browsers/firefox.ts @@ -13,10 +13,20 @@ import { Browser, BrowserInstance } from './types' import { EventEmitter } from 'events' import os from 'os' import treeKill from 'tree-kill' +import mimeDb from 'mime-db' + const errors = require('../errors') const debug = Debug('cypress:server:browsers:firefox') +// used to prevent the download prompt for the specified file types. +// this should cover most/all file types, but if it's necessary to +// discover more, open Firefox DevTools, download the file yourself +// and observe the Response Headers content-type in the Network tab +const downloadMimeTypes = Object.keys(mimeDb).filter((mimeType) => { + return mimeDb[mimeType].extensions?.length +}).join(',') + const defaultPreferences = { /** * Taken from https://github.com/puppeteer/puppeteer/blob/8b49dc62a62282543ead43541316e23d3450ff3c/lib/Launcher.js#L520 @@ -288,6 +298,14 @@ const defaultPreferences = { 'media.getusermedia.insecure.enabled': true, 'marionette.log.level': launcherDebug.log.enabled ? 'Debug' : undefined, + + // where to download files + // 0: desktop + // 1: default "Downloads" directory + // 2: directory specified with 'browser.download.dir' (set dynamically below) + 'browser.download.folderList': 2, + // prevents the download prompt for the specified types of files + 'browser.helperApps.neverAsk.saveToDisk': downloadMimeTypes, } export function _createDetachedInstance (browserInstance: BrowserInstance): BrowserInstance { @@ -341,6 +359,7 @@ export async function open (browser: Browser, url, options: any = {}): Bluebird< 'network.proxy.http_port': +port, 'network.proxy.ssl_port': +port, 'network.proxy.no_proxies_on': '', + 'browser.download.dir': options.downloadsFolder, }) } diff --git a/packages/server/lib/open_project.js b/packages/server/lib/open_project.js index 0730571a11..ab129613ba 100644 --- a/packages/server/lib/open_project.js +++ b/packages/server/lib/open_project.js @@ -4,6 +4,7 @@ const debug = require('debug')('cypress:server:open_project') const Promise = require('bluebird') const chokidar = require('chokidar') const pluralize = require('pluralize') +const path = require('path') const Project = require('./project') const browsers = require('./browsers') @@ -73,6 +74,7 @@ const moduleFactory = () => { options.proxyServer = cfg.proxyUrl options.socketIoRoute = cfg.socketIoRoute options.chromeWebSecurity = cfg.chromeWebSecurity + options.downloadsFolder = path.join(cfg.projectRoot, 'cypress', 'downloads') options.url = url diff --git a/packages/server/package.json b/packages/server/package.json index 2ce2517137..87b05ee978 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -80,6 +80,7 @@ "marionette-client": "cypress-io/marionette-client#2cddf7d791cca7be5191d7fe103d58be7283957d", "md5": "2.3.0", "mime": "2.4.4", + "mime-db": "1.45.0", "minimatch": "3.0.4", "minimist": "1.2.5", "mocha-7.0.1": "npm:mocha@7.0.1", diff --git a/packages/server/test/e2e/4_downloads_spec.ts b/packages/server/test/e2e/4_downloads_spec.ts new file mode 100644 index 0000000000..22dcd5bd76 --- /dev/null +++ b/packages/server/test/e2e/4_downloads_spec.ts @@ -0,0 +1,14 @@ +import e2e from '../support/helpers/e2e' +import Fixtures from '../support/helpers/fixtures' + +describe('e2e downloads', () => { + e2e.setup() + + e2e.it('handles various file downloads', { + project: Fixtures.projectPath('downloads'), + spec: '*', + config: { + video: false, + }, + }) +}) diff --git a/packages/server/test/integration/cypress_spec.js b/packages/server/test/integration/cypress_spec.js index 4af5531059..b1f3f2daf6 100644 --- a/packages/server/test/integration/cypress_spec.js +++ b/packages/server/test/integration/cypress_spec.js @@ -1147,6 +1147,7 @@ describe('lib/cypress', () => { // it accepts URL to visit and then waits for actual CRI client reference // and only then navigates to that URL sinon.stub(chromeBrowser, '_navigateUsingCRI').resolves() + sinon.stub(chromeBrowser, '_setDownloadsDir').resolves() sinon.stub(chromeBrowser, '_setAutomation').returns() diff --git a/packages/server/test/support/fixtures/projects/downloads/cypress.json b/packages/server/test/support/fixtures/projects/downloads/cypress.json new file mode 100644 index 0000000000..e81c35571a --- /dev/null +++ b/packages/server/test/support/fixtures/projects/downloads/cypress.json @@ -0,0 +1,5 @@ +{ + "fixturesFolder": false, + "pluginsFile": false, + "supportFile": false +} diff --git a/packages/server/test/support/fixtures/projects/downloads/cypress/fixtures/downloads.html b/packages/server/test/support/fixtures/projects/downloads/cypress/fixtures/downloads.html new file mode 100644 index 0000000000..0c22c55e27 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/downloads/cypress/fixtures/downloads.html @@ -0,0 +1,15 @@ + + + + + +

Download CSV

+ records.csv + +

Download XLSX

+ people.xlsx + +

Download ZIP file

+ files.zip + + diff --git a/packages/server/test/support/fixtures/projects/downloads/cypress/fixtures/files.zip b/packages/server/test/support/fixtures/projects/downloads/cypress/fixtures/files.zip new file mode 100644 index 0000000000000000000000000000000000000000..d64a2dd2466f9b2d517f5b6b69eb9b410306e8ff GIT binary patch literal 401 zcmWIWW@Zs#-~hsn5q*ISP{0eMSs4@0#HAJ742&#a z85tOWS{WF;&z#mj>7g6sar%tsQznK0Z+4CwT{lnj1EoOb1i)>mj78WW2ely)Zi5@p zWOpD2(P%cD_Bea;WcZ3xIuE?gcn21&)6wwK)eF<`@;#^Hq5BMC<*E9o4hld+K?VnS zGct)VBfN#|9FVtQU`Zp0MU*FzjRg4v**a7slYmAN<*NX1RyL6ROhC8{NM8kU7yzGH BT$2C* literal 0 HcmV?d00001 diff --git a/packages/server/test/support/fixtures/projects/downloads/cypress/fixtures/people.xlsx b/packages/server/test/support/fixtures/projects/downloads/cypress/fixtures/people.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..a0521854fe9d65aa2325e5af74938af71baed6da GIT binary patch literal 3946 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 + +describe('downloads', () => { + beforeEach(() => { + cy.visit('/cypress/fixtures/downloads.html') + }) + + it('handles csv file download', () => { + cy.get('[data-cy=download-csv]').click() + cy + .readFile('cypress/downloads/records.csv') + .should('contain', '"Joe","Smith"') + }) + + it('handles zip file download', () => { + cy.get('[data-cy=download-zip]').click() + // not worth adding a dependency to read contents, just ensure it's there + cy.readFile('cypress/downloads/files.zip') + }) + + it('handles xlsx file download', () => { + cy.get('[data-cy=download-xlsx]').click() + // not worth adding a dependency to read contents, just ensure it's there + cy.readFile('cypress/downloads/people.xlsx') + }) +}) diff --git a/packages/server/test/unit/browsers/chrome_spec.js b/packages/server/test/unit/browsers/chrome_spec.js index 06a715271e..f6f345c6e9 100644 --- a/packages/server/test/unit/browsers/chrome_spec.js +++ b/packages/server/test/unit/browsers/chrome_spec.js @@ -50,14 +50,15 @@ describe('lib/browsers/chrome', () => { expect(this.criClient.ensureMinimumProtocolVersion).to.be.calledOnce }) - it('focuses on the page and calls CRI Page.visit', function () { + it('focuses on the page, calls CRI Page.visit, and sets download behavior', function () { return chrome.open('chrome', 'http://', {}, this.automation) .then(() => { expect(utils.getPort).to.have.been.calledOnce // to get remote interface port - expect(this.criClient.send).to.have.been.calledTwice + expect(this.criClient.send).to.have.been.calledThrice expect(this.criClient.send).to.have.been.calledWith('Page.bringToFront') expect(this.criClient.send).to.have.been.calledWith('Page.navigate') + expect(this.criClient.send).to.have.been.calledWith('Page.setDownloadBehavior') }) }) diff --git a/packages/server/test/unit/open_project_spec.js b/packages/server/test/unit/open_project_spec.js index dcb278e43f..680398be61 100644 --- a/packages/server/test/unit/open_project_spec.js +++ b/packages/server/test/unit/open_project_spec.js @@ -17,6 +17,7 @@ describe('lib/open_project', () => { integrationFolder: '/user/foo/cypress/integration', testFiles: '**/*.*', ignoreTestFiles: '**/*.nope', + projectRoot: '/project/root', } sinon.stub(browsers, 'get').resolves() diff --git a/yarn.lock b/yarn.lock index b2effda492..eb43f97052 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22233,6 +22233,11 @@ mime-db@1.44.0, "mime-db@>= 1.43.0 < 2", mime-db@^1.28.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== +mime-db@1.45.0: + version "1.45.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea" + integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w== + mime-db@~1.33.0: version "1.33.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db"