はじめに
こんにちは。トライベック株式会社の清水です。
私が所属しているサービス開発ユニット(SDU)はコーダー集団ではなく、
サービスの品質×効率×価値(顧客価値)を俯瞰して、開発現場全体を支えることが役割です。
「コードが手元になくても自然言語でブラウザテストを自動化できないか?」
今回はエンジニアにとって永遠のテーマでもある、テスト工数の削減×品質の向上に対して、
Claude in Chrome を使ったブラウザテストの効率化に挑戦した記録となります。
この記事で分かること:
- Claude in Chromeの基本的な使い方
- URLから解析レポートを生成する方法
- 解析レポートからテストケースを作成する方法
- テストを実際に実行する方法
- 試行錯誤の過程と学んだこと
この記事は「成功事例」ではなく「実践記録」です。
まだ完璧には動いていませんが、同じ課題を持つ方の参考になれば幸いです。
よろしければ、前回記事も御覧ください。
なぜ Claude in Chrome なのか
抱えていた課題
- 手動テストの非効率性:手動テストの為、品質、効率性に共に限界がある
- テストケース不足:体系的なテストケースがない為、網羅性に欠ける
- チーム共有の難しさ:Pro契約は自分だけ
検討したツール
比較検討した3つのツール:
| ツール | コスト | コード必要 | チーム共有 | 判定 |
|---|---|---|---|---|
| Claude in Chrome | Pro必須 | 不要 | △ | 個人用途で採用 |
| Playwright | 無料 | 必要 | ○ | チーム共有用 |
| Google Antigravity | 無料 | 不要 | ○ | 将来検討 |
Claude in Chromeを選んだ理由
- 既にPro契約済み - 追加コストなし
- コード不要 - 自然言語で指示できる
- 即座に実行 - セットアップ不要で今すぐ使える
- 都度確認に最適 - テストシナリオが頻繁に変わる
今回は、Claude in Chromeで探索的テストを効率化することにしました。
Playwright MCP × Claude Code も気になるのですが、今回の検証結果を以て検討。
Google Antigravityは実務利用できない為、将来的に。
方針決め
ここからプロンプトで実践していきます。モデルはSonnet 4.5で進めます。
進めていくにあたり、まずはいくつか相談することにしました。
テストしたい画面のURLを一貫して聞かれ続けています。笑
質問⓷は、JUnitのように複数機能をまとめて自動テストさせながら、他の作業も並行したいので聞きましたが、ここは特に聞く必要なかったなと思います。
感覚的にプロンプトでの指示は、1問1答(1目的:1実行)が良いかと思います。
特にコーディングを依頼する場合、複数の指示を出すと間違ったときの軌道修正が大変面倒になる傾向があると思います。
相談 or 依頼(人) → 提案(AI) → 決断(人) → 実行(AI) → 検証(人) 最初に戻る🔙
AIであっても、作業の進め方の流れは人同士のコミュニケーションと同じです。
質問➁の提案のとおり、大体の方針を以下のように進めることにしました。
工程:
Step.1 テスト画面の解析
Step.2 テストケースの作成
Step.3 テスト実行ツールの作成
Step1. テスト画面の解析
学習データに利用されたくない場合は、予め、設定>プライバシーからオプトアウト設定にしておきます。
以下のような指示文を出します。
以下の開発中の画面を解析してください。
初期設定:http://localhost:8080/test1
編集エディタ:http://localhost:8080/test2
ログインが必要な場合は、ID/PWとします。
解析後は、分析レポートを出力してください。
サンプルURLはローカルですが、ローカルでなくてもアクセスできれば問題ありません。
接続がうまくいくと、ブラウザが立ち上がり、Claude in Chromeが起動します。
うまく起動しない場合は、以下をお試しください。
1. Claude for Desktopを閉じる
2. Chrome を閉じる
3. Claude for Desktopを起動
4. Chrome を起動
画面にもよると思いますが、解析が長いと途中で停止する場合があります。
解析が終わり分析レポートの生成中で何度か停止したので、その場合は再実行します。
解析が終わるとアーティファクトを作成してくれて、右側のレイアウトに表示されます。
「アーティファクトを作成しました」とあるのに、表示されない時もありました。
Claude for Desktopの不具合なのか、このあたりはよくわかりませんでした。
Step2. テストケースの作成
解析した分析レポートを元にテストケースを作成します。
どういったテストケースを作成するか相談をされます。
ログイン・認証は既にテスト済なので、いらないです。
テストケースは、前提条件、テスト手順、期待結果が欲しいです。
最終的には、テスト実行ツールで取り込みたい為、マークダウン形式にしました。
Step.3 テスト実行ツールの作成
最終的なツール画面は以下のようになりました。
以下はまだ検証中のサンプルコードとなります。ご参考までに
サンプルコード
import React, { useState } from 'react';
import { Play, RotateCcw, Download, Settings, CheckCircle, XCircle, Clock, AlertCircle, ChevronDown, ChevronRight, FileText, Upload } from 'lucide-react';
const testCasesData = [];
export default function TestTool() {
const [testCases, setTestCases] = useState([]);
const [markdownInput, setMarkdownInput] = useState('');
const [showMarkdownInput, setShowMarkdownInput] = useState(false);
const [selectedGroup, setSelectedGroup] = useState('全て');
const [expandedTests, setExpandedTests] = useState({});
const [testAccount, setTestAccount] = useState({
email: '',
password: ''
});
const [isRunning, setIsRunning] = useState(false);
const [currentTestIndex, setCurrentTestIndex] = useState(-1);
const [testResults, setTestResults] = useState([]);
const [logs, setLogs] = useState([]);
const [showSettings, setShowSettings] = useState(false);
const [settings, setSettings] = useState({
screenshotOnError: true,
screenshotOnSuccess: false,
waitTime: 1000
});
const groups = ['全て', ...Array.from(new Set(testCases.map(tc => tc.group)))];
const filteredCases = selectedGroup === '全て' ? testCases : testCases.filter(tc => tc.group === selectedGroup);
const parseMarkdown = (markdown) => {
const cases = [];
const sections = markdown.split(/(?=### TC-[A-Z]+-\d+:)/);
sections.forEach(section => {
if (!section.trim() || !section.includes('### TC-')) return;
const lines = section.split('\n').filter(line => line.trim());
// テストケースID と名前
const titleMatch = lines[0].match(/### (TC-[A-Z]+-\d+): (.+)/);
if (!titleMatch) return;
const id = titleMatch[1];
const name = titleMatch[2];
// グループ名を推測
let group = 'その他';
if (id.startsWith('TC-MOD-')) group = 'モーダル操作';
else if (id.startsWith('TC-TAB-')) group = 'タブ切り替え';
else if (id.startsWith('TC-FIL-')) group = 'フィルタリング';
else if (id.startsWith('TC-TMP-')) group = 'テンプレート選択';
else if (id.startsWith('TC-AI-')) group = 'AI生成';
else if (id.startsWith('TC-EDT-')) group = 'エディタ基本操作';
else if (id.startsWith('TC-SAV-')) group = '保存・公開';
else if (id.startsWith('TC-ERR-')) group = 'エラーシナリオ';
else if (id.startsWith('TC-E2E-')) group = 'E2Eシナリオ';
// 優先度
let priority = '中';
const priorityLine = lines.find(l => l.includes('**優先度**:'));
if (priorityLine) {
if (priorityLine.includes('高')) priority = '高';
else if (priorityLine.includes('低')) priority = '低';
}
// 前提条件
const preconditions = [];
let inPreconditions = false;
let inSteps = false;
let inExpected = false;
const steps = [];
let expectedResult = '';
let stepCount = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.includes('**前提条件**:')) {
inPreconditions = true;
inSteps = false;
inExpected = false;
continue;
}
if (line.includes('**テスト手順**:')) {
inPreconditions = false;
inSteps = true;
inExpected = false;
continue;
}
if (line.includes('**期待結果**:')) {
inPreconditions = false;
inSteps = false;
inExpected = true;
continue;
}
if (inPreconditions && line.startsWith('- ')) {
preconditions.push(line.substring(2).trim());
}
if (inSteps && line.match(/^\d+\./)) {
stepCount++;
const stepText = line.replace(/^\d+\.\s*/, '').trim();
// URLの抽出
let url = null;
const urlMatch = stepText.match(/`(https?:\/\/[^`]+)`/);
if (urlMatch) url = urlMatch[1];
// アクション部分と期待結果の分離
let action = stepText;
let expected = '';
// 次の行に期待結果があるかチェック
if (i + 1 < lines.length && !lines[i + 1].match(/^\d+\./)) {
expected = lines[i + 1].trim();
}
steps.push({
step: stepCount,
action: action.replace(/`[^`]+`/, '').trim(),
url: url,
expected: expected || 'ステップが完了する'
});
}
if (inExpected && line.startsWith('- ')) {
expectedResult += (expectedResult ? '\n' : '') + line.substring(2).trim();
} else if (inExpected && line.trim() && !line.startsWith('**') && !line.startsWith('###')) {
expectedResult += (expectedResult ? ' ' : '') + line.trim();
}
}
cases.push({
id,
name,
group,
priority,
status: 'pending',
preconditions: preconditions.length > 0 ? preconditions : ['前提条件なし'],
steps: steps.length > 0 ? steps : [{ step: 1, action: 'テスト手順を実行', expected: '正常に完了する' }],
expectedResult: expectedResult || '期待する結果が得られる'
});
});
return cases;
};
const handleLoadMarkdown = () => {
if (!markdownInput.trim()) {
addLog('マークダウンテキストを入力してください', 'error');
return;
}
try {
const parsedCases = parseMarkdown(markdownInput);
setTestCases(parsedCases);
addLog(`${parsedCases.length}件のテストケースを読み込みました`, 'success');
setShowMarkdownInput(false);
} catch (error) {
addLog(`マークダウンの解析に失敗しました: ${error.message}`, 'error');
}
};
const toggleTestExpand = (testId) => {
setExpandedTests(prev => ({ ...prev, [testId]: !prev[testId] }));
};
const addLog = (message, type = 'info') => {
const timestamp = new Date().toLocaleTimeString('ja-JP');
setLogs(prev => [...prev, { timestamp, message, type }]);
};
const handleStartTest = () => {
setIsRunning(true);
setTestResults([]);
setLogs([]);
addLog('=== テスト実行計画 ===', 'info');
addLog('', 'info');
addLog(`📋 実行対象: ${filteredCases.length}件のテストケース`, 'info');
addLog(`👤 テストアカウント: ${testAccount.email}`, 'info');
addLog(`🔑 パスワード: ${testAccount.password}`, 'info');
addLog('', 'info');
addLog('【実行するテストケース】', 'info');
filteredCases.forEach((testCase, index) => {
addLog(`${index + 1}. ${testCase.id}: ${testCase.name} (優先度: ${testCase.priority})`, 'info');
});
addLog('', 'info');
addLog('【実行手順】', 'info');
addLog('1. Claude in Chromeでブラウザタブを作成', 'info');
addLog('2. ログイン画面にアクセスしてログイン', 'info');
addLog('3. 各テストケースを順番に実行', 'info');
addLog('4. 各ステップの結果を記録', 'info');
addLog('5. エラー時はスクリーンショットを撮影', 'info');
addLog('6. テスト結果をwindow.storageに保存', 'info');
addLog('7. 完了後、「結果を同期」ボタンで結果を取得', 'info');
addLog('', 'info');
addLog('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', 'warning');
addLog('', 'warning');
addLog('✅ この内容で実行しますか?', 'warning');
addLog('', 'warning');
addLog('👉 チャット欄に以下のいずれかを入力してください:', 'warning');
addLog(' • 「はい」または「実行してください」→ テストを開始', 'warning');
addLog(' • 「いいえ」または「キャンセル」→ 実行を中止', 'warning');
addLog('', 'warning');
addLog('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', 'warning');
};
const handleSyncResults = () => {
addLog('', 'info');
addLog('=== テスト結果の同期を開始します ===', 'info');
addLog('', 'info');
addLog('Claudeからテスト結果を取得しています...', 'info');
addLog('', 'info');
addLog('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', 'warning');
addLog('', 'warning');
addLog('👉 Claudeがテスト結果を報告するのを待っています...', 'warning');
addLog('', 'warning');
addLog('結果が報告されたら自動的に反映されます', 'warning');
addLog('', 'warning');
addLog('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', 'warning');
};
const handleResetTest = () => {
setIsRunning(false);
setCurrentTestIndex(-1);
setTestResults([]);
setLogs([]);
};
const exportReport = () => {
const report = {
timestamp: new Date().toISOString(),
testAccount: testAccount.email,
totalTests: testResults.length,
passed: testResults.filter(r => r.status === 'passed').length,
failed: testResults.filter(r => r.status === 'failed').length,
results: testResults,
logs: logs
};
const dataStr = JSON.stringify(report, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `test-report-${Date.now()}.json`;
link.click();
};
const getStatusIcon = (status) => {
switch (status) {
case 'passed': return <CheckCircle className="text-green-600" size={20} />;
case 'failed': return <XCircle className="text-red-600" size={20} />;
case 'running': return <Clock className="text-blue-600 animate-spin" size={20} />;
default: return <Clock className="text-gray-400" size={20} />;
}
};
const getLogIcon = (type) => {
switch (type) {
case 'success': return <CheckCircle className="text-green-600" size={16} />;
case 'error': return <XCircle className="text-red-600" size={16} />;
case 'warning': return <AlertCircle className="text-yellow-600" size={16} />;
default: return <AlertCircle className="text-blue-600" size={16} />;
}
};
const getPriorityColor = (priority) => {
switch (priority) {
case '高': return 'text-red-600 bg-red-50';
case '中': return 'text-yellow-600 bg-yellow-50';
default: return 'text-green-600 bg-green-50';
}
};
const passedCount = testResults.filter(r => r.status === 'passed').length;
const failedCount = testResults.filter(r => r.status === 'failed').length;
const totalTests = filteredCases.length;
const progress = testResults.length > 0 ? (testResults.length / totalTests) * 100 : 0;
return (
<div className="min-h-screen bg-gray-50 p-6">
<div className="max-w-7xl mx-auto">
<div className="bg-white rounded-lg shadow-sm p-6 mb-6">
<div className="flex justify-between items-center mb-4">
<div>
<h1 className="text-3xl font-bold text-gray-900">テスト実行ツール</h1>
<p className="text-gray-600 mt-1">テストケース管理と自動実行</p>
</div>
<button onClick={() => setShowSettings(!showSettings)} className="p-2 text-gray-600 hover:bg-gray-100 rounded-lg">
<Settings size={24} />
</button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-1 space-y-6">
<div className="bg-white rounded-lg shadow-sm p-6">
<h2 className="text-xl font-bold text-gray-900 mb-4">テストアカウント</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">メールアドレス</label>
<input type="email" value={testAccount.email} onChange={(e) => setTestAccount({...testAccount, email: e.target.value})} disabled={isRunning} className="w-full px-3 py-2 border border-gray-300 rounded-lg disabled:bg-gray-100" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">パスワード</label>
<input type="password" value={testAccount.password} onChange={(e) => setTestAccount({...testAccount, password: e.target.value})} disabled={isRunning} className="w-full px-3 py-2 border border-gray-300 rounded-lg disabled:bg-gray-100" />
</div>
</div>
</div>
{showSettings && (
<div className="bg-white rounded-lg shadow-sm p-6">
<h2 className="text-xl font-bold text-gray-900 mb-4">テスト設定</h2>
<div className="space-y-3">
<label className="flex items-center gap-2">
<input type="checkbox" checked={settings.screenshotOnError} onChange={(e) => setSettings({...settings, screenshotOnError: e.target.checked})} className="w-4 h-4" />
<span className="text-sm text-gray-700">エラー時にスクリーンショット</span>
</label>
</div>
</div>
)}
<div className="bg-white rounded-lg shadow-sm p-6">
<h2 className="text-xl font-bold text-gray-900 mb-4">テスト実行</h2>
<div className="space-y-3">
<button onClick={() => setShowMarkdownInput(!showMarkdownInput)} className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition font-semibold">
<FileText size={20} />
{showMarkdownInput ? 'マークダウン入力を閉じる' : 'マークダウンから読込'}
</button>
<button onClick={handleStartTest} disabled={isRunning || testCases.length === 0} className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition font-semibold disabled:opacity-50">
<Play size={20} />
{isRunning ? 'テスト実行中...' : 'テスト開始'}
</button>
<button onClick={handleSyncResults} className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition font-semibold">
<Download size={20} />
結果を同期
</button>
<button onClick={handleResetTest} disabled={isRunning} className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition font-semibold disabled:opacity-50">
<RotateCcw size={20} />
リセット
</button>
{testResults.length > 0 && (
<button onClick={exportReport} className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition font-semibold">
<Download size={20} />
レポート出力
</button>
)}
</div>
</div>
<div className="bg-white rounded-lg shadow-sm p-6">
<h2 className="text-xl font-bold text-gray-900 mb-4">実行統計</h2>
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-gray-600">総テスト数</span>
<span className="font-semibold text-gray-900">{totalTests}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-green-600">成功</span>
<span className="font-semibold text-green-600">{passedCount}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-red-600">失敗</span>
<span className="font-semibold text-red-600">{failedCount}</span>
</div>
<div className="pt-3 border-t">
<div className="flex justify-between items-center mb-2">
<span className="text-gray-600">進捗</span>
<span className="font-semibold text-gray-900">{Math.round(progress)}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-3">
<div className="bg-blue-600 h-3 rounded-full transition-all duration-300" style={{ width: `${progress}%` }} />
</div>
</div>
</div>
</div>
</div>
<div className="lg:col-span-2 space-y-6">
{showMarkdownInput && (
<div className="bg-white rounded-lg shadow-sm p-6">
<h2 className="text-xl font-bold text-gray-900 mb-4">マークダウンテキスト入力</h2>
<p className="text-sm text-gray-600 mb-3">テストケースのマークダウンテキストを貼り付けてください</p>
<textarea value={markdownInput} onChange={(e) => setMarkdownInput(e.target.value)} className="w-full h-64 px-3 py-2 border border-gray-300 rounded-lg font-mono text-sm" placeholder="### TC-MOD-001: テストケース名
**優先度**: 高
**前提条件**:
- 条件1
- 条件2
**テスト手順**:
1. 手順1
2. 手順2
**期待結果**:
- 期待される結果" />
<div className="flex gap-3 mt-4">
<button onClick={handleLoadMarkdown} className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition font-semibold">
読み込む
</button>
<button onClick={() => setMarkdownInput('')} className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition font-semibold">
クリア
</button>
</div>
</div>
)}
<div className="bg-white rounded-lg shadow-sm p-4">
<div className="flex gap-2 overflow-x-auto">
{groups.map(group => (
<button key={group} onClick={() => setSelectedGroup(group)} className={`px-4 py-2 rounded-lg whitespace-nowrap transition ${selectedGroup === group ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}`}>
{group}
</button>
))}
</div>
</div>
<div className="bg-white rounded-lg shadow-sm p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold text-gray-900">テストケース一覧</h2>
{testCases.length === 0 && (
<span className="text-sm text-gray-500">マークダウンを読み込んでください</span>
)}
</div>
{testCases.length === 0 ? (
<div className="text-center py-12 text-gray-500">
<FileText size={48} className="mx-auto mb-4 text-gray-400" />
<p className="text-lg font-medium mb-2">テストケースが読み込まれていません</p>
<p className="text-sm">「マークダウンから読込」ボタンをクリックしてテストケースを追加してください</p>
</div>
) : (
<div className="space-y-3 max-h-[500px] overflow-y-auto">
{filteredCases.map((testCase, index) => {
const result = testResults.find(r => r.id === testCase.id);
const isCurrent = currentTestIndex === index && isRunning;
const isExpanded = expandedTests[testCase.id];
return (
<div key={testCase.id} className={`rounded-lg border transition ${isCurrent ? 'bg-blue-50 border-blue-300' : result?.status === 'passed' ? 'bg-green-50 border-green-300' : result?.status === 'failed' ? 'bg-red-50 border-red-300' : 'bg-gray-50 border-gray-200'}`}>
<div className="flex items-center gap-3 p-4 cursor-pointer hover:bg-opacity-80" onClick={() => toggleTestExpand(testCase.id)}>
<div className="flex-shrink-0">
{getStatusIcon(isCurrent ? 'running' : result?.status || 'pending')}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="font-mono text-sm font-semibold text-gray-700">{testCase.id}</span>
<span className={`px-2 py-0.5 rounded text-xs font-medium ${getPriorityColor(testCase.priority)}`}>{testCase.priority}</span>
</div>
<p className="text-sm font-semibold text-gray-900">{testCase.name}</p>
<p className="text-xs text-gray-600 mt-0.5">{testCase.group}</p>
</div>
<div className="flex-shrink-0">
{isExpanded ? <ChevronDown size={20} /> : <ChevronRight size={20} />}
</div>
</div>
{isExpanded && (
<div className="px-4 pb-4 border-t border-gray-200 pt-4 space-y-4">
<div>
<h4 className="text-sm font-semibold text-gray-900 mb-2">前提条件</h4>
<ul className="list-disc list-inside space-y-1">
{testCase.preconditions.map((condition, idx) => (
<li key={idx} className="text-sm text-gray-700">{condition}</li>
))}
</ul>
</div>
<div>
<h4 className="text-sm font-semibold text-gray-900 mb-2">テスト手順</h4>
<div className="space-y-2">
{testCase.steps.map((step) => (
<div key={step.step} className="bg-white p-3 rounded border border-gray-200">
<div className="flex gap-3">
<div className="flex-shrink-0 w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-xs font-semibold">{step.step}</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 mb-1">{step.action}</p>
{step.url && <p className="text-xs text-gray-600 mb-1"><span className="font-medium">URL:</span> {step.url}</p>}
<p className="text-xs text-green-700 mt-1"><span className="font-medium">期待:</span> {step.expected}</p>
</div>
</div>
</div>
))}
</div>
</div>
<div>
<h4 className="text-sm font-semibold text-gray-900 mb-2">期待結果</h4>
<p className="text-sm text-gray-700 bg-white p-3 rounded border border-gray-200">{testCase.expectedResult}</p>
</div>
</div>
)}
</div>
);
})}
</div>
)}
</div>
<div className="bg-white rounded-lg shadow-sm p-6">
<h2 className="text-xl font-bold text-gray-900 mb-4">実行ログ</h2>
<div className="bg-gray-900 rounded-lg p-4 max-h-96 overflow-y-auto font-mono text-sm">
{logs.length === 0 ? (
<p className="text-gray-400">ログはまだありません</p>
) : (
<div className="space-y-1">
{logs.map((log, index) => (
<div key={index} className="flex items-start gap-2">
<span className="text-gray-500 flex-shrink-0">[{log.timestamp}]</span>
<div className="flex items-start gap-2 flex-1">
{getLogIcon(log.type)}
<span className={log.type === 'success' ? 'text-green-400' : log.type === 'error' ? 'text-red-400' : log.type === 'warning' ? 'text-yellow-400' : 'text-gray-300'}>{log.message}</span>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
</div>
</div>
);
}
使い方
1. ログインに必要なメールアドレス、パスワードを入力
2. 「マークダウンから読込」ボタン押下。マークダウンテキスト入力+「読み込む」ボタン押下
3. 読み込みが完了するとテストケース一覧として表示
4. 「テスト開始」ボタンを押下すると実行ログが走る
5. チャット欄で「実行してください」を入力すると、Claude(MCP)が起動。ブラウザテストが開始。
制作フロー
基本的には自然言語のやりとり(トライ&エラーの繰り返し)で作成しています。
+------------+ +------------+
| 人 | -----> | AI |
| | 依頼 | |
+------------+ +------------+
| |
|「〇〇できますか」 |
|-------------------->|
| |
| |「できます」
|<--------------------|
| |
|「実装してください」 |
|-------------------->|
| |
| | 「できました!」
|<--------------------|
| |
|「うまく動かないな」 |
|「原因は?」 ★Point |
|-------------------->|
| |
| |「〇〇(原因)だから、できませんでした」
| | 修正しますか OR 代替案提示
| |
|<--------------------|
| |
|「修正してください」 |
| OR「代替案で」 |
|-------------------->|
AIも人間と同じように、できることをできないと言ってみたり、できないことをできると言うことがあります。
デザインや抽象的なものは別として、これは、できる or できないに至るまでの十分な判断要素が揃っていないことが主な原因で、会話の中で判断要素を増やす(原因追及)ことを人間側で意識すれば、最近のモデルであれば、たいていのものはいい感じに作ってくれると思います。
制作時のポイント
Reactアーティファクトから直接Claude for Desktopのチャット欄に自動的にメッセージを送信することはできないので、代替案として、実行計画を表示してチャットで確認を求める仕組みにしました。
同じ理由で、実行結果も、window.storageに保存して、「結果を同期」ボタンで取り込み、最終的に実行統計に表示する仕組みとしていますが、ここはまだ検証中です。
制作時ですが、以下のようなメッセージが頻繁にでました。

これはアーティファクト作成にはコンテンツサイズの制限があるそうです。
トークン数の上限とは別で、長大なコードやドキュメントを1つのアーティファクトに含めようとするとコンテンツサイズ制限に引っかかる為、今回のテスト実行ツールが完成形まで作成していない背景の一つとなります。
メモリー機能により重要な文脈は一応、新規チャットに引き継がれるようです。
検証
検証不十分ですが、傾向をまとめておきます。
トークンの上限に近づいているからか、詳細の原因はわかりませんが、テストケースは分割実行(1リクエストで3~4ケース程度)とすると、多少緩和されます。
ただ、完全自動実行で1~2時間、裏で動かしておきたいという使い方がしたいので、これでは使いどころが難しいと感じました。
トークン上限などで途中ストップすることもあり正確な回数をお伝えするのは難しいのですが、今回は2画面・約70テストケースを対象に検証してみましたが、Step.2〜3の会話フローを各工程あたり1日3〜4回実施しました。また、期間としては1〜2週間ほど要しました。画面の複雑さやテストケース数、契約プランによっても変わってくるかと思いますので、あくまで目安としてご覧ください。
まとめ
テストケースの作成中、開発中の画面解析では正しい期待値にはならないことに気付きました。(遅)
開発の進捗の度に解析、または手動でテストケースの期待値を修正することも可能ですが、新規機能ではなく、リグレッションテストのテストケース作成やリバースエンジニアリング向きかと思います。
「コードが手元になくても自然言語でブラウザテストを自動化できないか?」
については、実運用はもう少し先の未来になりそうというのが私の見解となりました。
Hirameki 7 は、Nuxt3移行を行っていますが、その際に大規模なリグレッションテストを行いました。こういった場面で使いどころがないわけではなさそうです。
Nuxt3移行については、TRITECHメンバーの記事もご覧ください。
余談
ここまでお読みいただきありがとうございました。
今回検証に使用したHirameki 7のWebサイト(CMS)機能は、2026年3月18日に「新エディター」がリリースされ、AIによるコンテンツ生成にも対応しています。本記事の検証環境もこの新エディターを使用しています。
詳細:https://www.hirameki7.io/function/marketing/site/
参考URL









