以下の記事で書いた CSSをテストする方法について、試してみました。
ソースコードは、以下に置いています。
検証ページは、以下のURLです。
CSSを書いていて、以下のミスをしたことはありませんか?
flex-shrink の指定を忘れて、要素が押しつぶされてしまったz-index の指定を間違えて、要素が意図せず前面(または背面)に表示されてしまったobject-fit の指定を間違えて、画像の一部が欠損してしまったミスした場合、誤りを修正してリリースすると思います。
そのミスが再発しないように、テストコードを書きたくありませんか?
CSSの場合、見た目に関するテストというと、Visual Regression Testing(以下、VRT)が挙げられます。
対象のUIの画像を保存しておき、変更後のUIの画像と比較して差分を出し、意図するものか確認する。
これでも問題の検知はできるのですが、もう少し詳細な検証が欲しくなる場合があります。
特定のデータを入れた場合や特定のビューポートを設定した場合 など、さまざまな条件においてのテストが書きたいかと思います。
Vitest Browser Mode を使います。
それを使うことで、テスト実行環境がPlaywrightなどブラウザ上で動かすこと ができます。
JSDOMのような仮想的なDOMではなく、Playwrightのような本物に近いブラウザ環境でテストを動かすことが重要です。
Playwright上でコンポーネントを表示すれば、テスト上で要素のスタイル情報を正確に取得できます。
さらに、Web APIも使ってテストコードを書くことができます。
getBoundingClientRect で要素の寸法を取得できる
elementFromPoint で前面に出ている要素を検出できる
加えて、Playwrightを動かす際にビューポートを指定することができます。
モバイル端末のようなビューポートが狭い状態の中で発生するデザイン崩れ も、テストコードを書くことができます。
では、具体的なテストコードを2つ紹介します。
1つ目は、"fixedなグローバルヘッダーの上に画像が前面に出てしまった不具合" を守るためのテストコードです。
以下のテストは、https://learn-layout.vercel.app にあるグローバルヘッダーがデモ画像より上に来ていることをテストしています。
import { expect, test } from "vitest";
import { page } from "vitest/browser";
import { render } from "vitest-browser-react";
import { CssAiPage } from "./CssAiPage";
test("global header stays above demo image when they overlap", async () => {
// Arrange
await page.viewport(1200, 800);
await render(<CssAiPage />);
const header = document.querySelector<HTMLElement>(".fixed-header");
const demoImage = document.querySelector<HTMLElement>(".demo-image");
if (!header || !demoImage) {
throw new Error("header or demo image not found");
}
// Act
document.documentElement.style.scrollBehavior = "auto";
const imgAbsTop = demoImage.getBoundingClientRect().top;
window.scrollTo({ top: imgAbsTop });
await new Promise(requestAnimationFrame);
// Assert
const imgRect = demoImage.getBoundingClientRect();
const pointX = Math.round(imgRect.left);
const pointY = Math.round(imgRect.top);
const hit = document.elementFromPoint(pointX, pointY);
expect(hit?.closest(".fixed-header")).toBe(header);
});上記のコードは、"デモ画像までスクロールして、デモ画像の表示位置のX,Y座標の elementFromPoint が グローバルヘッダーである" ことをテストしています。
デモ画像に z-index: 99999 を指定するとデモ画像がグローバルヘッダーより上になるため、エラーとなります。
2つ目は、"モバイルとデスクトップで表示する位置関係が変わる" ことを守るテストコードです。
以下のテストは、https://learn-layout.vercel.app にあるデモセクションの左右にあるコンテンツ(デモ動画とデモ手順)がモバイルとデスクトップで以下の状態であることをテストします。
import { page } from "vitest/browser";
import { expect, test } from "vitest";
import { render } from "vitest-browser-react";
import { CssAiDemo } from "./CssAiDemo";
const setViewport = async (width: number, height = 800) => {
await page.viewport(width, height);
};
const getRects = () => {
const video = document.querySelector<HTMLElement>(".sidebar-video");
const steps = document.querySelector<HTMLElement>(".sidebar-steps");
if (!video || !steps) {
throw new Error("sidebar blocks not found");
}
return {
video: video.getBoundingClientRect(),
steps: steps.getBoundingClientRect(),
};
};
test("positions demo blocks for desktop viewport", async () => {
// Arrange
await setViewport(1200);
// Act
await render(<CssAiDemo />);
// Assert
const { video, steps } = getRects();
expect(video.left).toBeLessThan(steps.left);
expect(video.top).toBe(steps.top);
});
test("positions demo blocks for mobile viewport", async () => {
// Arrange
await setViewport(375);
// Act
await render(<CssAiDemo />);
// Assert
const { video, steps } = getRects();
expect(video.top).toBeLessThan(steps.top);
});どちらのテストも、ビューポートを最初に設定し、コンポーネントを描画します。
そして、大事なのが 各要素を getBoundingClientRect を使って 要素のビューポートからの相対位置を取得します。
位置関係の情報を使って、どちらが左側にあるか、どちらが下側にあるか ということを検証します。
今回は簡単な2つのテストケースを紹介しました。
プロダクションコードでは、reflowの懸念があるので getBoundingClientRect などは 比較的使うことを躊躇います。
しかし、テストコードでは そのような躊躇いは不要です。
getBoundingClientRect だけでなく、innerHeight や scrollY といったものもテストコードで使えます。
テストのバリエーションは広がるのではないでしょうか?
昨今、さまざまなビューポートでの動作確認に加え、与えられるデータのバリエーションやボリュームも多様になってきました。
CSSのリンターやVRTに加えて、テストコードも加えると品質向上の一助になるかと思います。
ご参考にしてください。
-
タグ「フロントエンド」の記事
最近、ヒューマンインターフェース ガイドライン(HIG)という言葉を知りました。 「ヒューマンインターフェイスガイドライン」には、どのAppleプラットフォームでも優れた体験を設計できるようにするためのガイドとベストプラクティスが含まれてい
2026-01-24
主にWeb関連の個人開発をしている際に心がけていることを書きます。 月末に近づくにつれ、AIの利用上限に達してしまうことがあります。 その状況になった時、以下のいずれかの選択肢が私の中では残っています。 課金して利用上限を増やす 無料モデル
個人サイトをリニューアルをしています。 ノート風のデザインを目指して、スタイルを調整していました。 ノートの見た目は、現実にあるノートを再現しようとCSSを書いていました。 現在、以下の画像のようなノートになっています。 ノート風デザインの
2026-01-20
タグ「テスト」の記事
Web のフロントエンド実装において、次のようなミスによってデザイン崩れを起こしてしまったことはありませんか。 flex-shrink の指定を忘れて、要素が押しつぶされてしまった z-index の指定を間違えて、要素が意図せず前面(また
AIの進化によって、プロダクションコードに対するテストコードは、以前と比べて格段に書きやすくなったと感じています。 単体テストに関する基本的なお作法については、以前に以下の記事で整理しました。 興味があれば、参考として読んでもらえると嬉しい
2026-01-09
はじめに Playwright で E2E テストを書く際、playwright codegen や、近年では Playwright MCP を利用して、テストコードの雛形を作成することが多いと思います。 ただし、生成したテストコードが正し
2025-12-26