refactor: _global*.json 설정 파일을 e2e/docs로 이동
This commit is contained in:
@@ -1,304 +0,0 @@
|
||||
{
|
||||
"$schema": "E2E Accessibility Testing Configuration",
|
||||
"version": "1.0.0",
|
||||
"description": "WCAG 기반 접근성 테스트 설정",
|
||||
"lastUpdated": "2026-01-31",
|
||||
|
||||
"accessibility": {
|
||||
"enabled": true,
|
||||
"standard": "WCAG 2.1",
|
||||
"level": "AA",
|
||||
"runOnEveryPage": true,
|
||||
"failOnViolation": false
|
||||
},
|
||||
|
||||
"wcagLevels": {
|
||||
"A": {
|
||||
"description": "최소 접근성 수준",
|
||||
"required": true,
|
||||
"criteria": 30
|
||||
},
|
||||
"AA": {
|
||||
"description": "권장 접근성 수준 (법적 기준)",
|
||||
"required": true,
|
||||
"criteria": 20
|
||||
},
|
||||
"AAA": {
|
||||
"description": "최고 접근성 수준",
|
||||
"required": false,
|
||||
"criteria": 28
|
||||
}
|
||||
},
|
||||
|
||||
"categories": {
|
||||
"perceivable": {
|
||||
"description": "인식의 용이성",
|
||||
"checks": [
|
||||
"imageAltText",
|
||||
"colorContrast",
|
||||
"textResize",
|
||||
"audioVideoAlternatives",
|
||||
"contentStructure"
|
||||
]
|
||||
},
|
||||
"operable": {
|
||||
"description": "운용의 용이성",
|
||||
"checks": [
|
||||
"keyboardAccessible",
|
||||
"focusVisible",
|
||||
"skipLinks",
|
||||
"pageTitle",
|
||||
"linkPurpose",
|
||||
"noKeyboardTrap"
|
||||
]
|
||||
},
|
||||
"understandable": {
|
||||
"description": "이해의 용이성",
|
||||
"checks": [
|
||||
"languageOfPage",
|
||||
"consistentNavigation",
|
||||
"errorIdentification",
|
||||
"labels",
|
||||
"errorSuggestion"
|
||||
]
|
||||
},
|
||||
"robust": {
|
||||
"description": "견고성",
|
||||
"checks": [
|
||||
"validHTML",
|
||||
"ariaUsage",
|
||||
"nameRoleValue",
|
||||
"statusMessages"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
"rules": {
|
||||
"imageAltText": {
|
||||
"id": "image-alt",
|
||||
"description": "이미지에 대체 텍스트 필수",
|
||||
"wcag": "1.1.1",
|
||||
"level": "A",
|
||||
"impact": "critical",
|
||||
"selector": "img",
|
||||
"check": "hasAltAttribute"
|
||||
},
|
||||
"colorContrast": {
|
||||
"id": "color-contrast",
|
||||
"description": "텍스트와 배경의 명도 대비",
|
||||
"wcag": "1.4.3",
|
||||
"level": "AA",
|
||||
"impact": "serious",
|
||||
"ratios": {
|
||||
"normalText": 4.5,
|
||||
"largeText": 3,
|
||||
"uiComponents": 3
|
||||
}
|
||||
},
|
||||
"keyboardAccessible": {
|
||||
"id": "keyboard",
|
||||
"description": "키보드로 모든 기능 사용 가능",
|
||||
"wcag": "2.1.1",
|
||||
"level": "A",
|
||||
"impact": "critical",
|
||||
"check": "focusableElements"
|
||||
},
|
||||
"focusVisible": {
|
||||
"id": "focus-visible",
|
||||
"description": "포커스 표시 명확",
|
||||
"wcag": "2.4.7",
|
||||
"level": "AA",
|
||||
"impact": "serious",
|
||||
"check": "hasFocusIndicator"
|
||||
},
|
||||
"formLabels": {
|
||||
"id": "label",
|
||||
"description": "폼 요소에 레이블 연결",
|
||||
"wcag": "1.3.1",
|
||||
"level": "A",
|
||||
"impact": "critical",
|
||||
"selector": "input, select, textarea",
|
||||
"check": "hasAssociatedLabel"
|
||||
},
|
||||
"linkPurpose": {
|
||||
"id": "link-name",
|
||||
"description": "링크 목적 명확",
|
||||
"wcag": "2.4.4",
|
||||
"level": "A",
|
||||
"impact": "serious",
|
||||
"selector": "a[href]",
|
||||
"check": "hasAccessibleName"
|
||||
},
|
||||
"headingOrder": {
|
||||
"id": "heading-order",
|
||||
"description": "제목 순서 논리적",
|
||||
"wcag": "1.3.1",
|
||||
"level": "A",
|
||||
"impact": "moderate",
|
||||
"check": "sequentialHeadings"
|
||||
},
|
||||
"ariaValid": {
|
||||
"id": "aria-valid",
|
||||
"description": "ARIA 속성 올바른 사용",
|
||||
"wcag": "4.1.2",
|
||||
"level": "A",
|
||||
"impact": "critical",
|
||||
"check": "validAriaAttributes"
|
||||
},
|
||||
"buttonName": {
|
||||
"id": "button-name",
|
||||
"description": "버튼에 접근 가능한 이름",
|
||||
"wcag": "4.1.2",
|
||||
"level": "A",
|
||||
"impact": "critical",
|
||||
"selector": "button, [role='button']",
|
||||
"check": "hasAccessibleName"
|
||||
},
|
||||
"tabIndex": {
|
||||
"id": "tabindex",
|
||||
"description": "tabindex 올바른 사용",
|
||||
"wcag": "2.4.3",
|
||||
"level": "A",
|
||||
"impact": "serious",
|
||||
"check": "validTabIndex"
|
||||
}
|
||||
},
|
||||
|
||||
"scripts": {
|
||||
"runAccessibilityAudit": "(async function() { const issues = []; const checks = { images: [], forms: [], buttons: [], links: [], headings: [], aria: [], focus: [], contrast: [] }; document.querySelectorAll('img').forEach(img => { if (!img.alt && !img.getAttribute('aria-label') && !img.getAttribute('aria-labelledby')) { checks.images.push({ element: 'img', src: img.src?.substring(0, 50), issue: 'Missing alt text', wcag: '1.1.1', impact: 'critical' }); } }); document.querySelectorAll('input, select, textarea').forEach(input => { const id = input.id; const hasLabel = id && document.querySelector(`label[for='${id}']`); const hasAriaLabel = input.getAttribute('aria-label') || input.getAttribute('aria-labelledby'); if (!hasLabel && !hasAriaLabel && input.type !== 'hidden' && input.type !== 'submit' && input.type !== 'button') { checks.forms.push({ element: input.tagName.toLowerCase(), type: input.type, issue: 'Missing label', wcag: '1.3.1', impact: 'critical' }); } }); document.querySelectorAll('button, [role=\"button\"]').forEach(btn => { const hasName = btn.innerText?.trim() || btn.getAttribute('aria-label') || btn.getAttribute('title'); if (!hasName) { checks.buttons.push({ element: 'button', issue: 'Missing accessible name', wcag: '4.1.2', impact: 'critical' }); } }); document.querySelectorAll('a[href]').forEach(link => { const hasName = link.innerText?.trim() || link.getAttribute('aria-label'); if (!hasName) { checks.links.push({ element: 'a', href: link.href?.substring(0, 30), issue: 'Missing link text', wcag: '2.4.4', impact: 'serious' }); } }); const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); let lastLevel = 0; headings.forEach(h => { const level = parseInt(h.tagName[1]); if (level > lastLevel + 1 && lastLevel !== 0) { checks.headings.push({ element: h.tagName, issue: `Skipped heading level (h${lastLevel} to h${level})`, wcag: '1.3.1', impact: 'moderate' }); } lastLevel = level; }); const summary = { total: 0, critical: 0, serious: 0, moderate: 0, minor: 0 }; Object.values(checks).forEach(arr => { arr.forEach(issue => { summary.total++; summary[issue.impact] = (summary[issue.impact] || 0) + 1; }); }); return { url: window.location.href, timestamp: new Date().toISOString(), summary, checks, passed: summary.critical === 0 }; })()",
|
||||
|
||||
"checkColorContrast": "(function() { const results = []; const textElements = document.querySelectorAll('p, span, a, button, label, h1, h2, h3, h4, h5, h6, li, td, th'); const sampleSize = Math.min(textElements.length, 50); for (let i = 0; i < sampleSize; i++) { const el = textElements[i]; const style = window.getComputedStyle(el); const color = style.color; const bgColor = style.backgroundColor; if (color && bgColor && bgColor !== 'rgba(0, 0, 0, 0)') { results.push({ element: el.tagName, text: el.innerText?.substring(0, 20), color, bgColor }); } } return { checked: sampleSize, results: results.slice(0, 10) }; })()",
|
||||
|
||||
"checkKeyboardNavigation": "(async function() { const focusable = document.querySelectorAll('a[href], button, input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'); const results = { total: focusable.length, withFocusStyle: 0, tabbable: 0, issues: [] }; focusable.forEach(el => { const tabIndex = el.getAttribute('tabindex'); if (tabIndex === null || parseInt(tabIndex) >= 0) results.tabbable++; el.focus(); const style = window.getComputedStyle(el); if (style.outlineWidth !== '0px' || style.boxShadow !== 'none') { results.withFocusStyle++; } else { results.issues.push({ element: el.tagName, text: el.innerText?.substring(0, 20) || el.placeholder, issue: 'No visible focus indicator' }); } }); return results; })()",
|
||||
|
||||
"checkAriaUsage": "(function() { const ariaElements = document.querySelectorAll('[aria-label], [aria-labelledby], [aria-describedby], [role]'); const results = { total: ariaElements.length, valid: 0, issues: [] }; ariaElements.forEach(el => { const role = el.getAttribute('role'); const ariaLabelledby = el.getAttribute('aria-labelledby'); const ariaDescribedby = el.getAttribute('aria-describedby'); if (ariaLabelledby) { const labelEl = document.getElementById(ariaLabelledby); if (!labelEl) { results.issues.push({ element: el.tagName, issue: `aria-labelledby references non-existent id: ${ariaLabelledby}` }); } } if (ariaDescribedby) { const descEl = document.getElementById(ariaDescribedby); if (!descEl) { results.issues.push({ element: el.tagName, issue: `aria-describedby references non-existent id: ${ariaDescribedby}` }); } } if (!results.issues.find(i => i.element === el.tagName)) results.valid++; }); return results; })()",
|
||||
|
||||
"checkFormAccessibility": "(function() { const forms = document.querySelectorAll('form'); const results = { forms: forms.length, issues: [] }; forms.forEach((form, formIndex) => { const inputs = form.querySelectorAll('input, select, textarea'); inputs.forEach(input => { if (input.type === 'hidden' || input.type === 'submit' || input.type === 'button') return; const id = input.id; const name = input.name; const hasLabel = id && document.querySelector(`label[for='${id}']`); const hasAriaLabel = input.getAttribute('aria-label'); const hasPlaceholder = input.placeholder; if (!hasLabel && !hasAriaLabel) { results.issues.push({ form: formIndex + 1, element: input.tagName, type: input.type, name: name || id, issue: 'No associated label', suggestion: hasPlaceholder ? 'Has placeholder but no label' : 'Add label element or aria-label' }); } }); const submitBtn = form.querySelector('button[type=\"submit\"], input[type=\"submit\"]'); if (!submitBtn) { results.issues.push({ form: formIndex + 1, issue: 'No submit button found' }); } }); return results; })()",
|
||||
|
||||
"generateAccessibilityReport": "(function() { const audit = { url: window.location.href, timestamp: new Date().toISOString(), images: { total: 0, withAlt: 0, issues: [] }, forms: { total: 0, labeled: 0, issues: [] }, buttons: { total: 0, named: 0, issues: [] }, links: { total: 0, named: 0, issues: [] }, headings: { structure: [], issues: [] }, landmarks: [], focusable: 0 }; document.querySelectorAll('img').forEach(img => { audit.images.total++; if (img.alt || img.getAttribute('aria-label')) audit.images.withAlt++; else audit.images.issues.push({ src: img.src?.split('/').pop() }); }); document.querySelectorAll('input:not([type=hidden]), select, textarea').forEach(el => { audit.forms.total++; const hasLabel = (el.id && document.querySelector(`label[for='${el.id}']`)) || el.getAttribute('aria-label'); if (hasLabel) audit.forms.labeled++; else audit.forms.issues.push({ type: el.type, name: el.name }); }); document.querySelectorAll('button, [role=button]').forEach(btn => { audit.buttons.total++; if (btn.innerText?.trim() || btn.getAttribute('aria-label')) audit.buttons.named++; else audit.buttons.issues.push({ class: btn.className?.substring(0, 30) }); }); document.querySelectorAll('h1,h2,h3,h4,h5,h6').forEach(h => { audit.headings.structure.push({ level: h.tagName, text: h.innerText?.substring(0, 30) }); }); audit.landmarks = Array.from(document.querySelectorAll('header, nav, main, aside, footer, [role=banner], [role=navigation], [role=main], [role=complementary], [role=contentinfo]')).map(el => el.tagName.toLowerCase() + (el.getAttribute('role') ? `[role=${el.getAttribute('role')}]` : '')); audit.focusable = document.querySelectorAll('a[href], button, input, select, textarea, [tabindex]:not([tabindex=\"-1\"])').length; const score = Math.round(((audit.images.withAlt / Math.max(audit.images.total, 1)) + (audit.forms.labeled / Math.max(audit.forms.total, 1)) + (audit.buttons.named / Math.max(audit.buttons.total, 1))) / 3 * 100); return { ...audit, score, grade: score >= 90 ? 'A' : score >= 70 ? 'B' : score >= 50 ? 'C' : 'F' }; })()"
|
||||
},
|
||||
|
||||
"impactLevels": {
|
||||
"critical": {
|
||||
"description": "스크린 리더 사용자가 콘텐츠 접근 불가",
|
||||
"action": "즉시 수정",
|
||||
"examples": ["이미지 alt 누락", "폼 레이블 누락", "키보드 접근 불가"]
|
||||
},
|
||||
"serious": {
|
||||
"description": "접근성 심각한 장애",
|
||||
"action": "우선 수정",
|
||||
"examples": ["색상 대비 부족", "포커스 표시 없음", "링크 텍스트 없음"]
|
||||
},
|
||||
"moderate": {
|
||||
"description": "사용성 저하",
|
||||
"action": "개선 권장",
|
||||
"examples": ["제목 순서 불규칙", "랜드마크 누락"]
|
||||
},
|
||||
"minor": {
|
||||
"description": "모범 사례 미준수",
|
||||
"action": "선택적 개선",
|
||||
"examples": ["중복 ID", "빈 링크"]
|
||||
}
|
||||
},
|
||||
|
||||
"reporting": {
|
||||
"includeInTestReport": true,
|
||||
"separateReport": true,
|
||||
"format": {
|
||||
"summary": {
|
||||
"score": "접근성 점수 (0-100)",
|
||||
"grade": "등급 (A/B/C/F)",
|
||||
"violations": "위반 사항 수",
|
||||
"passed": "통과 항목 수"
|
||||
},
|
||||
"detail": {
|
||||
"rule": "위반 규칙",
|
||||
"impact": "영향도",
|
||||
"wcag": "WCAG 기준",
|
||||
"element": "해당 요소",
|
||||
"suggestion": "수정 제안"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"header": "## ♿ 접근성 테스트 결과",
|
||||
"sections": {
|
||||
"summary": "### 요약",
|
||||
"violations": "### 위반 사항",
|
||||
"passed": "### 통과 항목",
|
||||
"recommendations": "### 개선 권장"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"thresholds": {
|
||||
"pass": {
|
||||
"score": 70,
|
||||
"critical": 0,
|
||||
"serious": 5
|
||||
},
|
||||
"warn": {
|
||||
"score": 50,
|
||||
"critical": 3,
|
||||
"serious": 10
|
||||
}
|
||||
},
|
||||
|
||||
"pageSpecific": {
|
||||
"login": {
|
||||
"critical": ["formLabels", "buttonName", "colorContrast"],
|
||||
"focus": ["keyboardAccessible", "focusVisible"]
|
||||
},
|
||||
"dashboard": {
|
||||
"critical": ["imageAltText", "linkPurpose", "headingOrder"],
|
||||
"focus": ["ariaValid", "landmarks"]
|
||||
},
|
||||
"form": {
|
||||
"critical": ["formLabels", "errorIdentification", "inputInstructions"],
|
||||
"focus": ["focusOrder", "errorSuggestion"]
|
||||
},
|
||||
"table": {
|
||||
"critical": ["tableHeaders", "tableCaption"],
|
||||
"focus": ["tableSummary"]
|
||||
}
|
||||
},
|
||||
|
||||
"automation": {
|
||||
"runOn": {
|
||||
"pageLoad": true,
|
||||
"formSubmit": false,
|
||||
"modalOpen": true
|
||||
},
|
||||
"skipElements": [
|
||||
"[aria-hidden='true']",
|
||||
"[hidden]",
|
||||
".sr-only",
|
||||
".visually-hidden"
|
||||
]
|
||||
},
|
||||
|
||||
"recommendations": {
|
||||
"imageAltText": "모든 이미지에 alt 속성 추가. 장식용 이미지는 alt='' 사용",
|
||||
"formLabels": "<label for='id'>를 사용하거나 aria-label 속성 추가",
|
||||
"colorContrast": "텍스트와 배경의 명도 대비 4.5:1 이상 유지",
|
||||
"keyboardNav": "모든 인터랙티브 요소에 키보드로 접근 가능하게",
|
||||
"focusVisible": "포커스 시 outline 또는 다른 시각적 표시 제공",
|
||||
"headingOrder": "h1 → h2 → h3 순서로 사용, 레벨 건너뛰기 금지",
|
||||
"linkPurpose": "링크 텍스트만으로 목적지 이해 가능하게",
|
||||
"ariaUsage": "ARIA는 네이티브 HTML이 불가능할 때만 사용"
|
||||
},
|
||||
|
||||
"integration": {
|
||||
"withVisual": {
|
||||
"captureOnViolation": true,
|
||||
"highlightIssues": true
|
||||
},
|
||||
"withPerformance": {
|
||||
"trackLoadTime": true,
|
||||
"correlateWithA11y": true
|
||||
},
|
||||
"withCRUD": {
|
||||
"checkForms": true,
|
||||
"checkMessages": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
{
|
||||
"$schema": "E2E Global API Monitoring Configuration",
|
||||
"version": "1.0.0",
|
||||
"description": "모든 E2E 테스트 시나리오에 적용되는 API 요청/응답 검증 설정",
|
||||
"lastUpdated": "2026-01-31",
|
||||
|
||||
"apiMonitoring": {
|
||||
"enabled": true,
|
||||
"captureAllRequests": true,
|
||||
"baseUrl": "https://dev.codebridge-x.com",
|
||||
"apiPathPrefix": "/api/",
|
||||
|
||||
"captureConfig": {
|
||||
"includePatterns": [
|
||||
"/api/v1/**",
|
||||
"/api/**"
|
||||
],
|
||||
"excludePatterns": [
|
||||
"/_next/**",
|
||||
"/static/**",
|
||||
"*.js",
|
||||
"*.css",
|
||||
"*.png",
|
||||
"*.jpg",
|
||||
"*.svg",
|
||||
"*.woff",
|
||||
"*.woff2"
|
||||
],
|
||||
"captureRequestBody": true,
|
||||
"captureResponseBody": true,
|
||||
"maxBodySize": 10240
|
||||
}
|
||||
},
|
||||
|
||||
"validation": {
|
||||
"defaultRules": {
|
||||
"statusCodes": {
|
||||
"success": [200, 201, 204],
|
||||
"clientError": [400, 401, 403, 404],
|
||||
"serverError": [500, 502, 503]
|
||||
},
|
||||
"responseTime": {
|
||||
"warning": 2000,
|
||||
"error": 5000
|
||||
},
|
||||
"contentType": {
|
||||
"json": "application/json",
|
||||
"pdf": "application/pdf",
|
||||
"excel": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
}
|
||||
},
|
||||
|
||||
"methodRules": {
|
||||
"GET": {
|
||||
"expectedStatus": [200],
|
||||
"maxResponseTime": 3000
|
||||
},
|
||||
"POST": {
|
||||
"expectedStatus": [200, 201],
|
||||
"maxResponseTime": 5000,
|
||||
"requireResponseBody": true
|
||||
},
|
||||
"PUT": {
|
||||
"expectedStatus": [200],
|
||||
"maxResponseTime": 5000
|
||||
},
|
||||
"DELETE": {
|
||||
"expectedStatus": [200, 204],
|
||||
"maxResponseTime": 3000
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"scripts": {
|
||||
"setupApiMonitoring": "(function() { window.__API_LOGS__ = []; window.__API_ERRORS__ = []; const originalFetch = window.fetch; window.fetch = async function(...args) { const startTime = Date.now(); const url = typeof args[0] === 'string' ? args[0] : args[0].url; const method = args[1]?.method || 'GET'; try { const response = await originalFetch.apply(this, args); const endTime = Date.now(); const logEntry = { url, method, status: response.status, duration: endTime - startTime, timestamp: new Date().toISOString(), ok: response.ok }; window.__API_LOGS__.push(logEntry); if (!response.ok) { window.__API_ERRORS__.push(logEntry); } return response; } catch (error) { const errorEntry = { url, method, error: error.message, timestamp: new Date().toISOString() }; window.__API_ERRORS__.push(errorEntry); throw error; } }; return 'API monitoring initialized'; })()",
|
||||
|
||||
"getApiLogs": "(function() { return JSON.stringify(window.__API_LOGS__ || []); })()",
|
||||
|
||||
"getApiErrors": "(function() { return JSON.stringify(window.__API_ERRORS__ || []); })()",
|
||||
|
||||
"clearApiLogs": "(function() { window.__API_LOGS__ = []; window.__API_ERRORS__ = []; return 'API logs cleared'; })()",
|
||||
|
||||
"getApiSummary": "(function() { const logs = window.__API_LOGS__ || []; const errors = window.__API_ERRORS__ || []; const summary = { total: logs.length, success: logs.filter(l => l.ok).length, failed: errors.length, avgResponseTime: logs.length > 0 ? Math.round(logs.reduce((sum, l) => sum + (l.duration || 0), 0) / logs.length) : 0, slowRequests: logs.filter(l => l.duration > 2000).length, byMethod: {}, byStatus: {} }; logs.forEach(l => { summary.byMethod[l.method] = (summary.byMethod[l.method] || 0) + 1; summary.byStatus[l.status] = (summary.byStatus[l.status] || 0) + 1; }); return JSON.stringify(summary); })()",
|
||||
|
||||
"waitForApiCall": "(async function(urlPattern, timeout = 5000) { const start = Date.now(); while (Date.now() - start < timeout) { const logs = window.__API_LOGS__ || []; const found = logs.find(l => l.url.includes(urlPattern)); if (found) return JSON.stringify(found); await new Promise(r => setTimeout(r, 100)); } return JSON.stringify({ error: 'timeout', pattern: urlPattern }); })",
|
||||
|
||||
"validateApiCall": "(function(urlPattern, expectedStatus) { const logs = window.__API_LOGS__ || []; const matching = logs.filter(l => l.url.includes(urlPattern)); if (matching.length === 0) return JSON.stringify({ valid: false, error: 'API call not found', pattern: urlPattern }); const latest = matching[matching.length - 1]; const statusMatch = Array.isArray(expectedStatus) ? expectedStatus.includes(latest.status) : latest.status === expectedStatus; return JSON.stringify({ valid: statusMatch, call: latest, expected: expectedStatus }); })"
|
||||
},
|
||||
|
||||
"commonApiEndpoints": {
|
||||
"auth": {
|
||||
"login": { "method": "POST", "path": "/api/v1/auth/login", "expectedStatus": 200 },
|
||||
"logout": { "method": "POST", "path": "/api/v1/auth/logout", "expectedStatus": 200 },
|
||||
"refresh": { "method": "POST", "path": "/api/v1/auth/refresh", "expectedStatus": 200 }
|
||||
},
|
||||
"clients": {
|
||||
"list": { "method": "GET", "path": "/api/v1/clients", "expectedStatus": 200 },
|
||||
"detail": { "method": "GET", "path": "/api/v1/clients/{id}", "expectedStatus": 200 },
|
||||
"create": { "method": "POST", "path": "/api/v1/clients", "expectedStatus": [200, 201] },
|
||||
"update": { "method": "PUT", "path": "/api/v1/clients/{id}", "expectedStatus": 200 },
|
||||
"delete": { "method": "DELETE", "path": "/api/v1/clients/{id}", "expectedStatus": [200, 204] }
|
||||
},
|
||||
"approvals": {
|
||||
"inbox": { "method": "GET", "path": "/api/v1/approvals/inbox", "expectedStatus": 200 },
|
||||
"detail": { "method": "GET", "path": "/api/v1/approvals/{id}", "expectedStatus": 200 },
|
||||
"approve": { "method": "POST", "path": "/api/v1/approvals/{id}/approve", "expectedStatus": 200 },
|
||||
"reject": { "method": "POST", "path": "/api/v1/approvals/{id}/reject", "expectedStatus": 200 }
|
||||
}
|
||||
},
|
||||
|
||||
"reportTemplate": {
|
||||
"apiSection": {
|
||||
"title": "API 호출 검증 결과",
|
||||
"fields": [
|
||||
"총 API 호출 수",
|
||||
"성공 호출",
|
||||
"실패 호출",
|
||||
"평균 응답 시간",
|
||||
"느린 요청 (>2초)"
|
||||
]
|
||||
},
|
||||
"errorSection": {
|
||||
"title": "API 오류 상세",
|
||||
"fields": [
|
||||
"URL",
|
||||
"Method",
|
||||
"Status",
|
||||
"응답 시간",
|
||||
"오류 메시지"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
"actionHooks": {
|
||||
"beforeScenario": {
|
||||
"description": "시나리오 시작 전 API 모니터링 초기화",
|
||||
"actions": [
|
||||
"setupApiMonitoring",
|
||||
"clearApiLogs"
|
||||
]
|
||||
},
|
||||
"afterStep": {
|
||||
"description": "각 스텝 후 API 호출 확인",
|
||||
"actions": [
|
||||
"checkForApiErrors"
|
||||
]
|
||||
},
|
||||
"afterScenario": {
|
||||
"description": "시나리오 종료 후 API 요약 생성",
|
||||
"actions": [
|
||||
"getApiSummary",
|
||||
"generateApiReport"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
"errorHandling": {
|
||||
"on500Error": {
|
||||
"action": "captureAndReport",
|
||||
"screenshot": true,
|
||||
"logApiDetails": true,
|
||||
"continueTest": false
|
||||
},
|
||||
"on401Error": {
|
||||
"action": "reAuthenticate",
|
||||
"maxRetries": 1,
|
||||
"continueTest": true
|
||||
},
|
||||
"on404Error": {
|
||||
"action": "logAndContinue",
|
||||
"screenshot": true,
|
||||
"continueTest": true
|
||||
},
|
||||
"onTimeout": {
|
||||
"action": "retry",
|
||||
"maxRetries": 2,
|
||||
"continueTest": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,321 +0,0 @@
|
||||
{
|
||||
"$schema": "E2E Global CRUD Testing Configuration",
|
||||
"version": "1.0.0",
|
||||
"description": "CRUD 전체 흐름 테스트를 위한 전역 설정 - 생성→조회→수정→삭제 완전 검증",
|
||||
"lastUpdated": "2026-01-31",
|
||||
|
||||
"crudPolicy": {
|
||||
"description": "CRUD 테스트 정책",
|
||||
"rules": [
|
||||
"테스트 데이터는 반드시 테스트 내에서 생성",
|
||||
"기존 데이터 수정/삭제 절대 금지",
|
||||
"삭제는 생성한 데이터에 대해서만 허용",
|
||||
"테스트 실패 시에도 생성된 데이터 정리 시도",
|
||||
"테스트 데이터는 고유 식별자로 구분"
|
||||
],
|
||||
"testDataPrefix": "E2E_TEST_",
|
||||
"uniqueIdentifier": "timestamp",
|
||||
"cleanupOnFailure": true
|
||||
},
|
||||
|
||||
"phases": {
|
||||
"CREATE": {
|
||||
"order": 1,
|
||||
"description": "테스트 데이터 생성",
|
||||
"requiredValidations": [
|
||||
"등록 폼/모달 열림 확인",
|
||||
"필수 필드 입력",
|
||||
"저장 버튼 클릭",
|
||||
"API 호출 확인 (POST)",
|
||||
"성공 토스트/메시지 확인",
|
||||
"목록에 데이터 표시 확인"
|
||||
],
|
||||
"expectedApi": {
|
||||
"method": "POST",
|
||||
"successStatus": [200, 201],
|
||||
"responseValidation": "id 또는 생성된 데이터 반환"
|
||||
},
|
||||
"onSuccess": "createdId 저장 → READ 단계 진행",
|
||||
"onFailure": "스크린샷 + 오류 로그 → 테스트 중단"
|
||||
},
|
||||
"READ": {
|
||||
"order": 2,
|
||||
"description": "생성된 데이터 조회 확인",
|
||||
"requiredValidations": [
|
||||
"목록에서 생성된 데이터 검색",
|
||||
"상세 페이지 진입",
|
||||
"입력한 데이터가 정확히 표시되는지 확인",
|
||||
"모든 필드 값 검증"
|
||||
],
|
||||
"expectedApi": {
|
||||
"method": "GET",
|
||||
"successStatus": [200],
|
||||
"responseValidation": "생성 시 입력한 데이터와 일치"
|
||||
},
|
||||
"onSuccess": "UPDATE 단계 진행",
|
||||
"onFailure": "스크린샷 + 데이터 불일치 기록 → DELETE 단계로 건너뛰기"
|
||||
},
|
||||
"UPDATE": {
|
||||
"order": 3,
|
||||
"description": "데이터 수정",
|
||||
"requiredValidations": [
|
||||
"수정 모드 진입 (URL: ?mode=edit 또는 수정 버튼)",
|
||||
"필드 값 변경",
|
||||
"저장 버튼 클릭",
|
||||
"API 호출 확인 (PUT/PATCH)",
|
||||
"성공 토스트/메시지 확인",
|
||||
"변경된 데이터 표시 확인"
|
||||
],
|
||||
"expectedApi": {
|
||||
"method": ["PUT", "PATCH"],
|
||||
"successStatus": [200],
|
||||
"responseValidation": "수정된 데이터 반환"
|
||||
},
|
||||
"onSuccess": "DELETE 단계 진행",
|
||||
"onFailure": "스크린샷 + 오류 로그 → DELETE 단계 진행 (정리 필요)"
|
||||
},
|
||||
"DELETE": {
|
||||
"order": 4,
|
||||
"description": "테스트 데이터 삭제 (정리)",
|
||||
"requiredValidations": [
|
||||
"삭제 버튼 클릭",
|
||||
"확인 다이얼로그 표시",
|
||||
"삭제 확인 클릭",
|
||||
"API 호출 확인 (DELETE)",
|
||||
"성공 토스트/메시지 확인",
|
||||
"목록에서 데이터 제거 확인"
|
||||
],
|
||||
"expectedApi": {
|
||||
"method": "DELETE",
|
||||
"successStatus": [200, 204],
|
||||
"responseValidation": "삭제 성공 응답"
|
||||
},
|
||||
"onSuccess": "테스트 완료 (PASS)",
|
||||
"onFailure": "수동 정리 필요 경고 + 테스트 데이터 정보 기록"
|
||||
}
|
||||
},
|
||||
|
||||
"testDataTemplates": {
|
||||
"vendor": {
|
||||
"entityName": "거래처",
|
||||
"menuPath": "회계관리 > 거래처관리",
|
||||
"url": "/accounting/vendors",
|
||||
"fields": {
|
||||
"vendorName": { "type": "text", "required": true, "testValue": "E2E_TEST_거래처_{timestamp}" },
|
||||
"businessNumber": { "type": "text", "required": false, "testValue": "123-45-67890" },
|
||||
"representative": { "type": "text", "required": false, "testValue": "테스트대표" },
|
||||
"vendorType": { "type": "select", "required": true, "testValue": "매출" },
|
||||
"phone": { "type": "text", "required": false, "testValue": "02-1234-5678" },
|
||||
"email": { "type": "email", "required": false, "testValue": "e2e@test.com" }
|
||||
},
|
||||
"updateFields": {
|
||||
"vendorName": "E2E_TEST_수정완료_{timestamp}",
|
||||
"representative": "수정대표"
|
||||
},
|
||||
"searchField": "vendorName",
|
||||
"api": {
|
||||
"list": "GET /api/v1/clients",
|
||||
"create": "POST /api/v1/clients",
|
||||
"read": "GET /api/v1/clients/{id}",
|
||||
"update": "PUT /api/v1/clients/{id}",
|
||||
"delete": "DELETE /api/v1/clients/{id}"
|
||||
}
|
||||
},
|
||||
"freeBoard": {
|
||||
"entityName": "자유게시판 글",
|
||||
"menuPath": "게시판 > 자유게시판",
|
||||
"url": "/boards/free",
|
||||
"fields": {
|
||||
"title": { "type": "text", "required": true, "testValue": "E2E_TEST_게시글_{timestamp}" },
|
||||
"content": { "type": "textarea", "required": true, "testValue": "E2E 테스트용 게시글 내용입니다. 자동 생성됨." }
|
||||
},
|
||||
"updateFields": {
|
||||
"title": "E2E_TEST_수정됨_{timestamp}",
|
||||
"content": "수정된 게시글 내용입니다."
|
||||
},
|
||||
"searchField": "title",
|
||||
"api": {
|
||||
"list": "GET /api/v1/boards/free",
|
||||
"create": "POST /api/v1/boards/free",
|
||||
"read": "GET /api/v1/boards/free/{id}",
|
||||
"update": "PUT /api/v1/boards/free/{id}",
|
||||
"delete": "DELETE /api/v1/boards/free/{id}"
|
||||
}
|
||||
},
|
||||
"deposit": {
|
||||
"entityName": "입금",
|
||||
"menuPath": "회계관리 > 입금관리",
|
||||
"url": "/accounting/deposits",
|
||||
"fields": {
|
||||
"vendorName": { "type": "select", "required": true, "testValue": "가우스전자" },
|
||||
"amount": { "type": "number", "required": true, "testValue": "100000" },
|
||||
"depositDate": { "type": "date", "required": true, "testValue": "today" },
|
||||
"description": { "type": "text", "required": false, "testValue": "E2E_TEST_입금_{timestamp}" }
|
||||
},
|
||||
"updateFields": {
|
||||
"amount": "200000",
|
||||
"description": "E2E_TEST_수정입금_{timestamp}"
|
||||
},
|
||||
"searchField": "description"
|
||||
}
|
||||
},
|
||||
|
||||
"scripts": {
|
||||
"generateTimestamp": "(function() { const now = new Date(); const pad = n => n.toString().padStart(2, '0'); return `${now.getFullYear()}${pad(now.getMonth()+1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`; })()",
|
||||
|
||||
"generateTestData": "(function(template, timestamp) { const result = {}; for (const [key, config] of Object.entries(template.fields)) { let value = config.testValue; if (typeof value === 'string' && value.includes('{timestamp}')) { value = value.replace('{timestamp}', timestamp); } if (value === 'today') { value = new Date().toISOString().split('T')[0]; } result[key] = value; } return result; })",
|
||||
|
||||
"findTestDataInTable": "(function(searchText) { const rows = document.querySelectorAll('table tbody tr'); for (const row of rows) { if (row.innerText.includes(searchText)) { return { found: true, row: row, text: row.innerText.substring(0, 100) }; } } return { found: false }; })",
|
||||
|
||||
"verifyDataCreated": "(async function(searchText, timeout = 5000) { const start = Date.now(); while (Date.now() - start < timeout) { const result = findTestDataInTable(searchText); if (result.found) return result; await new Promise(r => setTimeout(r, 500)); } return { found: false, error: 'timeout' }; })",
|
||||
|
||||
"clickCreateButton": "(function() { const buttons = Array.from(document.querySelectorAll('button')); const createBtn = buttons.find(b => ['등록', '추가', '신규', '작성'].some(t => b.innerText?.includes(t))); if (createBtn) { createBtn.click(); return { clicked: true, text: createBtn.innerText }; } return { clicked: false, error: 'Create button not found' }; })",
|
||||
|
||||
"clickEditButton": "(function() { const buttons = Array.from(document.querySelectorAll('button')); const editBtn = buttons.find(b => ['수정', '편집', 'Edit'].some(t => b.innerText?.includes(t))); if (editBtn) { editBtn.click(); return { clicked: true, text: editBtn.innerText }; } return { clicked: false, error: 'Edit button not found' }; })",
|
||||
|
||||
"clickDeleteButton": "(function() { const buttons = Array.from(document.querySelectorAll('button')); const deleteBtn = buttons.find(b => ['삭제', '제거', 'Delete'].some(t => b.innerText?.includes(t))); if (deleteBtn) { deleteBtn.click(); return { clicked: true, text: deleteBtn.innerText }; } return { clicked: false, error: 'Delete button not found' }; })",
|
||||
|
||||
"clickSaveButton": "(function() { const buttons = Array.from(document.querySelectorAll('button')); const saveBtn = buttons.find(b => ['저장', '등록', '확인', 'Save', 'Submit'].some(t => b.innerText?.trim() === t || b.innerText?.includes(t))); if (saveBtn) { saveBtn.click(); return { clicked: true, text: saveBtn.innerText }; } return { clicked: false, error: 'Save button not found' }; })",
|
||||
|
||||
"clickConfirmDialog": "(async function() { await new Promise(r => setTimeout(r, 500)); const dialog = document.querySelector('[role=\"alertdialog\"], [role=\"dialog\"], [class*=\"modal\"]'); if (dialog) { const confirmBtn = Array.from(dialog.querySelectorAll('button')).find(b => ['확인', '예', 'Yes', 'OK', '삭제'].some(t => b.innerText?.includes(t))); if (confirmBtn) { confirmBtn.click(); return { clicked: true, text: confirmBtn.innerText }; } } return { clicked: false, error: 'Confirm button not found' }; })",
|
||||
|
||||
"fillField": "(function(labelOrName, value) { const inputs = document.querySelectorAll('input, textarea, select'); for (const input of inputs) { const label = input.closest('label')?.innerText || input.placeholder || input.name || input.id; if (label?.includes(labelOrName)) { if (input.tagName === 'SELECT') { const option = Array.from(input.options).find(o => o.text.includes(value)); if (option) { input.value = option.value; input.dispatchEvent(new Event('change', { bubbles: true })); return { filled: true, field: labelOrName }; } } else { input.value = value; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); return { filled: true, field: labelOrName }; } } } return { filled: false, error: `Field '${labelOrName}' not found` }; })",
|
||||
|
||||
"getFieldValue": "(function(labelOrName) { const inputs = document.querySelectorAll('input, textarea, select'); for (const input of inputs) { const label = input.closest('label')?.innerText || input.placeholder || input.name || input.id; if (label?.includes(labelOrName)) { return { found: true, field: labelOrName, value: input.value }; } } return { found: false, error: `Field '${labelOrName}' not found` }; })",
|
||||
|
||||
"waitForToast": "(async function(expectedText, timeout = 5000) { const start = Date.now(); while (Date.now() - start < timeout) { const toasts = document.querySelectorAll('[class*=\"toast\"], [class*=\"Toast\"], [class*=\"notification\"], [class*=\"alert\"]:not([role=\"alertdialog\"])'); for (const toast of toasts) { if (toast.innerText?.includes(expectedText)) { return { found: true, text: toast.innerText }; } } await new Promise(r => setTimeout(r, 200)); } return { found: false, error: 'Toast not found', expected: expectedText }; })",
|
||||
|
||||
"captureTestContext": "(function() { return { url: window.location.href, timestamp: new Date().toISOString(), pageTitle: document.title, apiLogs: window.__API_LOGS__ || [], apiErrors: window.__API_ERRORS__ || [] }; })"
|
||||
},
|
||||
|
||||
"flowTemplates": {
|
||||
"standardCRUD": {
|
||||
"description": "표준 CRUD 흐름 템플릿",
|
||||
"steps": [
|
||||
{
|
||||
"phase": "SETUP",
|
||||
"name": "테스트 준비",
|
||||
"actions": [
|
||||
{ "type": "generateTimestamp", "saveAs": "testTimestamp" },
|
||||
{ "type": "initApiMonitoring" },
|
||||
{ "type": "navigateToPage" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "CREATE",
|
||||
"name": "데이터 생성",
|
||||
"actions": [
|
||||
{ "type": "clickCreateButton" },
|
||||
{ "type": "waitForModal", "timeout": 2000 },
|
||||
{ "type": "fillTestData", "template": "{{entityTemplate}}" },
|
||||
{ "type": "clickSaveButton" },
|
||||
{ "type": "waitForToast", "contains": ["등록", "완료", "성공"] },
|
||||
{ "type": "verifyApiCall", "method": "POST", "status": [200, 201] },
|
||||
{ "type": "closeModalIfOpen" },
|
||||
{ "type": "searchTestData" },
|
||||
{ "type": "verifyDataInTable" }
|
||||
],
|
||||
"saveResult": "createdData"
|
||||
},
|
||||
{
|
||||
"phase": "READ",
|
||||
"name": "데이터 조회",
|
||||
"actions": [
|
||||
{ "type": "clickTestDataRow" },
|
||||
{ "type": "waitForDetailPage" },
|
||||
{ "type": "verifyFieldValues", "expected": "{{createdData}}" },
|
||||
{ "type": "verifyApiCall", "method": "GET", "status": [200] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "UPDATE",
|
||||
"name": "데이터 수정",
|
||||
"actions": [
|
||||
{ "type": "clickEditButton" },
|
||||
{ "type": "waitForEditMode" },
|
||||
{ "type": "updateTestData", "template": "{{updateTemplate}}" },
|
||||
{ "type": "clickSaveButton" },
|
||||
{ "type": "waitForToast", "contains": ["수정", "완료", "성공"] },
|
||||
{ "type": "verifyApiCall", "method": ["PUT", "PATCH"], "status": [200] },
|
||||
{ "type": "verifyUpdatedValues" }
|
||||
],
|
||||
"saveResult": "updatedData"
|
||||
},
|
||||
{
|
||||
"phase": "DELETE",
|
||||
"name": "데이터 삭제",
|
||||
"actions": [
|
||||
{ "type": "clickDeleteButton" },
|
||||
{ "type": "waitForConfirmDialog" },
|
||||
{ "type": "clickConfirmDialog" },
|
||||
{ "type": "waitForToast", "contains": ["삭제", "완료", "성공"] },
|
||||
{ "type": "verifyApiCall", "method": "DELETE", "status": [200, 204] },
|
||||
{ "type": "verifyRedirectToList" },
|
||||
{ "type": "verifyDataNotInTable" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "CLEANUP",
|
||||
"name": "정리",
|
||||
"actions": [
|
||||
{ "type": "clearSearchFilter" },
|
||||
{ "type": "captureApiSummary" },
|
||||
{ "type": "generateReport" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
"errorHandling": {
|
||||
"onCreateFailure": {
|
||||
"actions": ["screenshot", "logError", "skipToCleanup"],
|
||||
"testResult": "FAIL",
|
||||
"reason": "CREATE 단계 실패 - 데이터 생성 불가"
|
||||
},
|
||||
"onReadFailure": {
|
||||
"actions": ["screenshot", "logError", "continueToDelete"],
|
||||
"testResult": "PARTIAL",
|
||||
"reason": "READ 단계 실패 - 데이터 조회 불일치"
|
||||
},
|
||||
"onUpdateFailure": {
|
||||
"actions": ["screenshot", "logError", "continueToDelete"],
|
||||
"testResult": "PARTIAL",
|
||||
"reason": "UPDATE 단계 실패 - 수정 저장 실패"
|
||||
},
|
||||
"onDeleteFailure": {
|
||||
"actions": ["screenshot", "logError", "recordManualCleanup"],
|
||||
"testResult": "FAIL",
|
||||
"reason": "DELETE 단계 실패 - 수동 정리 필요",
|
||||
"manualCleanupRequired": true
|
||||
}
|
||||
},
|
||||
|
||||
"reportTemplate": {
|
||||
"sections": [
|
||||
{
|
||||
"name": "테스트 요약",
|
||||
"fields": ["시나리오명", "실행시간", "전체결과", "소요시간"]
|
||||
},
|
||||
{
|
||||
"name": "CRUD 단계별 결과",
|
||||
"fields": ["CREATE", "READ", "UPDATE", "DELETE"],
|
||||
"format": "table"
|
||||
},
|
||||
{
|
||||
"name": "API 호출 검증",
|
||||
"fields": ["총 호출수", "성공", "실패", "응답시간"],
|
||||
"includeDetails": true
|
||||
},
|
||||
{
|
||||
"name": "테스트 데이터",
|
||||
"fields": ["생성 데이터", "수정 데이터", "삭제 확인"]
|
||||
},
|
||||
{
|
||||
"name": "오류 및 경고",
|
||||
"conditional": "hasErrors",
|
||||
"fields": ["오류 메시지", "스크린샷", "수동 정리 필요 여부"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
{
|
||||
"$schema": "E2E Global Modal Handling Configuration",
|
||||
"version": "1.0.0",
|
||||
"description": "모든 E2E 테스트 시나리오에 적용되는 전역 모달/팝업 처리 설정",
|
||||
"lastUpdated": "2026-01-31",
|
||||
|
||||
"modalDetection": {
|
||||
"description": "모달/팝업 감지를 위한 셀렉터",
|
||||
"selectors": [
|
||||
"[role='dialog']",
|
||||
"[role='alertdialog']",
|
||||
"[data-state='open']",
|
||||
".modal",
|
||||
"[class*='modal']",
|
||||
"[class*='Modal']",
|
||||
"[class*='dialog']",
|
||||
"[class*='Dialog']",
|
||||
"[class*='popup']",
|
||||
"[class*='Popup']",
|
||||
"[class*='overlay'][class*='open']",
|
||||
"[aria-modal='true']"
|
||||
],
|
||||
"excludeSelectors": [
|
||||
"[class*='tooltip']",
|
||||
"[class*='Tooltip']",
|
||||
"[class*='dropdown']",
|
||||
"[role='menu']"
|
||||
],
|
||||
"checkScript": "(function() { const selectors = [\"[role='dialog']\", \"[role='alertdialog']\", \"[data-state='open']\", \".modal\", \"[class*='modal']\", \"[class*='Modal']\", \"[class*='Dialog']\", \"[aria-modal='true']\"]; for (const sel of selectors) { const el = document.querySelector(sel); if (el && el.offsetParent !== null && !el.className?.includes('tooltip') && !el.className?.includes('dropdown')) return { isOpen: true, selector: sel, element: el.tagName }; } return { isOpen: false }; })()"
|
||||
},
|
||||
|
||||
"modalFocus": {
|
||||
"description": "모달이 열리면 반드시 모달 내부에서만 동작",
|
||||
"rules": [
|
||||
"모달 감지 시 모달 내부 요소만 선택/클릭 가능",
|
||||
"모달 외부 요소 클릭 시도 전 반드시 모달 닫기 실행",
|
||||
"모달 내 작업 완료 후 다음 스텝 진행 전 모달 상태 확인"
|
||||
],
|
||||
"focusScript": "(function() { const modal = document.querySelector(\"[role='dialog'], [aria-modal='true'], [class*='modal']:not([class*='tooltip'])\"); if (modal && modal.offsetParent !== null) { modal.focus(); return { focused: true, element: modal.tagName }; } return { focused: false }; })()"
|
||||
},
|
||||
|
||||
"modalClose": {
|
||||
"description": "모달 닫기 절차 (우선순위 순서대로 시도)",
|
||||
"methods": [
|
||||
{
|
||||
"priority": 1,
|
||||
"name": "X 버튼 클릭",
|
||||
"description": "모달 우측 상단 X 버튼",
|
||||
"selectors": [
|
||||
"button[class*='close']",
|
||||
"button[class*='Close']",
|
||||
"[aria-label='닫기']",
|
||||
"[aria-label='Close']",
|
||||
"[aria-label='close']",
|
||||
"svg[class*='close']",
|
||||
"[class*='modal-close']",
|
||||
"[class*='dialog-close']",
|
||||
"button[class*='dismiss']"
|
||||
]
|
||||
},
|
||||
{
|
||||
"priority": 2,
|
||||
"name": "닫기/Close 텍스트 버튼",
|
||||
"description": "텍스트가 '닫기' 또는 'Close'인 버튼",
|
||||
"textMatches": ["닫기", "Close", "close"]
|
||||
},
|
||||
{
|
||||
"priority": 3,
|
||||
"name": "취소/Cancel 버튼",
|
||||
"description": "작업 취소 버튼",
|
||||
"textMatches": ["취소", "Cancel", "cancel"]
|
||||
},
|
||||
{
|
||||
"priority": 4,
|
||||
"name": "확인/OK 버튼",
|
||||
"description": "확인 후 닫기 (알림 다이얼로그)",
|
||||
"textMatches": ["확인", "OK", "ok", "예", "Yes"]
|
||||
},
|
||||
{
|
||||
"priority": 5,
|
||||
"name": "ESC 키",
|
||||
"description": "Escape 키로 닫기",
|
||||
"action": "pressKey",
|
||||
"key": "Escape"
|
||||
},
|
||||
{
|
||||
"priority": 6,
|
||||
"name": "백드롭 클릭",
|
||||
"description": "모달 외부 영역 클릭 (최후 수단)",
|
||||
"action": "clickBackdrop"
|
||||
}
|
||||
],
|
||||
"closeScript": "(async function() { const modal = document.querySelector(\"[role='dialog'], [aria-modal='true'], [class*='modal']:not([class*='tooltip']), [class*='Modal'], [class*='Dialog']\"); if (!modal || modal.offsetParent === null) return { status: 'no_modal', closed: true }; const xBtn = modal.querySelector(\"button[class*='close'], button[class*='Close'], [aria-label='닫기'], [aria-label='Close'], svg[class*='close'], [class*='modal-close'], [class*='dialog-close']\"); if (xBtn) { xBtn.click(); await new Promise(r => setTimeout(r, 500)); return { status: 'closed_by_x', closed: true }; } const textBtn = Array.from(modal.querySelectorAll('button, [role=\"button\"]')).find(b => ['닫기', 'Close', '취소', 'Cancel', '확인', 'OK'].some(t => b.innerText?.trim() === t || b.innerText?.trim().includes(t))); if (textBtn) { textBtn.click(); await new Promise(r => setTimeout(r, 500)); return { status: 'closed_by_text_btn', closed: true }; } document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', keyCode: 27, bubbles: true })); await new Promise(r => setTimeout(r, 500)); const stillOpen = document.querySelector(\"[role='dialog'], [aria-modal='true'], [class*='modal']:not([class*='tooltip'])\"); return { status: stillOpen?.offsetParent ? 'failed_to_close' : 'closed_by_esc', closed: !stillOpen?.offsetParent }; })()"
|
||||
},
|
||||
|
||||
"actionHooks": {
|
||||
"description": "액션 전후에 실행할 모달 처리 훅",
|
||||
|
||||
"beforeAction": {
|
||||
"description": "모든 액션 실행 전 모달 상태 확인",
|
||||
"checkModalOpen": true,
|
||||
"ifModalOpen": {
|
||||
"action": "focus_modal",
|
||||
"description": "모달이 열려있으면 모달에 포커스"
|
||||
}
|
||||
},
|
||||
|
||||
"afterAction": {
|
||||
"description": "모달 열기 액션 후 처리",
|
||||
"triggers": ["click_register", "click_add", "click_detail", "click_edit", "click_view"],
|
||||
"waitForModal": 1000,
|
||||
"actions": [
|
||||
"wait_for_modal_animation",
|
||||
"focus_modal_content",
|
||||
"verify_modal_visible"
|
||||
]
|
||||
},
|
||||
|
||||
"beforeNavigation": {
|
||||
"description": "페이지 이동 또는 다른 요소 클릭 전 모달 닫기",
|
||||
"forceCloseModal": true,
|
||||
"maxCloseAttempts": 3,
|
||||
"waitAfterClose": 500
|
||||
},
|
||||
|
||||
"onStepComplete": {
|
||||
"description": "각 스텝 완료 시 모달 상태 정리",
|
||||
"checkAndCloseModal": true,
|
||||
"reportIfNotClosed": true
|
||||
}
|
||||
},
|
||||
|
||||
"specialCases": {
|
||||
"printDialog": {
|
||||
"description": "인쇄 다이얼로그 처리",
|
||||
"detection": "window.print() 호출 또는 인쇄 버튼 클릭",
|
||||
"handling": "ESC 키로 닫기, 1000ms 대기",
|
||||
"script": "(async function() { document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', keyCode: 27, bubbles: true })); await new Promise(r => setTimeout(r, 1000)); return 'print_dialog_handled'; })()"
|
||||
},
|
||||
|
||||
"pdfPreview": {
|
||||
"description": "PDF 미리보기 처리",
|
||||
"detection": "iframe[src*='pdf'], embed[type*='pdf'], [class*='pdf-preview']",
|
||||
"handling": "부모 모달의 닫기 버튼 클릭",
|
||||
"script": "(function() { const pdf = document.querySelector(\"iframe[src*='pdf'], embed[type*='pdf'], [class*='pdf-preview']\"); if (pdf) { const parentModal = pdf.closest(\"[role='dialog'], [class*='modal']\"); const closeBtn = parentModal?.querySelector(\"[class*='close']\"); if (closeBtn) { closeBtn.click(); return 'pdf_closed'; } } return 'no_pdf'; })()"
|
||||
},
|
||||
|
||||
"confirmDialog": {
|
||||
"description": "확인 다이얼로그 (alert, confirm)",
|
||||
"detection": "[role='alertdialog'], [class*='confirm'], [class*='alert']",
|
||||
"handling": "확인 또는 취소 버튼 클릭"
|
||||
},
|
||||
|
||||
"toastNotification": {
|
||||
"description": "토스트 알림 (닫기 불필요)",
|
||||
"detection": "[class*='toast'], [class*='Toast'], [class*='notification']",
|
||||
"handling": "자동 닫힘 대기 또는 무시"
|
||||
}
|
||||
},
|
||||
|
||||
"enforceRules": {
|
||||
"description": "강제 적용 규칙",
|
||||
|
||||
"rule1_modal_focus": {
|
||||
"name": "모달 포커스 규칙",
|
||||
"description": "모달이 열린 상태에서는 모달 내부 요소만 조작",
|
||||
"enforcement": "모달 외부 클릭 시 자동으로 모달 닫기 먼저 실행"
|
||||
},
|
||||
|
||||
"rule2_close_before_navigate": {
|
||||
"name": "이동 전 닫기 규칙",
|
||||
"description": "페이지 이동 또는 메뉴 클릭 전 모든 모달 닫기",
|
||||
"enforcement": "navigate, menuClick 액션 전 forceCloseAllModals 실행"
|
||||
},
|
||||
|
||||
"rule3_step_cleanup": {
|
||||
"name": "스텝 정리 규칙",
|
||||
"description": "각 스텝 완료 후 모달 상태 확인 및 정리",
|
||||
"enforcement": "스텝 종료 시 checkAndCloseModal 자동 실행"
|
||||
},
|
||||
|
||||
"rule4_report_unclosed": {
|
||||
"name": "미닫힘 보고 규칙",
|
||||
"description": "모달을 닫지 못한 경우 리포트에 기록",
|
||||
"enforcement": "BLOCKED: modal_not_closed 상태로 기록"
|
||||
}
|
||||
},
|
||||
|
||||
"scripts": {
|
||||
"checkModalState": "(function() { const selectors = [\"[role='dialog']\", \"[role='alertdialog']\", \"[aria-modal='true']\", \"[class*='modal']:not([class*='tooltip'])\", \"[class*='Modal']\", \"[class*='Dialog']\"]; for (const sel of selectors) { const el = document.querySelector(sel); if (el && el.offsetParent !== null) { return { isOpen: true, selector: sel, tagName: el.tagName, className: el.className }; } } return { isOpen: false }; })()",
|
||||
|
||||
"forceCloseAllModals": "(async function() { const results = []; let attempts = 0; while (attempts < 3) { const modal = document.querySelector(\"[role='dialog'], [aria-modal='true'], [class*='modal']:not([class*='tooltip']), [class*='Modal']\"); if (!modal || modal.offsetParent === null) break; const closeBtn = modal.querySelector(\"button[class*='close'], [aria-label='닫기'], [aria-label='Close']\") || Array.from(modal.querySelectorAll('button')).find(b => ['닫기', 'Close', '취소', 'Cancel', '확인'].some(t => b.innerText?.includes(t))); if (closeBtn) { closeBtn.click(); results.push({ attempt: attempts + 1, method: 'button' }); } else { document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', keyCode: 27, bubbles: true })); results.push({ attempt: attempts + 1, method: 'escape' }); } await new Promise(r => setTimeout(r, 500)); attempts++; } const stillOpen = document.querySelector(\"[role='dialog'], [aria-modal='true'], [class*='modal']:not([class*='tooltip'])\"); return { closed: !stillOpen?.offsetParent, attempts, results }; })()",
|
||||
|
||||
"waitForModalAndFocus": "(async function(timeout = 2000) { const start = Date.now(); while (Date.now() - start < timeout) { const modal = document.querySelector(\"[role='dialog'], [aria-modal='true'], [class*='modal']:not([class*='tooltip'])\"); if (modal && modal.offsetParent !== null) { modal.focus(); const firstInput = modal.querySelector('input, textarea, select, button'); if (firstInput) firstInput.focus(); return { found: true, focused: true, waitTime: Date.now() - start }; } await new Promise(r => setTimeout(r, 100)); } return { found: false, focused: false, waitTime: timeout }; })()",
|
||||
|
||||
"interactWithinModal": "(function(selector, action = 'click') { const modal = document.querySelector(\"[role='dialog'], [aria-modal='true'], [class*='modal']:not([class*='tooltip'])\"); if (!modal || modal.offsetParent === null) return { error: 'no_modal_open' }; const element = modal.querySelector(selector); if (!element) return { error: 'element_not_found_in_modal', selector }; if (action === 'click') element.click(); return { success: true, action, selector }; })"
|
||||
},
|
||||
|
||||
"testExecutionFlow": {
|
||||
"description": "테스트 실행 흐름에서 모달 처리 통합",
|
||||
|
||||
"stepExecution": [
|
||||
"1. 스텝 시작 전: checkModalState 실행",
|
||||
"2. 모달 열림 상태면: 모달 내부에서만 액션 실행",
|
||||
"3. 모달 열기 액션 후: waitForModalAndFocus 실행",
|
||||
"4. 모달 내 작업 완료 후: 다음 스텝 전 모달 닫기 여부 확인",
|
||||
"5. 페이지 이동 전: forceCloseAllModals 실행",
|
||||
"6. 스텝 완료 시: 모달 상태 확인 및 리포트"
|
||||
],
|
||||
|
||||
"errorHandling": {
|
||||
"modalNotClosed": {
|
||||
"action": "retry_close",
|
||||
"maxRetries": 3,
|
||||
"onFailure": "report_as_blocked"
|
||||
},
|
||||
"elementNotInModal": {
|
||||
"action": "check_if_modal_closed",
|
||||
"ifClosed": "proceed_normally",
|
||||
"ifOpen": "report_error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,317 +0,0 @@
|
||||
{
|
||||
"$schema": "E2E Parallel Execution Configuration",
|
||||
"version": "1.0.0",
|
||||
"description": "테스트 시나리오 병렬 실행 및 최적화 설정",
|
||||
"lastUpdated": "2026-01-31",
|
||||
|
||||
"parallel": {
|
||||
"enabled": true,
|
||||
"maxWorkers": 4,
|
||||
"defaultConcurrency": 2,
|
||||
"isolatedBrowsers": true,
|
||||
"sharedLogin": false
|
||||
},
|
||||
|
||||
"workerConfig": {
|
||||
"maxWorkers": {
|
||||
"description": "최대 동시 실행 워커 수",
|
||||
"default": 4,
|
||||
"min": 1,
|
||||
"max": 8,
|
||||
"autoDetect": true,
|
||||
"cpuMultiplier": 0.5
|
||||
},
|
||||
"workerTimeout": {
|
||||
"description": "워커별 최대 실행 시간",
|
||||
"default": 300000,
|
||||
"perScenario": 60000
|
||||
},
|
||||
"workerRetry": {
|
||||
"enabled": true,
|
||||
"maxRetries": 1
|
||||
}
|
||||
},
|
||||
|
||||
"executionModes": {
|
||||
"sequential": {
|
||||
"description": "순차 실행 (기본)",
|
||||
"concurrency": 1,
|
||||
"useCase": "디버깅, 의존성 있는 테스트"
|
||||
},
|
||||
"parallel": {
|
||||
"description": "병렬 실행",
|
||||
"concurrency": 4,
|
||||
"useCase": "독립적인 테스트, 빠른 실행"
|
||||
},
|
||||
"grouped": {
|
||||
"description": "그룹별 병렬 실행",
|
||||
"groupConcurrency": 2,
|
||||
"withinGroupSequential": true,
|
||||
"useCase": "관련 테스트 그룹화"
|
||||
},
|
||||
"prioritized": {
|
||||
"description": "우선순위 기반 실행",
|
||||
"highPriorityFirst": true,
|
||||
"concurrency": 3,
|
||||
"useCase": "중요 테스트 먼저"
|
||||
}
|
||||
},
|
||||
|
||||
"grouping": {
|
||||
"byModule": {
|
||||
"description": "모듈별 그룹화",
|
||||
"groups": {
|
||||
"auth": ["login", "logout", "session"],
|
||||
"hr": ["attendance-*", "vacation-*", "employee-*"],
|
||||
"accounting": ["vendor-*", "deposit-*", "card-*"],
|
||||
"approval": ["approval-*", "draft-*", "reference-*"],
|
||||
"boards": ["free-board", "notice-board", "popup-*"]
|
||||
}
|
||||
},
|
||||
"byPriority": {
|
||||
"description": "우선순위별 그룹화",
|
||||
"groups": {
|
||||
"critical": ["login", "dashboard", "approval-box"],
|
||||
"high": ["vendor-management", "attendance-*"],
|
||||
"medium": ["*-board", "popup-*"],
|
||||
"low": ["settings-*", "help-*"]
|
||||
}
|
||||
},
|
||||
"byDuration": {
|
||||
"description": "실행 시간별 그룹화",
|
||||
"groups": {
|
||||
"fast": { "maxDuration": 30000, "concurrency": 4 },
|
||||
"medium": { "maxDuration": 60000, "concurrency": 3 },
|
||||
"slow": { "maxDuration": 120000, "concurrency": 2 }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"dependencies": {
|
||||
"description": "테스트 간 의존성 관리",
|
||||
"rules": [
|
||||
{
|
||||
"scenario": "approval-box",
|
||||
"dependsOn": ["login"],
|
||||
"reason": "로그인 필요"
|
||||
},
|
||||
{
|
||||
"scenario": "crud-*-delete",
|
||||
"dependsOn": ["crud-*-create"],
|
||||
"reason": "삭제 전 생성 필요"
|
||||
}
|
||||
],
|
||||
"resolutionStrategy": "topological",
|
||||
"blockOnFailure": true
|
||||
},
|
||||
|
||||
"isolation": {
|
||||
"browserContext": {
|
||||
"description": "브라우저 컨텍스트 격리",
|
||||
"newContextPerWorker": true,
|
||||
"newContextPerScenario": false,
|
||||
"sharedCookies": false,
|
||||
"sharedStorage": false
|
||||
},
|
||||
"testData": {
|
||||
"description": "테스트 데이터 격리",
|
||||
"prefixWithWorkerId": true,
|
||||
"format": "E2E_TEST_W{workerId}_{entity}_{timestamp}"
|
||||
},
|
||||
"ports": {
|
||||
"description": "포트 격리 (로컬 서버)",
|
||||
"basePort": 3000,
|
||||
"portPerWorker": true
|
||||
}
|
||||
},
|
||||
|
||||
"loadBalancing": {
|
||||
"strategy": "roundRobin",
|
||||
"strategies": {
|
||||
"roundRobin": {
|
||||
"description": "순환 분배",
|
||||
"algorithm": "각 워커에 순서대로 할당"
|
||||
},
|
||||
"leastBusy": {
|
||||
"description": "최소 부하 분배",
|
||||
"algorithm": "가장 적은 작업 가진 워커에 할당"
|
||||
},
|
||||
"durationBased": {
|
||||
"description": "실행 시간 기반",
|
||||
"algorithm": "예상 실행 시간으로 균등 분배"
|
||||
},
|
||||
"adaptive": {
|
||||
"description": "적응형",
|
||||
"algorithm": "실시간 성능 기반 동적 조절"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"resourceManagement": {
|
||||
"memory": {
|
||||
"maxPerWorker": "512MB",
|
||||
"warningThreshold": "400MB",
|
||||
"killOnExceed": true
|
||||
},
|
||||
"cpu": {
|
||||
"maxPerWorker": 25,
|
||||
"throttleOnHigh": true
|
||||
},
|
||||
"network": {
|
||||
"rateLimit": false,
|
||||
"maxConcurrentRequests": 10
|
||||
}
|
||||
},
|
||||
|
||||
"scheduling": {
|
||||
"order": "dependency-aware",
|
||||
"orders": {
|
||||
"fifo": "선입선출",
|
||||
"priority": "우선순위순",
|
||||
"duration": "짧은 것 먼저",
|
||||
"dependency-aware": "의존성 고려"
|
||||
},
|
||||
"batchSize": 10,
|
||||
"batchDelay": 1000
|
||||
},
|
||||
|
||||
"failureHandling": {
|
||||
"onWorkerFailure": {
|
||||
"action": "reassign",
|
||||
"maxReassigns": 2,
|
||||
"reassignDelay": 5000
|
||||
},
|
||||
"onScenarioFailure": {
|
||||
"continueOthers": true,
|
||||
"isolateFailedWorker": false
|
||||
},
|
||||
"failFast": {
|
||||
"enabled": false,
|
||||
"threshold": 50,
|
||||
"description": "50% 이상 실패 시 중단"
|
||||
}
|
||||
},
|
||||
|
||||
"scripts": {
|
||||
"createWorkerPool": "(function(maxWorkers) { const pool = { workers: [], available: [], busy: [] }; for (let i = 0; i < maxWorkers; i++) { pool.workers.push({ id: i, status: 'idle', currentTask: null, completedTasks: 0 }); pool.available.push(i); } return pool; })",
|
||||
|
||||
"assignTask": "(function(pool, task) { if (pool.available.length === 0) return null; const workerId = pool.available.shift(); const worker = pool.workers[workerId]; worker.status = 'busy'; worker.currentTask = task; pool.busy.push(workerId); return workerId; })",
|
||||
|
||||
"releaseWorker": "(function(pool, workerId) { const worker = pool.workers[workerId]; worker.status = 'idle'; worker.currentTask = null; worker.completedTasks++; pool.busy = pool.busy.filter(id => id !== workerId); pool.available.push(workerId); })",
|
||||
|
||||
"getPoolStatus": "(function(pool) { return { total: pool.workers.length, available: pool.available.length, busy: pool.busy.length, tasks: pool.workers.map(w => ({ id: w.id, status: w.status, completed: w.completedTasks })) }; })",
|
||||
|
||||
"partitionScenarios": "(function(scenarios, workerCount) { const partitions = Array.from({ length: workerCount }, () => []); scenarios.forEach((scenario, index) => { partitions[index % workerCount].push(scenario); }); return partitions; })",
|
||||
|
||||
"sortByDependency": "(function(scenarios, dependencies) { const graph = new Map(); const inDegree = new Map(); scenarios.forEach(s => { graph.set(s, []); inDegree.set(s, 0); }); dependencies.forEach(dep => { if (graph.has(dep.from) && graph.has(dep.to)) { graph.get(dep.from).push(dep.to); inDegree.set(dep.to, inDegree.get(dep.to) + 1); } }); const queue = scenarios.filter(s => inDegree.get(s) === 0); const result = []; while (queue.length > 0) { const current = queue.shift(); result.push(current); graph.get(current).forEach(next => { inDegree.set(next, inDegree.get(next) - 1); if (inDegree.get(next) === 0) queue.push(next); }); } return result; })"
|
||||
},
|
||||
|
||||
"reporting": {
|
||||
"aggregateResults": true,
|
||||
"perWorkerReport": true,
|
||||
"timeline": true,
|
||||
"format": {
|
||||
"summary": {
|
||||
"totalScenarios": "총 시나리오",
|
||||
"totalWorkers": "사용 워커",
|
||||
"totalDuration": "총 실행 시간",
|
||||
"parallelEfficiency": "병렬 효율"
|
||||
},
|
||||
"perWorker": {
|
||||
"workerId": "워커 ID",
|
||||
"scenarios": "실행 시나리오",
|
||||
"passed": "성공",
|
||||
"failed": "실패",
|
||||
"duration": "소요 시간"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"header": "## ⚡ 병렬 실행 결과",
|
||||
"sections": {
|
||||
"summary": "### 실행 요약",
|
||||
"timeline": "### 타임라인",
|
||||
"workers": "### 워커별 결과"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"optimization": {
|
||||
"autoTuning": {
|
||||
"enabled": true,
|
||||
"adjustWorkersOnLoad": true,
|
||||
"minWorkers": 1,
|
||||
"maxWorkers": 8
|
||||
},
|
||||
"caching": {
|
||||
"shareAuthTokens": false,
|
||||
"shareStaticAssets": true,
|
||||
"cacheTestData": false
|
||||
},
|
||||
"warmup": {
|
||||
"enabled": true,
|
||||
"preloadBrowsers": true,
|
||||
"preloadPages": ["login"]
|
||||
}
|
||||
},
|
||||
|
||||
"monitoring": {
|
||||
"realtime": true,
|
||||
"metrics": [
|
||||
"activeWorkers",
|
||||
"queuedTasks",
|
||||
"completedTasks",
|
||||
"failedTasks",
|
||||
"averageTaskDuration",
|
||||
"cpuUsage",
|
||||
"memoryUsage"
|
||||
],
|
||||
"alertThresholds": {
|
||||
"queueBacklog": 20,
|
||||
"workerIdleTime": 30000,
|
||||
"memoryUsage": 80
|
||||
}
|
||||
},
|
||||
|
||||
"integration": {
|
||||
"withRetry": {
|
||||
"retryFailedOnDifferentWorker": true,
|
||||
"maxWorkerRetries": 1
|
||||
},
|
||||
"withReporting": {
|
||||
"consolidateReports": true,
|
||||
"generateTimeline": true
|
||||
},
|
||||
"withCI": {
|
||||
"supportSharding": true,
|
||||
"shardIndex": "auto",
|
||||
"totalShards": "auto"
|
||||
}
|
||||
},
|
||||
|
||||
"presets": {
|
||||
"quick": {
|
||||
"description": "빠른 실행 (4 워커)",
|
||||
"maxWorkers": 4,
|
||||
"timeout": 180000,
|
||||
"failFast": true
|
||||
},
|
||||
"thorough": {
|
||||
"description": "철저한 실행 (2 워커)",
|
||||
"maxWorkers": 2,
|
||||
"timeout": 600000,
|
||||
"failFast": false
|
||||
},
|
||||
"ci": {
|
||||
"description": "CI 환경 (CPU 기반)",
|
||||
"maxWorkers": "auto",
|
||||
"timeout": 300000,
|
||||
"headless": true
|
||||
},
|
||||
"debug": {
|
||||
"description": "디버그 (1 워커)",
|
||||
"maxWorkers": 1,
|
||||
"timeout": 600000,
|
||||
"verbose": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
{
|
||||
"$schema": "E2E Performance Metrics Configuration",
|
||||
"version": "1.0.0",
|
||||
"description": "성능 메트릭 수집 및 분석을 위한 전역 설정",
|
||||
"lastUpdated": "2026-01-31",
|
||||
|
||||
"metrics": {
|
||||
"pageLoad": {
|
||||
"enabled": true,
|
||||
"description": "페이지 로드 성능 측정",
|
||||
"thresholds": {
|
||||
"good": 1000,
|
||||
"acceptable": 2000,
|
||||
"slow": 3000,
|
||||
"critical": 5000
|
||||
},
|
||||
"measurements": [
|
||||
"domContentLoaded",
|
||||
"load",
|
||||
"firstContentfulPaint",
|
||||
"largestContentfulPaint",
|
||||
"timeToInteractive"
|
||||
]
|
||||
},
|
||||
|
||||
"apiResponse": {
|
||||
"enabled": true,
|
||||
"description": "API 응답 시간 측정",
|
||||
"thresholds": {
|
||||
"fast": 200,
|
||||
"good": 500,
|
||||
"acceptable": 1000,
|
||||
"slow": 2000,
|
||||
"critical": 5000
|
||||
}
|
||||
},
|
||||
|
||||
"resourceUsage": {
|
||||
"enabled": true,
|
||||
"description": "리소스 사용량 모니터링",
|
||||
"track": [
|
||||
"jsHeapSize",
|
||||
"domNodes",
|
||||
"networkRequests",
|
||||
"transferSize"
|
||||
]
|
||||
},
|
||||
|
||||
"userInteraction": {
|
||||
"enabled": true,
|
||||
"description": "사용자 인터랙션 응답 시간",
|
||||
"thresholds": {
|
||||
"instant": 100,
|
||||
"fast": 300,
|
||||
"acceptable": 1000,
|
||||
"slow": 3000
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"scripts": {
|
||||
"initPerformanceMonitoring": "(function() { window.__PERF_METRICS__ = { startTime: Date.now(), pageLoads: [], apiCalls: [], interactions: [], resources: [], errors: [] }; window.__PERF_OBSERVER__ = new PerformanceObserver((list) => { list.getEntries().forEach(entry => { if (entry.entryType === 'navigation') { window.__PERF_METRICS__.pageLoads.push({ url: entry.name, domContentLoaded: entry.domContentLoadedEventEnd - entry.startTime, load: entry.loadEventEnd - entry.startTime, dns: entry.domainLookupEnd - entry.domainLookupStart, tcp: entry.connectEnd - entry.connectStart, ttfb: entry.responseStart - entry.requestStart, timestamp: new Date().toISOString() }); } else if (entry.entryType === 'paint') { window.__PERF_METRICS__[entry.name] = entry.startTime; } else if (entry.entryType === 'largest-contentful-paint') { window.__PERF_METRICS__.lcp = entry.startTime; } }); }); try { window.__PERF_OBSERVER__.observe({ entryTypes: ['navigation', 'paint', 'largest-contentful-paint'] }); } catch(e) { console.warn('Performance observer not fully supported'); } return 'Performance monitoring initialized'; })()",
|
||||
|
||||
"getNavigationTiming": "(function() { const perf = performance.getEntriesByType('navigation')[0]; if (!perf) return null; return { url: window.location.href, domContentLoaded: Math.round(perf.domContentLoadedEventEnd - perf.startTime), load: Math.round(perf.loadEventEnd - perf.startTime), domInteractive: Math.round(perf.domInteractive - perf.startTime), dns: Math.round(perf.domainLookupEnd - perf.domainLookupStart), tcp: Math.round(perf.connectEnd - perf.connectStart), ttfb: Math.round(perf.responseStart - perf.requestStart), responseTime: Math.round(perf.responseEnd - perf.responseStart), domProcessing: Math.round(perf.domComplete - perf.domInteractive), transferSize: perf.transferSize, encodedBodySize: perf.encodedBodySize }; })()",
|
||||
|
||||
"getPaintTiming": "(function() { const paints = performance.getEntriesByType('paint'); const result = {}; paints.forEach(p => { result[p.name] = Math.round(p.startTime); }); return result; })()",
|
||||
|
||||
"getLCP": "(function() { return new Promise((resolve) => { let lcp = 0; const observer = new PerformanceObserver((list) => { const entries = list.getEntries(); entries.forEach(entry => { lcp = entry.startTime; }); }); try { observer.observe({ type: 'largest-contentful-paint', buffered: true }); setTimeout(() => { observer.disconnect(); resolve(Math.round(lcp)); }, 1000); } catch(e) { resolve(null); } }); })()",
|
||||
|
||||
"getResourceMetrics": "(function() { const resources = performance.getEntriesByType('resource'); const summary = { total: resources.length, totalSize: 0, byType: {}, slowest: [] }; resources.forEach(r => { const type = r.initiatorType || 'other'; summary.byType[type] = summary.byType[type] || { count: 0, size: 0, totalTime: 0 }; summary.byType[type].count++; summary.byType[type].size += r.transferSize || 0; summary.byType[type].totalTime += r.duration || 0; summary.totalSize += r.transferSize || 0; }); summary.slowest = resources.sort((a, b) => b.duration - a.duration).slice(0, 5).map(r => ({ name: r.name.split('/').pop().substring(0, 30), duration: Math.round(r.duration), size: r.transferSize })); summary.totalSizeKB = Math.round(summary.totalSize / 1024); return summary; })()",
|
||||
|
||||
"getMemoryUsage": "(function() { if (!performance.memory) return { supported: false }; return { supported: true, usedJSHeapSize: Math.round(performance.memory.usedJSHeapSize / 1024 / 1024), totalJSHeapSize: Math.round(performance.memory.totalJSHeapSize / 1024 / 1024), jsHeapSizeLimit: Math.round(performance.memory.jsHeapSizeLimit / 1024 / 1024), usagePercent: Math.round((performance.memory.usedJSHeapSize / performance.memory.jsHeapSizeLimit) * 100) }; })()",
|
||||
|
||||
"getDOMMetrics": "(function() { return { nodeCount: document.getElementsByTagName('*').length, bodySize: document.body.innerHTML.length, maxDepth: (function getDepth(el) { if (!el.children.length) return 0; return 1 + Math.max(...Array.from(el.children).map(getDepth)); })(document.body), forms: document.forms.length, inputs: document.querySelectorAll('input, textarea, select').length, images: document.images.length, scripts: document.scripts.length, stylesheets: document.styleSheets.length }; })()",
|
||||
|
||||
"measureInteraction": "(async function(action, selector) { const start = performance.now(); try { const el = document.querySelector(selector); if (el) { el.click(); await new Promise(r => setTimeout(r, 100)); } const duration = performance.now() - start; return { action, selector, duration: Math.round(duration), timestamp: new Date().toISOString() }; } catch(e) { return { action, selector, error: e.message }; } })",
|
||||
|
||||
"getFullPerformanceReport": "(function() { const nav = performance.getEntriesByType('navigation')[0]; const paints = {}; performance.getEntriesByType('paint').forEach(p => paints[p.name] = Math.round(p.startTime)); const resources = performance.getEntriesByType('resource'); const apiLogs = window.__API_LOGS__ || []; return { timestamp: new Date().toISOString(), url: window.location.href, navigation: nav ? { domContentLoaded: Math.round(nav.domContentLoadedEventEnd - nav.startTime), load: Math.round(nav.loadEventEnd - nav.startTime), ttfb: Math.round(nav.responseStart - nav.requestStart), domInteractive: Math.round(nav.domInteractive - nav.startTime) } : null, paint: paints, resources: { count: resources.length, totalSizeKB: Math.round(resources.reduce((sum, r) => sum + (r.transferSize || 0), 0) / 1024) }, api: { totalCalls: apiLogs.length, avgResponseTime: apiLogs.length > 0 ? Math.round(apiLogs.reduce((sum, l) => sum + (l.duration || 0), 0) / apiLogs.length) : 0, slowCalls: apiLogs.filter(l => l.duration > 2000).length }, memory: performance.memory ? { usedMB: Math.round(performance.memory.usedJSHeapSize / 1024 / 1024), usagePercent: Math.round((performance.memory.usedJSHeapSize / performance.memory.jsHeapSizeLimit) * 100) } : null, dom: { nodeCount: document.getElementsByTagName('*').length } }; })()",
|
||||
|
||||
"getPerformanceSummary": "(function() { const nav = performance.getEntriesByType('navigation')[0]; const apiLogs = window.__API_LOGS__ || []; const thresholds = { pageLoad: { good: 1000, acceptable: 2000, slow: 3000 }, api: { good: 500, acceptable: 1000, slow: 2000 } }; const pageLoadTime = nav ? Math.round(nav.loadEventEnd - nav.startTime) : 0; const avgApiTime = apiLogs.length > 0 ? Math.round(apiLogs.reduce((sum, l) => sum + (l.duration || 0), 0) / apiLogs.length) : 0; const getGrade = (value, th) => { if (value <= th.good) return '🟢 Good'; if (value <= th.acceptable) return '🟡 Acceptable'; if (value <= th.slow) return '🟠 Slow'; return '🔴 Critical'; }; return { pageLoad: { time: pageLoadTime, grade: getGrade(pageLoadTime, thresholds.pageLoad) }, api: { avgTime: avgApiTime, grade: getGrade(avgApiTime, thresholds.api), totalCalls: apiLogs.length, slowCalls: apiLogs.filter(l => l.duration > 2000).length }, overall: pageLoadTime <= 2000 && avgApiTime <= 1000 ? '✅ PASS' : '⚠️ NEEDS ATTENTION' }; })()"
|
||||
},
|
||||
|
||||
"thresholds": {
|
||||
"pageLoad": {
|
||||
"domContentLoaded": {
|
||||
"good": 800,
|
||||
"acceptable": 1500,
|
||||
"slow": 2500,
|
||||
"critical": 4000
|
||||
},
|
||||
"load": {
|
||||
"good": 1000,
|
||||
"acceptable": 2000,
|
||||
"slow": 3000,
|
||||
"critical": 5000
|
||||
},
|
||||
"ttfb": {
|
||||
"good": 200,
|
||||
"acceptable": 500,
|
||||
"slow": 1000,
|
||||
"critical": 2000
|
||||
},
|
||||
"fcp": {
|
||||
"good": 1000,
|
||||
"acceptable": 2000,
|
||||
"slow": 3000,
|
||||
"critical": 4000
|
||||
},
|
||||
"lcp": {
|
||||
"good": 2500,
|
||||
"acceptable": 4000,
|
||||
"slow": 6000,
|
||||
"critical": 8000
|
||||
}
|
||||
},
|
||||
"api": {
|
||||
"responseTime": {
|
||||
"fast": 200,
|
||||
"good": 500,
|
||||
"acceptable": 1000,
|
||||
"slow": 2000,
|
||||
"critical": 5000
|
||||
}
|
||||
},
|
||||
"memory": {
|
||||
"usagePercent": {
|
||||
"good": 50,
|
||||
"acceptable": 70,
|
||||
"warning": 85,
|
||||
"critical": 95
|
||||
}
|
||||
},
|
||||
"dom": {
|
||||
"nodeCount": {
|
||||
"good": 1000,
|
||||
"acceptable": 2000,
|
||||
"warning": 3000,
|
||||
"critical": 5000
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"grading": {
|
||||
"levels": {
|
||||
"excellent": { "symbol": "🟢", "label": "Excellent", "range": "< good" },
|
||||
"good": { "symbol": "🟢", "label": "Good", "range": "good ~ acceptable" },
|
||||
"acceptable": { "symbol": "🟡", "label": "Acceptable", "range": "acceptable ~ slow" },
|
||||
"slow": { "symbol": "🟠", "label": "Slow", "range": "slow ~ critical" },
|
||||
"critical": { "symbol": "🔴", "label": "Critical", "range": "> critical" }
|
||||
},
|
||||
"overall": {
|
||||
"pass": "모든 핵심 지표가 acceptable 이상",
|
||||
"warning": "일부 지표가 slow",
|
||||
"fail": "하나 이상의 지표가 critical"
|
||||
}
|
||||
},
|
||||
|
||||
"reporting": {
|
||||
"includeInTestReport": true,
|
||||
"separatePerformanceReport": true,
|
||||
"format": {
|
||||
"summary": {
|
||||
"sections": [
|
||||
"pageLoadMetrics",
|
||||
"apiMetrics",
|
||||
"resourceMetrics",
|
||||
"memoryMetrics"
|
||||
]
|
||||
},
|
||||
"detail": {
|
||||
"includeRawData": false,
|
||||
"includeCharts": false,
|
||||
"includeRecommendations": true
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"header": "## ⚡ 성능 메트릭",
|
||||
"sections": {
|
||||
"pageLoad": "### 페이지 로드 성능",
|
||||
"api": "### API 응답 성능",
|
||||
"resources": "### 리소스 사용량",
|
||||
"memory": "### 메모리 사용량",
|
||||
"recommendations": "### 개선 권장사항"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"recommendations": {
|
||||
"slowPageLoad": [
|
||||
"이미지 최적화 (WebP 형식, lazy loading)",
|
||||
"JavaScript 번들 크기 최적화",
|
||||
"CSS 최적화 및 Critical CSS 적용",
|
||||
"서버 응답 시간 개선"
|
||||
],
|
||||
"slowApi": [
|
||||
"API 응답 캐싱 적용",
|
||||
"데이터베이스 쿼리 최적화",
|
||||
"불필요한 API 호출 제거",
|
||||
"페이지네이션 적용"
|
||||
],
|
||||
"highMemory": [
|
||||
"메모리 누수 확인",
|
||||
"불필요한 이벤트 리스너 제거",
|
||||
"대용량 데이터 가상화 적용",
|
||||
"컴포넌트 언마운트 시 정리"
|
||||
],
|
||||
"tooManyDomNodes": [
|
||||
"가상 스크롤 적용",
|
||||
"조건부 렌더링 최적화",
|
||||
"불필요한 DOM 요소 제거",
|
||||
"컴포넌트 분할"
|
||||
]
|
||||
},
|
||||
|
||||
"hooks": {
|
||||
"beforeNavigation": {
|
||||
"description": "페이지 이동 전 성능 데이터 수집 시작",
|
||||
"actions": ["initPerformanceMonitoring"]
|
||||
},
|
||||
"afterNavigation": {
|
||||
"description": "페이지 로드 후 성능 데이터 수집",
|
||||
"actions": ["getNavigationTiming", "getPaintTiming", "getResourceMetrics"],
|
||||
"delay": 2000
|
||||
},
|
||||
"afterTest": {
|
||||
"description": "테스트 종료 후 성능 리포트 생성",
|
||||
"actions": ["getFullPerformanceReport", "generatePerformanceSection"]
|
||||
}
|
||||
},
|
||||
|
||||
"baseline": {
|
||||
"description": "성능 기준선 (비교용)",
|
||||
"values": {
|
||||
"dashboard": {
|
||||
"pageLoad": 1500,
|
||||
"apiCalls": 5,
|
||||
"avgApiTime": 200
|
||||
},
|
||||
"listPage": {
|
||||
"pageLoad": 2000,
|
||||
"apiCalls": 3,
|
||||
"avgApiTime": 300
|
||||
},
|
||||
"formPage": {
|
||||
"pageLoad": 1200,
|
||||
"apiCalls": 2,
|
||||
"avgApiTime": 150
|
||||
}
|
||||
},
|
||||
"tolerance": 20
|
||||
}
|
||||
}
|
||||
@@ -1,286 +0,0 @@
|
||||
{
|
||||
"$schema": "E2E Retry Logic Configuration",
|
||||
"version": "1.0.0",
|
||||
"description": "테스트 실패 시 자동 재시도 및 복구 로직 설정",
|
||||
"lastUpdated": "2026-01-31",
|
||||
|
||||
"retry": {
|
||||
"enabled": true,
|
||||
"maxRetries": 3,
|
||||
"defaultDelay": 1000,
|
||||
"backoffMultiplier": 2,
|
||||
"maxDelay": 10000
|
||||
},
|
||||
|
||||
"retryStrategies": {
|
||||
"immediate": {
|
||||
"description": "즉시 재시도",
|
||||
"delay": 0,
|
||||
"maxRetries": 2,
|
||||
"useCase": "단순 타이밍 이슈"
|
||||
},
|
||||
"linear": {
|
||||
"description": "일정 간격 재시도",
|
||||
"delay": 1000,
|
||||
"maxRetries": 3,
|
||||
"useCase": "일반적인 실패"
|
||||
},
|
||||
"exponential": {
|
||||
"description": "지수 백오프 재시도",
|
||||
"initialDelay": 1000,
|
||||
"multiplier": 2,
|
||||
"maxDelay": 10000,
|
||||
"maxRetries": 5,
|
||||
"useCase": "서버 부하, 네트워크 이슈"
|
||||
},
|
||||
"conditional": {
|
||||
"description": "조건부 재시도",
|
||||
"conditions": ["errorType", "elementState", "networkStatus"],
|
||||
"maxRetries": 3,
|
||||
"useCase": "특정 조건에서만 재시도"
|
||||
}
|
||||
},
|
||||
|
||||
"retryableErrors": {
|
||||
"timeout": {
|
||||
"pattern": ["timeout", "Timeout", "TIMEOUT", "시간 초과"],
|
||||
"strategy": "exponential",
|
||||
"maxRetries": 3,
|
||||
"description": "타임아웃 오류"
|
||||
},
|
||||
"elementNotFound": {
|
||||
"pattern": ["not found", "cannot find", "no such element", "찾을 수 없"],
|
||||
"strategy": "linear",
|
||||
"maxRetries": 2,
|
||||
"preRetryAction": "waitForPageLoad",
|
||||
"description": "요소를 찾을 수 없음"
|
||||
},
|
||||
"elementNotVisible": {
|
||||
"pattern": ["not visible", "not displayed", "hidden", "표시되지 않"],
|
||||
"strategy": "linear",
|
||||
"maxRetries": 2,
|
||||
"preRetryAction": "scrollIntoView",
|
||||
"description": "요소가 보이지 않음"
|
||||
},
|
||||
"elementNotClickable": {
|
||||
"pattern": ["not clickable", "intercepted", "obscured", "클릭할 수 없"],
|
||||
"strategy": "linear",
|
||||
"maxRetries": 2,
|
||||
"preRetryAction": "closeOverlays",
|
||||
"description": "클릭 불가 상태"
|
||||
},
|
||||
"networkError": {
|
||||
"pattern": ["network", "Network", "ERR_", "net::"],
|
||||
"strategy": "exponential",
|
||||
"maxRetries": 5,
|
||||
"preRetryAction": "waitForNetwork",
|
||||
"description": "네트워크 오류"
|
||||
},
|
||||
"serverError": {
|
||||
"pattern": ["500", "502", "503", "504", "서버 오류"],
|
||||
"strategy": "exponential",
|
||||
"maxRetries": 3,
|
||||
"preRetryAction": "waitAndRefresh",
|
||||
"description": "서버 오류"
|
||||
},
|
||||
"staleElement": {
|
||||
"pattern": ["stale", "detached", "removed from DOM"],
|
||||
"strategy": "immediate",
|
||||
"maxRetries": 2,
|
||||
"preRetryAction": "refindElement",
|
||||
"description": "오래된 요소 참조"
|
||||
},
|
||||
"sessionExpired": {
|
||||
"pattern": ["session", "login", "unauthorized", "401", "세션 만료"],
|
||||
"strategy": "conditional",
|
||||
"maxRetries": 1,
|
||||
"preRetryAction": "reAuthenticate",
|
||||
"description": "세션 만료"
|
||||
}
|
||||
},
|
||||
|
||||
"nonRetryableErrors": {
|
||||
"description": "재시도하지 않는 오류 유형",
|
||||
"patterns": [
|
||||
"assertion failed",
|
||||
"test data not found",
|
||||
"validation error",
|
||||
"business logic error",
|
||||
"permission denied",
|
||||
"403"
|
||||
]
|
||||
},
|
||||
|
||||
"preRetryActions": {
|
||||
"waitForPageLoad": {
|
||||
"description": "페이지 로드 대기",
|
||||
"script": "(async () => { await new Promise(r => setTimeout(r, 2000)); const loading = document.querySelector('.loading, .spinner'); return !loading || loading.offsetParent === null; })()"
|
||||
},
|
||||
"scrollIntoView": {
|
||||
"description": "요소를 뷰포트로 스크롤",
|
||||
"script": "(selector) => { const el = document.querySelector(selector); if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'center' }); return true; } return false; }"
|
||||
},
|
||||
"closeOverlays": {
|
||||
"description": "오버레이/모달 닫기",
|
||||
"script": "(async () => { const overlays = document.querySelectorAll('[class*=\"overlay\"], [class*=\"modal\"]:not([style*=\"display: none\"]), [role=\"dialog\"]'); overlays.forEach(o => { const closeBtn = o.querySelector('[class*=\"close\"], [aria-label*=\"닫기\"]'); if (closeBtn) closeBtn.click(); }); await new Promise(r => setTimeout(r, 500)); return true; })()"
|
||||
},
|
||||
"waitForNetwork": {
|
||||
"description": "네트워크 안정화 대기",
|
||||
"script": "(async () => { await new Promise(r => setTimeout(r, 3000)); return navigator.onLine; })()"
|
||||
},
|
||||
"waitAndRefresh": {
|
||||
"description": "대기 후 페이지 새로고침",
|
||||
"script": "(async () => { await new Promise(r => setTimeout(r, 5000)); window.location.reload(); await new Promise(r => setTimeout(r, 3000)); return true; })()"
|
||||
},
|
||||
"refindElement": {
|
||||
"description": "요소 다시 찾기",
|
||||
"script": "(selector) => { return !!document.querySelector(selector); }"
|
||||
},
|
||||
"reAuthenticate": {
|
||||
"description": "재인증",
|
||||
"action": "navigateToLogin",
|
||||
"requiresCredentials": true
|
||||
},
|
||||
"clearCache": {
|
||||
"description": "브라우저 캐시 정리",
|
||||
"script": "(() => { if (window.caches) { caches.keys().then(names => names.forEach(name => caches.delete(name))); } sessionStorage.clear(); return true; })()"
|
||||
}
|
||||
},
|
||||
|
||||
"scripts": {
|
||||
"retryWithBackoff": "(async function(fn, options = {}) { const { maxRetries = 3, initialDelay = 1000, multiplier = 2, maxDelay = 10000 } = options; let lastError; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await fn(); } catch (error) { lastError = error; if (attempt < maxRetries) { const delay = Math.min(initialDelay * Math.pow(multiplier, attempt - 1), maxDelay); console.log(`Retry ${attempt}/${maxRetries} after ${delay}ms`); await new Promise(r => setTimeout(r, delay)); } } } throw lastError; })",
|
||||
|
||||
"retryUntilSuccess": "(async function(fn, options = {}) { const { maxRetries = 3, delay = 1000, condition = () => true } = options; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const result = await fn(); if (condition(result)) return { success: true, result, attempts: attempt }; } catch (error) { if (attempt === maxRetries) return { success: false, error: error.message, attempts: attempt }; } await new Promise(r => setTimeout(r, delay)); } return { success: false, error: 'Max retries exceeded', attempts: maxRetries }; })",
|
||||
|
||||
"waitForCondition": "(async function(conditionFn, options = {}) { const { timeout = 10000, interval = 100 } = options; const start = Date.now(); while (Date.now() - start < timeout) { try { if (await conditionFn()) return true; } catch (e) {} await new Promise(r => setTimeout(r, interval)); } return false; })",
|
||||
|
||||
"isRetryableError": "(function(error) { const retryablePatterns = ['timeout', 'not found', 'not visible', 'network', '500', '502', '503', 'stale']; const errorStr = String(error).toLowerCase(); return retryablePatterns.some(p => errorStr.includes(p.toLowerCase())); })",
|
||||
|
||||
"getRetryStrategy": "(function(error) { const errorStr = String(error).toLowerCase(); if (errorStr.includes('timeout') || errorStr.includes('network')) return 'exponential'; if (errorStr.includes('stale')) return 'immediate'; if (errorStr.includes('not found') || errorStr.includes('not visible')) return 'linear'; return 'linear'; })",
|
||||
|
||||
"executeWithRetry": "(async function(action, selector, options = {}) { const { maxRetries = 3, delay = 1000, preRetry = null } = options; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const element = document.querySelector(selector); if (!element) throw new Error('Element not found: ' + selector); if (action === 'click') element.click(); else if (action === 'focus') element.focus(); return { success: true, attempts: attempt }; } catch (error) { console.log(`Attempt ${attempt} failed: ${error.message}`); if (attempt < maxRetries) { if (preRetry) await preRetry(); await new Promise(r => setTimeout(r, delay)); } else { return { success: false, error: error.message, attempts: attempt }; } } } })"
|
||||
},
|
||||
|
||||
"stepRetry": {
|
||||
"enabled": true,
|
||||
"defaultMaxRetries": 2,
|
||||
"stepTypeConfig": {
|
||||
"click": {
|
||||
"maxRetries": 3,
|
||||
"delay": 500,
|
||||
"preRetryActions": ["closeOverlays", "scrollIntoView"]
|
||||
},
|
||||
"fill": {
|
||||
"maxRetries": 2,
|
||||
"delay": 300,
|
||||
"preRetryActions": ["waitForPageLoad"]
|
||||
},
|
||||
"navigate": {
|
||||
"maxRetries": 3,
|
||||
"delay": 2000,
|
||||
"preRetryActions": ["waitForNetwork"]
|
||||
},
|
||||
"waitFor": {
|
||||
"maxRetries": 5,
|
||||
"delay": 1000,
|
||||
"preRetryActions": ["waitForPageLoad"]
|
||||
},
|
||||
"assert": {
|
||||
"maxRetries": 2,
|
||||
"delay": 500,
|
||||
"preRetryActions": []
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"scenarioRetry": {
|
||||
"enabled": true,
|
||||
"maxRetries": 2,
|
||||
"retryOnFailure": true,
|
||||
"retryOnError": true,
|
||||
"freshStart": true,
|
||||
"resetState": {
|
||||
"clearCookies": false,
|
||||
"clearStorage": false,
|
||||
"reloadPage": true
|
||||
}
|
||||
},
|
||||
|
||||
"circuitBreaker": {
|
||||
"enabled": true,
|
||||
"description": "연속 실패 시 중단",
|
||||
"failureThreshold": 5,
|
||||
"resetTimeout": 30000,
|
||||
"states": {
|
||||
"closed": "정상 동작",
|
||||
"open": "요청 차단",
|
||||
"halfOpen": "테스트 요청 허용"
|
||||
}
|
||||
},
|
||||
|
||||
"reporting": {
|
||||
"logRetries": true,
|
||||
"includeInReport": true,
|
||||
"format": {
|
||||
"retryLog": {
|
||||
"timestamp": "시도 시간",
|
||||
"attempt": "시도 횟수",
|
||||
"error": "오류 내용",
|
||||
"action": "재시도 전 액션",
|
||||
"result": "결과"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"header": "## 🔄 재시도 로그",
|
||||
"sections": {
|
||||
"summary": "### 요약",
|
||||
"details": "### 상세 로그"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"hooks": {
|
||||
"beforeRetry": {
|
||||
"description": "재시도 전 실행",
|
||||
"actions": ["logAttempt", "executePreRetryAction"]
|
||||
},
|
||||
"afterRetry": {
|
||||
"description": "재시도 후 실행",
|
||||
"actions": ["logResult", "updateMetrics"]
|
||||
},
|
||||
"onMaxRetriesExceeded": {
|
||||
"description": "최대 재시도 초과 시",
|
||||
"actions": ["captureScreenshot", "logFinalError", "reportFailure"]
|
||||
}
|
||||
},
|
||||
|
||||
"metrics": {
|
||||
"track": true,
|
||||
"collect": [
|
||||
"totalRetries",
|
||||
"successAfterRetry",
|
||||
"failureAfterRetry",
|
||||
"averageRetriesPerStep",
|
||||
"mostRetriedSteps",
|
||||
"retrySuccessRate"
|
||||
]
|
||||
},
|
||||
|
||||
"integration": {
|
||||
"withAPI": {
|
||||
"retryOn5xx": true,
|
||||
"retryOnTimeout": true,
|
||||
"maxRetries": 3
|
||||
},
|
||||
"withVisual": {
|
||||
"retryOnCaptureFail": true,
|
||||
"maxRetries": 2
|
||||
},
|
||||
"withCRUD": {
|
||||
"retryCreate": true,
|
||||
"retryUpdate": true,
|
||||
"retryDelete": false,
|
||||
"maxRetries": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
{
|
||||
"$schema": "E2E Test Data Management Configuration",
|
||||
"version": "1.0.0",
|
||||
"description": "테스트 데이터 생성, 관리, 정리를 위한 전역 설정",
|
||||
"lastUpdated": "2026-01-31",
|
||||
|
||||
"namingConvention": {
|
||||
"prefix": "E2E_TEST_",
|
||||
"format": "{prefix}{entity}_{timestamp}",
|
||||
"timestampFormat": "YYYYMMDD_HHmmss",
|
||||
"examples": [
|
||||
"E2E_TEST_게시글_20260131_110000",
|
||||
"E2E_TEST_거래처_20260131_110000",
|
||||
"E2E_TEST_사원_20260131_110000"
|
||||
]
|
||||
},
|
||||
|
||||
"dataTemplates": {
|
||||
"freeBoard": {
|
||||
"entity": "게시글",
|
||||
"fields": {
|
||||
"title": "E2E_TEST_게시글_{timestamp}",
|
||||
"content": "E2E 자동화 테스트용 게시글입니다.\n생성시간: {datetime}\n테스트 목적: CRUD 흐름 검증"
|
||||
},
|
||||
"updateFields": {
|
||||
"title": "E2E_TEST_게시글_수정됨_{timestamp}"
|
||||
},
|
||||
"identifierField": "title",
|
||||
"searchableBy": ["title"]
|
||||
},
|
||||
|
||||
"vendor": {
|
||||
"entity": "거래처",
|
||||
"fields": {
|
||||
"name": "E2E_TEST_거래처_{timestamp}",
|
||||
"businessNumber": "123-45-{random6}",
|
||||
"representative": "테스트담당자",
|
||||
"phone": "02-1234-5678",
|
||||
"address": "서울시 테스트구 자동화로 123"
|
||||
},
|
||||
"updateFields": {
|
||||
"representative": "수정된담당자",
|
||||
"phone": "02-9999-8888"
|
||||
},
|
||||
"identifierField": "name",
|
||||
"searchableBy": ["name", "businessNumber"]
|
||||
},
|
||||
|
||||
"employee": {
|
||||
"entity": "사원",
|
||||
"fields": {
|
||||
"name": "E2E_TEST_사원_{timestamp}",
|
||||
"employeeNumber": "E2E{random4}",
|
||||
"department": "테스트부서",
|
||||
"position": "테스터",
|
||||
"email": "e2e_test_{timestamp}@test.com"
|
||||
},
|
||||
"updateFields": {
|
||||
"department": "수정부서",
|
||||
"position": "시니어테스터"
|
||||
},
|
||||
"identifierField": "name",
|
||||
"searchableBy": ["name", "employeeNumber"]
|
||||
},
|
||||
|
||||
"deposit": {
|
||||
"entity": "입금",
|
||||
"fields": {
|
||||
"description": "E2E_TEST_입금_{timestamp}",
|
||||
"amount": 100000,
|
||||
"depositor": "테스트입금자",
|
||||
"accountNumber": "110-123-456789"
|
||||
},
|
||||
"updateFields": {
|
||||
"amount": 200000,
|
||||
"description": "E2E_TEST_입금_수정됨_{timestamp}"
|
||||
},
|
||||
"identifierField": "description",
|
||||
"searchableBy": ["description", "depositor"]
|
||||
},
|
||||
|
||||
"card": {
|
||||
"entity": "카드",
|
||||
"fields": {
|
||||
"cardName": "E2E_TEST_카드_{timestamp}",
|
||||
"cardNumber": "1234-5678-{random4}-{random4}",
|
||||
"cardCompany": "테스트카드사",
|
||||
"holder": "테스트소지자"
|
||||
},
|
||||
"updateFields": {
|
||||
"holder": "수정된소지자"
|
||||
},
|
||||
"identifierField": "cardName",
|
||||
"searchableBy": ["cardName", "cardNumber"]
|
||||
}
|
||||
},
|
||||
|
||||
"scripts": {
|
||||
"generateTimestamp": "(() => { const now = new Date(); const pad = n => n.toString().padStart(2, '0'); return `${now.getFullYear()}${pad(now.getMonth()+1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`; })()",
|
||||
|
||||
"generateRandom": "(length) => Math.random().toString().substring(2, 2 + length)",
|
||||
|
||||
"generateTestData": "(template, timestamp) => { const data = {}; Object.entries(template.fields).forEach(([key, value]) => { if (typeof value === 'string') { data[key] = value.replace('{timestamp}', timestamp).replace('{datetime}', new Date().toLocaleString('ko-KR')).replace('{random4}', Math.random().toString().substring(2, 6)).replace('{random6}', Math.random().toString().substring(2, 8)); } else { data[key] = value; } }); return data; }",
|
||||
|
||||
"findTestData": "(prefix = 'E2E_TEST_') => { const rows = document.querySelectorAll('table tbody tr, [class*=\"table\"] [class*=\"row\"]'); return Array.from(rows).filter(row => row.innerText?.includes(prefix)).map(row => ({ text: row.innerText?.substring(0, 100), element: row })); }",
|
||||
|
||||
"cleanupTestData": "(async (prefix = 'E2E_TEST_') => { const testRows = []; const rows = document.querySelectorAll('table tbody tr'); rows.forEach(row => { if (row.innerText?.includes(prefix)) { testRows.push(row); } }); return { found: testRows.length, message: `Found ${testRows.length} test data rows to cleanup` }; })",
|
||||
|
||||
"verifyDataCreated": "(identifierValue) => { const pageText = document.body.innerText; return pageText.includes(identifierValue); }",
|
||||
|
||||
"verifyDataDeleted": "(identifierValue) => { const pageText = document.body.innerText; return !pageText.includes(identifierValue); }"
|
||||
},
|
||||
|
||||
"lifecycle": {
|
||||
"beforeTest": {
|
||||
"description": "테스트 시작 전 실행",
|
||||
"actions": [
|
||||
"generateTimestamp",
|
||||
"prepareTestData"
|
||||
]
|
||||
},
|
||||
"afterCreate": {
|
||||
"description": "데이터 생성 후 실행",
|
||||
"actions": [
|
||||
"verifyDataCreated",
|
||||
"storeCreatedId"
|
||||
]
|
||||
},
|
||||
"afterUpdate": {
|
||||
"description": "데이터 수정 후 실행",
|
||||
"actions": [
|
||||
"verifyDataUpdated"
|
||||
]
|
||||
},
|
||||
"afterDelete": {
|
||||
"description": "데이터 삭제 후 실행",
|
||||
"actions": [
|
||||
"verifyDataDeleted"
|
||||
]
|
||||
},
|
||||
"afterTest": {
|
||||
"description": "테스트 종료 후 실행",
|
||||
"actions": [
|
||||
"cleanupOrphanedData",
|
||||
"generateReport"
|
||||
]
|
||||
},
|
||||
"onError": {
|
||||
"description": "에러 발생 시 실행",
|
||||
"actions": [
|
||||
"attemptCleanup",
|
||||
"logError"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
"cleanup": {
|
||||
"strategy": "immediate",
|
||||
"strategies": {
|
||||
"immediate": "테스트 완료 즉시 삭제 (권장)",
|
||||
"batch": "테스트 세션 종료 시 일괄 삭제",
|
||||
"scheduled": "정기적으로 삭제 (위험)"
|
||||
},
|
||||
"orphanedDataHandling": {
|
||||
"description": "이전 테스트에서 정리되지 않은 데이터 처리",
|
||||
"maxAge": "24h",
|
||||
"autoCleanup": true,
|
||||
"pattern": "E2E_TEST_*"
|
||||
},
|
||||
"protectedData": {
|
||||
"description": "삭제하면 안되는 데이터 패턴",
|
||||
"patterns": [
|
||||
"가우스*",
|
||||
"실제데이터*",
|
||||
"운영*"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
"validation": {
|
||||
"rules": {
|
||||
"uniqueIdentifier": {
|
||||
"description": "테스트 데이터는 고유 식별자를 가져야 함",
|
||||
"check": "timestamp 포함 여부"
|
||||
},
|
||||
"noProductionData": {
|
||||
"description": "운영 데이터와 구분 가능해야 함",
|
||||
"check": "E2E_TEST_ 접두사 필수"
|
||||
},
|
||||
"cleanupable": {
|
||||
"description": "쉽게 식별하고 삭제 가능해야 함",
|
||||
"check": "검색 가능한 필드 존재"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"isolation": {
|
||||
"description": "테스트 데이터 격리 전략",
|
||||
"levels": {
|
||||
"session": {
|
||||
"description": "세션별 고유 타임스탬프 사용",
|
||||
"implementation": "timestamp를 세션 시작 시 한번만 생성"
|
||||
},
|
||||
"scenario": {
|
||||
"description": "시나리오별 고유 데이터",
|
||||
"implementation": "시나리오 ID + timestamp 조합"
|
||||
},
|
||||
"step": {
|
||||
"description": "스텝별 고유 데이터",
|
||||
"implementation": "스텝 번호 + timestamp 조합"
|
||||
}
|
||||
},
|
||||
"currentLevel": "session"
|
||||
},
|
||||
|
||||
"dependencies": {
|
||||
"description": "테스트 데이터 간 의존성 관리",
|
||||
"examples": {
|
||||
"approval": {
|
||||
"requires": ["employee", "document"],
|
||||
"description": "결재 테스트는 사원과 문서 데이터 필요"
|
||||
},
|
||||
"payment": {
|
||||
"requires": ["vendor", "invoice"],
|
||||
"description": "지급 테스트는 거래처와 청구서 필요"
|
||||
}
|
||||
},
|
||||
"resolutionOrder": "topological",
|
||||
"autoCreate": true
|
||||
},
|
||||
|
||||
"reporting": {
|
||||
"trackCreated": true,
|
||||
"trackDeleted": true,
|
||||
"trackOrphaned": true,
|
||||
"includeInTestReport": true,
|
||||
"format": {
|
||||
"summary": {
|
||||
"created": "생성된 테스트 데이터 수",
|
||||
"deleted": "삭제된 테스트 데이터 수",
|
||||
"orphaned": "정리되지 않은 데이터 수"
|
||||
},
|
||||
"detail": {
|
||||
"entity": "엔티티 유형",
|
||||
"identifier": "식별자",
|
||||
"createdAt": "생성 시간",
|
||||
"deletedAt": "삭제 시간",
|
||||
"status": "상태 (created/deleted/orphaned)"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"errorRecovery": {
|
||||
"onCreateFailure": {
|
||||
"action": "skip",
|
||||
"log": true,
|
||||
"continueTest": false
|
||||
},
|
||||
"onDeleteFailure": {
|
||||
"action": "retry",
|
||||
"maxRetries": 2,
|
||||
"log": true,
|
||||
"continueTest": true
|
||||
},
|
||||
"onOrphanedData": {
|
||||
"action": "warn",
|
||||
"autoCleanup": false,
|
||||
"log": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
{
|
||||
"$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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user