ボックスモデルの要約による簡易 VRT を作ってみた - @mizchi/visc
「動いてるのがわかっている状態」からサンプリングして描画の揺れ幅を計測し、それを元に自動で差分テストを行うツールを書いてみました。
とにかくレンダリングの崩れを自動的に検出したい!でもそのためのE2Eテストは書きたくない!というときに使う想定です。
$ npx -y @mizchi/visc render https://example.com
# 目視確認用 SVG を出力
注意: 自分がCSSを修正に使う用、兼PoCです。ヒューリスティックなアルゴリズムスコアを多く含むので、アップデートすると出力はたぶん安定しません。
目視用のSVG出力例です。
これは何
- 視覚的なボックスモデルの diff をとって、差分を検出する
- 内部ロジック
- puppeteer で指定された URL をクロールして、全部の DOM に BoundingRect を付与して抽出する
- 座標のボックスモデルで(ヒューリスティックに)近似して、VisualNodeGroup という単位に要約する
- 2つの VisualNodeGroup[] の距離を計算する
- 視覚的に近いものが存在する
- 同じアクセシビリティノードを含む(木構造の編集距離)
- 同じテキストを含む(レーベンシュタイン距離)
- AI は直接データを見ればよいが、人間が確認できるように svg で diff を出力する
可視化部分は pixelmatch を参考にしています。
なぜ作ったか
開発中の手動テストや目視確認では、だいたいJSが壊れて何も表示されないか、CSSのボックスモデルが崩れて激しくズレるのを見て修正します。これは機械的に検出できるはず。
本来、これを確認するために、VRTがあるんですが、まじめなE2E VRTを書くのは大変です。
というわけで、とりあえず動いてるのがわかっている状態で複数回データを取って自動でキャリブレーションして、自動的にレンダリング誤差を丸めた設定を作るようにしました。
$ npx -y @mizchi/visc calibrate https://zenn.dev
{
"url": "https://zenn.dev",
"settings": {
"positionTolerance": 2,
"sizeTolerance": 5,
"textSimilarityThreshold": 0.95,
"importanceThreshold": 10
},
"calibration": {
"timestamp": "2025-08-05T09:11:19.097Z",
"sampleCount": 5,
"confidence": 75,
"stats": {
"avgPositionVariance": 0,
"avgSizeVariance": 0,
"avgTextSimilarity": 0.95,
"stableElementRatio": 1
},
"viewport": {
"width": 1280,
"height": 800
}
}
}
中で puppeteer が動いてLCPを待ってからクローリングという手順なので、プロセス的にはブラウザと同等で軽量ではないんですが、無設定で動くのが良いです。
最終的に比較に使うサマリの構造が小さく、目視用の軽量な SVG 出力できるのが便利です。
CLI でマトリクスでテストする
設定ファイルを置いて、URLxViewportのマトリクスでテストします。
{
"version": "1.0",
"viewports": {
"mobile": {
"name": "Mobile",
"width": 375,
"height": 667,
"deviceScaleFactor": 2
},
"desktop": {
"name": "Desktop",
"width": 1280,
"height": 800,
"deviceScaleFactor": 1
}
},
"testCases": [
{
"id": "home",
"url": "https://example.com",
"description": "Homepage"
}
]
}
初回実行は自動キャリブレーションのためのサンプリングが行われます。
$ npx @mizchi/visc check -p 2
🚀 Visual Check
📋 Config loaded: 1 test cases × 2 viewports
📥 Capture Phase (parallel: 2)
[██████████████████████████████] 2/2 100%
✅ Captured 2 layouts
📊 Compare Phase
Capturing current layouts (parallel: 2)...
[██████████████████████████████] 2/2 100%
✅ Captured 2 current layouts
Comparing layouts...
[██████████████████████████████] 2/2 100% home 1280x800 (100%)
✅ Compared 2 layouts
home:
Homepage
✅ 375x667: 100.0% similar
✅ 1280x800: 100.0% similar
コア機能
...というのは自分が今の仕事で必要としたから作ったもので、各種計算のコア部分は使い回せるに関数単位で切り出してあります。
import puppeteer from 'puppeteer';
import { promises as fs } from 'fs';
import {
fetchRawLayoutData,
extractLayoutTree,
compareLayoutTrees,
renderLayoutToSvg,
renderComparisonToSvg
} from '@mizchi/visc';
// Launch browser and capture layout
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
// Fetch and extract layout
const rawData = await fetchRawLayoutData(page);
const layout = await extractLayoutTree(rawData, {
viewportOnly: true,
groupingThreshold: 20,
importanceThreshold: 10
});
// Render layout as SVG
const svg = renderLayoutToSvg(layout, {
showLabels: true,
highlightLevel: 'moderate'
});
// Compare two layouts
const comparison = compareLayoutTrees(baselineLayout, currentLayout, {
threshold: 5,
ignoreText: true
});
// Render comparison as diff SVG
const diffSvg = renderComparisonToSvg(comparison, baselineLayout, currentLayout, {
showLabels: true,
highlightLevel: 'moderate'
});
// Save diff SVG to file
await fs.writeFile('diff.svg', diffSvg);
await browser.close();
どういうときに使う?
自分が想定しているシナリオは
- ローカル環境で
http://localhost:3000
等で手動チェックが済んでいる状態でキャリブレーション - css や JS を書きかえながら、自動で visc check を回して、レイアウト崩れを検知する
- 壊れたら
.visc/output/<id>/diff-<width>x<height>.svg
のリンクが表示されるので、目視で確認
CSS変更やレイアウト調整の影響を素早く確認したい時に便利かも程度。もうちょっと使い込みたいです
Discussion