Skip to content

Commit 80e77fd

Browse files
authored
fix: include a note about selected elements missing from the snapshot (#593)
Closes #586
1 parent 2eaf268 commit 80e77fd

File tree

4 files changed

+144
-4
lines changed

4 files changed

+144
-4
lines changed

src/McpContext.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ export interface TextSnapshot {
4242
idToNode: Map<string, TextSnapshotNode>;
4343
snapshotId: string;
4444
selectedElementUid?: string;
45+
// It might happen that there is a selected element, but it is not part of the
46+
// snapshot. This flag indicates if there is any selected element.
47+
hasSelectedElement: boolean;
48+
verbose: boolean;
4549
}
4650

4751
interface McpContextOptions {
@@ -529,9 +533,12 @@ export class McpContext implements Context {
529533
root: rootNodeWithId,
530534
snapshotId: String(snapshotId),
531535
idToNode,
536+
hasSelectedElement: false,
537+
verbose,
532538
};
533539
const data = devtoolsData ?? (await this.getDevToolsData());
534540
if (data?.cdpBackendNodeId) {
541+
this.#textSnapshot.hasSelectedElement = true;
535542
this.#textSnapshot.selectedElementUid = this.resolveCdpElementId(
536543
data?.cdpBackendNodeId,
537544
);

src/formatters/snapshotFormatter.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,20 @@ export function formatSnapshotNode(
1010
snapshot?: TextSnapshot,
1111
depth = 0,
1212
): string {
13-
let result = '';
13+
const chunks: string[] = [];
14+
15+
if (depth === 0) {
16+
// Top-level content of the snapshot.
17+
if (
18+
snapshot?.verbose &&
19+
snapshot?.hasSelectedElement &&
20+
!snapshot.selectedElementUid
21+
) {
22+
chunks.push(`Note: there is a selected element in the DevTools Elements panel but it is not included into the current a11y tree snapshot.
23+
Get a verbose snapshot to include all elements if you are interested in the selected element.\n\n`);
24+
}
25+
}
26+
1427
const attributes = getAttributes(root);
1528
const line =
1629
' '.repeat(depth * 2) +
@@ -19,13 +32,13 @@ export function formatSnapshotNode(
1932
? ' [selected in the DevTools Elements panel]'
2033
: '') +
2134
'\n';
22-
result += line;
35+
chunks.push(line);
2336

2437
for (const child of root.children) {
25-
result += formatSnapshotNode(child, snapshot, depth + 1);
38+
chunks.push(formatSnapshotNode(child, snapshot, depth + 1));
2639
}
2740

28-
return result;
41+
return chunks.join('');
2942
}
3043

3144
function getAttributes(serializedAXNodeRoot: TextSnapshotNode): string[] {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
exports[`snapshotFormatter > does not include a note if the snapshot is already verbose 1`] = `
2+
Note: there is a selected element in the DevTools Elements panel but it is not included into the current a11y tree snapshot.
3+
Get a verbose snapshot to include all elements if you are interested in the selected element.
4+
5+
uid=1_1 checkbox "checkbox" checked
6+
uid=1_2 statictext "text"
7+
8+
`;
9+
10+
exports[`snapshotFormatter > formats with DevTools data included into a snapshot 1`] = `
11+
uid=1_1 checkbox "checkbox" checked [selected in the DevTools Elements panel]
12+
uid=1_2 statictext "text"
13+
14+
`;
15+
16+
exports[`snapshotFormatter > formats with DevTools data not included into a snapshot 1`] = `
17+
uid=1_1 checkbox "checkbox" checked
18+
uid=1_2 statictext "text"
19+
20+
`;

tests/formatters/snapshotFormatter.test.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,104 @@ describe('snapshotFormatter', () => {
148148
`,
149149
);
150150
});
151+
152+
it('formats with DevTools data not included into a snapshot', t => {
153+
const node: TextSnapshotNode = {
154+
id: '1_1',
155+
role: 'checkbox',
156+
name: 'checkbox',
157+
checked: true,
158+
children: [
159+
{
160+
id: '1_2',
161+
role: 'statictext',
162+
name: 'text',
163+
children: [],
164+
elementHandle: async (): Promise<ElementHandle<Element> | null> => {
165+
return null;
166+
},
167+
},
168+
],
169+
elementHandle: async (): Promise<ElementHandle<Element> | null> => {
170+
return null;
171+
},
172+
};
173+
174+
const formatted = formatSnapshotNode(node, {
175+
snapshotId: '1',
176+
root: node,
177+
idToNode: new Map(),
178+
hasSelectedElement: true,
179+
verbose: false,
180+
});
181+
182+
t.assert.snapshot?.(formatted);
183+
});
184+
185+
it('does not include a note if the snapshot is already verbose', t => {
186+
const node: TextSnapshotNode = {
187+
id: '1_1',
188+
role: 'checkbox',
189+
name: 'checkbox',
190+
checked: true,
191+
children: [
192+
{
193+
id: '1_2',
194+
role: 'statictext',
195+
name: 'text',
196+
children: [],
197+
elementHandle: async (): Promise<ElementHandle<Element> | null> => {
198+
return null;
199+
},
200+
},
201+
],
202+
elementHandle: async (): Promise<ElementHandle<Element> | null> => {
203+
return null;
204+
},
205+
};
206+
207+
const formatted = formatSnapshotNode(node, {
208+
snapshotId: '1',
209+
root: node,
210+
idToNode: new Map(),
211+
hasSelectedElement: true,
212+
verbose: true,
213+
});
214+
215+
t.assert.snapshot?.(formatted);
216+
});
217+
218+
it('formats with DevTools data included into a snapshot', t => {
219+
const node: TextSnapshotNode = {
220+
id: '1_1',
221+
role: 'checkbox',
222+
name: 'checkbox',
223+
checked: true,
224+
children: [
225+
{
226+
id: '1_2',
227+
role: 'statictext',
228+
name: 'text',
229+
children: [],
230+
elementHandle: async (): Promise<ElementHandle<Element> | null> => {
231+
return null;
232+
},
233+
},
234+
],
235+
elementHandle: async (): Promise<ElementHandle<Element> | null> => {
236+
return null;
237+
},
238+
};
239+
240+
const formatted = formatSnapshotNode(node, {
241+
snapshotId: '1',
242+
root: node,
243+
idToNode: new Map(),
244+
hasSelectedElement: true,
245+
selectedElementUid: '1_1',
246+
verbose: false,
247+
});
248+
249+
t.assert.snapshot?.(formatted);
250+
});
151251
});

0 commit comments

Comments
 (0)