Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 18 additions & 13 deletions packages/playwright-core/src/tools/backend/find.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>();
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')}`);
},
});

Expand Down
45 changes: 45 additions & 0 deletions tests/mcp/find.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,51 @@ test('browser_find marks gaps within off-path context with an ellipsis', async (
});
});

const repeatedPage = `
<main>
<section aria-label="Sidebar">
<nav aria-label="Primary">
<ul>
<li><a href="/a">Target A</a></li>
<li>filler one</li>
<li>filler two</li>
<li>filler three</li>
<li>filler four</li>
<li>filler five</li>
<li><a href="/b">Target B</a></li>
</ul>
</nav>
</section>
</main>
`;

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 } });
Expand Down
Loading