From 2e16da9549f143c48cc69951278110a8d047a0dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Sat, 7 Feb 2026 13:46:52 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20step-executor.js=20Shadcn=20UI=20?= =?UTF-8?q?=ED=98=B8=ED=99=98=20=EC=85=80=EB=A0=89=ED=84=B0=20=ED=99=95?= =?UTF-8?q?=EC=9E=A5=20(7=EA=B0=9C=20=ED=95=B8=EB=93=A4=EB=9F=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - search: 11개 폴백 셀렉터 (Shadcn Input 지원) - click_first_row: 5초 폴링 + role/class 폴백 - verify_table: role="table"/role="grid" 지원 - wait_for_table: 다중 row 셀렉터 폴링 - click_row: role="row", class*="list-item" 폴백 - fill: SELECT/combobox/date 스마트 위임 - select_dropdown: data-value, cmdk-item 등 Shadcn 옵션 지원 Co-Authored-By: Claude Opus 4.6 --- e2e/runner/step-executor.js | 147 +++++++++++++++++++++++++++++------- 1 file changed, 120 insertions(+), 27 deletions(-) diff --git a/e2e/runner/step-executor.js b/e2e/runner/step-executor.js index 4f5fa44..9ca47ea 100644 --- a/e2e/runner/step-executor.js +++ b/e2e/runner/step-executor.js @@ -592,24 +592,52 @@ }, async click_row(action, ctx) { - // Click a table row containing text + // Click a table row containing text with fallback selectors 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}`); + const rowSelectors = [ + 'table tbody tr', + '[role="row"]', + 'tr', + '[class*="list-item"]', + '[class*="row"]', + ]; + for (const sel of rowSelectors) { + const rows = document.querySelectorAll(sel); + const row = Array.from(rows).find((r) => r.innerText?.includes(text)); + if (row) { + scrollIntoView(row); + row.click(); + await sleep(500); + return pass(`Clicked row: ${text}`); + } + } + return fail(`Row with "${text}" not found`); }, 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'); + // Poll for up to 5 seconds with fallback selectors + const rowSelectors = [ + 'table tbody tr', + '[role="row"]:not([role="row"]:first-child)', + 'table tr:nth-child(2)', + '[class*="table"] [class*="row"]', + '[class*="list"] [class*="item"]', + ]; + const timeout = 5000; + const t0 = now(); + while (now() - t0 < timeout) { + for (const sel of rowSelectors) { + const row = document.querySelector(sel); + if (row && row.offsetParent !== null) { + scrollIntoView(row); + row.click(); + await sleep(500); + return pass('Clicked first row'); + } + } + await sleep(300); + } + return fail('No table rows found'); }, async click_button(action, ctx) { @@ -647,11 +675,35 @@ let el = findEl(action.target, { selectors: ctx.selectors }); if (!el) return fail(`Input not found: ${action.target}`); scrollIntoView(el); + + let value = action.value ?? ''; + value = replaceVars(value, ctx.variables); + + // Smart type detection: delegate to appropriate handler + if (el.tagName === 'SELECT') { + // Delegate to select handler + return ActionHandlers.select({ ...action, value }, ctx); + } + if (el.getAttribute('role') === 'combobox') { + // Delegate to select_dropdown handler + return ActionHandlers.select_dropdown({ ...action, value }, ctx); + } + el.focus(); if (action.clear !== false) clearInput(el); - let value = action.value ?? ''; - // Replace {timestamp} placeholder - value = replaceVars(value, ctx.variables); + + // Date input: use nativeInputValueSetter + change event + if (el.type === 'date' || el.type === 'datetime-local') { + const proto = HTMLInputElement.prototype; + const nativeSetter = Object.getOwnPropertyDescriptor(proto, 'value')?.set; + if (nativeSetter) nativeSetter.call(el, value); + else el.value = value; + el.dispatchEvent(new Event('input', { bubbles: true })); + el.dispatchEvent(new Event('change', { bubbles: true })); + await sleep(200); + return pass(`Filled date "${action.target}" with "${value}"`); + } + setInputValue(el, value); await sleep(200); return pass(`Filled "${action.target}" with "${value.substring(0, 30)}"`); @@ -773,13 +825,19 @@ triggerClick(trigger); await sleep(500); - // Find option in dropdown list + // Find option in dropdown list with expanded selectors for Shadcn UI const optionSelectors = [ '[role="option"]', '[role="listbox"] li', + '[data-value]', '[class*="option"]', + '[class*="Option"]', + '[class*="select-option"]', '[class*="menu-item"]', + '[class*="MenuItem"]', '[class*="dropdown-item"]', + '[class*="DropdownItem"]', + '[cmdk-item]', 'li', ]; for (const sel of optionSelectors) { @@ -861,10 +919,18 @@ async wait_for_table(action, ctx) { const timeout = action.timeout || 10000; + const rowSelectors = [ + 'table tbody tr', + '[role="table"] [role="row"]', + '[role="grid"] [role="row"]', + '[class*="table"]:not([class*="tooltip"]) tbody tr', + ]; const t0 = now(); while (now() - t0 < timeout) { - const rows = document.querySelectorAll('table tbody tr'); - if (rows.length > 0) return pass(`Table loaded: ${rows.length} rows`); + for (const sel of rowSelectors) { + const rows = document.querySelectorAll(sel); + if (rows.length > 0) return pass(`Table loaded: ${rows.length} rows`); + } await sleep(300); } return fail('Timeout waiting for table data'); @@ -1013,13 +1079,24 @@ async verify_table(action, ctx) { const v = action.verification || {}; - const table = document.querySelector('table'); + // Expanded table detection for Shadcn UI + const tableSelectors = [ + 'table', + '[role="table"]', + '[role="grid"]', + '[class*="table"]:not([class*="tooltip"]):not([class*="Table"][class*="Cell"])', + ]; + let table = null; + for (const sel of tableSelectors) { + table = document.querySelector(sel); + if (table) break; + } if (!table) return warn('No table found'); - const headers = Array.from(table.querySelectorAll('thead th, thead td')).map((h) => + const headers = Array.from(table.querySelectorAll('thead th, thead td, [role="columnheader"]')).map((h) => h.textContent?.trim() ); - const rows = table.querySelectorAll('tbody tr'); + const rows = table.querySelectorAll('tbody tr, [role="row"]'); if (v.columns) { const missing = v.columns.filter( @@ -1225,10 +1302,26 @@ async search(action, ctx) { const el = findEl(action.target, { selectors: ctx.selectors }); if (!el) { - // Fallback: find any search input - const searchInput = document.querySelector( - 'input[type="search"], input[placeholder*="검색"], input[placeholder*="Search"]' - ); + // Fallback: find any search input with expanded selectors for Shadcn UI + const searchSelectors = [ + 'input[type="search"]', + 'input[placeholder*="검색"]', + 'input[placeholder*="Search"]', + 'input[placeholder*="search"]', + 'input[placeholder*="조회"]', + 'input[role="searchbox"]', + 'input[aria-label*="검색"]', + '[class*="search"] input', + '[class*="Search"] input', + 'div.relative > input[type="text"]', + 'header input[type="text"]', + '[class*="toolbar"] input', + ]; + let searchInput = null; + for (const sel of searchSelectors) { + searchInput = document.querySelector(sel); + if (searchInput) break; + } if (!searchInput) return fail('Search input not found'); clearInput(searchInput); setInputValue(searchInput, action.value || '');