FileList no se puede escribir en las especificaciones, pero para hacer que input.files
comprobable, necesitaremos implementar un método útil para modificarlo.
/ cc @cpojer
input.files = createFileList(file1, file2, ...)
estaría bien.
@cpojer ¿Está generando objetos File
reales para ponerlos allí?
Hacer que input.files
se pueda escribir es probablemente malo, probablemente podamos devolver la matriz sin procesar para completarlo usted mismo o hacer algo como fillFileList(input.files, [file])
.
En realidad, solo usamos datos simulados, por lo que completamos .files
con una matriz de objetos. Pero creo que requerir que sea un objeto File sería razonable.
¿Parece que defineProperty estaría bien aquí? Las propiedades DOM son reconfigurables por una razón ...
Sin embargo, prefiero crear una FileList real con datos reales.
También debería ser posible acceder a los elementos de FileList utilizando sus índices de matriz. .item
no es la única forma de acceder a sus campos.
Bien, parece que hay algunos problemas potenciales:
Pero modificar inputEl.files no es el problema.
Sí, quiero decir que no es genial decirle a los ingenieros que usen Object.defineProperty en una asignación regular, pero puedo vivir con eso.
Bueno, tienen que hacer eso de todos modos en un navegador real, así que me parece razonable ...
Hola, veo que esto tiene más de un año, me gustaría preguntar si hay algún progreso con respecto a este problema. Utilizo el marco Jest para probar mi aplicación React / Redux que internamente usa jsdom. Tengo un problema en el que necesito crear FileList dinámicamente con uno o más objetos File.
Mirando lib / jsdom / living / filelist.js puedo ver que hay un constructor para FileList pero no hay ninguna opción para pasarle archivos. Entiendo que FileList y File no tienen constructor de acuerdo con las especificaciones debido a razones de seguridad, pero ¿hay alguna intención de permitir que el constructor acepte una matriz de objetos File o al menos un método adicional (digamos _setItem_) que nos permita agregar File objetos en la lista específicamente con fines de prueba?
También veo otro problema con FileList. Si no me equivoco, debería ser un objeto tipo Array, igual que NodeList (lib / jsdom / living / node-list.js), lo que significa que debería existir la posibilidad de acceder a los objetos File de dos formas:
var fileList = document.getElementById("myfileinput").files;
fileList[0];
fileList.item(0);
Actualmente, solo es posible acceder a través del método. Esto significa que aquí se debe aplicar la misma lógica que en NodeList:
FileList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
y los archivos deberían almacenarse de esta manera si permitiéramos pasar una matriz de archivos al constructor:
for (let i = 0; i < files.length; ++i) {
this[i] = files[i];
}
No tengo opiniones sólidas sobre cómo se debe hacer esto, estos son solo ejemplos que utilizo para explicar mejor lo que estoy tratando de señalar.
Preguntas adicionales (no quiero abrir problemas innecesarios antes de preguntar aquí):
1.) ¿Sería beneficioso agregar el constructor de archivos para fines de prueba que crea un objeto de archivo a partir de un archivo real utilizando la ruta proporcionada como cadena? Encontré la biblioteca (lo siento, ya no puedo encontrar el enlace) que permitía esto:
const file = new File('../fixtures/files/test-image.png');
Este archivo creado para mí con propiedades (tamaño, última modificación, tipo ...) sin que tenga que crearlo manualmente:
const file = new File([''], 'test-image.png', {
lastModified: 1449505890000,
lastModifiedDate: new Date(1449505890000),
name: "ecp-logo.png",
size: 44320,
type: "image/png",
});
No sé cómo funcionó esta biblioteca, todo lo que sé es que no se mantuvo durante más de un año y dejamos de usarla. Parece que ya no puedo encontrarlo.
2.) window.URL.createObjectURL no es compatible con jsdom. No estoy seguro de si debería informarse.
Logré crear FileList sin tener que alterar ninguno de los códigos de la biblioteca 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());
En este caso, estoy creando un constructor de llamada de objeto FileList en FileList proporcionado por jsdom. Después de eso, solo estoy agregando un archivo a esa lista usando la notación Array. En mi caso, solo necesito 1 archivo en la matriz, pero esto también se puede cambiar a for loop para agregar varios archivos a FileList. Estoy creando un archivo con nombre / tipo / tamaño personalizado a través del constructor de archivos también proporcionado por jsdom, cuya funcionalidad sigue la especificación.
Esto podría ser útil para alguien que esté buscando cómo simular FileList en el entorno jsdom, pero todavía hay un problema. Crear FileList con una matriz de archivos usando este método no permitiría obtener elementos de la matriz usando el método FileList.item (index). Pero, incluso eso se puede arreglar anulando su método algo como esto:
const createFileList = (file) => {
const fileList = new FileList();
fileList[0] = file;
fileList.item = index => fileList[index]; // override method functionality
return fileList;
}
Sigo sintiendo que sería mejor si jsdom pudiera ofrecer estas funcionalidades para propósitos de prueba desde el primer momento.
Hola tios ,
Me estoy enfrentando a un problema mientras me burlo del objeto $ ("# selectorID of Upload file") [0] .files [0] que está devolviendo FileList.
¿Alguien puede ayudarme a crear un objeto FileList? Porque no puedo encontrar ningún enlace de referencia en ninguna parte de WWW
Actualmente no es posible.
Gracias Domenic,
Necesito escribir un caso de prueba para el evento de cambio de carga de archivos. cualquier forma alternativa de ejecutar ese escenario de prueba.
¿Algún progreso en esto?
@niksajanjic ¿
Ref: const file = new File('../fixtures/files/test-image.png');
¿O algo parecido?
Mejor
@domenic Parece ser un poco disidente en esa publicación de Twitter sobre el tema.
@domenic Ok, he configurado el comienzo básico para esto. No todos los controles están ahí, pero es esencialmente de lo que estaba hablando @niksajanjic .
crea un archivo
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
}
Archivo de demostración
/*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 Hice la prueba de la manera que expliqué en mi publicación posterior. Desafortunadamente, unos meses después dejó de funcionar en versiones más nuevas de jsdom cuando uno de mis colegas intentó copiar ese código. Entonces, tuvimos que comentar las pruebas para FileList en ese momento. A partir de ahí, no pudimos encontrar la forma de escribir esas pruebas y en los últimos meses nadie tuvo tiempo de echarle otro vistazo y tratar de encontrar la forma.
@niksajanjic Gracias por tu actualización. Estoy trabajando en la solución que propuso y parece estar funcionando (consulte el código anterior), pero creo que es poco probable que se agregue a jsdom ya que sería muy difícil averiguar cómo agregarlo.
Siéntase libre de echarle un vistazo y usarlo si lo desea.
Creó un script de ayuda simple que resuelve este problema hasta que el proyecto jsdom encuentre una solución.
https://bitbucket.org/william_rusnack/addfilelist/src/master/
Agregar un archivo:
const input = document.querySelector('input[type=file]')
addFileList(input, 'path/to/file')
Agregar varios archivos:
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'
)
Resultado
$ node example.js
FileList [ File {} ]
File {}
lastModified 1518523506000
name example.js
size 647
type application/javascript
@BebeSparkelSparkel parece que su repositorio ha sido eliminado?
Tuve un problema similar: escribí una función que tomaba FileList
como entrada y quería escribir una prueba unitaria para ella usando jest. Pude usar la siguiente función auxiliar para falsificar un objeto FileList
suficiente como para trabajar con mi función. La sintaxis es ES6 con anotaciones de flujo. No promete que funcionará en todas las situaciones, ya que solo está fingiendo la funcionalidad de la clase FileList
real ...
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,
};
};
En la interfaz, puedo hacer algo como lo siguiente para (de manera indirecta) construir un FileList
'real':
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
Desafortunadamente, jsdom
no parece ser compatible con DataTransfer
todavía, por lo que esto no funciona en mis pruebas:
Otras referencias:
Buscando un poco la fuente encontré exports.FileList = require("./generated/FileList").interface;
~ Pero no me quedó claro en GitHub dónde encontrar la fuente que termina construyendo ./generated
~
La exportación principal del paquete npm es ./lib/api.js
y exporta una API pública muy pequeña:
exports.JSDOM = JSDOM;
exports.VirtualConsole = VirtualConsole;
exports.CookieJar = CookieJar;
exports.ResourceLoader = ResourceLoader;
exports.toughCookie = toughCookie;
Pero mirando en mi directorio ./node_modules/jsdom/lib/jsdom
... puedo ver que todos los archivos internos / de implementación también están allí, incluidos ./node_modules/jsdom/lib/jsdom/living/file-api
:
Blob-impl.js File-impl.js FileList-impl.js FileReader-impl.js
FileList-impl.js aquí contiene la implementación de JS real que respalda la api FileList
expuesta en jsdom.
Ahora, si miramos ./node_modules/jsdom/lib/jsdom/living/generated/FileList.js
, vemos la 'API pública' generada real que terminamos viendo a través del uso normal, incluido nuestro demasiado familiar:
class FileList {
constructor() {
throw new TypeError("Illegal constructor");
}
Este archivo exporta module.exports = iface;
, que contiene mucha más funcionalidad de la que obtenemos a través de la exposición de API pública 'normal', que solo usa la clave iface.interface
. Entonces, quizás podríamos hacer algo divertido si usamos require("./generated/FileList")
directamente. Eliminando los detalles de implementación, tenemos una interfaz que se parece a:
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
Entonces, ahora que sabemos que hay más poder para obtener ... veamos cómo otras áreas de jsdom
acceden a él ...
Echando un vistazo a HTMLInputElement-impl
, parece usar FileList.createImpl()
, aunque desafortunadamente no nos muestra cómo usar los parámetros:
createImpl
es solo un pequeño envoltorio alrededor del setup
en el iface
exportado:
createImpl(constructorArgs, privateData) {
let obj = Object.create(FileList.prototype);
obj = this.setup(obj, constructorArgs, privateData);
return utils.implForWrapper(obj);
},
Jugando con esto en una consola parece que tenemos la API expresivo del Array
elemento de la FileListImpl
está respaldado por. Entonces podemos hacer cosas como:
var flist = require('./node_modules/jsdom/lib/jsdom/living/generated/FileList.js')
var myFileListImpl = flist.createImpl()
myFileListImpl.push('aa')
Tiene una propiedad Symbol(wrapper)
, a la que necesitaremos usar ./node_modules/jsdom/lib/jsdom/living/generated/utils.js:37
para acceder:
var utils = require('./node_modules/jsdom/lib/jsdom/living/generated/utils.js')
var wrapper = myFileListImpl[utils.wrapperSymbol]
El iface
exportado tiene una función convert
, que throw new TypeError(
$ {context} no es del tipo 'FileList'. );
cuando el objeto proporcionado no es FileList
. Podemos usar esto para probar cosas.
Si lo llamamos en el crudo myFileListImpl
arroja el error:
flist.convert(myFileListImpl)
Mientras que al usar wrapper
que extrajimos anteriormente, podemos ver que no arroja el error:
flist.convert(myFileListImpl[utils.wrapperSymbol])
Con esto, podemos modificar myFileListImpl
, y recuperar un objeto FileList
aceptable, para pasarlo a donde lo necesitemos. Un ejemplo completamente trabajado (usando util.wrapperForImpl()
lugar de nuestro código anterior):
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
Ahora, con ese conocimiento, puedo implementar la versión de prueba jsdom de mi truco de solución original del navegador (implementado sobre ImmutableJS para la pereza):
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
}
Y luego conectarlo manualmente a mis vars globales para que mis pruebas ava puedan usarlo:
import {
restoreGlobalDataTransfer,
stubGlobalDataTransfer,
} from ../helpers/jsdom-helpers'
test.before(t => {
stubGlobalDataTransfer()
})
test.after(t => {
restoreGlobalDataTransfer()
})
Todos los navegadores modernos (es decir, no IE <= 11) ahora admiten la configuración de archivos de entrada en una lista de archivos https://stackoverflow.com/a/47522812/2744776
Esto parece haber cambiado en una versión reciente. Esto es lo que funcionó para mí:
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;
}
Si está utilizando JSDOM a través de Jest, debe asegurarse de requerir los componentes internos fuera de la máquina virtual de prueba. Creé un entorno de prueba personalizado como este:
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;
Para poder acceder a las importaciones 'externas'.
Esto parece haber cambiado en una versión reciente. Esto es lo que funcionó para mí:
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; }
Si está utilizando JSDOM a través de Jest, debe asegurarse de requerir los componentes internos fuera de la máquina virtual de prueba. Creé un entorno de prueba personalizado como este:
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;
Para poder acceder a las importaciones 'externas'.
Esto me acercó, pero no puedo superar la validación interna:
exportaciones.is = valor => {
return utils.isObject (valor) && utils.hasOwn (valor, implSymbol) && value [implSymbol] instancia de Impl.implementation;
};
TypeError: No se pudo establecer la propiedad 'archivos' en 'HTMLInputElement': El valor proporcionado no es del tipo 'FileList'.
Pasó horas. Super frustrante
Esto parece haber cambiado en una versión reciente. Esto es lo que funcionó para mí:
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; }
Si está utilizando JSDOM a través de Jest, debe asegurarse de requerir los componentes internos fuera de la máquina virtual de prueba. Creé un entorno de prueba personalizado como este:
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;
Para poder acceder a las importaciones 'externas'.
Esto me acercó, pero no puedo superar la validación interna:
exportaciones.is = valor => {
return utils.isObject (valor) && utils.hasOwn (valor, implSymbol) && value [implSymbol] instancia de Impl.implementation;
};TypeError: No se pudo establecer la propiedad 'archivos' en 'HTMLInputElement': El valor proporcionado no es del tipo 'FileList'.
Pasó horas. Super frustrante
Después de comenzar de nuevo con un cerebro nuevo, encontré el problema. De alguna manera, tenía 2 entornos jsdom separados en ejecución que eliminaron las referencias a los símbolos.
Comentario más útil
Logré crear FileList sin tener que alterar ninguno de los códigos de la biblioteca jsdom:
En este caso, estoy creando un constructor de llamada de objeto FileList en FileList proporcionado por jsdom. Después de eso, solo estoy agregando un archivo a esa lista usando la notación Array. En mi caso, solo necesito 1 archivo en la matriz, pero esto también se puede cambiar a for loop para agregar varios archivos a FileList. Estoy creando un archivo con nombre / tipo / tamaño personalizado a través del constructor de archivos también proporcionado por jsdom, cuya funcionalidad sigue la especificación.
Esto podría ser útil para alguien que esté buscando cómo simular FileList en el entorno jsdom, pero todavía hay un problema. Crear FileList con una matriz de archivos usando este método no permitiría obtener elementos de la matriz usando el método FileList.item (index). Pero, incluso eso se puede arreglar anulando su método algo como esto:
Sigo sintiendo que sería mejor si jsdom pudiera ofrecer estas funcionalidades para propósitos de prueba desde el primer momento.