Jsdom: Expose some method add files to a FileList

Created on 23 Oct 2015  ·  30Comments  ·  Source: jsdom/jsdom

FileList's are not writeable in the specs, but to make input.files testable, we'll need to implement a util method to modify it.

/cc @cpojer

feature

Most helpful comment

I managed to create FileList without having to alter any of the jsdom library code:

const createFile = (size = 44320, name = 'ecp-logo.png', type = 'image/png') =>
  new File([new ArrayBuffer(size)], name , {
    type: type,
  });

const createFileList = (file) => {
  const fileList = new FileList();
  fileList[0] = file;
  return fileList;
}

const fileList = createFileList(createFile());

In this case I'm creating FileList object calling constructor on FileList provided by jsdom. After that I'm just adding file to that list using Array notation. In my case I only need 1 File in array but this can also be changed to for loop to add multiple files to FileList. I'm creating file with custom name/type/size through File constructor also provided by jsdom which functionality is following specification.

This might be helpful for someone who is looking how to mock FileList in jsdom environment, but one issue still stands. Creating FileList with array of Files using this method wouldn't allow getting items from array using FileList.item(index) method. But, even that can be fixed by overriding it's method something like this:

const createFileList = (file) => {
  const fileList = new FileList();
  fileList[0] = file;
  fileList.item = index => fileList[index]; // override method functionality
  return fileList;
}

I still feel it might be better if jsdom could offer these functionalities for testing purposes out of the box.

All 30 comments

input.files = createFileList(file1, file2, ...) would be nice.

@cpojer Are you generating actual File objects to put in there?

Making input.files writeable is probably bad, we can probably either return the raw array to fill yourself, or do something like fillFileList(input.files, [file]).

We actually only use mock data, so we populate .files with an array of objects. But requiring it to be a File object would be reasonable I think.

It seems like defineProperty would be fine here? DOM properties are reconfigurable for a reason...

I'd much rather create a real FileList with real data, though.

It should also be possible to access FileList items using their array-indices. .item is not the only way to access their fields.

OK, so it sounds like there are a few potential issues:

  • FileList doesn't properly have indexed access working (bug)
  • There's no way to create FileList objects for testing (web platform feature gap).

But modifying inputEl.files is not the problem.

Yeah, I mean it isn't awesome to tell engineers to use Object.defineProperty over a regular assignment but I can live with it.

Well, they have to do that anyway in a real browser, so it seems reasonable to me...

Hi, I see this is over a year old, I would like to ask if there's any progress done concerning this issue? I use Jest framework to test my React/Redux app which internally uses jsdom. I have an issue where I need to dynamically create FileList with one or more File objects.

Looking at lib/jsdom/living/filelist.js I can see there is a constructor for FileList but there isn't any option to pass files to it. I do understand that FileList and File don't have constructor according to specifications due to security reasons but is there any intention to allow constructor to accept array of File objects or at least additional method (let's say _setItem_) that would allow us to add File objects into list specifically for testing purposes?

I also see one other issue with FileList. If I'm not mistaking it should be Array-like object, same as NodeList (lib/jsdom/living/node-list.js) meaning there should be a possibility to access File objects in two ways:

var fileList = document.getElementById("myfileinput").files;

fileList[0];
fileList.item(0);

Currently, it is only possible to access through method. This means same logic as in NodeList should be applied here:

FileList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];

and files should be stored something like this if we would allow passing array of Files to constructor:

for (let i = 0; i < files.length; ++i) {
  this[i] = files[i];
}

I don't have any strong opinions about how this should be done, these are just examples that I use to explain better what I'm trying to point out.

Extra questions (I don't want to open unnecessary issues before asking here):

1.) Would it be beneficial to add File constructor for testing purposes that creates File object from an actual file using path provided as string? I found library (I'm sorry, I can't find link anymore) that allowed this:

const file = new File('../fixtures/files/test-image.png');

This created File for me with properties (size, lastModified, type...) without me having to create it manually:

const file = new File([''], 'test-image.png', {
  lastModified: 1449505890000,
  lastModifiedDate: new Date(1449505890000),
  name: "ecp-logo.png",
  size: 44320,
  type: "image/png",
});

I don't know how this library worked all I know is that it was not maintained for over a year and we stopped using it. I can't seem to find it anymore.

2.) window.URL.createObjectURL isn't supported by jsdom. Not sure if it should be reported.

I managed to create FileList without having to alter any of the jsdom library code:

const createFile = (size = 44320, name = 'ecp-logo.png', type = 'image/png') =>
  new File([new ArrayBuffer(size)], name , {
    type: type,
  });

const createFileList = (file) => {
  const fileList = new FileList();
  fileList[0] = file;
  return fileList;
}

const fileList = createFileList(createFile());

In this case I'm creating FileList object calling constructor on FileList provided by jsdom. After that I'm just adding file to that list using Array notation. In my case I only need 1 File in array but this can also be changed to for loop to add multiple files to FileList. I'm creating file with custom name/type/size through File constructor also provided by jsdom which functionality is following specification.

This might be helpful for someone who is looking how to mock FileList in jsdom environment, but one issue still stands. Creating FileList with array of Files using this method wouldn't allow getting items from array using FileList.item(index) method. But, even that can be fixed by overriding it's method something like this:

const createFileList = (file) => {
  const fileList = new FileList();
  fileList[0] = file;
  fileList.item = index => fileList[index]; // override method functionality
  return fileList;
}

I still feel it might be better if jsdom could offer these functionalities for testing purposes out of the box.

Hi Guys ,
I am facing some issue for while mocking $("#selectorID of Upload file")[0].files[0] object which is returning FileList.
Can someone help me to create FileList Object? Because I can't find any reference link anywhere in WWW

It's not currently possible.

Thanks Domenic,

I need to write a test case for File upload change event. any alternate way to execute that test scenario .

Any progress on this?

@niksajanjic Did you manage to implement the the best solution presented in this thread?
Ref: const file = new File('../fixtures/files/test-image.png');
Or something similar to it?
Best

@domenic Seems to be a bit of dissent in that twitter post on the issue

@domenic Ok, I've set up the basic start to this. Not all the checks are there but it is essentially what @niksajanjic was talking about.

createFile

function createFile(file_path) {
  const { mtimeMs: lastModified, size } = fs.statSync(file_path)

  return new File(
    [new fs.readFileSync(file_path)],
    path.basename(file_path),
    {
      lastModified,
      type: mime.lookup(file_path) || '',
    }
  )
}

addFileList

function addFileList(input, file_paths) {
  if (typeof file_paths === 'string')
    file_paths = [file_paths]
  else if (!Array.isArray(file_paths)) {
    throw new Error('file_paths needs to be a file path string or an Array of file path strings')
  }

  const file_list = file_paths.map(fp => createFile(fp))
  file_list.__proto__ = Object.create(FileList.prototype)

  Object.defineProperty(input, 'files', {
    value: file_list,
    writeable: false,
  })

  return input
}

Demo File

/*eslint-disable no-console, no-unused-vars */

/*
https://github.com/jsdom/jsdom/issues/1272
*/

const fs = require('fs')
const path = require('path')
const mime = require('mime-types')

const { JSDOM } = require('jsdom')
const dom = new JSDOM(`
<!DOCTYPE html>
<body>
  <input type="file">
</body>
`)

const { window } = dom
const { document, File, FileList } = window


const file_paths = [
  '/Users/williamrusnack/Documents/form_database/test/try-input-file.html',
  '/Users/williamrusnack/Documents/form_database/test/try-jsdom-input-file.js',
]

function createFile(file_path) {
  const { mtimeMs: lastModified, size } = fs.statSync(file_path)

  return new File(
    [new fs.readFileSync(file_path)],
    path.basename(file_path),
    {
      lastModified,
      type: mime.lookup(file_path) || '',
    }
  )
}

function addFileList(input, file_paths) {
  if (typeof file_paths === 'string')
    file_paths = [file_paths]
  else if (!Array.isArray(file_paths)) {
    throw new Error('file_paths needs to be a file path string or an Array of file path strings')
  }

  const file_list = file_paths.map(fp => createFile(fp))
  file_list.__proto__ = Object.create(FileList.prototype)

  Object.defineProperty(input, 'files', {
    value: file_list,
    writeable: false,
  })

  return input
}



const input = document.querySelector('input')

addFileList(input, file_paths)

for (let i = 0; i < input.files.length; ++i) {
  const file = input.files[i]
  console.log('file', file)
  console.log('file.name', file.name)
  console.log('file.size', file.size)
  console.log('file.type', file.type)
  console.log('file.lastModified', file.lastModified)
  console.log()
}

@BebeSparkelSparkel I made the test in a way I explained in my later post up there. Unfortunately, a few months later it stopped working on newer versions of jsdom when one of my colleagues tried to copy that code. So, we had to comment out the tests for FileList at that point. From there on, we couldn't find a way to write those tests and in last few months nobody had the time to take another look at it and try to find a way.

@niksajanjic Thanks for your update. I'm working on the solution that you proposed and it seems to be working (see above code), but I think it unlikely to be added to jsdom since it would be very hard to figure out how to add it.
Feel free to take a look and use it if you want though

Created a simple helper script that solves this issue until the jsdom project comes up with a solution.
https://bitbucket.org/william_rusnack/addfilelist/src/master/

Add one file:

const input = document.querySelector('input[type=file]')
addFileList(input, 'path/to/file')

Add multiple files:

const input = document.querySelector('input[type=file]')
addFileList(input, [
  'path/to/file',
  'path/to/another/file',
  // add as many as you want
])

Install and Require

npm install https://github.com/BebeSparkelSparkel/addFileList.git

```javascript
const { addFileList } = require('addFileList')

## Functions
**addFileList**(input, file_paths)  
Effects: puts the file_paths as File object into input.files as a FileList  
Returns: input  
Arguments:  
- input: HTML input element  
- file_paths: String or Array of string file paths to put in input.files  
`const { addFileList } = require('addFileList')`  

## Example
Extract from example.js
```javascript
// add a single file
addFileList(input, 'example.js')

// log input's FileList
console.log(input.files)

// log file properties
const [ file ] = input.files
console.log(file)
console.log(
  '\nlastModified', file.lastModified,
  '\nname', file.name,
  '\nsize', file.size,
  '\ntype', file.type,
  '\n'
)

Result

$ node example.js 
FileList [ File {} ]
File {}

lastModified 1518523506000 
name example.js 
size 647 
type application/javascript 

@BebeSparkelSparkel your repo seems to have been deleted?

I had a similar problem— I wrote a function that took in a FileList as input and wanted to write a unit test for it using jest. I was able to use the below helper function to spoof a FileList object enough to work with my function. Syntax is ES6 with Flow annotations. No promises that it will work in all situations as its just faking the functionality of the real FileList class…

const createFileList = (files: Array<File>): FileList => {
  return {
    length: files.length,
    item: (index: number) => files[index],
    * [Symbol.iterator]() {
      for (let i = 0; i < files.length; i++) {
        yield files[i];
      }
    },
    ...files,
  };
};

In the frontend, I can do something like the following to (in a round about way) construct a 'real' FileList:

export const makeFileList = files => {
  const reducer = (dataTransfer, file) => {
    dataTransfer.items.add(file)
    return dataTransfer
  }

  return files.reduce(reducer, new DataTransfer()).files
}

Ref: https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer

Unfortunately, jsdom doesn't seem to actually support DataTransfer yet, so this doesn't work in my tests:

Other refs:


Searching the source a little i found exports.FileList = require("./generated/FileList").interface;

~But it wasn't clear to me from GitHub where to find the source that ends up building ./generated~

The main export of the npm package is ./lib/api.js and exports a very small public API:

exports.JSDOM = JSDOM;
exports.VirtualConsole = VirtualConsole;
exports.CookieJar = CookieJar;
exports.ResourceLoader = ResourceLoader;
exports.toughCookie = toughCookie;

But looking in my ./node_modules/jsdom/lib/jsdom directory.. i can see that all of the internal/implementation files are there as well, including ./node_modules/jsdom/lib/jsdom/living/file-api:

Blob-impl.js  File-impl.js  FileList-impl.js  FileReader-impl.js

FileList-impl.js here contains the actual JS implementation that backs the exposed FileList api in jsdom.

Now if we look at ./node_modules/jsdom/lib/jsdom/living/generated/FileList.js we see the actual generated 'public API' that we end up seeing through normal usage, including our all too familiar:

class FileList {
  constructor() {
    throw new TypeError("Illegal constructor");
  }

This file exports module.exports = iface;, which contains a lot more functionality than what we end up with through the 'normal' public API exposure, which only uses the iface.interface key. So perhaps we could do something fun if we use require("./generated/FileList") directly. Removing the implementation details, we have an interface that looks something like:

const iface = {
  _mixedIntoPredicates: [],
  is(obj) {..snip..},
  isImpl(obj) {..snip..},
  convert(obj, { context = "The provided value" } = {}) {..snip..},
  create(constructorArgs, privateData) {..snip..},
  createImpl(constructorArgs, privateData) {..snip..},
  _internalSetup(obj) {},
  setup(obj, constructorArgs, privateData) {...snip...},
  interface: FileList,
  expose: {
    Window: { FileList },
    Worker: { FileList }
  }
}; // iface

So now that we know that there is more power to be got.. let's see how other areas of jsdom access it..

Taking a look at HTMLInputElement-impl, it seems to use FileList.createImpl(), though unfortunately it doesn't actually show us how to use the params:

createImpl is just a tiny wrapper around the setup in the exported iface:

createImpl(constructorArgs, privateData) {
    let obj = Object.create(FileList.prototype);
    obj = this.setup(obj, constructorArgs, privateData);
    return utils.implForWrapper(obj);
  },

Playing around with this in a console it seems we have the expressive api of the Array element the FileListImpl is backed by. So we can do things like:

var flist = require('./node_modules/jsdom/lib/jsdom/living/generated/FileList.js')
var myFileListImpl = flist.createImpl()
myFileListImpl.push('aa')

It has a Symbol(wrapper) property on it, which we will need to use ./node_modules/jsdom/lib/jsdom/living/generated/utils.js:37 to access:

var utils = require('./node_modules/jsdom/lib/jsdom/living/generated/utils.js')
var wrapper = myFileListImpl[utils.wrapperSymbol]

The exported iface has a function convert, which will throw new TypeError(${context} is not of type 'FileList'.); when the provided object isn't a FileList. We can use this to test things.

If we call it on the raw myFileListImpl it throws the error:

flist.convert(myFileListImpl)

Whereas using the wrapper we extracted above, we can see that it doesn't throw the error:

flist.convert(myFileListImpl[utils.wrapperSymbol])

With this, we can modify myFileListImpl, and get an acceptable FileList object back, to pass where we need it. A fully worked example (using util.wrapperForImpl() instead of our previous code):

var _FileList = require('./node_modules/jsdom/lib/jsdom/living/generated/FileList.js')
var utils = require('./node_modules/jsdom/lib/jsdom/living/generated/utils.js')

var myMutableFileListImpl = _FileList.createImpl()

myMutableFileListImpl.length // 0
myMutableFileListImpl.push(new File([], 'a.jpg'))
myMutableFileListImpl.length // 1

var myFileList = utils.wrapperForImpl(myMutableFileListImpl)
_FileList.convert(myFileList) // no error

myFileList.length // 1
myFileList[0] // the File{} object

Now with that knowledge, I can implement the jsdom testing version of my original browser workaround hack (implemented on top of ImmutableJS for laziness):

import { Map, Record } from 'immutable'

import jsdomFileList from 'jsdom/lib/jsdom/living/generated/FileList'
import { wrapperForImpl } from 'jsdom/lib/jsdom/living/generated/utils'

// Note: relying on internal API's is super hacky, and will probably break
// As soon as we can, we should use whatever the proper outcome from this issue is:
//   https://github.com/jsdom/jsdom/issues/1272#issuecomment-486088445

export const makeFileList = files => {
  const reducer = (fileListImpl, file) => {
    fileListImpl.push(file)
    return fileListImpl
  }

  const fileListImpl = files.reduce(reducer, jsdomFileList.createImpl())

  return wrapperForImpl(fileListImpl)
}

export class DataTransferStub extends Record({ items: Map() }) {
  get files() {
    return makeFileList(this.items.toList().toArray())
  }
}

export const stubGlobalDataTransfer = () => {
  global.DataTransfer = DataTransferStub
}

export const restoreGlobalDataTransfer = () => {
  global.DataTransfer = undefined
}

And then manually wire it into my global vars so that my ava tests can use it:

import {
  restoreGlobalDataTransfer,
  stubGlobalDataTransfer,
} from ../helpers/jsdom-helpers'

test.before(t => {
  stubGlobalDataTransfer()
})

test.after(t => {
  restoreGlobalDataTransfer()
})

All modern browsers (i.e. not IE <= 11) now support setting input.files to a FileList https://stackoverflow.com/a/47522812/2744776

This seems to have changed in a recent release. Here's what worked for me:

const jsdomUtils = require('jsdom/lib/jsdom/living/generated/utils');
const jsdomFileList = require('jsdom/lib/jsdom/living/generated/FileList');
function makeFileList(...files) {
    const impl = jsdomFileList.createImpl(window);
    const ret = Object.assign([...files], {
        item: (ix) => ret[ix],
        [jsdomUtils.implSymbol]: impl,
    });
    impl[jsdomUtils.wrapperSymbol] = ret;
    Object.setPrototypeOf(ret, FileList.prototype);
    return ret;
}

If you're using JSDOM via Jest, then you have to make sure to require the internals outside of the testing VM. I created a custom test env like this:

const JsdomEnvironment = require('jest-environment-jsdom');

/** See jsdom/jsdom#1272 */
class EnvWithSyntheticFileList extends JsdomEnvironment {
    async setup() {
        await super.setup();
        this.global.jsdomUtils = require('jsdom/lib/jsdom/living/generated/utils');
        this.global.jsdomFileList = require('jsdom/lib/jsdom/living/generated/FileList');
    }
}

module.exports = EnvWithSyntheticFileList;

So that I could access the 'outer' imports.

This seems to have changed in a recent release. Here's what worked for me:

const jsdomUtils = require('jsdom/lib/jsdom/living/generated/utils');
const jsdomFileList = require('jsdom/lib/jsdom/living/generated/FileList');
function makeFileList(...files) {
    const impl = jsdomFileList.createImpl(window);
    const ret = Object.assign([...files], {
        item: (ix) => ret[ix],
        [jsdomUtils.implSymbol]: impl,
    });
    impl[jsdomUtils.wrapperSymbol] = ret;
    Object.setPrototypeOf(ret, FileList.prototype);
    return ret;
}

If you're using JSDOM via Jest, then you have to make sure to require the internals outside of the testing VM. I created a custom test env like this:

const JsdomEnvironment = require('jest-environment-jsdom');

/** See jsdom/jsdom#1272 */
class EnvWithSyntheticFileList extends JsdomEnvironment {
    async setup() {
        await super.setup();
        this.global.jsdomUtils = require('jsdom/lib/jsdom/living/generated/utils');
        this.global.jsdomFileList = require('jsdom/lib/jsdom/living/generated/FileList');
    }
}

module.exports = EnvWithSyntheticFileList;

So that I could access the 'outer' imports.

This got me close but I cant get past the internal validation:

exports.is = value => {
return utils.isObject(value) && utils.hasOwn(value, implSymbol) && value[implSymbol] instanceof Impl.implementation;
};

TypeError: Failed to set the 'files' property on 'HTMLInputElement': The provided value is not of type 'FileList'.

Spent hours. Super frustrating

This seems to have changed in a recent release. Here's what worked for me:

const jsdomUtils = require('jsdom/lib/jsdom/living/generated/utils');
const jsdomFileList = require('jsdom/lib/jsdom/living/generated/FileList');
function makeFileList(...files) {
    const impl = jsdomFileList.createImpl(window);
    const ret = Object.assign([...files], {
        item: (ix) => ret[ix],
        [jsdomUtils.implSymbol]: impl,
    });
    impl[jsdomUtils.wrapperSymbol] = ret;
    Object.setPrototypeOf(ret, FileList.prototype);
    return ret;
}

If you're using JSDOM via Jest, then you have to make sure to require the internals outside of the testing VM. I created a custom test env like this:

const JsdomEnvironment = require('jest-environment-jsdom');

/** See jsdom/jsdom#1272 */
class EnvWithSyntheticFileList extends JsdomEnvironment {
    async setup() {
        await super.setup();
        this.global.jsdomUtils = require('jsdom/lib/jsdom/living/generated/utils');
        this.global.jsdomFileList = require('jsdom/lib/jsdom/living/generated/FileList');
    }
}

module.exports = EnvWithSyntheticFileList;

So that I could access the 'outer' imports.

This got me close but I cant get past the internal validation:

exports.is = value => {
return utils.isObject(value) && utils.hasOwn(value, implSymbol) && value[implSymbol] instanceof Impl.implementation;
};

TypeError: Failed to set the 'files' property on 'HTMLInputElement': The provided value is not of type 'FileList'.

Spent hours. Super frustrating

After starting again with a fresh brain I found the issue. Somehow I had 2 separate jsdom environments running which threw off the references to symbols.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

lehni picture lehni  ·  4Comments

khalyomede picture khalyomede  ·  3Comments

camelaissani picture camelaissani  ·  4Comments

mitar picture mitar  ·  4Comments

potapovDim picture potapovDim  ·  4Comments