Files
autotest/editor/public/index.html
T
2026-05-22 13:46:02 +02:00

886 lines
36 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test Case Editor • PBX Automation</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
:root {
--bg: #0b1120;
--card: #1e2937;
--border: #334155;
}
body { background: var(--bg); color: #e2e8f0; }
.card { background: var(--card); border: 1px solid var(--border); }
.row-hover:hover { background: #162032; }
.section-title { font-size: 11px; font-weight: 700; letter-spacing: .6px; text-transform: uppercase; color: #64748b; }
.modal { animation: fadeInScale .15s ease-out; }
@keyframes fadeInScale { from { opacity: 0; transform: scale(0.96); } to { opacity: 1; transform: scale(1); } }
.editable-list li { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
.editable-list input { flex: 1; background: #0f172a; border: 1px solid #334155; color: #e2e8f0; padding: 6px 10px; border-radius: 6px; font-size: 13px; }
.editable-list button { color: #64748b; }
.editable-list button:hover { color: #f87171; }
.dep-row { display: flex; gap: 8px; margin-bottom: 6px; }
.dep-row input { background: #0f172a; border: 1px solid #334155; color: #e2e8f0; padding: 5px 8px; border-radius: 4px; font-size: 13px; }
.status-automated { background: #052e16; color: #4ade80; }
.status-partial { background: #451a03; color: #fbbf24; }
.status-manual { background: #450a0a; color: #f87171; }
.dirty { color: #fbbf24; }
</style>
</head>
<body class="text-sm">
<div class="max-w-[1600px] mx-auto">
<!-- Header -->
<div class="bg-[#020617] border-b border-[#1e293b] px-8 py-5 flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold flex items-center gap-3">
<i class="fa-solid fa-robot text-orange-400"></i>
Autotest
</h1>
<p class="text-[#64748b] text-xs mt-0.5">Graphical editor for PBX / Core Automation Plan</p>
</div>
<div class="flex items-center gap-3">
<div id="status" class="text-xs px-3 py-1 rounded bg-[#1e2937] text-amber-300 font-medium">
<span id="count">0</span> test cases loaded
</div>
<button onclick="saveAll()"
class="px-5 py-2 bg-[#0b1120] text-orange-400 border border-orange-400 hover:bg-orange-900/30 rounded-lg font-semibold flex items-center gap-2 text-sm">
<i class="fa-solid fa-save"></i>
<span>Save All Changes</span>
</button>
<!-- Report Button (generates grouped summary like the editor) -->
<button onclick="generateAndDownloadReport()"
class="px-4 py-2 bg-[#0b1120] text-orange-400 border border-orange-400 hover:bg-orange-900/30 rounded-lg text-sm font-semibold flex items-center gap-2">
<i class="fa-solid fa-file-alt"></i>
<span>Report</span>
</button>
<!-- Generate Test Plan for manual execution -->
<button onclick="openGenerateTestPlanModal()"
class="px-4 py-2 bg-[#0b1120] text-orange-400 border border-orange-400 hover:bg-orange-900/30 rounded-lg text-sm font-semibold flex items-center gap-2">
<i class="fa-solid fa-list-check"></i>
<span>Generate Test Plan</span>
</button>
<!-- Export Automated Playwright Tests -->
<button onclick="exportAutomatedTests()"
class="px-4 py-2 bg-[#0b1120] text-orange-400 border border-orange-400 hover:bg-orange-900/30 rounded-lg text-sm font-semibold flex items-center gap-2">
<i class="fa-solid fa-file-code"></i>
<span>Export Automated Tests</span>
</button>
<!-- Load existing Test Plan -->
<button onclick="openLoadTestPlanModal()"
class="px-4 py-2 bg-[#0b1120] text-orange-400 border border-orange-400 hover:bg-orange-900/30 rounded-lg text-sm font-semibold flex items-center gap-2">
<i class="fa-solid fa-folder-open"></i>
<span>Load Test Plan</span>
</button>
<button onclick="reloadData()"
class="px-4 py-2 bg-[#1e2937] hover:bg-[#334155] rounded-lg text-sm flex items-center gap-2">
<i class="fa-solid fa-sync"></i>
<span>Reload</span>
</button>
</div>
</div>
<!-- Filters -->
<div class="px-8 pt-6">
<div class="card rounded-xl p-4 flex flex-wrap gap-4 items-end">
<div>
<div class="section-title mb-1">Module</div>
<select id="fModule" onchange="renderTable()" class="bg-[#0f172a] border border-[#334155] rounded px-3 py-1.5 text-sm w-40">
<option value="">All</option>
<option value="core">Core</option>
<option value="pbx">PBX</option>
</select>
</div>
<div>
<div class="section-title mb-1">Group</div>
<select id="fGroup" onchange="renderTable()" class="bg-[#0f172a] border border-[#334155] rounded px-3 py-1.5 text-sm w-56">
<option value="">All</option>
</select>
</div>
<div>
<div class="section-title mb-1">Status</div>
<select id="fStatus" onchange="renderTable()" class="bg-[#0f172a] border border-[#334155] rounded px-3 py-1.5 text-sm w-40">
<option value="">All</option>
<option value="automated">Automated</option>
<option value="partial">Partial</option>
<option value="manual">Manual</option>
</select>
</div>
<div class="flex-1 min-w-[240px]">
<div class="section-title mb-1">Search</div>
<input id="fSearch" oninput="renderTable()" type="text" placeholder="ID, description, group..."
class="w-full bg-[#0f172a] border border-[#334155] rounded px-3 py-1.5 text-sm">
</div>
<button onclick="resetFilters()"
class="px-4 py-1.5 text-sm bg-[#1e2937] hover:bg-[#334155] rounded border border-[#334155]">
Clear Filters
</button>
</div>
</div>
<!-- Table -->
<div class="px-8 pt-4 pb-12">
<!-- Selection Toolbar -->
<div class="flex items-center gap-2 mb-2 px-1 text-xs">
<span class="text-[#64748b]">Selection:</span>
<button onclick="selectOnlyManualAndPartial()" class="px-2 py-1 bg-[#1e2937] hover:bg-orange-600 rounded text-orange-300">Manual + Partial</button>
<button onclick="selectAllCore()" class="px-2 py-1 bg-[#1e2937] hover:bg-orange-600 rounded">All Core</button>
<button onclick="selectAllPbx()" class="px-2 py-1 bg-[#1e2937] hover:bg-orange-600 rounded">All PBX</button>
<button onclick="clearSelection()" class="px-2 py-1 bg-[#1e2937] hover:bg-red-600 rounded text-red-300">Clear</button>
<button onclick="selectOnlyFullyAutomated()" class="px-2 py-1 bg-[#1e2937] hover:bg-orange-600 rounded text-orange-300">Only Fully Automated</button>
<span class="ml-3 text-amber-300 font-medium" id="selectionCount"></span>
</div>
<div class="card rounded-xl overflow-hidden">
<table class="w-full text-sm">
<thead>
<tr class="bg-[#0f172a] text-[#64748b] text-xs">
<th class="w-8 py-3 px-2"></th>
<th class="w-10 py-3 px-2 text-left">#</th>
<th class="py-3 px-3 text-left">Test ID</th>
<th class="w-16 py-3 px-2 text-left">Module</th>
<th class="w-16 py-3 px-2 text-left">Action</th>
<th class="py-3 px-3 text-left">Description</th>
<th class="w-24 py-3 px-2 text-left">Status</th>
</tr>
</thead>
<tbody id="tableBody" class="divide-y divide-[#334155]"></tbody>
</table>
</div>
<div id="noResults" class="hidden text-center py-8 text-[#64748b]">No tests match the current filters.</div>
</div>
</div>
<!-- EDIT MODAL -->
<div id="editModal" class="hidden fixed inset-0 bg-black/70 flex items-center justify-center z-50" onclick="if (event.target.id === 'editModal') closeModal()">
<div class="modal bg-[#1e2937] border border-[#334155] rounded-2xl w-full max-w-4xl max-h-[92vh] flex flex-col mx-4" onclick="event.stopImmediatePropagation()">
<!-- Modal Header -->
<div class="px-6 py-4 border-b border-[#334155] flex items-center justify-between bg-[#0f172a] rounded-t-2xl">
<div>
<div id="modalId" class="font-mono text-sm text-[#94a3b8]"></div>
<div id="modalGroup" class="text-xs text-[#64748b]"></div>
</div>
<div class="flex items-center gap-3">
<select id="modalStatus" class="bg-[#0f172a] border border-[#334155] text-sm rounded px-3 py-1">
<option value="automated">Automated</option>
<option value="partial">Partial</option>
<option value="manual">Manual</option>
</select>
<button onclick="resetToSuggested()"
class="text-xs px-3 py-1 bg-[#0b1120] text-orange-400 border border-orange-400 hover:bg-orange-900/30 rounded flex items-center gap-1 whitespace-nowrap">
<i class="fa-solid fa-magic"></i>
<span>Reset to suggested</span>
</button>
<button onclick="closeModal()" class="text-[#64748b] hover:text-white px-2 text-xl">×</button>
</div>
</div>
<div class="p-6 overflow-auto flex-1 space-y-6 text-sm">
<!-- Description -->
<div>
<div class="section-title mb-1.5">Description</div>
<textarea id="modalDescription" rows="2"
class="w-full bg-[#0f172a] border border-[#334155] rounded-lg p-3 text-sm"></textarea>
</div>
<!-- Steps -->
<div>
<div class="flex justify-between items-center mb-1.5">
<div class="section-title">Test Steps</div>
<button onclick="addListItem('steps')" class="text-xs px-2.5 py-0.5 bg-[#334155] hover:bg-[#475569] rounded">+ Add step</button>
</div>
<ul id="listSteps" class="editable-list text-sm"></ul>
</div>
<!-- Automatable -->
<div>
<div class="flex justify-between items-center mb-1.5">
<div class="section-title text-emerald-400">Automatable</div>
<button onclick="addListItem('automatable')" class="text-xs px-2.5 py-0.5 bg-emerald-900/60 hover:bg-emerald-800 rounded">+ Add</button>
</div>
<ul id="listAutomatable" class="editable-list text-sm"></ul>
</div>
<!-- Not Yet -->
<div>
<div class="flex justify-between items-center mb-1.5">
<div class="section-title text-red-400">Not Automatable Yet / Manual</div>
<button onclick="addListItem('notYet')" class="text-xs px-2.5 py-0.5 bg-red-900/60 hover:bg-red-800 rounded">+ Add</button>
</div>
<ul id="listNotYet" class="editable-list text-sm"></ul>
</div>
<!-- Code Generation (for automated tests) -->
<div>
<div class="section-title mb-1.5 text-orange-400">Code Generation (Automated)</div>
<div class="flex items-center gap-2 mb-2">
<input type="checkbox" id="fullyAutomated" class="w-4 h-4">
<label for="fullyAutomated" class="text-sm">Mark as <strong>fullyAutomated</strong> (ready for code generation)</label>
</div>
<div class="mb-2">
<div class="text-xs text-[#64748b] mb-1">Implementation Notes (for generator / LLM)</div>
<textarea id="implementationNotes" rows="2"
class="w-full bg-[#0f172a] border border-[#334155] rounded p-2 text-sm"
placeholder="Hints for the code generator..."></textarea>
</div>
<!-- codeSteps editor -->
<div>
<div class="flex justify-between items-center mb-1">
<div class="text-xs text-[#64748b]">Code Steps (for generator)</div>
<button onclick="addCodeStep()" class="text-xs px-2 py-0.5 bg-orange-900/60 hover:bg-orange-800 rounded">+ Add Step</button>
</div>
<div id="codeStepsContainer" class="space-y-2 text-sm"></div>
</div>
</div>
<!-- Dependencies -->
<div>
<div class="flex justify-between items-center mb-1.5">
<div class="section-title">Dependencies &amp; Prerequisites</div>
<button onclick="addDependency()" class="text-xs px-2.5 py-0.5 bg-[#334155] hover:bg-[#475569] rounded">+ Add dependency</button>
</div>
<div id="depContainer" class="space-y-1"></div>
</div>
</div>
<div class="px-6 py-4 border-t border-[#334155] bg-[#0f172a] rounded-b-2xl flex justify-end gap-3">
<button onclick="closeModal()" class="px-5 py-2 text-sm rounded-lg bg-[#334155] hover:bg-[#475569]">Cancel</button>
<button onclick="saveCurrentTest()" class="px-6 py-2 text-sm rounded-lg bg-sky-600 hover:bg-sky-500 font-semibold">Save Changes to Test</button>
</div>
</div>
</div>
<script>
let allTests = [];
let filteredTests = [];
let currentTest = null;
let currentIndex = -1;
let isDirty = false;
let selectedIds = new Set(); // For Generate Test Plan selection
async function loadData() {
try {
const res = await fetch('/api/testcases');
allTests = await res.json();
populateGroupFilter();
renderTable();
updateSelectionCount();
document.getElementById('count').textContent = allTests.length;
} catch (e) {
alert('Could not load test cases. Make sure the server is running and data file exists.');
console.error(e);
}
}
function populateGroupFilter() {
const select = document.getElementById('fGroup');
const groups = [...new Set(allTests.map(t => t.group))].sort();
select.innerHTML = '<option value="">All</option>' + groups.map(g => `<option value="${g}">${g}</option>`).join('');
}
function getFiltered() {
const mod = document.getElementById('fModule').value;
const grp = document.getElementById('fGroup').value;
const stat = document.getElementById('fStatus').value;
const search = document.getElementById('fSearch').value.toLowerCase().trim();
return allTests.filter(t => {
if (mod && t.module !== mod) return false;
if (grp && t.group !== grp) return false;
if (stat && t.status !== stat) return false;
if (search) {
const hay = (t.id + ' ' + t.description + ' ' + t.group).toLowerCase();
if (!hay.includes(search)) return false;
}
return true;
});
}
function renderTable() {
filteredTests = getFiltered();
const tbody = document.getElementById('tableBody');
const noRes = document.getElementById('noResults');
if (filteredTests.length === 0) {
tbody.innerHTML = '';
noRes.classList.remove('hidden');
return;
}
noRes.classList.add('hidden');
// Group by 'group' field (e.g. core-repository, pbx-calls, etc.)
const grouped = {};
filteredTests.forEach(t => {
if (!grouped[t.group]) grouped[t.group] = [];
grouped[t.group].push(t);
});
const groupNames = Object.keys(grouped).sort();
let html = '';
groupNames.forEach(groupName => {
const testsInGroup = grouped[groupName];
const allSelectedInGroup = testsInGroup.every(t => selectedIds.has(t.id));
// Group header
html += `
<tr class="bg-[#111827]">
<td colspan="7" style="padding: 5px 10px; border-bottom: 1px solid #334155;">
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer; color: #fb923c; font-weight: 600; font-size: 13px;">
<input type="checkbox" ${allSelectedInGroup ? 'checked' : ''}
onclick="event.stopImmediatePropagation(); toggleGroupSelection('${groupName}', this.checked)">
<span>${groupName}</span>
<span style="font-size: 11px; color: #64748b; font-weight: normal;">(${testsInGroup.length} tests)</span>
</label>
</td>
</tr>
`;
testsInGroup.forEach((t, idx) => {
const statusClass = `status-${t.status}`;
const shortId = t.id.length > 52 ? t.id.slice(0, 49) + '…' : t.id;
const isChecked = selectedIds.has(t.id) ? 'checked' : '';
html += `
<tr class="row-hover">
<td class="py-1.5 px-2">
<input type="checkbox" ${isChecked}
onclick="event.stopImmediatePropagation(); toggleSelection('${t.id}', this.checked)">
</td>
<td class="py-1.5 px-2 text-[#64748b] cursor-pointer text-xs" onclick="openEditor(${allTests.indexOf(t)})">${idx + 1}</td>
<td class="py-1.5 px-2 font-mono text-xs text-[#94a3b8] cursor-pointer" onclick="openEditor(${allTests.indexOf(t)})">${shortId}</td>
<td class="py-1.5 px-2 cursor-pointer" onclick="openEditor(${allTests.indexOf(t)})">
<span class="px-2 py-0.5 text-xs rounded ${t.module === 'core' ? 'bg-blue-900 text-blue-300' : 'bg-violet-900 text-violet-300'}">${t.module}</span>
</td>
<td class="py-1.5 px-2 text-[#94a3b8] cursor-pointer text-xs" onclick="openEditor(${allTests.indexOf(t)})">${t.action}</td>
<td class="py-1.5 px-2 text-[#e2e8f0] cursor-pointer text-sm" onclick="openEditor(${allTests.indexOf(t)})">${t.description}</td>
<td class="py-1.5 px-2 cursor-pointer" onclick="openEditor(${allTests.indexOf(t)})">
<span class="px-2.5 py-0.5 text-xs font-semibold rounded-full ${statusClass}">${t.status}</span>
</td>
</tr>
`;
});
});
tbody.innerHTML = html;
updateSelectionCount();
}
function resetFilters() {
document.getElementById('fModule').value = '';
document.getElementById('fGroup').value = '';
document.getElementById('fStatus').value = '';
document.getElementById('fSearch').value = '';
renderTable();
}
function toggleSelection(id, checked) {
if (checked) {
selectedIds.add(id);
} else {
selectedIds.delete(id);
}
renderTable();
updateSelectionCount();
}
function toggleGroupSelection(groupName, checked) {
const testsInGroup = allTests.filter(t => t.group === groupName);
testsInGroup.forEach(t => {
if (checked) {
selectedIds.add(t.id);
} else {
selectedIds.delete(t.id);
}
});
renderTable();
updateSelectionCount();
}
function toggleGroupSelection(groupName, checked) {
const testsInGroup = allTests.filter(t => t.group === groupName);
testsInGroup.forEach(t => {
if (checked) {
selectedIds.add(t.id);
} else {
selectedIds.delete(t.id);
}
});
renderTable();
updateSelectionCount();
}
// Bulk selection helpers
function selectOnlyManualAndPartial() {
selectedIds.clear();
allTests.forEach(t => {
if (t.status === 'manual' || t.status === 'partial') selectedIds.add(t.id);
});
renderTable();
updateSelectionCount();
}
function selectAllCore() {
selectedIds.clear();
allTests.forEach(t => {
if (t.module === 'core') selectedIds.add(t.id);
});
renderTable();
updateSelectionCount();
}
function selectAllPbx() {
selectedIds.clear();
allTests.forEach(t => {
if (t.module === 'pbx') selectedIds.add(t.id);
});
renderTable();
updateSelectionCount();
}
function selectOnlyFullyAutomated() {
selectedIds.clear();
allTests.forEach(t => {
if (t.fullyAutomated === true) selectedIds.add(t.id);
});
renderTable();
updateSelectionCount();
}
function clearSelection() {
selectedIds.clear();
renderTable();
updateSelectionCount();
}
function updateSelectionCount() {
const el = document.getElementById('selectionCount');
if (el) {
el.textContent = selectedIds.size > 0 ? `${selectedIds.size} selected` : '';
}
}
function generateAndDownloadReport() {
alert("La función de Report está temporalmente deshabilitada mientras corregimos un error de sintaxis.\n\nEstamos trabajando en una versión estable.");
// TODO: Implementar una versión limpia del generador de report
}
async function openLoadTestPlanModal() {
try {
const res = await fetch('/api/test-plans');
const plans = await res.json();
if (!plans || plans.length === 0) {
alert('No saved test plans found.');
return;
}
let listHtml = '<div style="max-height:400px; overflow:auto;">';
plans.forEach(p => {
const date = new Date(p.updatedAt || p.createdAt).toLocaleString();
listHtml += `
<div onclick="window.open('/tester-plan.html?plan=${p.id}', '_blank'); closeLoadModal();"
style="padding:10px; border-bottom:1px solid #334155; cursor:pointer; display:flex; justify-content:space-between;">
<div>
<strong>${p.name}</strong><br>
<small style="color:#64748b">${p.testCount} tests • ${date}</small>
</div>
<div style="color:#f59e0b">Open →</div>
</div>
`;
});
listHtml += '</div>';
const modal = document.createElement('div');
modal.id = 'loadPlanModal';
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;z-index:9999';
modal.innerHTML = `
<div style="background:#1e2937; border:1px solid #334155; border-radius:12px; width:90%; max-width:600px; padding:20px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
<h3 style="margin:0; color:#f59e0b;">Saved Test Plans</h3>
<button onclick="closeLoadModal()" style="background:none; border:none; color:#64748b; font-size:20px; cursor:pointer;">×</button>
</div>
${listHtml}
</div>
`;
document.body.appendChild(modal);
} catch (e) {
alert('Error loading plans: ' + e.message);
}
}
function closeLoadModal() {
const m = document.getElementById('loadPlanModal');
if (m) m.remove();
}
async function exportAutomatedTests() {
const automatedTests = allTests.filter(t => t.fullyAutomated === true || t.status === 'automated');
if (automatedTests.length === 0) {
alert('No fully automated tests found.');
return;
}
// For now we just generate the whole group of the first selected test (simple approach)
const firstGroup = automatedTests[0]?.group;
const confirmed = confirm(`Generate automated tests for group "${firstGroup}"?\n\nFiles will be written to tests/generated/.`);
if (!confirmed) return;
try {
const res = await fetch('/api/export/automated-tests', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ group: firstGroup })
});
const data = await res.json();
if (data.success) {
alert(`✅ Automated tests generated for group "${firstGroup}".\nCheck tests/generated/ folder.\n\nPlease review before committing.`);
} else {
alert('Generation failed: ' + (data.error || 'Unknown error'));
}
} catch (e) {
alert('Error exporting automated tests: ' + e.message);
}
}
function openEditor(globalIndex) {
currentIndex = globalIndex;
currentTest = JSON.parse(JSON.stringify(allTests[globalIndex])); // deep clone
document.getElementById('modalId').textContent = currentTest.id;
document.getElementById('modalGroup').textContent = `${currentTest.group}${currentTest.file}`;
document.getElementById('modalDescription').value = currentTest.description || '';
document.getElementById('modalStatus').value = currentTest.status;
// Code Generation fields (Phase 1)
const fullyAutomatedEl = document.getElementById('fullyAutomated');
fullyAutomatedEl.checked = !!currentTest.fullyAutomated;
const notesEl = document.getElementById('implementationNotes');
notesEl.value = currentTest.implementationNotes || '';
// Code Steps
renderCodeSteps(currentTest.codeSteps || []);
// Render lists
renderList('listSteps', currentTest.steps || []);
renderList('listAutomatable', currentTest.automatable || []);
renderList('listNotYet', currentTest.notYet || []);
renderDependencies(currentTest.dependencies || []);
document.getElementById('editModal').classList.remove('hidden');
document.getElementById('editModal').classList.add('flex');
}
function renderList(containerId, items) {
const ul = document.getElementById(containerId);
ul.innerHTML = items.map((item, idx) => `
<li>
<input value="${item.replace(/"/g, '&quot;')}" oninput="updateListItem('${containerId}', ${idx}, this.value)">
<button onclick="removeListItem('${containerId}', ${idx})"><i class="fa-solid fa-trash text-xs"></i></button>
</li>
`).join('');
ul.dataset.items = JSON.stringify(items);
}
function updateListItem(containerId, idx, value) {
const ul = document.getElementById(containerId);
const items = JSON.parse(ul.dataset.items || '[]');
items[idx] = value;
ul.dataset.items = JSON.stringify(items);
}
function removeListItem(containerId, idx) {
const ul = document.getElementById(containerId);
const items = JSON.parse(ul.dataset.items || '[]');
items.splice(idx, 1);
renderList(containerId, items);
}
function addListItem(type) {
const map = { steps: 'listSteps', automatable: 'listAutomatable', notYet: 'listNotYet' };
const id = map[type];
const ul = document.getElementById(id);
const items = JSON.parse(ul.dataset.items || '[]');
items.push('');
renderList(id, items);
}
function renderDependencies(deps) {
const container = document.getElementById('depContainer');
container.innerHTML = deps.map((d, i) => `
<div class="dep-row">
<input value="${d.label || ''}" placeholder="Label" style="width:160px" oninput="updateDep(${i}, 'label', this.value)">
<input value="${d.value || ''}" placeholder="Value / URL / Requirement" style="flex:1" oninput="updateDep(${i}, 'value', this.value)">
<button onclick="removeDep(${i})" class="px-2 text-red-400"><i class="fa-solid fa-trash"></i></button>
</div>
`).join('');
container.dataset.deps = JSON.stringify(deps);
}
function updateDep(idx, field, val) {
const container = document.getElementById('depContainer');
const deps = JSON.parse(container.dataset.deps || '[]');
deps[idx][field] = val;
container.dataset.deps = JSON.stringify(deps);
}
function removeDep(idx) {
const container = document.getElementById('depContainer');
const deps = JSON.parse(container.dataset.deps || '[]');
deps.splice(idx, 1);
renderDependencies(deps);
}
function addDependency() {
const container = document.getElementById('depContainer');
const deps = JSON.parse(container.dataset.deps || '[]');
deps.push({ label: '', value: '' });
renderDependencies(deps);
}
// === Code Steps Editor (for automated generation) ===
function renderCodeSteps(codeSteps = []) {
const container = document.getElementById('codeStepsContainer');
container.innerHTML = '';
codeSteps.forEach((step, index) => {
const div = document.createElement('div');
div.className = 'flex gap-2 items-center bg-[#0f172a] p-2 rounded';
const paramsValue = step.params ? JSON.stringify(step.params) : '';
div.innerHTML = `
<input placeholder="Step Name" value="${step.stepName || ''}" class="flex-1 text-xs" oninput="updateCodeStep(${index}, 'stepName', this.value)">
<input placeholder="Action Key" value="${step.actionKey || ''}" class="flex-1 text-xs" oninput="updateCodeStep(${index}, 'actionKey', this.value)">
<input placeholder='Params (JSON)' value='${paramsValue}' class="flex-1 text-xs" oninput="updateCodeStepParams(${index}, this.value)">
<button onclick="removeCodeStep(${index})" class="text-red-400 hover:text-red-500 px-1">×</button>
`;
container.appendChild(div);
});
container.dataset.codeSteps = JSON.stringify(codeSteps);
}
function updateCodeStep(index, field, value) {
const container = document.getElementById('codeStepsContainer');
const steps = JSON.parse(container.dataset.codeSteps || '[]');
if (!steps[index]) steps[index] = {};
steps[index][field] = value;
container.dataset.codeSteps = JSON.stringify(steps);
}
function updateCodeStepParams(index, value) {
const container = document.getElementById('codeStepsContainer');
const steps = JSON.parse(container.dataset.codeSteps || '[]');
if (!steps[index]) steps[index] = {};
try {
steps[index].params = value ? JSON.parse(value) : undefined;
} catch (e) {
// keep previous value if JSON is invalid
}
container.dataset.codeSteps = JSON.stringify(steps);
}
function removeCodeStep(index) {
const container = document.getElementById('codeStepsContainer');
const steps = JSON.parse(container.dataset.codeSteps || '[]');
steps.splice(index, 1);
renderCodeSteps(steps);
}
function addCodeStep() {
const container = document.getElementById('codeStepsContainer');
const steps = JSON.parse(container.dataset.codeSteps || '[]');
steps.push({ stepName: '', actionKey: '' });
renderCodeSteps(steps);
}
async function openGenerateTestPlanModal() {
if (selectedIds.size === 0) {
alert('Please select at least one test case using the checkboxes.');
return;
}
const baseName = prompt('Enter name for this Test Plan:', 'Manual Test Plan');
if (!baseName) return;
const ids = Array.from(selectedIds);
try {
const res = await fetch('/api/test-plans', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids, name: baseName })
});
const data = await res.json();
if (data.success && data.planId) {
// Open the editable tester view in a new tab
window.open(`/tester-plan.html?plan=${data.planId}`, '_blank');
alert(`✅ Test Plan created!\n\nYou can now open the editable plan in the new tab.\nThe tester can edit results with auto-save.`);
} else {
alert('Error creating plan: ' + (data.error || 'Unknown error'));
}
} catch (e) {
alert('Failed to create Test Plan: ' + e.message);
}
}
async function resetToSuggested() {
if (!currentTest) return;
// Build a minimal test object from current modal values
const tempTest = {
id: currentTest.id,
group: currentTest.group,
module: currentTest.module,
action: currentTest.action,
description: document.getElementById('modalDescription').value.trim(),
status: document.getElementById('modalStatus').value,
manualReason: currentTest.manualReason,
partialNotes: currentTest.partialNotes
};
try {
const res = await fetch('/api/enrich', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(tempTest)
});
const rich = await res.json();
// Repopulate lists with suggested values
currentTest.steps = rich.steps || [];
currentTest.automatable = rich.automatable || [];
currentTest.notYet = rich.notYet || [];
currentTest.dependencies = rich.dependencies || [];
renderList('listSteps', currentTest.steps);
renderList('listAutomatable', currentTest.automatable);
renderList('listNotYet', currentTest.notYet);
renderDependencies(currentTest.dependencies);
} catch (e) {
alert('Could not fetch suggestions from server');
console.error(e);
}
}
function saveCurrentTest() {
if (currentIndex < 0) return;
// Collect values
currentTest.description = document.getElementById('modalDescription').value.trim();
currentTest.status = document.getElementById('modalStatus').value;
// Code Generation fields (Phase 1)
const fullyAutomatedEl = document.getElementById('fullyAutomated');
currentTest.fullyAutomated = fullyAutomatedEl.checked;
const notesEl = document.getElementById('implementationNotes');
currentTest.implementationNotes = notesEl.value.trim() || undefined;
// Code Steps
const codeStepsContainer = document.getElementById('codeStepsContainer');
currentTest.codeSteps = JSON.parse(codeStepsContainer.dataset.codeSteps || '[]')
.filter(s => s.stepName || s.actionKey);
// Collect lists
const stepsUl = document.getElementById('listSteps');
currentTest.steps = JSON.parse(stepsUl.dataset.items || '[]').filter(Boolean);
const autoUl = document.getElementById('listAutomatable');
currentTest.automatable = JSON.parse(autoUl.dataset.items || '[]').filter(Boolean);
const notUl = document.getElementById('listNotYet');
currentTest.notYet = JSON.parse(notUl.dataset.items || '[]').filter(Boolean);
const depContainer = document.getElementById('depContainer');
currentTest.dependencies = JSON.parse(depContainer.dataset.deps || '[]')
.filter(d => d.label || d.value);
// Update master array
allTests[currentIndex] = currentTest;
isDirty = true;
closeModal();
renderTable();
document.getElementById('status').classList.add('dirty');
document.getElementById('status').innerHTML = '<span class="text-amber-400">Unsaved changes</span>';
}
function closeModal() {
document.getElementById('editModal').classList.remove('flex');
document.getElementById('editModal').classList.add('hidden');
currentTest = null;
currentIndex = -1;
}
async function saveAll() {
const btns = document.querySelectorAll('button');
try {
const res = await fetch('/api/testcases', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(allTests)
});
const json = await res.json();
if (json.success) {
isDirty = false;
document.getElementById('status').classList.remove('dirty');
document.getElementById('status').innerHTML = `<span id="count">${allTests.length}</span> test cases saved`;
alert('✅ All changes saved successfully!');
}
} catch (e) {
alert('Error saving data: ' + e.message);
}
}
async function reloadData() {
if (isDirty && !confirm('You have unsaved changes. Reload anyway?')) return;
await loadData();
isDirty = false;
document.getElementById('status').classList.remove('dirty');
document.getElementById('status').innerHTML = `<span id="count">${allTests.length}</span> test cases loaded`;
}
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
const modal = document.getElementById('editModal');
if (!modal.classList.contains('hidden')) closeModal();
}
if (e.metaKey && e.key === 's') {
e.preventDefault();
saveAll();
}
});
// Init
window.onload = () => {
loadData();
};
</script>
</body>
</html>