Skip to content

Commit c1aeddd

Browse files
committed
Merge remote-tracking branch 'upstream/main' into vishal/resolve-oct02-pr-2941
# Conflicts: # packages/sdk-go/internal/extensionassets/stagehand-extension.zip
2 parents 2105c11 + a237c77 commit c1aeddd

76 files changed

Lines changed: 3323 additions & 870 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.changeset/fifty-teams-decide.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@browserbasehq/stagehand-python": minor
3+
"@browserbasehq/stagehand-extension": minor
4+
"@browserbasehq/stagehand-protocol": minor
5+
"@browserbasehq/stagehand-go": minor
6+
"@browserbasehq/stagehand": minor
7+
---
8+
9+
expose a timeout option for page.snapshot() across all sdks, defaulting to 20 seconds.
10+
set it to 0 for unlimited execution. one budget covers the whole snapshot capture.

‎.changeset/gentle-eagles-browse.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@browserbasehq/eve": minor
3+
---
4+
5+
Replace the Eve extension's browser and web tools with Stagehand V4 `run`, `snapshot`, and `screenshot`.

‎.github/workflows/release.yml‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,57 @@ jobs:
141141
if: always() && !cancelled() && steps.status.outcome == 'success' && steps.status.outputs.has-changesets == 'false'
142142
run: pnpm exec tsx scripts/release/push-cli-release-tag.ts
143143

144+
publish-cli-alpha:
145+
name: Publish Browse alpha
146+
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
147+
runs-on: ubuntu-latest
148+
timeout-minutes: 30
149+
permissions:
150+
contents: read
151+
id-token: write
152+
steps:
153+
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
154+
with:
155+
fetch-depth: 0
156+
157+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
158+
with:
159+
version: "11.10.0"
160+
run_install: false
161+
162+
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
163+
with:
164+
node-version: "24"
165+
cache: pnpm
166+
registry-url: "https://registry.npmjs.org"
167+
168+
- name: Install npm
169+
run: npm install --global npm@11.16.0
170+
171+
- run: pnpm install --frozen-lockfile
172+
173+
# Independent of stable jobs: failed tag recovery must not block alphas.
174+
# A push may contain several commits, so compare against its before SHA.
175+
- name: Prepare Browse alpha
176+
id: alpha
177+
env:
178+
CLI_ALPHA_BASE: ${{ github.event.before }}
179+
run: pnpm exec tsx scripts/release/cli-alpha.ts >> "$GITHUB_OUTPUT"
180+
181+
- name: Wait for published SDK dependencies
182+
if: steps.alpha.outputs.should-publish == 'true'
183+
run: pnpm exec tsx scripts/release/scoped-release.ts wait-dependencies cli
184+
185+
- name: Build and smoke-test Browse alpha against the published SDK
186+
if: steps.alpha.outputs.should-publish == 'true'
187+
run: |
188+
pnpm exec tsx scripts/release/smoke-cli-package.ts
189+
pnpm --filter browse test:cli
190+
191+
- name: Publish Browse alpha
192+
if: steps.alpha.outputs.should-publish == 'true'
193+
run: pnpm exec tsx scripts/release/scoped-release.ts publish cli --alpha
194+
144195
publish-typescript-alpha:
145196
name: Publish TypeScript SDK alpha
146197
needs: release-typescript

‎justfile‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ _preview commit:
9999

100100
_publish-typescript:
101101
pnpm --filter ./packages/sdk-ts build
102+
pnpm --filter @browserbasehq/eve build
102103
pnpm exec tsx scripts/release/scoped-release.ts publish sdk
103104

104105
# Publishes a commit-addressed alpha of the TypeScript SDK (`<next>-alpha-<sha>`)

‎packages/docs/v4/reference/page.mdx‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,10 @@ const snapshot = await page.snapshot();
691691
<ParamField path="options.includeIframes" type="boolean" optional>
692692
Whether to include iframe content.
693693
</ParamField>
694+
<ParamField path="options.timeout" type="number" optional>
695+
Maximum time in milliseconds for the whole snapshot, including frame traversal & browser reads.
696+
Defaults to 20,000 ms. Use `0` for no timeout. Request delivery is outside this budget.
697+
</ParamField>
694698
</ParamField>
695699

696700
<ResponseField name="result" type="Promise<SnapshotResult>">
@@ -1442,6 +1446,11 @@ snapshot = await page.snapshot()
14421446
Whether to include iframe content.
14431447
</ParamField>
14441448

1449+
<ParamField path="timeout" type="float | None" optional>
1450+
Maximum time in milliseconds for the whole snapshot, including frame traversal & browser reads.
1451+
Defaults to 20,000 ms. Use `0` for no timeout. Request delivery is outside this budget.
1452+
</ParamField>
1453+
14451454
<ResponseField name="result" type="SnapshotResult">
14461455
The operation result.
14471456

@@ -2271,6 +2280,10 @@ fmt.Println(snapshot.FormattedTree)
22712280
<ParamField path="options.IncludeIframes" type="*bool" optional>
22722281
Whether to include iframe content.
22732282
</ParamField>
2283+
<ParamField path="options.Timeout" type="*float64" optional>
2284+
Maximum time in milliseconds for the whole snapshot, including frame traversal & browser reads.
2285+
Defaults to 20,000 ms. Use `0` for no timeout. Request delivery is outside this budget.
2286+
</ParamField>
22742287
</ParamField>
22752288

22762289
<ResponseField name="result" type="(SnapshotResult, error)">

‎packages/extension/tests/frame-locator.test.ts‎

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -152,21 +152,16 @@ describe("FrameLocator readiness", () => {
152152
const { locator, childFrame } = createFrameLocator(oldSession, getSessionForFrame);
153153

154154
await expect(locator.resolveFrame()).resolves.toBe(childFrame);
155-
expect(waitForLocatorWorld).toHaveBeenNthCalledWith(
156-
1,
157-
oldSession,
158-
"child",
159-
200,
160-
undefined,
161-
false,
162-
);
155+
expect(waitForLocatorWorld).toHaveBeenNthCalledWith(1, oldSession, "child", 200, undefined, {
156+
readinessRetries: "none",
157+
});
163158
expect(waitForLocatorWorld).toHaveBeenNthCalledWith(
164159
2,
165160
adoptedSession,
166161
"child",
167162
200,
168163
undefined,
169-
false,
164+
{ readinessRetries: "none" },
170165
);
171166
});
172167
});

‎packages/extension/understudy/a11y/snapshot/a11yTree.test.ts‎

Lines changed: 106 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import type { Protocol } from "devtools-protocol";
2-
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
33
import type { CDPSessionLike } from "../../cdp.js";
4+
import { Progress } from "../../progress.js";
5+
import { executionContexts } from "../../executionContextRegistry.js";
46
import { a11yForFrame } from "./a11yTree.js";
57
import { resolveObjectIdForCss, resolveObjectIdForXPath } from "./focusSelectors.js";
68

7-
vi.mock("./focusSelectors.js", () => ({
9+
vi.mock("./focusSelectors.js", async (importOriginal) => ({
10+
...(await importOriginal<typeof import("./focusSelectors.js")>()),
811
resolveObjectIdForCss: vi.fn(),
912
resolveObjectIdForXPath: vi.fn(),
1013
}));
@@ -14,6 +17,92 @@ describe("a11yForFrame focused locators", () => {
1417
vi.resetAllMocks();
1518
});
1619

20+
afterEach(() => {
21+
vi.useRealTimers();
22+
vi.restoreAllMocks();
23+
});
24+
25+
it.each(["Accessibility.getFullAXTree", "DOM.describeNode"])(
26+
"does not fall back after %s exceeds the deadline",
27+
async (command) => {
28+
vi.useFakeTimers();
29+
const progress = new Progress("act()", 10);
30+
vi.mocked(resolveObjectIdForCss).mockResolvedValue("object-second");
31+
const session = fakeSession(20, (method) => {
32+
if (method === command) {
33+
vi.spyOn(performance, "now").mockReturnValue(10);
34+
throw new Error("Frame with the given id is not found");
35+
}
36+
});
37+
const send = vi.spyOn(session, "send");
38+
try {
39+
await expect(
40+
a11yForFrame(
41+
session,
42+
"frame",
43+
{
44+
focusLocator: { selector: ".card" },
45+
tagNameMap: {},
46+
scrollableMap: {},
47+
encode: String,
48+
},
49+
progress,
50+
),
51+
).rejects.toThrow(/act\(\) timed out/);
52+
expect(send.mock.calls.filter(([method]) => method === command)).toHaveLength(1);
53+
if (command === "DOM.describeNode") {
54+
expect(send).toHaveBeenCalledWith("Runtime.releaseObject", {
55+
objectId: "object-second",
56+
});
57+
}
58+
} finally {
59+
progress.dispose();
60+
}
61+
},
62+
);
63+
64+
it.each([".card", "xpath=//article"])(
65+
"bounds a stalled %s focus lookup by the caller's deadline",
66+
async (selector) => {
67+
vi.useFakeTimers();
68+
const progress = new Progress("snapshot", 10);
69+
const actual =
70+
await vi.importActual<typeof import("./focusSelectors.js")>("./focusSelectors.js");
71+
vi.mocked(resolveObjectIdForCss).mockImplementation(actual.resolveObjectIdForCss);
72+
vi.mocked(resolveObjectIdForXPath).mockImplementation(actual.resolveObjectIdForXPath);
73+
const session = fakeSession(20, async (method) => {
74+
if (method === "Runtime.evaluate") await new Promise((resolve) => setTimeout(resolve, 25));
75+
});
76+
executionContexts.registerExtensionWorld(session, "frame", 1);
77+
const send = vi.spyOn(session, "send");
78+
let failure: unknown;
79+
const settled = a11yForFrame(
80+
session,
81+
"frame",
82+
{
83+
focusLocator: { selector },
84+
tagNameMap: {},
85+
scrollableMap: {},
86+
encode: String,
87+
},
88+
progress,
89+
).catch((error: unknown) => {
90+
failure = error;
91+
});
92+
try {
93+
await vi.advanceTimersByTimeAsync(10);
94+
const failureAtDeadline = failure;
95+
await vi.advanceTimersByTimeAsync(15);
96+
await settled;
97+
expect(failureAtDeadline).toBe(progress.signal.reason);
98+
expect(send).toHaveBeenCalledWith("Runtime.releaseObject", { objectId: "object-second" });
99+
expect(send).not.toHaveBeenCalledWith("DOM.describeNode", expect.anything());
100+
} finally {
101+
progress.dispose();
102+
}
103+
},
104+
);
105+
17106
it.each([
18107
{
19108
name: "CSS",
@@ -30,6 +119,7 @@ describe("a11yForFrame focused locators", () => {
30119
async ({ selector, resolver }) => {
31120
vi.mocked(resolver).mockResolvedValue("object-second");
32121
const session = fakeSession(20);
122+
const send = vi.spyOn(session, "send");
33123

34124
const result = await a11yForFrame(session, "root-frame", {
35125
focusLocator: { selector, nth: 1 },
@@ -43,24 +133,35 @@ describe("a11yForFrame focused locators", () => {
43133
expect(result.outline).toContain("Second detail");
44134
expect(result.outline).not.toContain("First match");
45135
expect(result.outline).not.toContain("First detail");
46-
expect(resolver).toHaveBeenCalledWith(session, selector, "root-frame", 1);
136+
expect(resolver).toHaveBeenCalledWith(session, selector, "root-frame", 1, undefined);
137+
expect(send).toHaveBeenCalledWith("Runtime.releaseObject", {
138+
objectId: "object-second",
139+
});
47140
},
48141
);
49142
});
50143

51-
function fakeSession(focusedBackendNodeId: number): CDPSessionLike {
144+
function fakeSession(
145+
focusedBackendNodeId: number,
146+
beforeSend?: (method: string) => void | Promise<void>,
147+
): CDPSessionLike {
52148
return {
53149
send: vi.fn(async (method: string, params?: Record<string, unknown>) => {
150+
await beforeSend?.(method);
54151
if (
55152
method === "Accessibility.enable" ||
56153
method === "Runtime.enable" ||
57-
method === "DOM.enable"
154+
method === "DOM.enable" ||
155+
method === "Runtime.releaseObject"
58156
) {
59157
return {};
60158
}
61159
if (method === "Accessibility.getFullAXTree") {
62160
return { nodes: repeatedSubtreeNodes() };
63161
}
162+
if (method === "Runtime.evaluate") {
163+
return { result: { type: "object", objectId: "object-second" } };
164+
}
64165
if (method === "DOM.describeNode" && params?.objectId === "object-second") {
65166
return { node: { backendNodeId: focusedBackendNodeId } };
66167
}

0 commit comments

Comments
 (0)