Skip to content
Merged
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
89 changes: 89 additions & 0 deletions app/e2e/agent-browser-tabs.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { type App, expect, mockOnly, test } from "./fixtures";

// A worktree's agent browser has tabs on a box that says "browser.tabs":
// each agent that asks gets a page of its own. The agent's view shows them
// in a strip, watches the one you pick, and never moves on its own while
// agents work in the others. The mock's checkout-fix worktree on devl has
// Main (the cart, used last), signup-a, signup-b and onboarding;
// window.__berthMockBrowser stands in for the agents.

type MockBrowser = { __berthMockBrowser: { navigate(tab: string, path: string): void; openTab(name: string): void; closeTab(name: string): void } };

async function agentView(app: App) {
await app.open();
const page = app.page;
await app.openWorktree("devl/checkout-fix");
await page.getByRole("button", { name: "New tab" }).click();
await page.getByRole("option", { name: /New browser tab/ }).click();
const pane = page.locator("[data-testid=browser-pane]:visible");
await pane.getByRole("button", { name: /Agent's view/ }).click();
const strip = pane.getByRole("group", { name: "The agent's browser's tabs" });
await expect(strip).toBeVisible();
return { page, pane, strip, frame: pane.getByTestId("agent-frame"), tab: (name: string) => strip.getByRole("button", { name, exact: true }) };
}

test("the agent's view lists the browser's tabs, opens on the one used last, and watches the one you pick", async ({ app }) => {
mockOnly("casts the demo's agent browser and its tabs");
const { pane, strip, frame, tab } = await agentView(app);
await expect(strip.getByRole("button")).toHaveText(["Main", "signup-a", "signup-b", "onboarding"]);
await expect(tab("Main")).toHaveAttribute("aria-pressed", "true");
await expect(frame).toHaveAttribute("data-tab", "");
await expect(pane.getByText(/\/cart$/)).toBeVisible();

await tab("signup-b").click();
await expect(tab("signup-b")).toHaveAttribute("aria-pressed", "true");
await expect(tab("Main")).toHaveAttribute("aria-pressed", "false");
await expect(frame).toHaveAttribute("data-tab", "signup-b");
await expect(pane.getByText(/\/signup\?plan=b$/)).toBeVisible();
// The Console drawer has the watched tab's lines, not main's.
await pane.getByTestId("devtools-toggle").click();
const rows = pane.getByTestId("devtools-drawer").getByTestId("console-row");
await expect(rows).toHaveCount(1);
await expect(rows).toContainText("signup-b: Sign up: plan B is ready");
await pane.getByTestId("devtools-toggle").click();

// Each tab has its own size, and the bar says the watched one's.
await tab("onboarding").click();
await expect(frame).toHaveAttribute("data-size", "390×844 @3x");
await expect(pane.getByTestId("agent-size")).toContainText("390×844 @3x");
await expect(pane.getByText(/\/welcome$/)).toBeVisible();
});

test("agents busy in other tabs never move the view off the one you watch", async ({ app }) => {
mockOnly("plays agents in the mock's browser tabs");
const { page, pane, strip, frame, tab } = await agentView(app);
await tab("signup-a").click();
await expect(frame).toHaveAttribute("data-tab", "signup-a");

// Another agent moves signup-b on and opens a tab of its own: the strip
// shows it, and the view stays where it was.
await page.evaluate(() => (window as unknown as MockBrowser).__berthMockBrowser.navigate("signup-b", "/signup/verify?plan=b"));
await expect(tab("signup-b")).toHaveAttribute("data-active", "true");
await page.evaluate(() => (window as unknown as MockBrowser).__berthMockBrowser.openTab("checkout-c"));
await expect(tab("checkout-c")).toBeVisible();
await expect(strip.getByRole("button", { pressed: true })).toHaveText("signup-a");
await expect(frame).toHaveAttribute("data-tab", "signup-a");
await expect(pane.getByText(/\/signup\?plan=a$/)).toBeVisible();

// And it keeps staying: another poll and more activity later, still signup-a.
await page.evaluate(() => (window as unknown as MockBrowser).__berthMockBrowser.navigate("checkout-c", "/checkout"));
await expect(tab("checkout-c")).toHaveAttribute("data-active", "true");
await expect(strip.getByRole("button", { pressed: true })).toHaveText("signup-a");
await expect(frame).toHaveAttribute("data-tab", "signup-a");
});

test("when the watched tab closes, the view moves to the tab used last and says so", async ({ app }) => {
mockOnly("plays agents in the mock's browser tabs");
const { page, pane, frame, tab } = await agentView(app);
await tab("signup-a").click();
await expect(frame).toHaveAttribute("data-tab", "signup-a");
await page.evaluate(() => (window as unknown as MockBrowser).__berthMockBrowser.navigate("signup-b", "/signup/verify?plan=b"));
await expect(tab("signup-b")).toHaveAttribute("data-active", "true");

await page.evaluate(() => (window as unknown as MockBrowser).__berthMockBrowser.closeTab("signup-a"));
await expect(pane.getByRole("status").filter({ hasText: "signup-a closed. Watching signup-b." })).toBeVisible();
await expect(tab("signup-a")).toHaveCount(0);
await expect(tab("signup-b")).toHaveAttribute("aria-pressed", "true");
await expect(frame).toHaveAttribute("data-tab", "signup-b");
await expect(pane.getByText(/\/signup\/verify\?plan=b$/)).toBeVisible();
});
2 changes: 1 addition & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"perf:guard": "node perf/guard.mjs",
"perf:soak": "node perf/soak.mjs",
"perf:terminals": "node perf/terminals.mjs",
"test": "node --experimental-strip-types --test src/lib/layout.test.ts src/lib/groups.test.ts src/lib/screen.test.ts src/lib/removing.test.ts src/lib/conversation-store.test.ts src/lib/wheel.test.ts src/lib/browser-url.test.ts src/lib/terminal.test.ts src/lib/predict.test.ts src/lib/browser-sandbox.test.ts src/lib/requirements.test.ts src/lib/diagnostics.test.ts src/lib/preview-frames.test.ts src/lib/home-layout.test.ts src/lib/file-match.test.ts src/lib/file-marks.test.ts src/lib/syntax-colors.test.ts src/lib/draft-text.test.ts src/lib/derive.test.ts src/lib/tree-model.test.ts src/lib/team-ref.test.ts src/lib/agent-messages.test.ts src/lib/page-loads.test.ts src/lib/devtools-model.test.ts src/lib/sidebar-width.test.ts src/lib/local-titles.test.ts src/lib/net.test.ts src/lib/share.test.ts src/lib/palette-shortcuts.test.ts src/lib/art/chart-spec.test.ts src/lib/art/gist.test.ts src/lib/art/vdiff.test.ts src/lib/term-output.test.ts src/lib/term-links.test.ts src/lib/poll.test.ts src/lib/whats-new.test.ts src/lib/platform.test.ts src/lib/processes.test.ts src/lib/link.test.ts src/lib/worktree-tree.test.ts src/lib/review-link.test.ts src/lib/login-url.test.ts src/lib/team-suggest-model.test.ts src/lib/last-of.test.ts src/lib/css-has.test.ts ../site/review/review.test.mjs src/lib/linear-issues.test.ts src/lib/marketplace.test.ts scripts/build-marketplace.test.mjs src/lib/leadline.test.ts src/lib/helper-path.test.ts src/lib/plugin-helpers.test.ts src/lib/database.test.ts src/lib/command-rank.test.ts"
"test": "node --experimental-strip-types --test src/lib/layout.test.ts src/lib/groups.test.ts src/lib/screen.test.ts src/lib/removing.test.ts src/lib/conversation-store.test.ts src/lib/wheel.test.ts src/lib/browser-url.test.ts src/lib/terminal.test.ts src/lib/predict.test.ts src/lib/browser-sandbox.test.ts src/lib/requirements.test.ts src/lib/diagnostics.test.ts src/lib/preview-frames.test.ts src/lib/home-layout.test.ts src/lib/file-match.test.ts src/lib/file-marks.test.ts src/lib/syntax-colors.test.ts src/lib/draft-text.test.ts src/lib/derive.test.ts src/lib/tree-model.test.ts src/lib/team-ref.test.ts src/lib/agent-messages.test.ts src/lib/page-loads.test.ts src/lib/devtools-model.test.ts src/lib/sidebar-width.test.ts src/lib/local-titles.test.ts src/lib/net.test.ts src/lib/share.test.ts src/lib/palette-shortcuts.test.ts src/lib/art/chart-spec.test.ts src/lib/art/gist.test.ts src/lib/art/vdiff.test.ts src/lib/term-output.test.ts src/lib/term-links.test.ts src/lib/poll.test.ts src/lib/whats-new.test.ts src/lib/platform.test.ts src/lib/processes.test.ts src/lib/link.test.ts src/lib/worktree-tree.test.ts src/lib/review-link.test.ts src/lib/login-url.test.ts src/lib/team-suggest-model.test.ts src/lib/last-of.test.ts src/lib/css-has.test.ts ../site/review/review.test.mjs src/lib/linear-issues.test.ts src/lib/marketplace.test.ts scripts/build-marketplace.test.mjs src/lib/leadline.test.ts src/lib/helper-path.test.ts src/lib/plugin-helpers.test.ts src/lib/database.test.ts src/lib/command-rank.test.ts src/lib/browser-tabs.test.ts"
},
"dependencies": {
"@base-ui/react": "^1.8.0",
Expand Down
Loading
Loading