Skip to content

Commit 9d45739

Browse files
committed
Update PR #2335
1 parent 19ba1df commit 9d45739

2 files changed

Lines changed: 82 additions & 3 deletions

File tree

‎e2e/scenarios/stagehand-instrumentation/scenario.impl.mjs‎

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,39 @@ const SHOP_HTML = `<!doctype html>
2929
</body>
3030
</html>`;
3131

32+
const STABLE_ACCESSIBILITY_TREE = `[0-2] RootWebArea: Trail Supply
33+
[0-4] scrollable, html
34+
[0-9] main
35+
[0-10] heading: Trail Backpack
36+
[0-12] paragraph
37+
[0-13] StaticText: $129.00
38+
[0-14] button: Add to cart
39+
[0-16] region: Shopping cart
40+
[0-17] paragraph
41+
[0-18] StaticText: Cart is empty
42+
[0-19] button: Checkout`;
43+
44+
export function stabilizeStagehandActMessages(messages) {
45+
let liveElementId;
46+
const normalizedMessages = messages.map((message) => {
47+
if (!message || typeof message.content !== "string") {
48+
return message;
49+
}
50+
liveElementId ??= message.content.match(
51+
/^\s*\[(\d+-\d+)\]\s+button: Add to cart\s*$/m,
52+
)?.[1];
53+
return {
54+
...message,
55+
content: message.content.replace(
56+
/Accessibility Tree:\s*[\s\S]*$/,
57+
`Accessibility Tree: \n${STABLE_ACCESSIBILITY_TREE}\n`,
58+
),
59+
};
60+
});
61+
62+
return { liveElementId, messages: normalizedMessages };
63+
}
64+
3265
async function startShopServer() {
3366
const server = createServer((_request, response) => {
3467
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
@@ -85,15 +118,39 @@ async function createStagehandSession(Stagehand, instrumentedAI) {
85118
"Expected Stagehand act to request a structured response",
86119
);
87120
}
121+
// Chromium's accessibility node IDs vary across browser versions. Keep the
122+
// deterministic fixture prompt stable for cassette replay, then translate
123+
// the model's fixture ID back to the live page before Stagehand acts on it.
124+
const { liveElementId, messages } = stabilizeStagehandActMessages(
125+
options.messages,
126+
);
127+
if (!liveElementId) {
128+
throw new Error("Expected Stagehand to observe the add-to-cart button");
129+
}
88130
const response = await instrumentedAI.generateObject({
89131
allowSystemInMessages: true,
90-
messages: options.messages,
132+
messages,
91133
model: languageModel,
92134
schema: options.response_model.schema,
93135
temperature: options.temperature,
94136
});
137+
let data = response.object;
138+
if (data && typeof data === "object") {
139+
if ("elementId" in data) {
140+
data = { ...data, elementId: liveElementId };
141+
} else if (
142+
"action" in data &&
143+
data.action &&
144+
typeof data.action === "object"
145+
) {
146+
data = {
147+
...data,
148+
action: { ...data.action, elementId: liveElementId },
149+
};
150+
}
151+
}
95152
return {
96-
data: response.object,
153+
data,
97154
usage: {
98155
cached_input_tokens: response.usage.cachedInputTokens ?? 0,
99156
completion_tokens: response.usage.outputTokens ?? 0,

‎e2e/scenarios/stagehand-instrumentation/scenario.test.ts‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { describe } from "vitest";
1+
import { describe, expect, test } from "vitest";
22
import {
33
prepareScenarioDir,
44
readInstalledPackageVersion,
55
resolveScenarioDir,
66
} from "../../helpers/scenario-harness";
77
import { defineStagehandInstrumentationAssertions } from "./assertions";
8+
import { stabilizeStagehandActMessages } from "./scenario.impl.mjs";
89

910
const originalScenarioDir = resolveScenarioDir(import.meta.url);
1011
const scenarioDir = await prepareScenarioDir({
@@ -27,6 +28,27 @@ const scenarios = await Promise.all(
2728
})),
2829
);
2930

31+
test("stabilizes Chromium accessibility IDs for cassette replay", () => {
32+
const result = stabilizeStagehandActMessages([
33+
{
34+
content: `instruction: Add the Trail Backpack to the cart
35+
Accessibility Tree:
36+
[7-20] RootWebArea: Trail Supply
37+
[7-42] button: Add to cart`,
38+
role: "user",
39+
},
40+
]);
41+
42+
expect(result.liveElementId).toBe("7-42");
43+
expect(result.messages).toEqual([
44+
{
45+
content: expect.stringContaining("[0-14] button: Add to cart"),
46+
role: "user",
47+
},
48+
]);
49+
expect(result.messages[0]?.content).not.toContain("[7-42]");
50+
});
51+
3052
describe.sequential("variants", () => {
3153
for (const scenario of scenarios) {
3254
describe.sequential(`Stagehand ${scenario.version}`, () => {

0 commit comments

Comments
 (0)