WebdriverIO でファイルの添付・ペースト・ドロップをテストする

行うにあたって少し調べる時間を取られたよくある三種のテストをまとめました。

WebdriverIO は v5 です。

画像の添付・ペースト・ドロップを行うと img タグで表示するので、正しく表示されているかを検査するテストを想定しています。

input[type="file"] にファイルを添付する

$(selector).click() によりファイル選択ダイアログは表示できますが、Webdriver からそれを操作することができません。そこでしかるべきメソッドを使用する必要があります。

v5 では chooseFile の替わりに browser.uploadFile + $(selector).setValue(remoteFile) の組みあわせで添付を実現します。

describe('attach file', () => {
  const attachment = '[data-role="attach_file"]';
  const addedImage = '[data-role="added_image"]';
  const filePath = path.join(__dirname, '../../fixtures/100_150.png');
  const expectedSize = {
    width: 100,
    height: 150
  };

  it('attach', async () => {
    browser.url('https://codepen.io/mmmpa/pen/mNzqpP');
    browser.switchToFrame(0);

    const remoteFilePath = browser.uploadFile(filePath);
    $(attachment).setValue(remoteFilePath);

    assert.deepEqual($(addedImage).getSize(), expectedSize);
  });
});

クリップボード上のなにかをペーストする

任意のローカル上のファイルをペーストしたい場合は、環境に応じた clipboard 操作コマンドを使ってデータを登録し、ペーストします。

わたしは Debian を使用しているので xclip を使用しました。

describe('paste file', () => {
  const pasteZone = '[data-role="paste_zone"]';
  const addedImage = '[data-role="added_image"]';
  const filePath = path.join(__dirname, '../../fixtures/100_150.png');
  const expectedSize = {
    width: 100,
    height: 150
  };

  it('paste', async () => {
    browser.url('https://codepen.io/mmmpa/pen/bXmYvr');
    browser.switchToFrame(0);
    $(pasteZone).click();

    exec(`xclip -i -selection clipboard -t image/png < ${filePath}`)
    browser.keys(["Control", "v"])

    assert.deepEqual($(addedImage).getSize(), expectedSize);
  });
});

ファイルをドロップする

残念ながら画面外のファイルをドロップする方法は見つかりませんでした。(OS を操作すれば可能かもしれません)

そこでフェイクイベントを作成して dispatchEvent し、それを以て動作の確認を行います。フェイクイベントが持つためのファイルの転送には先述の uploadFile を使用します。

なお React を使用している場合は普通の dispatchEvent には反応しないため、ReactTestUtils.Simulate.drop を使用する必要がありました。

共通部分: ファイル転送処理

browser.addCommand で任意のメソッドを登録できるので、ファイル転送用のメソッドを登録します。

input[type="file"] にファイルを添付し、それを後ほど作成するフェイクイベントに持たせます。返り値は selector と除去用の関数です。

const { v4 } = require('uuid')

browser.addCommand("prepareFileContainer", function (filePath) {
  const elementID = `new${v4()}`;
  const selector = `#${elementID}`;

  this.execute(`
    var input = document.createElement('input');
    input.setAttribute('id', '${elementID}');
    input.setAttribute('type', 'file');
    document.body.appendChild(input);
  `);

  const remoteFilePath = this.uploadFile(filePath);
  $(selector).setValue(remoteFilePath);

  this.execute(`
    var input = document.querySelector('${selector}');
    var items = Array.from(input.files).map(f => ({ kind: 'file', getAsFile: () => f}));
    input.items = items;
  `);

  const clear = () => this.execute(`
    var input = document.querySelector('${selector}');
    document.body.removeChild(input);
  `);

  return [selector, clear];
});

普通の DOM 用のイベント発行メソッド登録

フェイクイベントを作成しペーストされる DOM にイベントを発行する処理をメソッドとして登録します。

browser.addCommand("dropFile", function (targetSelector, filePath) {
  const [selector, clear] = this.prepareFileContainer(filePath);

  this.execute(`
    var items = document.querySelector('${selector}').items;
    var event = new Event('drop');
    event.dataTransfer = { items: items }
    document.querySelector('${targetSelector}').dispatchEvent(event);
  `);

  clear();
});

React DOM 用のイベント発行メソッド登録

ReactTestUtils を使用するためにグローバルな位置に宣言しなければならなかったので (https://codepen.io/mmmpa/pen/JgmreB) 、なにか正しい方法をご存知の方はご教示いただけると幸いです。

browser.addCommand("dropFileToReact", function (targetSelector, filePath) {
  const [selector, clear] = this.prepareFileContainer(filePath);

  this.execute(`
    var items = document.querySelector('${selector}').items;
    ReactTestUtils.Simulate.drop(
      document.querySelector('${targetSelector}'),
      { dataTransfer: { items: items } }
    );
  `);

  clear();
});

テスト本体

登録済みのメソッドを用いてペーストを擬似的に再現し、動作を確認します。

describe('drop file', () => {
  const dropZone = '[data-role="drop_zone"]';
  const addedImage = '[data-role="added_image"]';
  const filePath = path.join(__dirname, '../../fixtures/100_150.png');
  const expectedSize = {
    width: 100,
    height: 150
  };

  it('plain textarea', async () => {
    browser.url('https://codepen.io/mmmpa/pen/voVLQR');
    browser.switchToFrame(0);

    browser.dropFile(dropZone, filePath);

    assert.deepEqual($(addedImage).getSize(), expectedSize);
  });

  it('react textarea', async () => {
    browser.url('https://codepen.io/mmmpa/pen/JgmreB');
    browser.switchToFrame(0);

    browser.dropFileToReact(dropZone, filePath, true);

    assert.deepEqual($(addedImage).getSize(), expectedSize);
  });
});

おわりに

ファイル周りのテストは手作業で行うのは辛い面があります。私の場合 e2e が求められていなくてもローカルに用意することが多々あるので、少しでも自動化できるのは嬉しいですね。