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:
@@ -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 || '');
|
||||
|
||||
Reference in New Issue
Block a user