250 lines
7.6 KiB
JavaScript
250 lines
7.6 KiB
JavaScript
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 = `<!DOCTYPE html>
|
|
<html><head><meta charset="utf-8"><title>Test Plan Summary</title>
|
|
<style>body{font-family:system-ui;background:#0b1120;color:#e2e8f0;padding:40px} .card{background:#1e2937;padding:24px;border-radius:12px}</style>
|
|
</head><body>
|
|
<h1>Test Automation Summary</h1>
|
|
<div class="card">
|
|
<h2>${total} Test Cases</h2>
|
|
<h3 style="color:#4ade80">${automated} Automated (${autoPct}%)</h3>
|
|
<p>Generated on ${new Date().toLocaleString()}</p>
|
|
</div>
|
|
</body></html>`;
|
|
|
|
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);
|
|
});
|
|
|
|
// 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🚀 Test Plan Editor running at http://localhost:${PORT}`);
|
|
console.log(` Data file: ${DATA_FILE}\n`);
|
|
});
|