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 app = express(); const PORT = process.env.PORT || 4321; const DATA_FILE = path.join(__dirname, 'data', 'testcases.json'); app.use(cors()); app.use(express.json({ limit: '10mb' })); app.use(express.static(path.join(__dirname, 'public'))); // Get all testcases app.get('/api/testcases', (req, res) => { try { if (!fs.existsSync(DATA_FILE)) { fs.writeFileSync(DATA_FILE, '[]', 'utf8'); } const data = fs.readFileSync(DATA_FILE, 'utf8'); res.json(JSON.parse(data)); } catch (e) { res.status(500).json({ error: 'Could not read testcases data' }); } }); // Save all testcases (overwrite) 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' }); } fs.writeFileSync(DATA_FILE, JSON.stringify(testcases, null, 2), 'utf8'); res.json({ success: true, count: testcases.length }); } 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 = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8')); 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 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' }); } const allTests = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8')); 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: '' })) }; 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')); 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() }; fs.writeFileSync(planPath, JSON.stringify(updated, null, 2)); res.json({ success: true }); } catch (e) { res.status(500).json({ error: 'Failed to save 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); }); // Health app.get('/api/health', (req, res) => res.json({ status: 'ok' })); app.listen(PORT, () => { console.log(`\nšŸš€ Test Plan Editor running at http://localhost:${PORT}`); console.log(` Data file: ${DATA_FILE}\n`); });