refactor: _global*.json 설정 파일을 e2e/docs로 이동

This commit is contained in:
김보곤
2026-02-03 20:45:37 +09:00
parent c63f42d137
commit 40146d7ba1
9 changed files with 0 additions and 2435 deletions

View File

@@ -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
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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": ["오류 메시지", "스크린샷", "수동 정리 필요 여부"]
}
]
}
}

View File

@@ -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"
}
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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
}
}
}