mirror of
https://github.com/cypress-io/cypress.git
synced 2026-03-09 10:09:52 -05:00
feat: Allow downloading files without prompts (#14431)
Co-authored-by: Jennifer Shehane <jennifer@cypress.io>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
14
packages/server/test/e2e/4_downloads_spec.ts
Normal file
14
packages/server/test/e2e/4_downloads_spec.ts
Normal 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,
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"fixturesFolder": false,
|
||||
"pluginsFile": false,
|
||||
"supportFile": false
|
||||
}
|
||||
@@ -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>
|
||||
Binary file not shown.
Binary file not shown.
@@ -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"
|
||||
|
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ describe('lib/open_project', () => {
|
||||
integrationFolder: '/user/foo/cypress/integration',
|
||||
testFiles: '**/*.*',
|
||||
ignoreTestFiles: '**/*.nope',
|
||||
projectRoot: '/project/root',
|
||||
}
|
||||
|
||||
sinon.stub(browsers, 'get').resolves()
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user