feat(ui): add tool-test-console and tester dashboards
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m51s
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m51s
- Add Auto-Tester Dashboard (tester.html) in gateway for batch testing tools - Add standalone Tool Test Console (tool-test-console.html) in core - Fix Tailwind CSS Preflight conflicts in console UI - Update console payload schema resolution to support both gateway and pod modes - Strip JSON-RPC metadata from tool payload output in console - Add unified navigation headers across all static HTML pages
This commit is contained in:
@@ -12,6 +12,7 @@ import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Supplier;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -21,6 +22,7 @@ import org.springframework.stereotype.Service;
|
||||
public class ToolManifestService {
|
||||
|
||||
private static final long DEFAULT_TIMEOUT_MILLIS = 300000L;
|
||||
private static final AtomicLong LAST_ISSUED_REVISION = new AtomicLong();
|
||||
private final Supplier<List<ToolMetadata>> toolSupplier;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final McpProperties properties;
|
||||
@@ -89,10 +91,9 @@ public class ToolManifestService {
|
||||
private synchronized String revision(String bundleId, List<ToolManifestItem> tools) {
|
||||
String fingerprint = fingerprint(bundleId, tools);
|
||||
if (!fingerprint.equals(lastFingerprint)) {
|
||||
long nextTimestamp = System.currentTimeMillis();
|
||||
if (lastRevision != null) {
|
||||
nextTimestamp = Math.max(nextTimestamp, Long.parseLong(lastRevision) + 1);
|
||||
}
|
||||
long localMinimum = lastRevision == null ? Long.MIN_VALUE : Long.parseLong(lastRevision) + 1;
|
||||
long nextTimestamp = LAST_ISSUED_REVISION.updateAndGet(previous ->
|
||||
Math.max(Math.max(System.currentTimeMillis(), localMinimum), previous + 1));
|
||||
lastFingerprint = fingerprint;
|
||||
lastRevision = Long.toString(nextTimestamp);
|
||||
}
|
||||
|
||||
260
dap-tool-core/src/main/resources/static/tool-test-console.html
Normal file
260
dap-tool-core/src/main/resources/static/tool-test-console.html
Normal file
@@ -0,0 +1,260 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AX HUB Tool Test Console</title>
|
||||
<style>
|
||||
:root { color-scheme: dark; --bg:#09090b; --surface:#18181b; --surface2:#27272a; --line:#3f3f46; --muted:#a1a1aa; --text:#f4f4f5; --blue:#60a5fa; --green:#34d399; --red:#fb7185; --yellow:#fbbf24; }
|
||||
* { box-sizing:border-box; } body { margin:0; background:var(--bg); color:var(--text); font-family:Arial,"Malgun Gothic",sans-serif; }
|
||||
header { border-bottom:1px solid var(--line); background:#111113; position:sticky; top:0; z-index:2; } .header-inner { max-width:1280px; min-height:64px; margin:auto; padding:0 24px; display:flex; align-items:center; justify-content:space-between; gap:16px; }
|
||||
h1 { margin:0; font-size:18px; } h2 { font-size:15px; margin:0 0 14px; } p { color:var(--muted); line-height:1.55; }
|
||||
main { max-width:1280px; margin:auto; padding:28px 24px 48px; } .intro { margin-bottom:20px; } .intro h1 { font-size:24px; margin-bottom:8px; }
|
||||
.grid { display:grid; grid-template-columns:340px minmax(0,1fr); gap:18px; } .card { background:var(--surface); border:1px solid var(--line); border-radius:12px; padding:18px; }
|
||||
label { display:block; color:var(--muted); font-size:12px; font-weight:700; margin:0 0 7px; } input, select, textarea { width:100%; border:1px solid var(--line); border-radius:8px; padding:10px 12px; background:#101012; color:var(--text); font:14px ui-monospace,SFMono-Regular,Consolas,monospace; } textarea { min-height:270px; resize:vertical; line-height:1.55; }
|
||||
select { font-family:Arial,"Malgun Gothic",sans-serif; } .stack { display:grid; gap:12px; } .buttons { display:flex; gap:8px; flex-wrap:wrap; }
|
||||
button { border:1px solid var(--line); border-radius:8px; padding:9px 12px; color:var(--text); background:var(--surface2); cursor:pointer; font-weight:700; } button:hover { border-color:var(--blue); } button.primary { background:#2563eb; border-color:#3b82f6; } button.danger { color:#fecdd3; } button:disabled { opacity:.5; cursor:not-allowed; }
|
||||
.meta { display:flex; gap:8px; flex-wrap:wrap; margin:0 0 14px; } .badge { border:1px solid var(--line); border-radius:99px; padding:4px 8px; color:var(--muted); font-size:11px; font-family:ui-monospace,monospace; } .badge.ok { color:var(--green); border-color:#166534; } .badge.fail { color:var(--red); border-color:#9f1239; }
|
||||
.notice { border-left:3px solid var(--blue); padding:10px 12px; background:#172554; color:#dbeafe; border-radius:4px; font-size:13px; margin-bottom:16px; } .hidden { display:none !important; }
|
||||
.result { background:#101012; border:1px solid var(--line); border-radius:8px; padding:14px; white-space:pre-wrap; overflow:auto; max-height:520px; font:13px/1.55 ui-monospace,SFMono-Regular,Consolas,monospace; }
|
||||
.case-list { display:grid; gap:8px; margin-top:12px; } .case-row { display:flex; align-items:center; gap:8px; padding:9px; border:1px solid var(--line); border-radius:8px; } .case-row main { padding:0; margin:0; flex:1; min-width:0; } .case-row strong,.case-row small { display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .case-row small { color:var(--muted); margin-top:3px; }
|
||||
.run-log { margin-top:12px; font:12px/1.55 ui-monospace,monospace; color:var(--muted); white-space:pre-wrap; max-height:220px; overflow:auto; }
|
||||
.footer-note { margin-top:18px; color:#71717a; font-size:12px; } @media (max-width:900px) { .grid { grid-template-columns:1fr; } .header-inner { padding:0 16px; } main { padding:20px 16px; } }
|
||||
</style>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
corePlugins: {
|
||||
preflight: false,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="min-h-screen">
|
||||
<header style="border-bottom: 1px solid #27272a; background: rgba(9,9,11,0.85); backdrop-filter: blur(16px);" class="sticky top-0 z-50">
|
||||
<div class="max-w-6xl mx-auto px-6 h-14 flex items-center justify-between">
|
||||
<div class="flex items-center space-x-5">
|
||||
<a href="/index.html" class="flex items-center group" style="text-decoration:none;">
|
||||
<div class="w-2 h-2 rounded-full mr-2" style="background:#3b82f6; box-shadow: 0 0 8px rgba(59,130,246,0.8);"></div>
|
||||
<span class="font-semibold tracking-tight text-sm" style="color:#f4f4f5;">AXHUB Gateway</span>
|
||||
</a>
|
||||
<div class="h-4 w-px" style="background:#27272a;"></div>
|
||||
<nav class="flex space-x-5 text-[13px] font-medium">
|
||||
<a href="/admin/scaffold.html" style="color:#a1a1aa; text-decoration:none;" class="hover:text-white transition-colors">Scaffold</a>
|
||||
<a href="/catalog.html" style="color:#a1a1aa; text-decoration:none;" class="hover:text-white transition-colors">Catalog</a>
|
||||
<a href="/playground.html" style="color:#a1a1aa; text-decoration:none;" class="hover:text-white transition-colors">Playground</a>
|
||||
<a href="/chat.html" style="color:#a1a1aa; text-decoration:none;" class="hover:text-white transition-colors">Chat</a>
|
||||
<a href="/tester.html" style="color:#a1a1aa; text-decoration:none;" class="hover:text-white transition-colors">Tester</a>
|
||||
<a href="/tool-test-console.html" style="color:#ffffff; text-decoration:none;" class="font-semibold">Console</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="badge" id="manifestStatus" style="border:1px solid #3f3f46; border-radius:99px; padding:4px 8px; color:#a1a1aa; font-size:11px; font-family:ui-monospace,monospace;">Manifest loading</span>
|
||||
<span class="text-[10px] uppercase tracking-widest px-2 py-1 rounded font-bold" style="background:rgba(59,130,246,0.1); color:#60a5fa; border:1px solid rgba(59,130,246,0.2);">v0.0.1</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<section class="intro"><h1>Schema 기반 Tool 테스트</h1><p>Tool을 선택하고 요청 JSON을 확인한 뒤 실행하세요. 검증한 요청은 브라우저에 저장되며, 저장된 케이스 전체를 한 번에 다시 실행할 수 있습니다.</p></section>
|
||||
<div class="grid">
|
||||
<aside class="stack">
|
||||
<section class="card stack"><h2>1. Tool 선택</h2><div><label for="filter">검색</label><input id="filter" placeholder="이름, 설명으로 검색"></div><div><label for="toolSelect">Tool</label><select id="toolSelect" size="12"></select></div><div class="meta" id="toolMeta"></div><div class="buttons"><button id="sampleButton">Schema 샘플 채우기</button><button id="reloadButton">Manifest 새로고침</button></div></section>
|
||||
<section class="card"><h2>저장된 테스트 케이스</h2><div class="buttons"><button class="primary" id="runAllButton">Run saved cases</button><button class="danger" id="clearCasesButton">전체 삭제</button></div><div id="caseList" class="case-list"></div><div id="runLog" class="run-log"></div></section>
|
||||
</aside>
|
||||
<section class="stack">
|
||||
<section class="card"><h2>2. 요청 JSON</h2><div class="notice">필수값과 형식은 Tool의 inputSchema 기준입니다. MCI·외부 연동 Tool은 업무에 맞는 테스트 데이터를 입력한 후 저장하세요.</div><textarea id="arguments" spellcheck="false" aria-label="요청 JSON"></textarea><div class="buttons" style="margin-top:12px"><button id="saveButton">현재 요청 저장</button><button class="primary" id="executeButton">실행</button></div></section>
|
||||
<section class="card"><h2>3. 실행 결과</h2><div class="meta"><span class="badge" id="httpStatus">대기</span><span class="badge" id="latency">-</span><span class="badge" id="traceId">trace-id: -</span><span class="badge" id="requestId">request-id: -</span></div><pre class="result" id="result">Tool을 선택하고 실행하세요.</pre></section>
|
||||
</section>
|
||||
</div>
|
||||
<p class="footer-note">이 화면은 현재 Tool Pod의 <code>/tool-manifest</code>와 <code>/mcp/{toolName}</code>만 사용합니다. 저장된 케이스는 이 브라우저의 localStorage에만 보관됩니다.</p>
|
||||
</main>
|
||||
<script>
|
||||
(() => {
|
||||
const STORAGE_KEY = 'axhub.tool-test-console.cases.v1';
|
||||
const state = { tools: [], selected: null, cases: loadCases() };
|
||||
const $ = id => document.getElementById(id);
|
||||
const select = $('toolSelect'), args = $('arguments'), result = $('result');
|
||||
|
||||
function loadCases() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); } catch (_) { return []; } }
|
||||
function persistCases() { localStorage.setItem(STORAGE_KEY, JSON.stringify(state.cases)); renderCases(); }
|
||||
function requestId() { return crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(16).slice(2)}`; }
|
||||
function escapeHtml(value) { return String(value).replace(/[&<>]/g, item => ({'&':'&','<':'<','>':'>'}[item])); }
|
||||
|
||||
let isGatewayMode = false;
|
||||
|
||||
async function loadManifest() {
|
||||
$('manifestStatus').textContent = 'Manifest loading';
|
||||
try {
|
||||
let response = await fetch('/tool-manifest', { headers: { 'Cache-Control': 'no-cache' } });
|
||||
|
||||
if (!response.ok && response.status === 404) {
|
||||
// Gateway 환경 감지 및 Fallback 처리
|
||||
response = await fetch('/mcp/api/v1/tools/list');
|
||||
if (!response.ok) throw new Error(`Gateway HTTP ${response.status}`);
|
||||
|
||||
const rpcData = await response.json();
|
||||
state.tools = rpcData.result?.tools || [];
|
||||
isGatewayMode = true;
|
||||
|
||||
$('manifestStatus').textContent = `${state.tools.length} tools · Gateway Mode`;
|
||||
$('manifestStatus').className = 'badge ok';
|
||||
renderTools();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) throw new Error(`Manifest HTTP ${response.status}`);
|
||||
const manifest = await response.json();
|
||||
state.tools = (manifest.tools || []).filter(tool => tool._meta?.enabled !== false);
|
||||
$('manifestStatus').textContent = `${state.tools.length} tools · rev ${manifest.revision || '-'}`;
|
||||
$('manifestStatus').className = 'badge ok';
|
||||
renderTools();
|
||||
} catch (error) {
|
||||
$('manifestStatus').textContent = `Load error: ${error.message}`;
|
||||
$('manifestStatus').className = 'badge fail';
|
||||
select.innerHTML = '<option>Tool 목록을 불러오지 못했습니다</option>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderTools() {
|
||||
const query = $('filter').value.trim().toLowerCase();
|
||||
const tools = state.tools.filter(tool => `${tool.name} ${tool.title || ''} ${tool.description || ''}`.toLowerCase().includes(query));
|
||||
select.innerHTML = '';
|
||||
for (const tool of tools) {
|
||||
const option = document.createElement('option'); option.value = tool.name;
|
||||
option.textContent = `${tool.name} — ${tool.title || tool.description || ''}`; select.appendChild(option);
|
||||
}
|
||||
if (tools.length) choose(tools.find(tool => tool.name === state.selected?.name) || tools[0]);
|
||||
else { state.selected = null; $('toolMeta').innerHTML = ''; args.value = ''; }
|
||||
}
|
||||
|
||||
function choose(tool) {
|
||||
state.selected = tool; select.value = tool.name;
|
||||
const meta = tool._meta || {}; const hints = tool.annotations || {};
|
||||
$('toolMeta').innerHTML = [
|
||||
`<span class="badge">${escapeHtml(tool.name)}</span>`,
|
||||
`<span class="badge">timeout ${meta.timeoutMillis || 300000}ms</span>`,
|
||||
hints.readOnlyHint ? '<span class="badge ok">read-only</span>' : '',
|
||||
hints.destructiveHint ? '<span class="badge fail">destructive</span>' : ''
|
||||
].join('');
|
||||
args.value = JSON.stringify(exampleForSchema(tool.inputSchema || tool.parametersSchema || {}), null, 2);
|
||||
result.textContent = tool.description || '설명이 없습니다.';
|
||||
resetResult();
|
||||
}
|
||||
|
||||
function exampleForSchema(schema) {
|
||||
if (schema.default !== undefined) return schema.default;
|
||||
if (Array.isArray(schema.examples) && schema.examples.length) return schema.examples[0];
|
||||
if (Array.isArray(schema.enum) && schema.enum.length) return schema.enum[0];
|
||||
if (schema.type === 'object' || schema.properties) {
|
||||
const output = {}; const required = new Set(schema.required || []);
|
||||
for (const [name, property] of Object.entries(schema.properties || {})) {
|
||||
if (required.has(name) || property.default !== undefined || property.examples?.length || property.enum?.length) output[name] = exampleForSchema(property);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
if (schema.type === 'array') return [];
|
||||
if (schema.type === 'integer') return 1;
|
||||
if (schema.type === 'number') return 1.0;
|
||||
if (schema.type === 'boolean') return false;
|
||||
if (schema.format === 'date') return new Date().toISOString().slice(0, 10);
|
||||
if (schema.format === 'date-time') return new Date().toISOString();
|
||||
return 'test-value';
|
||||
}
|
||||
|
||||
function parseArguments() { try { const value = JSON.parse(args.value || '{}'); if (value && typeof value === 'object' && !Array.isArray(value)) return value; throw new Error('요청 JSON은 객체여야 합니다.'); } catch (error) { throw new Error(`요청 JSON 오류: ${error.message}`); } }
|
||||
function endpointFor(tool) {
|
||||
const fallback = `/mcp/${encodeURIComponent(tool.name)}`;
|
||||
try { const endpoint = new URL(tool.endpoint || fallback, window.location.origin); return endpoint.origin === window.location.origin ? `${endpoint.pathname}${endpoint.search}` : fallback; } catch (_) { return fallback; }
|
||||
}
|
||||
function resetResult() { $('httpStatus').textContent = '대기'; $('httpStatus').className = 'badge'; $('latency').textContent = '-'; $('traceId').textContent = 'trace-id: -'; $('requestId').textContent = 'request-id: -'; }
|
||||
|
||||
async function execute(tool = state.selected, body = null) {
|
||||
if (!tool) throw new Error('실행할 Tool을 선택하세요.');
|
||||
const payload = body || parseArguments(); const trace = requestId(), request = requestId(), started = performance.now();
|
||||
$('executeButton').disabled = true; $('httpStatus').textContent = '실행 중'; $('httpStatus').className = 'badge';
|
||||
try {
|
||||
let response;
|
||||
if (isGatewayMode) {
|
||||
const reqPayload = { jsonrpc: "2.0", method: "tools/call", params: { name: tool.name, arguments: payload }, id: Date.now() };
|
||||
response = await fetch('/mcp/api/v1/tools/call', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type':'application/json', 'trace-id':trace, 'request-id':request, 'X-Request-Id':request },
|
||||
body: JSON.stringify(reqPayload)
|
||||
});
|
||||
} else {
|
||||
response = await fetch(endpointFor(tool), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type':'application/json', 'trace-id':trace, 'request-id':request, 'X-Request-Id':request },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
const text = await response.text(); let data; try { data = text ? JSON.parse(text) : null; } catch (_) { data = text; }
|
||||
|
||||
let isOk = response.ok;
|
||||
let displayStatus = response.status;
|
||||
if (isGatewayMode && isOk && data?.result?.isError) {
|
||||
isOk = false;
|
||||
displayStatus = "MCP ERR";
|
||||
}
|
||||
|
||||
const elapsed = Math.round(performance.now() - started);
|
||||
$('httpStatus').textContent = `HTTP ${displayStatus}`; $('httpStatus').className = `badge ${isOk ? 'ok' : 'fail'}`;
|
||||
$('latency').textContent = `${elapsed}ms`; $('traceId').textContent = `trace-id: ${response.headers.get('trace-id') || trace}`; $('requestId').textContent = `request-id: ${response.headers.get('request-id') || request}`;
|
||||
let displayData = data;
|
||||
if (isGatewayMode && data && typeof data === 'object') {
|
||||
if (data.result && data.result.result) {
|
||||
displayData = data.result.result.data !== undefined ? data.result.result.data : data.result.result;
|
||||
} else if (data.error) {
|
||||
displayData = data.error;
|
||||
} else if (data.result && data.result.error_message) {
|
||||
displayData = { error: data.result.error_message, code: data.result.error_code };
|
||||
}
|
||||
}
|
||||
|
||||
result.textContent = JSON.stringify(displayData, null, 2); return { ok: isOk, status: displayStatus, elapsed, data };
|
||||
} finally { $('executeButton').disabled = false; }
|
||||
}
|
||||
|
||||
function saveCase() {
|
||||
try {
|
||||
const payload = parseArguments(); const name = window.prompt('테스트 케이스 이름', state.selected.name);
|
||||
if (!name) return;
|
||||
state.cases.push({ id: requestId(), name, toolName: state.selected.name, arguments: payload, savedAt: new Date().toISOString() }); persistCases();
|
||||
} catch (error) { alert(error.message); }
|
||||
}
|
||||
function renderCases() {
|
||||
const list = $('caseList'); list.innerHTML = '';
|
||||
if (!state.cases.length) { list.innerHTML = '<p style="margin:0;color:#a1a1aa;font-size:13px">저장된 케이스가 없습니다.</p>'; return; }
|
||||
for (const item of state.cases) {
|
||||
const row = document.createElement('div'); row.className = 'case-row';
|
||||
row.innerHTML = `<main><strong>${escapeHtml(item.name)}</strong><small>${escapeHtml(item.toolName)}</small></main><button data-run="${item.id}">실행</button><button class="danger" data-delete="${item.id}">삭제</button>`;
|
||||
list.appendChild(row);
|
||||
}
|
||||
}
|
||||
async function runSavedCases() {
|
||||
if (!state.cases.length) return alert('저장된 테스트 케이스가 없습니다.');
|
||||
$('runAllButton').disabled = true; $('runLog').textContent = `총 ${state.cases.length}건 실행 시작\n`;
|
||||
let success = 0;
|
||||
for (const item of state.cases) {
|
||||
const tool = state.tools.find(candidate => candidate.name === item.toolName);
|
||||
if (!tool) { $('runLog').textContent += `FAIL ${item.name} · Tool 없음\n`; continue; }
|
||||
try { const execution = await execute(tool, item.arguments); success += execution.ok ? 1 : 0; $('runLog').textContent += `${execution.ok ? 'PASS' : 'FAIL'} ${item.name} · HTTP ${execution.status} · ${execution.elapsed}ms\n`; }
|
||||
catch (error) { $('runLog').textContent += `FAIL ${item.name} · ${error.message}\n`; }
|
||||
}
|
||||
$('runLog').textContent += `완료: ${success}/${state.cases.length} 성공`; $('runAllButton').disabled = false;
|
||||
}
|
||||
|
||||
select.addEventListener('change', () => choose(state.tools.find(tool => tool.name === select.value)));
|
||||
$('filter').addEventListener('input', renderTools);
|
||||
$('sampleButton').addEventListener('click', () => { if (state.selected) args.value = JSON.stringify(exampleForSchema(state.selected.inputSchema || state.selected.parametersSchema || {}), null, 2); });
|
||||
$('reloadButton').addEventListener('click', loadManifest);
|
||||
$('saveButton').addEventListener('click', saveCase);
|
||||
$('executeButton').addEventListener('click', async () => { try { await execute(); } catch (error) { $('httpStatus').textContent = '입력 오류'; $('httpStatus').className = 'badge fail'; result.textContent = error.message; } });
|
||||
$('runAllButton').addEventListener('click', runSavedCases);
|
||||
$('clearCasesButton').addEventListener('click', () => { if (confirm('저장된 테스트 케이스를 모두 삭제할까요?')) { state.cases = []; persistCases(); } });
|
||||
$('caseList').addEventListener('click', async event => { const id = event.target.dataset.run || event.target.dataset.delete; if (!id) return; const item = state.cases.find(candidate => candidate.id === id); if (event.target.dataset.delete) { state.cases = state.cases.filter(candidate => candidate.id !== id); persistCases(); return; } const tool = state.tools.find(candidate => candidate.name === item.toolName); try { await execute(tool, item.arguments); } catch (error) { result.textContent = error.message; } });
|
||||
renderCases(); loadManifest();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,24 @@
|
||||
package io.shinhanlife.dap.mcc.presentation;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ToolTestConsoleResourceTest {
|
||||
|
||||
@Test
|
||||
void publishesManifestDrivenToolTestConsole() throws Exception {
|
||||
try (InputStream resource = getClass().getResourceAsStream("/static/tool-test-console.html")) {
|
||||
assertNotNull(resource);
|
||||
String html = new String(resource.readAllBytes(), StandardCharsets.UTF_8);
|
||||
|
||||
assertTrue(html.contains("/tool-manifest"));
|
||||
assertTrue(html.contains("Run saved cases"));
|
||||
assertTrue(html.contains("localStorage"));
|
||||
assertTrue(html.contains("request-id"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user