cypress-file-upload icon indicating copy to clipboard operation
cypress-file-upload copied to clipboard

[Feature] adding webkitdirectory to support directory uploads

Open endymonium opened this issue 5 years ago • 19 comments

I would like to test a directory upload input, for that it should be enough to set the webkitRelativePath property in the created File object(s), eg:

File { 
name: "PANO_20190103_141758.jpg", 
lastModified: 1546521489000, 
webkitRelativePath: "images/2017/01/PANO_20190103_141758.jpg", 
size: 11591640, 
type: "image/jpeg" }

The File constructor does not support that, but you can force set it afterwards via Object.set. I'm using that approach already in my app if the user selects a single file instead of a directory.

Could that be implemented?

endymonium avatar Jan 02 '20 13:01 endymonium

Hi @endymonium Thanks for submitting the issue! It was requested a while ago from @artemgurzhii in #69 but there was not much users so I decided not to do that.

Right now I feel it might be a good thing to implement, but not sure if I can help with coding that. Can you please look through the source code and make a PR for that? We can review it together with @artemgurzhii and finally make it happen 😄

Thanks!

abramenal avatar Jan 05 '20 12:01 abramenal

@abramenal Any timeline on when this will happen? We would love to see this feature happen!

MingruiZhang avatar May 26 '20 08:05 MingruiZhang

Hey @MingruiZhang Thanks for feedback!

I am not sure which value is expected for webkitRelativePath – please suggest. Otherwise it can roughly be taken from the command arguments. For this simple solution I would expect having it by the end of this week. Are you using v4?

cc @endymonium @artemgurzhii

abramenal avatar May 26 '20 08:05 abramenal

I’m eagerly waiting to run our pending test case using cypress for folder upload functionality.

vishnuprabhu-95 avatar Jan 30 '21 18:01 vishnuprabhu-95

Hey @MingruiZhang Thanks for feedback!

I am not sure which value is expected for webkitRelativePath – please suggest. Otherwise it can roughly be taken from the command arguments. For this simple solution I would expect having it by the end of this week. Are you using v4?

cc @endymonium @artemgurzhii

Hey @abramenal, any update on this?

mysticdevx avatar Mar 17 '21 12:03 mysticdevx

I would like to test a directory upload input, for that it should be enough to set the webkitRelativePath property in the created File object(s), eg:

File { 
name: "PANO_20190103_141758.jpg", 
lastModified: 1546521489000, 
webkitRelativePath: "images/2017/01/PANO_20190103_141758.jpg", 
size: 11591640, 
type: "image/jpeg" }

The File constructor does not support that, but you can force set it afterwards via Object.set. I'm using that approach already in my app if the user selects a single file instead of a directory.

Could that be implemented?

Hey @endymonium can you share pseudocode on how you achieve this?

mysticdevx avatar Mar 17 '21 12:03 mysticdevx

Hello ... is there any update on this? We would also need to upload a directory structure (folder with files and subfolders). @endymonium Could you please write an example how your workaround looks like for this.

I couldn't find anywhere example how to test folder structure upload with cypress.

Donaab avatar Mar 30 '21 10:03 Donaab

@abramenal Can you maybe use this implementation https://github.com/knotthere/cypress-file-upload/blob/21e16fbbc0f9d1f8b842eab390a3beebbe53ce1f/src/attachDirectory.js ?

Donaab avatar Apr 01 '21 10:04 Donaab

Hey @Donaab, this looks good – just wondering why @knotthere didn't submit a PR with this. I can pick it up early next week

abramenal avatar Apr 01 '21 12:04 abramenal

@abramenal Only thing with that command is it doesn’t recognize a subfolder as item to add to the dataTransfer. It just reads the files from that folder for me.

Donaab avatar Apr 01 '21 13:04 Donaab

@abramenal Maybe it would be good idea to add an option to add some items in the dataTransfer .

This is how my dataTransfer looks like:

dataTransfer: {
        items: [
          {
            webkitGetAsEntry: () => ({
              isDirectory: true,
              isFile: false,
              fullPath: '/foo',
              createReader() {
                return {
                  sentEntries: false,
                  readEntries(callback) {
                    if (!this.sentEntries) {
                      this.sentEntries = true;
                      callback([
                        {
                          isDirectory: false,
                          isFile: true,
                          file: callback => callback(testFile),
                          fullPath: '/foo/alpha.txt',
                        },
                      ]);
                    } else {
                      callback([]);
                    }
                  },
                };
              },
            }),
          },
          {
            webkitGetAsEntry: () => ({
              isDirectory: true,
              isFile: false,
              fullPath: '/bar',
              createReader() {
                return {
                  sentEntries: false,
                  readEntries(callback) {
                    if (!this.sentEntries) {
                      this.sentEntries = true;
                      callback([]);
                    } else {
                      callback([]);
                    }
                  },
                };
              },
            }),
          },
        ],
        types: ['Files'],
      }

Donaab avatar Apr 01 '21 14:04 Donaab

Would be nice if someone can join code review – I am not sure I fully understand use cases, so might have done some garbage instead of solution 😉

abramenal avatar Apr 06 '21 20:04 abramenal

I have an use case, It like uploading a folder with multiple files. Then we list all the files in UI. After reviewing, it will be submitted in the application.

It’s almost like google drive folder upload behaviour.

Can you please help me, when can I use this feature? @abramenal

Please suggest workaround for scripts in cypress.

I checked with this link https://github.com/knotthere/cypress-file-upload/blob/21e16fbbc0f9d1f8b842eab390a3beebbe53ce1f/src/attachDirectory.js

Didn’t help me…

vishnuprabhu-95 avatar May 04 '21 19:05 vishnuprabhu-95

Please provide this feature soon. Waiting for six months. @abramenal

vishnuprabhu-95 avatar May 24 '21 19:05 vishnuprabhu-95

I'm trying to help with this.

It seems the simplest solution would be to fix the upload fixtures so they return the correct information on when webkitGetAsEntry(https://wicg.github.io/entries-api/#dom-datatransferitem-webkitgetasentry) is called on the dataTransfer items.

For some reason when uploading files via cypress-file-upload webkitGetAsEntry() always returns null. If we could fix this to give the correct FileEntry object the folder uploading should be pretty trivial

Any help on what we need to do to make webkitGetAsEntry() work with the files selected via cypress-file-upload would be appreciate as I am stuck as to what needs to change to get that data.

anark avatar Jun 06 '21 17:06 anark

I believe the reason that webkitGetAsEntry is null is because the drag item is stuck in protected mode. This data is only accessible on the drop event(not drag or drag over). https://stackoverflow.com/a/31922258.

As we are using a custom event to call drop it appears this will always be null. Not sure what the best approach would be here. Ideally we would be able to get the real data that would come from calling item.webkitGetAsEntry() however as that appears to be protected and I cannot find a way to simulate an actual drop with real files we may need to stub this? Any recommendations on an approach here would be appreciated.

anark avatar Jun 08 '21 18:06 anark

Same problem. We're using webkitGetAsEntry together with react-dnd to distinguish between files and directories. Any workaround?

C3PablO avatar Sep 08 '21 08:09 C3PablO

I went for a solution without cypress-file-upload mocking the DataTranfer class and all the other classes involved. Maybe for other cases these mock will need a more detailed implementation but this worked fine for me:

export class DataTransferMock {
  constructor () {
    this.data = { dragX: '', dragY: '' }
    this.dropEffect = 'none'
    this.effectAllowed = 'all'
    this.files = []
    this.img = ''
    this.items = new DataTransferItemListMock()
    this.types = ['Files']
    this.xOffset = 0
    this.yOffset = 0
  }

  clearData () {
    this.data = {}
  }

  getData (format) {
    return this.data[format]
  }

  setData (format, data) {
    this.data[format] = data
  }

  setDragImage (img, xOffset, yOffset) {
    this.img = img
    this.xOffset = xOffset
    this.yOffset = yOffset
  }
}

export class DataTransferItemListMock extends Array {
  clear () {
    this.splice(0, this.length - 1)
  }

  add (data, options) {
    this.push(data)
  }

  remove (index) {
    if (index > -1) {
      this.splice(index, 1)
    }
  }
}

export class DataTransferItemMock {
  constructor (entry) {
    this.kind = 'file'
    this.type = entry.data.type
    this.file = entry.data
    this.entry = entry
  }

  getAsFile () {
    return this.file
  }

  getAsString () {
    return ''
  }

  webkitGetAsEntry () {
    return this.entry
  }
}

export class FileSystemEntryMock {
  constructor (file, options) {
    this.filesystem = { name: file.name, root: '' }
    this.isFile = options.isFile
    this.isDirectory = !options.isFile
    this.fullPath = options.fullPath ?? ''
    this.name = file.name
    this.data = file
  }

  file (callback) {
    return callback(this.data)
  }

  getParent () {

  }
}

export class FileSystemDirectoryEntryMock extends FileSystemEntryMock {
  constructor (data, options) {
    super(data, options)
    this.entries = []
  }

  setEntries (entries) {
    this.entries = entries
  }

  getDirectory (path, options, callback) {
    callback(this.data)
  }

  getFile (path, options, callback) {
    callback(options.fullPath)
  }

  createReader () {
    return new FileSystemDirectoryReaderMock(this.entries)
  }
}

export class FileSystemDirectoryReaderMock {
  constructor (entries) {
    this.entries = entries
    this.read = false
  }

  readEntries (callback) {
    if (this.read === false) {
      this.read = true
      callback(this.entries)
    } else {
      this.entries = []
      callback(this.entries)
    }
  }
}

then created the following command:

const mapEntries = (blob, files, fullPath = '') => {
  return files.map((fileData) => getEntry(blob, fileData, fullPath))
}
const getEntry = (blob, fileData, fullPath = '') => {
  let transferItem
  if (fileData.type) {
    const systemEntry = new FileSystemEntryMock(new File([blob], fileData.name, { type: fileData.type }), { isFile: true, fullPath: fullPath ? `/${fileData.name}` : fileData.name })
    transferItem = systemEntry
  } else if (fileData.entries) {
    const systemDirectoryEntry = new FileSystemDirectoryEntryMock(new File([], fileData.name), { isFile: false, fullPath: fullPath ? `/${fileData.name}` : fileData.name })
    const entries = mapEntries(blob, fileData.entries, fullPath)
    systemDirectoryEntry.setEntries(entries)
    transferItem = systemDirectoryEntry
  }
  return transferItem
}

Cypress.Commands.add('dropFiles', (fixture, files) => {
  const dataTransfer = new DataTransferMock()

  files.forEach((fileData, i) => {
    const blob = Cypress.Blob.base64StringToBlob(fixture, fileData.type)
    const testFile = new File([blob], fileData.name, { type: fileData.type })
    dataTransfer.files.push(testFile)
    dataTransfer.items.add(new DataTransferItemMock(getEntry(blob, fileData)))
  })

  cy.get('.your_selector').trigger('dragenter', { dataTransfer: dataTransfer })
    .trigger('dragover', { dataTransfer: dataTransfer })
    .trigger('drop', { dataTransfer: dataTransfer })
})

can be used like this:

cy.dropFiles(fixture, [
        { name: `${name}0`, type: 'image/jpeg' },
        {
          name: 'directory1',
          entries: [
            { name: `${name}1`, type: 'image/jpeg' },
            {
              name: 'directory2',
              entries: [
                { name: `${name}2`, type: 'image/jpeg' },
                { name: 'directory3', entries: [] },
                {
                  name: 'directory4',
                  entries: [
                    { name: `${name}3`, type: 'image/jpeg' },
                    { name: `${name}4`, type: 'image/jpeg' }
                  ]
                }
              ]
            }
          ]
        }
      ])
      

C3PablO avatar Sep 21 '21 09:09 C3PablO

Hey @C3PablO , This is great, but it would be so awesome if we could extract your mocks into this plugin in some way. I've setup an example and gotten it working with uppy at https://github.com/abramenal/cypress-file-upload/pull/329, however I would really appreciate help in trying to make it possible to select a folder instead or in addition to files in this case.

anark avatar Oct 20 '21 21:10 anark