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

フロントエンド
テスト

-

シェアする

フォローする

前のページ

silverbirder という名前の由来

関連する記事

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

ヒューマンインターフェース ガイドライン という言葉を知りました。

最近、ヒューマンインターフェース ガイドライン(HIG)という言葉を知りました。 「ヒューマンインターフェイスガイドライン」には、どのAppleプラットフォームでも優れた体験を設計できるようにするためのガイドとベストプラクティスが含まれてい

2026-01-24

フロントエンド
AIの利用上限に達した時にすることを残しておく

主にWeb関連の個人開発をしている際に心がけていることを書きます。 月末に近づくにつれ、AIの利用上限に達してしまうことがあります。 その状況になった時、以下のいずれかの選択肢が私の中では残っています。 課金して利用上限を増やす 無料モデル

2026-01-22

フロントエンド
AI
CSSで頑張らなくても、SVGで楽にできるときもある

個人サイトをリニューアルをしています。 ノート風のデザインを目指して、スタイルを調整していました。 ノートの見た目は、現実にあるノートを再現しようとCSSを書いていました。 現在、以下の画像のようなノートになっています。 ノート風デザインの

2026-01-20

フロントエンド

タグ「テスト」の記事

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

テスト
← ブログ一覧へ