1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

GoogleChromeのDevToolsでブラウザ操作を記録・再生できるようになるので試した

Last updated at Posted at 2021-11-14

(システム)テスト自動化エヴァンジェリストの@YoshikiItoです。

この記事を書く一年ほど前に、DevToolsでブラウザ操作を記録してPuppeteerのコードが吐ける機能がプレビューされていると知って試していました。

参考:GoogleChromeのDevToolsでブラウザ操作を記録し、Puppeteerのコードを出力してみる - Qiita

まだChromeの正式版には載っていませんが、かなり進化をしているようなニュースが先日リリースされています。

参考:Record, replay and measure user flows - Chrome Developers

ということで、どれだけすごくなったのか、1年ぶりに試してみました。

前提

  • Windows 10

準備

現行のChrome95では機能をONにする項目が見当たらず・・・上記Chrome Developersのページによると

This is a preview feature in Chrome 97. Our team is actively working on this feature and we are looking for your feedback for further enhancements.

らしいので、Chrome 97(本記事を書いている時点でDev)を入れてみます。

ちなみにChromeの直近のリリーススケジュールはここから見られます。

Chrome Platform Status

image.png

Chrome 97をクリックすると、ダウンロードページに飛べます。今回はココ:Google Chrome デベロッパー ツール - Google Chrome

image.png

Chrome Devをダウンロードします。すぐにexeがダウンロードされるので、起動してインストール。

Chrome Devがインストールできました。

image.png

Recorderの有効化

■■■(縦)からMore Tools→Recorderを選択

image.png

Recorderタブが開きます。

大体の説明はここに載ってます。勝手にシステムテストやE2Eテストと呼ばれそうなテストレベルを想定してますが、

Measure performance across an entire user journey

と書いてあるので、ネットワークのパフォーマンスなどそちらを測るのが主・・・にも見えつつ、③でPuppeteerのスクリプトが吐けるよと書いてあるので、どっちメインとかは無いのかも。

image.png

操作を記録する

同画面の`Start new recording"ボタンを押して開始します。

ボタンを押すとRECORDING NAMEを入れろと言われるので、Qiita loginとでもしておきます。

RECORDING NAMEを入れたらStart a new recordingボタンをクリック。

image.png

比較のため、GoogleChromeのDevToolsでブラウザ操作を記録し、Puppeteerのコードを出力してみる - Qiitaで記録したのと同じ手順で操作してみます。

  1. ユーザー名にHogeを入力
  2. メールアドレスにhoge@hoge.comを入力
  3. パスワードにhogehogeを入力
  4. 登録するボタンをクリック

image.png

1年前にこの機能を触ってみたときと異なり、操作しているステップがリアルタイムに表示されます。

これはAutifyやmablなどのレコーダーに近い印象。個人的には好きです。

End recordingをクリックして記録を停止します。

記録したテストを再生する

そのままReplayボタンがあるので押します。

image.png

無事再生されます。

アサーションを入れる

ステップで▶をクリックしてみると、セレクタやx, y座標などの情報が見られます。

image.png

ここにAdd assertedEventsというボタンがあります。押してみると、

  • type
  • url
  • title

を入力できます。

試しにAssertがFailするよう、わざとへんなURLを与えてみます。(iが一つ多い)

image.png

この状態で案の定失敗するのですが、urlとtitleを正しい値にしてもFailしてしまいます。ナゼ

公式の記事(Record, replay and measure user flows - Chrome Developers)にも特にAssertionに関する記載がないので、まだ開発途中なのかもしれません。

Puppeteerのコードを出力

image.png

上部にある↑のボタンを押すと、ファイルを保存するダイアログが出ます。
好きな名前で保存。

実際に出力されたコードがこちら。長いです。

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":1012,"height":639})
    }
    {
        const targetPage = page;
        const promises = [];
        promises.push(targetPage.waitForNavigation());
        await targetPage.goto('https://qiita.com/');
        await Promise.all(promises);
    }
    {
        const targetPage = page;
        const element = await waitForSelectors([["aria/ユーザー名"],["#url_name"]], targetPage);
        await element.click({ offset: { x: 101.33331298828125, y: 12} });
    }
    {
        const targetPage = page;
        const element = await waitForSelectors([["aria/ユーザー名"],["#url_name"]], 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('Hoge');
        } 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 }));
          }, "Hoge");
        }
    }
    {
        const targetPage = page;
        const element = await waitForSelectors([["aria/メールアドレス"],["#email"]], targetPage);
        await element.click({ offset: { x: 92.33331298828125, y: 30} });
    }
    {
        const targetPage = page;
        const element = await waitForSelectors([["aria/メールアドレス"],["#email"]], 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('hoge@hoge.com');
        } 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 }));
          }, "hoge@hoge.com");
        }
    }
    {
        const targetPage = page;
        const element = await waitForSelectors([["aria/パスワード"],["#password"]], targetPage);
        await element.click({ offset: { x: 88.33331298828125, y: 12} });
    }
    {
        const targetPage = page;
        const element = await waitForSelectors([["aria/パスワード"],["#password"]], 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('hogehoge');
        } 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 }));
          }, "hogehoge");
        }
    }
    {
        const targetPage = page;
        const element = await waitForSelectors([["aria/登録する"],["body > div.allWrapper > div.p-home.is-anonymous > div.nl-Hero > div > div.nl-Hero_end > form > div.nl-SignupForm_buttonArea > div > button"]], targetPage);
        await element.click({ offset: { x: 62.33331298828125, y: 18.6666259765625} });
    }

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

クリック操作は座標まで保存されています。
登録するボタンなんかは座標でなくセレクタ何か使ってクリックしてほしいところですね・・・

所感

そもそもキャプチャ・リプレイ自体がそうなのですが、操作を記録してコード出力したからといってそのまま使えるわけではない、のは変わらずですね。
ただ、何もないところからいきなり書き始めるよりは、記録したものを修正するほうが楽、という方もいるので、その手の使いみちはありそうです。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?