UNPKG

@kalxjs/core

Version:

A modern JavaScript framework for building user interfaces with reactive state, composition API, and built-in performance optimizations

274 lines (232 loc) 6.46 kB
/** * Snapshot Testing Utilities * Component snapshot testing * * @module @kalxjs/testing/snapshot */ import * as fs from 'fs'; import * as path from 'path'; /** * Snapshot storage */ const snapshots = new Map(); /** * Serialize value to snapshot string */ function serialize(value) { if (value === null) return 'null'; if (value === undefined) return 'undefined'; if (typeof value === 'string') return `"${value}"`; if (typeof value === 'number' || typeof value === 'boolean') return String(value); if (value instanceof Date) return `Date(${value.toISOString()})`; if (value instanceof RegExp) return value.toString(); if (value instanceof Error) return `Error(${value.message})`; // DOM Element if (value instanceof Element) { return serializeElement(value); } // Array if (Array.isArray(value)) { const items = value.map(item => serialize(item)).join(',\n '); return `[\n ${items}\n]`; } // Object if (typeof value === 'object') { const entries = Object.entries(value) .map(([key, val]) => ` ${key}: ${serialize(val)}`) .join(',\n'); return `{\n${entries}\n}`; } return String(value); } /** * Serialize DOM element */ function serializeElement(element) { const tag = element.tagName.toLowerCase(); const attributes = Array.from(element.attributes) .map(attr => ` ${attr.name}="${attr.value}"`) .join(''); const children = Array.from(element.childNodes) .map(child => { if (child.nodeType === Node.TEXT_NODE) { const text = child.textContent.trim(); return text ? text : null; } if (child.nodeType === Node.ELEMENT_NODE) { return serializeElement(child); } return null; }) .filter(Boolean); if (children.length === 0) { return `<${tag}${attributes} />`; } if (children.length === 1 && typeof children[0] === 'string') { return `<${tag}${attributes}>${children[0]}</${tag}>`; } const childrenStr = children .map(child => ` ${child}`) .join('\n'); return `<${tag}${attributes}>\n${childrenStr}\n</${tag}>`; } /** * Match snapshot */ export function toMatchSnapshot(received, snapshotName) { const serialized = serialize(received); const stored = snapshots.get(snapshotName); if (stored === undefined) { // New snapshot snapshots.set(snapshotName, serialized); return { pass: true, message: () => `Snapshot saved: ${snapshotName}`, }; } // Compare with stored snapshot const pass = serialized === stored; return { pass, message: () => { if (pass) { return `Snapshot matches: ${snapshotName}`; } return [ `Snapshot mismatch: ${snapshotName}`, '', 'Expected:', stored, '', 'Received:', serialized, ].join('\n'); }, }; } /** * Match inline snapshot */ export function toMatchInlineSnapshot(received, inlineSnapshot) { const serialized = serialize(received); const pass = serialized === inlineSnapshot; return { pass, message: () => { if (pass) { return 'Inline snapshot matches'; } return [ 'Inline snapshot mismatch', '', 'Expected:', inlineSnapshot, '', 'Received:', serialized, ].join('\n'); }, }; } /** * Create snapshot matcher */ export function createSnapshotMatcher(testPath, testName) { const snapshotPath = path.join( path.dirname(testPath), '__snapshots__', `${path.basename(testPath)}.snap` ); let snapshotCount = 0; return { matchSnapshot: (received, name) => { snapshotCount++; const snapshotName = name || `${testName} ${snapshotCount}`; return toMatchSnapshot(received, snapshotName); }, matchInlineSnapshot: (received, inlineSnapshot) => { return toMatchInlineSnapshot(received, inlineSnapshot); }, save: () => { saveSnapshots(snapshotPath); }, load: () => { loadSnapshots(snapshotPath); }, }; } /** * Save snapshots to file */ function saveSnapshots(filePath) { const dir = path.dirname(filePath); // Create directory if it doesn't exist if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } const content = Array.from(snapshots.entries()) .map(([name, snapshot]) => { return `exports[\`${name}\`] = \`\n${snapshot}\n\`;`; }) .join('\n\n'); fs.writeFileSync(filePath, content, 'utf8'); } /** * Load snapshots from file */ function loadSnapshots(filePath) { if (!fs.existsSync(filePath)) { return; } const content = fs.readFileSync(filePath, 'utf8'); const regex = /exports\[`(.+?)`\] = `\n([\s\S]+?)\n`;/g; let match; while ((match = regex.exec(content)) !== null) { const [, name, snapshot] = match; snapshots.set(name, snapshot); } } /** * Clear all snapshots */ export function clearSnapshots() { snapshots.clear(); } /** * Get all snapshots */ export function getSnapshots() { return new Map(snapshots); } /** * Update snapshots */ export function updateSnapshots(updates) { Object.entries(updates).forEach(([name, snapshot]) => { snapshots.set(name, serialize(snapshot)); }); } /** * Snapshot testing plugin for test runner */ export function snapshotPlugin(options = {}) { const { updateSnapshots = false, snapshotDir = '__snapshots__', } = options; return { name: 'snapshot-plugin', beforeAll(context) { context.snapshots = new Map(); }, beforeEach(context) { context.snapshotCount = 0; }, afterAll(context) { if (updateSnapshots) { // Save snapshots Object.assign(snapshots, context.snapshots); } }, }; }