diff --git a/packages/playwright-core/src/tools/backend/find.ts b/packages/playwright-core/src/tools/backend/find.ts index c29b01dfbf9f6..4c7da1461d275 100644 --- a/packages/playwright-core/src/tools/backend/find.ts +++ b/packages/playwright-core/src/tools/backend/find.ts @@ -92,21 +92,26 @@ const find = defineTabTool({ path.add(ancestor); } - const snippets = windows.map(window => { - const indices = ancestorIndices(lines, indents, window.start); + // Render all the windows into a single tree, so that a path shared by several matches is + // printed once instead of being repeated for every one of them. + const included = new Set(); + for (const window of windows) { + for (const ancestor of ancestorIndices(lines, indents, window.start)) + included.add(ancestor); for (let i = window.start; i <= window.end; i++) - indices.push(i); - const out: string[] = []; - for (let i = 0; i < indices.length; i++) { - const index = indices[i]; - if (i > 0 && index > indices[i - 1] + 1 && !path.has(index) && !path.has(indices[i - 1])) - out.push(' '.repeat(indents[index]) + '...'); - out.push(lines[index]); - } - return out.join('\n'); - }); + included.add(i); + } + + const indices = [...included].sort((a, b) => a - b); + const out: string[] = []; + for (let i = 0; i < indices.length; i++) { + const index = indices[i]; + if (i > 0 && index > indices[i - 1] + 1 && !path.has(index) && !path.has(indices[i - 1])) + out.push(' '.repeat(indents[index]) + '...'); + out.push(lines[index]); + } const matchWord = matchedLines.length === 1 ? 'match' : 'matches'; - response.addTextResult(`Found ${matchedLines.length} ${matchWord} for ${query}:\n\n${snippets.join('\n\n----\n\n')}`); + response.addTextResult(`Found ${matchedLines.length} ${matchWord} for ${query}:\n\n${out.join('\n')}`); }, }); diff --git a/tests/mcp/find.spec.ts b/tests/mcp/find.spec.ts index 1b4ee9f93bc50..9b0fd3f372ad3 100644 --- a/tests/mcp/find.spec.ts +++ b/tests/mcp/find.spec.ts @@ -132,6 +132,51 @@ test('browser_find marks gaps within off-path context with an ellipsis', async ( }); }); +const repeatedPage = ` +
+
+ +
+
+`; + +test('browser_find prints the path shared by several matches once', async ({ client, server }) => { + server.setContent('/', repeatedPage, 'text/html'); + await client.callTool({ name: 'browser_navigate', arguments: { url: server.PREFIX } }); + + const response = await client.callTool({ + name: 'browser_find', + arguments: { text: 'Target' }, + }); + + expect(response).toHaveResponse({ + result: expect.stringContaining(`Found 2 matches for "Target": + +- main [ref=e2]: + - region "Sidebar" [ref=e3]: + - navigation "Primary" [ref=e4]: + - list [ref=e5]: + - listitem [ref=e6]:`), + }); + // Both matches live in the same tree, so the path above them is printed once and the gap + // between the two context windows is marked with an ellipsis. + expect(response).toHaveResponse({ + result: expect.stringContaining(` - listitem [ref=e9]: filler two + ... + - listitem [ref=e11]: filler four`), + }); +}); + test('browser_find is case-insensitive for text', async ({ client, server }) => { server.setContent('/', listPage, 'text/html'); await client.callTool({ name: 'browser_navigate', arguments: { url: server.PREFIX } });