Jsdom: Exponha alguns métodos de adição de arquivos a um FileList

Criado em 23 out. 2015  ·  30Comentários  ·  Fonte: jsdom/jsdom

FileList's não são graváveis ​​nas especificações, mas para tornar input.files testável, precisaremos implementar um método utilitário para modificá-lo.

/ cc @cpojer

feature

Comentários muito úteis

Consegui criar FileList sem ter que alterar nenhum código da 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());

Neste caso, estou criando o construtor de chamada de objeto FileList em FileList fornecido por jsdom. Depois disso, estou apenas adicionando o arquivo à lista usando a notação Array. No meu caso, preciso apenas de 1 arquivo no array, mas também pode ser alterado para loop for para adicionar vários arquivos a FileList. Estou criando um arquivo com nome / tipo / tamanho personalizado por meio do construtor de arquivo também fornecido por jsdom, cuja funcionalidade está de acordo com a especificação.

Isso pode ser útil para quem está procurando como simular FileList no ambiente jsdom, mas um problema ainda permanece. Criar FileList com array de arquivos usando este método não permitiria obter itens de array usando o método FileList.item (index). Mas, mesmo isso pode ser corrigido substituindo seu método mais ou menos assim:

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

Ainda acho que seria melhor se jsdom pudesse oferecer essas funcionalidades para fins de teste fora da caixa.

Todos 30 comentários

input.files = createFileList(file1, file2, ...) seria bom.

@cpojer Você está gerando objetos File reais para colocar lá?

Tornar input.files gravável provavelmente é ruim, podemos provavelmente retornar o array bruto para preencher você mesmo ou fazer algo como fillFileList(input.files, [file]) .

Na verdade, usamos apenas dados fictícios, então populamos .files com um array de objetos. Mas exigir que seja um objeto File seria razoável, eu acho.

Parece que defineProperty seria adequado aqui? As propriedades DOM são reconfiguráveis ​​por um motivo ...

Eu prefiro criar um FileList real com dados reais, no entanto.

Também deve ser possível acessar itens FileList usando seus índices de matriz. .item não é a única forma de acessar seus campos.

OK, parece que existem alguns problemas em potencial:

  • FileList não tem acesso indexado funcionando corretamente (bug)
  • Não há como criar objetos FileList para teste (lacuna de recurso da plataforma web).

Mas modificar inputEl.files não é o problema.

Sim, quero dizer, não é incrível dizer aos engenheiros para usar Object.defineProperty em vez de uma atribuição normal, mas posso viver com isso.

Bem, eles têm que fazer isso de qualquer maneira em um navegador real, então me parece razoável ...

Olá, vejo que faz mais de um ano, gostaria de perguntar se houve algum progresso em relação a esse problema. Eu uso o framework Jest para testar meu aplicativo React / Redux, que usa jsdom internamente. Tenho um problema em que preciso criar FileList dinamicamente com um ou mais objetos File.

Olhando para lib / jsdom / living / filelist.js, posso ver que há um construtor para FileList, mas não há nenhuma opção para passar arquivos para ele. Eu entendo que FileList e File não têm construtor de acordo com as especificações por motivos de segurança, mas há alguma intenção de permitir que o construtor aceite array de objetos File ou pelo menos método adicional (digamos _setItem_) que nos permitiria adicionar File objetos na lista especificamente para fins de teste?

Também vejo outro problema com FileList. Se não estou enganado, deve ser um objeto do tipo Array, o mesmo que NodeList (lib / jsdom / living / node-list.js), o que significa que deve haver a possibilidade de acessar objetos File de duas maneiras:

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

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

Atualmente, só é possível acessar através do método. Isso significa que a mesma lógica de NodeList deve ser aplicada aqui:

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

e os arquivos devem ser armazenados mais ou menos assim se permitirmos a passagem da matriz de arquivos para o construtor:

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

Não tenho opiniões fortes sobre como isso deve ser feito, são apenas exemplos que utilizo para explicar melhor o que estou tentando apontar.

Perguntas extras (não quero abrir questões desnecessárias antes de perguntar aqui):

1.) Seria benéfico adicionar o construtor de Arquivo para fins de teste que cria o objeto Arquivo a partir de um arquivo real usando o caminho fornecido como string? Encontrei uma biblioteca (desculpe, não consigo mais encontrar o link) que permitia isso:

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

Isso criou um arquivo para mim com propriedades (size, lastModified, type ...) sem que eu tivesse que criá-lo manualmente:

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

Não sei como essa biblioteca funcionava, tudo que sei é que ela não foi mantida por mais de um ano e paramos de usá-la. Não consigo mais encontrar.

2.) window.URL.createObjectURL não é compatível com jsdom. Não tenho certeza se deve ser relatado.

Consegui criar FileList sem ter que alterar nenhum código da 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());

Neste caso, estou criando o construtor de chamada de objeto FileList em FileList fornecido por jsdom. Depois disso, estou apenas adicionando o arquivo à lista usando a notação Array. No meu caso, preciso apenas de 1 arquivo no array, mas também pode ser alterado para loop for para adicionar vários arquivos a FileList. Estou criando um arquivo com nome / tipo / tamanho personalizado por meio do construtor de arquivo também fornecido por jsdom, cuja funcionalidade está de acordo com a especificação.

Isso pode ser útil para quem está procurando como simular FileList no ambiente jsdom, mas um problema ainda permanece. Criar FileList com array de arquivos usando este método não permitiria obter itens de array usando o método FileList.item (index). Mas, mesmo isso pode ser corrigido substituindo seu método mais ou menos assim:

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

Ainda acho que seria melhor se jsdom pudesse oferecer essas funcionalidades para fins de teste fora da caixa.

Oi, pessoal ,
Estou enfrentando alguns problemas durante a simulação do objeto $ ("# selectorID of Upload file") [0] .files [0] que está retornando FileList.
Alguém pode me ajudar a criar o objeto FileList? Porque não consigo encontrar nenhum link de referência em qualquer lugar da WWW

Atualmente não é possível.

Obrigado Domenic,

Preciso escrever um caso de teste para evento de alteração de upload de arquivo. qualquer forma alternativa de executar esse cenário de teste.

Algum progresso nisso?

@niksajanjic Você conseguiu implementar a melhor solução apresentada neste tópico?
Ref: const file = new File('../fixtures/files/test-image.png');
Ou algo parecido com isso?
Melhor

@domenic parece um pouco

@domenic Ok, configurei o início básico para isso. Nem todos os cheques estão lá, mas é essencialmente sobre isso que

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
}

Arquivo de demonstração

/*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 Fiz o teste de uma maneira que expliquei em meu post posterior lá. Infelizmente, alguns meses depois, ele parou de funcionar em versões mais recentes do jsdom quando um de meus colegas tentou copiar aquele código. Portanto, tivemos que comentar os testes para FileList naquele ponto. A partir daí, não conseguimos encontrar uma maneira de escrever esses testes e nos últimos meses ninguém teve tempo de dar uma nova olhada e tentar encontrar um jeito.

@niksajanjic Obrigado por sua atualização. Estou trabalhando na solução que você propôs e parece estar funcionando (veja o código acima), mas acho improvável que seja adicionado ao jsdom, pois seria muito difícil descobrir como adicioná-lo.
Sinta-se à vontade para dar uma olhada e usá-lo se quiser

Criou um script auxiliar simples que resolve esse problema até que o projeto jsdom encontre uma solução.
https://bitbucket.org/william_rusnack/addfilelist/src/master/

Adicionar um arquivo:

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

Adicionar vários arquivos:

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

Instalar e exigir

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 seu repo parece ter sido excluído?

Tive um problema semelhante - escrevi uma função que recebia FileList como entrada e queria escrever um teste de unidade para ela usando jest. Consegui usar a função auxiliar abaixo para falsificar um objeto FileList suficiente para trabalhar com minha função. A sintaxe é ES6 com anotações de fluxo. Sem promessas de que funcionará em todas as situações, pois está apenas fingindo a funcionalidade da classe 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,
  };
};

No frontend, posso fazer algo como o seguinte para (de uma maneira geral) construir um 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

Infelizmente, jsdom não parece realmente suportar DataTransfer ainda, então isso não funciona em meus testes:

Outros árbitros:


Pesquisando um pouco a fonte, encontrei exports.FileList = require("./generated/FileList").interface;

~ Mas não estava claro para mim no GitHub onde encontrar a fonte que acaba construindo ./generated ~

A exportação principal do pacote npm é ./lib/api.js e exporta uma API pública muito pequena:

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

Mas olhando em meu diretório ./node_modules/jsdom/lib/jsdom ... posso ver que todos os arquivos internos / de implementação também estão lá, incluindo ./node_modules/jsdom/lib/jsdom/living/file-api :

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

FileList-impl.js aqui contém a implementação JS real que apóia a FileList api exposta em jsdom.

Agora, se olharmos para ./node_modules/jsdom/lib/jsdom/living/generated/FileList.js , veremos a 'API pública' real gerada que acabamos vendo por meio do uso normal, incluindo o nosso muito familiar:

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

Este arquivo exporta module.exports = iface; , que contém muito mais funcionalidade do que acabamos com a exposição da API pública 'normal', que usa apenas a chave iface.interface . Então, talvez pudéssemos fazer algo divertido se usarmos require("./generated/FileList") diretamente. Removendo os detalhes de implementação, temos uma interface semelhante 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

Portanto, agora que sabemos que há mais energia a ser obtida .. vamos ver como outras áreas de jsdom acessam ..

Dando uma olhada em HTMLInputElement-impl , parece usar FileList.createImpl() , embora, infelizmente, não nos mostre realmente como usar os parâmetros:

createImpl é apenas um pequeno invólucro em torno de setup no iface exportado:

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

Brincando com isso em um console, parece que temos o api expressivo do elemento Array , o qual FileListImpl é apoiado. Portanto, podemos fazer coisas como:

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

Ele tem uma propriedade Symbol(wrapper) , que precisaremos usar ./node_modules/jsdom/lib/jsdom/living/generated/utils.js:37 para acessar:

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

O iface exportado tem uma função convert , que throw new TypeError( $ {context} não é do tipo 'FileList'. ); quando o objeto fornecido não é um FileList . Podemos usar isso para testar coisas.

Se chamarmos de myFileListImpl bruto, ele gerará o erro:

flist.convert(myFileListImpl)

Considerando que usando o wrapper extraído acima, podemos ver que ele não gera o erro:

flist.convert(myFileListImpl[utils.wrapperSymbol])

Com isso, podemos modificar myFileListImpl e obter um objeto FileList aceitável de volta, para passar onde precisamos. Um exemplo totalmente trabalhado (usando util.wrapperForImpl() vez de nosso 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

Agora, com esse conhecimento, posso implementar a versão de teste do jsdom do meu hack de solução alternativa para o navegador (implementado em cima do ImmutableJS para preguiça):

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
}

E, em seguida, conecte-o manualmente ao meu vars global para que meus testes ava possam usá-lo:

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

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

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

Todos os navegadores modernos (ou seja, não o IE <= 11) agora oferecem suporte à configuração de input.files para um FileList https://stackoverflow.com/a/47522812/2744776

Isso parece ter mudado em um lançamento recente. Aqui está o que funcionou para mim:

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

Se você estiver usando JSDOM via Jest, certifique-se de exigir os internos fora da VM de teste. Eu criei um ambiente de teste 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 que eu pudesse acessar as importações 'externas'.

Isso parece ter mudado em um lançamento recente. Aqui está o que funcionou para mim:

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

Se você estiver usando JSDOM via Jest, certifique-se de exigir os internos fora da VM de teste. Eu criei um ambiente de teste 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 que eu pudesse acessar as importações 'externas'.

Isso me aproximou, mas não consigo superar a validação interna:

exportações.is = valor => {
return utils.isObject (value) && utils.hasOwn (value, implSymbol) && value [implSymbol] instanceof Impl.implementation;
};

TypeError: Falha ao definir a propriedade 'files' em 'HTMLInputElement': O valor fornecido não é do tipo 'FileList'.

Horas gastas. Super frustrante

Isso parece ter mudado em um lançamento recente. Aqui está o que funcionou para mim:

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

Se você estiver usando JSDOM via Jest, certifique-se de exigir os internos fora da VM de teste. Eu criei um ambiente de teste 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 que eu pudesse acessar as importações 'externas'.

Isso me aproximou, mas não consigo superar a validação interna:

exportações.is = valor => {
return utils.isObject (value) && utils.hasOwn (value, implSymbol) && value [implSymbol] instanceof Impl.implementation;
};

TypeError: Falha ao definir a propriedade 'files' em 'HTMLInputElement': O valor fornecido não é do tipo 'FileList'.

Horas gastas. Super frustrante

Depois de começar de novo com um cérebro novo, descobri o problema. De alguma forma, eu tinha 2 ambientes jsdom separados em execução, o que eliminava as referências a símbolos.

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

mitar picture mitar  ·  4Comentários

philipwalton picture philipwalton  ·  4Comentários

lehni picture lehni  ·  4Comentários

potapovDim picture potapovDim  ·  4Comentários

JacksonGariety picture JacksonGariety  ·  4Comentários