| name | test-create-fixture |
| description | Create HTML test fixture AND Playwright spec for UI components. Uses project templates to create isolated test pages for TDD validation. Web-specific enhancement creates both files. |
test-create-fixture - Component Test Fixture & Spec Creator
Creates HTML test fixture files AND Playwright spec files for UI components. Used by dev agents during TDD to create isolated test pages before implementation.
Web Platform Enhancement: For web projects, this skill creates BOTH the fixture HTML AND the Playwright spec file to avoid silent failures from missing specs.
Input Schema
{
"project_dir": "/path/to/project",
"ticket_id": "TICKET-XXX_A.1",
"component_path": "src/components/impact/MetricCard.js",
"fixture_path": "src/tests/metric-card.html",
"spec_path": "tests/metric-card.spec.js",
"component_name": "MetricCard",
"component_class": "MetricCard",
"platform": "web",
"acceptance_criteria": [
{"id": "AC-001", "description": "Counter animates from 0 to target"},
{"id": "AC-002", "description": "Displays unit label correctly"}
]
}
Note: spec_path is REQUIRED for web platform. If not provided, derive from fixture_path:
src/tests/metric-card.html → tests/metric-card.spec.js
CRITICAL: Use Absolute URL Paths (NOT Relative)
NEVER use relative paths like ../../ in fixture imports.
The server runs from src/ directory, making src/ the web root:
src/tests/*.html → served at /tests/*.html
src/components/**/*.js → served at /components/**/*.js
WHY relative paths break:
fixture_path: src/tests/metric-card.html
component_path: src/components/impact/MetricCard.js
WRONG: ../../components/impact/MetricCard.js
→ resolves to project_root/components/impact/MetricCard.js (OUTSIDE src/)
→ FILE NOT FOUND
RIGHT: /components/impact/MetricCard.js
→ resolves from server root (src/) to src/components/impact/MetricCard.js
→ FILE FOUND ✓
Instructions
1. Locate Templates
2. Process Template Variables
Replace mustache-style variables in templates:
Shared Variables (Fixture + Spec)
| Variable | Source | Example |
|---|
{{TICKET_ID}} | input.ticket_id | TICKET-OXY-003_A.1 |
{{COMPONENT_NAME}} | input.component_name | MetricCard |
{{COMPONENT_CLASS}} | input.component_class | MetricCard |
{{#ACCEPTANCE_CRITERIA}} | Loop over ACs | Creates test sections |
{{AC_ID}} | Each AC ID | AC-001 |
{{AC_DESCRIPTION}} | Each AC description | Counter animates... |
Fixture-Specific Variables
| Variable | Source | Example |
|---|
{{COMPONENT_PATH}} | Absolute URL from server root | /components/impact/MetricCard.js |
{{COMPONENT_CSS}} | CSS absolute URL | /styles/metric-card.css |
{{COMPONENT_OPTIONS}} | Default init options | {} or from ticket |
{{COMPONENT_SELECTOR}} | data-* attribute name (kebab-case) | circular-progress |
Spec-Specific Variables (Web Platform)
| Variable | Source | Example |
|---|
{{SPEC_FILENAME}} | Basename of spec_path | metric-card.spec.js |
{{FIXTURE_FILENAME}} | Basename of fixture_path | metric-card.html |
3. Calculate Absolute URL Paths
CRITICAL: Check for import_prefix in context first!
The import_prefix context variable tells you if the server needs a path prefix (e.g., /src).
function toAbsoluteUrl(filePath, importPrefix) {
let relativePath = filePath;
if (filePath.startsWith('src/')) {
relativePath = filePath.slice(4);
}
if (importPrefix) {
return importPrefix + '/' + relativePath;
}
return '/' + relativePath;
}
Always check context for import_prefix before generating import paths!
4. Generate AC Test Sections
For each acceptance criterion, create a test section:
<div class="test-section" id="test-AC-001">
<h2>AC-001: Counter animates from 0 to target</h2>
<div data-test-scenario="ac-001"></div>
</div>
5. Write Fixture File
const fixtureDir = path.dirname(fixturePath);
await fs.mkdir(fixtureDir, { recursive: true });
await fs.writeFile(fixturePath, processedTemplate);
6. Write Spec File (Web Platform Only)
CRITICAL: For web platform, ALWAYS create the spec file alongside the fixture.
This step prevents silent failures where the fixture is created but the spec is missing, causing TDD verification to fail.
function deriveSpecPath(fixturePath) {
const basename = path.basename(fixturePath, '.html');
return `tests/${basename}.spec.js`;
}
const specPath = input.spec_path || deriveSpecPath(input.fixture_path);
const specDir = path.dirname(specPath);
await fs.mkdir(specDir, { recursive: true });
const specTemplate = await loadTemplate('component.spec.template.js');
const processedSpec = specTemplate
.replace(/\{\{TICKET_ID\}\}/g, input.ticket_id)
.replace(/\{\{COMPONENT_NAME\}\}/g, input.component_name)
.replace(/\{\{COMPONENT_CLASS\}\}/g, input.component_class)
.replace(/\{\{SPEC_FILENAME\}\}/g, path.basename(specPath))
.replace(/\{\{FIXTURE_FILENAME\}\}/g, path.basename(input.fixture_path));
const acTests = input.acceptance_criteria.map(ac =>
` test('${ac.id}: ${ac.description}', async ({ page }) => {
const status = page.locator('#test-status');
await expect(status).toHaveClass(/success/);
// TODO: Add specific assertions for ${ac.id}
});`
).join('\n\n');
processedSpec = processedSpec.replace(/\{\{#ACCEPTANCE_CRITERIA\}\}[\s\S]*?\{\{\/ACCEPTANCE_CRITERIA\}\}/g, acTests);
await fs.writeFile(path.join(input.project_dir, specPath), processedSpec);
Output Format
{
"skill": "test-create-fixture",
"status": "success|failure",
"fixture_created": "src/tests/metric-card.html",
"spec_created": "tests/metric-card.spec.js",
"fixture_template_used": "templates/project/tests/web/component-fixture.template.html",
"spec_template_used": "templates/project/tests/web/component.spec.template.js",
"platform": "web",
"variables_replaced": {
"TICKET_ID": "TICKET-OXY-003_A.1",
"COMPONENT_NAME": "MetricCard",
"COMPONENT_PATH": "/components/impact/MetricCard.js",
"ACCEPTANCE_CRITERIA": 2
},
"test_sections": ["AC-001", "AC-002"],
"warnings": [],
"next_action": "validate_fixture"
}
IMPORTANT: For web platform, both fixture_created AND spec_created must be present for success. If either file fails to create, return status: "failure" with appropriate error message.
Happy Path Only (CRITICAL)
Initial fixtures focus on happy path ONLY. Edge case testing is deferred to later phases.
What to Include (Happy Path)
- Primary data loading with valid endpoints
- Standard component initialization
- Default configurations
- Expected successful responses
What to Defer (Edge Cases)
- Missing file/endpoint tests (AC descriptions mentioning "missing", "invalid", "error handling")
- Error recovery scenarios
- Timeout/failure handling
- Malformed input validation
Handling Edge Case ACs
When an AC describes edge case behavior (missing files, errors, invalid data), do NOT test it directly. Instead:
setOutput('ac-3', JSON.stringify({
status: 'deferred',
note: 'Edge case test deferred until happy path validated'
}, null, 2));
Rationale: Early TDD phases validate that components work correctly with valid inputs. Edge case testing requires additional infrastructure (mock servers, error injection) that may not exist yet.
Fixture Requirements
The generated fixture MUST include:
- Isolated component - Only the target component, no page context
- Real imports - Import from actual src/ path, not copies
- Test sections per AC - Each acceptance criterion gets a section
- data-testid attributes - For Playwright selection
- Status indicator - Shows init success/failure (#test-status.success/.error)
- Playwright hooks - CRITICAL for test access:
Required Window Globals (CRITICAL)
window.__testModule = {
ComponentClass,
initComponentFactory,
...otherExports
};
window.__testInstances = [...];
window.__getTestInstance = () => instances[0];
window.__getTestInstances = () => instances;
window.__isInitialized = () => initComplete && !error;
window.__getInitError = () => initError;
Why __testModule is CRITICAL:
- ES6 module loading timing varies by browser (Chromium is slower)
- Tests use
page.waitForFunction(() => window.__testModule) to wait for module
- If not set synchronously, tests fail with race conditions
- Chromium especially affected (19 failures vs WebKit's 4)
Null-Return Handling (CRITICAL - Prevents False Positives)
Problem: Many components return null on failure instead of throwing exceptions. Fixtures that only use try/catch will show "success" even when initialization failed.
Solution: Always check for null returns in the initialization IIFE:
(async () => {
try {
const instance = await createComponent(container);
if (!instance) {
initError = new Error('Component returned null - initialization failed');
statusEl.textContent = 'Error: Component failed to initialize';
statusEl.className = 'error';
window.__isInitialized = () => false;
window.__getInitError = () => initError;
return;
}
instances.push(instance);
window.__testInstances = instances;
statusEl.textContent = 'Component initialized successfully';
statusEl.className = 'success';
window.__isInitialized = () => true;
} catch (error) {
initError = error;
console.error('Component initialization error:', error);
statusEl.textContent = 'Error: ' + error.message;
statusEl.className = 'error';
window.__isInitialized = () => false;
window.__getInitError = () => initError;
}
})();
Why This Matters for Fallback Tests:
When testing graceful degradation (e.g., "shows fallback when Mapbox fails"):
test('AC-6: Shows fallback when dependency fails', async ({ page }) => {
await page.route('**/mapbox-gl.js', route => route.abort());
await page.reload({ waitUntil: 'networkidle' });
await page.waitForSelector('#test-status.error', { timeout: 5000 });
await expect(page.locator('.component-fallback')).toBeVisible();
});
Without null-return handling, this test fails because the fixture shows "success" with 0 instances instead of "error".
Example Generated Fixture
<!DOCTYPE html>
<html lang="en">
<head>
<title>MetricCard Test - TICKET-OXY-003_A.1</title>
<link rel="stylesheet" href="/styles/impact/metric-card.css">
</head>
<body>
<div class="test-container">
<h1>MetricCard Test</h1>
<div class="test-section" id="test-AC-001">
<h2>AC-001: Counter animates from 0 to target</h2>
<div data-testid="ac-001-container"></div>
</div>
<div class="test-section" id="test-AC-002">
<h2>AC-002: Displays unit label correctly</h2>
<div data-testid="ac-002-container"></div>
</div>
<div id="test-status">Loading...</div>
</div>
<script type="module">
import { MetricCard } from '/components/impact/MetricCard.js';
</script>
</body>
</html>
Token Efficiency
- Template-based generation (no LLM needed for structure)
- ~2-5 second execution
- Returns ready-to-use HTML fixture AND Playwright spec (web platform)
- Self-sufficient for basic TDD scaffolding - no agent fallback needed for standard cases
Platform Detection
function getPlatform(input) {
if (input.platform) return input.platform;
if (input.fixture_path.endsWith('.html')) return 'web';
if (input.fixture_path.includes('widget_test')) return 'flutter';
if (input.fixture_path.includes('XCTest')) return 'ios';
return 'web';
}
const platform = getPlatform(input);
if (platform === 'web') {
} else {
}