こんにちは。小学生向けのニュースサイト、こどもニュースをつくっています。
このサイトには、表示設定を変えたときにスクロール位置を保つ処理があります。文字を大きくすると全体の高さが変わるので、そのままだと読んでいた場所が画面外へ飛びます。読んでいた記事の先頭に合わせ直す、ということをしています。
これを E2E で検証していたのですが、間欠的に落ちました。落ちるときは、期待した座標より 78px 下にありました。
78px を探す
まず疑ったのは、合わせ直す処理そのものです。ヘッダの高さぶんを引き忘れているのではないか、と考えました。ヘッダの高さは 78px ではなかったので、これは外れでした。
次に、レイアウトの何かが 78px なのだろうと考えて、要素の高さを片端から測りました。当たったのは、記事の一覧の上に出る「つかいかた」の案内でした。
この案内は、初回訪問の読者にだけ出ます。判定はブラウザ側でしか行えない(cookie を見る)ので、React がマウントされてから表示されます。つまり、初期 HTML には無く、少し遅れて挿入されます。
テストは、初期 HTML の時点で座標を測っていました。その後に案内が挿入されて、以降の要素が 78px 押し下げられていました。速いときは測る前に挿入が終わっていて、遅いときは間に合いません。間欠的に落ちる理由もこれで説明が付きます。
待ち方を用意する
「案内が出るまで待つ」でも直りますが、それだと別の遅延要素が増えたときにまた落ちます。実際、Web フォントの読み込みでも同じずれが起きていました。
そこで、対象そのものの座標が安定するまで待つヘルパを用意しました。
/** locator の boundingBox が 250ms 間隔で 2 回連続一致するまで待って返す。
* マウント後に現れる要素(「つかいかた」Alert 等)や Web フォント遅延ロードの
* リフローで位置がずれる要素を、レイアウト確定後に計測するためのガード。 */
export async function waitForStableBox(locator: Locator): Promise<{ x: number; y: number; width: number; height: number }> {
let prev = '';
await expect
.poll(async () => {
const box = await locator.boundingBox();
const cur = JSON.stringify(box);
const stable = box !== null && cur === prev;
prev = cur;
return stable;
}, { message: 'boundingBox が安定しない', intervals: Array.from({ length: 40 }, () => 250), timeout: 10_000 })
.toBe(true);
…
}
250ms 間隔で位置を測り、2 回続けて同じなら確定とみなします。何が座標を動かしているかを知らなくても待てるのが利点です。
これは厳密な保証ではありません。250ms より遅れて動く要素があれば、すり抜けます。それでも「特定の要素を名指しで待つ」よりは壊れにくい形になりました。原因を 1 つずつ潰していくと、新しい遅延要素が入るたびにテストを直すことになります。
待ちを入れる場所を選ぶ
ただし、これを全部の測定に入れると遅くなります。250ms × 2 回で最低 500ms かかります。
なので、座標を測るテストにだけ使っています。要素の存在や文言を見るテストには要りません。Playwright の通常のアサーションは自動で待つので、そちらに任せます。
「待ちのヘルパを作ったから全部に入れる」をやると、スイート全体が遅くなって、そのうち誰かが外します。使う場所を絞るところまで含めて設計だと思います。
固定の sleep を入れない
いちばん簡単な対処は waitForTimeout(500) を挟むことです。やりませんでした。
固定の待ちは、遅いマシンでは足りず、速いマシンでは無駄になります。しかも「なぜ 500ms なのか」が誰にも分からなくなり、失敗するたびに数字が増えていきます。
待つのは時間ではなく状態にします。この場合の状態は「座標が動かなくなったこと」でした。
一般化できること
E2E で座標を検証するときは、測る瞬間にレイアウトが確定しているかを常に疑う必要があります。とくに次のものが遅れて効きます。
- クライアント側の判定でだけ現れる要素(初回訪問の案内、ログイン状態による表示)
- Web フォントの読み込み
- 画像の読み込み(サイズを指定していない場合)
- アニメーションの終了
このサイトでは、画像には幅と高さの属性を必ず入れているので、画像によるずれは起きません。指定していれば、読み込み前から場所が確保されます。E2E の安定性のためというより、実際の読者にとってもそのほうがよいので、こちらは実装側で対処しました。
テスト側で待つか、実装側でずれないようにするかです。選べるなら後者を選びます。
まとめ
- 間欠的に座標がずれるときは、マウント後に挿入される要素を疑う
- 特定の要素を名指しで待つと、遅延要素が増えるたびに壊れる
- 対象の座標が安定するまで待つヘルパを用意すると、原因を知らなくても待てる
- 固定の sleep は入れない。待つのは時間ではなく状態
- 待ちのヘルパは座標を測るテストにだけ使う。全部に入れると遅くなって外される
- 画像に幅・高さを指定するなど、実装側でずれを無くせるならそちらを選ぶ