背景・目的
前編では、kiro-cli に playwright-cli を導入し、QuickSight ダッシュボードにアクセスして「数値が表示されているか」の確認とキャプチャ取得までを行いました。ここまでは初期表示の静的な確認が中心でした。
本記事(後編)では、残る観点、「数字が正しいか(期待値と一致するか)」「画面が崩れていないか」「フィルターをかけたら数字が変わるか」に踏み込みます。
特に「画面崩れ」の判定は悩ましく、スクリーンショットのピクセル完全一致で比較する方法は、データやフォントのわずかな違いでも失敗しやすく脆いという問題があります。
そこで本記事では、性質の異なる2つのやり方を組み合わせた「二段構え」のUI検証を設計します。機械的な合否判定を土台にして、生成AIによる目視チェックで補強する、という考え方です。あわせて、フィルタが必要な検証のために、フィルタ付きの検証用ダッシュボードを CDK で用意するところから始めます。
まとめ
| 項目 | 内容 |
|---|---|
| これは何 | QuickSight ダッシュボードのUI検証を「機械的な合否判定+AIの目視チェック」の二段構えで行う手法 |
| 何ができる | 数値の正しさ・画面崩れ・フィルタ変化を、再現性をもって検証できる。加えてAIがスクショから見た目の異常を検出できる |
| 検証用ダッシュボード | フィルタ(コントロール)付きダッシュボードを CDK で用意。データは既存データセットを再利用 |
| 第1層 |
eval で数値・要素の座標を取得し期待値と突合。ピクセル一致でなく構造で判定するため再現性が高い |
| 第2層 | スクリーンショットを生成AIに解析させ、崩れ・欠落・文字化けなど人が気づく系の異常を検出。ただし非決定的なので補助 |
| 役割分担 | 正確性・再現性は第1層、見た目の網羅性は第2層。両者を組み合わせて両立させる |
テスト設計:二段構えとは
UIテストで悩ましいのが「画面が崩れていないか」の判定です。同じ「UIをチェックする」でも、性質の違う2つのやり方があり、これを組み合わせるのが本記事の趣旨です。
| 第1層:機械的な合否判定 | 第2層:AIの目視チェック | |
|---|---|---|
| やること | プログラムで数値や座標を取り出して条件で判定する | スクリーンショットをAIに見せて「おかしい所はないか」を尋ねる |
| 例 | 「表の y座標 > フィルタの y座標」なら正常とみなす | AIが「グラフが表示されていない」と指摘する |
| 強み | 毎回同じ結果になる(再現性が高い) | 人が気づくような崩れを幅広く拾える |
| 弱み | あらかじめ想定した項目しか見ない | 回答が毎回わずかに変わる(非決定的) |
| 役割 | 合否判定の主軸 | 補助(見落としの拾い上げ) |
要は、機械的な合否判定(第1層)を土台にして、AIの目視チェック(第2層)で補強する、というだけの話です。第1層で正確性と再現性を担保し、第2層で「想定していなかった崩れ」を拾います。
実践
※本節のコマンド例・出力例は、対象ダッシュボード名・カテゴリ名・数値・アカウントIDなどをプレースホルダ(<ダッシュボード名>、<カテゴリA>、<ACCOUNT_ID>、ダミー値)に置き換えて記載します。
事前準備
前回の検証でセットアップとログイン(attach)は完了している前提です。
-
承認ダイアログをバイパスするためのトークンを取得します。拡張の status 画面(chrome-extension://<拡張ID>/status.html)を開くと「Set this environment variable to bypass the connection dialog」としてトークンが表示されます
-
環境変数にTOKENを設定します
export PLAYWRIGHT_MCP_EXTENSION_TOKEN=<TOKEN> -
QuickSightに同ブラウザでログインします
-
ログイン済みのブラウザにアタッチします
$ playwright-cli -s=qs attach --extension=chrome### Session `qs` created, attached to `chrome`. Run commands with: playwright-cli --s=qs <command> ### Page - Page URL: chrome-extension://省略 - Page Title: Welcome ### Snapshot - [Snapshot](省略)
Step 0: フィルタ付き検証用ダッシュボードを CDK で用意する
「画面が崩れていないか」「フィルターをかけたら数字が変わるか」を検証するには、フィルタ(コントロール)を持つダッシュボードが必要です。前編で使った Monthly Sales Dashboard にはフィルタが無いため、フィルタ付きのダッシュボードを CDK で新規に用意します。データは既存のデータセットを再利用します(新たにデータマートは作りません)。
-
再利用するデータセットを確認します
# データセット一覧の取得 aws quicksight list-data-sets --aws-account-id <ACCOUNT_ID> \ --query "DataSetSummaries[?contains(Name,'Sales')].[Name,DataSetId]" --output table --------------------------------------------------------- | ListDataSets | +------------------------+------------------------------+ | Monthly Sales Summary | monthly-sales-summary | +------------------------+------------------------------+ -
ID と列を確認します
# 列名の確認 aws quicksight describe-data-set --aws-account-id <ACCOUNT_ID> \ --data-set-id <DATASET_ID> \ --query "DataSet.OutputColumns[].[Name,Type]" --output table ---------------------------- | DescribeDataSet | +--------------+-----------+ | sales_month | INTEGER | | category | STRING | | region | STRING | | quantity | INTEGER | | revenue | DECIMAL | +--------------+-----------+今回のデータセットは、カテゴリ列(STRING)と売上列(DECIMAL)を持っています。このカテゴリでフィルタし、売上を集計するテーブルを作ります。
-
CDK でフィルタ付きダッシュボードを定義します。要点は、
CfnAnalysisのsheets[].filterControlsに List コントロールを、visualsに集計テーブルを置くことです。フィルタ本体はfilterGroups[].categoryFilterで定義し、コントロールから参照します// CfnAnalysis の sheets[0] 抜粋 { sheetId: 'sheet-1', name: 'Main', // フィルタコントロール(画面上部に出る複数選択リスト) filterControls: [{ list: { filterControlId: 'control-category', sourceFilterId: 'filter-category', title: 'Category', type: 'MULTI_SELECT', }, }], // 集計テーブル(数値検証の対象) visuals: [{ tableVisual: { visualId: 'visual-table-1', chartConfiguration: { fieldWells: { tableAggregatedFieldWells: { groupBy: [{ categoricalDimensionField: { fieldId: 'f-category', column: { columnName: '<カテゴリ列>', dataSetIdentifier: 'SalesDataSet' }, }}], values: [{ numericalMeasureField: { fieldId: 'f-revenue', column: { columnName: '<売上列>', dataSetIdentifier: 'SalesDataSet' }, aggregationFunction: { simpleNumericalAggregation: 'SUM' }, }}], }}, }, }, }], }
CDKの準備をする
-
フォルダを作ります
cd ~/XXXXX/XXXXX/XXXXX/playwright-cli-kiro-quicksight mkdir qs-ui-test-cdk && cd qs-ui-test-cdk cdk init app --language typescript # Welcome to your CDK TypeScript project This is a blank project for CDK development with TypeScript. The `cdk.json` file tells the CDK Toolkit how to execute your app. ## Useful commands * `npm run build` compile typescript to js * `npm run watch` watch for changes and compile * `npm run test` perform the jest unit tests * `npx cdk deploy` deploy this stack to your default AWS account/region * `npx cdk diff` compare deployed stack with current state * `npx cdk synth` emits the synthesized CloudFormation template Initializing a new git repository... -
前回作成したテンプレートをコピーします
cp ../cdk-sample/quicksight-filtered-dashboard-stack.ts lib/ -
cdk deployします
-
下記のような画面が表示されました
Step 1: 数値が正しいか(期待値との突合)
前編では「数値が表示されているか」(存在チェック)まででした。後編では一歩進めて「その数値が正しいか」を検証します。判定を再現性のあるものにするため、あらかじめ期待値を固定ファイル(expected.json)に定義しておき、実際に画面から取得した値と突き合わせます。
-
ダッシュボードを開きます
playwright-cli -s=qs click "getByRole('link', { name: 'Filtered UI Test Dashboard', exact: true })" -
テーブルの中身を確認します(数値とカテゴリがDOMに出ているか確認します)
playwright-cli -s=qs --raw eval "document.body.innerText.substring(0, 2000)" -
表示されました
"探索\nダッシュボード\n\nFiltered UI Test Dashboard\n\n元のダッシュボード\n\nXXXXXXXXX\nオリジナルにリセット\n\n作成\n\nコントロール\nCategory\nビジュアル Sales by Category の新しいデータがあります\nビジュアル Sales by Category の新しいデータがあります\nSales by Category\nClothing\n3,330,000\nElectronics\n28,788,887\nFood\n1,385,000\ncategory\nrevenue\nテーブル、Sales by Category\n\n展開する\n\n今日は何でここに来たのですか?\n\nMy Assistant\n\nFiltered UI Test Dash...\n\nエグゼクティブサマリーを教えてください\n\nどのようなデータがこのダッシュボードにありますか?\n\nView more\n使用は以下の対象となります:\nAWS 責任ある AI ポリシー" -
期待値を
expected.jsonに定義します。カテゴリ別の売上合計を、正解として書いておきます本来は、期待値は仕様からあるべきデータを用意するべきですが、今回は仕組みを確認しているので、答えをそのままコピーしています
{ "Clothing": 3330000, "Electronics": 28788887, "Food": 1385000 } -
テーブル型ビジュアルは、セルの値がすべて DOM に出ます。行ごとに「カテゴリ名」と「数値」のペアを取得します。(QuickSight のテーブルは標準の HTML テーブルではなく独自コンポーネントを使っています。innerText からパースします。)
playwright-cli -s=qs --raw eval " JSON.stringify( (document.body.innerText.match(/(Clothing|Electronics|Food)\\n([\\d,]+)/g) || []) ) " -
取得できました
"[\"Clothing\\n3,330,000\",\"Electronics\\n28,788,887\",\"Food\\n1,385,000\"]" -
次に突合します(今回、突合スクリプト「qs_value_check.sh」を作成しました。)
bash /XXXXXX/XXXXXX/XXXXXX/XXXXXX/XXXXXX/quicksight-ui-test/scripts/qs_value_check.sh \ --session qs \ --expected /XXXXXX/XXXXXX/XXXXXX/XXXXXX/XXXXXX/playwright-cli-kiro-quicksight/test-output/step1/expected.json 2>&1 === 数値突合テスト === session=qs expected=/XXXXXX/XXXXXX/XXXXXX/XXXXXX/XXXXXX/playwright-cli-kiro-quicksight/test-output/step1/expected.json PASS: "Clothing" = 3,330,000 PASS: "Electronics" = 28,788,887 PASS: "Food" = 1,385,000 === 数値検証: OK ===
Step 2: 画面が崩れていないか(第1層:レイアウト構造)
「崩れていないか」をピクセル一致で判定すると、データやフォントのわずかな差で落ちて脆くなります。そこで、要素の位置関係を数値で検証します。具体的には getBoundingClientRect で各要素の座標・サイズを取得し、期待する構造ルールを満たすかを確認します。
-
はじめに、セッションが生きているか確認します。まだ有効でした
playwright-cli -s=qs --raw eval "document.title" 2>&1 "Quick - Filtered UI Test Dashboard" -
フィルタコントロールとテーブルの座標を取得します。QuickSight は標準HTMLではないので、どういうセレクタで取れるか確認します
playwright-cli -s=qs --raw eval " JSON.stringify((() => { // テーブルタイトルから親を辿ってビジュアル全体の枠を見つける const title = Array.from(document.querySelectorAll('[aria-label]')) .find(e => e.getAttribute('aria-label').includes('Sales by Category')); if (!title) return { error: 'title not found' }; let el = title; // 親を辿って height が 200px 以上の要素を見つける(ビジュアル枠) for (let i = 0; i < 10; i++) { el = el.parentElement; if (!el) break; const r = el.getBoundingClientRect(); if (r.height > 150) return { tag: el.tagName, class: el.className.substring(0, 80), rect: r }; } return { error: 'large parent not found' }; })()) " -
座標が取得できました
- フィルタコントロール: y=85, height=14
- テーブル: y=128, height=608
"{\"tag\":\"DIV\",\"class\":\"widget-container\",\"rect\":{\"x\":18,\"y\":128,\"width\":928,\"height\":608,\"top\":128,\"right\":946,\"bottom\":736,\"left\":18}}" -
これを基にレイアウト構造チェックスクリプトを作成します
- 各要素の座標・サイズが期待値から tolerance 以内か → 位置ズレ検知
- rules の条件を満たすか → 構造崩れ検知
-
予測値を作ります
{ "filterControl": { "x": 116, "y": 85, "width": 1756, "height": 14 }, "tableVisual": { "x": 18, "y": 128, "width": 928, "height": 608 }, "tolerance": { "position": 20, "size": 50 }, "rules": [ "filterControl.y < tableVisual.y", "tableVisual.height >= 50", "tableVisual.width >= 200" ] } -
スクリプトを実行します
bash /XXXXXX/XXXXXX/XXXXXX/XXXXXX/XXXXXX/quicksight-ui-test/scripts/qs_layout_check.sh \ --session qs \ --expected /XXXXXX/XXXXXX/XXXXXX/XXXXXX/XXXXXX/playwright-cli-kiro-quicksight/test-output/step2/expected_layout.json -
結果として、全てパスしました
=== レイアウト構造チェック === session=qs expected=/XXXXXX/XXXXXX/XXXXXX/XXXXXX/XXXXXX/test-output/step2/expected_layout.json tolerance: position=±20px size=±50px viewport: 実測 1920x936 filterControl: 期待 x=116 y=85 w=1756 h=14 filterControl: 実測 x=116 y=85 w=1756 h=14 PASS: filterControl.x が許容範囲内 (差=0 <= 20) PASS: filterControl.y が許容範囲内 (差=0 <= 20) PASS: filterControl.width が許容範囲内 (差=0 <= 50) PASS: filterControl.height が許容範囲内 (差=0 <= 50) tableVisual: 期待 x=18 y=128 w=928 h=608 tableVisual: 実測 x=18 y=128 w=928 h=608 PASS: tableVisual.x が許容範囲内 (差=0 <= 20) PASS: tableVisual.y が許容範囲内 (差=0 <= 20) PASS: tableVisual.width が許容範囲内 (差=0 <= 50) PASS: tableVisual.height が許容範囲内 (差=0 <= 50) --- 構造ルール (3件) --- PASS: ルール: filterControl.y < tableVisual.y PASS: ルール: tableVisual.height >= 50 PASS: ルール: tableVisual.width >= 200 === レイアウト検証: OK ===
Step 3: 画面が崩れていないか(第2層:AIによる視覚検査)
Step 3 は「スクリーンショットを撮って、AIに視覚検査させる」です。
-
まずキャプチャを取ります
playwright-cli -s=qs screenshot --filename=/XXXXXX/XXXXXX/XXXXXX/XXXXXX/XXXXXX/playwright-cli-kiro-quicksight/test-output/step3/current.png --hires -
キャプチャが取得できたので、エージェントに画像パスを渡します
/XXXXXX/XXXXXX/XXXXXX/XXXXXX/XXXXXX/playwright-cli-kiro-quicksight/test-output/step3/current.png -
下記の結果になりました
スクリーンショットを確認しました。以下の観点でチェックします: | チェック観点 | 結果 | 詳細 | |---|---|---| | テーブルが描画されているか | ✅ OK | 「Sales by Category」テーブルが表示されている | | データ行が存在するか | ✅ OK | Clothing / Electronics / Food の3行が表示 | | 数値が表示されているか | ✅ OK | 3,330,000 / 28,788,887 / 1,385,000 | | 文字化け・NaN・undefined | ✅ OK | なし | | No data / 空表示 | ✅ OK | なし | | 要素の重なり・はみ出し | ✅ OK | なし | | フィルタコントロール表示 | ✅ OK | 「コントロール Category」が上部に見えている | | レイアウトの整列 | ✅ OK | テーブルが左寄せで正常配置 | 気づき(異常ではないが記録): - テーブルビジュアルの右半分が空白になっている(データ列が2列のみで幅が余っている状態)。これは崩れではなくデータ量に応じた正常な表示 - ダッシュボード右側も空白(ビジュアルが1つだけのため) 判定: 異常なし ✅ -
取得できたのでスクリプト化します。方針としては、スクショを撮るところまでスクリプト、判定はエージェントに任せる。とします
-
skillsのreferencesに下記を用意します
# AI 視覚検査の検査項目 スクリーンショットをエージェントに渡して視覚検査する際、以下の観点で判定すること。 ## 判定基準 各項目について OK / NG を判定し、NG の場合は具体的な箇所と内容を記載する。 ## 検査項目 | # | 観点 | OK の条件 | NG の例 | |---|------|-----------|---------| | 1 | データ描画 | テーブル・グラフにデータ行/要素が表示されている | 空白、「No data」、スピナーのまま | | 2 | 数値の妥当性 | 数値が表示されている(0 のみ・空欄でない) | 全行が 0、数値セルが空 | | 3 | 文字化け・プレースホルダ残り | ラベル・数値が正常な文字列 | `undefined`、`NaN`、`null`、`{{ }}`、文字化け | | 4 | 要素の重なり | テキストやビジュアルが重なっていない | ラベル同士が重なって読めない | | 5 | はみ出し | 要素が画面枠内に収まっている | テーブルが右端で切れている、スクロールバーが出ている | | 6 | フィルタコントロール | フィルタ UI が表示されている | フィルタが消えている、「コントロール」表示がない | | 7 | 配色・コントラスト | テキストが背景に対して読める | 白背景に白文字、極端に薄い色 | | 8 | エラー表示 | エラーメッセージが出ていない | 「ビジュアルを表示できません」等のエラーバナー | ## 判定結果のフォーマット ``` === AI視覚検査 === [1] データ描画: OK [2] 数値の妥当性: OK [3] 文字化け: OK [4] 要素の重なり: OK [5] はみ出し: OK [6] フィルタコントロール: OK [7] 配色・コントラスト: OK [8] エラー表示: OK === 視覚検査: OK(異常なし) === ``` NG がある場合: ``` === AI視覚検査 === [1] データ描画: NG — テーブルが空白(データ行が描画されていない) ... === 視覚検査: NG(1件) === ``` ## 注意事項 - この検査は非決定的(AIの判断が毎回わずかに揺れる)ため、合否判定の主軸にはしない - 第1層(数値突合・レイアウト構造チェック)が主軸、この視覚検査は補助(見落としの拾い上げ) - 判断に迷う場合は「要確認」として報告し、人に委ねる -
作成したスクリプトを実行します
bash /XXXXXX/XXXXXX/XXXXXX/XXXXXX/XXXXXX/quicksight-ui-test/scripts/qs_screenshot.sh \ --session qs \ --out /XXXXXX/XXXXXX/XXXXXX/XXXXXX/XXXXXX/playwright-cli-kiro-quicksight/test-output/step3 -
実行結果は下記のとおりです
=== スクリーンショット取得(AI視覚検査用) === session=qs output=/XXXXXX/XXXXXX/XXXXXX/XXXXXX/XXXXXX/playwright-cli-kiro-quicksight/test-output/step3/20260707-181944 [1] 全体キャプチャ OK: /XXXXXX/XXXXXX/XXXXXX/XXXXXX/XXXXXX/playwright-cli-kiro-quicksight/test-output/step3/20260707-181944/full.png (34278 bytes) [2] ページ情報記録 OK: /XXXXXX/XXXXXX/XXXXXX/XXXXXX/XXXXXX/playwright-cli-kiro-quicksight/test-output/step3/20260707-181944/metadata.json === 完了 === エージェントに以下を渡して視覚検査を依頼してください: 画像: /XXXXXX/XXXXXX/XXXXXX/XXXXXX/XXXXXX/playwright-cli-kiro-quicksight/test-output/step3/20260707-181944/full.png > スクリプト OK。次に references の検査項目に従って視覚検査します。 Reading images: /XXXXXX/XXXXXX/XXXXXX/XXXXXX/XXXXXX/playwright-cli-kiro-quicksight/test-output/step3/20260707-182834/full.png (using tool: read) ✓ Successfully read image - Completed in 0.1s > references/visual-inspection-criteria.md に従って判定します。 === AI視覚検査 === [1] データ描画: OK — テーブル「Sales by Category」に3行のデータが表示されている [2] 数値の妥当性: OK — 3,330,000 / 28,788,887 / 1,385,000 が表示 [3] 文字化け: OK — undefined/NaN/{{ }} なし [4] 要素の重なり: OK — 重なりなし [5] はみ出し: OK — 要素が画面内に収まっている [6] フィルタコントロール: OK — 「コントロール Category」が上部に表示 [7] 配色・コントラスト: OK — テキストが読める [8] エラー表示: OK — エラーメッセージなし === 視覚検査: OK(異常なし) ===
Step 4: フィルターをかけたら数字が変わるか
フィルタ(コントロール)を操作し、その前後でテーブルの数値が変化することを確認します。フィルタが実際にデータを絞り込めているかの検証です。
-
フィルタ操作前の数値を取得します
playwright-cli -s=qs --raw eval " JSON.stringify( (document.body.innerText.match(/(Clothing|Electronics|Food)\\n([\\d,]+)/g) || []).map(function(s) { var parts = s.split('\\n'); return [parts[0], parts[1]]; }) ) " -
フィルタ前は 3カテゴリが表示されています
[["Clothing","3,330,000"],["Electronics","28,788,887"],["Food","1,385,000"]] -
フィルタコントロールの構造を確認します。QuickSight のフィルタは「コントロール」ボタン → combobox → option のツリーになっています
playwright-cli -s=qs snapshot 2>&1 | grep -B2 -A10 "コントロール"- button "コントロール" [ref=f5e848] [cursor=pointer]: - heading "コントロール" [level=2] [ref=f5e850] - heading "Category" [level=5] [ref=f5e852]: - generic "Category" [ref=f5e853] -
フィルタを操作します。手順は「コントロール展開 → combobox を開く → すべて選択を解除 → 目的のカテゴリを選択 → Escape で閉じる」です
# コントロールを展開 playwright-cli -s=qs click "getByRole('button', { name: 'コントロール' })" # combobox を開く playwright-cli -s=qs click "getByRole('combobox', { name: 'Category' })" # すべて選択を解除(全チェックを外す) playwright-cli -s=qs click "getByRole('option', { name: 'すべて選択' })" # Electronics だけを選択 playwright-cli -s=qs click "getByRole('option', { name: 'Electronics' })" # ドロップダウンを閉じる playwright-cli -s=qs press Escape -
フィルタ後の数値を取得します
playwright-cli -s=qs --raw eval " JSON.stringify( (document.body.innerText.match(/(Clothing|Electronics|Food)\\n([\\d,]+)/g) || []).map(function(s) { var parts = s.split('\\n'); return [parts[0], parts[1]]; }) ) " -
Electronics の 1行だけに絞り込まれました
[["Electronics","28,788,887"]] -
前後を比較すると、フィルタが正常に動作していることが確認できました
フィルタ前: 3行(Clothing / Electronics / Food) フィルタ後: 1行(Electronics のみ) → PASS: フィルタ前後で数値が変化した -
この一連の操作をスクリプト化しました(
qs_filter_check.sh)。実行結果は下記のとおりですbash qs_filter_check.sh --session qs --filter-value Electronics=== フィルタ変化チェック === session=qs control=Category filter-value=Electronics [1] フィルタ前の数値を取得 before: [["Clothing","3,330,000"],["Electronics","28,788,887"],["Food","1,385,000"]] PASS: フィルタ前の数値を取得 [2] フィルタ操作: 'Electronics' のみに絞る PASS: フィルタ操作完了 [3] フィルタ後の数値を取得 after: [["Electronics","28,788,887"]] PASS: フィルタ後の数値を取得 [4] 前後比較 PASS: フィルタ前後で数値が変化した (3行 → 1行) PASS: フィルタ後に 'Electronics' が残っている PASS: フィルタ後は 'Electronics' のみ表示 [5] フィルタをリセット PASS: リセット後に初期状態に復帰 reset: [["Clothing","3,330,000"],["Electronics","28,788,887"],["Food","1,385,000"]] === フィルタ変化チェック: PASS=7 / FAIL=0 === OK: フィルタが正常に動作
考察
- 画面崩れの検証はピクセル一致より座標・構造ベースのほうが安定する。データやフォントの揺れに強い
- AI視覚検査は「想定外の崩れ」を拾う補助として有効。ただし非決定的なので合否判定の主軸には据えない
- QuickSight のテーブルは標準
<table>ではないためinnerTextからパースする必要がある - CDK の
filterGroupsは罠が多い(status未指定で DISABLED になる等)。フィルタ付きダッシュボードを CDK で用意しておくと検証の再現性が上がる - 4本のスクリプトを kiro-cli スキルに配置し、「UIテストして」で一連の検証が実行できるようにした
参考




