mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-11-04 15:00:11 +00:00
fix: drop InlineTextBox nodes from text snapshots (#3202)
## Problem
In verbose snapshots (`interestingOnly: false`), Puppeteer serializes
`InlineTextBox` children of `StaticText` nodes. These nodes are layout
line fragments. They repeat the parent text and have no `backendNodeId`,
so no tool can target them.
They also all share one uid: the key `${loaderId}_${backendNodeId}`
resolves to `${loaderId}_undefined` for every one of them.
Example from the Wikipedia "Google Chrome" article:
```
uid=2_3 "Jump to content"
uid=2_2658 InlineTextBox "Jump "
uid=2_2658 InlineTextBox "to "
uid=2_2658 InlineTextBox "content"
```
## Fix
`TextSnapshot.assignIds` filters out `InlineTextBox` children when it
builds the tree. This removes them from the text output, the JSON
output, and `idToNode`.
## Results
Wikipedia "Google Chrome" article:
| Mode | Before | After |
|---|---|---|
| Normal | 8,054 lines / 661 KB | 8,054 lines / 661 KB |
| Verbose | 27,676 lines / 2,150 KB | 18,373 lines / 1,429 KB (-34%) |
## Test
Added a real-browser test in `tests/TextSnapshot.test.ts`. The test
depends on Puppeteer's actual serialization. It fails without the fix
and passes with it.
This commit is contained in:
1 parent
e08b9fa6aa
commit
f8c775a7d9
3 files changed
+42
-17
No files matched your search
+12
-1
@@ -14,6 +14,15 @@ import type {DevToolsData} from './tools/ToolDefinition.js';
|
||||
import type {TextSnapshotNode} from './types.js';
|
||||
import {logger} from './utils/logger.js';
|
||||
|
||||
/**
|
||||
* InlineTextBox nodes are layout line fragments of a StaticText node. They
|
||||
* repeat the parent text, have no backendNodeId and cannot be targeted by
|
||||
* tools. Puppeteer only serializes them for non-interesting-only snapshots.
|
||||
*/
|
||||
function isLayoutOnlyNode(node: SerializedAXNode): boolean {
|
||||
return node.role === 'InlineTextBox';
|
||||
}
|
||||
|
||||
export class TextSnapshot {
|
||||
static nextSnapshotId = 1;
|
||||
|
||||
@@ -94,7 +103,9 @@ export class TextSnapshot {
|
||||
...node,
|
||||
id,
|
||||
children: node.children
|
||||
? node.children.map(child => assignIds(child))
|
||||
? node.children
|
||||
.filter(child => !isLayoutOnlyNode(child))
|
||||
.map(child => assignIds(child))
|
||||
: [],
|
||||
};
|
||||
|
||||
|
||||
@@ -405,7 +405,6 @@ uid=1_0 RootWebArea "My test page" url="about:blank"
|
||||
uid=1_2 ignored
|
||||
uid=1_3 complementary
|
||||
uid=1_4 "test"
|
||||
InlineTextBox "test"
|
||||
|
||||
`;
|
||||
|
||||
@@ -432,13 +431,7 @@ exports[`McpResponse > returns verbose snapshot and structured content 2`] = `
|
||||
{
|
||||
"id": "1_4",
|
||||
"role": "StaticText",
|
||||
"name": "test",
|
||||
"children": [
|
||||
{
|
||||
"role": "InlineTextBox",
|
||||
"name": "test"
|
||||
}
|
||||
]
|
||||
"name": "test"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -467,7 +460,6 @@ uid=1_0 RootWebArea "My test page" url="about:blank"
|
||||
uid=1_2 ignored
|
||||
uid=1_3 complementary
|
||||
uid=1_4 "test"
|
||||
InlineTextBox "test"
|
||||
|
||||
`;
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ describe('TextSnapshot', () => {
|
||||
loaderId: '1',
|
||||
backendNodeId: 2,
|
||||
children: [
|
||||
createMockAXNode('InlineTextBox', {
|
||||
createMockAXNode('StaticText', {
|
||||
name: 'alpha',
|
||||
loaderId: '1',
|
||||
}),
|
||||
@@ -43,7 +43,7 @@ describe('TextSnapshot', () => {
|
||||
loaderId: '1',
|
||||
backendNodeId: 3,
|
||||
children: [
|
||||
createMockAXNode('InlineTextBox', {
|
||||
createMockAXNode('StaticText', {
|
||||
name: 'beta',
|
||||
loaderId: '1',
|
||||
}),
|
||||
@@ -54,21 +54,21 @@ describe('TextSnapshot', () => {
|
||||
page.pptrPage.accessibility.snapshot.resolves(mockRoot);
|
||||
|
||||
const snapshot = await TextSnapshot.create(page, {verbose: true});
|
||||
const inlineNodes = [...snapshot.idToNode.values()].filter(
|
||||
node => node.role === 'InlineTextBox',
|
||||
const textNodes = [...snapshot.idToNode.values()].filter(
|
||||
node => node.role === 'StaticText',
|
||||
);
|
||||
|
||||
assert.strictEqual(inlineNodes.length, 0);
|
||||
assert.strictEqual(textNodes.length, 0);
|
||||
|
||||
const paragraph1 = snapshot.idToNode.get('1_1');
|
||||
assert.strictEqual(paragraph1?.role, 'paragraph');
|
||||
assert.strictEqual(paragraph1?.children[0]?.role, 'InlineTextBox');
|
||||
assert.strictEqual(paragraph1?.children[0]?.role, 'StaticText');
|
||||
assert.strictEqual(paragraph1?.children[0]?.id, '');
|
||||
assert.strictEqual(paragraph1?.children[0]?.name, 'alpha');
|
||||
|
||||
const paragraph2 = snapshot.idToNode.get('1_2');
|
||||
assert.strictEqual(paragraph2?.role, 'paragraph');
|
||||
assert.strictEqual(paragraph2?.children[0]?.role, 'InlineTextBox');
|
||||
assert.strictEqual(paragraph2?.children[0]?.role, 'StaticText');
|
||||
assert.strictEqual(paragraph2?.children[0]?.id, '');
|
||||
assert.strictEqual(paragraph2?.children[0]?.name, 'beta');
|
||||
|
||||
@@ -104,6 +104,28 @@ describe('TextSnapshot', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('drops InlineTextBox nodes from verbose snapshots', async () => {
|
||||
await withMcpContext(async (_response, context) => {
|
||||
const page = context.getSelectedMcpPage();
|
||||
await page.pptrPage.setContent(
|
||||
html`<p style="width: 40px">Some wrapped text content</p>`,
|
||||
);
|
||||
|
||||
const snapshot = await TextSnapshot.create(page, {verbose: true});
|
||||
|
||||
let staticTextNode: TextSnapshotNode | undefined;
|
||||
for (const node of snapshot.idToNode.values()) {
|
||||
assert.notStrictEqual(node.role, 'InlineTextBox');
|
||||
if (node.role === 'StaticText') {
|
||||
staticTextNode = node;
|
||||
}
|
||||
}
|
||||
assert.ok(staticTextNode, 'StaticText node should be in the snapshot');
|
||||
assert.strictEqual(staticTextNode.name, 'Some wrapped text content');
|
||||
assert.deepStrictEqual(staticTextNode.children, []);
|
||||
});
|
||||
});
|
||||
|
||||
it('inserts extraHandles into the snapshot correctly', async () => {
|
||||
await withMcpContext(async (_response, context) => {
|
||||
const page = context.getSelectedMcpPage();
|
||||
|
||||
Reference in new issue
Block a user