




















11// Control UI tests cover chat flow behavior.
2-import { chromium, type Browser, type Page } from "playwright";
3-import { afterAll, beforeAll, describe, expect, it } from "vitest";
2+import { chromium, type Browser, type BrowserContext, type Page } from "playwright";
3+import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
44import {
55canRunPlaywrightChromium,
66installMockGateway,
@@ -15,8 +15,9 @@ const chromiumAvailable = canRunPlaywrightChromium(chromiumExecutablePath);
1515const allowMissingChromium = process.env.OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM === "1";
1616const describeControlUiE2e = chromiumAvailable || !allowMissingChromium ? describe : describe.skip;
171718-let browser: Browser;
1918let server: ControlUiE2eServer;
19+const contextBrowsers = new WeakMap<BrowserContext, Browser>();
20+const openBrowserContexts = new Set<BrowserContext>();
20212122function requireRecord(value: unknown): Record<string, unknown> {
2223if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -89,6 +90,33 @@ async function scrollChatThreadToTop(page: Page): Promise<void> {
8990});
9091}
919293+async function newBrowserContext(options: Parameters<Browser["newContext"]>[0]) {
94+const browser = await chromium.launch({ executablePath: chromiumExecutablePath });
95+let context: BrowserContext | undefined;
96+try {
97+context = await browser.newContext(options);
98+contextBrowsers.set(context, browser);
99+openBrowserContexts.add(context);
100+return context;
101+} catch (error) {
102+await context?.close().catch(() => {});
103+await browser.close().catch(() => {});
104+throw error;
105+}
106+}
107+108+async function closeBrowserContext(context: BrowserContext): Promise<void> {
109+const browser = contextBrowsers.get(context);
110+openBrowserContexts.delete(context);
111+contextBrowsers.delete(context);
112+await context.close().catch(() => {});
113+await browser?.close().catch(() => {});
114+}
115+116+async function closeOpenBrowserContexts(): Promise<void> {
117+await Promise.all([...openBrowserContexts].map((context) => closeBrowserContext(context)));
118+}
119+92120async function controlUiEventPayloads(
93121page: Page,
94122event: string,
@@ -173,16 +201,19 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => {
173201);
174202}
175203server = await startControlUiE2eServer();
176-browser = await chromium.launch({ executablePath: chromiumExecutablePath });
177204});
178205179206afterAll(async () => {
180-await browser?.close();
207+await closeOpenBrowserContexts();
181208await server?.close();
182209});
183210211+afterEach(async () => {
212+await closeOpenBrowserContexts();
213+});
214+184215it("sends a chat turn through the GUI and renders the final Gateway event", async () => {
185-const context = await browser.newContext({
216+const context = await newBrowserContext({
186217locale: "en-US",
187218serviceWorkers: "block",
188219viewport: { height: 900, width: 1280 },
@@ -217,12 +248,12 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => {
217248218249await page.getByText("Harness verified.").waitFor({ timeout: 10_000 });
219250} finally {
220-await context.close();
251+await closeBrowserContext(context);
221252}
222253});
223254224255it("copies a code block over a non-secure context via the execCommand fallback", async () => {
225-const context = await browser.newContext({
256+const context = await newBrowserContext({
226257locale: "en-US",
227258serviceWorkers: "block",
228259viewport: { height: 900, width: 1280 },
@@ -275,12 +306,12 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => {
275306expect(copied).toContain(code);
276307expect(await gateway.getRequests("chat.send")).toHaveLength(0);
277308} finally {
278-await context.close();
309+await closeBrowserContext(context);
279310}
280311});
281312282313it("starts the workspace files panel collapsed and toggles it open", async () => {
283-const context = await browser.newContext({
314+const context = await newBrowserContext({
284315locale: "en-US",
285316serviceWorkers: "block",
286317viewport: { height: 900, width: 1280 },
@@ -372,12 +403,12 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => {
372403await page.setViewportSize({ height: 900, width: 1000 });
373404expect(await page.locator(".chat-workspace-rail").isHidden()).toBe(true);
374405} finally {
375-await context.close();
406+await closeBrowserContext(context);
376407}
377408});
378409379410it("renders stable markdown during a streaming chat turn and finalizes the tail", async () => {
380-const context = await browser.newContext({
411+const context = await newBrowserContext({
381412locale: "en-US",
382413serviceWorkers: "block",
383414viewport: { height: 900, width: 1280 },
@@ -436,12 +467,12 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => {
436467await page.locator(".chat-thread strong").getByText("tail").waitFor({ timeout: 10_000 });
437468expect(await page.locator(".markdown-plain-text-fallback").count()).toBe(0);
438469} finally {
439-await context.close();
470+await closeBrowserContext(context);
440471}
441472});
442473443474it("keeps chat usable while sessions are still loading", async () => {
444-const context = await browser.newContext({
475+const context = await newBrowserContext({
445476locale: "en-US",
446477serviceWorkers: "block",
447478viewport: { height: 900, width: 1280 },
@@ -483,12 +514,12 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => {
483514timeout: 10_000,
484515});
485516} finally {
486-await context.close();
517+await closeBrowserContext(context);
487518}
488519});
489520490521it("sends the first chat turn while agents startup loading is still pending", async () => {
491-const context = await browser.newContext({
522+const context = await newBrowserContext({
492523locale: "en-US",
493524serviceWorkers: "block",
494525viewport: { height: 900, width: 1280 },
@@ -650,12 +681,12 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => {
650681await gateway.waitForRequest("commands.list");
651682expect(await gateway.getRequests("agents.list")).toHaveLength(0);
652683} finally {
653-await context.close();
684+await closeBrowserContext(context);
654685}
655686});
656687657688it("keeps streamed text visible when a chat error terminates the turn", async () => {
658-const context = await browser.newContext({
689+const context = await newBrowserContext({
659690locale: "en-US",
660691serviceWorkers: "block",
661692viewport: { height: 900, width: 1280 },
@@ -697,12 +728,12 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => {
697728await page.getByText(partialText).waitFor({ timeout: 10_000 });
698729await page.getByText("Error: gateway disconnected").waitFor({ timeout: 10_000 });
699730} finally {
700-await context.close();
731+await closeBrowserContext(context);
701732}
702733});
703734704735it("keeps a delayed chat.send ACK visible as pending until the ACK resolves", async () => {
705-const context = await browser.newContext({
736+const context = await newBrowserContext({
706737locale: "en-US",
707738serviceWorkers: "block",
708739viewport: { height: 900, width: 1280 },
@@ -736,12 +767,12 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => {
736767await page.locator(".chat-queue").waitFor({ state: "detached", timeout: 10_000 });
737768await page.locator(".chat-thread").getByText(prompt).waitFor({ timeout: 10_000 });
738769} finally {
739-await context.close();
770+await closeBrowserContext(context);
740771}
741772});
742773743774it("scrolls a delayed pending send into view before the ACK resolves", async () => {
744-const context = await browser.newContext({
775+const context = await newBrowserContext({
745776locale: "en-US",
746777serviceWorkers: "block",
747778viewport: { height: 900, width: 1280 },
@@ -792,12 +823,12 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => {
792823793824await gateway.resolveDeferred("chat.send", { runId, status: "started" });
794825} finally {
795-await context.close();
826+await closeBrowserContext(context);
796827}
797828});
798829799830it("keeps rejected pre-ACK sends visible and restores the draft", async () => {
800-const context = await browser.newContext({
831+const context = await newBrowserContext({
801832locale: "en-US",
802833serviceWorkers: "block",
803834viewport: { height: 900, width: 1280 },
@@ -824,12 +855,12 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => {
824855await page.locator(".chat-queue").getByText(prompt).waitFor({ timeout: 10_000 });
825856expect(await composer.inputValue()).toBe(prompt);
826857} finally {
827-await context.close();
858+await closeBrowserContext(context);
828859}
829860});
830861831862it("retries an ACK-lost send after reconnect with the same idempotency key", async () => {
832-const context = await browser.newContext({
863+const context = await newBrowserContext({
833864locale: "en-US",
834865serviceWorkers: "block",
835866viewport: { height: 900, width: 1280 },
@@ -857,12 +888,12 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => {
857888expect(secondParams.message).toBe(prompt);
858889await page.locator(".chat-queue").waitFor({ state: "detached", timeout: 10_000 });
859890} finally {
860-await context.close();
891+await closeBrowserContext(context);
861892}
862893});
863894864895it("keeps a session model override selected after switching away and back", async () => {
865-const context = await browser.newContext({
896+const context = await newBrowserContext({
866897locale: "en-US",
867898serviceWorkers: "block",
868899viewport: { height: 900, width: 1280 },
@@ -929,12 +960,12 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => {
929960"bedrock/claude-opus-4.5",
930961);
931962} finally {
932-await context.close();
963+await closeBrowserContext(context);
933964}
934965});
935966936967it("shows a pending send while a model override save is still pending", async () => {
937-const context = await browser.newContext({
968+const context = await newBrowserContext({
938969locale: "en-US",
939970serviceWorkers: "block",
940971viewport: { height: 900, width: 1280 },
@@ -976,12 +1007,12 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => {
9761007expect(params.message).toBe(prompt);
9771008expect(params.sessionKey).toBe("agent:main:session-a");
9781009} finally {
979-await context.close();
1010+await closeBrowserContext(context);
9801011}
9811012});
98210139831014it("refreshes history after a tool-call window disconnects and reconnects", async () => {
984-const context = await browser.newContext({
1015+const context = await newBrowserContext({
9851016locale: "en-US",
9861017serviceWorkers: "block",
9871018viewport: { height: 900, width: 1280 },
@@ -1032,7 +1063,7 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => {
10321063await page.getByText("Recovered from refreshed history.").waitFor({ timeout: 15_000 });
10331064expect(await page.locator(".chat-queue").count()).toBe(0);
10341065} finally {
1035-await context.close();
1066+await closeBrowserContext(context);
10361067}
10371068});
10381069});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。