unsweets.log

Frontend & Android.

HTMLInputElement.filesにFileListをセットする

Published at

状況

input[type="file"]上でファイルが選択されたときchangeイベントが発生するので、そのときの挙動をテストしたい。 jsdom環境においてファイルダイアログを出したりfilesに対して直接代入することは不可能なので、どうにかfilesを設定した上で、changeイベントを発生させ、挙動を確認したい。

環境

  • @vue/test-utils: 1.1.3
  • vue: 2.6.12
  • vue-template-compiler: 2.6.12

解決例

import { shallowMount } from "@vue/test-utils";
import Vue from "vue";

const App = Vue.extend({
  template: `
<div>
  <p>filename: {{ filename }}</p>
  <input @change="change" type="file" />
</div>`,
  data() {
    return {
      filename: "",
    };
  },
  methods: {
    change(e) {
      if (!e.target.files.length) return;
      this.filename = e.target.files[0].name;
    },
  },
});

test("files property test", async () => {
  const wrapper = shallowMount(App);
  const input = wrapper.find("input");
  expect(wrapper.text()).not.toContain("test.png");
  const file = new File([""], "test.png");
  const files = [file];
  const fileList = {
    item: (index) => files[index],
    length: files.length,
    ...files,
  };
  Object.defineProperty(input.element, "files", {
    value: fileList,
  });
  await input.trigger("change");
  expect(wrapper.text()).toContain("test.png");
});

Object.definePropertyで第一要素に設定したいinputのDOM,第二引数に設定したいフィールドである"files"を指定し、第三引数に実際に設定したい値であるFileListっぽいものを設定することで実現できた。

例はVueだけれども、DOMにアクセスできるものであれば任意の環境で再現可能なはず。filesフィールドに入るFileListは直接インスタンス化できないのでFileインターフェースを持ったデータの配列をオブジェクト内で展開しつつ、適当にitemとlengthを実装しておくとうまくいくはず(勿論実装上使ってなければ実装をサボってもいい)。TypeScript環境ならFileListを型アノテーションとして付与しておくとよりわかりやすい。その場合はitemとlengthの実装が必要になるが、手間としては誤差。

ちなみにinput.files = filesのようにFileListを直接代入すると TypeError: Failed to set the 'files' property on 'HTMLInputElement': The provided value is not of type 'FileList'.と怒られる。じゃあnew FileList()すればいいでしょと思ったがそうは問屋が卸さなかった。南無。


動くやつをCodeSandboxを使って書いてみた。 初めてなのでeslintのエラーがうまく消せなかったり、devDependenciesにテスト関連のファイルを移動させると怒られたりでうまく使いこなせない。