mirror of https://github.com/garrytan/gstack.git
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:
parent
b6a946aa06
commit
d597c33eab
|
|
@ -561,6 +561,10 @@ export class BrowserManager {
|
||||||
* Use this for operations that work on both Page and Frame (locator, evaluate, etc.).
|
* Use this for operations that work on both Page and Frame (locator, evaluate, etc.).
|
||||||
*/
|
*/
|
||||||
getActiveFrameOrPage(): import('playwright').Page | import('playwright').Frame {
|
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();
|
return this.activeFrame ?? this.getPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -219,7 +219,9 @@ export async function handleMetaCommand(
|
||||||
if (!Array.isArray(commands)) throw new Error('not array');
|
if (!Array.isArray(commands)) throw new Error('not array');
|
||||||
} catch {
|
} catch {
|
||||||
// Fallback: pipe-delimited format "goto url | click @e5 | snapshot -ic"
|
// 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[] = [];
|
const results: string[] = [];
|
||||||
|
|
@ -478,7 +480,11 @@ export async function handleMetaCommand(
|
||||||
if (action === 'load') {
|
if (action === 'load') {
|
||||||
if (!fs.existsSync(statePath)) throw new Error(`State not found: ${statePath}`);
|
if (!fs.existsSync(statePath)) throw new Error(`State not found: ${statePath}`);
|
||||||
const data = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
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)
|
// Close existing pages, then restore (replace, not merge)
|
||||||
|
bm.setFrame(null);
|
||||||
await bm.closeAllPages();
|
await bm.closeAllPages();
|
||||||
await bm.restoreState({
|
await bm.restoreState({
|
||||||
cookies: data.cookies,
|
cookies: data.cookies,
|
||||||
|
|
@ -516,6 +522,7 @@ export async function handleMetaCommand(
|
||||||
const locator = 'locator' in resolved ? resolved.locator : page.locator(resolved.selector);
|
const locator = 'locator' in resolved ? resolved.locator : page.locator(resolved.selector);
|
||||||
const elementHandle = await locator.elementHandle({ timeout: 5000 });
|
const elementHandle = await locator.elementHandle({ timeout: 5000 });
|
||||||
frame = await elementHandle?.contentFrame() ?? null;
|
frame = await elementHandle?.contentFrame() ?? null;
|
||||||
|
await elementHandle?.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!frame) throw new Error(`Frame not found: ${target}`);
|
if (!frame) throw new Error(`Frame not found: ${target}`);
|
||||||
|
|
|
||||||
|
|
@ -208,11 +208,11 @@ export async function handleSnapshot(
|
||||||
|
|
||||||
let locator: Locator;
|
let locator: Locator;
|
||||||
if (opts.selector) {
|
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,
|
name: node.name || undefined,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
locator = page.getByRole(node.role as any, {
|
locator = target.getByRole(node.role as any, {
|
||||||
name: node.name || undefined,
|
name: node.name || undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -236,7 +236,7 @@ export async function handleSnapshot(
|
||||||
// ─── Cursor-interactive scan (-C) ─────────────────────────
|
// ─── Cursor-interactive scan (-C) ─────────────────────────
|
||||||
if (opts.cursorInteractive) {
|
if (opts.cursorInteractive) {
|
||||||
try {
|
try {
|
||||||
const cursorElements = await page.evaluate(() => {
|
const cursorElements = await target.evaluate(() => {
|
||||||
const STANDARD_INTERACTIVE = new Set([
|
const STANDARD_INTERACTIVE = new Set([
|
||||||
'A', 'BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'SUMMARY', 'DETAILS',
|
'A', 'BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'SUMMARY', 'DETAILS',
|
||||||
]);
|
]);
|
||||||
|
|
@ -290,7 +290,7 @@ export async function handleSnapshot(
|
||||||
let cRefCounter = 1;
|
let cRefCounter = 1;
|
||||||
for (const elem of cursorElements) {
|
for (const elem of cursorElements) {
|
||||||
const ref = `c${cRefCounter++}`;
|
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 });
|
refMap.set(ref, { locator, role: 'cursor-interactive', name: elem.text });
|
||||||
output.push(`@${ref} [${elem.reason}] "${elem.text}"`);
|
output.push(`@${ref} [${elem.reason}] "${elem.text}"`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -258,7 +258,7 @@ export async function handleWriteCommand(
|
||||||
if ('locator' in resolved) {
|
if ('locator' in resolved) {
|
||||||
await resolved.locator.setInputFiles(filePaths);
|
await resolved.locator.setInputFiles(filePaths);
|
||||||
} else {
|
} else {
|
||||||
await page.locator(resolved.selector).setInputFiles(filePaths);
|
await target.locator(resolved.selector).setInputFiles(filePaths);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileInfo = filePaths.map(fp => {
|
const fileInfo = filePaths.map(fp => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue