fix: step-executor.js Shadcn UI 호환 셀렉터 확장 (7개 핸들러)

- 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 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-02-07 13:46:52 +09:00
parent 6d320b396d
commit 2e16da9549

View File

@@ -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 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) return fail(`Row with "${text}" not found`);
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');
// 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');
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 || '');