vibe coded maxx
This commit is contained in:
@@ -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.)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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, '..', 'data', 'testcases.json');
|
||||
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();
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Types related to automated test code generation from the editor data.
|
||||
* These extend the base test model stored in data/testcases.json.
|
||||
*/
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// Core Generation Types
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
export interface CodeStep {
|
||||
/** Human-readable step name that will become the title of `test.step()` */
|
||||
stepName: string;
|
||||
|
||||
/** Key that maps to an entry in the Action Catalog (e.g. "navigate_to_downloads_portal") */
|
||||
actionKey: string;
|
||||
|
||||
/** Optional parameters to be substituted into the template */
|
||||
params?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface VerifiedSnippet {
|
||||
/** The step name this snippet corresponds to */
|
||||
stepName: string;
|
||||
|
||||
/** Raw Playwright / MCP code that was manually validated */
|
||||
code: string;
|
||||
}
|
||||
|
||||
export interface AutomatedTestExtension {
|
||||
/**
|
||||
* When true, this test is considered ready for full automated code generation.
|
||||
* This is stricter than `status === "automated"`.
|
||||
*/
|
||||
fullyAutomated?: boolean;
|
||||
|
||||
/**
|
||||
* Ordered list of executable steps.
|
||||
* Each step will be turned into a `await test.step(...)` block.
|
||||
*/
|
||||
codeSteps?: CodeStep[];
|
||||
|
||||
/** Free-form notes for the developer or LLM when generating code */
|
||||
implementationNotes?: string;
|
||||
|
||||
/**
|
||||
* Manually verified code snippets.
|
||||
* These take precedence over the Action Catalog when present.
|
||||
*/
|
||||
verifiedSnippets?: VerifiedSnippet[];
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// Combined Type used by the Generator
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
export interface EnrichedTestForGeneration {
|
||||
// Base fields (from testcases.json)
|
||||
id: string;
|
||||
group: string;
|
||||
module: 'core' | 'pbx';
|
||||
action: 'check' | 'write';
|
||||
description: string;
|
||||
status: 'automated' | 'partial' | 'manual';
|
||||
file: string;
|
||||
|
||||
// Existing rich metadata
|
||||
steps?: string[];
|
||||
automatable?: string[];
|
||||
notYet?: string[];
|
||||
dependencies?: Array<{ label: string; value: string }>;
|
||||
|
||||
// New fields for code generation
|
||||
fullyAutomated?: boolean;
|
||||
codeSteps?: CodeStep[];
|
||||
implementationNotes?: string;
|
||||
verifiedSnippets?: VerifiedSnippet[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Type used when the generator receives data (after merging).
|
||||
*/
|
||||
export type TestForCodeGeneration = EnrichedTestForGeneration;
|
||||
Reference in New Issue
Block a user