XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

BrowserDebugger

【Skill】浏览器调试工具

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/BrowserDebugger

Fix passive CDP dialog observation and document hidden WebView2 pages

0655a36
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

5 个文件 +21 -6
Modified package-lock.json +2 -2
@@ -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 },
Modified package.json +1 -1
@@ -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" },
Modified references/media-and-embedded.md +4 -0
@@ -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)。
Modified scripts/browser-session.mjs +6 -1
@@ -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;
Modified scripts/self-test.mjs +8 -2
@@ -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.');