mirror of
https://github.com/garrytan/gstack.git
synced 2026-08-09 15:56:02 +02:00
feat: network idle detection + chain pipe format
- Upgrade click/fill/select from domcontentloaded to networkidle wait (2s timeout, best-effort). Catches XHR/fetch triggered by interactions. - Add pipe-delimited format to chain as JSON fallback: $B chain 'goto url | click @e5 | snapshot -ic' - Add post-loop networkidle wait in chain when last command was a write. - Frame-aware: commands use target (getActiveFrameOrPage) for locator ops, page-only ops (goto/back/forward/reload) guard against frame context. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
9daf33c84b
commit
e497d996c5
@@ -18,9 +18,13 @@ export async function handleWriteCommand(
|
||||
bm: BrowserManager
|
||||
): Promise<string> {
|
||||
const page = bm.getPage();
|
||||
// Frame-aware target for locator-based operations (click, fill, etc.)
|
||||
const target = bm.getActiveFrameOrPage();
|
||||
const inFrame = bm.getFrame() !== null;
|
||||
|
||||
switch (command) {
|
||||
case 'goto': {
|
||||
if (inFrame) throw new Error('Cannot use goto inside a frame. Run \'frame main\' first.');
|
||||
const url = args[0];
|
||||
if (!url) throw new Error('Usage: browse goto <url>');
|
||||
await validateNavigationUrl(url);
|
||||
@@ -30,16 +34,19 @@ export async function handleWriteCommand(
|
||||
}
|
||||
|
||||
case 'back': {
|
||||
if (inFrame) throw new Error('Cannot use back inside a frame. Run \'frame main\' first.');
|
||||
await page.goBack({ waitUntil: 'domcontentloaded', timeout: 15000 });
|
||||
return `Back → ${page.url()}`;
|
||||
}
|
||||
|
||||
case 'forward': {
|
||||
if (inFrame) throw new Error('Cannot use forward inside a frame. Run \'frame main\' first.');
|
||||
await page.goForward({ waitUntil: 'domcontentloaded', timeout: 15000 });
|
||||
return `Forward → ${page.url()}`;
|
||||
}
|
||||
|
||||
case 'reload': {
|
||||
if (inFrame) throw new Error('Cannot use reload inside a frame. Run \'frame main\' first.');
|
||||
await page.reload({ waitUntil: 'domcontentloaded', timeout: 15000 });
|
||||
return `Reloaded ${page.url()}`;
|
||||
}
|
||||
@@ -73,15 +80,14 @@ export async function handleWriteCommand(
|
||||
if ('locator' in resolved) {
|
||||
await resolved.locator.click({ timeout: 5000 });
|
||||
} else {
|
||||
await page.click(resolved.selector, { timeout: 5000 });
|
||||
await target.locator(resolved.selector).click({ timeout: 5000 });
|
||||
}
|
||||
} catch (err: any) {
|
||||
// Enhanced error guidance: clicking <option> elements always fails (not visible / timeout)
|
||||
const isOption = 'locator' in resolved
|
||||
? await resolved.locator.evaluate(el => el.tagName === 'OPTION').catch(() => false)
|
||||
: await page.evaluate(
|
||||
(sel: string) => document.querySelector(sel)?.tagName === 'OPTION',
|
||||
(resolved as { selector: string }).selector
|
||||
: await target.locator(resolved.selector).evaluate(
|
||||
el => el.tagName === 'OPTION'
|
||||
).catch(() => false);
|
||||
if (isOption) {
|
||||
throw new Error(
|
||||
@@ -90,8 +96,8 @@ export async function handleWriteCommand(
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
// Wait briefly for any navigation/DOM update
|
||||
await page.waitForLoadState('domcontentloaded').catch(() => {});
|
||||
// Wait for network to settle (catches XHR/fetch triggered by clicks)
|
||||
await page.waitForLoadState('networkidle', { timeout: 2000 }).catch(() => {});
|
||||
return `Clicked ${selector} → now at ${page.url()}`;
|
||||
}
|
||||
|
||||
@@ -103,8 +109,10 @@ export async function handleWriteCommand(
|
||||
if ('locator' in resolved) {
|
||||
await resolved.locator.fill(value, { timeout: 5000 });
|
||||
} else {
|
||||
await page.fill(resolved.selector, value, { timeout: 5000 });
|
||||
await target.locator(resolved.selector).fill(value, { timeout: 5000 });
|
||||
}
|
||||
// Wait for network to settle (form validation XHRs)
|
||||
await page.waitForLoadState('networkidle', { timeout: 2000 }).catch(() => {});
|
||||
return `Filled ${selector}`;
|
||||
}
|
||||
|
||||
@@ -116,8 +124,10 @@ export async function handleWriteCommand(
|
||||
if ('locator' in resolved) {
|
||||
await resolved.locator.selectOption(value, { timeout: 5000 });
|
||||
} else {
|
||||
await page.selectOption(resolved.selector, value, { timeout: 5000 });
|
||||
await target.locator(resolved.selector).selectOption(value, { timeout: 5000 });
|
||||
}
|
||||
// Wait for network to settle (dropdown-triggered requests)
|
||||
await page.waitForLoadState('networkidle', { timeout: 2000 }).catch(() => {});
|
||||
return `Selected "${value}" in ${selector}`;
|
||||
}
|
||||
|
||||
@@ -128,7 +138,7 @@ export async function handleWriteCommand(
|
||||
if ('locator' in resolved) {
|
||||
await resolved.locator.hover({ timeout: 5000 });
|
||||
} else {
|
||||
await page.hover(resolved.selector, { timeout: 5000 });
|
||||
await target.locator(resolved.selector).hover({ timeout: 5000 });
|
||||
}
|
||||
return `Hovered ${selector}`;
|
||||
}
|
||||
@@ -154,11 +164,11 @@ export async function handleWriteCommand(
|
||||
if ('locator' in resolved) {
|
||||
await resolved.locator.scrollIntoViewIfNeeded({ timeout: 5000 });
|
||||
} else {
|
||||
await page.locator(resolved.selector).scrollIntoViewIfNeeded({ timeout: 5000 });
|
||||
await target.locator(resolved.selector).scrollIntoViewIfNeeded({ timeout: 5000 });
|
||||
}
|
||||
return `Scrolled ${selector} into view`;
|
||||
}
|
||||
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
||||
await target.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
||||
return 'Scrolled to bottom';
|
||||
}
|
||||
|
||||
@@ -183,7 +193,7 @@ export async function handleWriteCommand(
|
||||
if ('locator' in resolved) {
|
||||
await resolved.locator.waitFor({ state: 'visible', timeout });
|
||||
} else {
|
||||
await page.waitForSelector(resolved.selector, { timeout });
|
||||
await target.locator(resolved.selector).waitFor({ state: 'visible', timeout });
|
||||
}
|
||||
return `Element ${selector} appeared`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user