fix: review fixes — iframe ref scoping, detached frame recovery, state validation

- snapshot.ts: ref locators, cursor-interactive scan, and cursor locator
  now use target (frame-aware) instead of page — fixes @ref clicking in iframes
- browser-manager.ts: getActiveFrameOrPage auto-recovers from detached frames
  via isDetached() check
- meta-commands.ts: state load resets activeFrame, elementHandle disposed after
  contentFrame(), state file schema validation (cookies + pages arrays),
  filter empty pipe segments in chain tokenizer
- write-commands.ts: upload command uses target.locator() for frame support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-03-26 07:33:32 -06:00
parent b6a946aa06
commit d597c33eab
4 changed files with 17 additions and 6 deletions
+4
View File
@@ -561,6 +561,10 @@ export class BrowserManager {
* Use this for operations that work on both Page and Frame (locator, evaluate, etc.).
*/
getActiveFrameOrPage(): import('playwright').Page | import('playwright').Frame {
// Auto-recover from detached frames (iframe removed/navigated)
if (this.activeFrame?.isDetached()) {
this.activeFrame = null;
}
return this.activeFrame ?? this.getPage();
}
+8 -1
View File
@@ -219,7 +219,9 @@ export async function handleMetaCommand(
if (!Array.isArray(commands)) throw new Error('not array');
} catch {
// Fallback: pipe-delimited format "goto url | click @e5 | snapshot -ic"
commands = jsonStr.split(' | ').map(seg => tokenizePipeSegment(seg.trim()));
commands = jsonStr.split(' | ')
.filter(seg => seg.trim().length > 0)
.map(seg => tokenizePipeSegment(seg.trim()));
}
const results: string[] = [];
@@ -478,7 +480,11 @@ export async function handleMetaCommand(
if (action === 'load') {
if (!fs.existsSync(statePath)) throw new Error(`State not found: ${statePath}`);
const data = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
if (!Array.isArray(data.cookies) || !Array.isArray(data.pages)) {
throw new Error('Invalid state file: expected cookies and pages arrays');
}
// Close existing pages, then restore (replace, not merge)
bm.setFrame(null);
await bm.closeAllPages();
await bm.restoreState({
cookies: data.cookies,
@@ -516,6 +522,7 @@ export async function handleMetaCommand(
const locator = 'locator' in resolved ? resolved.locator : page.locator(resolved.selector);
const elementHandle = await locator.elementHandle({ timeout: 5000 });
frame = await elementHandle?.contentFrame() ?? null;
await elementHandle?.dispose();
}
if (!frame) throw new Error(`Frame not found: ${target}`);
+4 -4
View File
@@ -208,11 +208,11 @@ export async function handleSnapshot(
let locator: Locator;
if (opts.selector) {
locator = page.locator(opts.selector).getByRole(node.role as any, {
locator = target.locator(opts.selector).getByRole(node.role as any, {
name: node.name || undefined,
});
} else {
locator = page.getByRole(node.role as any, {
locator = target.getByRole(node.role as any, {
name: node.name || undefined,
});
}
@@ -236,7 +236,7 @@ export async function handleSnapshot(
// ─── Cursor-interactive scan (-C) ─────────────────────────
if (opts.cursorInteractive) {
try {
const cursorElements = await page.evaluate(() => {
const cursorElements = await target.evaluate(() => {
const STANDARD_INTERACTIVE = new Set([
'A', 'BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'SUMMARY', 'DETAILS',
]);
@@ -290,7 +290,7 @@ export async function handleSnapshot(
let cRefCounter = 1;
for (const elem of cursorElements) {
const ref = `c${cRefCounter++}`;
const locator = page.locator(elem.selector);
const locator = target.locator(elem.selector);
refMap.set(ref, { locator, role: 'cursor-interactive', name: elem.text });
output.push(`@${ref} [${elem.reason}] "${elem.text}"`);
}
+1 -1
View File
@@ -258,7 +258,7 @@ export async function handleWriteCommand(
if ('locator' in resolved) {
await resolved.locator.setInputFiles(filePaths);
} else {
await page.locator(resolved.selector).setInputFiles(filePaths);
await target.locator(resolved.selector).setInputFiles(filePaths);
}
const fileInfo = filePaths.map(fp => {