añadido karpathy y refactorizacion del proyecto

This commit is contained in:
2026-05-25 19:48:56 +02:00
parent 8feb88dfa6
commit ee8cfa03ea
1225 changed files with 875463 additions and 5909 deletions
@@ -0,0 +1,29 @@
{
"_comment": "Action Catalog for generating Playwright + MCP tests from editor data.",
"_version": "0.1.0",
"_targetGroup": "core-documentation",
"actions": {
"open_admin_portal": {
"template": "await page.goto(process.env.BASE_URL ?? 'https://pbx.local:4443');"
},
"navigate_to_downloads_portal": {
"template": "await page.goto(REPO_URL);"
},
"search_for_document": {
"template": "const link = page.locator('a, li, td').filter({ hasText: /{{pattern}}/i });\nawait expect(link.first()).toBeVisible();",
"params": ["pattern"]
},
"assert_document_visible": {
"template": "const el = page.locator('a, li, td').filter({ hasText: /{{pattern}}/i });\nawait expect(el.first()).toBeVisible();",
"params": ["pattern"]
},
"extract_text_content": {
"template": "const content = await page.locator('body').innerText();\n// TODO: Parse and save the relevant section (Release Notes, Changelog, etc.)"
}
}
}
+23
View File
@@ -0,0 +1,23 @@
const fs = require('fs');
const path = require('path');
const CATALOG_PATH = path.join(__dirname, 'catalog.json');
let cachedCatalog = null;
function loadActionCatalog(forceReload = false) {
if (cachedCatalog && !forceReload) {
return cachedCatalog;
}
const raw = fs.readFileSync(CATALOG_PATH, 'utf-8');
cachedCatalog = JSON.parse(raw);
return cachedCatalog;
}
function getAction(actionKey) {
const catalog = loadActionCatalog();
return catalog.actions ? catalog.actions[actionKey] : undefined;
}
module.exports = { loadActionCatalog, getAction };
+33
View File
@@ -0,0 +1,33 @@
import fs from 'fs';
import path from 'path';
export interface ActionDefinition {
template: string;
params?: string[];
}
export interface ActionCatalog {
_comment?: string;
_version?: string;
_targetGroup?: string;
actions: Record<string, ActionDefinition>;
}
const CATALOG_PATH = path.join(__dirname, 'catalog.json');
let cachedCatalog: ActionCatalog | null = null;
export function loadActionCatalog(forceReload = false): ActionCatalog {
if (cachedCatalog && !forceReload) {
return cachedCatalog;
}
const raw = fs.readFileSync(CATALOG_PATH, 'utf-8');
cachedCatalog = JSON.parse(raw);
return cachedCatalog!;
}
export function getAction(actionKey: string): ActionDefinition | undefined {
const catalog = loadActionCatalog();
return catalog.actions[actionKey];
}
+135
View File
@@ -0,0 +1,135 @@
/**
* generate-playwright-tests.js
*
* Generates Playwright + MCP spec files from the editor's test data.
*/
const fs = require('fs');
const path = require('path');
const { loadActionCatalog, getAction } = require('./action-catalog/index.js');
const DATA_FILE = path.join(__dirname, '..', '..', 'legacy', 'data', 'testcases.json'); // legacy JSON (for export scripts)
const OUTPUT_BASE = path.join(__dirname, '..', '..', 'tests', 'generated');
function parseArgs() {
const args = process.argv.slice(2);
const options = {};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--group' && args[i + 1]) options.group = args[++i];
if (args[i] === '--dry-run') options.dryRun = true;
}
return options;
}
function loadTests() {
return JSON.parse(fs.readFileSync(DATA_FILE, 'utf-8'));
}
function getTestsToGenerate(tests, options) {
let filtered = tests.filter(t => t.fullyAutomated === true || t.status === 'automated');
if (options.group) {
filtered = filtered.filter(t => t.group === options.group);
}
return filtered;
}
function generateSpecFile(group, tests, catalog) {
let code = `import { test, expect } from '@playwright/test';\n\n`;
code += `// === AUTOGENERATED BY AUTOTEST EDITOR ===\n`;
code += `// Group: ${group}\n`;
code += `// Generated: ${new Date().toISOString()}\n\n`;
code += `test.describe('${group}', () => {\n`;
code += ` test.use({ ignoreHTTPSErrors: true });\n\n`;
for (const test of tests) {
code += generateSingleTest(test, catalog);
}
code += `});\n`;
return code;
}
function generateSingleTest(test, catalog) {
let body = '';
const steps = test.codeSteps && test.codeSteps.length > 0
? test.codeSteps
: (test.steps || []).map(s => ({ stepName: s, actionKey: 'search_for_document' }));
for (const step of steps) {
const action = getAction(step.actionKey);
if (action) {
let rendered = action.template;
if (step.params) {
Object.keys(step.params).forEach(key => {
rendered = rendered.replace(new RegExp(`{{${key}}}`, 'g'), step.params[key]);
});
}
body += ` await test.step('${step.stepName}', async () => {\n`;
body += ` ${rendered}\n`;
body += ` });\n\n`;
} else {
body += ` await test.step('${step.stepName}', async () => {\n`;
body += ` // TODO: Implement action "${step.actionKey}"\n`;
body += ` });\n\n`;
}
}
let testCode = ` test('${test.id}', async ({ page }, testInfo) => {\n`;
testCode += ` testInfo.annotations.push({ type: 'automationStatus', description: '${test.status}' });\n`;
testCode += ` testInfo.annotations.push({ type: 'testType', description: '${test.action}' });\n`;
testCode += ` testInfo.annotations.push({ type: 'description', description: '${test.description}' });\n\n`;
testCode += body;
testCode += ` });\n\n`;
return testCode;
}
function ensureDir(dir) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}
function main() {
const options = parseArgs();
const tests = loadTests();
const catalog = loadActionCatalog();
const toGenerate = getTestsToGenerate(tests, options);
if (toGenerate.length === 0) {
console.log('No automated tests found for the given filters.');
return;
}
const byGroup = {};
toGenerate.forEach(t => {
if (!byGroup[t.group]) byGroup[t.group] = [];
byGroup[t.group].push(t);
});
for (const group of Object.keys(byGroup)) {
const groupTests = byGroup[group];
const moduleFolder = group.startsWith('core-') ? 'core' : 'pbx';
const outputDir = path.join(OUTPUT_BASE, moduleFolder);
ensureDir(outputDir);
const fileName = group.replace('core-', '').replace('pbx-', '') + '.spec.ts';
const outputPath = path.join(outputDir, fileName);
const fileContent = generateSpecFile(group, groupTests, catalog);
if (options.dryRun) {
console.log(`\n=== DRY RUN: ${outputPath} ===\n`);
console.log(fileContent);
} else {
fs.writeFileSync(outputPath, fileContent, 'utf-8');
console.log(`✅ Generated: ${outputPath} (${groupTests.length} tests)`);
}
}
}
main();
+150
View File
@@ -0,0 +1,150 @@
/**
* generate-report.js
* Generates the automation analysis report (qa_automation.html) with timestamp.
* Output location: editor/output/reports/
*/
const fs = require('fs');
const path = require('path');
const DATA_FILE = path.join(__dirname, '..', '..', 'legacy', 'data', 'testcases.json'); // legacy JSON (for export scripts)
const OUTPUT_DIR = path.join(__dirname, '..', '..', 'output', 'reports');
function generateReport() {
const tests = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8'));
// Calculate statistics
const total = tests.length;
const automated = tests.filter(t => t.status === 'automated').length;
const partial = tests.filter(t => t.status === 'partial').length;
const manual = tests.filter(t => t.status === 'manual').length;
const autoPct = total > 0 ? ((automated / total) * 100).toFixed(1) : 0;
const partialPct = total > 0 ? ((partial / total) * 100).toFixed(1) : 0;
const manualPct = total > 0 ? ((manual / total) * 100).toFixed(1) : 0;
// Group counts for the bar chart
const groups = [...new Set(tests.map(t => t.group))].sort();
const groupStats = {};
groups.forEach(g => {
groupStats[g] = {
automated: tests.filter(t => t.group === g && t.status === 'automated').length,
partial: tests.filter(t => t.group === g && t.status === 'partial').length,
manual: tests.filter(t => t.group === g && t.status === 'manual').length
};
});
// Embed data
const embeddedData = JSON.stringify(tests);
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>QA Automation Test Plan Report</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body { background: #0b1120; color: #e2e8f0; font-family: system-ui, -apple-system, sans-serif; }
.card { background: #1e2937; border: 1px solid #334155; }
.status-automated { background:#052e16; color:#4ade80; }
.status-partial { background:#451a03; color:#fbbf24; }
.status-manual { background:#450a0a; color:#f87171; }
</style>
</head>
<body class="text-sm">
<div class="max-w-[1500px] mx-auto p-8">
<div class="flex justify-between items-end mb-8">
<div>
<h1 class="text-3xl font-bold">QA Automation Test Plan Report</h1>
<p class="text-[#64748b]">Generated from Test Case Editor • ${new Date().toISOString().slice(0,10)}</p>
</div>
<div class="text-right text-xs text-[#64748b]">
${total} test cases • ${autoPct}% automated
</div>
</div>
<!-- Summary Cards -->
<div class="grid grid-cols-4 gap-4 mb-8">
<div class="card p-5 rounded-2xl">
<div class="text-[#64748b] text-xs">TOTAL TESTS</div>
<div class="text-5xl font-bold mt-1">${total}</div>
</div>
<div class="card p-5 rounded-2xl border-l-4 border-emerald-500">
<div class="text-emerald-400 text-xs">AUTOMATED</div>
<div class="text-5xl font-bold text-emerald-400 mt-1">${automated}</div>
<div class="text-xs mt-1">${autoPct}%</div>
</div>
<div class="card p-5 rounded-2xl border-l-4 border-amber-500">
<div class="text-amber-400 text-xs">PARTIAL</div>
<div class="text-5xl font-bold text-amber-400 mt-1">${partial}</div>
<div class="text-xs mt-1">${partialPct}%</div>
</div>
<div class="card p-5 rounded-2xl border-l-4 border-red-500">
<div class="text-red-400 text-xs">MANUAL</div>
<div class="text-5xl font-bold text-red-400 mt-1">${manual}</div>
<div class="text-xs mt-1">${manualPct}%</div>
</div>
</div>
<div class="mb-4 text-[#64748b] text-xs font-semibold">TEST CASES</div>
<div class="card rounded-2xl overflow-hidden">
<table class="w-full">
<thead class="bg-[#0f172a] text-[#64748b] text-xs">
<tr>
<th class="px-4 py-3 text-left">#</th>
<th class="px-4 py-3 text-left">ID</th>
<th class="px-4 py-3 text-left">Group</th>
<th class="px-4 py-3">Status</th>
<th class="px-4 py-3 text-left">Description</th>
</tr>
</thead>
<tbody class="divide-y divide-[#334155]">
${tests.map((t, i) => `
<tr class="hover:bg-[#162032]">
<td class="px-4 py-3 text-[#64748b]">${i+1}</td>
<td class="px-4 py-3 font-mono text-xs">${t.id}</td>
<td class="px-4 py-3">${t.group}</td>
<td class="px-4 py-3">
<span class="px-3 py-0.5 rounded-full text-xs font-semibold status-${t.status}">${t.status}</span>
</td>
<td class="px-4 py-3 text-[#cbd5e1]">${t.description}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
<div class="mt-8 text-center text-xs text-[#64748b]">
Generated by Autotest • Data source: YAML under testcases/ (legacy JSON during transition)
</div>
</div>
</body>
</html>`;
// Ensure output directory exists
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}
// Generate timestamped filename
const now = new Date();
const timestamp = now.toISOString().slice(0, 19).replace(/:/g, '-').replace('T', '_');
const finalFilename = `${timestamp}_qa_automation.html`;
const finalPath = path.join(OUTPUT_DIR, finalFilename);
fs.writeFileSync(finalPath, html);
console.log(`✅ Automation report generated: ${finalPath}`);
console.log(` ${total} tests | ${automated} automated (${autoPct}%)`);
return finalPath;
}
if (require.main === module) {
generateReport();
}
module.exports = { generateReport };
+150
View File
@@ -0,0 +1,150 @@
/**
* generate-test-plan.js
* Generates a self-contained, fully inline-styled HTML Test Plan
* for manual execution by testers.
*
* Output: output/testplans/YYYY-MM-DD_HH-MM-SS_{baseName}.html
*/
const fs = require('fs');
const path = require('path');
const OUTPUT_DIR = path.join(__dirname, '..', '..', 'output', 'testplans');
function generateTestPlan(selectedTests, baseName = 'Manual-Test-Plan') {
if (!Array.isArray(selectedTests) || selectedTests.length === 0) {
throw new Error('No test cases selected for the Test Plan');
}
// Ensure output directory exists
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}
// Generate timestamped filename
const now = new Date();
const timestamp = now.toISOString().slice(0, 19).replace(/:/g, '-').replace('T', '_');
const safeBase = baseName.replace(/[^a-zA-Z0-9-_]/g, '').slice(0, 60) || 'Manual-Test-Plan';
const filename = `${timestamp}_${safeBase}.html`;
const finalPath = path.join(OUTPUT_DIR, filename);
// Group tests by group
const grouped = {};
selectedTests.forEach(test => {
const g = test.group || 'Other';
if (!grouped[g]) grouped[g] = [];
grouped[g].push(test);
});
const groups = Object.keys(grouped).sort();
// Build HTML
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Manual Test Plan - ${safeBase}</title>
<style>
body { font-family: Arial, Helvetica, sans-serif; font-size: 13px; line-height: 1.45; color: #222; margin: 20px; background: #fff; }
h1 { font-size: 22px; margin-bottom: 4px; }
h2 { font-size: 16px; margin-top: 24px; margin-bottom: 8px; border-bottom: 2px solid #333; padding-bottom: 4px; }
table { width: 100%; border-collapse: collapse; margin-bottom: 16px; }
th, td { border: 1px solid #999; padding: 6px 8px; vertical-align: top; }
th { background: #f0f0f0; text-align: left; font-weight: 600; }
.meta { margin-bottom: 16px; color: #555; }
.test-id { font-family: monospace; font-weight: 600; }
.steps { margin: 4px 0; padding-left: 18px; }
.field-label { font-weight: 600; font-size: 11px; color: #444; margin-top: 6px; }
textarea, select { width: 100%; box-sizing: border-box; font-size: 12px; }
textarea { min-height: 50px; }
.header { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 12px; }
.generated { font-size: 11px; color: #666; }
</style>
</head>
<body>
<div class="header">
<div>
<h1>Manual Test Plan</h1>
<div class="meta"><strong>${safeBase}</strong> — Generated on ${now.toLocaleString()}</div>
</div>
<div class="generated">Total cases: ${selectedTests.length}</div>
</div>
<table>
<tr>
<th style="width: 18%;">Test ID</th>
<th style="width: 28%;">Description &amp; Expected Result</th>
<th style="width: 22%;">Steps</th>
<th style="width: 12%;">Severity</th>
<th style="width: 20%;">Result / Comments</th>
</tr>
${groups.map(group => `
<tr>
<td colspan="5" style="background:#e8e8e8; font-weight:700; padding:8px 10px;">${group}</td>
</tr>
${grouped[group].map(test => {
const expected = test.description || '';
const stepsHtml = (test.steps || []).map((s, i) => `<li>${escapeHtml(s)}</li>`).join('');
const depsHtml = (test.dependencies || []).map(d => `<div>${escapeHtml(d.label)}: ${escapeHtml(d.value)}</div>`).join('');
return `
<tr>
<td class="test-id">${escapeHtml(test.id)}</td>
<td>
<strong>${escapeHtml(test.description || '')}</strong>
<div class="field-label">Expected Result</div>
<textarea>${escapeHtml(expected)}</textarea>
</td>
<td>
<ol class="steps">${stepsHtml}</ol>
${depsHtml ? `<div class="field-label">Dependencies</div><div style="font-size:11px;">${depsHtml}</div>` : ''}
</td>
<td>
<select>
<option>Low</option>
<option>Medium</option>
<option selected>High</option>
<option>Critical</option>
</select>
</td>
<td>
<div class="field-label">Status</div>
<select>
<option>Pass</option>
<option>Fail</option>
<option>Blocked</option>
<option selected>N/A</option>
</select>
<div class="field-label" style="margin-top:8px;">Actual Result / Comments</div>
<textarea></textarea>
</td>
</tr>`;
}).join('')}
`).join('')}
</table>
<p style="font-size:11px; color:#666; margin-top:30px;">
Generated by QA Automation Test Plan Editor • All results and comments should be filled by the tester.
</p>
</body>
</html>`;
fs.writeFileSync(finalPath, html);
console.log(`✅ Manual Test Plan generated: ${finalPath}`);
return finalPath;
}
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
module.exports = { generateTestPlan };