Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/templates.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Template Tests

on:
pull_request:
branches: [main, dev]
paths:
- 'templates/**'
- 'src/generate.js'
- 'src/dependencies.js'
- 'scripts/test-templates.js'

jobs:
test-templates:
runs-on: ubuntu-latest
timeout-minutes: 30

strategy:
fail-fast: false
matrix:
template: [portfolio, ecommerce, school, saas, blog]

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install CLI dependencies
run: npm ci

- name: Test ${{ matrix.template }} template
run: node scripts/test-single-template.js ${{ matrix.template }}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"test:templates": "node scripts/test-templates.js",
"test:templates:quick": "node scripts/test-templates.js --quick",
"lint": "eslint index.js src/",
"format": "prettier --write .",
"prepare": "husky install"
Expand Down
114 changes: 114 additions & 0 deletions scripts/test-single-template.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* Test a single template across all its architectures, designs, and sidebar options.
* Used by the CI matrix strategy to parallelize template testing.
*
* Usage: node scripts/test-single-template.js <template>
* Example: node scripts/test-single-template.js portfolio
*/

import { generateProject } from '../src/generate.js';
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';

const template = process.argv[2];

if (!template) {
console.error('Usage: node scripts/test-single-template.js <template>');
process.exit(1);
}

const DESIGNS = ['Minimal Clean', 'Dark Terminal', 'Glassmorphism'];

const ARCHITECTURES = {
portfolio: ['nextjs-monolith'],
ecommerce: ['nextjs-monolith'],
school: ['nextjs-monolith'],
saas: ['nextjs-monolith', 'vite-react'],
blog: ['nextjs-monolith'],
};

const VARIANTS = {
portfolio: 'Developer Folio',
ecommerce: 'Fashion Store',
school: 'University Portal',
saas: 'Analytics Tool',
blog: 'Tech Blog',
};

const SIDEBAR_SUPPORT = {
portfolio: false,
ecommerce: true,
school: true,
saas: true,
blog: true,
};

if (!ARCHITECTURES[template]) {
console.error(`Unknown template: ${template}`);
console.error(`Valid: ${Object.keys(ARCHITECTURES).join(', ')}`);
process.exit(1);
}

// Build test cases for this template
const tests = [];

for (const architecture of ARCHITECTURES[template]) {
for (const design of DESIGNS) {
const sidebarOptions = SIDEBAR_SUPPORT[template] ? [true, false] : [false];
for (const includeSidebar of sidebarOptions) {
tests.push({
projectName: `ci-${template}-${architecture.split('-')[0]}-${design.replace(/\s+/g, '').toLowerCase()}-${includeSidebar ? 's' : 'n'}`,
template,
variant: VARIANTS[template],
architecture,
design,
navCount: 5,
includeSidebar,
initGit: false,
enableSecurity: false,
noInstall: false,
});
}
}
}

console.log(`\nTesting ${template} template (${tests.length} combinations)...\n`);

const results = [];

for (let i = 0; i < tests.length; i++) {
const test = tests[i];
const projectPath = path.join(process.cwd(), test.projectName);

if (fs.existsSync(projectPath)) fs.rmSync(projectPath, { recursive: true, force: true });

const label = `${test.architecture} | ${test.design} | sidebar=${test.includeSidebar}`;
process.stdout.write(` [${i + 1}/${tests.length}] ${label}...`);

try {
await generateProject(test);

const buildCmd = test.architecture === 'vite-react' ? 'npx vite build' : 'npm run build';
execSync(buildCmd, { cwd: projectPath, stdio: 'pipe' });

results.push({ label, status: 'pass' });
process.stdout.write(' ✅\n');
} catch (err) {
const errorMsg = err.stderr?.toString().slice(0, 300) || err.message?.slice(0, 300) || 'Unknown error';
results.push({ label, status: 'fail', error: errorMsg });
process.stdout.write(' ❌\n');
console.error(` ${errorMsg.split('\n').slice(0, 3).join('\n ')}\n`);
} finally {
if (fs.existsSync(projectPath)) fs.rmSync(projectPath, { recursive: true, force: true });
}
}

const passed = results.filter((r) => r.status === 'pass').length;
const failed = results.filter((r) => r.status === 'fail').length;

console.log(`\n ${template}: ${passed} passed, ${failed} failed\n`);

if (failed > 0) {
process.exit(1);
}
127 changes: 127 additions & 0 deletions scripts/test-templates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* Template Integration Test Script
*
* Generates and builds every template+architecture+design combination
* to catch regressions before they reach users.
*
* Usage:
* node scripts/test-templates.js # Run all tests
* node scripts/test-templates.js --quick # Run a minimal subset (CI-friendly)
*/

import { generateProject } from '../src/generate.js';
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';

const TEMPLATES = ['portfolio', 'ecommerce', 'school', 'saas', 'blog'];
const DESIGNS = ['Minimal Clean', 'Dark Terminal', 'Glassmorphism'];

// Template → available architectures
const ARCHITECTURES = {
portfolio: ['nextjs-monolith'],
ecommerce: ['nextjs-monolith'],
school: ['nextjs-monolith'],
saas: ['nextjs-monolith', 'vite-react'],
blog: ['nextjs-monolith'],
};

// Default variant per template (first one)
const VARIANTS = {
portfolio: 'Developer Folio',
ecommerce: 'Fashion Store',
school: 'University Portal',
saas: 'Analytics Tool',
blog: 'Tech Blog',
};

const isQuick = process.argv.includes('--quick');

// Build the test matrix
const tests = [];

for (const template of TEMPLATES) {
for (const architecture of ARCHITECTURES[template]) {
for (const design of DESIGNS) {
// Test both sidebar=true and sidebar=false for templates that support it
const sidebarOptions = template === 'portfolio' ? [false] : [true, false];

for (const includeSidebar of sidebarOptions) {
tests.push({
projectName: `test-${template}-${architecture.split('-')[0]}-${design.replace(/\s+/g, '').toLowerCase()}-${includeSidebar ? 'sidebar' : 'nosidebar'}`,
template,
variant: VARIANTS[template],
architecture,
design,
navCount: 5,
includeSidebar,
initGit: false,
enableSecurity: false,
noInstall: false,
});
}
}
}
}

// In quick mode, only test one design per template+arch combo
const filteredTests = isQuick
? tests.filter((t) => t.design === 'Minimal Clean' && t.includeSidebar === false)
: tests;

console.log(`\nRunning ${filteredTests.length} template tests${isQuick ? ' (quick mode)' : ''}...\n`);

const results = [];
const startTime = Date.now();

for (let i = 0; i < filteredTests.length; i++) {
const test = filteredTests[i];
const projectPath = path.join(process.cwd(), test.projectName);

// Clean up if exists
if (fs.existsSync(projectPath)) fs.rmSync(projectPath, { recursive: true, force: true });

const label = `[${i + 1}/${filteredTests.length}] ${test.template}/${test.architecture} (${test.design}, sidebar=${test.includeSidebar})`;
process.stdout.write(` ${label}...`);

try {
// Generate
await generateProject(test);

// Build
const buildCmd = test.architecture === 'vite-react' ? 'npx vite build' : 'npm run build';
execSync(buildCmd, { cwd: projectPath, stdio: 'pipe' });

results.push({ label, status: 'pass' });
process.stdout.write(' ✅\n');
} catch (err) {
const errorMsg = err.stderr?.toString().slice(0, 200) || err.message?.slice(0, 200) || 'Unknown error';
results.push({ label, status: 'fail', error: errorMsg });
process.stdout.write(' ❌\n');
console.error(` Error: ${errorMsg.split('\n')[0]}\n`);
} finally {
// Clean up
if (fs.existsSync(projectPath)) fs.rmSync(projectPath, { recursive: true, force: true });
}
}

const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
const passed = results.filter((r) => r.status === 'pass').length;
const failed = results.filter((r) => r.status === 'fail').length;

console.log('\n' + '═'.repeat(60));
console.log(` Results: ${passed} passed, ${failed} failed (${elapsed}s)`);
console.log('═'.repeat(60));

if (failed > 0) {
console.log('\n Failed tests:');
for (const r of results.filter((r) => r.status === 'fail')) {
console.log(` ❌ ${r.label}`);
console.log(` ${r.error.split('\n')[0]}`);
}
console.log('');
process.exit(1);
}

console.log('');
process.exit(0);
Loading