diff --git a/browse/src/browser-manager.ts b/browse/src/browser-manager.ts index 243ed177..260c8219 100644 --- a/browse/src/browser-manager.ts +++ b/browse/src/browser-manager.ts @@ -208,6 +208,15 @@ export class BrowserManager { return { selector }; } + /** Get the ARIA role for a ref selector, or null for CSS selectors / unknown refs. */ + getRefRole(selector: string): string | null { + if (selector.startsWith('@e') || selector.startsWith('@c')) { + const entry = this.refMap.get(selector.slice(1)); + return entry?.role ?? null; + } + return null; + } + getRefCount(): number { return this.refMap.size; } diff --git a/browse/src/read-commands.ts b/browse/src/read-commands.ts index a7d76352..54877562 100644 --- a/browse/src/read-commands.ts +++ b/browse/src/read-commands.ts @@ -17,6 +17,24 @@ function hasAwait(code: string): boolean { return /\bawait\b/.test(stripped); } +/** Detect whether code needs a block wrapper {…} vs expression wrapper (…) inside an async IIFE. */ +function needsBlockWrapper(code: string): boolean { + const trimmed = code.trim(); + if (trimmed.split('\n').length > 1) return true; + if (/\b(const|let|var|function|class|return|throw|if|for|while|switch|try)\b/.test(trimmed)) return true; + if (trimmed.includes(';')) return true; + return false; +} + +/** Wrap code for page.evaluate(), using async IIFE with block or expression body as needed. */ +function wrapForEvaluate(code: string): string { + if (!hasAwait(code)) return code; + const trimmed = code.trim(); + return needsBlockWrapper(trimmed) + ? `(async()=>{\n${code}\n})()` + : `(async()=>(${trimmed}))()`; +} + // Security: Path validation to prevent path traversal attacks const SAFE_DIRECTORIES = ['/tmp', process.cwd()]; @@ -124,7 +142,7 @@ export async function handleReadCommand( case 'js': { const expr = args[0]; if (!expr) throw new Error('Usage: browse js '); - const wrapped = hasAwait(expr) ? `(async()=>(${expr}))()` : expr; + const wrapped = wrapForEvaluate(expr); const result = await page.evaluate(wrapped); return typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result ?? ''); } @@ -135,14 +153,8 @@ export async function handleReadCommand( validateReadPath(filePath); if (!fs.existsSync(filePath)) throw new Error(`File not found: ${filePath}`); const code = fs.readFileSync(filePath, 'utf-8'); - if (hasAwait(code)) { - const trimmed = code.trim(); - const isSingleExpr = trimmed.split('\n').length === 1; - const wrapped = isSingleExpr ? `(async()=>(${trimmed}))()` : `(async()=>{\n${code}\n})()`; - const result = await page.evaluate(wrapped); - return typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result ?? ''); - } - const result = await page.evaluate(code); + const wrapped = wrapForEvaluate(code); + const result = await page.evaluate(wrapped); return typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result ?? ''); } diff --git a/browse/src/write-commands.ts b/browse/src/write-commands.ts index 87b2fa5d..2b384920 100644 --- a/browse/src/write-commands.ts +++ b/browse/src/write-commands.ts @@ -44,11 +44,48 @@ export async function handleWriteCommand( case 'click': { const selector = args[0]; if (!selector) throw new Error('Usage: browse click '); + + // Auto-route: if ref points to a real