UNPKG

@vizzly-testing/cli

Version:

Visual review platform for UI developers and designers

101 lines (93 loc) 3.92 kB
/** * Screenshot Identity - Signature and Filename Generation * * CRITICAL: These functions MUST stay in sync with the cloud! * * Cloud counterpart: vizzly/src/utils/screenshot-identity.js * - generateScreenshotSignature() * - generateBaselineFilename() * * Contract tests: Both repos have golden tests that must produce identical values: * - Cloud: tests/contracts/signature-parity.test.js * - CLI: tests/contracts/signature-parity.spec.js * * If you modify signature or filename generation here, you MUST: * 1. Make the same change in the cloud repo * 2. Update golden test values in BOTH repos * 3. Run contract tests in both repos to verify parity * * The signature format is: name|viewport_width|browser|custom1|custom2|... * The filename format is: {sanitized-name}_{12-char-sha256-hash}.png */ import crypto from 'node:crypto'; /** * Generate a screenshot signature for baseline matching * * SYNC WITH: vizzly/src/utils/screenshot-identity.js - generateScreenshotSignature() * * Uses same logic as cloud: name + viewport_width + browser + custom properties * * @param {string} name - Screenshot name * @param {Object} properties - Screenshot properties (viewport, browser, metadata, etc.) * @param {Array<string>} customProperties - Custom property names from project settings * @returns {string} Signature like "Login|1920|chrome|iPhone 15 Pro" */ export function generateScreenshotSignature(name, properties = {}, customProperties = []) { // Match cloud screenshot-identity.js behavior exactly: // Always include all default properties (name, viewport_width, browser) // even if null/undefined, using empty string as placeholder let defaultProperties = ['name', 'viewport_width', 'browser']; let allProperties = [...defaultProperties, ...customProperties]; let parts = allProperties.map(propName => { let value; if (propName === 'name') { value = name; } else if (propName === 'viewport_width') { // Check for viewport_width as top-level property first (backend format) value = properties.viewport_width; // Fallback to nested viewport.width (SDK format) if (value === null || value === undefined) { value = properties.viewport?.width; } } else if (propName === 'browser') { value = properties.browser; } else { // Custom property - check multiple locations value = properties[propName] ?? properties.metadata?.[propName] ?? properties.metadata?.properties?.[propName]; } // Handle null/undefined values consistently (match cloud behavior) if (value === null || value === undefined) { return ''; } // Convert to string and normalize return String(value).trim(); }); return parts.join('|'); } /** * Generate a stable, filesystem-safe filename for a screenshot baseline * Uses a hash of the signature to avoid character encoding issues * Matches the cloud's generateBaselineFilename implementation exactly * * @param {string} name - Screenshot name * @param {string} signature - Full signature string * @returns {string} Filename like "homepage_a1b2c3d4e5f6.png" */ export function generateBaselineFilename(name, signature) { let hash = crypto.createHash('sha256').update(signature).digest('hex').slice(0, 12); // Sanitize the name for filesystem safety let safeName = name.replace(/[/\\:*?"<>|]/g, '') // Remove unsafe chars .replace(/\s+/g, '-') // Spaces to hyphens .slice(0, 50); // Limit length return `${safeName}_${hash}.png`; } /** * Generate a stable unique ID from signature for TDD comparisons * This allows UI to reference specific variants without database IDs * * @param {string} signature - Full signature string * @returns {string} 16-char hex hash */ export function generateComparisonId(signature) { return crypto.createHash('sha256').update(signature).digest('hex').slice(0, 16); }