👀

[PlayWright] heading タグの構造が不用意に崩れていないか気づくためのテストを用意する

2025/02/17に公開

React のようなコンポーネント指向のライブラリやフレームワークは、Web 開発において実装がしやすく便利です。しかし、実装やレビューがコンポーネント単位になりやすく、最終的に出力される HTML の確認が漏れてしまって意図せず不具合が起きているということが起こることもありますね。

私が気になっているのが、heading タグ(h1 ~ h6)のレベル構造です。SEO やアクセシビリティに影響が出てしまう可能性があるので、レベル構造の崩れは避けたいです。

ただ、レビューのたびに heading タグが崩れていないか疑ったり開発者ルールでチェックするのは少し大変で、見逃してしまうこともありそうです。そこで、heading タグのレベルを出力して差分がないか明示的にレビューできるようにしたいと考えました。

コンポーネントではなく最終的に出力される HTML をチェックしたいので、 E2E テストが良さそうです。そこで、E2E テストのライブラリである Playwright を活用して実装をしていきます。

毎 PR で回すほどのものではないと思っています。E2E テストはどうしても時間がかかりやすく、コストもかかります。heading タグのズレもそれほど頻繁に起こるというものでもおそらくないですね。

いつの間にかずれていて数ヶ月経ってしまったということを防ぐために、数日に 1 回ほど回すか、ローカルで気になったときに実行するものとして作ります。

Playwright の toMatchAriaSnapshot を活用し、heading のレベル構造を保存・検証する

Playwright には toMatchAriaSnapshot という関数があります。

今回の要件だと必ず toMatchAriaSnapshot を使った方がいいというわけではないのですが、テキストとタグレベルだけを出力してくれて便利なので活用します。

この関数は v1.49 と v1.50 で追加されたもので、アクセシビリティスナップショットとの一致を確認することができるアサーション関数です。

この toMatchAriaSnapshot は引数にファイル名を指定することもでき、npx playwright test --update-snapshots のコマンドを実行することで、アクセシビリティツリーの保存を指定したファイルに保存することができ、npx playwright test の実行でそのファイルとの一致チェックをすることができます。

heading タグのレベル構造がファイルに保存されれば、変更内容を容易に確認できるようになりますね。

それではコードを用意します。

import { test, expect } from "@playwright/test";

// heading タグ構造をチェックしたいページの一覧
const targetPages = [
  {
    url: 'your-app-url.com/page1'
    pageName: 'page1'
  },
  {
    url: 'your-app-url.com/page2'
    pageName: 'page2'
  },
]

test.describe("headingタグ構造をスナップショットで比較する", () => {
  for (const targetPage of targetPages) {
    test(`${targetPage.pageName}のheading タグレベルチェック`, async ({ page }) => {
      await page.goto(targetPage.url);
      
      // heading のクローンを詰め込んだラッパー要素を一時的に作成して body に追加
      await page.evaluate(() => {
        const wrapper = document.createElement('div');
        wrapper.id = 'combined-headings';
        const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
        headings.forEach((heading) => wrapper.appendChild(heading.cloneNode(true)));
        document.body.appendChild(wrapper);
      });

      const combinedHeadings = page.locator('#combined-headings');
      // `npx playwright test --update-snapshots` の場合、headings-snapshot-${targetPage.pageName}.yml に保存
      // `npx playwright test` の場合、headings-snapshot-${targetPage.pageName}.yml と一致確認
      await expect(combinedHeadings).toMatchAriaSnapshot({ name: `headings-snapshot-${targetPage.name}.yml` });
    });
  }
});

これで npx playwright test --update-snapshots コマンドを実行すると headings-snapshot-page1.yml や headings-snapshot-page2.yml が自動作成されタグレベルも表示されます。

- heading "heading 1 sample text" [level=1]
- heading "New heading 2 sample text" [level=2]
- heading "heading 2 sample text" [level=2]
- heading "heading 3 sample text" [level=3]
- heading "heading 3 sample text" [level=3]
    - ...

一度確認してコミットしてしまえば、変更があったときに差分の確認ができるので、heading タグのレベル構造が変更されているかを容易に確認できるようになりましたね。

これで heading タグの構造の一覧を出力してテストもできるようになりました。

ただ、toMatchAriaSnapshot を活用だけでは簡易的にタグレベルのチェックをすることはできるのですが、ネスト構造までは考慮することはできません。そちらは DOM のスナップショットの方が適していそうです。

また後日、別の記事にして公開をしようかなと思っています。

Discussion