2026-08-09 16:55:28 -07:00
|
|
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
|
|
|
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
|
|
|
|
import BrowserTab from "./BrowserTab";
|
2026-08-10 10:13:53 -07:00
|
|
|
|
import type {
|
|
|
|
|
|
BrowserSetupOutcome,
|
|
|
|
|
|
BrowserViewStatus,
|
|
|
|
|
|
PlaywrightDetection,
|
|
|
|
|
|
Project,
|
|
|
|
|
|
} from "../../../lib/types";
|
2026-08-09 16:55:28 -07:00
|
|
|
|
|
|
|
|
|
|
const getBrowserViewStatus = vi.fn<() => Promise<BrowserViewStatus>>();
|
|
|
|
|
|
const setBrowserViewEnabled = vi.fn<() => Promise<BrowserViewStatus>>();
|
2026-08-10 10:13:53 -07:00
|
|
|
|
const checkBrowserViewSupport = vi.fn<() => Promise<PlaywrightDetection>>();
|
|
|
|
|
|
const installBrowserViewSupport = vi.fn<() => Promise<BrowserSetupOutcome>>();
|
|
|
|
|
|
const installBrowserViewBrowser = vi.fn<(id: string, b: string) => Promise<BrowserSetupOutcome>>();
|
2026-08-11 06:50:40 -07:00
|
|
|
|
const openBrowserViewPopout = vi.fn<(id: string, onTop: boolean) => Promise<void>>();
|
|
|
|
|
|
const closeBrowserViewPopout = vi.fn<(id: string) => Promise<void>>();
|
2026-08-11 07:40:51 -07:00
|
|
|
|
const getBrowserViewPopoutState =
|
|
|
|
|
|
vi.fn<() => Promise<{ open: boolean; always_on_top: boolean }>>();
|
2026-08-11 06:50:40 -07:00
|
|
|
|
const setBrowserViewPopoutAlwaysOnTop = vi.fn<(id: string, onTop: boolean) => Promise<void>>();
|
2026-08-09 16:55:28 -07:00
|
|
|
|
const pushToast = vi.fn();
|
2026-08-10 10:13:53 -07:00
|
|
|
|
const setContainerProgress = vi.fn();
|
2026-08-09 16:55:28 -07:00
|
|
|
|
|
|
|
|
|
|
vi.mock("../../../lib/tauri-commands", () => ({
|
|
|
|
|
|
getBrowserViewStatus: () => getBrowserViewStatus(),
|
|
|
|
|
|
setBrowserViewEnabled: () => setBrowserViewEnabled(),
|
2026-08-10 10:13:53 -07:00
|
|
|
|
checkBrowserViewSupport: () => checkBrowserViewSupport(),
|
|
|
|
|
|
installBrowserViewSupport: () => installBrowserViewSupport(),
|
|
|
|
|
|
installBrowserViewBrowser: (id: string, b: string) => installBrowserViewBrowser(id, b),
|
2026-08-11 06:50:40 -07:00
|
|
|
|
openBrowserViewPopout: (id: string, onTop: boolean) => openBrowserViewPopout(id, onTop),
|
|
|
|
|
|
closeBrowserViewPopout: (id: string) => closeBrowserViewPopout(id),
|
2026-08-11 07:40:51 -07:00
|
|
|
|
getBrowserViewPopoutState: () => getBrowserViewPopoutState(),
|
2026-08-11 06:50:40 -07:00
|
|
|
|
setBrowserViewPopoutAlwaysOnTop: (id: string, onTop: boolean) =>
|
|
|
|
|
|
setBrowserViewPopoutAlwaysOnTop(id, onTop),
|
2026-08-09 16:55:28 -07:00
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
vi.mock("@tauri-apps/api/event", () => ({
|
|
|
|
|
|
listen: vi.fn(async () => () => {}),
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
2026-08-10 10:13:53 -07:00
|
|
|
|
const storeState = {
|
|
|
|
|
|
pushToast,
|
|
|
|
|
|
setContainerProgress,
|
|
|
|
|
|
containerProgress: {} as Record<string, string>,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-09 16:55:28 -07:00
|
|
|
|
vi.mock("../../../store/appState", () => ({
|
2026-08-10 10:13:53 -07:00
|
|
|
|
useAppState: (selector: (s: unknown) => unknown) => selector(storeState),
|
2026-08-09 16:55:28 -07:00
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
const OFF: BrowserViewStatus = {
|
|
|
|
|
|
enabled: false,
|
|
|
|
|
|
state: "off",
|
|
|
|
|
|
url: null,
|
|
|
|
|
|
host_port: null,
|
|
|
|
|
|
container_port: null,
|
|
|
|
|
|
started_at: null,
|
|
|
|
|
|
detection: null,
|
|
|
|
|
|
message: null,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-10 10:13:53 -07:00
|
|
|
|
const NOTHING: PlaywrightDetection = {
|
|
|
|
|
|
node_version: "22.11.0",
|
|
|
|
|
|
playwright_version: null,
|
|
|
|
|
|
playwright_path: null,
|
|
|
|
|
|
playwright_cli: null,
|
|
|
|
|
|
has_bind: false,
|
|
|
|
|
|
cli_version: null,
|
|
|
|
|
|
cli_entry: null,
|
|
|
|
|
|
browsers: [],
|
|
|
|
|
|
chrome_channel: null,
|
2026-08-11 09:04:06 -07:00
|
|
|
|
chromium_executable: null,
|
|
|
|
|
|
chromium_executable_exists: false,
|
|
|
|
|
|
script_playwright_version: null,
|
|
|
|
|
|
script_chromium_executable: null,
|
|
|
|
|
|
script_chromium_executable_exists: false,
|
2026-08-10 10:13:53 -07:00
|
|
|
|
searched: [
|
|
|
|
|
|
"/workspace",
|
|
|
|
|
|
"/usr/lib/node_modules",
|
|
|
|
|
|
"/home/claude/.npm/_npx/9f3a/node_modules",
|
|
|
|
|
|
],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const READY: PlaywrightDetection = {
|
|
|
|
|
|
...NOTHING,
|
|
|
|
|
|
playwright_version: "1.62.1",
|
|
|
|
|
|
playwright_path: "/workspace/node_modules/playwright-core/package.json",
|
|
|
|
|
|
playwright_cli: "/workspace/node_modules/playwright-core/cli.js",
|
|
|
|
|
|
has_bind: true,
|
|
|
|
|
|
cli_version: "0.1.18",
|
|
|
|
|
|
cli_entry: "/workspace/node_modules/@playwright/cli/playwright-cli.js",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-09 16:55:28 -07:00
|
|
|
|
const project: Project = {
|
|
|
|
|
|
id: "p1",
|
|
|
|
|
|
name: "api-server",
|
|
|
|
|
|
paths: [{ host_path: "/home/user/api", mount_name: "api" }],
|
|
|
|
|
|
container_id: "c1",
|
|
|
|
|
|
status: "running",
|
|
|
|
|
|
backend: "anthropic",
|
|
|
|
|
|
bedrock_config: null,
|
|
|
|
|
|
ollama_config: null,
|
|
|
|
|
|
openai_compatible_config: null,
|
|
|
|
|
|
allow_docker_access: false,
|
|
|
|
|
|
sandbox_mode_enabled: true,
|
|
|
|
|
|
mission_control_enabled: false,
|
|
|
|
|
|
auth_bridge_enabled: false,
|
|
|
|
|
|
use_shared_auth_token: true,
|
|
|
|
|
|
full_permissions: false,
|
|
|
|
|
|
permission_mode: "bypass",
|
|
|
|
|
|
ssh_key_path: null,
|
|
|
|
|
|
git_token: null,
|
|
|
|
|
|
git_user_name: null,
|
|
|
|
|
|
git_user_email: null,
|
|
|
|
|
|
custom_env_vars: [],
|
|
|
|
|
|
port_mappings: [],
|
|
|
|
|
|
claude_instructions: null,
|
|
|
|
|
|
claude_code_settings: null,
|
|
|
|
|
|
renamed_session_names: {},
|
|
|
|
|
|
created_at: "2026-01-01T00:00:00Z",
|
|
|
|
|
|
updated_at: "2026-01-01T00:00:00Z",
|
|
|
|
|
|
} as unknown as Project;
|
|
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
|
vi.clearAllMocks();
|
2026-08-10 10:13:53 -07:00
|
|
|
|
storeState.containerProgress = {};
|
2026-08-09 16:55:28 -07:00
|
|
|
|
getBrowserViewStatus.mockResolvedValue(OFF);
|
2026-08-10 10:13:53 -07:00
|
|
|
|
checkBrowserViewSupport.mockResolvedValue(READY);
|
2026-08-11 07:40:51 -07:00
|
|
|
|
getBrowserViewPopoutState.mockResolvedValue({ open: false, always_on_top: false });
|
2026-08-11 06:50:40 -07:00
|
|
|
|
openBrowserViewPopout.mockResolvedValue(undefined);
|
|
|
|
|
|
closeBrowserViewPopout.mockResolvedValue(undefined);
|
|
|
|
|
|
setBrowserViewPopoutAlwaysOnTop.mockResolvedValue(undefined);
|
2026-08-09 16:55:28 -07:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-11 06:50:40 -07:00
|
|
|
|
const LIVE: BrowserViewStatus = {
|
|
|
|
|
|
...OFF,
|
|
|
|
|
|
enabled: true,
|
|
|
|
|
|
state: "running",
|
|
|
|
|
|
url: "http://127.0.0.1:47820/index.html?ws=abc&token=SEKRIT",
|
|
|
|
|
|
host_port: 47820,
|
|
|
|
|
|
container_port: 39321,
|
|
|
|
|
|
started_at: "2026-08-09T10:00:00Z",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/** Render with the view already live, which is the only state that pops out. */
|
|
|
|
|
|
async function renderLive() {
|
|
|
|
|
|
checkBrowserViewSupport.mockResolvedValue({ ...READY, browsers: ["chromium-1200"] });
|
|
|
|
|
|
setBrowserViewEnabled.mockResolvedValue(LIVE);
|
|
|
|
|
|
render(<BrowserTab project={project} active />);
|
|
|
|
|
|
await waitFor(() => expect(getBrowserViewStatus).toHaveBeenCalled());
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: /start browser view/i }));
|
|
|
|
|
|
});
|
|
|
|
|
|
await screen.findByTitle("Playwright browser view for api-server");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-09 16:55:28 -07:00
|
|
|
|
describe("BrowserTab", () => {
|
|
|
|
|
|
it("does not offer to start anything while the container is stopped", async () => {
|
|
|
|
|
|
render(<BrowserTab project={{ ...project, status: "stopped" }} active />);
|
|
|
|
|
|
expect(await screen.findByText(/container isn’t running/i)).toBeInTheDocument();
|
|
|
|
|
|
expect(screen.queryByRole("button", { name: /start browser view/i })).toBeNull();
|
|
|
|
|
|
expect(getBrowserViewStatus).not.toHaveBeenCalled();
|
2026-08-10 10:13:53 -07:00
|
|
|
|
expect(checkBrowserViewSupport).not.toHaveBeenCalled();
|
2026-08-09 16:55:28 -07:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-10 10:13:53 -07:00
|
|
|
|
it("starts off, and never starts a view or installs anything without being asked", async () => {
|
|
|
|
|
|
checkBrowserViewSupport.mockResolvedValue({ ...READY, browsers: ["chromium-1200"] });
|
2026-08-09 16:55:28 -07:00
|
|
|
|
render(<BrowserTab project={project} active />);
|
|
|
|
|
|
await waitFor(() => expect(getBrowserViewStatus).toHaveBeenCalled());
|
|
|
|
|
|
expect(screen.getByText("Off")).toBeInTheDocument();
|
|
|
|
|
|
expect(screen.queryByTitle(/browser view for/i)).toBeNull();
|
|
|
|
|
|
expect(setBrowserViewEnabled).not.toHaveBeenCalled();
|
2026-08-10 10:13:53 -07:00
|
|
|
|
// Probing is read-only and expected; installing is a mutation and is not.
|
|
|
|
|
|
await waitFor(() => expect(checkBrowserViewSupport).toHaveBeenCalled());
|
|
|
|
|
|
expect(installBrowserViewSupport).not.toHaveBeenCalled();
|
|
|
|
|
|
expect(installBrowserViewBrowser).not.toHaveBeenCalled();
|
2026-08-09 16:55:28 -07:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("shows the live pane, pointed at loopback with a token, once started", async () => {
|
2026-08-10 10:13:53 -07:00
|
|
|
|
checkBrowserViewSupport.mockResolvedValue({ ...READY, browsers: ["chromium-1200"] });
|
2026-08-09 16:55:28 -07:00
|
|
|
|
setBrowserViewEnabled.mockResolvedValue({
|
|
|
|
|
|
...OFF,
|
|
|
|
|
|
enabled: true,
|
|
|
|
|
|
state: "running",
|
|
|
|
|
|
url: "http://127.0.0.1:47820/index.html?ws=abc&token=SEKRIT",
|
|
|
|
|
|
host_port: 47820,
|
|
|
|
|
|
container_port: 39321,
|
|
|
|
|
|
started_at: "2026-08-09T10:00:00Z",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
render(<BrowserTab project={project} active />);
|
|
|
|
|
|
await waitFor(() => expect(getBrowserViewStatus).toHaveBeenCalled());
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: /start browser view/i }));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const frame = await screen.findByTitle("Playwright browser view for api-server");
|
|
|
|
|
|
expect(frame).toHaveAttribute(
|
|
|
|
|
|
"src",
|
|
|
|
|
|
"http://127.0.0.1:47820/index.html?ws=abc&token=SEKRIT",
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(screen.getByText("Live")).toBeInTheDocument();
|
|
|
|
|
|
expect(screen.getByText(/127\.0\.0\.1:47820 → container :39321/)).toBeInTheDocument();
|
|
|
|
|
|
expect(screen.getByRole("button", { name: "Stop" })).toBeInTheDocument();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-10 10:13:53 -07:00
|
|
|
|
it("offers setup before the user hits a wall, naming what is missing", async () => {
|
|
|
|
|
|
checkBrowserViewSupport.mockResolvedValue(NOTHING);
|
|
|
|
|
|
|
|
|
|
|
|
render(<BrowserTab project={project} active />);
|
|
|
|
|
|
|
|
|
|
|
|
// No Start attempt was needed to learn this.
|
|
|
|
|
|
expect(await screen.findByRole("button", { name: /set up playwright/i })).toBeInTheDocument();
|
|
|
|
|
|
expect(screen.getByText(/Missing: playwright, @playwright\/cli/)).toBeInTheDocument();
|
|
|
|
|
|
// The npx cache is shown among the searched roots — that is where an
|
|
|
|
|
|
// MCP-installed Playwright actually lives.
|
|
|
|
|
|
expect(screen.getByText(/_npx\/9f3a\/node_modules/)).toBeInTheDocument();
|
|
|
|
|
|
// A browser can't be installed before Playwright is.
|
|
|
|
|
|
expect(screen.getByRole("button", { name: /install chromium/i })).toBeDisabled();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("installs Playwright on request and updates itself from the fresh probe", async () => {
|
|
|
|
|
|
checkBrowserViewSupport.mockResolvedValue(NOTHING);
|
|
|
|
|
|
installBrowserViewSupport.mockResolvedValue({
|
|
|
|
|
|
detection: READY,
|
|
|
|
|
|
log: "added 5 packages in 3s",
|
|
|
|
|
|
browser_launched: null,
|
|
|
|
|
|
warning: "Playwright is installed, but this container has no browser to drive yet.",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
render(<BrowserTab project={project} active />);
|
|
|
|
|
|
const button = await screen.findByRole("button", { name: /set up playwright/i });
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
|
fireEvent.click(button);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await waitFor(() => expect(installBrowserViewSupport).toHaveBeenCalled());
|
|
|
|
|
|
// The pane re-rendered from the returned probe — no reopening the tab.
|
|
|
|
|
|
expect(await screen.findByText("1.62.1")).toBeInTheDocument();
|
|
|
|
|
|
// Stated in the warning box, and again in the pane's own summary line.
|
|
|
|
|
|
expect(screen.getAllByText(/no browser to drive yet/).length).toBeGreaterThan(0);
|
|
|
|
|
|
// And the browser buttons are now live.
|
|
|
|
|
|
expect(screen.getByRole("button", { name: /install chromium/i })).toBeEnabled();
|
|
|
|
|
|
expect(screen.getByRole("button", { name: /install chrome channel/i })).toBeEnabled();
|
|
|
|
|
|
// The progress line is always cleared, whatever happened.
|
|
|
|
|
|
expect(setContainerProgress).toHaveBeenCalledWith("p1", null);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("says which browser is for which caller, and states the size first", async () => {
|
|
|
|
|
|
checkBrowserViewSupport.mockResolvedValue(READY);
|
|
|
|
|
|
render(<BrowserTab project={project} active />);
|
|
|
|
|
|
|
|
|
|
|
|
expect(await screen.findByText(/several hundred mb/i)).toBeInTheDocument();
|
|
|
|
|
|
// The copy is broken across a <code> element, so match the container.
|
|
|
|
|
|
expect(
|
|
|
|
|
|
screen.getByText((_, el) =>
|
|
|
|
|
|
(el?.textContent ?? "").includes("@playwright/mcp") &&
|
|
|
|
|
|
(el?.textContent ?? "").includes("asks for") &&
|
|
|
|
|
|
el?.tagName.toLowerCase() === "li",
|
|
|
|
|
|
),
|
|
|
|
|
|
).toBeInTheDocument();
|
|
|
|
|
|
expect(screen.getByText(/roughly 150 mb/i)).toBeInTheDocument();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("installs the chrome channel when that is the one asked for", async () => {
|
|
|
|
|
|
checkBrowserViewSupport.mockResolvedValue(READY);
|
|
|
|
|
|
installBrowserViewBrowser.mockResolvedValue({
|
|
|
|
|
|
detection: { ...READY, chrome_channel: "/usr/bin/google-chrome-stable" },
|
|
|
|
|
|
log: "Installing google-chrome-stable",
|
|
|
|
|
|
browser_launched: true,
|
|
|
|
|
|
warning: null,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
render(<BrowserTab project={project} active />);
|
|
|
|
|
|
const button = await screen.findByRole("button", { name: /install chrome channel/i });
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
|
fireEvent.click(button);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await waitFor(() =>
|
|
|
|
|
|
expect(installBrowserViewBrowser).toHaveBeenCalledWith("p1", "chrome"),
|
|
|
|
|
|
);
|
|
|
|
|
|
// Shown as the step's "done" line and again in the diagnostics table.
|
|
|
|
|
|
await waitFor(() =>
|
|
|
|
|
|
expect(screen.getAllByText(/google-chrome-stable/).length).toBeGreaterThan(0),
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("reports an install failure with the real command output", async () => {
|
|
|
|
|
|
checkBrowserViewSupport.mockResolvedValue(NOTHING);
|
|
|
|
|
|
installBrowserViewSupport.mockRejectedValue(
|
|
|
|
|
|
"npm couldn't install Playwright in this container (exit 1).\n\nnpm said:\nEACCES: permission denied",
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
render(<BrowserTab project={project} active />);
|
|
|
|
|
|
const button = await screen.findByRole("button", { name: /set up playwright/i });
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
|
fireEvent.click(button);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
expect(await screen.findByText(/EACCES: permission denied/)).toBeInTheDocument();
|
|
|
|
|
|
expect(pushToast).toHaveBeenCalledWith(
|
|
|
|
|
|
expect.objectContaining({ kind: "error" }),
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(setContainerProgress).toHaveBeenCalledWith("p1", null);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-09 16:55:28 -07:00
|
|
|
|
it("explains precisely what is missing instead of spinning", async () => {
|
2026-08-10 10:13:53 -07:00
|
|
|
|
checkBrowserViewSupport.mockRejectedValue("container busy");
|
2026-08-09 16:55:28 -07:00
|
|
|
|
getBrowserViewStatus.mockResolvedValue({
|
|
|
|
|
|
...OFF,
|
|
|
|
|
|
enabled: true,
|
|
|
|
|
|
state: "unavailable",
|
|
|
|
|
|
message:
|
2026-08-10 10:13:53 -07:00
|
|
|
|
"Playwright isn't installed in this container. Two packages are needed: `playwright` and `@playwright/cli`.",
|
|
|
|
|
|
detection: NOTHING,
|
2026-08-09 16:55:28 -07:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
render(<BrowserTab project={project} active />);
|
|
|
|
|
|
|
2026-08-10 10:13:53 -07:00
|
|
|
|
expect(await screen.findByText(/Two packages are needed/)).toBeInTheDocument();
|
2026-08-09 16:55:28 -07:00
|
|
|
|
expect(screen.getByText("Unavailable")).toBeInTheDocument();
|
|
|
|
|
|
// The probe's findings are shown, so the user can see why.
|
|
|
|
|
|
expect(screen.getByText("22.11.0")).toBeInTheDocument();
|
|
|
|
|
|
expect(screen.getByText("not in this build")).toBeInTheDocument();
|
|
|
|
|
|
expect(screen.getByText(/usr\/lib\/node_modules/)).toBeInTheDocument();
|
|
|
|
|
|
expect(screen.queryByTitle(/browser view for/i)).toBeNull();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("surfaces a start failure rather than leaving the pane blank", async () => {
|
2026-08-10 10:13:53 -07:00
|
|
|
|
checkBrowserViewSupport.mockResolvedValue({ ...READY, browsers: ["chromium-1200"] });
|
2026-08-09 16:55:28 -07:00
|
|
|
|
setBrowserViewEnabled.mockRejectedValue("container went away");
|
|
|
|
|
|
|
|
|
|
|
|
render(<BrowserTab project={project} active />);
|
|
|
|
|
|
await waitFor(() => expect(getBrowserViewStatus).toHaveBeenCalled());
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: /start browser view/i }));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
expect(await screen.findByText(/didn’t start/i)).toBeInTheDocument();
|
|
|
|
|
|
expect(screen.getByText(/container went away/)).toBeInTheDocument();
|
|
|
|
|
|
expect(pushToast).toHaveBeenCalledWith(
|
|
|
|
|
|
expect.objectContaining({ kind: "error" }),
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("stops the view when asked", async () => {
|
2026-08-10 10:13:53 -07:00
|
|
|
|
checkBrowserViewSupport.mockResolvedValue({ ...READY, browsers: ["chromium-1200"] });
|
2026-08-09 16:55:28 -07:00
|
|
|
|
getBrowserViewStatus.mockResolvedValue({
|
|
|
|
|
|
...OFF,
|
|
|
|
|
|
enabled: true,
|
|
|
|
|
|
state: "running",
|
|
|
|
|
|
url: "http://127.0.0.1:47821/?token=T",
|
|
|
|
|
|
host_port: 47821,
|
|
|
|
|
|
container_port: 39321,
|
|
|
|
|
|
});
|
|
|
|
|
|
setBrowserViewEnabled.mockResolvedValue(OFF);
|
|
|
|
|
|
|
|
|
|
|
|
render(<BrowserTab project={project} active />);
|
|
|
|
|
|
const stop = await screen.findByRole("button", { name: "Stop" });
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
|
fireEvent.click(stop);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await waitFor(() => expect(setBrowserViewEnabled).toHaveBeenCalled());
|
|
|
|
|
|
expect(await screen.findByText("Off")).toBeInTheDocument();
|
|
|
|
|
|
expect(screen.queryByTitle(/browser view for/i)).toBeNull();
|
|
|
|
|
|
});
|
2026-08-11 06:50:40 -07:00
|
|
|
|
|
2026-08-11 09:04:06 -07:00
|
|
|
|
it("names both halves when the installed browser isn\u2019t the one Playwright launches", async () => {
|
|
|
|
|
|
// The cache is full and every script fails \u2014 "install a browser" alone
|
|
|
|
|
|
// would read as nonsense, so the copy has to say which copy wants what.
|
|
|
|
|
|
checkBrowserViewSupport.mockResolvedValue({
|
|
|
|
|
|
...READY,
|
|
|
|
|
|
browsers: ["chromium-1237"],
|
|
|
|
|
|
chromium_executable: "/home/claude/.cache/ms-playwright/chromium-1237/chrome-linux64/chrome",
|
|
|
|
|
|
chromium_executable_exists: true,
|
|
|
|
|
|
script_playwright_version: "1.62.1",
|
|
|
|
|
|
script_chromium_executable:
|
|
|
|
|
|
"/home/claude/.cache/ms-playwright/chromium-1234/chrome-linux64/chrome",
|
|
|
|
|
|
script_chromium_executable_exists: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
render(<BrowserTab project={project} active />);
|
|
|
|
|
|
|
|
|
|
|
|
expect(await screen.findByText(/isn\u2019t the one Playwright launches/i)).toBeInTheDocument();
|
|
|
|
|
|
// Both revisions appear in the explanation: what is installed, and what
|
|
|
|
|
|
// the failing copy actually wants.
|
|
|
|
|
|
expect(screen.getAllByText(/chromium-1237/).length).toBeGreaterThan(0);
|
|
|
|
|
|
expect(screen.getAllByText(/chromium-1234/).length).toBeGreaterThan(0);
|
|
|
|
|
|
expect(screen.getAllByText(/Set up Playwright/).length).toBeGreaterThan(0);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("does not call an unanswered probe a skew", async () => {
|
|
|
|
|
|
// A container older than these fields omits them; unknown is not broken.
|
|
|
|
|
|
checkBrowserViewSupport.mockResolvedValue({ ...READY, browsers: ["chromium-1200"] });
|
|
|
|
|
|
getBrowserViewStatus.mockResolvedValue(LIVE);
|
|
|
|
|
|
|
|
|
|
|
|
render(<BrowserTab project={project} active />);
|
|
|
|
|
|
|
|
|
|
|
|
expect(await screen.findByTitle("Playwright browser view for api-server")).toBeInTheDocument();
|
|
|
|
|
|
expect(screen.queryByText(/isn\u2019t the one Playwright launches/i)).toBeNull();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-11 06:50:40 -07:00
|
|
|
|
it("only offers a window of its own once there is something to watch", async () => {
|
|
|
|
|
|
checkBrowserViewSupport.mockResolvedValue({ ...READY, browsers: ["chromium-1200"] });
|
|
|
|
|
|
render(<BrowserTab project={project} active />);
|
|
|
|
|
|
await waitFor(() => expect(getBrowserViewStatus).toHaveBeenCalled());
|
|
|
|
|
|
expect(screen.queryByRole("button", { name: /own window/i })).toBeNull();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("pops the live view out, and drops the iframe so only one viewer drives", async () => {
|
|
|
|
|
|
await renderLive();
|
|
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: /own window/i }));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
expect(openBrowserViewPopout).toHaveBeenCalledWith("p1", false);
|
|
|
|
|
|
// The window is showing it now — a second copy here would be a second
|
|
|
|
|
|
// cursor on the same page.
|
|
|
|
|
|
expect(screen.queryByTitle(/browser view for/i)).toBeNull();
|
|
|
|
|
|
expect(await screen.findByText(/in its own window/i)).toBeInTheDocument();
|
|
|
|
|
|
// Still live, and still stoppable from the tab.
|
|
|
|
|
|
expect(screen.getByText("Live")).toBeInTheDocument();
|
|
|
|
|
|
expect(screen.getByRole("button", { name: "Stop" })).toBeInTheDocument();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("puts the view back in the tab when the window is closed from here", async () => {
|
|
|
|
|
|
await renderLive();
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: /own window/i }));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
|
fireEvent.click(screen.getAllByRole("button", { name: /put back in tab/i })[0]);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
expect(closeBrowserViewPopout).toHaveBeenCalledWith("p1");
|
|
|
|
|
|
expect(await screen.findByTitle("Playwright browser view for api-server")).toBeInTheDocument();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("pins the window on top on request", async () => {
|
|
|
|
|
|
await renderLive();
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: /own window/i }));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
2026-08-11 07:40:51 -07:00
|
|
|
|
// The accessible name is the visible text, as with every other Toggle.
|
|
|
|
|
|
fireEvent.click(screen.getByRole("switch", { name: "Keep on top" }));
|
2026-08-11 06:50:40 -07:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
expect(setBrowserViewPopoutAlwaysOnTop).toHaveBeenCalledWith("p1", true);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("keeps a pop-out that outlived the tab, rather than showing an empty pane", async () => {
|
2026-08-11 07:40:51 -07:00
|
|
|
|
// The window belongs to the backend, so remounting the pane has to read its
|
2026-08-11 06:50:40 -07:00
|
|
|
|
// state back — otherwise the pane would render an iframe alongside it.
|
2026-08-11 07:40:51 -07:00
|
|
|
|
getBrowserViewPopoutState.mockResolvedValue({ open: true, always_on_top: true });
|
2026-08-11 06:50:40 -07:00
|
|
|
|
checkBrowserViewSupport.mockResolvedValue({ ...READY, browsers: ["chromium-1200"] });
|
|
|
|
|
|
getBrowserViewStatus.mockResolvedValue(LIVE);
|
|
|
|
|
|
|
|
|
|
|
|
render(<BrowserTab project={project} active />);
|
|
|
|
|
|
|
|
|
|
|
|
expect(await screen.findByText(/in its own window/i)).toBeInTheDocument();
|
|
|
|
|
|
expect(screen.queryByTitle(/browser view for/i)).toBeNull();
|
2026-08-11 07:40:51 -07:00
|
|
|
|
// The pin is read from the window too — the pane is unmounted every time
|
|
|
|
|
|
// another sub-tab is selected, so remembering it would show Off over a
|
|
|
|
|
|
// window that is still floating on top.
|
|
|
|
|
|
expect(screen.getByRole("switch", { name: "Keep on top" })).toBeChecked();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("never mounts the iframe before the window's state is known", async () => {
|
|
|
|
|
|
// The status and the pop-out state are two separate round trips. If the
|
|
|
|
|
|
// status wins the race, guessing "not popped out" would flash a second
|
|
|
|
|
|
// viewer onto a browser the window is already driving.
|
|
|
|
|
|
checkBrowserViewSupport.mockResolvedValue({ ...READY, browsers: ["chromium-1200"] });
|
|
|
|
|
|
getBrowserViewStatus.mockResolvedValue(LIVE);
|
|
|
|
|
|
let answer: (s: { open: boolean; always_on_top: boolean }) => void = () => {};
|
|
|
|
|
|
getBrowserViewPopoutState.mockReturnValue(
|
|
|
|
|
|
new Promise((resolve) => {
|
|
|
|
|
|
answer = resolve;
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
render(<BrowserTab project={project} active />);
|
|
|
|
|
|
await waitFor(() => expect(screen.getByText("Live")).toBeInTheDocument());
|
|
|
|
|
|
expect(screen.queryByTitle(/browser view for/i)).toBeNull();
|
|
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
|
answer({ open: false, always_on_top: false });
|
|
|
|
|
|
});
|
|
|
|
|
|
expect(await screen.findByTitle("Playwright browser view for api-server")).toBeInTheDocument();
|
2026-08-11 06:50:40 -07:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("says why the window wouldn’t open instead of pretending it did", async () => {
|
|
|
|
|
|
await renderLive();
|
|
|
|
|
|
openBrowserViewPopout.mockRejectedValue("no display");
|
|
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: /own window/i }));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
expect(pushToast).toHaveBeenCalledWith(
|
|
|
|
|
|
expect.objectContaining({ kind: "error", detail: "no display" }),
|
|
|
|
|
|
);
|
|
|
|
|
|
// The view is still in the tab, where it was.
|
|
|
|
|
|
expect(screen.getByTitle("Playwright browser view for api-server")).toBeInTheDocument();
|
|
|
|
|
|
});
|
2026-08-09 16:55:28 -07:00
|
|
|
|
});
|