feat: Allow downloading files without prompts (#14431)

Co-authored-by: Jennifer Shehane <jennifer@cypress.io>
This commit is contained in:
Chris Breiding
2021-01-11 09:10:29 -05:00
committed by GitHub
parent e6827d2262
commit 6728cfa23b
16 changed files with 116 additions and 2 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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,
})
}

View File

@@ -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

View File

@@ -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",

View File

@@ -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,
},
})
})

View File

@@ -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()

View File

@@ -0,0 +1,5 @@
{
"fixturesFolder": false,
"pluginsFile": false,
"supportFile": false
}

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<h3>Download CSV</h3>
<a data-cy="download-csv" href="records.csv" download>records.csv</a>
<h3>Download XLSX</h3>
<a data-cy="download-xlsx" href="people.xlsx" download>people.xlsx</a>
<h3>Download ZIP file</h3>
<a data-cy="download-zip" href="files.zip" download>files.zip</a>
</body>
</html>

View File

@@ -0,0 +1,4 @@
"First name","Last name","Occupation","Age","City","State"
"Joe","Smith","student",20,"Boston","MA"
"Mary","Sue","driver",21,"New York","NY"
"Adam","Brown","plumber",22,"Miami","FL"
1 First name Last name Occupation Age City State
2 Joe Smith student 20 Boston MA
3 Mary Sue driver 21 New York NY
4 Adam Brown plumber 22 Miami FL

View File

@@ -0,0 +1,26 @@
/// <reference types="cypress" />
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')
})
})

View File

@@ -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')
})
})

View File

@@ -17,6 +17,7 @@ describe('lib/open_project', () => {
integrationFolder: '/user/foo/cypress/integration',
testFiles: '**/*.*',
ignoreTestFiles: '**/*.nope',
projectRoot: '/project/root',
}
sinon.stub(browsers, 'get').resolves()

View File

@@ -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"