Skip to content

Commit 59a05ea

Browse files
committed
fix(web-integration): add browser agent page following
1 parent 82d76d9 commit 59a05ea

22 files changed

Lines changed: 851 additions & 232 deletions

apps/site/docs/en/automate-with-scripts-in-yaml.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@ web:
192192
# Whether to restrict page navigation to the current tab, optional, defaults to true.
193193
forceSameTabNavigation: <boolean>
194194

195+
# Whether to use BrowserAgent and automatically continue in newly opened pages, optional, defaults to false.
196+
# Cannot be used together with forceSameTabNavigation: true.
197+
autoFollowNewPage: <boolean>
198+
195199
# CDP endpoint, optional. Connects to an existing browser instance via CDP instead of launching a new one.
196200
# Mutually exclusive with bridgeMode.
197201
cdpEndpoint: ws://localhost:9222/devtools/browser

apps/site/docs/en/integrate-with-playwright.mdx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -267,14 +267,18 @@ After the command executes successfully, it will output: `Midscene - report file
267267

268268
Each Agent instance is bound to a single page. To make debugging easier, Midscene intercepts new tabs by default (for example, links with `target="_blank"`) and opens them in the current page.
269269

270-
If you want to restore opening in a new tab, set `forceSameTabNavigation` to `false`—but you’ll need to create a new Agent instance for each new tab.
270+
If you want to restore opening in a new tab while keeping the Agent on the original page, set `forceSameTabNavigation` to `false` and create a new Agent instance for each new tab yourself.
271+
272+
If subsequent actions should automatically continue in the newly opened tab, use `PlaywrightBrowserAgent` and enable `autoFollowNewPage`.
271273

272274
```typescript
273-
const mid = new PlaywrightAgent(page, {
274-
forceSameTabNavigation: false,
275+
const mid = new PlaywrightBrowserAgent(context, page, {
276+
autoFollowNewPage: true,
275277
});
276278
```
277279

280+
Use `PlaywrightBrowserAgent.create(context, options)` only when you do not already have a page. The factory uses `initialPage` when provided, otherwise it reuses the first existing context page or creates a new page.
281+
278282
### Connect Midscene Agent to a Remote Playwright Browser
279283

280284
:::info Example Project

apps/site/docs/en/integrate-with-puppeteer.mdx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,14 +106,18 @@ After the above command executes successfully, the console will output: `Midscen
106106

107107
Each Agent instance is bound to a single page. For easier debugging, Midscene intercepts new tabs by default (for example, links with `target="_blank"`) and opens them in the current page.
108108

109-
If you want to allow new tabs again, set `forceSameTabNavigation` to `false`—but you must create a new Agent instance for each new tab.
109+
If you want to allow new tabs again while keeping the Agent on the original page, set `forceSameTabNavigation` to `false` and create a new Agent instance for each new tab yourself.
110+
111+
If subsequent actions should automatically continue in the newly opened tab, use `PuppeteerBrowserAgent` and enable `autoFollowNewPage`.
110112

111113
```typescript
112-
const mid = new PuppeteerAgent(page, {
113-
forceSameTabNavigation: false,
114+
const mid = new PuppeteerBrowserAgent(browser, page, {
115+
autoFollowNewPage: true,
114116
});
115117
```
116118

119+
Use `PuppeteerBrowserAgent.create(browser, options)` only when you do not already have a page. The factory uses `initialPage` when provided, otherwise it reuses the first existing browser page or creates a new page.
120+
117121
### Connect Midscene Agent to a Remote Puppeteer Browser
118122

119123
:::info Example Project

apps/site/docs/en/web-api-reference.mdx

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,29 @@ In addition to the base agent options, Puppeteer exposes:
5656

5757
:::info
5858

59-
- One agent per page: by default (`forceSameTabNavigation: true`), Midscene opens new links in the current tab for easier debugging. Set it to `false` if you want new tabs, and create a new agent per tab.
59+
- One agent per page: by default (`forceSameTabNavigation: true`), Midscene opens new links in the current tab for easier debugging. Set it to `false` if you want normal new-tab behavior and create a new `PuppeteerAgent` for each page yourself. Use `PuppeteerBrowserAgent` when the same Agent should manage browser-level page switching.
6060
- For the full list of interaction methods, see [API reference (Common)](./api#interaction-methods).
6161

6262
:::
6363

64+
### Browser agent
65+
66+
Use `PuppeteerBrowserAgent` when one Midscene Agent should manage page switching inside a Puppeteer browser.
67+
68+
```ts
69+
const agent = new PuppeteerBrowserAgent(browser, page, {
70+
autoFollowNewPage: true,
71+
});
72+
```
73+
74+
- Constructor: `new PuppeteerBrowserAgent(browser, page, options?)` &mdash; Use this when you already have the initial page.
75+
- Factory: `PuppeteerBrowserAgent.create(browser, options?)` &mdash; Use this when you do not already have a page. It uses `initialPage` if provided, otherwise the first existing browser page, or creates a new page.
76+
- `initialPage: Page` &mdash; Initial Puppeteer page for the factory.
77+
- `autoFollowNewPage: boolean` &mdash; Automatically switch the active page when the browser opens a new page. Default `false`.
78+
- `newPageTimeout: number` &mdash; Timeout for `waitForNewPage`. Default `5000`.
79+
- `setActivePage(page: Page)` &mdash; Explicitly set which Puppeteer page the Browser Agent controls next.
80+
- `waitForNewPage(action?, options?)` &mdash; Wait for a newly opened page without implicitly switching the active page.
81+
6482
### Examples
6583

6684
#### Quick start
@@ -143,11 +161,29 @@ const agent = new PlaywrightAgent(page, {
143161

144162
:::info
145163

146-
- One agent per page: with `forceSameTabNavigation` (default `true`), Midscene intercepts new tabs for stability. Set it to `false` to allow new tabs and create a separate agent for each.
164+
- One agent per page: with `forceSameTabNavigation` (default `true`), Midscene intercepts new tabs for stability. Set it to `false` to allow normal new tabs and create a new `PlaywrightAgent` for each page yourself. Use `PlaywrightBrowserAgent` when the same Agent should manage browser-context-level page switching.
147165
- For the full list of interaction methods, see [API reference (Common)](./api#interaction-methods).
148166

149167
:::
150168

169+
### Browser agent
170+
171+
Use `PlaywrightBrowserAgent` when one Midscene Agent should manage page switching inside a Playwright browser context.
172+
173+
```ts
174+
const agent = new PlaywrightBrowserAgent(context, page, {
175+
autoFollowNewPage: true,
176+
});
177+
```
178+
179+
- Constructor: `new PlaywrightBrowserAgent(context, page, options?)` &mdash; Use this when you already have the initial page.
180+
- Factory: `PlaywrightBrowserAgent.create(context, options?)` &mdash; Use this when you do not already have a page. It uses `initialPage` if provided, otherwise the first existing context page, or creates a new page.
181+
- `initialPage: Page` &mdash; Initial Playwright page for the factory.
182+
- `autoFollowNewPage: boolean` &mdash; Automatically switch the active page when the context opens a new page. Default `false`.
183+
- `newPageTimeout: number` &mdash; Timeout for `waitForNewPage`. Default `5000`.
184+
- `setActivePage(page: Page)` &mdash; Explicitly set which Playwright page the Browser Agent controls next.
185+
- `waitForNewPage(action?, options?)` &mdash; Wait for a newly opened page without implicitly switching the active page.
186+
151187
### Examples
152188

153189
#### Quick start

apps/site/docs/zh/automate-with-scripts-in-yaml.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,10 @@ web:
191191
# 是否限制页面在当前 tab 打开,可选,默认 true
192192
forceSameTabNavigation: <boolean>
193193

194+
# 是否使用 BrowserAgent 并自动继续在新打开的页面中执行,可选,默认 false
195+
# 不能与 forceSameTabNavigation: true 同时使用
196+
autoFollowNewPage: <boolean>
197+
194198
# CDP 连接端点,可选。设置后通过 CDP 连接到已有浏览器实例,而非启动新浏览器。
195199
# 与 bridgeMode 互斥,不可同时使用。
196200
cdpEndpoint: ws://localhost:9222/devtools/browser

apps/site/docs/zh/integrate-with-playwright.mdx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -266,14 +266,18 @@ npx playwright test ./e2e/ebay-search.spec.ts
266266

267267
每个 Agent 实例都与对应的页面唯一绑定,为了方便开发者调试,Midscene 默认拦截了新 tab 的页面(如点击一个带有 `target="_blank"` 属性的链接),将其改为在当前页面打开。
268268

269-
如果你想恢复在新标签页打开的行为,你可以设置 `forceSameTabNavigation` 选项为 `false`,但相应的,你需要为新标签页创建一个 Agent 实例。
269+
如果你想恢复在新标签页打开的行为,同时让当前 Agent 仍留在原页面,可以设置 `forceSameTabNavigation``false`,并自行为每个新标签页创建新的 Agent 实例。
270+
271+
如果后续操作要自动继续在新打开的标签页中执行,请使用 `PlaywrightBrowserAgent` 并开启 `autoFollowNewPage`
270272

271273
```typescript
272-
const mid = new PlaywrightAgent(page, {
273-
forceSameTabNavigation: false,
274+
const mid = new PlaywrightBrowserAgent(context, page, {
275+
autoFollowNewPage: true,
274276
});
275277
```
276278

279+
仅当你手上还没有现成的 page 时,才需要使用 `PlaywrightBrowserAgent.create(context, options)`。这个工厂会优先使用 `initialPage`,否则复用 context 里的第一个页面,或者创建一个新页面。
280+
277281
### 连接远程 Playwright 浏览器并接入 Midscene Agent
278282

279283
:::info 示例项目

apps/site/docs/zh/integrate-with-puppeteer.mdx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,14 +106,18 @@ npx tsx demo.ts
106106

107107
每个 Agent 实例都与对应的页面唯一绑定,为了方便开发者调试,Midscene 默认拦截了新 tab 的页面(如点击一个带有 `target="_blank"` 属性的链接),将其改为在当前页面打开。
108108

109-
如果你想恢复在新标签页打开的行为,你可以设置 `forceSameTabNavigation` 选项为 `false`,但相应的,你需要为新标签页创建一个 Agent 实例。
109+
如果你想恢复在新标签页打开的行为,同时让当前 Agent 仍留在原页面,可以设置 `forceSameTabNavigation``false`,并自行为每个新标签页创建新的 Agent 实例。
110+
111+
如果后续操作要自动继续在新打开的标签页中执行,请使用 `PuppeteerBrowserAgent` 并开启 `autoFollowNewPage`
110112

111113
```typescript
112-
const mid = new PuppeteerAgent(page, {
113-
forceSameTabNavigation: false,
114+
const mid = new PuppeteerBrowserAgent(browser, page, {
115+
autoFollowNewPage: true,
114116
});
115117
```
116118

119+
仅当你手上还没有现成的 page 时,才需要使用 `PuppeteerBrowserAgent.create(browser, options)`。这个工厂会优先使用 `initialPage`,否则复用浏览器里的第一个页面,或者创建一个新页面。
120+
117121
### 连接远程 Puppeteer 浏览器并接入 Midscene Agent
118122

119123
:::info 示例项目

apps/site/docs/zh/web-api-reference.mdx

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,29 @@ const agent = new PuppeteerAgent(page, {
5656

5757
:::info
5858

59-
- 每个页面一个 Agent:默认情况下(`forceSameTabNavigation: true`)Midscene 会拦截新标签并在当前页打开,便于调试;若想保留新标签行为可设为 `false`并为每个标签创建新的 Agent。
59+
- 每个页面一个 Agent:默认情况下(`forceSameTabNavigation: true`)Midscene 会拦截新标签并在当前页打开,便于调试;若想保留浏览器原生的新标签行为可设为 `false`并自行给每个页面创建新的 `PuppeteerAgent`。如果需要同一个 Agent 管理浏览器级别的页面切换,请使用 `PuppeteerBrowserAgent`
6060
- 更多交互方法请参考 [API 参考(通用)](./api#interaction-methods)
6161

6262
:::
6363

64+
### Browser Agent
65+
66+
当一个 Midscene Agent 需要管理 Puppeteer 浏览器内的页面切换时,使用 `PuppeteerBrowserAgent`
67+
68+
```ts
69+
const agent = new PuppeteerBrowserAgent(browser, page, {
70+
autoFollowNewPage: true,
71+
});
72+
```
73+
74+
- 构造函数:`new PuppeteerBrowserAgent(browser, page, options?)` —— 已经有初始页面时使用。
75+
- 工厂方法:`PuppeteerBrowserAgent.create(browser, options?)` —— 手上还没有 page 时使用。它会优先使用 `initialPage`,否则复用浏览器里的第一个页面,或者创建一个新页面。
76+
- `initialPage: Page` —— 工厂方法使用的初始 Puppeteer 页面。
77+
- `autoFollowNewPage: boolean` —— 浏览器打开新页面时是否自动切换 active page,默认 `false`
78+
- `newPageTimeout: number` —— `waitForNewPage` 的超时时间,默认 `5000`
79+
- `setActivePage(page: Page)` —— 显式指定 Browser Agent 接下来控制哪个 Puppeteer 页面。
80+
- `waitForNewPage(action?, options?)` —— 等待新打开的页面,但不会隐式切换 active page。
81+
6482
### 示例
6583

6684
#### 快速上手
@@ -143,11 +161,29 @@ const agent = new PlaywrightAgent(page, {
143161

144162
:::info
145163

146-
- 每个页面一个 Agent:默认 `forceSameTabNavigation``true`,Midscene 会拦截新标签确保稳定性;如需新标签请设为 `false` 并为每个标签创建新的 Agent。
164+
- 每个页面一个 Agent:默认 `forceSameTabNavigation``true`,Midscene 会拦截新标签确保稳定性;如需浏览器原生新标签行为请设为 `false`,并自行给每个页面创建新的 `PlaywrightAgent`。如果需要同一个 Agent 管理 browser context 级别的页面切换,请使用 `PlaywrightBrowserAgent`
147165
- 更多交互方法请参考 [API 参考(通用)](./api#interaction-methods)
148166

149167
:::
150168

169+
### Browser Agent
170+
171+
当一个 Midscene Agent 需要管理 Playwright browser context 内的页面切换时,使用 `PlaywrightBrowserAgent`
172+
173+
```ts
174+
const agent = new PlaywrightBrowserAgent(context, page, {
175+
autoFollowNewPage: true,
176+
});
177+
```
178+
179+
- 构造函数:`new PlaywrightBrowserAgent(context, page, options?)` —— 已经有初始页面时使用。
180+
- 工厂方法:`PlaywrightBrowserAgent.create(context, options?)` —— 手上还没有 page 时使用。它会优先使用 `initialPage`,否则复用 context 里的第一个页面,或者创建一个新页面。
181+
- `initialPage: Page` —— 工厂方法使用的初始 Playwright 页面。
182+
- `autoFollowNewPage: boolean` —— context 打开新页面时是否自动切换 active page,默认 `false`
183+
- `newPageTimeout: number` —— `waitForNewPage` 的超时时间,默认 `5000`
184+
- `setActivePage(page: Page)` —— 显式指定 Browser Agent 接下来控制哪个 Playwright 页面。
185+
- `waitForNewPage(action?, options?)` —— 等待新打开的页面,但不会隐式切换 active page。
186+
151187
### 示例
152188

153189
#### 快速上手

packages/core/src/yaml.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,8 @@ export interface MidsceneYamlScriptWebEnv
162162
*/
163163
extraHTTPHeaders?: Record<string, string>;
164164

165-
forceSameTabNavigation?: boolean; // if track the newly opened tab, true for default in yaml script
165+
forceSameTabNavigation?: boolean; // if limit the new tab to the current page, true for default in yaml script
166+
autoFollowNewPage?: boolean; // if use BrowserAgent to follow newly opened pages, false for default
166167

167168
/**
168169
* Custom Chrome launch arguments (Puppeteer only, not supported in bridge mode).
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { getWebpackRequire } from '@/utils';
2+
import type { Page as PlaywrightPage } from 'playwright';
3+
import type { Page as PuppeteerPage } from 'puppeteer';
4+
import semver from 'semver';
5+
import {
6+
BROWSER_NAVIGATION_ERROR_PATTERN,
7+
forceChromeSelectRendering,
8+
} from '../puppeteer/base-page';
9+
10+
type BrowserRuntime = 'puppeteer' | 'playwright';
11+
12+
const browserRuntimeConfig: Record<
13+
BrowserRuntime,
14+
{
15+
displayName: string;
16+
packageName: string;
17+
minimumVersion: string;
18+
requirementLabel: string;
19+
}
20+
> = {
21+
puppeteer: {
22+
displayName: 'Puppeteer',
23+
packageName: 'puppeteer',
24+
minimumVersion: '24.6.0',
25+
requirementLabel: '> 24.6.0',
26+
},
27+
playwright: {
28+
displayName: 'Playwright',
29+
packageName: 'playwright',
30+
minimumVersion: '1.52.0',
31+
requirementLabel: '>= 1.52.0',
32+
},
33+
};
34+
35+
function getPackageVersion(packageName: string): string | null {
36+
try {
37+
const pkg = getWebpackRequire()(`${packageName}/package.json`);
38+
return pkg.version || null;
39+
} catch (error) {
40+
console.error(
41+
`[midscene:error] Failed to get ${packageName} version`,
42+
error,
43+
);
44+
return null;
45+
}
46+
}
47+
48+
export function isRetryableBrowserNavigationError(error: unknown): boolean {
49+
return (
50+
error instanceof Error &&
51+
BROWSER_NAVIGATION_ERROR_PATTERN.test(error.message)
52+
);
53+
}
54+
55+
export function applyForceChromeSelectRendering(
56+
page: PuppeteerPage | PlaywrightPage,
57+
runtime: BrowserRuntime,
58+
enabled?: boolean,
59+
): void {
60+
if (!enabled) {
61+
return;
62+
}
63+
64+
const config = browserRuntimeConfig[runtime];
65+
const version = getPackageVersion(config.packageName);
66+
if (version && !semver.gte(version, config.minimumVersion)) {
67+
console.warn(
68+
`[midscene:error] forceChromeSelectRendering requires ${config.displayName} ${config.requirementLabel}, but current version is ${version}. This feature may not work correctly.`,
69+
);
70+
}
71+
forceChromeSelectRendering(page);
72+
}

0 commit comments

Comments
 (0)