const express = require('express'); const fs = require('fs'); const path = require('path'); const cors = require('cors'); const { enrichTestCase } = require('./lib/enrich'); const { generateReport } = require('./scripts/generate-report'); const { generateTestPlan } = require('./scripts/generate-test-plan'); const { loadTestcases } = require('./lib/load-testcases'); const { saveTestcases } = require('./lib/save-testcases'); const app = express(); const PORT = process.env.PORT || 4321; const DATA_FILE = path.join(__dirname, '..', 'editor', 'data', 'testcases.json'); // legacy transition only app.use(cors()); app.use(express.json({ limit: '10mb' })); app.use(express.static(path.join(__dirname, '..', 'web'))); // Get all testcases - primary source is now the YAML tree under testcases/ app.get('/api/testcases', (req, res) => { try { const testcases = loadTestcases(); res.json(testcases); } catch (e) { res.status(500).json({ error: 'Could not read testcases data' }); } }); // Save all testcases - writes to YAML files (source of truth) + legacy JSON during transition app.post('/api/testcases', (req, res) => { try { const testcases = req.body; if (!Array.isArray(testcases)) { return res.status(400).json({ error: 'Body must be an array of testcases' }); } // Primary: persist to YAML tree saveTestcases(testcases); // Transition: still write the legacy JSON so other tools don't break yet (best effort) try { const legacyDir = path.dirname(DATA_FILE); if (!fs.existsSync(legacyDir)) { fs.mkdirSync(legacyDir, { recursive: true }); } fs.writeFileSync(DATA_FILE, JSON.stringify(testcases, null, 2), 'utf8'); } catch (legacyErr) { console.warn('Warning: Could not write legacy JSON file:', legacyErr.message); // Continue anyway - YAML is the source of truth now } res.json({ success: true, count: testcases.length, source: 'yaml+json' }); } catch (e) { res.status(500).json({ error: 'Failed to save data' }); } }); // Enrich a single test case with smart defaults (used by the editor "Reset suggestions" button) app.post('/api/enrich', (req, res) => { try { const test = req.body; if (!test || !test.id) { return res.status(400).json({ error: 'Test object with id is required' }); } const enriched = enrichTestCase(test); res.json(enriched); } catch (e) { res.status(500).json({ error: 'Enrichment failed' }); } }); // ==================== EXPORT ENDPOINTS ==================== // Full automation report (timestamped qa_automation.html) app.get('/api/export/report', (req, res) => { try { const generatedPath = generateReport(); const html = fs.readFileSync(generatedPath, 'utf8'); const filename = path.basename(generatedPath); res.setHeader('Content-Type', 'text/html'); res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); res.send(html); } catch (e) { res.status(500).send('Error generating report: ' + e.message); } }); // Simple summary HTML (lightweight) app.get('/api/export/summary', (req, res) => { try { const tests = loadTestcases(); const total = tests.length; const automated = tests.filter(t => t.status === 'automated').length; const autoPct = ((automated / total) * 100).toFixed(1); const html = ` Test Plan Summary

Test Automation Summary

${total} Test Cases

${automated} Automated (${autoPct}%)

Generated on ${new Date().toLocaleString()}

`; res.setHeader('Content-Type', 'text/html'); res.setHeader('Content-Disposition', 'attachment; filename="QA-Automation-Summary-Report.html"'); res.send(html); } catch (e) { res.status(500).send('Error: ' + e.message); } }); // ==================== TEST PLANS (Persistent & Editable) ==================== const PLANS_DIR = path.join(__dirname, '..', 'output', 'testplans', 'plans'); // Ensure plans dir if (!fs.existsSync(PLANS_DIR)) fs.mkdirSync(PLANS_DIR, { recursive: true }); function computeAutomationSummary(tests) { const total = tests.length; let automated = 0, partial = 0, manual = 0; tests.forEach(t => { const status = t.automation_status || t.status || 'manual'; if (status === 'automated') automated++; else if (status === 'partial') partial++; else manual++; }); const automatable = automated + partial; const automatable_percentage = total > 0 ? Math.round((automatable / total) * 100 * 10) / 10 : 0; return { total, automated, partial, manual, automatable_percentage }; } function getPlanPath(planId) { return path.join(PLANS_DIR, `${planId}.json`); } // Create a new Test Plan from selected IDs app.post('/api/test-plans', (req, res) => { try { const { ids = [], name = 'Manual Test Plan' } = req.body; if (!Array.isArray(ids) || ids.length === 0) { return res.status(400).json({ error: 'No test IDs provided' }); } // Use the proper loader instead of the broken legacy JSON file const allTests = loadTestcases(); const selectedTests = allTests.filter(t => ids.includes(t.id)); if (selectedTests.length === 0) { return res.status(400).json({ error: 'No valid tests found' }); } const planId = Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8); const now = new Date().toISOString(); const plan = { id: planId, name: name || 'Manual Test Plan', createdAt: now, updatedAt: now, tests: selectedTests.map(t => ({ ...t, severity: 'High', expectedResult: t.description || '', actualResult: '', testerStatus: 'N/A', comments: '' })) }; plan.automation_summary = computeAutomationSummary(plan.tests); fs.writeFileSync(getPlanPath(planId), JSON.stringify(plan, null, 2)); res.json({ success: true, planId, name: plan.name }); } catch (e) { res.status(500).json({ error: 'Failed to create Test Plan: ' + e.message }); } }); // Get a plan app.get('/api/test-plans/:id', (req, res) => { try { const planPath = getPlanPath(req.params.id); if (!fs.existsSync(planPath)) return res.status(404).json({ error: 'Plan not found' }); const plan = JSON.parse(fs.readFileSync(planPath, 'utf8')); if (!plan.automation_summary && plan.tests) { plan.automation_summary = computeAutomationSummary(plan.tests); } res.json(plan); } catch (e) { res.status(500).json({ error: 'Error loading plan' }); } }); // Save/update a plan (for auto-save from tester editor) app.post('/api/test-plans/:id', (req, res) => { try { const planPath = getPlanPath(req.params.id); if (!fs.existsSync(planPath)) return res.status(404).json({ error: 'Plan not found' }); const existing = JSON.parse(fs.readFileSync(planPath, 'utf8')); const updated = { ...existing, ...req.body, updatedAt: new Date().toISOString() }; updated.automation_summary = computeAutomationSummary(updated.tests || []); fs.writeFileSync(planPath, JSON.stringify(updated, null, 2)); res.json({ success: true }); } catch (e) { res.status(500).json({ error: 'Failed to save plan' }); } }); // Delete a plan app.delete('/api/test-plans/:id', (req, res) => { try { const planPath = getPlanPath(req.params.id); if (!fs.existsSync(planPath)) { return res.status(404).json({ error: 'Plan not found' }); } fs.unlinkSync(planPath); res.json({ success: true }); } catch (e) { res.status(500).json({ error: 'Failed to delete plan' }); } }); // List all plans (for future tester portal) app.get('/api/test-plans', (req, res) => { try { const files = fs.readdirSync(PLANS_DIR).filter(f => f.endsWith('.json')); const plans = files.map(file => { const data = JSON.parse(fs.readFileSync(path.join(PLANS_DIR, file), 'utf8')); return { id: data.id, name: data.name, createdAt: data.createdAt, updatedAt: data.updatedAt, testCount: data.tests ? data.tests.length : 0 }; }).sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)); res.json(plans); } catch (e) { res.json([]); } }); // Raw JSON app.get('/api/export/json', (req, res) => { res.setHeader('Content-Disposition', 'attachment; filename="testcases.json"'); res.sendFile(DATA_FILE); }); // Export Automated Playwright Tests (uses the generator script) app.post('/api/export/automated-tests', (req, res) => { try { const { group } = req.body; if (!group) { return res.status(400).json({ error: 'Group is required for now' }); } const { execSync } = require('child_process'); const scriptPath = path.join(__dirname, 'scripts', 'generate-playwright-tests.js'); // Generate for the specific group execSync(`node ${scriptPath} --group ${group}`, { stdio: 'inherit' }); res.json({ success: true, message: `Automated tests generated for group ${group}`, group }); } catch (e) { console.error(e); res.status(500).json({ error: 'Failed to export automated tests: ' + e.message }); } }); // Health app.get('/api/health', (req, res) => res.json({ status: 'ok' })); app.listen(PORT, () => { console.log(`\nšŸš€ Autotest Editor running at http://localhost:${PORT}`); console.log(` Primary data source: YAML files under testcases/ and testplans/\n`); });