Files
sam-hotfix/e2e/runner/chunk_0.txt
김보곤 6d320b396d test: E2E 전체 테스트 66/75 (88.0%) 통과 - 시나리오 리라이트 후 재실행
- 실패 시나리오 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>
2026-02-06 22:01:54 +09:00

1 line
7.9 KiB
Plaintext

"(function () { 'use strict'; if (window.__E2E__ && window.__E2E__._version >= 1) return; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const now = () => Date.now(); const scrollIntoView = (el) => { if (el && el.scrollIntoView) { el.scrollIntoView({ block: 'center', behavior: 'instant' }); } }; const ApiMonitor = { _logs: [], _errors: [], _installed: false, install() { if (this._installed) return; this._installed = true; const self = this; const origFetch = window.fetch; window.fetch = async function (...args) { const url = typeof args[0] === 'string' ? args[0] : args[0]?.url || ''; const method = (args[1]?.method || 'GET').toUpperCase(); const t0 = now(); try { const resp = await origFetch.apply(this, args); const entry = { url, method, status: resp.status, ok: resp.ok, duration: now() - t0, ts: new Date().toISOString(), }; self._logs.push(entry); if (!resp.ok) self._errors.push(entry); return resp; } catch (err) { self._errors.push({ url, method, error: err.message, ts: new Date().toISOString() }); throw err; } }; const origOpen = XMLHttpRequest.prototype.open; const origSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url, ...rest) { this.__e2e_method = (method || 'GET').toUpperCase(); this.__e2e_url = url; return origOpen.call(this, method, url, ...rest); }; XMLHttpRequest.prototype.send = function (...args) { const t0 = now(); const self2 = this; this.addEventListener('loadend', function () { const entry = { url: self2.__e2e_url, method: self2.__e2e_method, status: self2.status, ok: self2.status >= 200 && self2.status < 300, duration: now() - t0, ts: new Date().toISOString(), }; self._logs.push(entry); if (!entry.ok) self._errors.push(entry); }); return origSend.apply(this, args); }; }, summary() { const logs = this._logs; return { total: logs.length, success: logs.filter((l) => l.ok).length, failed: this._errors.length, avgResponseTime: logs.length > 0 ? Math.round(logs.reduce((s, l) => s + (l.duration || 0), 0) / logs.length) : 0, slowCalls: logs.filter((l) => l.duration > 2000).length, }; }, findCall(urlPattern, method) { return this._logs.find( (l) => l.url.includes(urlPattern) && (!method || l.method === method.toUpperCase()) ); }, reset() { this._logs = []; this._errors = []; }, }; const MODAL_SELECTORS = [ \"[role='dialog']\", \"[aria-modal='true']\", \"[class*='modal']:not([class*='tooltip']):not([class*='modal-backdrop'])\", \"[class*='Modal']:not([class*='Tooltip'])\", \"[class*='Dialog']:not([class*='tooltip'])\", ]; const ModalGuard = { check() { for (const sel of MODAL_SELECTORS) { const el = document.querySelector(sel); if (el && el.offsetParent !== null) { return { open: true, element: el }; } } return { open: false, element: null }; }, focus() { const { open, element } = this.check(); if (open && element) { const first = element.querySelector( 'input:not([type=\"hidden\"]), textarea, select, button:not([class*=\"close\"])' ); if (first) first.focus(); return true; } return false; }, async close() { const MAX = 3; for (let i = 0; i < MAX; i++) { const { open, element } = this.check(); if (!open) return { closed: true }; const xBtn = element.querySelector( \"button[class*='close'], [aria-label='닫기'], [aria-label='Close'], button[class*='Close']\" ); if (xBtn) { xBtn.click(); await sleep(500); if (!this.check().open) return { closed: true }; } const textBtn = Array.from(element.querySelectorAll('button')).find((b) => ['닫기', 'Close', '취소', 'Cancel'].some((t) => b.innerText?.trim().includes(t)) ); if (textBtn) { textBtn.click(); await sleep(500); if (!this.check().open) return { closed: true }; } document.dispatchEvent( new KeyboardEvent('keydown', { key: 'Escape', keyCode: 27, bubbles: true }) ); await sleep(500); } return { closed: !this.check().open }; }, scopedQuery(selector) { const { open, element } = this.check(); const scope = open ? element : document; return scope.querySelector(selector); }, scopedQueryAll(selector) { const { open, element } = this.check(); const scope = open ? element : document; return scope.querySelectorAll(selector); }, }; function findEl(selector, opts = {}) { if (!selector) return null; const { nth, selectors, scope } = opts; const root = scope || (ModalGuard.check().open ? ModalGuard.check().element : document); if (selectors && selectors[selector]) { return findEl(selectors[selector], { ...opts, selectors: null }); } if (selector.includes(',') && !selector.includes(':has-text(')) { const parts = selector.split(',').map((s) => s.trim()); for (const part of parts) { const el = findEl(part, opts); if (el) return el; } return null; } if (selector.includes(',') && selector.includes(':has-text(')) { const parts = selector.split(/,\\s*(?=\\w+:has-text|button:|a:|div:|\\[)/); for (const part of parts) { const el = findEl(part.trim(), opts); if (el) return el; } return null; } if (selector.startsWith('text=')) { const text = selector.slice(5); if (text.startsWith('/') && text.endsWith('/')) { const re = new RegExp(text.slice(1, -1)); const all = root.querySelectorAll('*'); const matches = Array.from(all).filter( (el) => el.children.length === 0 && re.test(el.textContent) ); return nth != null ? matches[nth] || null : matches[0] || null; } const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); while (walker.nextNode()) { if (walker.currentNode.textContent.includes(text)) { return walker.currentNode.parentElement; } } return null; } if (selector.startsWith('text=/') && selector.endsWith('/')) { const re = new RegExp(selector.slice(6, -1)); const all = root.querySelectorAll('*'); const matches = Array.from(all).filter( (el) => el.children.length === 0 && re.test(el.textContent) ); return nth != null ? matches[nth] || null : matches[0] || null; } if (selector.includes(':has-text(')) { const m = selector.match(/^(.+?):has-text\\(['\"]?(.+?)['\"]?\\)(.*)$/); if (m) { const [, tag, text, suffix] = m; let candidates = Array.from(root.querySelectorAll(tag)); candidates = candidates.filter((el) => el.innerText?.trim().includes(text)); if (suffix) { if (suffix.includes('last')) candidates = candidates.slice(-1); } return nth != null ? candidates[nth] || null : candidates[0] || null; } } if (/[가-힣]/.test(selector) && /^[가-힣\\s\\w]+$/.test(selector) && !selector.includes('#') && !selector.includes('.') && !selector.includes('[')) { const clickable = Array.from( root.querySelectorAll('a, button, [role=\"button\"], [role=\"menuitem\"], [role=\"tab\"], [role=\"treeitem\"], [onclick]') ); const match = clickable.find((el) => el.innerText?.trim().includes(selector)); if (match) return match; const all = Array.from(root.querySelectorAll('*')); const textMatch = all.find( (el) => el.children.length === 0 && el.textContent?.trim().includes(selector) ); return textMatch || null; } try { if (nth != null) { const all = root.querySelectorAll(selector); return all[nth] || null; } return root.querySelector(selector); } catch { const all = Array.from(root.querySelectorAll('*')); return all.find((el) => el.textContent?.trim().includes(selector)) || null; } } function normalizeActionType(type) { if (!type) return 'noop'; const ALIASES = { input: 'fill', type: 'fill', type_text: 'fill', text: 'fill', textarea: 'fill', email: 'fill', password: 'fill', number: 'fill', 'clear_and_type': 'fill', 'clear-and-type': 'fill', 'click+confirm': 'click_and_confirm', click_download: 'click', click_dropdown: 'select_dropdown', click_checkbox: 'check', click_if_exists: 'click_if_exists', clickFirstRow: 'click_first_row', clickInModal: 'click', navigateBack: 'navigate_back', goBack: 'navigate_back', navigation: 'navigate', directNavigation: 'navigate', navigateViaMenuClick: 'menu_navigate', refresh: 'reload', waitForModal: 'wait_for_modal', waitForNavigation: 'wait_for_navigation', waitForTable: 'wait_for_table', verify_elements: 'verify_element', verify_table_data: 'verify_table', verify_table_structure: 'verify_table', verify_field: 'verify_element', verify_checkbox: 'verify_element', verify_action_buttons: 'verify_e"