cypress-io / cypress

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

How to load an array of fixtures in cypress? #2932

Closed tnrich closed 5 years ago

tnrich commented 5 years ago

Hi there, I'm wondering how to upload an array of fixtures for them to be drag and dropped into a file upload. My code currently looks like:

Cypress.Commands.add('uploadFiles', (selector, fileUrlOrUrls, type = '') => {
  const fileUrls = Array.isArray(fileUrlOrUrls)
    ? fileUrlOrUrls
    : [fileUrlOrUrls]
  Promise.all(
    fileUrls.map(fileUrl => {
      const nameSegments = fileUrl.split('/')
      const name = nameSegments[nameSegments.length - 1]
      return cy
        .fixture(fileUrl, 'base64')
        .then(Cypress.Blob.base64StringToBlob)
        .then(blob => {
          console.log('name:', name)
          return new File([blob], name, { type })
        })
    })
  ).then(files => {
    console.log('files:', files)
    const event = { dataTransfer: { files } }
    return cy.get(selector).trigger('drop', event)
  })
})

I want to use it like so:

cy.uploadFiles('.tg-dropzone',["filePath1", "filePath2"])

This promise all doesn't seem to work. Multiple files are returned in the promise.all, but they are all the same! Is this because you can't really use cypress like normal promises? If so, how can I do what I want -- upload an array of fixtures?

Thanks!

jennifer-shehane commented 5 years ago

What is this line logging? console.log('files:', files)

tnrich commented 5 years ago

@jennifer-shehane given this code

cy.uploadFiles('.tg-dropzone',["filePath1", "filePath2"])

that line was logging

[fileObj, fileObj]

where the two fileObjs are identical.

tnrich commented 5 years ago

Curiously, my coworker changed the above code just slightly to this:

Cypress.Commands.add('uploadFiles', (selector, fileUrlOrUrls, type = '') => {
  const fileUrls = Array.isArray(fileUrlOrUrls)
    ? fileUrlOrUrls
    : [fileUrlOrUrls]

  let files = [];

  Promise.all(
    fileUrls.map(fileUrl => {
      const nameSegments = fileUrl.split('/')
      const name = nameSegments[nameSegments.length - 1]
      return cy
        .fixture(fileUrl, 'base64')
        .then(Cypress.Blob.base64StringToBlob)
        .then(blob => {
          const file = new File([blob], name, { type })
          files.push(file);
        })
    })
  ).then(() => {
    console.log('files:', files)
    const event = { dataTransfer: { files } }
    return cy.get(selector).trigger('drop', event)
  })
})

and it seems to be working now

jennifer-shehane commented 5 years ago

Closing as resolved.

tomatau commented 5 years ago

This isn't fixed, attempting to use two fixtures in a test is incredibly buggy and inconsistent. It's like JavaScript has somehow been broken by cypress here... common promise behaviour doesn't seem to apply in this strange cypress world. What did you do?

bahmutov commented 5 years ago

Thomas can we ask you to paste the test code snippet that is broken?

Sent from my iPhone

On Mar 18, 2019, at 19:17, Thomas notifications@github.com wrote:

This isn't fixed, attempting to use two fixtures in a test is incredibly buggy and inconsistent. It's like JavaScript has somehow been broken by cypress here... common promise behaviour doesn't seem to apply in this strange cypress world. What did you do?

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub, or mute the thread.

tomatau commented 5 years ago

Cypress Version 3.2.0 (3.2.0) OSX: 10.14.3 node --version v10.11.0 Chrome Version 73.0.3683.75

      it.only(`hmmmm`, () => {
        const fixtures = []

        Promise.all([
          cy.fixture('list-guess-incorrect.json').then(fx => fixtures.push(fx)),
          cy.fixture('list-head.json').then(fx => fixtures.push(fx)),
        ]).then(fx => {
          console.log(fx)
          console.log(fixtures)
        })
tomatau commented 5 years ago

Better yet.

it.only(`hmmm`, () => {
  const fixtures = []

  Promise.all([
    cy.fixture('list-guess-incorrect.json').then(fx => fixtures.push(fx)),
    cy.fixture('list-head.json').then(fx => fixtures.push(fx)),
  ]).then(fx => {
    cy.wait(1000).then(() => {
      console.log(fx)
      console.log(fixtures)
    })
  })

Now fx (the first log) still logs the same item twice... but the second log fixtures now logs [undefined, item]... \(ツ)

tomatau commented 5 years ago

Using Cypress.Promise.all seems to work!

But still... Need to close over an array that I mutate instead of using the resolved values :/

        const fixtures = []

        Cypress.Promise.all([
          cy.fixture('list-head.json').then(fx => fixtures.push(fx)),
          cy.fixture('list-guess-incorrect.json').then(fx => fixtures.push(fx)),
        ]).then((fx) => {
          console.log(fx)
          console.log(fixtures)
        })

Trying to make this more terse and trust the normal JavaScript promise behaviour

        Cypress.Promise.all([
          cy.fixture('list-head.json'),
          cy.fixture('list-guess-incorrect.json')
        ]).then((fixtures) => {
          console.log(fixtures)
        })
jean-moldovan commented 5 years ago

@jennifer-shehane , same issue as @tomatau has. Getting the first fixture item twice when using Cypress.Promise.all

bahmutov commented 5 years ago

I recommend looking at the "Fixtures" recipe I have added to https://github.com/cypress-io/cypress-example-recipes - which shows how to load one or several fixtures

jean-moldovan commented 5 years ago

Sorry, what would be the recommended approach when loading 3 or more fixtures? So called pyramid of doom of fixtures does not scale all that well.

tomatau commented 5 years ago

Yeah I'm not keen on the callback hell being the recommendation here either, especially when the language has features to move past that now.

This worked for me and could scale to more.

        const fixtures = []

        Cypress.Promise.all([
          cy.fixture('first.json').then(fx => fixtures.push(fx)),
          cy.fixture('second.json').then(fx => fixtures.push(fx)),
        ]).then((fx) => {
            const [ first, second ] = fixtures
        })

edit: it should also be faster than the nesting as the requests are being made concurrently.

IMightGitIt commented 4 years ago

After a lot of head-scratching and inspiration from previous comments I finally got a command working, which takes an array of fixturepaths and names, and produces a single payload for the upload plugin. not only does it help when I'm actually testing upload of multiple files, but it should also save me some time instead of uploading each of my required test file one by one. Next step is to fetch the right MimeType for each file base on the filename extension, but I think I can easily solve it with an npm package "mime-types".


Cypress.Commands.add('uploadMultiple', ({localPaths = [], fileNames = [], mimeType = 'image/png'}) => {
  const fixtures = (paths) => Promise.all(paths.map(p => cy.fixture(p)))
  const encoding = 'base64'

  fixtures(localPaths).then(filesContents => {
    console.log(filesContents)
    const payload = fileNames.map((fileName, i) => (
      {
        fileContent: filesContents[i],
        fileName,
        encoding,
        mimeType
      }))
    console.log(payload)
    cy.get('input[multiple]')
    .upload(payload) 
  })
})```
javier-cestau commented 3 years ago

I did something like this, thanks to @tomatau and it's working so far

--- modules/load_fixtures.js---
export const loadFixtures = arrayOfFixtures => {
  const fixtures = []
  const promises = arrayOfFixtures.map(fixture => cy.fixture(fixture).then(res => fixtures.push(res)))
  return Cypress.Promise.all(promises).then(() => {
    return fixtures
  })
}

--- integration/test.spec.js---
import { loadFixtures } from 'modules/load_fixtures.js'
it('description', function() {
  loadFixtures(['beneficiaries.json', 'transactions.json']).then(
   ([beneficiaries, transaction]) => {
     console.log(beneficiaries)
     console.log(transaction)
  })
})        
DudeRandom21 commented 3 years ago

Unfortunately this solution doesn't seem to work anymore, cypress complains about mixing promises and cypress commands. Though it seems that the fact that cypress actually runs these commands sequentially allows you to do something even simpler:

const loadFixtures = (fixtures: string[]) => {
  const res = [];
  fixtures.map((name, i) => cy.fixture(name).then((f) => (res[i] = f)));    // could also use `res.push(f)` they should be equivalent
  return cy.wrap(res);
}

Because we're mutating the res array, even though cy.wrap will likely be called on an empty array (since the cy.fixture calls will be enqueued but not yet run), that's not a problem because no cypress command after that will run either because they would only be enqueued. In this way any cy command functionally acts as a guard condition for any other, so we can guarantee that anything chaining off of this would see a fully populated array.

I normally would not recommend mutating state this way because it can lead to some confusing behavior but in this case it seems to be a necessary evil.