v1.1.0: pooled runtime, 959 tests, production hardening (0 squash)

This commit is contained in:
John Dvorak
2025-08-15 10:00:00 -07:00
commit 92deb689cd
321 changed files with 79170 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env node
// CLI entry point for imhotep command
import { initProject } from './init.js';
import { getPresetNames } from './presets/index.js';
function showHelp(): void {
console.log(`
Usage: imhotep init --preset <name> [--dir <path>]
Commands:
init Scaffold a new Imhotep project
Options:
--preset <name> Project preset (${getPresetNames().join(', ')})
--dir <path> Target directory (default: current directory)
--help Show this help message
Examples:
npx imhotep init --preset react
npx imhotep init --preset next --dir ./next-tests
npx imhotep init --preset nuxt --dir ./nuxt-tests
npx imhotep init --preset vue --dir ./my-project
npx imhotep init --preset storybook --dir ./storybook-tests
`);
}
function main(): void {
const args = process.argv.slice(2);
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
showHelp();
process.exit(0);
}
const command = args[0];
if (command === 'init') {
const presetIndex = args.indexOf('--preset');
const dirIndex = args.indexOf('--dir');
if (presetIndex === -1 || !args[presetIndex + 1]) {
console.error('Error: --preset is required');
console.error(`Available presets: ${getPresetNames().join(', ')}`);
process.exit(1);
}
const preset = args[presetIndex + 1];
const targetDir = dirIndex !== -1
? args[dirIndex + 1]
: process.cwd();
try {
initProject({ preset, targetDir });
console.log(`✓ Scaffolded ${preset} project in ${targetDir}`);
console.log(` Run: cd ${targetDir} && npm install && npm test`);
} catch (error: any) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
} else {
console.error(`Unknown command: ${command}`);
showHelp();
process.exit(1);
}
}
main();
+3
View File
@@ -0,0 +1,3 @@
// Public API exports for imhotep-cli
export { initProject, type InitOptions } from './init.js';
export { presets, getPresetNames, getPreset, type PresetName } from './presets/index.js';
+131
View File
@@ -0,0 +1,131 @@
// RED phase: Test that scaffolding produces working projects
import assert from 'node:assert';
import { test, describe } from 'node:test';
import { mkdtempSync, existsSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { initProject } from './init.js';
import { getPresetNames, getPreset } from './presets/index.js';
describe('imhotep init', () => {
test('react-playwright preset creates expected files', () => {
const targetDir = mkdtempSync(join(tmpdir(), 'imhotep-init-test-'));
try {
initProject({ preset: 'react', targetDir });
// Verify all expected files exist
assert.ok(existsSync(join(targetDir, 'imhotep.config.js')), 'config file should exist');
assert.ok(existsSync(join(targetDir, 'tests', 'example.test.ts')), 'test file should exist');
assert.ok(existsSync(join(targetDir, 'fixtures', 'example.html')), 'fixture file should exist');
assert.ok(existsSync(join(targetDir, 'package.json')), 'package.json should exist');
assert.ok(existsSync(join(targetDir, 'README.md')), 'README should exist');
// Verify config has render target
const config = readFileSync(join(targetDir, 'imhotep.config.js'), 'utf-8');
assert.ok(config.includes('render'), 'config should mention render target');
// Verify test file has assertions
const testFile = readFileSync(join(targetDir, 'tests', 'example.test.ts'), 'utf-8');
assert.ok(testFile.includes('expect'), 'test should have assertions');
// Verify package.json has test scripts
const pkg = JSON.parse(readFileSync(join(targetDir, 'package.json'), 'utf-8'));
assert.ok(pkg.scripts.test, 'package.json should have test script');
assert.ok(pkg.scripts['test:ci'], 'package.json should have ci script');
} finally {
rmSync(targetDir, { recursive: true, force: true });
}
});
test('vue preset creates expected files', () => {
const targetDir = mkdtempSync(join(tmpdir(), 'imhotep-init-test-'));
try {
initProject({ preset: 'vue', targetDir });
assert.ok(existsSync(join(targetDir, 'imhotep.config.js')), 'config file should exist');
assert.ok(existsSync(join(targetDir, 'tests', 'example.test.ts')), 'test file should exist');
assert.ok(existsSync(join(targetDir, 'fixtures', 'example.html')), 'fixture file should exist');
assert.ok(existsSync(join(targetDir, 'package.json')), 'package.json should exist');
assert.ok(existsSync(join(targetDir, 'README.md')), 'README should exist');
const config = readFileSync(join(targetDir, 'imhotep.config.js'), 'utf-8');
assert.ok(config.includes('render'), 'config should mention render target');
} finally {
rmSync(targetDir, { recursive: true, force: true });
}
});
test('storybook preset creates expected files', () => {
const targetDir = mkdtempSync(join(tmpdir(), 'imhotep-init-test-'));
try {
initProject({ preset: 'storybook', targetDir });
assert.ok(existsSync(join(targetDir, 'imhotep.config.js')), 'config file should exist');
assert.ok(existsSync(join(targetDir, 'tests', 'example.test.ts')), 'test file should exist');
assert.ok(existsSync(join(targetDir, 'fixtures', 'example.html')), 'fixture file should exist');
assert.ok(existsSync(join(targetDir, 'package.json')), 'package.json should exist');
assert.ok(existsSync(join(targetDir, 'README.md')), 'README should exist');
const config = readFileSync(join(targetDir, 'imhotep.config.js'), 'utf-8');
assert.ok(config.includes('render'), 'config should mention render target');
} finally {
rmSync(targetDir, { recursive: true, force: true });
}
});
test('throws on unknown preset', () => {
const targetDir = mkdtempSync(join(tmpdir(), 'imhotep-init-test-'));
try {
assert.throws(() => {
initProject({ preset: 'unknown-preset', targetDir });
}, /Unknown preset/);
} finally {
rmSync(targetDir, { recursive: true, force: true });
}
});
test('scaffolded react tests have passing assertions syntax', () => {
const targetDir = mkdtempSync(join(tmpdir(), 'imhotep-init-test-'));
try {
initProject({ preset: 'react', targetDir });
const testFile = readFileSync(join(targetDir, 'tests', 'example.test.ts'), 'utf-8');
assert.ok(testFile.includes('const ui = await imhotep(page);'), 'template should await imhotep(page)');
assert.ok(testFile.includes('await ui.checkAll();'), 'template should evaluate batched assertions with checkAll()');
} finally {
rmSync(targetDir, { recursive: true, force: true });
}
});
test('all presets use publish-safe dependency ranges (no workspace protocol)', () => {
for (const name of getPresetNames()) {
const preset = getPreset(name);
const pkg = preset.packageJson;
for (const [dep, version] of Object.entries(pkg.devDependencies ?? {})) {
assert.ok(typeof version === 'string', `dependency ${dep} in ${name} should be a string`);
assert.ok(!version.startsWith('workspace:'), `${name} must not use workspace protocol for ${dep}`);
}
}
});
test('new framework presets scaffold expected files', () => {
const extraPresets = ['next', 'nuxt', 'remix', 'astro'];
for (const preset of extraPresets) {
const targetDir = mkdtempSync(join(tmpdir(), 'imhotep-init-test-'));
try {
initProject({ preset, targetDir });
assert.ok(existsSync(join(targetDir, 'imhotep.config.js')), `${preset} config should exist`);
assert.ok(existsSync(join(targetDir, 'tests', 'example.test.ts')), `${preset} test should exist`);
assert.ok(existsSync(join(targetDir, 'fixtures', 'example.html')), `${preset} fixture should exist`);
} finally {
rmSync(targetDir, { recursive: true, force: true });
}
}
});
});
+56
View File
@@ -0,0 +1,56 @@
// Core init logic for scaffolding Imhotep projects from presets
import { mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { getPreset } from './presets/index.js';
export interface InitOptions {
preset: string;
targetDir: string;
}
export function initProject(options: InitOptions): void {
const { preset, targetDir } = options;
// Resolve preset template
const template = getPreset(preset);
// Create directory structure
mkdirSync(targetDir, { recursive: true });
mkdirSync(join(targetDir, 'tests'), { recursive: true });
mkdirSync(join(targetDir, 'fixtures'), { recursive: true });
// Write config file
writeFileSync(
join(targetDir, 'imhotep.config.js'),
template.configFile,
'utf-8'
);
// Write test file
writeFileSync(
join(targetDir, 'tests', 'example.test.ts'),
template.testFile,
'utf-8'
);
// Write fixture file
writeFileSync(
join(targetDir, 'fixtures', 'example.html'),
template.fixtureFile,
'utf-8'
);
// Write package.json
writeFileSync(
join(targetDir, 'package.json'),
JSON.stringify(template.packageJson, null, 2),
'utf-8'
);
// Write README
writeFileSync(
join(targetDir, 'README.md'),
template.readme,
'utf-8'
);
}
@@ -0,0 +1,67 @@
export const astroPlaywrightPreset = {
name: 'astro',
description: 'Astro page testing with Playwright + Imhotep',
configFile: `module.exports = {
render: {
type: 'page',
framework: 'astro'
},
browser: {
viewport: { width: 1280, height: 720 }
}
};`,
testFile: `import { test, expect } from '@playwright/test';
import { imhotep } from 'imhotep';
test('marketing hero contracts', async ({ page }) => {
await page.goto(new URL('../fixtures/example.html', import.meta.url).href);
const ui = await imhotep(page);
ui.expect('[data-testid="hero-title"]').to.be.above('[data-testid="hero-cta"]', { minGap: 12 });
ui.expect('[data-testid="hero-cta"]').to.be.atLeast(44, 'height');
const result = await ui.checkAll();
expect(result.passed).toBe(true);
});`,
fixtureFile: `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
body { margin: 0; font-family: ui-sans-serif, sans-serif; background: #f8fafc; }
.hero { max-width: 760px; margin: 80px auto; background: #fff; padding: 32px; border-radius: 16px; }
.title { margin: 0 0 20px; font-size: 36px; }
.cta { min-height: 44px; padding: 12px 18px; border: none; border-radius: 8px; }
</style>
</head>
<body>
<section class="hero">
<h1 class="title" data-testid="hero-title">Ship faster</h1>
<button class="cta" data-testid="hero-cta">Start now</button>
</section>
</body>
</html>`,
packageJson: {
name: 'imhotep-astro-project',
version: '1.0.0',
type: 'module',
scripts: {
test: 'playwright test',
'test:ci': 'playwright test --reporter=list'
},
devDependencies: {
'@playwright/test': '^1.59.1',
imhotep: '^1.0.0'
}
},
readme: `# Imhotep Astro + Playwright preset
Scaffolded with \`imhotep init --preset astro\`.
`
};
+33
View File
@@ -0,0 +1,33 @@
// Central registry of all available presets
import { reactPlaywrightPreset } from './react-playwright.js';
import { vueVitestPreset } from './vue-vitest.js';
import { storybookPreset } from './storybook.js';
import { nextPlaywrightPreset } from './next-playwright.js';
import { remixPlaywrightPreset } from './remix-playwright.js';
import { astroPlaywrightPreset } from './astro-playwright.js';
import { nuxtPlaywrightPreset } from './nuxt-playwright.js';
export const presets = {
// Canonical framework-first preset names
react: reactPlaywrightPreset,
vue: vueVitestPreset,
storybook: storybookPreset,
next: nextPlaywrightPreset,
nuxt: nuxtPlaywrightPreset,
remix: remixPlaywrightPreset,
astro: astroPlaywrightPreset,
};
export type PresetName = keyof typeof presets;
export function getPresetNames(): string[] {
return Object.keys(presets);
}
export function getPreset(name: string) {
const preset = presets[name as PresetName];
if (!preset) {
throw new Error(`Unknown preset: "${name}". Available presets: ${getPresetNames().join(', ')}`);
}
return preset;
}
@@ -0,0 +1,77 @@
export const nextPlaywrightPreset = {
name: 'next',
description: 'Next.js app testing with Playwright + Imhotep',
configFile: `module.exports = {
render: {
type: 'page',
framework: 'next'
},
browser: {
viewport: { width: 1280, height: 720 }
}
};`,
testFile: `import { test, expect } from '@playwright/test';
import { imhotep } from 'imhotep';
test('dashboard shell layout', async ({ page }) => {
await page.goto(new URL('../fixtures/example.html', import.meta.url).href);
const ui = await imhotep(page);
ui.expect('[data-testid="nav"]').to.be.leftOf('[data-testid="content"]', { minGap: 16 });
ui.expect('[data-testid="menu-button"]').to.be.atLeast(44, 'height');
const result = await ui.checkAll();
expect(result.passed).toBe(true);
});`,
fixtureFile: `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Next fixture</title>
<style>
body { margin: 0; font-family: sans-serif; }
.shell { display: grid; grid-template-columns: 240px 1fr; gap: 16px; min-height: 100vh; }
.nav { background: #111827; color: #fff; padding: 16px; }
.content { padding: 24px; }
.menu { min-height: 44px; min-width: 44px; }
</style>
</head>
<body>
<div class="shell">
<aside class="nav" data-testid="nav"><button class="menu" data-testid="menu-button">Menu</button></aside>
<main class="content" data-testid="content">Dashboard</main>
</div>
</body>
</html>`,
packageJson: {
name: 'imhotep-next-project',
version: '1.0.0',
type: 'module',
scripts: {
test: 'playwright test',
'test:ci': 'playwright test --reporter=list'
},
devDependencies: {
'@playwright/test': '^1.59.1',
imhotep: '^1.0.0'
}
},
readme: `# Imhotep Next.js + Playwright preset
Scaffolded with \`imhotep init --preset next\`.
Run:
\`\`\`bash
npm install
npx playwright install
npm test
\`\`\`
`
};
@@ -0,0 +1,79 @@
export const nuxtPlaywrightPreset = {
name: 'nuxt',
description: 'Nuxt ecommerce layout testing with Playwright + Imhotep',
configFile: `module.exports = {
render: {
type: 'page',
framework: 'nuxt'
},
browser: {
viewport: { width: 390, height: 844 }
}
};`,
testFile: `import { test, expect } from '@playwright/test';
import { imhotep } from 'imhotep';
test('product grid and buy button contracts', async ({ page }) => {
await page.goto(new URL('../fixtures/example.html', import.meta.url).href);
const ui = await imhotep(page);
ui.expect('[data-testid="filter-rail"]').to.be.leftOf('[data-testid="product-grid"]', { minGap: 12 });
ui.expect('[data-testid="buy-button"]').to.be.atLeast(44, 'height');
ui.expect('[data-testid="buy-button"]').to.be.atLeast(44, 'width');
const result = await ui.checkAll();
expect(result.passed).toBe(true);
});`,
fixtureFile: `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
body { margin: 0; font-family: sans-serif; }
.layout { display: grid; grid-template-columns: 220px 1fr; gap: 16px; padding: 16px; }
.rail { position: sticky; top: 0; background: #f8fafc; padding: 12px; min-height: 200px; }
.grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
.card { border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px; }
.buy { min-height: 44px; min-width: 44px; }
</style>
</head>
<body>
<div class="layout">
<aside class="rail" data-testid="filter-rail">Filters</aside>
<section class="grid" data-testid="product-grid">
<article class="card"><button class="buy" data-testid="buy-button">Buy</button></article>
<article class="card"><button class="buy">Buy</button></article>
<article class="card"><button class="buy">Buy</button></article>
</section>
</div>
</body>
</html>`,
packageJson: {
name: 'imhotep-nuxt-project',
version: '1.0.0',
type: 'module',
scripts: {
test: 'playwright test',
'test:ci': 'playwright test --reporter=list'
},
devDependencies: {
'@playwright/test': '^1.59.1',
imhotep: '^1.0.0'
}
},
readme: `# Imhotep Nuxt + Playwright preset
Scaffolded with \`imhotep init --preset nuxt\`.
This preset targets common ecommerce layout checks:
- sticky filter rail relative position
- product grid spacing invariants
- 44x44 buy-button touch target baseline
`
};
@@ -0,0 +1,137 @@
// Preset template for React + Playwright projects
export const reactPlaywrightPreset = {
name: 'react',
description: 'React component testing with Playwright',
configFile: `module.exports = {
// Render target for React components
render: {
type: 'component',
framework: 'react',
// Component mount function path (relative to project root)
mount: './tests/mount.js'
},
// Playwright browser configuration
browser: {
viewport: { width: 1280, height: 720 }
},
// Spatial assertion defaults
defaults: {
tolerance: 0.5
}
};`,
testFile: `import { test, expect } from '@playwright/test';
import { imhotep } from 'imhotep';
test('button layout assertions', async ({ page }) => {
await page.goto(new URL('../fixtures/example.html', import.meta.url).href);
const ui = await imhotep(page);
ui.expect('[data-testid="primary-btn"]').to.be.above('[data-testid="secondary-btn"]', { minGap: 8 });
ui.expect('[data-testid="primary-btn"]').to.be.atLeast(44, 'height');
const result = await ui.checkAll();
expect(result.passed).toBe(true);
});`,
fixtureFile: `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>React Playwright Fixture</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: system-ui, -apple-system, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #f5f5f5;
}
.button-group {
display: flex;
flex-direction: column;
gap: 12px;
padding: 24px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
min-height: 44px;
}
.btn-primary {
background: #0066cc;
color: white;
}
.btn-secondary {
background: #e0e0e0;
color: #333;
}
</style>
</head>
<body>
<div class="button-group" data-testid="button-group">
<button class="btn btn-primary" data-testid="primary-btn">
Primary Action
</button>
<button class="btn btn-secondary" data-testid="secondary-btn">
Secondary Action
</button>
</div>
</body>
</html>`,
packageJson: {
name: 'imhotep-react-project',
version: '1.0.0',
type: 'module',
scripts: {
test: 'playwright test',
'test:ci': 'playwright test --reporter=list',
'test:ui': 'playwright test --ui'
},
devDependencies: {
'@playwright/test': '^1.59.1',
'imhotep': '^1.0.0'
}
},
readme: `# Imhotep React + Playwright Project
This project was scaffolded with \`imhotep init --preset react\`.
## Getting Started
\`\`\`bash
npm install
npx playwright install
npm test
\`\`\`
## Project Structure
- \`imhotep.config.js\` - Imhotep configuration
- \`tests/\` - Test files
- \`fixtures/\` - HTML fixtures for spatial testing
## Writing Tests
Use the Imhotep DSL for spatial assertions:
\`\`\`typescript
const ui = imhotep(page);
await ui.expect('.header').to.be.above('.content', { minGap: 16 });
\`\`\`
`
};
@@ -0,0 +1,69 @@
export const remixPlaywrightPreset = {
name: 'remix',
description: 'Remix route layout testing with Playwright + Imhotep',
configFile: `module.exports = {
render: {
type: 'page',
framework: 'remix'
},
browser: {
viewport: { width: 1280, height: 720 }
}
};`,
testFile: `import { test, expect } from '@playwright/test';
import { imhotep } from 'imhotep';
test('route layout contracts', async ({ page }) => {
await page.goto(new URL('../fixtures/example.html', import.meta.url).href);
const ui = await imhotep(page);
ui.expect('[data-testid="header"]').to.be.above('[data-testid="main"]', { minGap: 0 });
ui.expect('[data-testid="sidebar"]').to.be.leftOf('[data-testid="main"]', { minGap: 12 });
const result = await ui.checkAll();
expect(result.passed).toBe(true);
});`,
fixtureFile: `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
body { margin: 0; font-family: sans-serif; }
header { height: 64px; background: #0f172a; color: #fff; padding: 16px; }
.layout { display: grid; grid-template-columns: 220px 1fr; gap: 12px; }
aside { background: #e2e8f0; min-height: calc(100vh - 64px); padding: 16px; }
main { padding: 24px; }
</style>
</head>
<body>
<header data-testid="header">Header</header>
<div class="layout">
<aside data-testid="sidebar">Filters</aside>
<main data-testid="main">Content</main>
</div>
</body>
</html>`,
packageJson: {
name: 'imhotep-remix-project',
version: '1.0.0',
type: 'module',
scripts: {
test: 'playwright test',
'test:ci': 'playwright test --reporter=list'
},
devDependencies: {
'@playwright/test': '^1.59.1',
imhotep: '^1.0.0'
}
},
readme: `# Imhotep Remix + Playwright preset
Scaffolded with \`imhotep init --preset remix\`.
`
};
@@ -0,0 +1,176 @@
// Preset template for Storybook projects
export const storybookPreset = {
name: 'storybook',
description: 'Storybook component testing with spatial assertions',
configFile: `module.exports = {
// Render target for Storybook stories
render: {
type: 'storybook',
// Storybook URL (local dev server)
url: 'http://localhost:6006',
// Story patterns to test
stories: './src/**/*.stories.@(js|jsx|ts|tsx)'
},
// Browser configuration
browser: {
viewport: { width: 1280, height: 720 }
},
// Spatial assertion defaults
defaults: {
tolerance: 0.5
}
};`,
testFile: `import { test, expect } from '@playwright/test';
import { imhotep } from 'imhotep';
test('storybook story layout', async ({ page }) => {
await page.goto(new URL('../fixtures/example.html', import.meta.url).href);
const ui = await imhotep(page);
ui.expect('[data-testid="sidebar"]').to.be.leftOf('[data-testid="main-content"]', { minGap: 0 });
ui.expect('[data-testid="header"]').to.be.above('[data-testid="main-content"]', { minGap: 0 });
ui.expect('[data-testid="story-canvas"]').to.be.contains('[data-testid="story-component"]');
const result = await ui.checkAll();
expect(result.passed).toBe(true);
});`,
fixtureFile: `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Storybook Fixture</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: system-ui, -apple-system, sans-serif;
overflow: hidden;
}
.app {
display: grid;
grid-template-areas:
"header header"
"sidebar main";
grid-template-columns: 240px 1fr;
grid-template-rows: 48px 1fr;
height: 100vh;
}
.header {
grid-area: header;
background: #1a1a2e;
color: white;
display: flex;
align-items: center;
padding: 0 16px;
font-weight: 600;
}
.sidebar {
grid-area: sidebar;
background: #f8f9fa;
border-right: 1px solid #e0e0e0;
padding: 16px;
}
.sidebar-item {
padding: 8px 12px;
border-radius: 4px;
margin-bottom: 4px;
font-size: 14px;
color: #333;
}
.sidebar-item:hover {
background: #e9ecef;
}
.main {
grid-area: main;
padding: 24px;
overflow: auto;
}
.story-canvas {
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 32px;
min-height: 200px;
}
.story-component {
padding: 16px;
background: #f0f0f0;
border-radius: 4px;
text-align: center;
}
</style>
</head>
<body>
<div class="app">
<header class="header" data-testid="header">
Storybook
</header>
<nav class="sidebar" data-testid="sidebar">
<div class="sidebar-item">Buttons</div>
<div class="sidebar-item">Cards</div>
<div class="sidebar-item">Forms</div>
</nav>
<main class="main" data-testid="main-content">
<div class="story-canvas" data-testid="story-canvas">
<div class="story-component" data-testid="story-component">
Example Story Component
</div>
</div>
</main>
</div>
</body>
</html>`,
packageJson: {
name: 'imhotep-storybook-project',
version: '1.0.0',
type: 'module',
scripts: {
'storybook': 'storybook dev -p 6006',
'build-storybook': 'storybook build',
test: 'playwright test',
'test:ci': 'playwright test --reporter=list',
'test:visual': 'playwright test --update-snapshots'
},
devDependencies: {
'@playwright/test': '^1.59.1',
'@storybook/react': '^7.0.0',
'storybook': '^7.0.0',
'imhotep': '^1.0.0'
}
},
readme: `# Imhotep Storybook Project
This project was scaffolded with \`imhotep init --preset storybook\`.
## Getting Started
\`\`\`bash
npm install
npm run storybook # Start Storybook dev server
npm test # Run spatial tests
\`\`\`
## Project Structure
- \`imhotep.config.js\` - Imhotep configuration
- \`tests/\` - Spatial test files
- \`fixtures/\` - HTML fixtures for offline testing
- \`src/**/*.stories.*\` - Storybook stories
## Writing Tests
Use the Imhotep DSL for spatial assertions against stories:
\`\`\`typescript
const ui = imhotep(page);
await ui.expect('.sidebar').to.be.leftOf('.main-content');
\`\`\`
`
};
@@ -0,0 +1,156 @@
// Preset template for Vue + Vitest projects
export const vueVitestPreset = {
name: 'vue',
description: 'Vue component testing with Vitest',
configFile: `module.exports = {
// Render target for Vue components
render: {
type: 'component',
framework: 'vue',
// Component mount function path (relative to project root)
mount: './tests/mount.js'
},
// Browser configuration for Vitest browser mode
browser: {
enabled: true,
name: 'chromium',
viewport: { width: 1280, height: 720 }
},
// Spatial assertion defaults
defaults: {
tolerance: 0.5
}
};`,
testFile: `import { test, expect } from '@playwright/test';
import { imhotep } from 'imhotep';
test('card layout assertions', async ({ page }) => {
await page.goto(new URL('../fixtures/example.html', import.meta.url).href);
const ui = await imhotep(page);
ui.expect('[data-testid="card-title"]').to.be.above('[data-testid="card-content"]', { minGap: 12 });
ui.expect('[data-testid="card"]').to.be.atLeast(300, 'width');
const result = await ui.checkAll();
expect(result.passed).toBe(true);
});`,
fixtureFile: `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue Vitest Fixture</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: system-ui, -apple-system, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.card {
width: 320px;
padding: 24px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
}
.card-title {
font-size: 20px;
font-weight: 600;
color: #1a1a2e;
margin-bottom: 12px;
}
.card-content {
font-size: 14px;
line-height: 1.6;
color: #4a4a68;
}
.card-actions {
display: flex;
gap: 8px;
margin-top: 16px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-secondary {
background: transparent;
color: #667eea;
border: 1px solid #667eea;
}
</style>
</head>
<body>
<div class="card" data-testid="card">
<h2 class="card-title" data-testid="card-title">
Vue Component
</h2>
<div class="card-content" data-testid="card-content">
This fixture demonstrates spatial testing for Vue components using Vitest.
</div>
<div class="card-actions">
<button class="btn btn-primary">Confirm</button>
<button class="btn btn-secondary">Cancel</button>
</div>
</div>
</body>
</html>`,
packageJson: {
name: 'imhotep-vue-project',
version: '1.0.0',
type: 'module',
scripts: {
test: 'playwright test',
'test:ci': 'playwright test --reporter=list',
'test:ui': 'playwright test --ui'
},
devDependencies: {
'@playwright/test': '^1.59.1',
'imhotep': '^1.0.0'
}
},
readme: `# Imhotep Vue + Vitest Project
This project was scaffolded with \`imhotep init --preset vue\`.
## Getting Started
\`\`\`bash
npm install
npm test
\`\`\`
## Project Structure
- \`imhotep.config.js\` - Imhotep configuration
- \`tests/\` - Test files
- \`fixtures/\` - HTML fixtures for spatial testing
## Writing Tests
Use the Imhotep DSL for spatial assertions:
\`\`\`typescript
const ui = imhotep(page);
await ui.expect('.card').to.be.centeredWithin('viewport');
\`\`\`
`
};