ホーム自己紹介ブログ
NO.254
DATE2026. 02. 10

CSSを、Vitestでテストしてみる

以下の記事で書いた CSSをテストする方法について、試してみました。

CSS Layout Testing というテスト手法の提案
zenn.dev

ソースコードは、以下に置いています。

GitHub - silverbirder/css-testing
Contribute to silverbirder/css-testing development by creating an account on GitHub.
github.com

検証ページは、以下のURLです。

CSS AI - AI駆動CSSスタイリングプラットフォーム
learn-layout.vercel.app

何をテストするか

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 で要素の寸法を取得できる
    • Element: getBoundingClientRect() メソッド - Web API | MDN - developer.mozilla.org
  • elementFromPoint で前面に出ている要素を検出できる
    • Document: elementFromPoint() メソッド - Web API | MDN - developer.mozilla.org

加えて、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に加えて、テストコードも加えると品質向上の一助になるかと思います。
ご参考にしてください。

フロントエンド
テスト

-

コメント

※ ログイン不要で投稿できます。

※ 同じブラウザから投稿を削除できます。

0

読み込み中...

記事をシェアする

筆者をフォローする

ブログの読者になる

次のページ

久々に本を読んでよかったこと

前のページ

silverbirder という名前の由来

関連する記事

タグ「フロントエンド」の記事

AIの書いたコードの手直しを減らすお作法

AI にコードを書かせた後、余計なコードを見つけて消す作業があります。 不毛なことなので、それらの作業を減らすためのお作法を紹介します。 未使用コードを消す 以下でも書きましたが、未使用コードの検査に knip を使うことが多いです。 ht

2026年02月25日

AI
フロントエンド
iframeの難しさ

最近、iframeを使っています。 クライアントサイドで埋め込む想定で、iframeを使おうとしています。 色々と苦労したことがあったので、書いて残しておこうと思います。 レスポンスヘッダー 前提として、ウェブアプリケーションをプロダクショ

2026年02月18日

フロントエンド
ブラウザ
SVGを書くと数学の知識が必要だった

紙を積んだイラストをSVGで書こうとしていました。 (当たり前ですが)図形を表現するためには数学の知識が必要で、学生の頃の記憶を思い出したので疲れました。 所感について、諸々書こうと思います。 成果物 実際に完成したのは、以下の画像ができま

2026年02月17日

フロントエンド

タグ「テスト」の記事

CSS Layout Testing というテスト手法の提案

Web のフロントエンド実装において、次のようなミスによってデザイン崩れを起こしてしまったことはありませんか。 flex-shrink の指定を忘れて、要素が押しつぶされてしまった z-index の指定を間違えて、要素が意図せず前面(また

2026年01月10日

フロントエンド
テスト
単体テストを全通り書くんじゃない!

AIの進化によって、プロダクションコードに対するテストコードは、以前と比べて格段に書きやすくなったと感じています。 単体テストに関する基本的なお作法については、以前に以下の記事で整理しました。 興味があれば、参考として読んでもらえると嬉しい

2026年01月09日

テスト
Playwright の POM を Storybook 上で確認してから E2E テストを書く

はじめに Playwright で E2E テストを書く際、playwright codegen や、近年では Playwright MCP を利用して、テストコードの雛形を作成することが多いと思います。 ただし、生成したテストコードが正し

2025年12月26日

テスト
← ブログ一覧へ