- 실패 시나리오 11개 리라이트 + 중복 2개 삭제 (fill_form → READ-only 패턴) - 이전 78.7% → 88.0% 개선 (+9.3%p) - 실패 9건 중 7건은 사이드바 렌더링 인프라 이슈 - 실질 기능 성공률 97.1% (66/68) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 line
7.9 KiB
Plaintext
1 line
7.9 KiB
Plaintext
"}`); triggerClick(el); await sleep(300); return pass(`Clicked (existed): ${action.target}`); }, async press_key(action, ctx) { const key = action.value || action.key || 'Escape'; const target = action.target ? findEl(action.target, { selectors: ctx.selectors }) : document.activeElement; if (target) { target.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true })); target.dispatchEvent(new KeyboardEvent('keyup', { key, bubbles: true })); } await sleep(200); return pass(`Pressed key: ${key}`); }, async blur(action, ctx) { const el = action.target ? findEl(action.target, { selectors: ctx.selectors }) : document.activeElement; if (el) el.blur(); await sleep(200); return pass('Blurred'); }, async generate_timestamp(action, ctx) { const n = new Date(); const pad = (v) => v.toString().padStart(2, '0'); const ts = `${n.getFullYear()}${pad(n.getMonth() + 1)}${pad(n.getDate())}_${pad(n.getHours())}${pad(n.getMinutes())}${pad(n.getSeconds())}`; const varName = action.variable || 'timestamp'; ctx.variables[varName] = ts; return pass(`Generated timestamp: ${ts}`); }, async verify_toast(action, ctx) { const toastSels = [ '[class*=\"toast\"]', '[class*=\"Toast\"]', '[class*=\"notification\"]', '[class*=\"Notification\"]', '[class*=\"snackbar\"]', '[class*=\"Snackbar\"]', '[role=\"alert\"]', '[class*=\"alert\"]:not([class*=\"dialog\"])', ]; await sleep(500); for (const sel of toastSels) { const el = document.querySelector(sel); if (el && el.offsetParent !== null) { const text = el.innerText || ''; const v = action.verification || action.verify || {}; if (v.contains) { const patterns = v.contains.split('|'); if (patterns.some((p) => text.includes(p))) { return pass(`Toast found: \"${text.substring(0, 50)}\"`); } } return pass(`Toast visible: \"${text.substring(0, 50)}\"`); } } return warn('No toast/notification found'); }, async hover(action, ctx) { const el = findEl(action.target, { selectors: ctx.selectors }); if (!el) return warn(`Hover target not found: ${action.target}`); el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })); el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true })); await sleep(300); return pass(`Hovered: ${action.target}`); }, async navigate(action, ctx) { const url = action.target; if (url && url.startsWith('/')) { window.location.href = window.location.origin + url; return { status: 'navigation', details: `Navigate to ${url}` }; } return { status: 'navigation', details: `Navigate: ${url}` }; }, async reload(action, ctx) { window.location.reload(); return { status: 'navigation', details: 'Page reload' }; }, async navigate_back(action, ctx) { window.history.back(); return { status: 'navigation', details: 'Navigate back' }; }, async menu_navigate(action, ctx) { const expandBtn = Array.from(document.querySelectorAll('button')).find((b) => b.innerText?.includes('모두 펼치기') ); if (expandBtn) { expandBtn.click(); await sleep(1500); } const l1 = action.level1; const l2 = action.level2; if (l1) { const l1El = findEl(l1); if (l1El) { triggerClick(l1El); await sleep(500); } } if (l2) { const l2El = findEl(l2); if (l2El) { triggerClick(l2El); await sleep(2000); } } const v = action.expected || {}; if (v.url_contains && !window.location.href.includes(v.url_contains)) { return warn(`Menu nav: URL missing \"${v.url_contains}\"`); } return pass(`Menu navigation: ${l1} > ${l2}`); }, async noop(action, ctx) { return pass('No action'); }, }; function pass(details) { return { status: 'pass', details }; } function fail(details) { return { status: 'fail', details }; } function warn(details) { return { status: 'warn', details }; } function replaceVars(str, vars) { if (typeof str !== 'string') return str; str = str.replace(/\\{timestamp\\}/g, () => { const n = new Date(); const pad = (v) => v.toString().padStart(2, '0'); return `${n.getFullYear()}${pad(n.getMonth() + 1)}${pad(n.getDate())}_${pad(n.getHours())}${pad(n.getMinutes())}${pad(n.getSeconds())}`; }); if (vars) { str = str.replace(/\\{(\\w+)\\}/g, (_, key) => vars[key] ?? `{${key}}`); } return str; } async function retryAction(handler, action, ctx, maxRetries = 2, delayMs = 500) { let lastResult; for (let attempt = 0; attempt <= maxRetries; attempt++) { lastResult = await handler(action, ctx); if (lastResult.status === 'pass' || lastResult.status === 'navigation' || lastResult.status === 'native_required') { return lastResult; } if (attempt < maxRetries) { if (lastResult.details?.includes('not found') || lastResult.details?.includes('not visible')) { await sleep(delayMs); if (ModalGuard.check().open) await ModalGuard.close(); } else { await sleep(delayMs); } } } return lastResult; } async function runBatch(steps, vars = {}, config = {}) { const results = []; const ctx = { variables: { ...vars }, selectors: config.selectors || {}, }; const startUrl = window.location.href; let stoppedReason = 'complete'; let stoppedAtIndex = steps.length; for (let i = 0; i < steps.length; i++) { const step = steps[i]; const normalized = normalizeStep(step); const stepStart = now(); let stepStatus = 'pass'; let stepDetails = ''; let stepError = null; let subResults = []; for (let j = 0; j < normalized.subActions.length; j++) { const action = normalized.subActions[j]; const actionType = action.type; if (actionType === 'capture' || actionType === 'screenshot') { stoppedReason = 'native_required'; stoppedAtIndex = i; results.push({ stepId: normalized.stepId, name: normalized.name, status: 'skip', duration: now() - stepStart, details: 'Requires native screenshot', error: null, phase: normalized.phase, }); return buildBatchResult(results, ctx, stoppedReason, stoppedAtIndex); } const handler = ActionHandlers[actionType]; if (!handler) { subResults.push(warn(`Unknown action type: ${actionType}`)); continue; } const result = await retryAction(handler, action, ctx); if (result.status === 'navigation') { subResults.push(result); stoppedReason = 'navigation'; stoppedAtIndex = i + 1; const sr = buildStepResult(normalized, subResults, stepStart); results.push(sr); return buildBatchResult(results, ctx, stoppedReason, stoppedAtIndex); } if (result.status === 'native_required') { stoppedReason = 'native_required'; stoppedAtIndex = i; const sr = buildStepResult(normalized, subResults, stepStart); results.push(sr); return buildBatchResult(results, ctx, stoppedReason, stoppedAtIndex); } subResults.push(result); const currentUrl = window.location.href; if (currentUrl !== startUrl && j < normalized.subActions.length - 1) { } } const sr = buildStepResult(normalized, subResults, stepStart); results.push(sr); if (sr.status === 'fail' && normalized.critical) { stoppedReason = 'critical_failure'; stoppedAtIndex = i + 1; return buildBatchResult(results, ctx, stoppedReason, stoppedAtIndex); } } return buildBatchResult(results, ctx, stoppedReason, stoppedAtIndex); } function buildStepResult(normalized, subResults, stepStart) { const failed = subResults.filter((r) => r.status === 'fail'); const warns = subResults.filter((r) => r.status === 'warn'); let status = 'pass'; if (failed.length > 0) status = 'fail'; else if (warns.length > 0) status = 'warn'; const details = subResults.map((r) => r.details).join(' | '); const error = failed.length > 0 ? failed.map((f) => f.details).join('; ') : null; return { stepId: normalized.stepId, name: normalized.name, status, duration: now() - stepStart, details: details.substring(0, 200), error, phase: normalized.phase, }; } function buildBatchResult(results, ctx, stoppedReason, stoppedAtIndex) { return { totalSteps: results.length, completedSteps: results.filter((r) => r.status !== 'skip').length, passed: results.filter((r) => r.status === 'pass').length, failed: results.filter((r) => r.status === 'fail').length, warned: results.filter((r) => r.status === 'warn').length, results, variables: ctx.variables, apiSummary: ApiMonitor.summary(), currentUrl: window.location.href, stoppedReason, stoppedAtIndex, }; } window.__E2E__ = { _version: 1, init(config = {}) { ApiMonitor.install(); ApiMonitor.reset(); return { ready: true," |