Files
autotest/server/index.js
T

255 lines
8.0 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 { 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
fs.writeFileSync(DATA_FILE, JSON.stringify(testcases, null, 2), 'utf8');
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 = 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🚀 Autotest Editor running at http://localhost:${PORT}`);
console.log(` Primary data source: YAML files under testcases/ and testplans/\n`);
});