feat: 재시도 로직 전역 설정 추가
- 재시도 전략: immediate, linear, exponential, conditional - 재시도 가능 오류: timeout, not found, network, 5xx 등 - 재시도 전 액션: waitForPageLoad, scrollIntoView, closeOverlays 등 - 스텝별 재시도 설정: click(3회), fill(2회), navigate(3회) - Circuit Breaker: 5회 연속 실패 시 차단 - 시나리오 전체 재시도 지원 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
286
_global-retry-config.json
Normal file
286
_global-retry-config.json
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
{
|
||||||
|
"$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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user