🎭

【Playwright】ページ内リンクをクリックした時に正しくスクロールされることを検証する

2023/02/23に公開

はじめに

先日、Playwright v1.31.0 がリリースされました🎉
https://github.com/microsoft/playwright/releases/tag/v1.31.0

追加された新機能の中で特に気になったのは toBeInViewport アサーションです。これを使うことで指定した要素がビューポート内に含まれているかを検証できるようになります。便利そうだなと思いつつ、具体的なユースケースが思いつかなかったので関連する PR などを眺めていたところ、下記の Issue のコメントに様々なユースケースが列挙されていました。
https://github.com/microsoft/playwright/issues/8740#issuecomment-914402337

中でも「ページ内リンクを押した時に正しくスクロールされていることを検証」はテストしたいケースがありそう!となったので、本記事では toBeInViewport アサーションを使ってページ内リンクを押した時に正しくスクロールされていることを検証するテストコード を追加してみます。

サンプルアプリを作りテストできる状態にする

もし、手元で確認するより先に toBeInViewport アサーションを使ったテストコードが見たい...という方がいればこちらから飛べます!

動作確認した環境

  • React v18.2.0
  • Vite v4.1.4
  • Playwright v1.31.0

サンプルアプリのセットアップ

まずは toBeInViewport アサーションを検証するためのサンプルアプリ(Vite と React)を作ります。

$ yarn create vite

...

✔ Project name: … vite-project
✔ Select a framework: › React
✔ Select a variant: › TypeScript + SWC

...

念の為、動作確認しておきましょう。

$ yarn dev

http://localhost:5173/ を開いて下記のように表示されれば OK です。

Playwright のセットアップ

次は Playwright です。

$ yarn create playwright

...

✔ Where to put your end-to-end tests? · tests
✔ Add a GitHub Actions workflow? (y/N) · false
✔ Install Playwright browsers (can be done manually via 'yarn playwright install')? (Y/n) · true

...

テストを実行する npm scripts も追加しておきます。

package.json
...
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
+   "e2e:chromium": "playwright test --project=chromium"
  },
...

この状態でテストを流して pass すれば OK です。

$ yarn e2e:chromium

Running 2 tests using 2 workers
  2 passed (3.0s)
To open last HTML report run:
  npx playwright show-report
✨  Done in 3.72s.

ページ内リンクを実装する

今回は React Anchor Link Smooth Scrollを使って実装していきます。

$ yarn add react-anchor-link-smooth-scroll

次にページ内リンクを入れていきます。
また、ページ内リンクのテストをしやすくするため、ファーストビューに対象の要素が含まれないようにしています。

src/App.tsx
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import './App.css'
+ import AnchorLink from 'react-anchor-link-smooth-scroll'

function App() {
  const [count, setCount] = useState(0)

  return (
    <div className="App">
      <div>
        <a href="https://vitejs.dev" target="_blank">
          <img src="/vite.svg" className="logo" alt="Vite logo" />
        </a>
        <a href="https://reactjs.org" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>
        <p>
          Edit <code>src/App.tsx</code> and save to test HMR
        </p>
      </div>
+     <AnchorLink href='#target' data-testid="scroll">#targetまでスクロール</AnchorLink>
+     {[...Array(30)].map((_, i) => <p>ダミーテキスト</p>)}
+     {/* 後ほど下記の要素までスクロールしているかテストしたい */}
+     <h2 id='target' data-testid="target">ターゲット</h2>
    </div>
  )
}

export default App

下記のようにファーストビューに『ターゲット』が含まれていないかつ、『targetまでスクロール』を押した時に『ターゲット』までスクロールされれば準備OKです。

toBeInViewportアサーションを使ってテストする

今回は toBeInViewport アサーションを使って下記の振る舞いをテストしてみます。

  1. ファーストビューに『ターゲット』が含まれていないこと
  2. ページ内リンクをクリックすると、『ターゲット』までスクロールされること

まずは空の spec ファイルを追加しましょう。

$ touch src/tests/SampleToBeInViewport.spec.ts

次に上記を確認するテストを追加します。

src/tests/SampleToBeInViewport.spec.ts
import { test, expect } from '@playwright/test';

test.beforeEach(async ({ page }) => {
  await page.goto('http://localhost:5173');
});

test('ファーストビューに『ターゲット』が含まれていないこと', async ({ page }) => {
  // data-testid 属性が target な要素がビューポート内に含まれていないことを確認
  await expect(page.locator('data-testid=target')).not.toBeInViewport();
});

test('ページ内リンクをクリックすると、『ターゲット』までスクロールされること', async ({ page }) => {
  await page.locator('data-testid=scroll').click()
  // data-testid 属性が target な要素がビューポート内に含まれていないことを確認
  await expect(page.locator('data-testid=target')).toBeInViewport();
});

この状態でテストを流してみます。

$ yarn e2e:chromium sandbox/vite-project/tests/SampleToBeInViewport.spec.ts

Running 2 tests using 2 workers
[1/2] …Viewport.spec.ts:12:1 › ページ内リンクをクリックすると、『
[2/2] …mpleToBeInViewport.spec.ts:7:1 › ファーストビューに『ターゲ
  2 passed (1.6s)
To open last HTML report run:
  npx playwright show-report
✨  Done in 2.49s.

pass していますね🙌
これだけでは味気ないのでページ内リンクが壊れるケースも試してみます。
ここでは、id 属性の値は変えたものの、href 属性で指定している値の更新が漏れていた...という想定でテストを書き換えてみます。

src/App.tsx
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import './App.css'
import AnchorLink from 'react-anchor-link-smooth-scroll'

function App() {
  const [count, setCount] = useState(0)

  return (
    <div className="App">
      <div>
        <a href="https://vitejs.dev" target="_blank">
          <img src="/vite.svg" className="logo" alt="Vite logo" />
        </a>
        <a href="https://reactjs.org" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>
        <p>
          Edit <code>src/App.tsx</code> and save to test HMR
        </p>
      </div>
      <AnchorLink href='#target' data-testid="scroll">#targetまでスクロール</AnchorLink>
      {[...Array(30)].map((_, i) => <p>ダミーテキスト</p>)}
      {/* のちほど下記の要素までスクロールしているかテストしたい */}
-     <h2 id='target' data-testid="target">ターゲット</h2>
+     <h2 id='target2' data-testid="target">ターゲット</h2>
    </div>
  )
}

export default App

この状態でテストを流してみます。

$ yarn e2e:chromium sandbox/vite-project/tests/SampleToBeInViewport.spec.ts
Running 2 tests using 2 workers
[1/2] …mpleToBeInViewport.spec.ts:7:1 › ファーストビューに『ターゲ
[2/2] …Viewport.spec.ts:12:1 › ページ内リンクをクリックすると、『
  1) [chromium] › SampleToBeInViewport.spec.ts:12:1 › ページ内リンクをクリックすると、『ターゲット』までスクロールされること 

    Error: expect(received).toBeInViewport()
    Call log:
      - expect.toBeInViewport with timeout 5000ms
      - waiting for locator('data-testid=target')
      -   locator resolved to <h2 id="target2" data-testid="target">ターゲット</h2>
      -   unexpected value "viewport ratio 0"
      - waiting for locator('data-testid=target')
      -   locator resolved to <h2 id="target2" data-testid="target">ターゲット</h2>
      -   unexpected value "viewport ratio 0"
      ...

      14 |   await page.locator('data-testid=scroll').click();
      15 |   // data-testid 属性が target な要素がビューポート内に含まれていることを確認
    > 16 |   await expect(page.locator('data-testid=target')).toBeInViewport();
         |                                                    ^
      17 | });
      18 |

  1 failed
    [chromium] › SampleToBeInViewport.spec.ts:12:1 › ページ内リンクをクリックすると、『ターゲット』までスクロールされること 
  1 passed (5.7s)

期待通りエラーになっているので、ページ内リンクが壊れた時に検知できそうです🎉

終わりに

今回は toBeInViewport アサーションを使ってページ内リンクの動作を確認するテストを追加してみました。今回は toBeInViewport アサーションのオプションまでは触れませんでしたが、ratio を使えば該当要素が少しでもビューポートに含まれていればOKとするといった検証もできるので気になる方がいれば見てみると面白そうです。
https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-be-in-viewport-option-ratio

参考

https://github.com/microsoft/playwright/issues/8740
https://playwright.dev/docs/api/class-locatorassertions

Discussion