diff --git a/src/McpContext.ts b/src/McpContext.ts index 1c3c988d..0bdf0663 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -42,6 +42,10 @@ export interface TextSnapshot { idToNode: Map; snapshotId: string; selectedElementUid?: string; + // It might happen that there is a selected element, but it is not part of the + // snapshot. This flag indicates if there is any selected element. + hasSelectedElement: boolean; + verbose: boolean; } interface McpContextOptions { @@ -529,9 +533,12 @@ export class McpContext implements Context { root: rootNodeWithId, snapshotId: String(snapshotId), idToNode, + hasSelectedElement: false, + verbose, }; const data = devtoolsData ?? (await this.getDevToolsData()); if (data?.cdpBackendNodeId) { + this.#textSnapshot.hasSelectedElement = true; this.#textSnapshot.selectedElementUid = this.resolveCdpElementId( data?.cdpBackendNodeId, ); diff --git a/src/formatters/snapshotFormatter.ts b/src/formatters/snapshotFormatter.ts index 42d567e5..5018381e 100644 --- a/src/formatters/snapshotFormatter.ts +++ b/src/formatters/snapshotFormatter.ts @@ -10,7 +10,20 @@ export function formatSnapshotNode( snapshot?: TextSnapshot, depth = 0, ): string { - let result = ''; + const chunks: string[] = []; + + if (depth === 0) { + // Top-level content of the snapshot. + if ( + snapshot?.verbose && + snapshot?.hasSelectedElement && + !snapshot.selectedElementUid + ) { + chunks.push(`Note: there is a selected element in the DevTools Elements panel but it is not included into the current a11y tree snapshot. +Get a verbose snapshot to include all elements if you are interested in the selected element.\n\n`); + } + } + const attributes = getAttributes(root); const line = ' '.repeat(depth * 2) + @@ -19,13 +32,13 @@ export function formatSnapshotNode( ? ' [selected in the DevTools Elements panel]' : '') + '\n'; - result += line; + chunks.push(line); for (const child of root.children) { - result += formatSnapshotNode(child, snapshot, depth + 1); + chunks.push(formatSnapshotNode(child, snapshot, depth + 1)); } - return result; + return chunks.join(''); } function getAttributes(serializedAXNodeRoot: TextSnapshotNode): string[] { diff --git a/tests/formatters/snapshotFormatter.test.js.snapshot b/tests/formatters/snapshotFormatter.test.js.snapshot new file mode 100644 index 00000000..bb2e1ccb --- /dev/null +++ b/tests/formatters/snapshotFormatter.test.js.snapshot @@ -0,0 +1,20 @@ +exports[`snapshotFormatter > does not include a note if the snapshot is already verbose 1`] = ` +Note: there is a selected element in the DevTools Elements panel but it is not included into the current a11y tree snapshot. +Get a verbose snapshot to include all elements if you are interested in the selected element. + +uid=1_1 checkbox "checkbox" checked + uid=1_2 statictext "text" + +`; + +exports[`snapshotFormatter > formats with DevTools data included into a snapshot 1`] = ` +uid=1_1 checkbox "checkbox" checked [selected in the DevTools Elements panel] + uid=1_2 statictext "text" + +`; + +exports[`snapshotFormatter > formats with DevTools data not included into a snapshot 1`] = ` +uid=1_1 checkbox "checkbox" checked + uid=1_2 statictext "text" + +`; diff --git a/tests/formatters/snapshotFormatter.test.ts b/tests/formatters/snapshotFormatter.test.ts index 1232929f..3ec8d5df 100644 --- a/tests/formatters/snapshotFormatter.test.ts +++ b/tests/formatters/snapshotFormatter.test.ts @@ -148,4 +148,104 @@ describe('snapshotFormatter', () => { `, ); }); + + it('formats with DevTools data not included into a snapshot', t => { + const node: TextSnapshotNode = { + id: '1_1', + role: 'checkbox', + name: 'checkbox', + checked: true, + children: [ + { + id: '1_2', + role: 'statictext', + name: 'text', + children: [], + elementHandle: async (): Promise | null> => { + return null; + }, + }, + ], + elementHandle: async (): Promise | null> => { + return null; + }, + }; + + const formatted = formatSnapshotNode(node, { + snapshotId: '1', + root: node, + idToNode: new Map(), + hasSelectedElement: true, + verbose: false, + }); + + t.assert.snapshot?.(formatted); + }); + + it('does not include a note if the snapshot is already verbose', t => { + const node: TextSnapshotNode = { + id: '1_1', + role: 'checkbox', + name: 'checkbox', + checked: true, + children: [ + { + id: '1_2', + role: 'statictext', + name: 'text', + children: [], + elementHandle: async (): Promise | null> => { + return null; + }, + }, + ], + elementHandle: async (): Promise | null> => { + return null; + }, + }; + + const formatted = formatSnapshotNode(node, { + snapshotId: '1', + root: node, + idToNode: new Map(), + hasSelectedElement: true, + verbose: true, + }); + + t.assert.snapshot?.(formatted); + }); + + it('formats with DevTools data included into a snapshot', t => { + const node: TextSnapshotNode = { + id: '1_1', + role: 'checkbox', + name: 'checkbox', + checked: true, + children: [ + { + id: '1_2', + role: 'statictext', + name: 'text', + children: [], + elementHandle: async (): Promise | null> => { + return null; + }, + }, + ], + elementHandle: async (): Promise | null> => { + return null; + }, + }; + + const formatted = formatSnapshotNode(node, { + snapshotId: '1', + root: node, + idToNode: new Map(), + hasSelectedElement: true, + selectedElementUid: '1_1', + verbose: false, + }); + + t.assert.snapshot?.(formatted); + }); });