Skip to content

Commit 0d7f331

Browse files
committed
test: add tests
1 parent 0eefd02 commit 0d7f331

2 files changed

Lines changed: 122 additions & 14 deletions

File tree

src/McpContext.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -586,25 +586,27 @@ export class McpContext implements Context {
586586
const idToNode = new Map<string, TextSnapshotNode>();
587587
const seenUniqueIds = new Set<string>();
588588
const assignIds = (node: SerializedAXNode): TextSnapshotNode => {
589-
const nodeWithId: TextSnapshotNode = {
590-
...node,
591-
id: '', // placeholder to be set below.
592-
children: node.children
593-
? node.children.map(child => assignIds(child))
594-
: [],
595-
};
596-
597-
const uniqueBackendId = `${nodeWithId.loaderId}_${nodeWithId.backendNodeId}`;
589+
let id = '';
590+
// @ts-expect-error untyped loaderId & backendNodeId.
591+
const uniqueBackendId = `${node.loaderId}_${node.backendNodeId}`;
598592
if (this.#uniqueBackendNodeIdToMcpId.has(uniqueBackendId)) {
599593
// Re-use MCP exposed ID if the uniqueId is the same.
600-
nodeWithId.id = this.#uniqueBackendNodeIdToMcpId.get(uniqueBackendId)!;
594+
id = this.#uniqueBackendNodeIdToMcpId.get(uniqueBackendId)!;
601595
} else {
602596
// Only generate a new ID if we have not seen the node before.
603-
nodeWithId.id = `${snapshotId}_${idCounter++}`;
604-
this.#uniqueBackendNodeIdToMcpId.set(uniqueBackendId, nodeWithId.id);
597+
id = `${snapshotId}_${idCounter++}`;
598+
this.#uniqueBackendNodeIdToMcpId.set(uniqueBackendId, id);
605599
}
606600
seenUniqueIds.add(uniqueBackendId);
607601

602+
const nodeWithId: TextSnapshotNode = {
603+
...node,
604+
id,
605+
children: node.children
606+
? node.children.map(child => assignIds(child))
607+
: [],
608+
};
609+
608610
// The AXNode for an option doesn't contain its `value`.
609611
// Therefore, set text content of the option as value.
610612
if (node.role === 'option') {

tests/McpResponse.test.ts

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
traceResultIsSuccess,
1717
} from '../src/trace-processing/parse.js';
1818

19+
import {serverHooks} from './server.js';
1920
import {loadTraceAsBuffer} from './trace-processing/fixtures/load.js';
2021
import {
2122
getImageContent,
@@ -63,8 +64,8 @@ describe('McpResponse', () => {
6364
await withMcpContext(async (response, context) => {
6465
const page = context.getSelectedPage();
6566
await page.setContent(
66-
html`<button>Click me</button
67-
><input
67+
html`<button>Click me</button>
68+
<input
6869
type="text"
6970
value="Input"
7071
/>`,
@@ -145,6 +146,111 @@ describe('McpResponse', () => {
145146
}
146147
});
147148

149+
it('preserves mapping ids across multiple snapshots', async () => {
150+
await withMcpContext(async (response, context) => {
151+
const page = context.getSelectedPage();
152+
await page.setContent(html`
153+
<div>
154+
<button id="btn1">Button 1</button>
155+
<span id="span1">Span 1</span>
156+
</div>
157+
`);
158+
response.includeSnapshot();
159+
// First snapshot
160+
const res1 = await response.handle('test', context);
161+
const text1 = getTextContent(res1.content[0]);
162+
const btn1IdMatch = text1.match(/uid=(\S+) .*Button 1/);
163+
const span1IdMatch = text1.match(/uid=(\S+) .*Span 1/);
164+
165+
assert.ok(btn1IdMatch, 'Button 1 ID not found in first snapshot');
166+
assert.ok(span1IdMatch, 'Span 1 ID not found in first snapshot');
167+
168+
const btn1Id = btn1IdMatch[1];
169+
const span1Id = span1IdMatch[1];
170+
171+
// Modify page: add a new element before the others to potentially shift indices if not stable
172+
await page.evaluate(() => {
173+
const newBtn = document.createElement('button');
174+
newBtn.textContent = 'Button 2';
175+
document.body.prepend(newBtn);
176+
});
177+
178+
// Second snapshot
179+
const res2 = await response.handle('test', context);
180+
const text2 = getTextContent(res2.content[0]);
181+
182+
const btn1IdMatch2 = text2.match(/uid=(\S+) .*Button 1/);
183+
const span1IdMatch2 = text2.match(/uid=(\S+) .*Span 1/);
184+
const btn2IdMatch = text2.match(/uid=(\S+) .*Button 2/);
185+
186+
assert.ok(btn1IdMatch2, 'Button 1 ID not found in second snapshot');
187+
assert.ok(span1IdMatch2, 'Span 1 ID not found in second snapshot');
188+
assert.ok(btn2IdMatch, 'Button 2 ID not found in second snapshot');
189+
190+
assert.strictEqual(
191+
btn1IdMatch2[1],
192+
btn1Id,
193+
'Button 1 ID changed between snapshots',
194+
);
195+
assert.strictEqual(
196+
span1IdMatch2[1],
197+
span1Id,
198+
'Span 1 ID changed between snapshots',
199+
);
200+
assert.notStrictEqual(
201+
btn2IdMatch[1],
202+
btn1Id,
203+
'Button 2 ID collides with Button 1',
204+
);
205+
assert.notStrictEqual(
206+
btn2IdMatch[1],
207+
btn1Id,
208+
'Button 2 ID collides with Button 1',
209+
);
210+
});
211+
});
212+
213+
describe('navigation', () => {
214+
const server = serverHooks();
215+
216+
it('resets ids after navigation', async () => {
217+
await withMcpContext(async (response, context) => {
218+
server.addHtmlRoute(
219+
'/page.html',
220+
html`
221+
<div>
222+
<button id="btn1">Button 1</button>
223+
</div>
224+
`,
225+
);
226+
const page = context.getSelectedPage();
227+
await page.goto(server.getRoute('/page.html'));
228+
229+
response.includeSnapshot();
230+
const res1 = await response.handle('test', context);
231+
const text1 = getTextContent(res1.content[0]);
232+
const btn1IdMatch = text1.match(/uid=(\S+) .*Button 1/);
233+
assert.ok(btn1IdMatch, 'Button 1 ID not found in first snapshot');
234+
const btn1Id = btn1IdMatch[1];
235+
236+
// Navigate to the same page again (or meaningful navigation)
237+
await page.goto(server.getRoute('/page.html'));
238+
239+
const res2 = await response.handle('test', context);
240+
const text2 = getTextContent(res2.content[0]);
241+
const btn1IdMatch2 = text2.match(/uid=(\S+) .*Button 1/);
242+
assert.ok(btn1IdMatch2, 'Button 1 ID not found in second snapshot');
243+
const btn1Id2 = btn1IdMatch2[1];
244+
245+
assert.notStrictEqual(
246+
btn1Id2,
247+
btn1Id,
248+
'ID should reset after navigation',
249+
);
250+
});
251+
});
252+
});
253+
148254
it('adds throttling setting when it is not null', async t => {
149255
await withMcpContext(async (response, context) => {
150256
context.setNetworkConditions('Slow 3G');

0 commit comments

Comments
 (0)