In Electron, when an anchor is clicked that downloads a file from the server, a "Save" dialog is shown thus preventing the rest of the test case from completing / succeeding. No dialog is shown in Chrome, at least currently, but this appears to be just because that's Chrome's default behaviour.
Furthermore, there is no way of specifying the desired location of file downloads.
This should work both for files downloaded from the server (with Content-Disposition
HTTP header), as well as for files downloaded using in-browser Blob
+ dynamically generated anchor approach.
Given any site that contains an <a href="">...</a>
pointing to a file served with Content-Disposition
header, use a test script similar to the one posted below:
describe('Lorem', function() {
it('Ipsum', function() {
cy.visit("https://localhost")
.get("a.download-link").click()
// This blocks in Electron because of the "Save" popup.
.readFile("/Users/kls/Downloads/file.txt")
.then(function() {
// Do something with the file.
});
})
})
This functionality is needed in cases where it is not possible to verify validity of the download using other methods suggested in the FAQ:
https://bank/transaction-history/34964483209802/pdf
, where 34964483209802
is a credit card number. This is unacceptable from the security / compliance perspective, because HTTP URL's are oftentimes logged in proxies or web servers. One solution is to send sensitive data over secure comms, i.e. WebSockets, just after user clicks the anchor. But this makes it impossible to just look at URL and issue cy.request()
.Blob
+ transient <a>
with data URI technique, in which case no request is made to the server at all.Related: #433 #311 #4675
@kamituel did you solve this problem?
can you share your solution?
@kutlaykural Sorry for the late response, I was on vacation.
Yes, I did solve this problem in a sufficient way, at least for our needs.
1. Files that depend on more than URL to generate
For the files which are being generated on the backend based on a parameters supplied by the frontend via means other than URL itself (e.g. over an out-of-band WebSockets connection), this approach works:
<a id="download-file" href="/file?download-id=ABCDEFGH">Download</a>
<script>
document.getElementById('download-file').onclick = function(event) {
// In test, prevent the browser from actually downloading the file.
if (window.Cypress) {
event.preventDefault();
}
// But let it do all other things, most importantly talk to the backend
// out-of-band to configure the download.
setupFileOverWebSockets('ABCDEFGH');
}
</script>
Then, in the test, I would do:
cy.get('a').click().then((anchor) => {
const url = anchor.prop('href');
// cy.request() will work, because web app did talk to the backend out-of-band.
cy.request(url).then(/* do something, e.g. assert */);
});
2. Files generated in the browser
This technique involves using a temporary anchor element, which has a href
set to a value obtained from URL.createObjectURL()
. I think the following should work:
<button id="test">Download file!</button>
<script>
const button = document.getElementById('test');
button.addEventListener('click', () => {
const blob = new Blob(['lorem ipsum dolor sit amet']);
const anchor = document.createElement('a');
const url = URL.createObjectURL(blob);
anchor.download = 'test.txt';
anchor.href = url;
document.body.appendChild(anchor);
if (window.Cypress) {
// Do not attempt to actually download the file in test.
// Just leave the anchor in there. Ensure your code doesn't
// automatically remove it either.
return;
}
anchor.click();
});
</script>
And the test:
it.only('asserts in-browser generated file', () => {
// Click the button to start downloading the file. The web app
// will detect it's running within Cypress, and will create
// a temporary anchor, will set a correct href on it, but it won't
// click it and won't attempt to remove it.
cy.get('#test').click();
// Obtain a reference to that temporary anchor.
cy.get('a[download]')
.then((anchor) => (
new Cypress.Promise((resolve, reject) => {
// Use XHR to get the blob that corresponds to the object URL.
const xhr = new XMLHttpRequest();
xhr.open('GET', anchor.prop('href'), true);
xhr.responseType = 'blob';
// Once loaded, use FileReader to get the string back from the blob.
xhr.onload = () => {
if (xhr.status === 200) {
const blob = xhr.response;
const reader = new FileReader();
reader.onload = () => {
// Once we have a string, resolve the promise to let
// the Cypress chain continue, e.g. to assert on the result.
resolve(reader.result);
};
reader.readAsText(blob);
}
};
xhr.send();
})
))
// Now the regular Cypress assertions should work.
.should('equal', 'lorem ipsum dolor sit amet');
})
In summary, I use the first approach in our test already and it works well. I don't test the in-browser generated files yet using Cypress (we have legacy Selenium suite for that), but I quickly tested the approach I described here and it seems to be working.
I'd love if Cypress would let us just download a file and assert on its contents, but in the meantime those approaches provide enough coverage for most, if not all, needs.
@kamituel
Can you please tell me where should the above piece of code be written?
I tried the in 'test portion' of the code in the cypress test, and yet I still saw the 'save pop up'
@Shubha-Alvares The code you posted:
// '#download-file' here is an anchor that user clicks to download a file.
// It has to be the correct DOM element in order for the code to cancel
// the correct 'event' object.
document.getElementById('download-file').onclick = function(event) {
// ...
// In test, prevent the browser from actually downloading the file.
if (window.Cypress) {
event.preventDefault();
}
// ...
});
If you're seeing the popup, it might mean that:
if (window.Cypress) { ... }
guard in the incorrect event handler.event
you invoked event.preventDefault()
on is not the correct event object.window.Cypress
is unset).Hard to be more definitive not seeing your code. But in general, this approach should work as long as you manage to cancel .preventDefault()
the correct event. In fact, I'm using this approach in our production app for months now with no issues.
Just a note on solution 1 by @kamituel:
cy.get('a').click().then((anchor) => {
const url = anchor.prop('href');
// cy.request() will work, because web app did talk to the backend out-of-band.
cy.request(url).then(/* do something, e.g. assert */);
});
_...didn't work for me, BUT using .attr()
instead of .props()
did:_
cy.get('a').click().then((anchor) => {
const url = anchor.attr('href');
// cy.request() will work, because web app did talk to the backend out-of-band.
cy.request(url).then(/* do something, e.g. assert */);
});
Thank you @kamituel! You solved a blocker for me.
I apologize if this is obvious but based on @kamituel solution above, are we able to change the save location when cypress downloads a file?
@natejcho No, because at no point the browser actually downloads the file using the native "download manager". I explicitly prevent that in test (when window.Cypress
is set).
That's in fact the main reason the two approaches I described work. I prevent the browser from actually downloading the file, and instead I download the file, store its contents in a variable, and assert that (in the test code).
@kamituel I realized this after playing around with your code more.
But I solved my unique issue in which I wanted cypress to download a file and place it in a specific location.
I used your solution so the test could receive a base64 string. Then created a plugin to handle the encoding and writing the file to a directory. Cypress plugins run in the node and I am able to utilize the fs library and write files at the project root level.
See [https://github.com/cypress-io/cypress/issues/2029] for writing the plugin
@kamituel I realized this after playing around with your code more.
But I solved my unique issue in which I wanted cypress to download a file and place it in a specific location.
I used your solution so the test could receive a base64 string. Then created a plugin to handle the encoding and writing the file to a directory. Cypress plugins run in the node and I am able to utilize the fs library and write files at the project root level.
See [https://github.com//issues/2029] for writing the plugin
Hello, natejcho
I want to implement the same way. I want to get the response of the file thats require to download.
it can be word/pdf or excel file.
Want to validate the downloaded content. Thought the same way as you have written, but its not working. Could you please describe more on this or share a prototype of your code.(way of implementation).
waiting for your reply.
Thanks
I have used cy.request
to get the response of the file. Which I am getting successfully. Now, How to pass the response to the nodejs plugin is the problem. I have tried the below code but its not working.
cy.request(href).then((response) => {
expect(response.status).to.equal(200);
expect(response.body).not.to.null;
cy.exec("node cypress/support/Core/Parser/PDFParser.js " + response.body, {failOnNonZeroExit: false}).then((result: any) => {
console.log(result);
});
});
For a future solution to this problem in Cypress itself: Once #4628 is merged, we'll have access to the Chrome DevTools Protocol, which has these 2 functions:
Cypress could set the downloadBehavior
to deny
(so any request to download a file isn't actually downloaded), and then listen on downloadWillBegin
to catch any requests for file downloads, allowing them to be captured & tested inside of Cypress.
Maybe the API could look something like this:
Cypress.on('download:will:begin', (url) => {
// cy.request(url) could be used here to assert on contents?
// potential problem: we don't have access to the request body or headers
// used to make this download happen
})
To add on top of this. I'd like to make a feature request
getDownload
or downloadToRoot
. The naming convention isn't important to me, but it would save a download to the root of cypress project. I commented above how I've already achieved this in my application. I would be happy to open an MR for it as well.
This issue has lots of activity and been mentioned in many different issues. Which I think indicates a good need for it
For a future solution to this problem in Cypress itself: Once #4628 is merged, we'll have access to the Chrome DevTools Protocol, which has these 2 functions:
- Method: Page.setDownloadBehavior
- Event: Page.downloadWillBegin
Cypress could set the
downloadBehavior
todeny
(so any request to download a file isn't actually downloaded), and then listen ondownloadWillBegin
to catch any requests for file downloads, allowing them to be captured & tested inside of Cypress.Maybe the API could look something like this:
Cypress.on('download:will:begin', (url) => { // cy.request(url) could be used here to assert on contents? // potential problem: we don't have access to the request body or headers // used to make this download happen })
Hello.
Any news from this feature?
Hello Any news to how to manage download in cypress?
Folder destination and popup?
Hello Any news to how to manage download in cypress?
Folder destination and popup?
Sooooo. Cypress is a wild forest in which I overestimated my navigating abilities. I don't have the time needed to finish an MR for the feature. But i'd be happy to share my custom solution:
Copy the second approach posted by @kamituel
2. Files generated in the browser
This technique involves using a temporary anchor element, which has a
href
set to a value obtained fromURL.createObjectURL()
. I think the following should work:<button id="test">Download file!</button> <script> const button = document.getElementById('test'); button.addEventListener('click', () => { const blob = new Blob(['lorem ipsum dolor sit amet']); const anchor = document.createElement('a'); const url = URL.createObjectURL(blob); anchor.download = 'test.txt'; anchor.href = url; document.body.appendChild(anchor); if (window.Cypress) { // Do not attempt to actually download the file in test. // Just leave the anchor in there. Ensure your code doesn't // automatically remove it either. return; } anchor.click(); }); </script>
And the test:
it.only('asserts in-browser generated file', () => { // Click the button to start downloading the file. The web app // will detect it's running within Cypress, and will create // a temporary anchor, will set a correct href on it, but it won't // click it and won't attempt to remove it. cy.get('#test').click(); // Obtain a reference to that temporary anchor. cy.get('a[download]') .then((anchor) => ( new Cypress.Promise((resolve, reject) => { // Use XHR to get the blob that corresponds to the object URL. const xhr = new XMLHttpRequest(); xhr.open('GET', anchor.prop('href'), true); xhr.responseType = 'blob'; // Once loaded, use FileReader to get the string back from the blob. xhr.onload = () => { if (xhr.status === 200) { const blob = xhr.response; const reader = new FileReader(); reader.onload = () => { // Once we have a string, resolve the promise to let // the Cypress chain continue, e.g. to assert on the result. resolve(reader.result); }; reader.readAsText(blob); } }; xhr.send(); }) )) // Now the regular Cypress assertions should work. .should('equal', 'lorem ipsum dolor sit amet'); })
This will make the download consumable by cypress
Then use this approach from [/issues/2029] by @WesleySSmith:
As a workaround, I found that I could use a custom plugin to perform the download. In my case, I was trying to download an
xlsx
file, but I think a similar approach could be used to test PDF files or just save the files after downloading them.Added via npm:
- node-xlsx (specific to xlsx parsing)
- request (to make https request)
plugins/index.js:
on('task', { // args must be {url: "<url>, cookies: [{name: "cookie1", value: "value1"}, {name: "cookie2", value: "value2"}, ...]"} parseXlsx(args) { const cookieheader = args.cookies.map(e => e.name + "=" + e.value).join(";"); return new Promise((resolve, reject) => { const r = request({url: args.url, encoding:null, headers: {Cookie: cookieheader}}, function(err, res, body) { if (!res) { return reject(new Error("No response")); } if (res.statusCode !== 200) { return reject(new Error("Bad status code: " + res.statusCode)); } const sheet = xlsx.parse(body); console.log(JSON.stringify(sheet)); resolve(sheet); }); }); } });
support/commands.js:
Cypress.Commands.add("parseXlsx", (url) => { return cy.getCookies().then(cookies => { return cy.task('parseXlsx', {url: url, cookies: cookies }); }); });
In my test file:
// Call parseXlsx and verify that the export is as expected cy.parseXlsx(exportUrl).should(data => { expect(data).to.have.xlsSheetName("Payments"); expect(data).to.have.xlsSheetRowCell(0,0,0,'Reference #'); });
You will have to play around with the encoding, and how you detect cypress. I set an env variable and check for that.
@kamituel
First of all thank you so much for the updates here. This is really useful as I am new to cypress.
I am facing the same issue while downloading a file using cypress.
<a id="download-file" href="/file?download-id=ABCDEFGH">Download</a>
<script>
document.getElementById('download-file').onclick = function(event) {
// In test, prevent the browser from actually downloading the file.
if (window.Cypress) {
event.preventDefault();
}
// But let it do all other things, most importantly talk to the backend
// out-of-band to configure the download.
setupFileOverWebSockets('ABCDEFGH');
}
</script>
I tried to implement this solution you provided but facing an error.
Property 'Cypress' does not exist on type 'Window & typeof globalThis'.
Could you please clarify why this error message is showing up?
@CuriousSoul230 Hard to tell without seeing more of the code you're running. The particular error message you're getting though...:
Property 'Cypress' does not exist on type 'Window & typeof globalThis'.
... it suggests you're using TypeScript. Is that the case? If so, then you need to extent a window interface to include the Cypress
property you refer to:
interface Window { // New line
Cypress: any // New line
} // New line
if (window.Cypress) {
event.preventDefault();
}
Ideally you'd put that interface piece it into some .d.ts
file.
I cant find any .ts file :(
could you please confirm where I can find it?
@CuriousSoul230 I wouldn't know that, I'm not able to see your whole project.
@kamituel
First of all thank you so much for the updates here. This is really useful as I am new to cypress.
I am facing the same issue while downloading a file using cypress.<a id="download-file" href="/file?download-id=ABCDEFGH">Download</a> <script> document.getElementById('download-file').onclick = function(event) { // In test, prevent the browser from actually downloading the file. if (window.Cypress) { event.preventDefault(); } // But let it do all other things, most importantly talk to the backend // out-of-band to configure the download. setupFileOverWebSockets('ABCDEFGH'); } </script>
I tried to implement this solution you provided but facing an error.
Property 'Cypress' does not exist on type 'Window & typeof globalThis'.
Could you please clarify why this error message is showing up?
I don't use window.Cypress to check. I actually set an env variable when I run my frontend for Cypress testing. At the end of the day you just need a branch specific to Cypress downloads.
You could even use localStorage
I managed to have it working on Chrome/Chromium in non-headless mode, without the need to edit the application's source code (as in my app I'm unable to do it because file download is handled by an external library):
// plugins/index.js
const fs = require('fs');
const path = require('path');
const downloadsDirectory = path.join(__dirname, '..', 'downloads');
const cleanDownloadsDirectory = () => {
fs.rmdirSync(downloadsDirectory, { recursive: true });
return null;
};
const setUpDownloadsDirectory = (browser, options) => {
cleanDownloadsDirectory();
if (browser.family === 'chromium' && browser.name !== 'electron') {
options.preferences.default.profile = { default_content_settings: { popups: 0 } };
options.preferences.default.download = { default_directory: downloadsDirectory };
return options;
}
// not-tested on FF
if (browser.family === 'firefox') {
options.preferences['browser.download.dir'] = downloadsDirectory;
options.preferences['browser.download.folderList'] = 2;
options.preferences['browser.helperApps.neverAsk.saveToDisk'] = 'text/csv';
return options;
}
};
module.exports = on => {
on('before:browser:launch', setUpDownloadsDirectory);
on('task', {
cleanDownloadsDirectory,
});
};
and in the test:
describe('Download file', () => {
beforeEach(() => {
cy.task('cleanDownloadsDirectory')
.visit('/something');
});
it('should export data to the correct file', () => {
// given
const expectedFileName = 'some_file.xlsx';
// when
cy.get('.download-button')
.click()
// then
.readFile(`cypress/downloads/${expectedFileName}`, 'base64').then(downloadedFile => {
// some assertions
});
});
});
Unfortunately, it does not work under Electron or headless Chrome (no file is saved) so I cannot run this on CI. It would be good to have any way to force Electron to save the file without prompt or have some workaround for headless Chrome.
@TomaszG thanks this is super useful. I'm guessing popups: 0
is supposed to disable the "site is trying to download multiple files" warning, but that didn't work for me. This does however:
options.preferences.default.profile = {
content_settings: {
exceptions: {
automatic_downloads: {
'*': { setting: 1 }
}
}
}
};
https://medium.com/@VivekNayyar/e2e-testing-of-excel-downloads-with-cypress-d6e46ccdc232
https://dev.to/viveknayyar/e2e-testing-of-excel-downloads-with-cypress-21fb
This might be useful for someone landing here.
@vivek12345 could you confirm that it works also with headless chrome or with electron? Looking at implementation it's very similar to the one which I have posted before where I wasn't able to run the same test with headless chrome (file was not downloaded).
@TomaszG no unfortunately on headless mode the download still won't happen.
I will add a disclaimer to the article. Thanks for pointing it out.
Doing simple stuff, clicking on a button in UI聽(web application), that will export(download) a file-聽.xlsx file in the download folder, same stuff when I am trying to do with cypress,cy.get ('css_id').click聽, and running through electron it prompts me system window to save the file聽,how to handle this stuff as my test cases is incomplete without downloading it聽?
Tried with each solution given in git for this problem , downloaded plugin,made changes in index file in cypress ,but nothing helped.
Trying to do the same stuff and test is being stuck at waiting for page to load
after click in chrome browser. Can someone suggest any workarounds without changing the application code?
Following is the test code running it on chrome browser:
describe('zip download', () => {
it('download a file', () => {
cy.server();
cy.route('GET', 'https://send.firefox.com/api/download/*').as('download');
cy.visit('https://send.firefox.com/download/abae004e811492ca/#ySq5RqyPlf3eti4fDVqkDQ');
cy.wait(1500);
cy.get('#download-btn').click().then( ()=> {
cy.wait('@download').its('status').should('eq', 200);
});
});
});
Any update?
Bump:
P.S. I'm using npm "file-saver": "^2.0.2",
, so I don't even have a button. And I'd like to read the content of the file. Solutions like HTTP GET the content by the URL will never work for me...
Perfect timing...I'm also using file-saver.
Bump:
P.S. I'm using npm
"file-saver": "^2.0.2",
, so I don't even have a button. And I'd like to read the content of the file. Solutions like HTTP GET the content by the URL will never work for me...
So just adding dependency "file-saver": "^2.0.2" in package.json will stop the prompts of the system to save the file, and eventually it will directlty save the file in system download folder?
nothing to add or modify at at script level?
Bump:
P.S. I'm using npm"file-saver": "^2.0.2",
, so I don't even have a button. And I'd like to read the content of the file. Solutions like HTTP GET the content by the URL will never work for me...So just adding dependency "file-saver": "^2.0.2" in package.json will stop the prompts of the system to save the file, and eventually it will directlty save the file in system download folder?
nothing to add or modify at at script level?
any update?
Any updates about the situation ? I am trying to run cypress version "4.7.0" in headless chrome, but it does not download anything. It works pretty well in ui mode but in order to make this part of our CI pipeline process I need headless version running.
@kamituel Hi, sorry to ask again this question but it seems it has not been answered yet. I would like to implement your second solution to get file content (when it's generated in browser and using Electron, so i can't force download options to directly get the file where i want).
Where should i put this code you wrote ?
<button id="test">Download file!</button>
<script>
const button = document.getElementById('test');
button.addEventListener('click', () => {
const blob = new Blob(['lorem ipsum dolor sit amet']);
const anchor = document.createElement('a');
const url = URL.createObjectURL(blob);
anchor.download = 'test.txt';
anchor.href = url;
document.body.appendChild(anchor);
if (window.Cypress) {
// Do not attempt to actually download the file in test.
// Just leave the anchor in there. Ensure your code doesn't
// automatically remove it either.
return;
}
anchor.click();
});
</script>
Should I inject this in current page ? how ? (i can't modify the code of the app itself)
Thanks for your help.
Regards,
@aubertaa This is the code that downloads the file - the application code, not a test code.
The crucial piece is the if (window.Cypress) return
part. It will prevent the browser from actually downloading the file, but will leave the dynamically-created <a>
element with the data URI in the DOM.
In your test you will be then able to find this <a>
and check if the data URI is correct.
Seems like they gave up on this one. It was opened in 2017.
I can tell you how I have things set up to support downloads in the application via setting
In the Cypress plugins file:
on('before:browser:launch', (browser, options) => {
if (browser.family === 'chromium') {
const browserPath = String(browser.path);
options.args.push('--disable-popup-blocking');
if (browserPath.indexOf('D:\\a\\r1\\a\\') > 0) {
options.args.push('--no-sandbox');
options.args.push('--incognito');
options.args.push('--webview-disable-safebrowsing-support');
options.args.push('--window-size=1920,1080');
options.args.push('--disable-headless-mode');
}
options.preferences.default.profile = {
content_settings: {
exceptions: {
automatic_downloads: {
'*': { setting: 1 }
}
}
},
default_content_settings: { popups: 0 }
};
options.preferences.default['download'] =
{
default_directory: defaultDownloadPath,
prompt_for_download: false
}
return options;
}
});
I used the
allowDownload: async () => {
client = client || await CDP({ port })
return client.send('Browser.setDownloadBehavior', { behavior: 'allow', downloadPath: 'C:\\Users\\dawid.goslawski\\folder' })
}
and code from https://github.com/cypress-io/cypress-example-recipes/tree/master/examples/fundamentals__chrome-remote-debugging
File is downloading on disk automatically, I will use others events to maybe try to use guids, await on promise and provide file instead
I used the
allowDownload: async () => { client = client || await CDP({ port }) return client.send('Browser.setDownloadBehavior', { behavior: 'allow', downloadPath: 'C:\\Users\\dawid.goslawski\\folder' }) }
and code from https://github.com/cypress-io/cypress-example-recipes/tree/master/examples/fundamentals__chrome-remote-debugging
File is downloading on disk automatically, I will use others events to maybe try to use guids, await on promise and provide file instead
Hi @alkuzad Would you mind posting a bit more of context of your code. Not sure how to implement this inside a test. Thanks in advance.
@ericclaeren yeah:
activateHoverPseudo
and activatePrintMediaQuery
functionsallowDownload
task along, from what I pastedbeforeEach(() => {
cy.task('resetCRI')
cy.task('allowDownload')
cy.visit('index.html')
})
Possible functions are defined in devtools-protocol. Altough protocol versions can match, they are not the same. Exact specification of what you can run can be downloaded using CRI:
async function x(port){
var fs = require('fs')
var CDP = require('chrome-remote-interface')
CDP.Protocol({port: port}, (err, proto) => {
fs.writeFile(`C:\\Users\\dawid.goslawski\\protocol_${port}.json`, JSON.stringify(proto, null, 4), console.log)
})
}
Note that Electron is lagging behind few versions after Chrome and not everything is ported from it. Page.setDownloadBehavior
is there though, so maybe someone will use that instead.
For those having issues with Cypress timing out when clicking download links, try adding target="_blank"
to your links.
Thank you @alkuzad , I got things working in both ci and gui using Chromium. We leverage thechromium
package from npm to automatically install chromium for any environment, so cypress run --browser chromium --headless
works out of the box!
Our code looks something like this:
// plugins/downloads.js
const path = require('path');
const { promisify } = require('util');
const CDP = require('chrome-remote-interface');
const debug = require('debug')('cypress:server:protocol');
const rimraf = promisify(require('rimraf'));
let port = 0;
let client = null;
module.exports = (on, config) => {
const downloadPath = path.resolve(config.projectRoot, './downloads');
function ensureRdpPort(args) {
const existing = args.find((arg) =>
arg.startsWith('--remote-debugging-port')
);
if (existing) {
return Number(existing.split('=')[1]);
}
const port = 40000 + Math.round(Math.random() * 25000);
args.push(`--remote-debugging-port=${port}`);
return port;
}
async function resetCRI() {
if (client) {
debug('resetting CRI client');
await client.close();
client = null;
}
}
async function cleanDownloads() {
debug(`cleaning up downloads at ${downloadPath}`);
await rimraf(downloadPath);
}
async function allowDownloads() {
await resetCRI();
await cleanDownloads();
debug(`enabling downloads at ${downloadPath}`);
client = client || (await CDP({ port }));
return client.send('Browser.setDownloadBehavior', {
behavior: 'allow',
downloadPath,
});
}
on('before:browser:launch', (browser, launchOptionsOrArgs) => {
debug('browser launch args or options %o', launchOptionsOrArgs);
const args = Array.isArray(launchOptionsOrArgs)
? launchOptionsOrArgs
: launchOptionsOrArgs.args;
port = ensureRdpPort(args);
debug('ensureRdpPort %d', port);
debug('Chrome arguments %o', args);
});
return {
resetCRI,
allowDownloads,
cleanDownloads,
};
};
// plugins/index.js
const downloads = require('./downloads');
const chromium = require('chromium');
const execa = require('execa');
module.exports = async (on, config) => {
on('task', downloads(on, config));
const hasChromium = config.browsers.some(
(browser) => browser.name === 'chromium'
);
if (!hasChromium) {
const { stdout } = await execa(chromium.path, ['--version']);
const [version] = /[\d\.]+/.exec(stdout);
const majorVersion = parseInt(version.split('.')[0]);
// Note: this extends the global config!
return {
browsers: [
...config.browsers,
{
name: 'chromium',
family: 'chromium',
channel: 'stable',
displayName: 'Chromium (npm)',
path: chromium.path,
version,
majorVersion,
},
],
};
}
};
// support/commands.js
Cypress.Commands.add('getDownload', (filepath) => {
cy.readFile(`downloads/${filepath}`);
});
// support/index.js
beforeEach(() => {
// Enables downloads for each test
// note: also clears downloads folder between them
cy.task('allowDownloads');
});
// integration/download/download.js
context('Downloads', () => {
beforeEach(() => {
cy.login();
});
it('can download a file', () => {
cy.visit('/');
// Trigger a file download
cy.get('[href="/dashboard/activity/csv/"]').click();
cy.getDownload('activity.csv')
.then((fileContents) => {
console.info('--->', fileContents);
return fileContents;
})
.should('contain', 'cypress');
});
});
@hhaidar yeah baby :) For other people, here are the limitations:
cy.emit('window:load')
after link is clicked (setTimeout?)@hhaidar : I am facing similar issue, The test I am doing is to click on Export file tab to download the file and validate file contents, I tried your scripts above but dint work, the save pop up is still displaying on click and cypress is not interacting with the download dialog and ending up with following "Your page did not fire its load event within 15000ms."
Please see below screen shot and advise
@Sree2412 what does your plugins file look like and have you installed the chromium package from npm?
@hhaidar: Thanks for the reply! Yes I have installed Chromium package from npm.
Below is my plugin:
const ntlmAuth = require("cypress-ntlm-auth/dist/plugin");
const downloads = require('./downloads');
const chromium = require('chromium');
const execa = require('execa');
module.exports = async (on, config) => {
config = ntlmAuth.initNtlmAuth(config);
return config;
on('task', downloads(on, config));
const hasChromium = config.browsers.some(
(browser) => browser.name === 'chromium'
);
if (!hasChromium) {
const { stdout } = await execa(chromium.path, ['--version']);
const [version] = /[\d\.]+/.exec(stdout);
const majorVersion = parseInt(version.split('.')[0]);
// Note: this extends the global config!
return {
browsers: [
...config.browsers,
{
name: 'chromium',
family: 'chromium',
channel: 'stable',
displayName: 'Chromium (npm)',
path: chromium.path,
version,
majorVersion,
},
],
};
}
}
I have included the code you mentioned above under support and integration folders,
My test looks like this
cy.get("#submit-export")
.should("be.visible")
.click()
cy.getDownload('TestData_1_Export.dat')
.then((fileContents) => {
console.info('--->', fileContents);
return fileContents;
})
.should('contain', 'DocumentID');
});
});
@Sree2412 is Chromium/Chrome actually running? Also, are you doing something like:
beforeEach(() => {
// Enables downloads for each test
// note: also clears downloads folder between them
cy.task('allowDownloads');
});
@hhaidar Yes Chrome is running not Chromium and yes I have that code support->Index file
Ran the test in chrome and ended up with this error "cy.task('allowDownloads') failed with the following error: The 'task' event has not been registered in the plugins file. You must register it before using cy.task()" please advise
@Sree2412 read the error, you need to add the allowDownloads
task in plugins file. Did you add all the code from my example?
I am very new to cypress, just started with it, going through cypress documentation changed my plugin file like this.. but still getting error.Please guide, yes I have added all the above mentioned code .
this is how my plugin file looks now
//// <reference types="cypress" />
// ***********************************************************
// reflect-metadata is required since PortsFileService and DebugLogger has decorator injectable
const ntlmAuth = require("cypress-ntlm-auth/dist/plugin");
const CDP = require('chrome-remote-interface')
const debug = require('debug')('cypress:server:protocol')
module.exports = (on, config) => {
config = ntlmAuth.initNtlmAuth(config);
return config;
function ensureRdpPort (args) {
const existing = args.find((arg) => arg.slice(0, 23) === '--remote-debugging-port')
if (existing) {
return Number(existing.split('=')[1])
}
const port = 40000 + Math.round(Math.random() * 25000)
args.push(`--remote-debugging-port=${port}`)
return port
}
let port = 0
let client = null
on('before:browser:launch', (browser, launchOptionsOrArgs) => {
debug('browser launch args or options %o', launchOptionsOrArgs)
const args = Array.isArray(launchOptionsOrArgs) ? launchOptionsOrArgs : launchOptionsOrArgs.args
port = ensureRdpPort(args)
debug('ensureRdpPort %d', port)
debug('Chrome arguments %o', args)
})
on('task', {
resetCRI: async () => {
if (client) {
debug('resetting CRI client')
await client.close()
client = null
}
return Promise.resolve(true)
},
allowDownload: async () => {
client = client || await CDP({ port })
return client.send('Browser.setDownloadBehavior', { behavior: 'allow', downloadPath: 'C:/Users/username/Downloads' })
}
});
}
I have this code under test
describe('Downloads', () => {
beforeEach(() => {
cy.task('resetCRI')
cy.task('allowDownload')
cy.visit("")
})
but still receiving "cy.task('resetCRI') failed with the following error:
The 'task' event has not been registered in the plugins file. You must register it before using cy.task()"
I think I registerd my task under my plugin index file, unable to figure out what I am missing. some one pls help!
some one pls look at above issue and advise
@hhaidar thank you for you solution. Works fine for me on Mac and Linux. With Windows Cypress didn't come up. Maybe this is also the problem for @Sree2412
@hhaidar thank you for you solution. Works fine for me on Mac and Linux. With Windows Cypress didn't come up. Maybe this is also the problem for @Sree2412
Are you using Chromium?
@hhaidar thank you for you solution. Works fine for me on Mac and Linux. With Windows Cypress didn't come up. Maybe this is also the problem for @Sree2412
Are you using Chromium?
Yes. With Windows Chromium opens immediately after "cypress open" with an error. I will paste a screenshot.
There is the message "Google API-Keys missing".
Currently I think the "chromium" packages load the latest SNAPSHOT and not a stable version from chromium.
After "npm ci" on my Mac. I also got problems with chromium. It seems there is a problem in newer version.
Now I'm using a fix revision of chromium by adding the file .npmrc with node_chromium_revision=782078
. This is currently a Chromium 85.
I can tell you how I have things set up to support downloads in the application via setting
In the Cypress plugins file:
on('before:browser:launch', (browser, options) => { if (browser.family === 'chromium') { const browserPath = String(browser.path); options.args.push('--disable-popup-blocking'); if (browserPath.indexOf('D:\\a\\r1\\a\\') > 0) { options.args.push('--no-sandbox'); options.args.push('--incognito'); options.args.push('--webview-disable-safebrowsing-support'); options.args.push('--window-size=1920,1080'); options.args.push('--disable-headless-mode'); } options.preferences.default.profile = { content_settings: { exceptions: { automatic_downloads: { '*': { setting: 1 } } } }, default_content_settings: { popups: 0 } }; options.preferences.default['download'] = { default_directory: defaultDownloadPath, prompt_for_download: false } return options; } });
These settings worked for me and seem to be a better solution than anything you might stick in beforeEach
@nickbreid I tried above solution with my windows machine but it's giving following error,
[excel download failed] - Download error
@nickbreid for me this works. But not in headless mode, which is a problem for me.
@AndreVirtimo have you found a solution for the headless version? Any idea how to pass specs in CI for this case?
@AndreVirtimo have you found a solution for the headless version? Any idea how to pass specs in CI for this case?
I have switched back to this solution https://github.com/cypress-io/cypress/issues/949#issuecomment-666638986
But I have removed the browser config in plugins/index.js which adds the local chromium. This didn't work under windows.
Most helpful comment
@kutlaykural Sorry for the late response, I was on vacation.
Yes, I did solve this problem in a sufficient way, at least for our needs.
1. Files that depend on more than URL to generate
For the files which are being generated on the backend based on a parameters supplied by the frontend via means other than URL itself (e.g. over an out-of-band WebSockets connection), this approach works:
Then, in the test, I would do:
2. Files generated in the browser
This technique involves using a temporary anchor element, which has a
href
set to a value obtained fromURL.createObjectURL()
. I think the following should work:And the test:
In summary, I use the first approach in our test already and it works well. I don't test the in-browser generated files yet using Cypress (we have legacy Selenium suite for that), but I quickly tested the approach I described here and it seems to be working.
I'd love if Cypress would let us just download a file and assert on its contents, but in the meantime those approaches provide enough coverage for most, if not all, needs.