Skip to content
Closed
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
2 changes: 1 addition & 1 deletion frontend/e2e/history-nav.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ test("titlebar back/forward arrows traverse history", async ({ page }) => {
await expect(page.getByText("Projects")).toBeVisible();

// Navigate: home → session view (in-app push).
await page.getByRole("button", { name: "Open refactor-mux" }).click();
await page.getByRole("button", { name: "Open Split terminal mux responsibilities" }).click();
await expect(page).toHaveURL(/sessions\/refactor-mux/);

const back = page.getByRole("button", { name: "Go back" });
Expand Down
2 changes: 1 addition & 1 deletion frontend/e2e/inspector-toggle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { expect, test } from "@playwright/test";
// the real rrp + CSS pipeline, which the mocked unit tests can't exercise.
test("topbar button collapses and reopens the inspector rail", async ({ page }) => {
await page.goto("/");
await page.getByRole("button", { name: "Open refactor-mux" }).click();
await page.getByRole("button", { name: "Open Split terminal mux responsibilities" }).click();
await expect(page).toHaveURL(/sessions\/refactor-mux/);

// Fresh profile: the rail must mount open, not get toggled shut by
Expand Down
38 changes: 24 additions & 14 deletions frontend/e2e/workbench.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, test } from "@playwright/test";
import { expect, test, type Page } from "@playwright/test";

// The Playwright web server runs `dev:web` (VITE_NO_ELECTRON=1), so
// useWorkspaceQuery serves the deterministic preview fixtures from
Expand All @@ -7,24 +7,34 @@ import { expect, test } from "@playwright/test";

test("renders the orchestrator-first workbench shell", async ({ page }) => {
await page.goto("/");
// The single pinned Orchestrator anchor + the Projects group + a name-only worker row.
await expect(page.getByRole("button", { name: "Orchestrator", exact: true })).toBeVisible();
// The single pinned Orchestrator anchor + the Projects group + current title-based worker rows.
await expect(page.getByRole("button", { name: "Orchestrator board" })).toBeVisible();
await expect(page.getByText("Projects")).toBeVisible();
await expect(page.getByRole("button", { name: "fix-webgl-fallback", exact: true })).toBeVisible();
// Orchestrator side rail = the quiet Workers list.
await expect(page.getByText("Workers", { exact: true })).toBeVisible();
await expect(
page.getByRole("button", { name: "Open Restore fallback renderer after WebGL init fails" }),
).toBeVisible();
await expect(page.getByRole("button", { name: "Open Split terminal mux responsibilities" })).toBeVisible();
});

test("deep-links into a worker session", async ({ page }) => {
await page.goto("/#/workspaces/api-gateway/sessions/refactor-mux");
// Worker view = emdash three-pane with the Git review rail.
await expect(page.getByText("Changed")).toBeVisible();
await expect(page.getByRole("button", { name: /Commit & Push/ })).toBeVisible();
await page.goto("/#/projects/api-gateway/sessions/refactor-mux");
// Worker view = terminal preview plus current Summary inspector rail.
await expectSessionDetail(page);
});

test("drilling into a worker opens its Git review rail", async ({ page }) => {
test("drilling into a worker opens its session detail view", async ({ page }) => {
await page.goto("/");
await page.getByRole("button", { name: "refactor-mux", exact: true }).click();
await expect(page.getByRole("button", { name: /Commit & Push/ })).toBeVisible();
await expect(page.getByText("internal/mux/terminal_mux.go")).toBeVisible();
await page.getByRole("button", { name: "Open Split terminal mux responsibilities" }).click();
await expect(page).toHaveURL(/projects\/api-gateway\/sessions\/refactor-mux/);
await expectSessionDetail(page);
});

async function expectSessionDetail(page: Page) {
const inspector = page.getByTestId("inspector");
await expect(inspector).toBeVisible();
await expect(page.getByText("Split terminal mux responsibilities")).toBeVisible();
await expect(inspector.getByText("feat/refactor-mux")).toBeVisible();
await expect(
page.getByTestId("terminal").getByText("Browser preview renders a static terminal surface."),
).toBeVisible();
}
26 changes: 22 additions & 4 deletions frontend/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,33 @@
import { defineConfig } from "@playwright/test";

const e2ePort = parseE2EPort(process.env.PLAYWRIGHT_E2E_PORT);
const e2eHost = "127.0.0.1";
const webServerEnv = Object.fromEntries(
Object.entries(process.env).filter(
(entry): entry is [string, string] => entry[0].startsWith("VITE_") && typeof entry[1] === "string",
),
);
webServerEnv.VITE_AO_API_BASE_URL = `http://${e2eHost}:${e2ePort}`;

function parseE2EPort(value: string | undefined): number {
const port = Number(value ?? 5174);
if (!Number.isInteger(port) || port < 1 || port > 65535) {
throw new Error(`PLAYWRIGHT_E2E_PORT must be an integer TCP port, got ${value ?? "5174"}`);
}
return port;
}

export default defineConfig({
testDir: "e2e",
use: {
baseURL: "http://127.0.0.1:5173",
baseURL: `http://${e2eHost}:${e2ePort}`,
},
webServer: {
// dev:web serves the renderer alone (VITE_NO_ELECTRON=1) — no Electron child to
// launch, which is all the browser-based e2e suite needs.
command: "npm run dev:web -- --port 5173",
port: 5173,
reuseExistingServer: !process.env.CI,
command: `npm run dev:web -- --host ${e2eHost} --port ${e2ePort} --strictPort`,
env: webServerEnv,
port: e2ePort,
reuseExistingServer: false,
},
});
15 changes: 15 additions & 0 deletions frontend/src/renderer/lib/event-transport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,25 @@ beforeEach(() => {
});

afterEach(() => {
vi.unstubAllEnvs();
delete (globalThis as unknown as { EventSource?: unknown }).EventSource;
});

describe("createEventTransport", () => {
it("skips the SSE stream in deterministic browser preview mode", () => {
vi.stubEnv("VITE_NO_ELECTRON", "1");

const disconnect = createEventTransport(fakeQueryClient()).connect();

expect(EventSourceStub.instances).toHaveLength(0);
expect(onStatusMock).not.toHaveBeenCalled();
expect(subscribeApiBaseUrlMock).not.toHaveBeenCalled();
expect(getEventsConnectionState()).toBe("idle");

disconnect();
expect(getEventsConnectionState()).toBe("idle");
});

it("opens a single SSE connection to the current base URL on connect", () => {
createEventTransport(fakeQueryClient()).connect();

Expand Down
11 changes: 11 additions & 0 deletions frontend/src/renderer/lib/event-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ const SSE_RETRY_MS = 5_000;
// constants still work.
const EVENTSOURCE_CLOSED = 2;

function usesDeterministicPreviewData(): boolean {
return import.meta.env.VITE_NO_ELECTRON === "1";
}

// CDC event types the daemon pushes over the SSE stream (see
// backend/internal/cdc/event.go). The SSE writer tags each frame with
// `event: <type>`, so named events bypass EventSource.onmessage and must be
Expand All @@ -42,6 +46,13 @@ const CDC_EVENT_TYPES = [
export function createEventTransport(queryClient: QueryClient): EventTransport {
return {
connect() {
if (usesDeterministicPreviewData()) {
setEventsConnectionState("idle");
return () => {
setEventsConnectionState("idle");
};
}

let debounce: ReturnType<typeof setTimeout> | undefined;
let retryTimer: ReturnType<typeof setTimeout> | undefined;
let source: EventSource | undefined;
Expand Down
Loading