Jsdom: 公开一些方法将文件添加到 FileList

创建于 2015-10-23  ·  30评论  ·  资料来源: jsdom/jsdom

FileList 在规范中是不可写的,但为了使input.files可测试,我们需要实现一个 util 方法来修改它。

/cc @cpojer

feature

最有用的评论

我设法创建 FileList 而无需更改任何 jsdom 库代码:

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());

在这种情况下,我在 jsdom 提供的 FileList 上创建 FileList 对象调用构造函数。 之后,我只是使用数组表示法将文件添加到该列表中。 在我的情况下,我只需要数组中的 1 个文件,但这也可以更改为 for 循环以将多个文件添加到 FileList。 我正在通过 jsdom 提供的文件构造函数创建具有自定义名称/类型/大小的文件,其功能遵循规范。

这对于正在寻找如何在 jsdom 环境中模拟 FileList 的人可能会有所帮助,但一个问题仍然存在。 使用此方法创建带有文件数组的 FileList 不允许使用 FileList.item(index) 方法从数组中获取项目。 但是,即使这样也可以通过覆盖它的方法来解决这个问题:

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

我仍然觉得如果 jsdom 可以提供这些功能用于开箱即用的测试目的可能会更好。

所有30条评论

input.files = createFileList(file1, file2, ...)会很好。

@cpojer您是否正在生成实际的File对象以放入其中?

使input.files可写可能很糟糕,我们可能可以返回原始数组来填充自己,或者执行类似fillFileList(input.files, [file])

我们实际上只使用模拟数据,所以我们用一个对象数组填充.files 。 但我认为要求它是一个 File 对象是合理的。

在这里defineProperty似乎没问题? DOM 属性可重新配置是有原因的...

不过,我更愿意用真实数据创建一个真实的 FileList。

还应该可以使用它们的数组索引访问 FileList 项目。 .item不是访问其字段的唯一方法。

好的,听起来有一些潜在的问题:

  • FileList 没有正确地进行索引访问(错误)
  • 无法创建用于测试的 FileList 对象(网络平台功能差距)。

但是修改 inputEl.files 不是问题。

是的,我的意思是告诉工程师在常规任务中使用 Object.defineProperty 并不是很好,但我可以接受它。

好吧,无论如何他们都必须在真正的浏览器中这样做,所以对我来说这似乎是合理的......

你好,我看到这已经一年多了,我想问一下这个问题是否有任何进展? 我使用 Jest 框架来测试我在内部使用 jsdom 的 React/Redux 应用程序。 我有一个问题,我需要使用一个或多个 File 对象动态创建 FileList。

查看 lib/jsdom/living/filelist.js 我可以看到 FileList 有一个构造函数,但没有任何选项可以将文件传递给它。 我确实理解 FileList 和 File 由于安全原因没有符合规范的构造函数,但是是否有任何意图允许构造函数接受 File 对象数组或至少允许我们添加 File 的附加方法(比如 _setItem_)对象进入列表专门用于测试目的?

我还看到 FileList 的另一个问题。 如果我没记错的话,它应该是类数组对象,与 NodeList (lib/jsdom/living/node-list.js) 相同,这意味着应该可以通过两种方式访问​​ File 对象:

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

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

目前只能通过方法访问。 这意味着应在此处应用与 NodeList 中相同的逻辑:

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

如果我们允许将文件数组传递给构造函数,则文件应该像这样存储:

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

我对应该如何做没有任何强烈的意见,这些只是我用来更好地解释我想要指出的内容的例子。

额外的问题(我不想在这里问之前打开不必要的问题):

1.) 为测试目的添加 File 构造函数以使用作为字符串提供的路径从实际文件创建 File 对象是否有益? 我找到了允许这样做的图书馆(对不起,我再也找不到链接了):

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

这为我创建了具有属性(大小、lastModified、类型...)的文件,而我无需手动创建它:

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

我不知道这个库是如何工作的,我只知道它一年多没有维护,我们停止使用它。 我好像再也找不到了。

2.) jsdom 不支持 window.URL.createObjectURL。 不知道该不该报。

我设法创建 FileList 而无需更改任何 jsdom 库代码:

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());

在这种情况下,我在 jsdom 提供的 FileList 上创建 FileList 对象调用构造函数。 之后,我只是使用数组表示法将文件添加到该列表中。 在我的情况下,我只需要数组中的 1 个文件,但这也可以更改为 for 循环以将多个文件添加到 FileList。 我正在通过 jsdom 提供的文件构造函数创建具有自定义名称/类型/大小的文件,其功能遵循规范。

这对于正在寻找如何在 jsdom 环境中模拟 FileList 的人可能会有所帮助,但一个问题仍然存在。 使用此方法创建带有文件数组的 FileList 不允许使用 FileList.item(index) 方法从数组中获取项目。 但是,即使这样也可以通过覆盖它的方法来解决这个问题:

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

我仍然觉得如果 jsdom 可以提供这些功能用于开箱即用的测试目的可能会更好。

嗨,大家好 ,
我在模拟 $("#selectorID of Upload file")[0].files[0] 返回 FileList 的对象时遇到了一些问题。
有人可以帮我创建 FileList 对象吗? 因为我在 WWW 的任何地方都找不到任何参考链接

目前是不可能的。

谢谢多梅尼克,

我需要为文件上传更改事件编写一个测试用例。 执行该测试场景的任何替代方法。

这方面有什么进展吗?

@niksajanjic您是否设法实施了此线程中提供的最佳解决方案?
参考: const file = new File('../fixtures/files/test-image.png');
或者类似的东西?
最好的事物

@domenic在那个关于这个问题的推特帖子中似乎有点异议

@domenic好的,我已经为此设置了基本的开始。 并非所有检查都在那里,但这基本上就是@niksajanjic所说的。

创建文件

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
}

演示文件

/*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我以我在后来的帖子中解释的方式进行了测试。 不幸的是,几个月后,当我的一位同事试图复制该代码时,它停止在较新版本的 jsdom 上工作。 因此,我们必须在那时注释掉 FileList 的测试。 从那时起,我们找不到编写这些测试的方法,而且在过去的几个月里,没有人有时间再看一遍并尝试找到方法。

@niksajanjic感谢您的更新。 我正在研究您提出的解决方案,它似乎有效(见上面的代码),但我认为它不太可能添加到 jsdom 中,因为很难弄清楚如何添加它。
如果您愿意,请随意查看并使用它

创建了一个简单的帮助脚本来解决这个问题,直到 jsdom 项目提出解决方案。
https://bitbucket.org/william_rusnack/addfilelist/src/master/

添加一个文件:

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

添加多个文件:

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

安装和要求

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'
)

结果

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

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

@BebeSparkelSparkel您的

我有一个类似的问题——我写了一个函数,它接受FileList作为输入,并想使用 jest 为其编写单元测试。 我能够使用下面的辅助函数来欺骗FileList对象,足以使用我的函数。 语法是带有 Flow 注释的 ES6。 没有承诺它会在所有情况下都有效,因为它只是伪造了真正的FileList类的功能......

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,
  };
};

在前端,我可以执行以下操作(以某种方式)构造一个“真实的” FileList

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

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

参考: https :

不幸的是, jsdom似乎实际上并不支持DataTransfer ,所以这在我的测试中不起作用:

其他参考:


稍微搜索一下来源,我发现exports.FileList = require("./generated/FileList").interface;

~但是我从 GitHub 上不清楚在哪里可以找到最终构建./generated的源代码 ~

npm 包的主要导出./lib/api.js并导出一个非常小的公共 API:

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

但是查看我的./node_modules/jsdom/lib/jsdom目录......我可以看到所有内部/实现文件都在那里,包括./node_modules/jsdom/lib/jsdom/living/file-api

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

FileList-impl.js此处包含支持FileList api 的实际 JS 实现。

现在,如果我们查看./node_modules/jsdom/lib/jsdom/living/generated/FileList.js我们会看到实际生成的“公共 API”,我们最终通过正常使用看到了它,包括我们非常熟悉的:

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

此文件导出module.exports = iface; ,它包含的功能比我们最终通过“普通”公开 API 公开的功能多得多,后者仅使用iface.interface密钥。 因此,如果我们直接使用require("./generated/FileList")也许我们可以做一些有趣的事情。 去掉实现细节,我们有一个看起来像这样的界面:

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

所以现在我们知道有更多的权力可以得到..让我们看看jsdom其他区域如何访问它..

看看HTMLInputElement-impl ,它似乎使用FileList.createImpl() ,但不幸的是它实际上并没有向我们展示如何使用参数:

createImpl只是导出的iface setup一个小包装:

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

在控制台中玩这个,似乎我们拥有FileListImpl支持的Array元素的富有表现力的 api。 所以我们可以做这样的事情:

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

它有一个Symbol(wrapper)属性,我们需要使用./node_modules/jsdom/lib/jsdom/living/generated/utils.js:37来访问:

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

导出的iface有一个函数convert ,它将throw new TypeError( ${context} 不是“FileList”类型。 );当提供的对象不是FileList 。 我们可以用它来测试事物。

如果我们在原始myFileListImpl上调用它,它会抛出错误:

flist.convert(myFileListImpl)

而使用我们上面提取的wrapper ,我们可以看到它不会抛出错误:

flist.convert(myFileListImpl[utils.wrapperSymbol])

有了这个,我们可以修改myFileListImpl ,并得到一个可接受的FileList对象,将它传递到我们需要的地方。 一个完整的示例(使用util.wrapperForImpl()而不是我们之前的代码):

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

现在有了这些知识,我可以实现我的原始浏览器解决方法 hack 的 jsdom 测试版本(在ImmutableJS之上实现):

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
}

然后手动将它连接到我的全局变量中,以便我的ava测试可以使用它:

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

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

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

所有现代浏览器(即不是 IE <= 11)现在都支持将 input.files 设置为 FileList https://stackoverflow.com/a/47522812/2744776

这似乎在最近的版本中发生了变化。 这是对我有用的:

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;
}

如果您通过 Jest 使用 JSDOM,那么您必须确保需要测试 VM 之外的内部组件。 我创建了一个像这样的自定义测试环境:

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;

这样我就可以访问“外部”进口。

这似乎在最近的版本中发生了变化。 这是对我有用的:

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;
}

如果您通过 Jest 使用 JSDOM,那么您必须确保需要测试 VM 之外的内部组件。 我创建了一个像这样的自定义测试环境:

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;

这样我就可以访问“外部”进口。

这让我很接近,但我无法通过内部验证:

export.is = 值 => {
返回 utils.isObject(value) && utils.hasOwn(value, implSymbol) && value[implSymbol] instanceof Impl.implementation;
};

类型错误:无法在“HTMLInputElement”上设置“files”属性:提供的值不是“FileList”类型。

花了几个小时。 超级郁闷

这似乎在最近的版本中发生了变化。 这是对我有用的:

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;
}

如果您通过 Jest 使用 JSDOM,那么您必须确保需要测试 VM 之外的内部组件。 我创建了一个像这样的自定义测试环境:

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;

这样我就可以访问“外部”进口。

这让我很接近,但我无法通过内部验证:

export.is = 值 => {
返回 utils.isObject(value) && utils.hasOwn(value, implSymbol) && value[implSymbol] instanceof Impl.implementation;
};

类型错误:无法在“HTMLInputElement”上设置“files”属性:提供的值不是“FileList”类型。

花了几个小时。 超级郁闷

用新鲜的大脑重新开始后,我发现了这个问题。 不知何故,我运行了 2 个单独的 jsdom 环境,它们丢弃了对符号的引用。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

mitar picture mitar  ·  4评论

kilianc picture kilianc  ·  4评论

jhegedus42 picture jhegedus42  ·  4评论

philipwalton picture philipwalton  ·  4评论

josephrexme picture josephrexme  ·  4评论