@ordojs/core
Version:
Core compiler and runtime for OrdoJS framework
283 lines (282 loc) • 9.53 kB
JavaScript
/**
* @fileoverview Snapshot Testing Utilities
*/
import { existsSync } from 'fs';
import { mkdir, readFile, writeFile } from 'fs/promises';
import { dirname, join } from 'path';
/**
* Utilities for snapshot testing generated code
*/
export class SnapshotTester {
snapshotDir;
updateSnapshots;
constructor(snapshotDir = '__snapshots__', updateSnapshots = false) {
this.snapshotDir = snapshotDir;
this.updateSnapshots = updateSnapshots;
}
/**
* Test generated code against snapshot
*/
async testSnapshot(testName, generatedCode, options) {
const startTime = performance.now();
const errors = [];
const warnings = [];
try {
// Normalize the generated code
const normalizedCode = this.normalizeGeneratedCode(generatedCode, options);
// Get snapshot file path
const snapshotPath = this.getSnapshotPath(testName);
// Check if snapshot exists
const snapshotExists = existsSync(snapshotPath);
if (!snapshotExists || options.updateSnapshots || this.updateSnapshots) {
// Create or update snapshot
await this.createSnapshot(snapshotPath, normalizedCode);
if (!snapshotExists) {
warnings.push(`Created new snapshot for test "${testName}"`);
}
else {
warnings.push(`Updated snapshot for test "${testName}"`);
}
return {
success: true,
errors,
warnings,
duration: performance.now() - startTime
};
}
// Load existing snapshot
const existingSnapshot = await this.loadSnapshot(snapshotPath);
// Compare snapshots
const comparison = this.compareSnapshots(normalizedCode, existingSnapshot, options);
if (!comparison.matches) {
errors.push(`Snapshot mismatch for test "${testName}"`);
errors.push(...comparison.differences);
}
return {
success: comparison.matches,
errors,
warnings,
duration: performance.now() - startTime,
metadata: {
snapshotPath,
differences: comparison.differences
}
};
}
catch (error) {
errors.push(error instanceof Error ? error.message : String(error));
return {
success: false,
errors,
warnings,
duration: performance.now() - startTime
};
}
}
/**
* Normalize generated code for consistent snapshots
*/
normalizeGeneratedCode(generatedCode, options) {
let normalized = '';
// Add client code
if (generatedCode.client) {
normalized += '// CLIENT CODE\n';
normalized += this.normalizeCode(generatedCode.client, options);
normalized += '\n\n';
}
// Add server code
if (generatedCode.server) {
normalized += '// SERVER CODE\n';
normalized += this.normalizeCode(generatedCode.server, options);
normalized += '\n\n';
}
// Add HTML
if (generatedCode.html) {
normalized += '// HTML TEMPLATE\n';
normalized += this.normalizeHTML(generatedCode.html, options);
normalized += '\n\n';
}
// Add CSS
if (generatedCode.css) {
normalized += '// CSS STYLES\n';
normalized += this.normalizeCode(generatedCode.css, options);
normalized += '\n\n';
}
return normalized.trim();
}
/**
* Normalize code content
*/
normalizeCode(code, options) {
let normalized = code;
// Apply custom normalizer if provided
if (options.customNormalizer) {
normalized = options.customNormalizer(normalized);
}
// Remove excessive whitespace if configured
if (options.ignoreWhitespace) {
normalized = normalized.replace(/\s+/g, ' ').trim();
}
// Normalize line endings
normalized = normalized.replace(/\r\n/g, '\n');
return normalized;
}
/**
* Normalize HTML content
*/
normalizeHTML(html, options) {
let normalized = html;
// Apply custom normalizer if provided
if (options.customNormalizer) {
normalized = options.customNormalizer(normalized);
}
// Normalize HTML if configured
if (options.normalizeHTML) {
// Remove extra whitespace between tags
normalized = normalized.replace(/>\s+</g, '><');
// Normalize attribute order (simple approach)
normalized = normalized.replace(/<(\w+)([^>]*)>/g, (match, tagName, attributes) => {
if (!attributes.trim())
return match;
// Sort attributes alphabetically
const attrs = attributes.trim().split(/\s+/)
.filter(attr => attr.includes('='))
.sort();
return `<${tagName} ${attrs.join(' ')}>`;
});
}
// Remove excessive whitespace if configured
if (options.ignoreWhitespace) {
normalized = normalized.replace(/\s+/g, ' ').trim();
}
// Normalize line endings
normalized = normalized.replace(/\r\n/g, '\n');
return normalized;
}
/**
* Get snapshot file path
*/
getSnapshotPath(testName) {
const sanitizedName = testName.replace(/[^a-zA-Z0-9-_]/g, '_');
return join(this.snapshotDir, `${sanitizedName}.snap`);
}
/**
* Create snapshot file
*/
async createSnapshot(snapshotPath, content) {
// Ensure directory exists
const dir = dirname(snapshotPath);
if (!existsSync(dir)) {
await mkdir(dir, { recursive: true });
}
// Write snapshot with metadata
const snapshotContent = this.createSnapshotContent(content);
await writeFile(snapshotPath, snapshotContent, 'utf8');
}
/**
* Create snapshot file content with metadata
*/
createSnapshotContent(content) {
const timestamp = new Date().toISOString();
const header = `// OrdoJS Snapshot
// Generated at: ${timestamp}
// Do not edit this file manually
`;
return header + content;
}
/**
* Load existing snapshot
*/
async loadSnapshot(snapshotPath) {
const content = await readFile(snapshotPath, 'utf8');
// Remove header comments
const lines = content.split('\n');
const contentStart = lines.findIndex(line => !line.startsWith('//') && line.trim() !== '');
return contentStart >= 0 ? lines.slice(contentStart).join('\n') : content;
}
/**
* Compare two snapshots
*/
compareSnapshots(current, existing, options) {
const differences = [];
// Simple line-by-line comparison
const currentLines = current.split('\n');
const existingLines = existing.split('\n');
const maxLines = Math.max(currentLines.length, existingLines.length);
for (let i = 0; i < maxLines; i++) {
const currentLine = currentLines[i] || '';
const existingLine = existingLines[i] || '';
if (currentLine !== existingLine) {
differences.push(`Line ${i + 1}:`);
differences.push(` Expected: ${existingLine}`);
differences.push(` Received: ${currentLine}`);
}
}
return {
matches: differences.length === 0,
differences
};
}
/**
* Test multiple snapshots
*/
async testMultipleSnapshots(tests) {
const results = [];
for (const test of tests) {
const options = {
name: test.name,
updateSnapshots: this.updateSnapshots,
...test.options
};
const result = await this.testSnapshot(test.name, test.generatedCode, options);
results.push(result);
}
return results;
}
/**
* Clean up old snapshots
*/
async cleanupSnapshots(activeTestNames) {
// This would implement cleanup of unused snapshot files
// For now, just log what would be cleaned up
console.log(`Would clean up snapshots not in: ${activeTestNames.join(', ')}`);
}
/**
* Get snapshot statistics
*/
async getSnapshotStats() {
// This would implement snapshot statistics
// For now, return placeholder data
return {
total: 0,
passed: 0,
failed: 0,
obsolete: 0
};
}
/**
* Set update snapshots mode
*/
setUpdateSnapshots(update) {
this.updateSnapshots = update;
}
/**
* Get update snapshots mode
*/
getUpdateSnapshots() {
return this.updateSnapshots;
}
/**
* Set snapshot directory
*/
setSnapshotDir(dir) {
this.snapshotDir = dir;
}
/**
* Get snapshot directory
*/
getSnapshotDir() {
return this.snapshotDir;
}
}
//# sourceMappingURL=snapshot-tester.js.map