949 lines
39 KiB
HTML
949 lines
39 KiB
HTML
<!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="h-10 px-4 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</span>
|
||
</button>
|
||
|
||
<!-- Report Button -->
|
||
<button onclick="generateAndDownloadReport()"
|
||
class="h-10 px-4 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 -->
|
||
<button onclick="openGenerateTestPlanModal()"
|
||
class="h-10 px-4 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>Plan</span>
|
||
</button>
|
||
|
||
<!-- Export Automated Playwright Tests -->
|
||
<button onclick="exportAutomatedTests()"
|
||
class="h-10 px-4 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</span>
|
||
</button>
|
||
|
||
<!-- Load existing Test Plan -->
|
||
<button onclick="openLoadTestPlanModal()"
|
||
class="h-10 px-4 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</span>
|
||
</button>
|
||
|
||
<button onclick="reloadData()"
|
||
class="h-10 px-4 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="addRichStep()" 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 & 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
|
||
renderRichSteps(currentTest.steps || [], currentTest.status);
|
||
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, '"')}" 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);
|
||
}
|
||
|
||
// === Rich per-step editor (Fase B) ===
|
||
function renderRichSteps(steps, overallStatus = 'manual') {
|
||
const ul = document.getElementById('listSteps');
|
||
ul.innerHTML = '';
|
||
|
||
const rich = steps.map(s => {
|
||
if (typeof s === 'string') {
|
||
return { action: s, automatable: overallStatus !== 'manual', reason_not_automatable: '' };
|
||
}
|
||
return { action: s.action || s, automatable: s.automatable !== undefined ? s.automatable : (overallStatus !== 'manual'), reason_not_automatable: s.reason_not_automatable || '' };
|
||
});
|
||
|
||
rich.forEach((step, idx) => {
|
||
const li = document.createElement('li');
|
||
li.style.flexDirection = 'column';
|
||
li.style.alignItems = 'stretch';
|
||
li.style.gap = '4px';
|
||
li.innerHTML = `
|
||
<div style="display:flex; gap:8px; align-items:center;">
|
||
<input value="${(step.action || '').replace(/"/g, '"')}" style="flex:1" oninput="updateRichStep(${idx}, 'action', this.value)">
|
||
<label style="font-size:11px; white-space:nowrap; display:flex; align-items:center; gap:4px;">
|
||
<input type="checkbox" ${step.automatable ? 'checked' : ''} onchange="updateRichStep(${idx}, 'automatable', this.checked)">
|
||
Auto
|
||
</label>
|
||
<button onclick="removeRichStep(${idx})" style="color:#f87171;"><i class="fa-solid fa-trash text-xs"></i></button>
|
||
</div>
|
||
${!step.automatable ? `
|
||
<input placeholder="Reason not automatable (optional)" value="${(step.reason_not_automatable || '').replace(/"/g, '"')}" style="font-size:12px; margin-top:2px;" oninput="updateRichStep(${idx}, 'reason_not_automatable', this.value)">
|
||
` : ''}
|
||
`;
|
||
ul.appendChild(li);
|
||
});
|
||
|
||
ul.dataset.richSteps = JSON.stringify(rich);
|
||
}
|
||
|
||
function updateRichStep(idx, field, value) {
|
||
const ul = document.getElementById('listSteps');
|
||
const steps = JSON.parse(ul.dataset.richSteps || '[]');
|
||
if (!steps[idx]) steps[idx] = {};
|
||
steps[idx][field] = value;
|
||
// Re-render to show/hide reason input
|
||
const overall = document.getElementById('modalStatus') ? document.getElementById('modalStatus').value : 'manual';
|
||
renderRichSteps(steps, overall);
|
||
}
|
||
|
||
function removeRichStep(idx) {
|
||
const ul = document.getElementById('listSteps');
|
||
const steps = JSON.parse(ul.dataset.richSteps || '[]');
|
||
steps.splice(idx, 1);
|
||
const overall = document.getElementById('modalStatus') ? document.getElementById('modalStatus').value : 'manual';
|
||
renderRichSteps(steps, overall);
|
||
}
|
||
|
||
function addRichStep() {
|
||
const ul = document.getElementById('listSteps');
|
||
const steps = JSON.parse(ul.dataset.richSteps || '[]');
|
||
steps.push({ action: '', automatable: true, reason_not_automatable: '' });
|
||
const overall = document.getElementById('modalStatus') ? document.getElementById('modalStatus').value : 'manual';
|
||
renderRichSteps(steps, overall);
|
||
}
|
||
|
||
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 (rich steps for Fase B)
|
||
const stepsUl = document.getElementById('listSteps');
|
||
const richFromUI = JSON.parse(stepsUl.dataset.richSteps || '[]');
|
||
currentTest.steps = richFromUI.length > 0 ? richFromUI : (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>
|