From b1f5d675843bbe7da0acd8b3969338cac9f408f8 Mon Sep 17 00:00:00 2001 From: light Date: Sat, 31 Jan 2026 11:47:47 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=9E=AC=EC=8B=9C=EB=8F=84=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=A0=84=EC=97=AD=20=EC=84=A4=EC=A0=95=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 재시도 전략: 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 --- _global-retry-config.json | 286 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 _global-retry-config.json diff --git a/_global-retry-config.json b/_global-retry-config.json new file mode 100644 index 0000000..f33449b --- /dev/null +++ b/_global-retry-config.json @@ -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 + } + } +}