{ "$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 } } }