[CodeceptJS] executeScript が便利すぎる

## はじめに
株式会社メディアテック所属の @mt-h2 です。
普段は OutSystems を利用したローコードでシステム開発を行っています。

今回は、ローコードチームで使用しているE2Eフレームワークの [CodeceptJS](https://codecept.io/) の [executeScript](https://codecept.io/helpers/Playwright/#executescript) について記載したいと思います。

:::note info
今回は Playwright で検証していますが、Puppetter や Nightmare 等でも同じように利用可能です。
:::

## executeScript とは

公式ではシンプルに以下のように説明されています。
> Executes a script on the page:

下記公式サンプルを実行すると、ブラウザ上でアラートに「Hello world」と表示されます。
```js
I.executeScript(() => window.alert('Hello world'));
```

また、Playwright のサンプルにはありませんが [Nightmaare](https://codecept.io/helpers/Nightmare/#executeasyncscript) には以下サンプルが提示されています。
```js
let date = await I.executeScript(function(el) {
  // only basic types can be returned
  return $(el).datetimepicker('getDate').toString();
}, '#date'); // passing jquery selector
```
CodeceptJSでは呼び出せない、「$」（jQuery）オブジェクトを使用して特定オブジェクトの文字列を取得しています。

このように、ブラウザ上で実行可能な JavaScript であれば何でも実行する事ができます。

## なぜ executeScript を使用するのか
しかしながら、CodeceptJS では E2E で必要な事は大抵カバーされています。
それであれば executeScript を使う機会は無いのでは？となりますが、そのカバーされていない点を対応できるという所が使用するポイントであると考えられます。

## executeScript の使用例
結論からお伝えします。
グローバル変数にある [window オブジェクト](https://developer.mozilla.org/ja/docs/Web/API/Window)をあれこれして、その時点で CodeceptJS だけでは対応できないものを対応できるようにしようという考えです。

では、executeScript を「どういった時に使用するのか」を、いくつかの例を用いて説明していきたいと思います。

:::note info
サンプルコードでは「window.」も記載していますが、この部分は省略しても動作可能です。
:::

### 画面履歴を戻る/進む
ブラウザ「戻る」「進む」実行時の動作を確認したい時。

```js
// ブラウザの「戻る」と同様の処理
I.executeScript(() => window.history.back());
```

```js
// ブラウザの「進む」と同様の処理
I.executeScript(() => window.history.forward());
```

```js
// ブラウザの「n個前の履歴に進む」と同様の処理
I.executeScript(() => window.history.go(+-n));
```

### ブラウザの言語設定を判定
ブラウザの言語設定を確認して、それにより i18n 対応を確認。

```js
const ret = await I.executeScript(async () => hoge = window.navigator.language);
console.debug(ret); //ja
```

### LocalStorage操作
Cookieはデフォルトで操作できるようになっていますが、LocalStorageは未対応。

```js
// localStorage に値を登録
I.executeScript(() => window.localStorage.setItem('HogeKey', 'HogeValue'));

// localStorage から値を取得
const insertValue = await I.executeScript(async () => window.localStorage.getItem('HogeKey'));
console.debug(insertValue); //HogeValue

// localStorage から値を削除
I.executeScript(() => window.localStorage.removeItem('HogeKey'));
const removeValue = await I.executeScript(async () => window.localStorage.getItem('HogeKey'));
console.debug(removeValue); //null
```

:::note info
[sessionStorage](https://developer.mozilla.org/ja/docs/Web/API/Window/sessionStorage) も同じように利用可能です。
:::

### パフォーマンス測定
DevToolのネットワークで測定できるような事を、jsonオブジェクトで取得してパフォーマンスを測定。様々なイベントを対象にミリ秒までの値を取得されます。
```js
const ret = await I.executeScript(async () => hoge = window.performance);
console.debug(ret);
// {
//   timeOrigin: 1740130630635.6,
//   timing: {
//     connectStart: 1740130631554,
//     secureConnectionStart: 1740130631562,
//     unloadEventEnd: 0,
//     domainLookupStart: 1740130631546,
//     domainLookupEnd: 1740130631553,
//     responseStart: 1740130631600,
//     connectEnd: 1740130631582,
//     responseEnd: 1740130631601,
//     requestStart: 1740130631582,
//     domLoading: 1740130631603,
//     redirectStart: 0,
//     loadEventEnd: 1740130631726,
//     domComplete: 1740130631726,
//     navigationStart: 1740130630635,
//     loadEventStart: 1740130631726,
//     domContentLoadedEventEnd: 1740130631726,
//     unloadEventStart: 0,
//     redirectEnd: 0,
//     domInteractive: 1740130631726,
//     fetchStart: 1740130630636,
//     domContentLoadedEventStart: 1740130631726
//   },
//   navigation: { type: 0, redirectCount: 0 }
// }
```

### フォーカスされている要素のIDを取得
フォーカスされている要素の判定が必要な場合に使用。

```js
const ret = await I.executeScript(async () => window.document.activeElement.id);
console.debug(ret); //hoge-element
```

### スタイルシートを適用
スクリーンショット取得時に任意の要素を目立つようにしておきたい・テスト環境で実施している事が分かるようにしておきたい等、なんらかの理由で画面にスタイルシートを適用したい時に使用。

```js
I.executeScript(() => {
  const extraSheet = new CSSStyleSheet();
  // h1タグの背景を赤、文字色を白にする
  extraSheet.replaceSync("h1 { color: white; background-color: red; }");
  // 既存のスタイルシートに結合
  window.document.adoptedStyleSheets = [...window.document.adoptedStyleSheets, extraSheet];
});
```

## あとがき
諸々使用例として挙げてきました通り、JavaScript で出来る事は大抵なんでもできます。
MDN に`実験的な機能`として記載されているものについてもすぐに試すことができたりと非常に便利。

ただし、executeScript を利用する時はあくまで CodeceptJS で出来ない事をやりたい時に留めておき、可能な限り CodeceptJS で用意されているものを利用する事をお勧めします。

## さいごに
株式会社メディアテックでは絶賛 [開発メンバを募集中](https://www.mediatech.jp/recruit) です。
ローコード開発だけではなく、RPAやBIエンジニア諸々多数の募集をしておりますので、みなさまのご応募をお待ちしております。