- 실패 시나리오 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.8 KiB
Plaintext
1 line
7.8 KiB
Plaintext
"lement', verify_pagination: 'verify_element', verify_summary: 'verify_text', verify_totals: 'verify_text', verify_search_result: 'verify_data', verify_row_count: 'verify_table', verify_vendor_info: 'verify_detail', verify_detail_info: 'verify_detail', verify_data_update: 'verify_data', verify_transactions_update: 'verify_data', verify_transaction_table: 'verify_table', verify_calculated_value: 'verify_text', verify_toast: 'verify_toast', verifyButtonExists: 'verify_element', verifyUrl: 'verify_url', verifyNoErrorPage: 'verify_url_stability', verify: 'verify_element', select_option: 'select_dropdown', select_or_click: 'select_dropdown', combobox: 'select_dropdown', closeModal: 'close_modal', close_modal: 'close_modal', modalClose: 'close_modal', openModal: 'wait_for_modal', checkModalOpen: 'wait_for_modal', fillInModal: 'fill', selectInModal: 'select_dropdown', confirm_dialog: 'click_dialog_confirm', delete: 'click', store: 'save_url', getCurrentUrl: 'save_url', clearSearch: 'clear', blur: 'blur', login: 'fill_form', keypress: 'press_key', press: 'press_key', pressKey: 'press_key', resize: 'noop', log: 'noop', manualVerification: 'noop', generateTimestamp: 'generate_timestamp', random: 'generate_timestamp', setupDownloadListener: 'noop', saveDownloadedFile: 'noop', verifyDownload: 'noop', verifyDownloadedFile: 'noop', download: 'click', change_date: 'fill', change_date_range: 'fill', date_range: 'fill', date: 'fill', datepicker: 'fill', setDateRange: 'fill', timepicker: 'fill', composite: 'noop', hierarchy: 'noop', permission: 'noop', ifStillFailed: 'noop', tryAlternativeUrls: 'noop', element: 'verify_element', elementExists: 'verify_element', tableExists: 'verify_table', tabsExist: 'verify_element', error_message: 'verify_text', warning: 'noop', url: 'verify_url', URL_STABILITY: 'verify_url_stability', checkFor404: 'verify_url_stability', expectResponse: 'noop', assertResponse: 'noop', apiResponse: 'noop', findRow: 'click_row', scroll: 'scrollAndFind', radio: 'check', toggle_switch: 'check', capture: 'capture', screenshot: 'capture', drag_start: 'noop', drag_over: 'noop', drag_end: 'noop', }; return ALIASES[type] || type; } function normalizeStep(step) { const stepId = step.id || step.step || 0; const name = step.name || step.description || `Step ${stepId}`; const critical = step.critical || false; const phase = step.phase || null; const verification = step.verification || step.verify || step.expected || null; let subActions = []; if (Array.isArray(step.actions)) { subActions = step.actions.map((a) => ({ ...a, type: normalizeActionType(a.type), })); } else if (step.action) { subActions = [ { type: normalizeActionType(step.action), target: step.target, value: step.value, variable: step.variable, pattern: step.pattern, nth: step.nth, clear: step.clear, fields: step.fields, checks: step.checks, search: step.search, level1: step.level1, level2: step.level2, script: step.script, duration: step.duration, verification: step.verification, verify: step.verify, expected: step.expected, critical: step.critical, }, ]; } else { subActions = [ { type: 'noop', verification, }, ]; } return { stepId, name, subActions, critical, phase, verification }; } function setInputValue(el, value) { if (!el) return false; const isTextarea = el instanceof HTMLTextAreaElement; const proto = isTextarea ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; const nativeSetter = Object.getOwnPropertyDescriptor(proto, 'value')?.set; const reactPropsKey = Object.keys(el).find(k => k.startsWith('__reactProps$')); if (reactPropsKey && el[reactPropsKey] && typeof el[reactPropsKey].onChange === 'function') { if (nativeSetter) nativeSetter.call(el, value); else el.value = value; el[reactPropsKey].onChange({ target: el, currentTarget: el }); if (el.value === value) return true; } el.focus(); el.select(); const execResult = document.execCommand('insertText', false, value); if (execResult && el.value === value) return true; if (nativeSetter) nativeSetter.call(el, value); else el.value = value; const tracker = el._valueTracker; if (tracker) tracker.setValue(''); el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); return true; } function clearInput(el) { if (!el) return false; el.focus(); setInputValue(el, ''); return true; } function triggerClick(el) { if (!el) return false; scrollIntoView(el); el.focus && el.focus(); el.click(); return true; } const ActionHandlers = { async click(action, ctx) { const el = findEl(action.target, { selectors: ctx.selectors }); if (!el) return fail(`Element not found: ${action.target}`); scrollIntoView(el); await sleep(100); triggerClick(el); await sleep(300); return pass(`Clicked: ${action.target}`); }, async click_nth(action, ctx) { const n = action.nth ?? 0; const el = findEl(action.target, { nth: n, selectors: ctx.selectors }); if (!el) return fail(`Element[${n}] not found: ${action.target}`); scrollIntoView(el); triggerClick(el); await sleep(300); return pass(`Clicked [${n}]: ${action.target}`); }, async click_row(action, ctx) { const text = action.target || action.value; const rows = document.querySelectorAll('table tbody tr'); const row = Array.from(rows).find((r) => r.innerText?.includes(text)); if (!row) return fail(`Row with \"${text}\" not found`); scrollIntoView(row); row.click(); await sleep(500); return pass(`Clicked row: ${text}`); }, async click_first_row(action, ctx) { const row = document.querySelector('table tbody tr'); if (!row) return fail('No table rows found'); scrollIntoView(row); row.click(); await sleep(500); return pass('Clicked first row'); }, async click_button(action, ctx) { const text = action.value || action.target; const btns = Array.from(document.querySelectorAll('button, [role=\"button\"]')); const btn = btns.find((b) => b.innerText?.trim().includes(text)); if (!btn) return fail(`Button \"${text}\" not found`); scrollIntoView(btn); triggerClick(btn); await sleep(300); return pass(`Clicked button: ${text}`); }, async click_dialog_confirm(action, ctx) { await sleep(300); const dialog = document.querySelector('[role=\"alertdialog\"]') || document.querySelector('[role=\"dialog\"]'); if (!dialog) return fail('No dialog found'); const confirmTexts = ['확인', '예', '삭제', 'OK', 'Yes', 'Confirm']; const btns = Array.from(dialog.querySelectorAll('button')); const btn = btns.find((b) => confirmTexts.some((t) => b.innerText?.trim().includes(t)) ); if (!btn) return fail('Confirm button not found in dialog'); triggerClick(btn); await sleep(500); return pass('Confirmed dialog'); }, async fill(action, ctx) { let el = findEl(action.target, { selectors: ctx.selectors }); if (!el) return fail(`Input not found: ${action.target}`); scrollIntoView(el); el.focus(); if (action.clear !== false) clearInput(el); let value = action.value ?? ''; value = replaceVars(value, ctx.variables); setInputValue(el, value); await sleep(200); return pass(`Filled \"${action.target}\" with \"${value.substring(0, 30)}\"`); }, async fill_nth(action, ctx) { const n = action.nth ?? 0; const el = findEl(action.target, { nth: n, selectors: ctx.selectors }); if (!el) return fail(`Input[${n}] not found: ${action.target}`); el.focus(); clearInput(el); const value = replaceVars(action.value ?? '', ctx.variables); setInputValue(el, value); await sleep(200); return pass(`Filled [${n}] with \"${value.substring(0, 30)}\"`); }, async fill_form(action, ctx) { const fields = action.fields || []; const results = []; for (const field of fields) { const label = field.name || field.label; let value = replaceVars(field.value ?? '', ctx.variables); let el = null; const labels = Array.from(document.querySelectorAll('label')); const matchLabel = labels.find((l) => l.textContent?.includes(label)); if (matchLabel) { const forId = matchLabel.getAttribute('for'); if (forId) el = document.getElementById(forId); if (!el) el = matchLabel.querySelector('input, textarea, select'); if (!el) el = matchLabel.parentElement?.querySelector('input, textarea, s" |