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

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

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

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

Zenn

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

GitHub - silverbirder/css-testing

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

GitHub

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

フロントエンド
テスト

-

読者になる

|

シェアする

|

silverbirders

silverbirder

Webソフトウェアエンジニア
個人ブログキュレーション こぶりー 運営

ブログを応援する

この記事がよかったら、お布施という形で応援してもらえるとうれしいです。

おふせぼたん

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

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

0

読み込み中...

次の記事へ前の記事へ

関連する記事

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

Webmention 使ってみた

Webmention というのを最近知りました。 名前の通り、Web 上でメンションすることができます。 受信側、送信側、どちらも対応したので感想を残そうと思います。 Webmention ざっくりと Web AサイトにBサイトのリンクをつ

2026年09月08日

フロントエンド
個人開発
モジュールアップデートでやっていること

ITヲタクな AI くんにピッタリな仕事の一つは、モジュールアップデート。 特に、メジャーアップデートが超助かる。 昔だったら 数ヶ月掛かった移行も、数日たらずに対応できるようになったので、 さすが ITヲタクくん。 対応してよかったフロー

2026年07月24日

AI
フロントエンド
写経テストコードを全部消した

以下で書いた通り、プロダクトコードを写経したテストコードを削除しました。 "こぶりー" ( https://kobliy.vercel.app/ ) という個人ブログを読むアプリのコードです。 https://silverbirder.gi

2026年06月08日

AI
フロントエンド
テスト

タグ「テスト」の記事

Testcontainersで開発PR単位にE2Eテストなど

Testcontainers + Playwright + Cucumber.js による受け入れテストを、公私ともに活用しています。 運用していて気づいた点について、また書き残そうと思います。 開発PR単位でE2Eテスト これはTestc

2026年09月07日

テスト
ブローガスト25日目: ミューテーションテストのキルゼロテスト

テストコードをテストする、ミューテーションテスト。 プロダクションコードを突然変異でコードを書き換えたら(ミューテーション)、テストが失敗してくれるかどうか。 失敗するということは、不具合を検知できたということです。 ミューテーションする箇

2026年08月25日

テスト
Testcontainers運用経験から分かった、良い点・困る点

お仕事先のWebアプリケーションに、Testcontainersを導入運用して数ヶ月経過しました。 良い点と困る点について、書き残しておきます。 前提 簡単な前提として、バックエンドDB・APIとフロントエンドの3層構成のよくあるWebアプ

2026年07月08日

テスト
← ブログ一覧へ