🐡

Stencilでselectを変更したイベントを発火するテストの書き方

2021/04/03に公開

Web Components開発ライブラリであるStencilは、他のJavaScriptフレームワークと同様にデフォルトでユニットテスト・E2Eテストの用意があります。

https://stenciljs.jp/docs/testing-overview

けれど、Web ComponentsでE2Eテストをしようと思うと、ShadowDOMへのアクセスなどでハマることも多く、今回、selectを変更してイベントを発火する方法について悩んだので共有しておきます。

テストするコンポーネント

コンポーネントは、Web Component内に select DOMがある以下のような形だとします。ここの onChange をE2Eテスト内で実行させて、 myChange イベントを発火するようにします。コンポーネント名称は my-select としましょう。

@Component({
  tag: 'my-select',
  styleUrl: 'my-select.scss',
  shadow: true,
})
export class MySelect {
  ...
  private onChange = event => {
    this.myChange.emit({ value: event.target.value });
  };
  render() {
    return (
      <Host>
        <select onChange={this.onChange}>
          {options.map(option => (
            <option value={option.value}>{option.label}</option>
          ))}
        </select>
      </Host>
    );
  }
}

テストコード

テストを書く上で注意しないといけないのは、 Puppeteerselect メソッドがShadowDOMで利用できないことです。

https://github.com/puppeteer/puppeteer/issues/4171

そのため、 PuppeteerevaluateHandle を利用して、windowオブジェクト、documentオブジェクトにアクセスしながら、DOMの操作を行います。

そうするとテストは以下のようになります。

describe('my-select', () => {
  it('change', async () => {
    const page = await newE2EPage();
    await page.setContent(`<my-select></my-select>`,);
    await page.waitForChanges();
    
    const change = await page.spyOnEvent('myChange');

    await page.evaluateHandle(() => {
      return new Promise(resolve => {
        // my-select DOMを取得
        const element = document.querySelector('my-select');

	// myChangeイベントが発火したら非同期処理を解決
        element.addEventListener('myChange', event => {
          resolve();
        });
	
	// my-select DOM内の select DOMを取得
        const select: HTMLSelectElement = element.shadowRoot.querySelector(
          'select',
        );
	// select DOMにイベントをdispatch
        select.dispatchEvent(
          new Event('change', { bubbles: true, composed: true }),
        );
      });
    });

    await page.waitForChanges();
    expect(change).toHaveReceivedEvent();
  });
});

page.spyOnEvent('myChange') でイベント myChange が発火したかをチェックするモックオブジェクトをつくって、変数 change に格納します。このあと、そしてイベントを発火させた場合、 expect(change).toHaveReceivedEvent() のテストが通ります。(※イベントが発火していない場合は toHaveReceivedEvent していないためテストは失敗します)

Puppeteerselect メソッドが使えないため、 dispatchEvent をつかって強制的に onChange イベントを発火させています。そうすると、 addEventListener で監視していた myChange が発火するため、 evaluateHandle 内のPromiseが解決し、後続の処理が行われます。

ShadowDOMという仕様上ちょっと手間取りましたが簡単にテストが書けました。ここでは発火したことしか確認していませんが toHaveReceivedEventDetail メソッドを利用すると、イベントの中身もテストすることができます。

それではまた。

Discussion