input[type=”file”]の要素では、ファイルを選択させることができない。
しかし、jsdomを利用してテストを書きたいときの解決方法です。

何かの参考になれば幸いです。
また、issueがすでにopenになっているので将来的にjsdomで別の方法で修正されるかもしれません。

https://github.com/jsdom/jsdom/issues/1272

解決方法まとめ

解決方法としては、次の手順を踏んでinput[type=”file”]要素でファイルを選択した状態を作る。

  1. FileListクラスをFile[]のprototypeとして設定する。
  2. Object.definePropertyで、input[type=”file”]のvalueに設定する。

環境

必要なパッケージ

JS用

1
npm install --save-dev jsdom mime-types

TypeScript用

1
npm install --save-dev jsdom mime-types @types/node @types/mime-types

解決方法詳細

FileListクラスのモック作成用クラスの作成

次のコードの様に、ファイルパスの配列からFileListクラスのモックを作成するクラスを実装した。
参考: https://bitbucket.org/william_rusnack/addfilelist/src/master/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import * as fs from "fs";
import * as path from "path";
import * as mime from "mime-types";

export default class FileMockFactory
{
private window;

constructor(window: Window)
{
this.window = window;
}

setFileList(input: HTMLInputElement, file_paths: string);
setFileList(input: HTMLInputElement, file_paths: string[]);

setFileList(input: HTMLInputElement, file_paths: string[]|string)
{
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 => this.createFile(fp));
// FileListはaddとかできないので、この様に配列をFileListとして扱わせる。
Object.setPrototypeOf(file_list, Object.create(this.window.FileList.prototype));

// definePropertyでvalueプロパティを変更する。
Object.defineProperty(input, 'files', {
value: file_list,
writable: false,
})

return input
}

createFile(file_path: string)
{
const { mtimeMs: lastModified } = fs.statSync(file_path)

return new this.window.File(
[fs.readFileSync(file_path)],
path.basename(file_path),
{
lastModified,
type: mime.lookup(file_path) || '',
}
)
}
}

使い方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import * as assert from 'power-assert';
import * as JSDOM from 'jsdom';
import * as path from "path";
import FileMockFactory from '../../TestHelpers/FileMockFactory';

const fixture = `<html>
<head></head>
<body>
<input type="file" id="file-input">
</body>
</html>`;

describe('file attached test', () => {
let jsdom: JSDOM.JSDOM;
let fileMockFactory: FileMockFactory;

before(() => {
jsdom = new JSDOM.JSDOM(fixture);
fileMockFactory = new FileMockFactory(jsdom.window);
});

describe('file attached test', () => {
it('should setting FileList.', () => {
const targetElement = <HTMLInputElement>jsdom.window.document.getElementById('file-input');
const generator = new FileUpload(parser);

// こんな感じで設定する。
fileMockFactory.setFileList(targetElement, [__filename]);

// これでfilesプロパティにセットされているはず。
assert.equal(targetElement.files[0].name, path.basename(__filename));
});
});
});

参考

jsdom issue