虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

Chromeに入るRecorder機能の利用と注意点

こんにちは。虎の穴ラボのH.Hです。
今回は先日発表されたChromeの開発中の新機能であるRecorder機能について使用方法や利用する際の注意点などをまとめました。

Recoder機能とは

ブラウザのChrome97で追加される予定の機能で、ブラウザの画面上で操作した記録を取得してくれる機能になります。
この記事を書いている2021年11月17日では一般に提供されているChromeの最新版は96となり、開発中の「Chrome Dev」もしくは「Chrome Canary」でRecorder機能を使用することができます。
利用している時の様子はChromeの開発者向けのページに公開されています。

developer.chrome.com

主な機能は以下の通りです。
・操作の記録及び再実行(リプレイ)できる
・再実行時にパフォーマンスの記録・確認できる
・記録した内容の編集ができる
・操作をPuppeteer(Chromeを自動で画面操作を行うNode.jsのライブラリ)で実行できる設定ファイルを出力できる

今回Puppeteerで実行できる設定ファイルがどのようなものになるのかを確認するために、Recorder機能を試しに使ってみました。
記録する内容はGoogleの検索ページで「虎の穴ラボ」を検索して、コーポレートサイトまでアクセスする操作を取得していきます。

利用開始手順

今回はMac版の「Chrome Dev」を利用して確認していきます。

1.インストール

以下のリンクにアクセスして、アプリケーションをダウンロードしてインストールします。

www.google.co.jp

手順はリリースされている一般公開されているChromeと変わりません。
また、異なるアプリケーションとしてみなされるので既存のChromeに上書きされることもありません。

2.起動

インストールするとChromeのロゴの上に「Dev」と書かれたアプリケーションができるので、そのまま起動します。
ちなみにChromeにはリリースされるバージョンの他に、一つ先のバージョンである「Beta」版、さらに先のバージョンの「Dev」版、開発中の内容が毎日適用される「Canary」版があります。
ロゴは下の画像のようになっています。左から一般公開されたChrome、Beta版、Dev版、Canary版となります。

Chromeロゴ
Chromeの各バージョンのロゴ

3.Recorderタブの表示

Developerツールには標準ではRecorderのタブは出てきませんので、歯車マークの右隣のボタンから表示する必要があります。
「More Tools」→「Recorder」

Recoderタブ

選択すると下記のようなタブが表示されます。

Recorderタブ

Recorder機能の利用方法

1.記録方法

1.1事前準備

記録を開始するページを表示しておきます。
次の手順で記録に名前をつけて確定するとすぐに記録が始まるので、事前に開始するページに移動しておく必要があります。
またレスポンシブデザインの場合、画面サイズによって表記が変わることも考えるので画面サイズも事前に変えておきます。
記録が始まると画面サイズの変更はできません。

1.2記録に名前をつける

後からわかりやすい任意の名前をつけることができます。特にアルファベット限定というわけではなく日本語でも設定可能です。今回は「テスト」という名前とします。

1.3画面操作を行う

記録が始まったら、行いたい操作をブラウザ上で実施します。
下の画像はGoogleの検索ページから「虎の穴ラボ」と検索して弊社のコーポレートサイトを表示した時の表記です。

選択したテキストボックスやキー入力のイベントが記録されていることがわかります。
操作が終わると、Recordingと表記されている下にある赤いストップボタンを押すことで記録は完了します。

記録している時の様子はこちらです。 f:id:toranoana-lab:20211116200815g:plain

2.リプレイ

操作のリプレイは名前の「テスト」の右にいくと青文字の「Replay」ボタンがあり、そのボタンを押すことで実行されます。
Recorder機能では複数の操作を記録しておくことができるので、以前に作成した操作をリプレイで実行することが可能です。
リプレイ時に設定として変更できるものは、現状ではネットワークの回線速度を疑似的に下げることだけで画面サイズなども変更できません。

3.パフォーマンス確認

再実行したときに表示する際のLoading時間などを確認することができます。
「Replay」ボタンの右側の「Measure performance」を押すことで、リプレイを一度動かして自動的にPerformanceのデータが取得されます。
タブが「Performance」に移り見ることができるので、一連の操作の中で遅い部分などを確認することができます。

4.編集

画面操作で取得した記録には、どのボタンを押したかどこにどのような文字を入力したかを細かく記録されています。この記録は画面上から変更することができます。
ClickやChangeなどの操作の左にある矢印を押すことで、操作の詳細が表示されるようになります。 画像はGoogleの検索画面に「虎の穴ラボ」と入力した部分となります。
valueの値を変えることで、別の文字列を検索するようにもできます。

5.Puppeteerで利用可能な設定ファイルの出力

記録した操作をPuppeteerで実行可能なJavaScriptファイルで出力することができます。
下記のテスト名のプルダウンの右にある上向の矢印のボタンで出力されます。

出力された設定ファイルはそのままPuppeteerのヘッドレスモードで実行可能な状態になっています。

こちらが出力した結果となります。

const puppeteer = require('puppeteer');

(async () => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();

    async function waitForSelectors(selectors, frame) {
      for (const selector of selectors) {
        try {
          return await waitForSelector(selector, frame);
        } catch (err) {
          console.error(err);
        }
      }
      throw new Error('Could not find element for selectors: ' + JSON.stringify(selectors));
    }

    async function waitForSelector(selector, frame) {
      if (selector instanceof Array) {
        let element = null;
        for (const part of selector) {
          if (!element) {
            element = await frame.waitForSelector(part);
          } else {
            element = await element.$(part);
          }
          if (!element) {
            throw new Error('Could not find element: ' + part);
          }
          element = (await element.evaluateHandle(el => el.shadowRoot ? el.shadowRoot : el)).asElement();
        }
        if (!element) {
          throw new Error('Could not find element: ' + selector.join('|'));
        }
        return element;
      }
      const element = await frame.waitForSelector(selector);
      if (!element) {
        throw new Error('Could not find element: ' + selector);
      }
      return element;
    }

    async function waitForElement(step, frame) {
      const count = step.count || 1;
      const operator = step.operator || '>=';
      const comp = {
        '==': (a, b) => a === b,
        '>=': (a, b) => a >= b,
        '<=': (a, b) => a <= b,
      };
      const compFn = comp[operator];
      await waitForFunction(async () => {
        const elements = await querySelectorsAll(step.selectors, frame);
        return compFn(elements.length, count);
      });
    }

    async function querySelectorsAll(selectors, frame) {
      for (const selector of selectors) {
        const result = await querySelectorAll(selector, frame);
        if (result.length) {
          return result;
        }
      }
      return [];
    }

    async function querySelectorAll(selector, frame) {
      if (selector instanceof Array) {
        let elements = [];
        let i = 0;
        for (const part of selector) {
          if (i === 0) {
            elements = await frame.$$(part);
          } else {
            const tmpElements = elements;
            elements = [];
            for (const el of tmpElements) {
              elements.push(...(await el.$$(part)));
            }
          }
          if (elements.length === 0) {
            return [];
          }
          const tmpElements = [];
          for (const el of elements) {
            const newEl = (await el.evaluateHandle(el => el.shadowRoot ? el.shadowRoot : el)).asElement();
            if (newEl) {
              tmpElements.push(newEl);
            }
          }
          elements = tmpElements;
          i++;
        }
        return elements;
      }
      const element = await frame.$$(selector);
      if (!element) {
        throw new Error('Could not find element: ' + selector);
      }
      return element;
    }

    async function waitForFunction(fn) {
      let isActive = true;
      setTimeout(() => {
        isActive = false;
      }, 5000);
      while (isActive) {
        const result = await fn();
        if (result) {
          return;
        }
        await new Promise(resolve => setTimeout(resolve, 100));
      }
      throw new Error('Timed out');
    }
    {
        const targetPage = page;
        await targetPage.setViewport({"width":799,"height":605})
    }
    {
        const targetPage = page;
        const promises = [];
        promises.push(targetPage.waitForNavigation());
        await targetPage.goto('https://www.google.com/?hl=ja');
        await Promise.all(promises);
    }
    {
        const targetPage = page;
        const element = await waitForSelectors([["aria/検索"],["body > div.L3eUgb > div.o3j99.ikrT4e.om7nvf > form > div:nth-child(1) > div.A8SBwf > div.RNNXgb > div > div.a4bIc > input"]], targetPage);
        await element.click({ offset: { x: 221.5, y: 21.5} });
    }
    {
        const targetPage = page;
        const element = await waitForSelectors([["aria/検索"],["body > div.L3eUgb > div.o3j99.ikrT4e.om7nvf > form > div:nth-child(1) > div.A8SBwf.sbfc > div.RNNXgb > div > div.a4bIc > input"]], targetPage);
        const type = await element.evaluate(el => el.type);
        if (["textarea","select-one","text","url","tel","search","password","number","email"].includes(type)) {
          await element.type('虎の穴ラボ');
        } else {
          await element.focus();
          await element.evaluate((el, value) => {
            el.value = value;
            el.dispatchEvent(new Event('input', { bubbles: true }));
            el.dispatchEvent(new Event('change', { bubbles: true }));
          }, "虎の穴ラボ");
        }
    }
    {
        const targetPage = page;
        await targetPage.keyboard.up("Enter");
    }
    {
        const targetPage = page;
        const promises = [];
        promises.push(targetPage.waitForNavigation());
        const element = await waitForSelectors([["aria/Google 検索"],["body > div.L3eUgb > div.o3j99.ikrT4e.om7nvf > form > div:nth-child(1) > div.A8SBwf > div.FPdoLc.lJ9FBc > center > input.gNO89b"]], targetPage);
        await element.click({ offset: { x: 68.21875, y: 20} });
        await Promise.all(promises);
    }
    {
        const targetPage = page;
        const promises = [];
        promises.push(targetPage.waitForNavigation());
        const element = await waitForSelectors([["aria/とらラボ – 虎の穴ラボ株式会社"],["#rso > div:nth-child(1) > div > div > div > div > div > div.yuRUbf > a > h3"]], targetPage);
        await element.click({ offset: { x: 208, y: 15.203125} });
        await Promise.all(promises);
    }

    await browser.close();
})();

これをPuppeteerがインストールされた端末で実行すれば同じ動きが再現されます。
ヘッドレスモードで実行されるので画面にはブラウザなどは立ち上がりません。もし画面の表示を確認したい場合は、以下のようにコードを書き換えることで可能です。

const browser = await puppeteer.launch();
↓
const browser = await puppeteer.launch({headless: false});

Puppeteerの使い方については、以前インストール方法などをまとめた記事を書いていますのでぜひ参考にしてみてください。

toranoana-lab.hatenablog.com

ここまでがRecorder機能の使い方となります。

注意点

1.詳細に操作の記録が取れる

これまで画面操作を記録するものとしてHeadlessRecorderというChromeの拡張機能がありました。今回のChromeのRecorder機能で出力したPuppeteer向けの設定ファイルの内容を見た限りHeadlessRecorderで取得できる内容よりかなり正確な内容でした。
HeadlessRecorderだと取れなかったテキストエリアの入力に関しても詳細に取得することができるため、ファイル出力後に実行したときにエラーになる事はかなり少なくなっていると考えられます。(HeadlessRecorderはPlaywriteというツール向けの設定ファイルも出力することができる点が今回のRecorder機能ではできない事になります)
ただし正確に取得できる反面IDやパスワードといった入力も平文で取得できてしまうため、誤って本番の環境で記録した設定ファイルを公開してしまうとパスワードも流出してしまう可能性があるので注意が必要です。

2.操作のコピーを作る事はできない

以前の操作をコピーして一部変更した操作を作ることができないため、どのような場合でも操作は一度記録を行う必要があります。
普段の操作ではあまりないと思いますが、テストで操作を記録していたWEBページの類似ページでも正常に動作するのか確かめることがあるかと思います。その際には一度操作をして別の操作の記録を取るか、すでにある操作を書き換えてリプレイする必要があります。

3.別ブラウザでの実行はPuppeteerのみ

操作を記録したChrome上では操作や画面遷移がわかりやすく表示されていますが、これを別の人のChromeにコピーする事はできません。そのため、他の人が操作を再現する場合にはPuppeteer用の設定ファイルをもらってPuppeteerで起動することになります。

まとめ

今回は開発中のRecorder機能について使用してみました。この記事を書いているタイミングでQiitaでも私と同じように動きを確認した結果をまとめられている方もいました。下記の記事を書かれた方も言及されていますが、まだ開発中の内容なので実際に公開される際には変更になってる可能性もあります。

https://qiita.com/YoshikiIto/items/62a6caf7a1e1cf96bcb3

今回出力できた内容を見る限りかなり詳細に操作が記録できたPuppeteer用のファイルを出力することができていたのでかなり有用だと感じました。

P.S.

採用情報

■募集職種
yumenosora.co.jp

カジュアル面談も随時開催中です

■お申し込みはこちら!
news.toranoana.jp

■ToraLab.fmスタートしました!

メンバーによるPodcastを配信中!
是非スキマ時間に聞いて頂けると嬉しいです。
anchor.fm

■Twitterもフォローしてくださいね!

ツイッターでも随時情報発信をしています
twitter.com