FileList 在规范中是不可写的,但为了使input.files
可测试,我们需要实现一个 util 方法来修改它。
/cc @cpojer
input.files = createFileList(file1, file2, ...)
会很好。
@cpojer您是否正在生成实际的File
对象以放入其中?
使input.files
可写可能很糟糕,我们可能可以返回原始数组来填充自己,或者执行类似fillFileList(input.files, [file])
。
我们实际上只使用模拟数据,所以我们用一个对象数组填充.files
。 但我认为要求它是一个 File 对象是合理的。
在这里defineProperty似乎没问题? DOM 属性可重新配置是有原因的...
不过,我更愿意用真实数据创建一个真实的 FileList。
还应该可以使用它们的数组索引访问 FileList 项目。 .item
不是访问其字段的唯一方法。
好的,听起来有一些潜在的问题:
但是修改 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 环境,它们丢弃了对符号的引用。
最有用的评论
我设法创建 FileList 而无需更改任何 jsdom 库代码:
在这种情况下,我在 jsdom 提供的 FileList 上创建 FileList 对象调用构造函数。 之后,我只是使用数组表示法将文件添加到该列表中。 在我的情况下,我只需要数组中的 1 个文件,但这也可以更改为 for 循环以将多个文件添加到 FileList。 我正在通过 jsdom 提供的文件构造函数创建具有自定义名称/类型/大小的文件,其功能遵循规范。
这对于正在寻找如何在 jsdom 环境中模拟 FileList 的人可能会有所帮助,但一个问题仍然存在。 使用此方法创建带有文件数组的 FileList 不允许使用 FileList.item(index) 方法从数组中获取项目。 但是,即使这样也可以通过覆盖它的方法来解决这个问题:
我仍然觉得如果 jsdom 可以提供这些功能用于开箱即用的测试目的可能会更好。