v1.1.0: pooled runtime, 959 tests, production hardening (0 squash)
This commit is contained in:
@@ -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();
|
||||
@@ -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';
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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\`.
|
||||
`
|
||||
};
|
||||
@@ -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');
|
||||
\`\`\`
|
||||
`
|
||||
};
|
||||
Reference in New Issue
Block a user