fix(snapshot): resolve element ids on the correct snapshot (#2295)

closes https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/2243

This probably regressed with the addition of third party developer tools
because the resolveCdpElementId was not moved from page to TextSnapshot.
This commit is contained in:
Alex Rudenko
2026-07-06 11:08:46 +02:00
committed by GitHub
parent 913308263b
commit b703f2ce20
5 changed files with 36 additions and 50 deletions
+2 -25
View File
@@ -296,7 +296,8 @@ export class McpPage implements ContextPage {
);
return `stashed-${index}`;
}
const cdpElementId = this.resolveCdpElementId(backendNodeId);
const cdpElementId =
this.textSnapshot?.resolveCdpElementId(backendNodeId);
if (!cdpElementId) {
logger?.(
`Could not get cdpElementId for backend node ${backendNodeId}`,
@@ -369,30 +370,6 @@ export class McpPage implements ContextPage {
return this.textSnapshot?.idToNode.get(uid);
}
resolveCdpElementId(cdpBackendNodeId: number): string | undefined {
if (!cdpBackendNodeId) {
logger?.('no cdpBackendNodeId');
return;
}
const snapshot = this.textSnapshot;
if (!snapshot) {
logger?.('no text snapshot');
return;
}
// TODO: index by backendNodeId instead.
const queue = [snapshot.root];
while (queue.length) {
const current = queue.pop()!;
if (current.backendNodeId === cdpBackendNodeId) {
return current.id;
}
for (const child of current.children) {
queue.push(child);
}
}
return;
}
async getDevToolsData(): Promise<DevToolsData> {
try {
logger?.('Getting DevTools UI data');
+3 -1
View File
@@ -652,7 +652,9 @@ export class McpResponse implements Response {
context,
this.#page,
),
elementIdResolver: this.#page.resolveCdpElementId.bind(this.#page),
elementIdResolver: this.#page.textSnapshot?.resolveCdpElementId.bind(
this.#page.textSnapshot,
),
});
if (!formatter.isValid()) {
throw new Error(
+20 -1
View File
@@ -133,7 +133,7 @@ export class TextSnapshot {
const data = options.devtoolsData ?? (await page.getDevToolsData());
if (data?.cdpBackendNodeId) {
snapshot.hasSelectedElement = true;
snapshot.selectedElementUid = page.resolveCdpElementId(
snapshot.selectedElementUid = snapshot.resolveCdpElementId(
data.cdpBackendNodeId,
);
}
@@ -148,6 +148,25 @@ export class TextSnapshot {
return snapshot;
}
resolveCdpElementId(cdpBackendNodeId: number): string | undefined {
if (!cdpBackendNodeId) {
logger?.('no cdpBackendNodeId');
return;
}
// TODO: index by backendNodeId instead.
const queue = [this.root];
while (queue.length) {
const current = queue.pop()!;
if (current.backendNodeId === cdpBackendNodeId) {
return current.id;
}
for (const child of current.children) {
queue.push(child);
}
}
return;
}
// ExtraHandles represent DOM nodes which might not be part of the accessibility tree, e.g. DOM nodes
// returned by third-party developer tools. We insert them into the tree by finding the closest ancestor
// in the tree and inserting the node as a child. The ancestor's child nodes are re-parented if necessary.
@@ -188,6 +188,9 @@ describe('snapshotFormatter', () => {
idToNode: new Map(),
hasSelectedElement: true,
verbose: false,
resolveCdpElementId() {
return undefined;
},
});
const formatted = formatter.toString();
@@ -222,6 +225,9 @@ describe('snapshotFormatter', () => {
idToNode: new Map(),
hasSelectedElement: true,
verbose: true,
resolveCdpElementId() {
return undefined;
},
});
const formatted = formatter.toString();
@@ -257,6 +263,9 @@ describe('snapshotFormatter', () => {
hasSelectedElement: true,
selectedElementUid: '1_1',
verbose: false,
resolveCdpElementId() {
return '1_1';
},
});
const formatted = formatter.toString();
+2 -23
View File
@@ -767,10 +767,6 @@ describe('thirdPartyDeveloperTools', () => {
};
});
const stub = sinon
.stub(page, 'resolveCdpElementId')
.returns('mock-uid');
await executeThirdPartyDeveloperTool.handler(
{
params: {
@@ -785,10 +781,8 @@ describe('thirdPartyDeveloperTools', () => {
assert.strictEqual(
response.responseLines[0],
JSON.stringify({uid: 'mock-uid'}, null, 2),
JSON.stringify({uid: '1_1'}, null, 2),
);
stub.restore();
},
undefined,
{categoryExperimentalThirdParty: true},
@@ -826,14 +820,6 @@ describe('thirdPartyDeveloperTools', () => {
};
});
const stubSnapshot = sinon
.stub(TextSnapshot, 'create')
.resolves({} as TextSnapshot);
const stubResolve = sinon
.stub(page, 'resolveCdpElementId')
.returns('mock-uid');
await executeThirdPartyDeveloperTool.handler(
{
params: {
@@ -846,17 +832,10 @@ describe('thirdPartyDeveloperTools', () => {
context,
);
assert.ok(
stubSnapshot.calledOnce,
'Expected TextSnapshot.create to be called',
);
assert.strictEqual(
response.responseLines[0],
JSON.stringify({uid: 'mock-uid'}, null, 2),
JSON.stringify({uid: '1_1'}, null, 2),
);
stubResolve.restore();
stubSnapshot.restore();
},
undefined,
{categoryExperimentalThirdParty: true},