cypress-io / cypress

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

Support ability to visit non `text/html` content-type pages (aka - visit downloaded file page) #1551

Open xtingray opened 6 years ago

xtingray commented 6 years ago

Current behavior:

I am trying to test a download action from a .ts file using the attribute window.location.href. It doesn't matter what value I assign for the URL string, the test instruction fails claiming a timeout waiting for the request when the file actually has been downloaded in few seconds. For some unknown reason Cypress never can reach the URL.

Note: I am using a Genesis UI theme as my development code base.

These are the paths that I have tried aiming to get an Ok result from the Cypress test:

  1. window.location.href = '/api/download/file_id';
  2. window.location.href = '/#/api/download/file_id';
  3. window.location.href = 'http://localhost:5000/api/download/file_id';
  4. window.location.href = 'http://localhost:5000/#/api/download/file_id';

All of them fail. But if I try the first option as a normal user from the browser directly, the download works perfectly.

Note: During the Cypress test, I can see how the file is actually downloaded. The download action occurs.

Desired behavior:

All that I am expecting is to be able to test my download action from the Cypress test.

If I trigger the download button using something like:

cy.get('[data-cy=download-file]').click();

I expect to get a positive response from the test if the file was actually downloaded.

How to reproduce:

Note: It would be great if you can try this steps using a Genesis UI theme as the base application.

  1. Create a form template with a button pointing to the .ts method on the (click) parameter: (click)="downloadFile()" Note: Include a data-cy parameter as part of the button in the form: data-cy="download-file"
  2. Inside the .ts method use the instruction, ensuring to use a valid path: window.location.href = '/path/to/file/for/download';
  3. Write a Cypress test when you try to trigger the download button from the form. Something like: cy.get('[data-cy=download-file]').click();
  4. You should get a timeout message similar to the one included as attached image.

Test code:

cy.get('[data-cy=download-file]').click(); 

Additional Info (images, stack traces, etc)

timeout

jennifer-shehane commented 6 years ago

You can only navigate to pages within Cypress that are content-type='text/html'. I imagine that your application is navigating to a page that is not of that type (where the file is located), so Cypress is expecting a load event to happen under the assumption of having been navigated to a new text/html page.

As a workaround, you could test that upon clicking the button that this downloadFile method is called using cy.spy() or change the implementation of the button to an <a> with href attribute.

A a more permanent fix, Cypress could potentially be updated to handle some other content-types.

ohenrique commented 6 years ago

I'm with same problem, when I click the download link, the Cypress still in the same step (expecting a load event happen). But, how can I spy the event downloadFile, if I need fire it by clicking the link? The problem is the click() need to have a loaded page response.

lucialuzuriaga commented 6 years ago

I am having this exact same issue. I am asserting on the status code of the call, but even after the assert passes it just keeps waitifing for the page to load.

Is there a way I could force cypress to stop the test execution after the "its" statement?

epszaw commented 6 years ago

Same story here. Tests have a big lag and fail after in CI when file was downloaded.

LeeRodney commented 6 years ago

I have the same issue. Is there a way to stop the page load from triggering?

Prashant-Kan commented 5 years ago

Did any one find the workaround for this issue. I am having the Same issue. Cypress getting stuck at the step of clicking anchor element whose HREF has the full URL to download the file. Though the file gets successfully downloaded but the response never comes back to the cypress steps and eventually it gets timeout.

Garry2012 commented 5 years ago

I am having same issue; Is there any workaround anyone found for this issue?

breakpointninja commented 5 years ago

Same issue. I can't hack around it. This is a complete blocker.

mitschmidt commented 5 years ago

Same for me - when running via cypress binary in Chrome, everything works fine- however when running cypress CLI the test just keeps on dangling forever w/o a fail or error message.

Would be a shame if you cannot test this crucial functionality - in my case it is downloading a dynamically generated export file from the backend.

ddesna commented 5 years ago

+1 I'm clicking button that executes JS function creating element with URL and click it. Cypress test fails due timeout but file is downloaded correctly.

chrismelbourne commented 5 years ago

+1 Now we're just waiting for the 60s "Page Load" timeout each time a file download is triggered.

At the very least we need a way to prevent Cypress from triggering a Page Load event for non-html types.

KWorke commented 5 years ago

+1

nirpeled commented 5 years ago

+1

aposiker commented 5 years ago

+1. Considered to switch from our current framework + cloud service for cross-browser tests to Cypress, but for now, this issue looks like a blocker for us.

ioana-gln commented 5 years ago

Any news on this?

jennifer-shehane commented 4 years ago

I was looking for ways to work around this, so have created a reproducible example of the issue in the code below:

Reproducible Example

This requires a sheet.csv within the root of the project, I just used a blank csv.

index.html

<!doctype html>
<html lang="en">
<body>
<script>
  function startDownload(url) {
    window.location.href = url;
  }
</script>
<button onclick="startDownload('/assets/sheet.csv')">
</body>
</html>

spec.js

it('download csv', () => {
  cy.visit('index.html')
  cy.get('button').click()
})
Screen Shot 2019-11-26 at 4 00 22 PM

Workaround

For the above example, we can stub the call to the function. This way the method is not called directly, but we can still test that the correct argument (the path to the file) is being passed to our download method.

it('download csv', () => {
  cy.visit('index.html')
  cy.window().then((win) => {
    cy.stub(win, 'startDownload').as('downloadMethod')
  })
  cy.get('button').click()
  cy.get('@downloadMethod').should('be.calledWith', 'sheet.csv')
})
Screen Shot 2019-11-26 at 3 57 43 PM

A less optimal workaround

You could also just not call the code within the download method when running within Cypress. This is less optimal because you do lose testing the file argument, but I thought I would mention it just in case it is helpful to someone.

From within your application code, in the example above you would do:

function startDownload(url) {
  if (!window.Cypress) {
    window.location.href = url;
  }
}
Anastassiabp commented 4 years ago

How can I call this workaround method if I have "Download all" button, where all files are selected?

tuly12 commented 4 years ago

it('download csv', () => { cy.visit('index.html') cy.window().then((win) => { cy.stub(win, 'startDownload').as('downloadMethod') }) cy.get('button').click() cy.get('@downloadMethod').should('be.calledWith', 'sheet.csv') })

where the startDownload function is implemented?

sanagaraj-pivotal commented 4 years ago

This is resolved when you disabling chrome web security. This can be done by adding following line into cypress.json

  "chromeWebSecurity": false
egemon commented 3 years ago

One more possible solution:

cy.intercept(downloadUrl, cy.spy().as("fileDownload"));
...

cy.get("@fileDownload").should("have.been.calledOnce");

NOTE: downloadUrl probably should be relative, as absolute will hangs, if you are using faker and it doesn't exist. Relative will end with 404 which is ok.

suryakumara commented 2 years ago

have any update ?

ChrisCrossCrash commented 2 years ago

I've found a workaround/solution for a similar problem as described in the original issue. The difference is that the original issue describes running a callback function after clicking a button which triggers a download. Mine works on a form's "Submit" button that triggers a POST request with a response for downloading a CSV file. However, I think this solution would work for both cases.

Here's my form:

image

As you can see, the user clicks the "Go" button which is type='submit', which submits the form data and gets a response with these headers:

image

The response will time out because 1) it's not content-type: text/html, and 2) the content-disposition header says that this file should be downloaded and saved on the computer. It can't load the page with these headers. The solution is to modify the headers of the response:

cy.intercept(
  'POST', // Method
  '/myapp/signups/', // URL
  (req) => {
    req.continue((res) => {
      // Change the response headers that would prevent the
      // request from being loaded in Cypress.
      res.headers['content-disposition'] = undefined
      res.headers['content-type'] = 'text/html'
      // I know that the CSV file should contain `fname`
      expect(res.body).to.include('fname')
    })
  }
).as('download')
laerteneto commented 1 year ago

I had the same issue while I was trying to download a csv file. I have a button and when I click on it I get its respective response with a content-type different of text/html based on csv format (in my case text/comma-separated-values).

My first approach was basically to retrieve the href from the button and visit it. However, as we know, the visit command only allow us to visit content-type of text/html docs.cypress.io/api/commands/visit#Requirements-Icon-namequestion-circle

So, there are the 2 workarounds I created in here:

1 - intercept the request that is triggered when I click in the download button and replace content-disposition and content-type:

  /**
   * Show the CSV in the screen by changing the headers to not fail by reading responses with content-type other than text/html
   */
  openCSVWithInterceptionWorkAround() {
    cy.get('#downloadBtn')
      .invoke('attr', 'href')
      .then(($href) => {
        cy.intercept(
          'GET',
          $href, // URL
          (req) => {
            req.continue((res) => {
              // Change the response headers that would prevent the request from being loaded in Cypress.
              // @ts-ignore
              res.headers['content-disposition'] = undefined
              res.headers['content-type'] = 'text/html'
            })
          }
        ).as('exportCsv')

        cy.forcedWait(2000) // Give some time before clicking
        cy.get(selectors.exportIcon).click()
        cy.wait('@exportCsv')
      })
  }

2 - Extract the button url, make a request on it and write a csv file with the response:

  /**
   * This method exports the csv file by sending a request rather than clicking in the export csv button.
   * This approach avoids the page load issue to get stuck. Also, it allow us to control the file name output from the csv that is being downloaded.
   *
   *
   * @param {String} filePathToSaveTheCsv The file path name you can choose to save the csv file. E.g "cypress/downloads/Expense Detail Report.csv"
   */
  exportCsvWithRequest(filePathToSaveTheCsv) {
    cy.get('#downloadBtn')
      .invoke('attr', 'href')
      .then(($href) => {
        cy.request($href) => {
          cy.writeFile(filePathToSaveTheCsv, response.body)
        })
      })
  }

While with the first approach you can basically open a csv in the browser, the second one allows you to properly save a csv file as if you were downloading it.

I hope it helps someone, the second approach was the best shot for my case.

taoeffect commented 1 year ago

Alright guys! I figured this out after a day of trial and error!

I needed to be able to download a PDF. The problem is the workflow involved me clicking on a link that would direct me to a page that would render a PDF inline using the browser's in-browser PDF render.

This only works when using Firefox (and maybe Chrome, haven't tested). Electron will not work.

Replace relevant parts in all caps marked REPLACE with your URLs, selectors, etc.:

session = saveSession()
const downloadReq = {}

cy.intercept('GET', '/REPLACE/WITH/YOUR/DOWNLOAD/URL/PREFIX/*', (req) => {
  downloadReq.url = req.url
  downloadReq.headers = req.headers
  req.redirect(session.url) // redirect to the current page we're on
}).as('download')
cy.get('#REPLACEWITHSELECTOR')
  .contains('=== REPLACE THIS WITH THE LINK TEXT TO CLICK ON ===')
  .click()
cy.log('waiting for download..')
cy.wait('@download').then(() => {
  cy.log(`downloading ${downloadReq.url} with headers:`, downloadReq.headers)
  cy.request({
    url: downloadReq.url,
    method: 'GET',
    headers: downloadReq.headers,
    encoding: 'binary',
  }).then((res) => {
    if (res.status === 200) {
      const filename = res.headers['content-disposition'].split('filename=')[1].replaceAll('"', '').trim()
      cy.log('got response with file:', filename)
      cy.writeFile(filename, res.body, 'binary')
    } else {
      cy.log(`failed response: ${res.status}: ${res.statusText}`)
    }
  })
})

What we do is we:

  1. First create an interceptor to listen for the download URL to be clicked on and set it up to capture the URL we want and then redirect us to the current page we're on
  2. Afterwards we use cy.request with cy.writeFile to save the PDF to where we want