Skip to content

Commit 065ae5c

Browse files
committed
Revert "feat(aria): only include changed chunks into snapshot diff (microsoft#37988)"
This reverts commit 7249e20.
1 parent 7249e20 commit 065ae5c

File tree

10 files changed

+47
-125
lines changed

10 files changed

+47
-125
lines changed

packages/injected/src/ariaSnapshot.ts

Lines changed: 13 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -519,83 +519,42 @@ function buildByRefMap(root: AriaNode | undefined, map: Map<string | undefined,
519519

520520
function compareSnapshots(ariaSnapshot: AriaSnapshot, previousSnapshot: AriaSnapshot | undefined): Map<AriaNode, 'skip' | 'same' | 'changed'> {
521521
const previousByRef = buildByRefMap(previousSnapshot?.root);
522-
const result = new Map<AriaNode, 'skip' | 'same' | 'changed'>();
522+
const result = new Map<AriaNode, 'same' | 'changed'>();
523523

524524
// Returns whether ariaNode is the same as previousNode.
525525
const visit = (ariaNode: AriaNode, previousNode: AriaNode | undefined): boolean => {
526526
let same: boolean = ariaNode.children.length === previousNode?.children.length && ariaNodesEqual(ariaNode, previousNode);
527527
if (ariaNode.role === 'iframe')
528528
same = false;
529-
let canBeSkipped = same;
530529

531530
for (let childIndex = 0 ; childIndex < ariaNode.children.length; childIndex++) {
532531
const child = ariaNode.children[childIndex];
533532
const previousChild = previousNode?.children[childIndex];
534533
if (typeof child === 'string') {
535534
same &&= child === previousChild;
536-
canBeSkipped &&= child === previousChild;
537535
} else {
538536
let previous = typeof previousChild !== 'string' ? previousChild : undefined;
539537
if (child.ref)
540538
previous = previousByRef.get(child.ref);
541539
const sameChild = visit(child, previous);
542-
// New child, different order of children, or changed child with no ref -
543-
// we have to include this node to list children in the right order.
544-
if (!previous || (!sameChild && !child.ref) || (previous !== previousChild))
545-
canBeSkipped = false;
546540
same &&= (sameChild && previous === previousChild);
547541
}
548542
}
549543

550-
result.set(ariaNode, same ? 'same' : (canBeSkipped ? 'skip' : 'changed'));
544+
result.set(ariaNode, same ? 'same' : 'changed');
551545
return same;
552546
};
553547

554548
visit(ariaSnapshot.root, previousByRef.get(previousSnapshot?.root?.ref));
555549
return result;
556550
}
557551

558-
// Chooses only the changed parts of the snapshot and returns them as new roots.
559-
function filterSnapshotDiff(nodes: (AriaNode | string)[], statusMap: Map<AriaNode, 'skip' | 'same' | 'changed'>): (AriaNode | string)[] {
560-
const result: (AriaNode | string)[] = [];
561-
562-
const visit = (ariaNode: AriaNode) => {
563-
const status = statusMap.get(ariaNode);
564-
if (status === 'same') {
565-
// No need to render unchanged root at all.
566-
} else if (status === 'skip') {
567-
// Only render changed children.
568-
for (const child of ariaNode.children) {
569-
if (typeof child !== 'string')
570-
visit(child);
571-
}
572-
} else {
573-
// Render this node's subtree.
574-
result.push(ariaNode);
575-
}
576-
};
577-
578-
for (const node of nodes) {
579-
if (typeof node === 'string')
580-
result.push(node);
581-
else
582-
visit(node);
583-
}
584-
return result;
585-
}
586-
587552
export function renderAriaTree(ariaSnapshot: AriaSnapshot, publicOptions: AriaTreeOptions, previousSnapshot?: AriaSnapshot): string {
588553
const options = toInternalOptions(publicOptions);
589554
const lines: string[] = [];
590555
const includeText = options.renderStringsAsRegex ? textContributesInfo : () => true;
591556
const renderString = options.renderStringsAsRegex ? convertToBestGuessRegex : (str: string) => str;
592-
593-
// Do not render the root fragment, just its children.
594-
let nodesToRender = ariaSnapshot.root.role === 'fragment' ? ariaSnapshot.root.children : [ariaSnapshot.root];
595-
596557
const statusMap = compareSnapshots(ariaSnapshot, previousSnapshot);
597-
if (previousSnapshot)
598-
nodesToRender = filterSnapshotDiff(nodesToRender, statusMap);
599558

600559
const visitText = (text: string, indent: string) => {
601560
const escaped = yamlEscapeValueIfNeeded(renderString(text));
@@ -645,15 +604,15 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, publicOptions: AriaTr
645604
};
646605

647606
const visit = (ariaNode: AriaNode, indent: string, renderCursorPointer: boolean) => {
607+
const status = statusMap.get(ariaNode);
608+
648609
// Replace the whole subtree with a single reference when possible.
649-
if (statusMap.get(ariaNode) === 'same' && ariaNode.ref) {
610+
if (status === 'same' && ariaNode.ref) {
650611
lines.push(indent + `- ref=${ariaNode.ref} [unchanged]`);
651612
return;
652613
}
653614

654-
// When producing a diff, add <changed> marker to all diff roots.
655-
const isDiffRoot = !!previousSnapshot && !indent;
656-
const escapedKey = indent + '- ' + (isDiffRoot ? '<changed> ' : '') + yamlEscapeKeyIfNeeded(createKey(ariaNode, renderCursorPointer));
615+
const escapedKey = indent + '- ' + yamlEscapeKeyIfNeeded(createKey(ariaNode, renderCursorPointer));
657616
const singleInlinedTextChild = getSingleInlinedTextChild(ariaNode);
658617

659618
if (!ariaNode.children.length && !Object.keys(ariaNode.props).length) {
@@ -671,18 +630,22 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, publicOptions: AriaTr
671630
lines.push(escapedKey + ':');
672631
for (const [name, value] of Object.entries(ariaNode.props))
673632
lines.push(indent + ' - /' + name + ': ' + yamlEscapeValueIfNeeded(value));
633+
}
674634

675-
const childIndent = indent + ' ';
635+
indent += ' ';
636+
if (singleInlinedTextChild === undefined) {
676637
const inCursorPointer = !!ariaNode.ref && renderCursorPointer && hasPointerCursor(ariaNode);
677638
for (const child of ariaNode.children) {
678639
if (typeof child === 'string')
679-
visitText(includeText(ariaNode, child) ? child : '', childIndent);
640+
visitText(includeText(ariaNode, child) ? child : '', indent);
680641
else
681-
visit(child, childIndent, renderCursorPointer && !inCursorPointer);
642+
visit(child, indent, renderCursorPointer && !inCursorPointer);
682643
}
683644
}
684645
};
685646

647+
// Do not render the root fragment, just its children.
648+
const nodesToRender = ariaSnapshot.root.role === 'fragment' ? ariaSnapshot.root.children : [ariaSnapshot.root];
686649
for (const nodeToRender of nodesToRender) {
687650
if (typeof nodeToRender === 'string')
688651
visitText(nodeToRender, '');

packages/playwright-core/src/server/page.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,7 +1046,7 @@ async function snapshotFrameForAI(progress: Progress, frame: frames.Frame, optio
10461046
if (!node)
10471047
return true;
10481048
return injected.ariaSnapshot(node, { mode: 'ai', ...options });
1049-
}, { refPrefix: frame.seq ? 'f' + frame.seq : '', incremental: options.mode === 'incremental' && !frame.parentFrame(), track: options.track }));
1049+
}, { refPrefix: frame.seq ? 'f' + frame.seq : '', incremental: options.mode === 'incremental', track: options.track }));
10501050
if (snapshotOrRetry === true)
10511051
return continuePolling;
10521052
return snapshotOrRetry;
@@ -1060,7 +1060,7 @@ async function snapshotFrameForAI(progress: Progress, frame: frames.Frame, optio
10601060
const lines = snapshot.split('\n');
10611061
const result = [];
10621062
for (const line of lines) {
1063-
const match = line.match(/^(\s*)-(?: <changed>)? iframe (?:\[active\] )?\[ref=([^\]]*)\]/);
1063+
const match = line.match(/^(\s*)- iframe (?:\[active\] )?\[ref=([^\]]*)\]/);
10641064
if (!match) {
10651065
result.push(line);
10661066
continue;

packages/playwright/src/mcp/browser/response.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,7 @@ function renderTabSnapshot(tabSnapshot: TabSnapshot, options: { omitSnapshot?: b
192192
lines.push(`- Page Title: ${tabSnapshot.title}`);
193193
lines.push(`- Page Snapshot:`);
194194
lines.push('```yaml');
195-
// TODO: perhaps not render page state when there are no changes?
196-
lines.push(options.omitSnapshot ? '<snapshot>' : (tabSnapshot.ariaSnapshot || '<no changes>'));
195+
lines.push(options.omitSnapshot ? '<snapshot>' : tabSnapshot.ariaSnapshot);
197196
lines.push('```');
198197

199198
return lines.join('\n');

tests/mcp/click.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ test('browser_click', async ({ client, server }) => {
4141
},
4242
})).toHaveResponse({
4343
code: `await page.getByRole('button', { name: 'Submit' }).click();`,
44-
pageState: expect.stringContaining(`button "Submit" [active] [ref=e2]`),
44+
pageState: expect.stringContaining(`- button "Submit" [active] [ref=e2]`),
4545
});
4646
});
4747

@@ -70,7 +70,7 @@ test('browser_click (double)', async ({ client, server }) => {
7070
},
7171
})).toHaveResponse({
7272
code: `await page.getByRole('heading', { name: 'Click me' }).dblclick();`,
73-
pageState: expect.stringContaining(`heading "Double clicked" [level=1] [ref=e3]`),
73+
pageState: expect.stringContaining(`- heading "Double clicked" [level=1] [ref=e3]`),
7474
});
7575
});
7676

@@ -100,7 +100,7 @@ test('browser_click (right)', async ({ client, server }) => {
100100
});
101101
expect(result).toHaveResponse({
102102
code: `await page.getByRole('button', { name: 'Menu' }).click({ button: 'right' });`,
103-
pageState: expect.stringContaining(`button "Right clicked"`),
103+
pageState: expect.stringContaining(`- button "Right clicked"`),
104104
});
105105
});
106106

@@ -131,7 +131,7 @@ test('browser_click (modifiers)', async ({ client, server, mcpBrowser }) => {
131131
},
132132
})).toHaveResponse({
133133
code: `await page.getByRole('button', { name: 'Submit' }).click({ modifiers: ['Control'] });`,
134-
pageState: expect.stringContaining(`generic [ref=e3]: ctrlKey:true metaKey:false shiftKey:false altKey:false`),
134+
pageState: expect.stringContaining(`- generic [ref=e3]: ctrlKey:true metaKey:false shiftKey:false altKey:false`),
135135
});
136136
}
137137

@@ -144,7 +144,7 @@ test('browser_click (modifiers)', async ({ client, server, mcpBrowser }) => {
144144
},
145145
})).toHaveResponse({
146146
code: `await page.getByRole('button', { name: 'Submit' }).click({ modifiers: ['Shift'] });`,
147-
pageState: expect.stringContaining(`generic [ref=e3]: ctrlKey:false metaKey:false shiftKey:true altKey:false`),
147+
pageState: expect.stringContaining(`- generic [ref=e3]: ctrlKey:false metaKey:false shiftKey:true altKey:false`),
148148
});
149149

150150
expect(await client.callTool({
@@ -156,7 +156,7 @@ test('browser_click (modifiers)', async ({ client, server, mcpBrowser }) => {
156156
},
157157
})).toHaveResponse({
158158
code: `await page.getByRole('button', { name: 'Submit' }).click({ modifiers: ['Shift', 'Alt'] });`,
159-
pageState: expect.stringContaining(`generic [ref=e3]: ctrlKey:false metaKey:false shiftKey:true altKey:true`),
159+
pageState: expect.stringContaining(`- generic [ref=e3]: ctrlKey:false metaKey:false shiftKey:true altKey:true`),
160160
});
161161
});
162162

tests/mcp/core.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ test('browser_select_option', async ({ client, server }) => {
5858
- Page Title: Title
5959
- Page Snapshot:
6060
\`\`\`yaml
61-
- <changed> combobox [ref=e2]:
61+
- combobox [ref=e2]:
6262
- option "Foo"
6363
- option "Bar" [selected]
6464
\`\`\``,
@@ -90,8 +90,8 @@ test('browser_select_option (multiple)', async ({ client, server }) => {
9090
})).toHaveResponse({
9191
code: `await page.getByRole('listbox').selectOption(['bar', 'baz']);`,
9292
pageState: expect.stringContaining(`
93-
- <changed> option "Bar" [selected] [ref=e4]
94-
- <changed> option "Baz" [selected] [ref=e5]`),
93+
- option "Bar" [selected] [ref=e4]
94+
- option "Baz" [selected] [ref=e5]`),
9595
});
9696
});
9797

tests/mcp/dialogs.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ test('confirm dialog (true)', async ({ client, server }) => {
139139
},
140140
})).toHaveResponse({
141141
modalState: undefined,
142-
pageState: expect.stringContaining(`generic [active] [ref=e1]: "true"`),
142+
pageState: expect.stringContaining(`- generic [active] [ref=e1]: "true"`),
143143
});
144144
});
145145

@@ -175,7 +175,7 @@ test('confirm dialog (false)', async ({ client, server }) => {
175175
},
176176
})).toHaveResponse({
177177
modalState: undefined,
178-
pageState: expect.stringContaining(`generic [active] [ref=e1]: "false"`),
178+
pageState: expect.stringContaining(`- generic [active] [ref=e1]: "false"`),
179179
});
180180
});
181181

@@ -213,7 +213,7 @@ test('prompt dialog', async ({ client, server }) => {
213213
});
214214

215215
expect(result).toHaveResponse({
216-
pageState: expect.stringContaining(`generic [active] [ref=e1]: Answer`),
216+
pageState: expect.stringContaining(`- generic [active] [ref=e1]: Answer`),
217217
});
218218
});
219219

tests/mcp/secrets.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ test('browser_type', async ({ startClient, server }) => {
5353
expect(response).toHaveResponse({
5454
code: `await page.getByRole('textbox').fill(process.env['X-PASSWORD']);
5555
await page.getByRole('textbox').press('Enter');`,
56-
pageState: expect.stringMatching(/textbox (\[active\] )?\[ref=e2\]: <secret>X-PASSWORD<\/secret>/),
56+
pageState: expect.stringContaining(`- textbox`),
5757
});
5858
}
5959

tests/mcp/snapshot-diff.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ test('should return aria snapshot diff', async ({ client, server }) => {
6565
})).toHaveResponse({
6666
pageState: expect.stringContaining(`Page Snapshot:
6767
\`\`\`yaml
68-
<no changes>
68+
- ref=e1 [unchanged]
6969
\`\`\``),
7070
});
7171

@@ -78,7 +78,7 @@ test('should return aria snapshot diff', async ({ client, server }) => {
7878
})).toHaveResponse({
7979
pageState: expect.stringContaining(`Page Snapshot:
8080
\`\`\`yaml
81-
- <changed> generic [ref=e1]:
81+
- generic [ref=e1]:
8282
- button "Button 1" [active] [ref=e2]
8383
- button "Button 2new text" [ref=e105]
8484
- ref=e4 [unchanged]

tests/mcp/type.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ test('browser_type', async ({ client, server }) => {
4444
expect(response).toHaveResponse({
4545
code: `await page.getByRole('textbox').fill('Hi!');
4646
await page.getByRole('textbox').press('Enter');`,
47-
pageState: expect.stringMatching(/textbox (\[active\] )?\[ref=e2\]: Hi!/),
47+
pageState: expect.stringContaining(`- textbox`),
4848
});
4949
}
5050

@@ -79,7 +79,7 @@ test('browser_type (slowly)', async ({ client, server }) => {
7979

8080
expect(response).toHaveResponse({
8181
code: `await page.getByRole('textbox').pressSequentially('Hi!');`,
82-
pageState: expect.stringMatching(/textbox (\[active\] )?\[ref=e2\]: Hi!/),
82+
pageState: expect.stringContaining(`- textbox`),
8383
});
8484
}
8585
const response = await client.callTool({

0 commit comments

Comments
 (0)