Files
sam-hotfix/e2e/runner/eval_chunk_0.js
김보곤 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
12 KiB
JavaScript

window.__C = '/**\n * E2E Step Executor - Browser-injected test runner\n * Injected via playwright_evaluate, exposes window.__E2E__\n *\n * Handles both scenario JSON formats:\n * Format A: { action, target, value, ... }\n * Format B: { actions: [{ type, target, value }, ...] }\n */\n(function () {\n \'use strict\';\n\n // Prevent double-injection\n if (window.__E2E__ && window.__E2E__._version >= 1) return;\n\n // ─── Helpers ────────────────────────────────────────────\n\n const sleep = (ms) => new Promise((r) => setTimeout(r, ms));\n\n const now = () => Date.now();\n\n /** Scroll element into view center */\n const scrollIntoView = (el) => {\n if (el && el.scrollIntoView) {\n el.scrollIntoView({ block: \'center\', behavior: \'instant\' });\n }\n };\n\n // ─── ApiMonitor ─────────────────────────────────────────\n\n const ApiMonitor = {\n _logs: [],\n _errors: [],\n _installed: false,\n\n install() {\n if (this._installed) return;\n this._installed = true;\n const self = this;\n const origFetch = window.fetch;\n window.fetch = async function (...args) {\n const url = typeof args[0] === \'string\' ? args[0] : args[0]?.url || \'\';\n const method = (args[1]?.method || \'GET\').toUpperCase();\n const t0 = now();\n try {\n const resp = await origFetch.apply(this, args);\n const entry = {\n url,\n method,\n status: resp.status,\n ok: resp.ok,\n duration: now() - t0,\n ts: new Date().toISOString(),\n };\n self._logs.push(entry);\n if (!resp.ok) self._errors.push(entry);\n return resp;\n } catch (err) {\n self._errors.push({ url, method, error: err.message, ts: new Date().toISOString() });\n throw err;\n }\n };\n\n // Also intercept XMLHttpRequest\n const origOpen = XMLHttpRequest.prototype.open;\n const origSend = XMLHttpRequest.prototype.send;\n XMLHttpRequest.prototype.open = function (method, url, ...rest) {\n this.__e2e_method = (method || \'GET\').toUpperCase();\n this.__e2e_url = url;\n return origOpen.call(this, method, url, ...rest);\n };\n XMLHttpRequest.prototype.send = function (...args) {\n const t0 = now();\n const self2 = this;\n this.addEventListener(\'loadend\', function () {\n const entry = {\n url: self2.__e2e_url,\n method: self2.__e2e_method,\n status: self2.status,\n ok: self2.status >= 200 && self2.status < 300,\n duration: now() - t0,\n ts: new Date().toISOString(),\n };\n self._logs.push(entry);\n if (!entry.ok) self._errors.push(entry);\n });\n return origSend.apply(this, args);\n };\n },\n\n summary() {\n const logs = this._logs;\n return {\n total: logs.length,\n success: logs.filter((l) => l.ok).length,\n failed: this._errors.length,\n avgResponseTime:\n logs.length > 0\n ? Math.round(logs.reduce((s, l) => s + (l.duration || 0), 0) / logs.length)\n : 0,\n slowCalls: logs.filter((l) => l.duration > 2000).length,\n };\n },\n\n findCall(urlPattern, method) {\n return this._logs.find(\n (l) =>\n l.url.includes(urlPattern) && (!method || l.method === method.toUpperCase())\n );\n },\n\n reset() {\n this._logs = [];\n this._errors = [];\n },\n };\n\n // ─── ModalGuard ─────────────────────────────────────────\n\n const MODAL_SELECTORS = [\n "[role=\'dialog\']",\n "[aria-modal=\'true\']",\n "[class*=\'modal\']:not([class*=\'tooltip\']):not([class*=\'modal-backdrop\'])",\n "[class*=\'Modal\']:not([class*=\'Tooltip\'])",\n "[class*=\'Dialog\']:not([class*=\'tooltip\'])",\n ];\n\n const ModalGuard = {\n check() {\n for (const sel of MODAL_SELECTORS) {\n const el = document.querySelector(sel);\n if (el && el.offsetParent !== null) {\n return { open: true, element: el };\n }\n }\n return { open: false, element: null };\n },\n\n focus() {\n const { open, element } = this.check();\n if (open && element) {\n const first = element.querySelector(\n \'input:not([type="hidden"]), textarea, select, button:not([class*="close"])\'\n );\n if (first) first.focus();\n return true;\n }\n return false;\n },\n\n async close() {\n const MAX = 3;\n for (let i = 0; i < MAX; i++) {\n const { open, element } = this.check();\n if (!open) return { closed: true };\n\n // Try X button\n const xBtn = element.querySelector(\n "button[class*=\'close\'], [aria-label=\'닫기\'], [aria-label=\'Close\'], button[class*=\'Close\']"\n );\n if (xBtn) {\n xBtn.click();\n await sleep(500);\n if (!this.check().open) return { closed: true };\n }\n\n // Try text buttons\n const textBtn = Array.from(element.querySelectorAll(\'button\')).find((b) =>\n [\'닫기\', \'Close\', \'취소\', \'Cancel\'].some((t) => b.innerText?.trim().includes(t))\n );\n if (textBtn) {\n textBtn.click();\n await sleep(500);\n if (!this.check().open) return { closed: true };\n }\n\n // Try ESC\n document.dispatchEvent(\n new KeyboardEvent(\'keydown\', { key: \'Escape\', keyCode: 27, bubbles: true })\n );\n await sleep(500);\n }\n return { closed: !this.check().open };\n },\n\n /** Find element within modal if open, else in document */\n scopedQuery(selector) {\n const { open, element } = this.check();\n const scope = open ? element : document;\n return scope.querySelector(selector);\n },\n\n scopedQueryAll(selector) {\n const { open, element } = this.check();\n const scope = open ? element : document;\n return scope.querySelectorAll(selector);\n },\n };\n\n // ─── findEl: Universal Element Finder ───────────────────\n\n /**\n * Find element by flexible selector syntax:\n * - CSS: "#id", ".class", "input[type=\'text\']"\n * - :has-text(): "button:has-text(\'등록\')"\n * - text=: "text=E2E 테스트"\n * - text=/regex/: "text=/\\\\d+/"\n * - plain Korean: "등록" → find clickable element containing text\n * - comma-fallback: "button:has-text(\'저장\'), button:has-text(\'등록\')"\n * - selector ref: looks up in selectors map if provided\n *\n * @param {string} selector\n * @param {object} opts - { nth, selectors, scope }\n * @returns {Element|null}\n */\n function findEl(selector, opts = {}) {\n if (!selector) return null;\n const { nth, selectors, scope } = opts;\n const root = scope || (ModalGuard.check().open ? ModalGuard.check().element : document);\n\n // 1) Selector reference lookup (from scenario selectors map)\n if (selectors && selectors[selector]) {\n return findEl(selectors[selector], { ...opts, selectors: null });\n }\n\n // 2) Comma-separated fallback: try each\n if (selector.includes(\',\') && !selector.includes(\':has-text(\')) {\n const parts = selector.split(\',\').map((s) => s.trim());\n for (const part of parts) {\n const el = findEl(part, opts);\n if (el) return el;\n }\n return null;\n }\n // Also handle comma within :has-text patterns\n if (selector.includes(\',\') && selector.includes(\':has-text(\')) {\n const parts = selector.split(/,\\s*(?=\\w+:has-text|button:|a:|div:|\\[)/);\n for (const part of parts) {\n const el = findEl(part.trim(), opts);\n if (el) return el;\n }\n return null;\n }\n\n // 3) text= selector\n if (selector.startsWith(\'text=\')) {\n const text = selector.slice(5);\n // regex\n if (text.startsWith(\'/\') && text.endsWith(\'/\')) {\n const re = new RegExp(text.slice(1, -1));\n const all = root.querySelectorAll(\'*\');\n const matches = Array.from(all).filter(\n (el) => el.children.length === 0 && re.test(el.textContent)\n );\n return nth != null ? matches[nth] || null : matches[0] || null;\n }\n // plain text\n const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);\n while (walker.nextNode()) {\n if (walker.currentNode.textContent.includes(text)) {\n return walker.currentNode.parentElement;\n }\n }\n return null;\n }\n\n // 4) text=/regex/ standalone\n if (selector.startsWith(\'text=/\') && selector.endsWith(\'/\')) {\n const re = new RegExp(selector.slice(6, -1));\n const all = root.querySelectorAll(\'*\');\n const matches = Array.from(all).filter(\n (el) => el.children.length === 0 && re.test(el.textContent)\n );\n return nth != null ? matches[nth] || null : matches[0] || null;\n }\n\n // 5) :has-text() pseudo selector\n if (selector.includes(\':has-text(\')) {\n const m = selector.match(/^(.+?):has-text\\([\'"]?(.+?)[\'"]?\\)(.*)$/);\n if (m) {\n const [, tag, text, suffix] = m;\n let candidates = Array.from(root.querySelectorAll(tag));\n candidates = candidates.filter((el) => el.innerText?.trim().includes(text));\n if (suffix) {\n // e.g. :last-of-type\n if (suffix.includes(\'last\')) candidates = candidates.slice(-1);\n }\n return nth != null ? candidates[nth] || null : candidates[0] || null;\n }\n }\n\n // 6) Pure Korean/text (no CSS special chars) → search clickable elements\n // Must contain at least one Korean character to enter this path (avoid matching CSS tag selectors like \'textarea\', \'button\')\n if (/[가-힣]/.test(selector) && /^[가-힣\\s\\w]+$/.test(selector) && !selector.includes(\'#\') && !selector.includes(\'.\') && !selector.includes(\'[\')) {\n const clickable = Array.from(\n root.querySelectorAll(\'a, button, [role="button"], [role="menuitem"], [role="tab"], [role="treeitem"], [onclick]\')\n );\n const match = clickable.find((el) => el.innerText?.trim().includes(selector));\n if (match) return match;\n\n // Also try any element\n const all = Array.from(root.querySelectorAll(\'*\'));\n const textMatch = all.find(\n (el) => el.children.length === 0 && el.textContent?.trim().includes(selector)\n );\n return textMatch || null;\n }\n\n // 7) Standard CSS selector\n try {\n if (nth != null) {\n const all = root.querySelectorAll(selector);\n return all[nth] || null;\n }\n return root.querySelector(selector);\n } catch {\n // Invalid CSS selector, try text search fallback\n const all = Array.from(root.querySelectorAll(\'*\'));\n return all.find((el) => el.textContent?.trim().includes(selector)) || null;\n }\n }\n\n // ─── StepNormalizer ─────────────────────────────────────\n\n /** Normalize action type aliases */\n function normalizeActionType(type) {\n if (!type) return \'noop\';\n const ALIASES = {\n // Fill variants\n input: \'fill\',\n type: \'fill\',\n type_text: \'fill\',\n text: \'fill\',\n textarea: \'fill\',\n email: \'fill\',\n password: \'fill\',\n number: \'fill\',\n \'clear_and_type\': \'fill\',\n \'clear-and-type\': \'fill\',\n // Click variants\n \'click+confirm\': \'click_and_confirm\',\n click_download: \'click\',\n click_dropdown: \'select_dropdown\',\n click_checkbox: \'check\',\n click_if_exists: \'click_if_exists\',\n clickFirstRow: \'click_first_row\',\n clickInModal: \'click\',\n // Navigation\n navigateBack: \'navigate_back\',\n goBack: \'navigate_back\',\n navigation: \'n';