返回提交历史
Modified
package-lock.json
+2
-2
Modified
package.json
+1
-1
Modified
references/media-and-embedded.md
+4
-0
Modified
scripts/browser-session.mjs
+6
-1
Modified
scripts/self-test.mjs
+8
-2
XFEstudio/BrowserDebugger
Fix passive CDP dialog observation and document hidden WebView2 pages
0655a36
代码差异
5 个文件
+21
-6
@@ -1,12 +1,12 @@
1
1
{
2
2
"name": "browser-debugger-skill",
3
"version": "1.2.0",
3
"version": "1.2.1",
4
4
"lockfileVersion": 3,
5
5
"requires": true,
6
6
"packages": {
7
7
"": {
8
8
"name": "browser-debugger-skill",
9
"version": "1.2.0",
9
"version": "1.2.1",
10
10
"dependencies": {
11
11
"playwright-core": "1.64.1"
12
12
},
@@ -1,6 +1,6 @@
1
1
{
2
2
"name": "browser-debugger-skill",
3
"version": "1.2.0",
3
"version": "1.2.1",
4
4
"private": true,
5
5
"type": "module",
6
6
"engines": { "node": ">=22" },
@@ -47,4 +47,8 @@ var browser = await playwright.Chromium.ConnectOverCDPAsync($"http://127.0.0.1:{
47
47
48
48
Playwright .NET 把 `EvaluateAsync<T>` 直接映射到 C# DTO 时,要验证 JSON 字段名映射,不能因为反序列化不匹配而把默认空列表当作网页无内容。可读取 `JsonElement` 后用明确的 `JsonSerializerOptions` 转换,并用真实浏览器集成测试覆盖。应用的去敏网络诊断日志通常只有 URL/状态,不能称为保存了请求/响应体的完整 HAR。
49
49
50
需要纯原生界面、后台多页面时,`Context.NewPageAsync` / CDP `Target.createTarget` 在 WebView2 中可能创建带地址栏的可见窗口。应由应用的 STA 消息泵在同一环境下创建多个 message-only HWND 的 `CoreWebView2Controller`,通过唯一初始 URL 或实际 targetId 关联已有 Playwright page;记录器只附加现成目标。校验完成状态时使用独立隐藏目录页面,避免刷新正在播放的课程。释放页面时由拥有 controller 的进程关闭它,避免累积后台 controller。
51
52
一个浏览器同时连接多个 Playwright 客户端时,未注册 `dialog` 监听的观察客户端会自动 dismiss 弹窗,可能与主客户端竞争并使连接出错。附加模式 helper 对现有 context 安装被动监听,保留弹窗处理权给宿主或显式动作。需要处理弹窗时仍在触发前注册自己的接受/取消逻辑;不能把监听器注册成功当成站点流程成功。单纯拥有 WebView2 环境的辅助进程无需再维持第二个 Playwright 连接。
53
50
54
参考:[Playwright .NET WebView2](https://playwright.dev/dotnet/docs/webview2)、[WebView2 Environment Options](https://learn.microsoft.com/en-us/dotnet/api/microsoft.web.webview2.core.corewebview2environmentoptions)、[HTMLMediaElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement)。
@@ -44,6 +44,8 @@ export async function openSession(options = {}) {
44
44
catch (error) { if (error.code !== 'ENOENT') throw error; }
45
45
await mkdir(outputDir, { recursive: true });
46
46
let browser, context, protocol, recorder, endpoint, version;
47
const passiveDialogContexts = [];
48
const passiveDialog = () => {}; // An observing CDP client must not dismiss another client's dialogs.
47
49
const owned = !options.endpoint;
48
50
let profileDir;
49
51
try {
@@ -66,6 +68,7 @@ export async function openSession(options = {}) {
66
68
endpoint = options.endpoint;
67
69
browser = await chromium.connectOverCDP(endpoint);
68
70
const contexts = browser.contexts();
71
for (const candidate of contexts) { candidate.on('dialog', passiveDialog); passiveDialogContexts.push(candidate); }
69
72
const index = options.contextIndex ?? 0;
70
73
context = contexts[index];
71
74
if (!context) throw new Error(`No existing browser context at index ${index}. Available: ${contexts.length}`);
@@ -78,8 +81,9 @@ export async function openSession(options = {}) {
78
81
await recorder.start();
79
82
const existingPages = context.pages();
80
83
let page = existingPages.find(page => /^https?:/i.test(page.url())) ?? existingPages[0];
84
if (!page && !owned) throw new Error('The attached browser has no existing page. Let its owning application create one before attaching; WebView2 Target.createTarget may open a visible external window.');
81
85
if (!page) page = await context.newPage();
82
const session = new BrowserDebugSession({ browser, context, page, protocol, recorder, outputDir, workDir, endpoint, owned, profileDir, version });
86
const session = new BrowserDebugSession({ browser, context, page, protocol, recorder, outputDir, workDir, endpoint, owned, profileDir, version, passiveDialogContexts, passiveDialog });
83
87
await session.pageSessionId(page);
84
88
await writeFile(path.join(outputDir, 'session.json'), `${JSON.stringify({ endpoint, owned, profileDir, outputDir, browser: version, captureStartedAt: recorder.startedAt }, null, 2)}\n`, 'utf8');
85
89
if (/^https?:/i.test(endpoint)) {
@@ -155,6 +159,7 @@ export class BrowserDebugSession {
155
159
if (this.protocol.connected) await this.protocol.close();
156
160
if (this.owned) await this.context.close();
157
161
else await this.browser.close(); // connectOverCDP: disconnect; do not send Browser.close.
162
for (const candidate of this.passiveDialogContexts) candidate.off('dialog', this.passiveDialog);
158
163
this.closed = true;
159
164
}
160
165
this.finalSummary = summary;
@@ -155,7 +155,7 @@ try {
155
155
const recorder = new MediaRecorder(stream, { mimeType: 'video/webm' });
156
156
recorder.ondataavailable = event => chunks.push(event.data);
157
157
const stopped = new Promise(resolve => recorder.onstop = resolve);
158
recorder.start(); await new Promise(resolve => setTimeout(resolve, 2200)); recorder.stop(); await stopped;
158
recorder.start(); await new Promise(resolve => setTimeout(resolve, 6000)); recorder.stop(); await stopped;
159
159
clearInterval(paint); stream.getTracks().forEach(track => track.stop());
160
160
const video = document.createElement('video'); video.width = 320; video.height = 180; video.muted = true;
161
161
video.src = URL.createObjectURL(new Blob(chunks, { type: 'video/webm' })); document.body.append(video);
@@ -168,7 +168,7 @@ try {
168
168
const actualVideo = mediaState.media.find(item => item.tag === 'video');
169
169
assert.ok(actualVideo.frameUrl.endsWith('/frame'));
170
170
assert.equal(actualVideo.playbackRate, 1);
171
const evidence = await captureMediaEvidence(page, { outputDir: path.join(root, 'media'), seconds: 1.4, intervalSeconds: .3, pollMs: 100 });
171
const evidence = await captureMediaEvidence(page, { outputDir: path.join(root, 'media'), seconds: 3.5, intervalSeconds: .3, pollMs: 100 });
172
172
assert.ok(evidence.images >= 2, `Expected progressing media frames, got ${evidence.images}`);
173
173
const manifest = await readFile(evidence.manifestPath, 'utf8');
174
174
assert.ok(manifest.includes('media-evidence-caption'));
@@ -182,6 +182,12 @@ try {
182
182
attached = await openSession({ endpoint: session.endpoint, workDir: root, outputDir: path.join(root, 'attached') });
183
183
attached.page = attached.context.pages().find(page => page.url() === `${base}/`);
184
184
assert.ok(attached.page, 'Select the actual fixture tab rather than browser-internal pages or the popup.');
185
const dialogOpened = new Promise(resolve => page.once('dialog', resolve));
186
const dialogResult = page.evaluate(() => confirm('attached-observer-dialog'));
187
const ownerDialog = await dialogOpened;
188
await new Promise(resolve => setTimeout(resolve, 200));
189
await ownerDialog.accept();
190
assert.equal(await dialogResult, true, 'An attached observer must not automatically dismiss dialogs belonging to the primary client.');
185
191
await attached.page.evaluate(() => fetch('/api/attached').then(r => r.json()));
186
192
await attached.close();
187
193
assert.equal(await page.evaluate(() => 40 + 2), 42, 'Disconnecting the attached client must preserve the original browser.');