mejoras en el modal de los testcases

This commit is contained in:
Juan
2026-05-27 11:51:17 +02:00
parent 19ea13dadd
commit 1b638f192e
89 changed files with 1458 additions and 629 deletions
+189 -195
View File
@@ -69,10 +69,12 @@
}
.editable-list li {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
display: block;
margin-bottom: 0;
}
#listSteps li {
margin-bottom: 0;
}
.editable-list input {
@@ -93,6 +95,47 @@
color: var(--red);
}
/* Step cards for better visual grouping in the modal */
.step-card {
background: var(--input);
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px 10px;
margin-bottom: 8px;
}
.step-card input {
background: var(--bg);
border: 1px solid var(--border);
}
.step-header {
display: flex;
gap: 6px;
align-items: center;
margin-bottom: 6px;
}
.step-number {
font-size: 11px;
font-weight: 600;
color: var(--text-secondary);
min-width: 20px;
text-align: center;
}
.step-label {
font-size: 10px;
color: var(--text-secondary);
margin-bottom: 2px;
display: block;
letter-spacing: 0.3px;
}
.step-expected input {
font-size: 12px;
}
.dep-row {
display: flex;
gap: 8px;
@@ -182,9 +225,9 @@
</style>
</head>
<body class="text-sm">
<div class="max-w-[1600px] mx-auto">
<!-- Header -->
<div class="bg-[var(--bg-secondary)] border-b border-[var(--border)] px-8 py-5 flex items-center justify-between">
<!-- Full-width Navbar -->
<div class="bg-[var(--bg-secondary)] border-b border-[var(--border)]">
<div class="max-w-[1600px] mx-auto px-8 py-5 flex items-center justify-between">
<div>
<h1 onclick="resetToHome()"
class="text-2xl font-bold flex items-center gap-3 cursor-pointer hover:text-[var(--accent)] transition-colors">
@@ -195,10 +238,6 @@
</div>
<div class="flex items-center gap-3">
<div id="status" class="text-xs px-3 py-1 rounded bg-[var(--card)] text-[var(--accent-warm)] font-medium">
<span id="count">0</span> test cases loaded
</div>
<button onclick="saveAll()"
class="h-10 px-4 nav-button rounded-lg font-semibold flex items-center gap-2 text-sm">
<i class="fa-solid fa-save"></i>
@@ -219,8 +258,10 @@
</button>
</div>
</div>
</div>
<!-- Constrained content -->
<div class="max-w-[1600px] mx-auto">
<!-- Groups Overview (high-level bird's eye view) -->
<div class="px-8 pt-2">
@@ -231,6 +272,15 @@
<div id="groupsOverview" class="min-h-[120px]"></div>
</div>
<!-- Tests overview -->
<div class="px-8 pt-3 pb-1 flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="text-sm font-semibold text-[#a0a0b0]">Tests overview</span>
<span id="visibleCount" class="text-xs text-[var(--accent-warm)] font-medium">0 tests</span>
</div>
<span id="saveFeedback" class="text-xs text-amber-400"></span>
</div>
<!-- Table -->
<div class="px-8 pt-4 pb-12">
<!-- Selection Toolbar -->
@@ -270,15 +320,10 @@
<!-- Modal Header -->
<div class="px-6 py-4 border-b border-[var(--border)] flex items-center justify-between bg-[var(--input)] rounded-t-2xl">
<div>
<div id="modalId" class="font-mono text-sm text-[#94a3b8]"></div>
<div id="modalId" class="font-mono text-sm text-[var(--accent-warm)]"></div>
<div id="modalGroup" class="text-xs text-[#64748b]"></div>
</div>
<div class="flex items-center gap-3">
<select id="modalStatus" class="bg-[var(--input)] border border-[var(--border)] 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-[var(--input)] text-[var(--accent)] border border-[var(--accent)] hover:bg-[#2f2a3f] rounded flex items-center gap-1 whitespace-nowrap">
<i class="fa-solid fa-magic"></i>
@@ -289,14 +334,42 @@
</div>
<div class="p-6 overflow-auto flex-1 space-y-6 text-sm">
<!-- Description -->
<!-- 1. Description -->
<div>
<div class="section-title mb-1.5">Description</div>
<textarea id="modalDescription" rows="2"
class="w-full bg-[var(--input)] border border-[var(--border)] rounded-lg p-3 text-sm"></textarea>
</div>
<!-- Steps -->
<!-- 2. Automatable -->
<div>
<div class="flex items-center gap-2">
<input type="checkbox" id="automatableCheckbox" class="w-4 h-4" onchange="toggleAutomationTool()">
<label for="automatableCheckbox" class="section-title text-emerald-400 cursor-pointer">Automatable</label>
</div>
<!-- Automation tool selector (shown only when Automatable is checked) -->
<div id="automationToolContainer" class="mt-2 hidden">
<div class="text-xs text-[#64748b] mb-1">Automation tool</div>
<select id="automationTool" class="bg-[var(--input)] border border-[var(--border)] rounded px-3 py-1 text-sm w-full">
<option value="">Select tool...</option>
<option value="agent-ai">Agent AI</option>
<option value="playwright">Playwright</option>
<option value="paramiko">Paramiko</option>
</select>
</div>
</div>
<!-- 3. Dependencies -->
<div>
<div class="flex justify-between items-center mb-1.5">
<div class="section-title">Dependencies</div>
<button onclick="addDependency()" class="text-xs px-2.5 py-0.5 bg-[var(--border)] hover:bg-[var(--border-light)] rounded">+ Add</button>
</div>
<div id="depContainer" class="space-y-1"></div>
</div>
<!-- 4. Test Steps (action + expected result only) -->
<div>
<div class="flex justify-between items-center mb-1.5">
<div class="section-title">Test Steps</div>
@@ -304,64 +377,14 @@
</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-[#a277ff]">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-[var(--input)] border border-[var(--border)] 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-[var(--border)] hover:bg-[var(--border-light)] rounded text-[var(--accent-warm)]">+ 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-[var(--border)] hover:bg-[var(--border-light)] rounded">+ Add dependency</button>
</div>
<div id="depContainer" class="space-y-1"></div>
</div>
</div>
<div class="px-6 py-4 border-t border-[var(--border)] bg-[var(--input)] rounded-b-2xl flex justify-end gap-3">
<button onclick="closeModal()" class="px-5 py-2 text-sm rounded-lg bg-[var(--border)] hover:bg-[var(--border-light)]">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>
<button onclick="saveCurrentTest()"
class="px-5 py-2 text-sm rounded-lg bg-[var(--input)] text-[var(--accent)] border border-[var(--accent)] hover:bg-[#2f2a3f] font-semibold">
Save
</button>
</div>
</div>
</div>
@@ -381,7 +404,6 @@
renderTable();
renderGroupsOverview();
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);
@@ -611,6 +633,7 @@
tbody.innerHTML = html;
updateSelectionCount();
updateVisibleCount();
}
function resetFilters() {
@@ -707,6 +730,14 @@
}
}
function updateVisibleCount() {
const el = document.getElementById('visibleCount');
if (el) {
const n = filteredTests.length;
el.textContent = `${n} test${n === 1 ? '' : 's'}`;
}
}
// Unified Plans modal (Create + Load)
@@ -864,24 +895,26 @@
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;
document.getElementById('modalDescription').value = currentTest.description || '';
// Code Generation fields (Phase 1)
const fullyAutomatedEl = document.getElementById('fullyAutomated');
fullyAutomatedEl.checked = !!currentTest.fullyAutomated;
// Automatable checkbox + tool selector
const autoCb = document.getElementById('automatableCheckbox');
if (typeof currentTest.automatable === 'boolean') {
autoCb.checked = currentTest.automatable;
} else {
autoCb.checked = currentTest.status === 'automated';
}
const notesEl = document.getElementById('implementationNotes');
notesEl.value = currentTest.implementationNotes || '';
const toolSelect = document.getElementById('automationTool');
if (toolSelect) {
toolSelect.value = currentTest.automationTool || '';
}
// Code Steps
renderCodeSteps(currentTest.codeSteps || []);
toggleAutomationTool();
// Render lists
renderRichSteps(currentTest.steps || []);
renderList('listAutomatable', currentTest.automatable || []);
renderList('listNotYet', currentTest.notYet || []);
renderDependencies(currentTest.dependencies || []);
// Render the 4 definition fields
renderRichSteps(currentTest.steps || []);
renderDependencies(currentTest.dependencies || []);
document.getElementById('editModal').classList.remove('hidden');
document.getElementById('editModal').classList.add('flex');
@@ -921,34 +954,45 @@
renderList(id, items);
}
// === Simplified step editor (automation decision moved to testcase level) ===
// === Step editor: action + expected result only (notes removed per definition rules) ===
function renderRichSteps(steps) {
const ul = document.getElementById('listSteps');
ul.innerHTML = '';
const rich = steps.map(s => {
if (typeof s === 'string') {
return { action: s, expected: '', notes: '' };
return { action: s, expected: '' };
}
return {
action: s.action || s,
expected: s.expected || '',
notes: s.notes || ''
expected: s.expected || ''
};
});
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, '&quot;')}" placeholder="Action" style="flex:1" oninput="updateRichStep(${idx}, 'action', this.value)">
<button onclick="removeRichStep(${idx})" style="color:#f87171;"><i class="fa-solid fa-trash text-xs"></i></button>
<div class="step-card">
<div class="step-header">
<span class="step-number">${idx + 1}</span>
<input
value="${(step.action || '').replace(/"/g, '&quot;')}"
placeholder="Action / Description"
style="flex:1; font-size:13px;"
oninput="updateRichStep(${idx}, 'action', this.value)">
<button onclick="removeRichStep(${idx})" style="color:#f87171; padding: 2px 4px;">
<i class="fa-solid fa-trash text-xs"></i>
</button>
</div>
<div class="step-expected">
<span class="step-label">Expected result</span>
<input
value="${(step.expected || '').replace(/"/g, '&quot;')}"
placeholder="What should happen after this step"
style="width:100%;"
oninput="updateRichStep(${idx}, 'expected', this.value)">
</div>
</div>
<input value="${(step.expected || '').replace(/"/g, '&quot;')}" placeholder="Expected result (optional)" style="font-size:12px; margin-top:2px;" oninput="updateRichStep(${idx}, 'expected', this.value)">
<input value="${(step.notes || '').replace(/"/g, '&quot;')}" placeholder="Notes (optional)" style="font-size:12px; margin-top:2px; color:#94a3b8;" oninput="updateRichStep(${idx}, 'notes', this.value)">
`;
ul.appendChild(li);
});
@@ -973,10 +1017,18 @@
function addRichStep() {
const ul = document.getElementById('listSteps');
const steps = JSON.parse(ul.dataset.richSteps || '[]');
steps.push({ action: '', expected: '', notes: '' });
steps.push({ action: '', expected: '' });
renderRichSteps(steps);
}
function toggleAutomationTool() {
const cb = document.getElementById('automatableCheckbox');
const container = document.getElementById('automationToolContainer');
if (container) {
container.classList.toggle('hidden', !cb.checked);
}
}
function renderDependencies(deps) {
const container = document.getElementById('depContainer');
container.innerHTML = deps.map((d, i) => `
@@ -1010,75 +1062,16 @@
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-[var(--input)] 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 resetToSuggested() {
if (!currentTest) return;
// Build a minimal test object from current modal values
// Build a minimal test object from current modal values (no status select anymore)
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
description: document.getElementById('modalDescription').value.trim()
};
try {
@@ -1089,15 +1082,19 @@
});
const rich = await res.json();
// Repopulate lists with suggested values
// Repopulate only the fields the modal still edits
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);
// Set the simple automatable checkbox from the suggestion if possible
const autoCb = document.getElementById('automatableCheckbox');
if (rich.automatable && rich.automatable.length > 0) {
autoCb.checked = true;
}
toggleAutomationTool();
renderRichSteps(currentTest.steps);
renderDependencies(currentTest.dependencies);
} catch (e) {
@@ -1109,33 +1106,27 @@
function saveCurrentTest() {
if (currentIndex < 0) return;
// Collect values
// Collect the 4 definition fields (aligned with YAML)
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;
// Automatable as boolean + keep status in sync for the rest of the system
const autoChecked = document.getElementById('automatableCheckbox').checked;
currentTest.automatable = autoChecked;
currentTest.status = autoChecked ? 'automated' : 'manual';
const notesEl = document.getElementById('implementationNotes');
currentTest.implementationNotes = notesEl.value.trim() || undefined;
// Save selected automation tool (only meaningful when automatable)
const toolSelect = document.getElementById('automationTool');
if (autoChecked && toolSelect) {
currentTest.automationTool = toolSelect.value || undefined;
} else {
currentTest.automationTool = undefined;
}
// Code Steps
const codeStepsContainer = document.getElementById('codeStepsContainer');
currentTest.codeSteps = JSON.parse(codeStepsContainer.dataset.codeSteps || '[]')
.filter(s => s.stepName || s.actionKey);
// Collect steps (now simplified: action + expected + optional notes)
// Steps: only action + expected (notes are never written from the modal anymore)
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);
@@ -1147,8 +1138,8 @@
closeModal();
renderTable();
document.getElementById('status').classList.add('dirty');
document.getElementById('status').innerHTML = '<span class="text-amber-400">Unsaved changes</span>';
const fb = document.getElementById('saveFeedback');
if (fb) fb.textContent = 'Unsaved changes';
}
function closeModal() {
@@ -1178,10 +1169,10 @@
if (json.success) {
isDirty = false;
document.getElementById('status').classList.remove('dirty');
document.getElementById('status').innerHTML = `<span id="count">${allTests.length}</span> test cases saved`;
const fb = document.getElementById('saveFeedback');
if (fb) fb.textContent = 'Saved!';
// Show temporary success feedback
// Show temporary success feedback on the button
if (saveBtn) {
saveBtn.innerHTML = `<i class="fa-solid fa-check"></i> <span>Saved!</span>`;
saveBtn.style.borderColor = '#6cc76f';
@@ -1195,6 +1186,8 @@
saveBtn.style.color = '';
saveBtn.disabled = false;
}
const fb = document.getElementById('saveFeedback');
if (fb) fb.textContent = '';
}, 1800);
} else {
throw new Error(json.error || 'Unknown error');
@@ -1222,8 +1215,8 @@
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`;
const fb = document.getElementById('saveFeedback');
if (fb) fb.textContent = '';
}
// Keyboard shortcuts
@@ -1243,5 +1236,6 @@
loadData();
};
</script>
</div> <!-- close max-w content container -->
</body>
</html>