cypress-io / cypress

Fast, easy and reliable testing for anything that runs in a browser.
https://cypress.io
MIT License
46.84k stars 3.17k forks source link

Add support to get downloaded file when its name is not known. #14863

Open TomaszG opened 3 years ago

TomaszG commented 3 years ago

It would be useful to have an option to parse downloaded files when their name is not known at the moment of download. I tried to create a helper function (task) for that which returns the latest file, however, it's not reliable as it's executed when file is not there yet:

// plugins/index.js
const getLastDownloadFilePath = () => {
  const dirPath = 'cypress/downloads';
  const filesOrdered = readdirSync(dirPath)
    .map(entry => path.join(dirPath, entry))
    .filter(entryWithPath => lstatSync(entryWithPath).isFile())
    .map(fileName => ({ fileName, mtime: lstatSync(fileName).mtime }))
    .sort((a, b) => b.mtime.getTime() - a.mtime.getTime());

  return filesOrdered.length ? filesOrdered[0].fileName : false;
};

I believe it may be useful to have the ability to:

  1. wait for file download,
  2. get the name of the downloaded file.

These two should allow handling such cases.

jennifer-shehane commented 3 years ago

There's an example of reading a file directly after download by looking for extension. Is this helpful? https://github.com/cypress-io/cypress-example-recipes/blob/master/examples/testing-dom__download/cypress/integration/spec.js#L263

TomaszG commented 3 years ago

It solves only first part of the request as there's a chance that file is not in the downloads folder yet as Cypress doesn't wait for the file to download before the task is executed. It could work when used with waitUntil plugin. Maybe there should be a command to wait for file download which would return download results?

viktorgogulenko commented 3 years ago

I solved this issue with really great feature of Cypress - interceptor. Filename you can automatically grab from 'content-disposition' response header.

Screenshot 2021-03-26 at 11 59 33

Here is a code snippet:

    // Prepare interceptor to get response after click on download button
    cy.intercept('file').as('incidentPdfReport');
    // Click on download button
    cy.get('[data-test=reportDownload]').click();
    // Interceptor waiting when response will come and file is completely downloaded
    cy.wait('@incidentPdfReport', { timeout: 20000 }).then((res) => {
      const downloadsFolder = Cypress.config('downloadsFolder');
      // grab filename from 'content-disposition' header
      const filename = res.response.headers['content-disposition'].split('filename=')[1];
      const downloadedFilename = path.join(downloadsFolder, filename);
      cy.readFile(downloadedFilename, 'binary', { timeout: 15000 })
        .should((buffer) => {
          expect(buffer.length).to.be.gt(100);
        });
      cy.log(`**File ${filename} exists in downloads folder**`);
      cy.readFile(downloadedFilename).should((text) => {
        const lines = text.split('\n');
        expect(lines).to.have.length.gt(2);
        expect(lines[0]).to.equal('%PDF-1.4');
      });
    });

In this way you don't need any manipulations with downloads folder like finding the last downloaded file, delete all files before test etc. - you have exact filename and you can do with this file whatever you need. Hopefully it will help you.

TomaszG commented 3 years ago

@viktorgogulenko thanks for that, that should work when the file is generated in the backend and later sent in the response. Unfortunately, in my case, the file is generated by frontend code with file-saver library. Therefore there's no additional request with a filename in it.

I managed to create a workaround with:

// plugins/index.js
const path = require('path');
const { existsSync, readdirSync, lstatSync } = require('fs');

const downloadsDirPath = 'cypress/downloads';

const getLastDownloadFilePath = () => {
  if (!existsSync(downloadsDirPath)) {
    return null;
  }

  const filesOrdered = readdirSync(downloadsDirPath)
    .map(entry => path.join(downloadsDirPath, entry))
    .filter(entryWithPath => lstatSync(entryWithPath).isFile())
    .map(fileName => ({ fileName, mtime: lstatSync(fileName).mtime }))
    .sort((a, b) => b.mtime.getTime() - a.mtime.getTime());

  if (!filesOrdered.length) {
    return null;
  }

  // TODO: this works only for chrome family browsers
  if (filesOrdered[0].fileName.indexOf('crdownload') > -1) {
    return null;
  }

  return filesOrdered[0].fileName;
};

and in test, I use it with wait-until plugin:

// integration/test.spec.js
cy.fixture('expectedFile').then(expectedFile => cy
  .waitUntil(() => cy
    .task('getLastDownloadFilePath')
    .then(result => result),
  { timeout: 3000, interval: 100 })
  .then(filePath => {
    cy.readFile(filePath).should(actualFile => {
      // assertion goes here
    });
  })
);
VicenteRuizWefoxgroup commented 3 years ago

There's an example of reading a file directly after download by looking for extension. Is this helpful? https://github.com/cypress-io/cypress-example-recipes/blob/master/examples/testing-dom__download/cypress/integration/spec.js#L263

Seems that the link is broken

samtsai commented 3 years ago

Just go to the base directory with newer examples: https://github.com/cypress-io/cypress-example-recipes/tree/master/examples/testing-dom__download

dkoudrinhc commented 2 years ago

I solved this issue with really great feature of Cypress - interceptor. Filename you can automatically grab from 'content-disposition' response header. Screenshot 2021-03-26 at 11 59 33

Here is a code snippet:

    // Prepare interceptor to get response after click on download button
    cy.intercept('file').as('incidentPdfReport');
    // Click on download button
    cy.get('[data-test=reportDownload]').click();
    // Interceptor waiting when response will come and file is completely downloaded
    cy.wait('@incidentPdfReport', { timeout: 20000 }).then((res) => {
      const downloadsFolder = Cypress.config('downloadsFolder');
      // grab filename from 'content-disposition' header
      const filename = res.response.headers['content-disposition'].split('filename=')[1];
      const downloadedFilename = path.join(downloadsFolder, filename);
      cy.readFile(downloadedFilename, 'binary', { timeout: 15000 })
        .should((buffer) => {
          expect(buffer.length).to.be.gt(100);
        });
      cy.log(`**File ${filename} exists in downloads folder**`);
      cy.readFile(downloadedFilename).should((text) => {
        const lines = text.split('\n');
        expect(lines).to.have.length.gt(2);
        expect(lines[0]).to.equal('%PDF-1.4');
      });
    });

In this way you don't need any manipulations with downloads folder like finding the last downloaded file, delete all files before test etc. - you have exact filename and you can do with this file whatever you need. Hopefully it will help you.

Didn't work unfortunately. Just times out. CypressError: Timed out retrying after 20000ms: cy.wait() timed out waiting 20000ms for the 1st request to the route: pdfReport. No request ever occurred.

https://on.cypress.io/wait

nalanraj commented 2 years ago

I solved this issue with really great feature of Cypress - interceptor. Filename you can automatically grab from 'content-disposition' response header. Screenshot 2021-03-26 at 11 59 33 Here is a code snippet:

    // Prepare interceptor to get response after click on download button
    cy.intercept('file').as('incidentPdfReport');
    // Click on download button
    cy.get('[data-test=reportDownload]').click();
    // Interceptor waiting when response will come and file is completely downloaded
    cy.wait('@incidentPdfReport', { timeout: 20000 }).then((res) => {
      const downloadsFolder = Cypress.config('downloadsFolder');
      // grab filename from 'content-disposition' header
      const filename = res.response.headers['content-disposition'].split('filename=')[1];
      const downloadedFilename = path.join(downloadsFolder, filename);
      cy.readFile(downloadedFilename, 'binary', { timeout: 15000 })
        .should((buffer) => {
          expect(buffer.length).to.be.gt(100);
        });
      cy.log(`**File ${filename} exists in downloads folder**`);
      cy.readFile(downloadedFilename).should((text) => {
        const lines = text.split('\n');
        expect(lines).to.have.length.gt(2);
        expect(lines[0]).to.equal('%PDF-1.4');
      });
    });

In this way you don't need any manipulations with downloads folder like finding the last downloaded file, delete all files before test etc. - you have exact filename and you can do with this file whatever you need. Hopefully it will help you.

Didn't work unfortunately. Just times out. CypressError: Timed out retrying after 20000ms: cy.wait() timed out waiting 20000ms for the 1st request to the route: pdfReport. No request ever occurred.

https://on.cypress.io/wait

Same for me too it says Timed out retrying after 20000ms: cy.wait() timed out waiting 20000ms for the 1st request to the route: pdfReport. No request ever occurred.

kaiyoma commented 2 years ago

@TomaszG Thank you so much for posting your solution! It was a real life saver for our project and now we can actually test all our file downloads that have random names. 🙏

henricook commented 2 years ago

@TomaszG In your example, how does getLastDownloadFilePath get registered as a task?

Edit: I see that plugins/index.js is deprecated these days - i'm looking for an equivalent solution at the moment

chaitanyarajugithub commented 1 year ago

I solved this issue with really great feature of Cypress - interceptor. Filename you can automatically grab from 'content-disposition' response header. Screenshot 2021-03-26 at 11 59 33 Here is a code snippet:

    // Prepare interceptor to get response after click on download button
    cy.intercept('file').as('incidentPdfReport');
    // Click on download button
    cy.get('[data-test=reportDownload]').click();
    // Interceptor waiting when response will come and file is completely downloaded
    cy.wait('@incidentPdfReport', { timeout: 20000 }).then((res) => {
      const downloadsFolder = Cypress.config('downloadsFolder');
      // grab filename from 'content-disposition' header
      const filename = res.response.headers['content-disposition'].split('filename=')[1];
      const downloadedFilename = path.join(downloadsFolder, filename);
      cy.readFile(downloadedFilename, 'binary', { timeout: 15000 })
        .should((buffer) => {
          expect(buffer.length).to.be.gt(100);
        });
      cy.log(`**File ${filename} exists in downloads folder**`);
      cy.readFile(downloadedFilename).should((text) => {
        const lines = text.split('\n');
        expect(lines).to.have.length.gt(2);
        expect(lines[0]).to.equal('%PDF-1.4');
      });
    });

In this way you don't need any manipulations with downloads folder like finding the last downloaded file, delete all files before test etc. - you have exact filename and you can do with this file whatever you need. Hopefully it will help you.

Didn't work unfortunately. Just times out. CypressError: Timed out retrying after 20000ms: cy.wait() timed out waiting 20000ms for the 1st request to the route: pdfReport. No request ever occurred. https://on.cypress.io/wait

Same for me too it says Timed out retrying after 20000ms: cy.wait() timed out waiting 20000ms for the 1st request to the route: pdfReport. No request ever occurred.

Same for me too it says Timed out retrying after 20000ms: cy.wait() timed out waiting 20000ms for the 1st request to the route: pdfReport. No request ever occurred.

Cypress.Commands.add('getDownloadfile', (element) => {
    // Prepare interceptor to get response after click on download button
    cy.intercept('**/api/v1.0/n/export/data?file_type=*').as('incidentReport');
    // Click on download button
    cy.contains('button',element).click();
    // Interceptor waiting when response will come and file is completely downloaded
    cy.wait('@incidentReport', { timeout: 20000 }).then((res) => {
      const downloadsFolder = Cypress.config('downloadsFolder');
      // grab filename from 'content-disposition' header
      const filename = res.response.headers['content-disposition'].split('filename=')[1];
      const downloadedFilename = path.join(downloadsFolder, filename);
      return downloadedFilename;
    });
})
viktorgogulenko commented 1 year ago

I solved this issue with really great feature of Cypress - interceptor. Filename you can automatically grab from 'content-disposition' response header. Screenshot 2021-03-26 at 11 59 33 Here is a code snippet:

    // Prepare interceptor to get response after click on download button
    cy.intercept('file').as('incidentPdfReport');
    // Click on download button
    cy.get('[data-test=reportDownload]').click();
    // Interceptor waiting when response will come and file is completely downloaded
    cy.wait('@incidentPdfReport', { timeout: 20000 }).then((res) => {
      const downloadsFolder = Cypress.config('downloadsFolder');
      // grab filename from 'content-disposition' header
      const filename = res.response.headers['content-disposition'].split('filename=')[1];
      const downloadedFilename = path.join(downloadsFolder, filename);
      cy.readFile(downloadedFilename, 'binary', { timeout: 15000 })
        .should((buffer) => {
          expect(buffer.length).to.be.gt(100);
        });
      cy.log(`**File ${filename} exists in downloads folder**`);
      cy.readFile(downloadedFilename).should((text) => {
        const lines = text.split('\n');
        expect(lines).to.have.length.gt(2);
        expect(lines[0]).to.equal('%PDF-1.4');
      });
    });

In this way you don't need any manipulations with downloads folder like finding the last downloaded file, delete all files before test etc. - you have exact filename and you can do with this file whatever you need. Hopefully it will help you.

Didn't work unfortunately. Just times out. CypressError: Timed out retrying after 20000ms: cy.wait() timed out waiting 20000ms for the 1st request to the route: pdfReport. No request ever occurred. https://on.cypress.io/wait

Same for me too it says Timed out retrying after 20000ms: cy.wait() timed out waiting 20000ms for the 1st request to the route: pdfReport. No request ever occurred.

Same for me too it says Timed out retrying after 20000ms: cy.wait() timed out waiting 20000ms for the 1st request to the route: pdfReport. No request ever occurred.

Cypress.Commands.add('getDownloadfile', (element) => {
    // Prepare interceptor to get response after click on download button
    cy.intercept('**/api/v1.0/n/export/data?file_type=*').as('incidentReport');
    // Click on download button
    cy.contains('button',element).click();
    // Interceptor waiting when response will come and file is completely downloaded
    cy.wait('@incidentReport', { timeout: 20000 }).then((res) => {
      const downloadsFolder = Cypress.config('downloadsFolder');
      // grab filename from 'content-disposition' header
      const filename = res.response.headers['content-disposition'].split('filename=')[1];
      const downloadedFilename = path.join(downloadsFolder, filename);
      return downloadedFilename;
    });
})

Could you please show a content of your "Network" tab after you clicked on button to download a file? Do you have a call at all?

chaitanyarajugithub commented 1 year ago

I solved this issue with really great feature of Cypress - interceptor. Filename you can automatically grab from 'content-disposition' response header. Screenshot 2021-03-26 at 11 59 33 Here is a code snippet:

    // Prepare interceptor to get response after click on download button
    cy.intercept('file').as('incidentPdfReport');
    // Click on download button
    cy.get('[data-test=reportDownload]').click();
    // Interceptor waiting when response will come and file is completely downloaded
    cy.wait('@incidentPdfReport', { timeout: 20000 }).then((res) => {
      const downloadsFolder = Cypress.config('downloadsFolder');
      // grab filename from 'content-disposition' header
      const filename = res.response.headers['content-disposition'].split('filename=')[1];
      const downloadedFilename = path.join(downloadsFolder, filename);
      cy.readFile(downloadedFilename, 'binary', { timeout: 15000 })
        .should((buffer) => {
          expect(buffer.length).to.be.gt(100);
        });
      cy.log(`**File ${filename} exists in downloads folder**`);
      cy.readFile(downloadedFilename).should((text) => {
        const lines = text.split('\n');
        expect(lines).to.have.length.gt(2);
        expect(lines[0]).to.equal('%PDF-1.4');
      });
    });

In this way you don't need any manipulations with downloads folder like finding the last downloaded file, delete all files before test etc. - you have exact filename and you can do with this file whatever you need. Hopefully it will help you.

Didn't work unfortunately. Just times out. CypressError: Timed out retrying after 20000ms: cy.wait() timed out waiting 20000ms for the 1st request to the route: pdfReport. No request ever occurred. https://on.cypress.io/wait

Same for me too it says Timed out retrying after 20000ms: cy.wait() timed out waiting 20000ms for the 1st request to the route: pdfReport. No request ever occurred.

Same for me too it says Timed out retrying after 20000ms: cy.wait() timed out waiting 20000ms for the 1st request to the route: pdfReport. No request ever occurred.

Cypress.Commands.add('getDownloadfile', (element) => {
    // Prepare interceptor to get response after click on download button
    cy.intercept('**/api/v1.0/n/export/data?file_type=*').as('incidentReport');
    // Click on download button
    cy.contains('button',element).click();
    // Interceptor waiting when response will come and file is completely downloaded
    cy.wait('@incidentReport', { timeout: 20000 }).then((res) => {
      const downloadsFolder = Cypress.config('downloadsFolder');
      // grab filename from 'content-disposition' header
      const filename = res.response.headers['content-disposition'].split('filename=')[1];
      const downloadedFilename = path.join(downloadsFolder, filename);
      return downloadedFilename;
    });
})

Could you please show a content of your "Network" tab after you clicked on button to download a file? Do you have a call at all?

please find the ref network calls

Screenshot 2023-03-06 at 7 38 23 PM
viktorgogulenko commented 1 year ago

@chaitanyarajugithub please try to use cy.intercept('**/api/v1.0/n/export/data?file_type=**').as('incidentReport');

chaitanyarajugithub commented 1 year ago

Cypress.Commands.add('getDownloadfile', (element) => { // Prepare interceptor to get response after click on download button cy.intercept('*/api/v1.0/n/export/data?file_type=').as('incidentReport'); // Click on download button cy.contains('button',element).click(); // Interceptor waiting when response will come and file is completely downloaded cy.wait('@incidentReport', { timeout: 20000 }).then((res) => { const downloadsFolder = Cypress.config('downloadsFolder'); // grab filename from 'content-disposition' header const filename = res.response.headers['content-disposition'].split('filename=')[1]; const downloadedFilename = path.join(downloadsFolder, filename); return downloadedFilename; }); })

No luck, Same issue --> Used cy.intercept('**/api/v1.0/n/export/data?file_type=**').as('incidentReport'); --> Timed out retrying after 20000ms: cy.wait() timed out waiting 20000ms for the 1st request to the route: incidentReport. No request ever occurred.

Can you provide any open-source example that works intercept? I am using the latest cypress version 12.7.0

viktorgogulenko commented 1 year ago

@chaitanyarajugithub I'll try to push something, but meanwhile I'm curious why do you have cy.wait('@IncidentReport'... as you are saving alias as .as('incidentReport'); (capital letter "I") - they should be the same.

chaitanyarajugithub commented 1 year ago

@chaitanyarajugithub I'll try to push something, but meanwhile I'm curious why do you have cy.wait('@IncidentReport'... as you are saving alias as .as('incidentReport'); (capital letter "I") - they should be the same.

It's just a typo error in the above comment but I used the correct match case in the code. here is the screenshot. Screenshot 2023-03-07 at 12 02 11 PM

viktorgogulenko commented 1 year ago

@chaitanyarajugithub I've found in examples something similar how to deal with files using interceptor Main idea here is to: prepare interceptor for expected call -> make an action which is making this call (push the button for ex) -> wait with cy.wait when call will be successfully executed. There could be few issues - was used wrong regex to filter out proper call, if call happened - be sure that Content-Disposition header is in list of response headers to be able to grab filename:

Screenshot 2023-03-22 at 12 22 50
TomaszG commented 1 year ago

@TomaszG In your example, how does getLastDownloadFilePath get registered as a task?

Edit: I see that plugins/index.js is deprecated these days - i'm looking for an equivalent solution at the moment

// cypress.config.js
const { defineConfig } = require('cypress')

const { testUsers } = require('@hm/common-data')

module.exports = defineConfig({
    (...)
    e2e: {
        setupNodeEvents(on, config) {
            return require('./cypress/plugins')(on, config)
        },
        (...)
    },
})
// cypress/plugins/index.js

module.exports = (on, config) => {
    on('task', {
        ...require('./tasks'),
    })

    return config
}
// cypress/plugins/tasks.js

const yourTask = () => {
    // do stuff

    return null
}

module.exports = {
    yourTask,
}
jorunfa commented 1 year ago

amended generic version, without path dependency (might not work on Windows):

Cypress.Commands.add(
  "downloadAndReadFile",
  { prevSubject: "element" },
  (subject) => {
    return cy.wrap(subject).then(($el) => {
      cy.wrap($el).invoke("attr", "href").then(url => {
        cy.intercept(url).as("download");
        cy.wrap($el).click();
        cy.wait("@download").then((res) => {
          const downloads = Cypress.config("downloadsFolder");
          const fileName = res.response.headers["content-disposition"].split("filename=")[1];
          return cy.readFile(`${downloads}/${fileName}`);
        });
      });
    });
  });

used like this:

cy.contains("Download link")
      .downloadAndReadFile()
      .should("exist")
      .and("contain", "my-expected-content");
pratikjain10999 commented 11 months ago

Hey guys can someone help in how to download dynamic files in cypress and test the content present in it (My file doesnt contain a content diposition line in headers so that might not work for me

satya081 commented 4 months ago

Hey guys can someone help in how to download dynamic files in cypress and test the content present in it (My file doesnt contain a content diposition line in headers so that might not work for me

Yes same for me. Could any one help on this?

viktorgogulenko commented 4 months ago

@satya081 @pratikjain10999 please open your Network tab in Developer Tools, initiate downloading of the file and check what call is responsible for that, click on it and make a screenshot with details of this call (URL, headers etc). I'm pretty sure that call could be unique enough so we could grab some info from there but this is hard to understand what's happening in your case without more details.

dibyanshusinha commented 3 weeks ago

Hi Folks, I have the same problem of downloading dynamically generated file and we are using some third party plugin which creates a blob and downloads the file dynamically with some timestamp in the name. Since we are able to see the logged value in the cypress console, is there a way to access those values inside the test itself. I checked the network logs there's nothing there.

However am able to confirm the downloaded file getting saved and even in the cypress logs it's available.

Screenshot 2024-09-18 at 12 51 51 PM

    "message": "/cypress/downloads/Policy_1726644028992.xlsx",
    "name": "download",
    "type": "parent",
    "event": true,
    "timeout": 0,
    "isCrossOriginLog": false,
    "id": "log-http://localhost:0000-381",
    "chainerId": "ch-http://localhost:0000-42",
    "state": "passed",
    "hidden": false,
    "instrument": "command",
    "url": "http://localhost:0000/policy-search#/",
    "hookId": "r3",
    "testId": "r3",
    "testCurrentRetry": 0,
    "viewportWidth": 1000,
    "viewportHeight": 660,
    "wallClockStartedAt": "2024-09-18T07:20:30.963Z",
    "createdAtTimestamp": 1726644030963.1,
    "updatedAtTimestamp": 1726644031046.1,
    "consoleProps": {
        "name": "download",
        "type": "event",
        "props": {
            "Download URL": "blob:http://localhost:0000/a6f98e18-eabf-485a-b76b-a2154eb4f14e",
            "Saved To": "/cypress/downloads/Policy_1726644028992.xlsx",
            "Mime Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
        }
    },
    "renderProps": {},
    "snapshots": [
        {
            "timestamp": 1726644030964.7002,
            "htmlAttrs": {},
            "body": {}
        },
        {
            "timestamp": 1726644031013.5,
            "htmlAttrs": {},
            "body": {}
        }
    ],
    "ended": true,
    "snapshot": false
}

Would really appreciate your help here. Thanks.
jorunfa commented 3 weeks ago

@dibyanshusinha: if you have access to the file path, can't you just use cy.readFile()?

something like

cy.readFile(thing.message).should('eq', 'Hello World')

https://docs.cypress.io/api/commands/readfile#Text

dibyanshusinha commented 3 weeks ago

@jorunfa The filename is dynamic in nature I mean filename_${timestamp}.${extension}. So the exact filepath will not be known, hence my question.

Is there a way to know the exact filename of the file which was just downloaded ? I can see the filename in cypress console. Is there a way to access the filename, by spying on the event which triggered that log present in the cypress console, instead of only relying on the log events ?

dibyanshusinha commented 3 weeks ago

FYI As I was saying, I did this which works quite well. I wish there was a cypress event something such as cy.on('download:triggered', (...args)), it would have helped a lot.

For all those who end up here and don't mind relying on the logs. Here's a snippet.

spec.cy.js

 //   ..................  //
 //   ..................  //

let fileName;

const checkDownloadLog = (attr) => {
  console.log(attr);
  const { name, event, message } = attr;
  if (name==='download' && event) {
    fileName = message;
  }
};

cy.on('log:added', checkDownloadLog);

//ACTION Which lead to download
cy.get('button').click().then(_res => {
  cy.removeListener('log:added', checkDownloadLog);
  console.log('FILE_NAME', fileName);
  //   ..................  //
  //   ..................  //
});