From 374fc056c05a7457ef852eb5ebc491308deed7e0 Mon Sep 17 00:00:00 2001 From: light Date: Sat, 31 Jan 2026 11:39:37 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Visual=20Regression=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=A0=84=EC=97=AD=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 스크린샷 캡처/비교/리포트 워크플로우 - 뷰포트 프리셋: desktop, laptop, tablet, mobile - 동적 콘텐츠 마스킹: 시간, 아바타, 배지, 차트 - 비교 임계값: critical(1%), high(5%), medium(10%), low(20%) - CRUD/성능/API 테스트와 통합 지원 Co-Authored-By: Claude Opus 4.5 --- _global-visual-config.json | 280 +++++++++++++++++++++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 _global-visual-config.json diff --git a/_global-visual-config.json b/_global-visual-config.json new file mode 100644 index 0000000..c4dfb5b --- /dev/null +++ b/_global-visual-config.json @@ -0,0 +1,280 @@ +{ + "$schema": "E2E Visual Regression Configuration", + "version": "1.0.0", + "description": "스크린샷 기반 시각적 회귀 테스트 설정", + "lastUpdated": "2026-01-31", + + "screenshot": { + "enabled": true, + "baseDir": "e2e/results/screenshots", + "baselineDir": "e2e/results/screenshots/baseline", + "currentDir": "e2e/results/screenshots/current", + "diffDir": "e2e/results/screenshots/diff", + "format": "png", + "quality": 90, + "fullPage": false, + "defaultViewport": { + "width": 1920, + "height": 1080 + } + }, + + "naming": { + "format": "{scenario}_{page}_{viewport}_{timestamp}", + "baselineFormat": "{scenario}_{page}_{viewport}_baseline", + "examples": [ + "dashboard_main_1920x1080_20260131_113000.png", + "dashboard_main_1920x1080_baseline.png" + ] + }, + + "viewports": { + "desktop": { "width": 1920, "height": 1080, "label": "Desktop HD" }, + "laptop": { "width": 1366, "height": 768, "label": "Laptop" }, + "tablet": { "width": 768, "height": 1024, "label": "Tablet Portrait" }, + "tabletLandscape": { "width": 1024, "height": 768, "label": "Tablet Landscape" }, + "mobile": { "width": 375, "height": 812, "label": "Mobile (iPhone X)" }, + "mobileLarge": { "width": 414, "height": 896, "label": "Mobile Large" } + }, + + "comparison": { + "enabled": true, + "threshold": 0.1, + "thresholds": { + "strict": 0.01, + "normal": 0.1, + "relaxed": 0.5 + }, + "ignoreAntialiasing": true, + "ignoreColors": false, + "ignoreDynamicContent": true, + "algorithm": "pixelmatch" + }, + + "dynamicContent": { + "description": "동적으로 변하는 콘텐츠 처리", + "masks": [ + { + "name": "timestamp", + "selector": "[class*='time'], [class*='date'], time", + "action": "mask" + }, + { + "name": "avatar", + "selector": "[class*='avatar'], .profile-image", + "action": "mask" + }, + { + "name": "notification-badge", + "selector": "[class*='badge'], .notification-count", + "action": "mask" + }, + { + "name": "chart", + "selector": "canvas, svg[class*='chart']", + "action": "mask" + }, + { + "name": "ads", + "selector": "[class*='ad'], [class*='banner']", + "action": "hide" + } + ], + "hideSelectors": [ + ".loading", + ".spinner", + "[class*='skeleton']", + ".cursor-blink" + ], + "waitForSelectors": [ + "main", + "[class*='content']", + "table tbody" + ] + }, + + "capturePoints": { + "description": "스크린샷 캡처 시점", + "triggers": [ + { + "name": "pageLoad", + "description": "페이지 로드 완료 후", + "delay": 1000 + }, + { + "name": "afterAction", + "description": "주요 액션 후", + "delay": 500 + }, + { + "name": "modalOpen", + "description": "모달 열림 후", + "delay": 300 + }, + { + "name": "formFilled", + "description": "폼 입력 완료 후", + "delay": 200 + }, + { + "name": "error", + "description": "에러 발생 시", + "delay": 0 + } + ] + }, + + "scripts": { + "prepareForScreenshot": "(async function() { await new Promise(r => setTimeout(r, 500)); const hideElements = document.querySelectorAll('.loading, .spinner, [class*=\"skeleton\"], .cursor-blink, [class*=\"toast\"]'); hideElements.forEach(el => el.style.visibility = 'hidden'); const waitForImages = Array.from(document.images).filter(img => !img.complete); await Promise.all(waitForImages.map(img => new Promise(r => { img.onload = r; img.onerror = r; setTimeout(r, 3000); }))); return { hidden: hideElements.length, imagesLoaded: document.images.length }; })()", + + "maskDynamicContent": "(function() { const masks = [ { selector: '[class*=\"time\"], [class*=\"date\"], time', color: '#888' }, { selector: '[class*=\"avatar\"], .profile-image', color: '#ccc' }, { selector: '[class*=\"badge\"]', color: '#666' } ]; let maskedCount = 0; masks.forEach(m => { document.querySelectorAll(m.selector).forEach(el => { el.style.backgroundColor = m.color; el.style.color = m.color; maskedCount++; }); }); return { maskedElements: maskedCount }; })()", + + "getPageState": "(function() { return { url: window.location.href, title: document.title, scrollY: window.scrollY, viewportWidth: window.innerWidth, viewportHeight: window.innerHeight, bodyHeight: document.body.scrollHeight, hasModal: !!document.querySelector('[role=\"dialog\"], [class*=\"modal\"]:not([style*=\"display: none\"])'), loadingElements: document.querySelectorAll('.loading, .spinner, [class*=\"skeleton\"]').length }; })()", + + "waitForStableState": "(async function(timeout = 5000) { const start = Date.now(); let lastHeight = 0; let stableCount = 0; while (Date.now() - start < timeout) { const currentHeight = document.body.scrollHeight; const loadingCount = document.querySelectorAll('.loading, .spinner').length; if (currentHeight === lastHeight && loadingCount === 0) { stableCount++; if (stableCount >= 3) return { stable: true, duration: Date.now() - start }; } else { stableCount = 0; } lastHeight = currentHeight; await new Promise(r => setTimeout(r, 200)); } return { stable: false, duration: timeout }; })()", + + "highlightDifferences": "(function(diffAreas) { diffAreas.forEach(area => { const highlight = document.createElement('div'); highlight.style.cssText = `position: absolute; left: ${area.x}px; top: ${area.y}px; width: ${area.width}px; height: ${area.height}px; border: 3px solid red; background: rgba(255,0,0,0.2); pointer-events: none; z-index: 99999;`; document.body.appendChild(highlight); }); })", + + "captureElementScreenshot": "(async function(selector) { const element = document.querySelector(selector); if (!element) return { error: 'Element not found', selector }; const rect = element.getBoundingClientRect(); return { selector, x: rect.x, y: rect.y, width: rect.width, height: rect.height, visible: rect.width > 0 && rect.height > 0 }; })" + }, + + "pages": { + "description": "페이지별 스크린샷 설정", + "dashboard": { + "url": "/dashboard", + "captures": ["full", "sidebar", "main-content"], + "masks": ["chart", "notification-badge", "timestamp"], + "threshold": 0.1 + }, + "login": { + "url": "/login", + "captures": ["full", "login-form"], + "masks": [], + "threshold": 0.05 + }, + "list": { + "url": "/**/list", + "captures": ["full", "table", "pagination"], + "masks": ["timestamp", "avatar"], + "threshold": 0.1 + }, + "form": { + "url": "/**?mode=new", + "captures": ["full", "form"], + "masks": [], + "threshold": 0.05 + } + }, + + "reporting": { + "generateReport": true, + "reportFormat": "markdown", + "includeImages": true, + "includeDiff": true, + "template": { + "header": "## 📸 Visual Regression 테스트 결과", + "sections": { + "summary": "### 요약", + "passed": "### ✅ 일치", + "failed": "### ❌ 차이 감지", + "new": "### 🆕 신규 페이지" + } + } + }, + + "baseline": { + "autoUpdate": false, + "updateOnPass": false, + "reviewRequired": true, + "storage": { + "type": "local", + "path": "e2e/results/screenshots/baseline" + } + }, + + "workflow": { + "steps": [ + { + "order": 1, + "name": "prepare", + "description": "스크린샷 준비", + "actions": ["waitForStableState", "prepareForScreenshot", "maskDynamicContent"] + }, + { + "order": 2, + "name": "capture", + "description": "스크린샷 캡처", + "actions": ["takeScreenshot", "saveToCurrentDir"] + }, + { + "order": 3, + "name": "compare", + "description": "베이스라인과 비교", + "actions": ["loadBaseline", "compareImages", "calculateDiff"] + }, + { + "order": 4, + "name": "report", + "description": "결과 리포트", + "actions": ["generateDiffImage", "updateReport"] + } + ] + }, + + "thresholdLevels": { + "critical": { + "value": 0.01, + "description": "로그인, 결제 등 핵심 페이지", + "action": "fail" + }, + "high": { + "value": 0.05, + "description": "폼, 상세 페이지", + "action": "warn" + }, + "medium": { + "value": 0.1, + "description": "목록, 대시보드", + "action": "warn" + }, + "low": { + "value": 0.2, + "description": "동적 콘텐츠가 많은 페이지", + "action": "info" + } + }, + + "errorHandling": { + "onCaptureFailure": { + "action": "retry", + "maxRetries": 2, + "continueTest": true + }, + "onCompareFailure": { + "action": "createNew", + "log": true, + "continueTest": true + }, + "onBaselineNotFound": { + "action": "createBaseline", + "log": true, + "continueTest": true + } + }, + + "integration": { + "withCRUD": { + "captureAfterCreate": true, + "captureAfterUpdate": true, + "captureBeforeDelete": true + }, + "withPerformance": { + "captureOnSlowLoad": true, + "slowLoadThreshold": 3000 + }, + "withAPI": { + "captureOnError": true, + "captureOn500": true + } + } +}