diff --git a/ui/src/app/app-host.ts b/ui/src/app/app-host.ts index bac2cf6762d0..16cddd13394a 100644 --- a/ui/src/app/app-host.ts +++ b/ui/src/app/app-host.ts @@ -812,6 +812,12 @@ class OpenClawShell extends OpenClawLightDomElement { resolved?.focus(); } + private visibleNavDrawerToggle(): HTMLElement | undefined { + return [ + ...this.querySelectorAll(".topbar-nav-toggle, .chat-pane__nav-toggle"), + ].find((candidate) => candidate.checkVisibility()); + } + private closeNavDrawer(options: { restoreFocus?: boolean } = {}) { if (this.navDrawerOpen) { this.dismissSidebarTransientMenus(); @@ -884,14 +890,14 @@ class OpenClawShell extends OpenClawLightDomElement { }; private readonly handleWindowResize = () => { - const dismissedHiddenMenus = - isMobileNavLayout() && !this.navDrawerOpen && this.dismissSidebarTransientMenus(); this.requestUpdate(); - if (dismissedHiddenMenus) { - void this.updateComplete.then(() => { - this.restoreFocusTo(this.querySelector(".topbar-nav-toggle")); - }); - } + void this.updateComplete.then(() => { + if (isMobileNavLayout() && !this.navDrawerOpen && this.dismissSidebarTransientMenus()) { + requestAnimationFrame(() => { + this.restoreFocusTo(this.visibleNavDrawerToggle()); + }); + } + }); }; private dismissSidebarTransientMenus(): boolean { diff --git a/ui/src/e2e/agent-page-scope.e2e.test.ts b/ui/src/e2e/agent-page-scope.e2e.test.ts index 42b488fbcaba..ca293856e1a5 100644 --- a/ui/src/e2e/agent-page-scope.e2e.test.ts +++ b/ui/src/e2e/agent-page-scope.e2e.test.ts @@ -114,14 +114,16 @@ describeControlUiE2e("Control UI agent page scope", () => { await page.goto(`${server.baseUrl}usage`); await gateway.waitForRequest("agents.list"); const sidebar = page.locator("openclaw-app-sidebar"); - await sidebar.getByRole("button", { name: /Agent menu/ }).click(); + await sidebar.getByRole("button", { name: /Switch agent/ }).click(); await sidebar .locator("wa-dropdown.sidebar-agent-menu") .locator('wa-dropdown-item[value="agent:writer"]') .click(); await waitForRequest(gateway, "sessions.list", (params) => params.agentId === "writer"); await expect - .poll(() => sidebar.locator(".sidebar-agent-card__name").textContent()) + .poll(async () => + (await sidebar.locator(".sidebar-agent-card__name").textContent())?.trim(), + ) .toBe("Writer"); await sidebar.getByRole("link", { name: "Usage" }).click(); @@ -142,11 +144,13 @@ describeControlUiE2e("Control UI agent page scope", () => { }) .toBe(true); await expect - .poll(() => sidebar.locator(".sidebar-agent-card__name").textContent()) + .poll(async () => + (await sidebar.locator(".sidebar-agent-card__name").textContent())?.trim(), + ) .toBe("Writer"); await screenshot(page, "02-all-agents-usage.png"); - await sidebar.getByRole("button", { name: /Agent menu/ }).click(); + await sidebar.getByRole("button", { name: /Switch agent/ }).click(); await sidebar .locator("wa-dropdown.sidebar-agent-menu") .locator('wa-dropdown-item[value="command:agent-settings"]') diff --git a/ui/src/e2e/agent-select-avatar-timeout.e2e.test.ts b/ui/src/e2e/agent-select-avatar-timeout.e2e.test.ts index 62fafa016053..bebfc5611595 100644 --- a/ui/src/e2e/agent-select-avatar-timeout.e2e.test.ts +++ b/ui/src/e2e/agent-select-avatar-timeout.e2e.test.ts @@ -74,7 +74,7 @@ describeControlUiE2e("Control UI agent picker avatar timeout", () => { avatarAuthorization = route.request().headers().authorization; // Leave the route unanswered. The page-owned deadline must cancel it. }); - await installMockGateway(page, { + const gateway = await installMockGateway(page, { methodResponses: { "agent.identity.get": { agentId: "main", @@ -88,11 +88,12 @@ describeControlUiE2e("Control UI agent picker avatar timeout", () => { try { const response = await page.goto(`${server.baseUrl}agents`); expect(response?.status()).toBe(200); + await gateway.waitForRequest("agent.identity.get"); + await expect.poll(() => avatarRequestCount).toBe(1); const picker = page.locator("openclaw-agent-select"); await expect .poll(() => picker.locator(".agent-select__avatar--text").first().textContent()) .toBe("O"); - expect(avatarRequestCount).toBe(1); expect(avatarAuthorization).toBe("Bearer e2e-device-token"); await screenshot(page, "01-request-stalled.png"); diff --git a/ui/src/e2e/app-chrome-interaction.e2e.test.ts b/ui/src/e2e/app-chrome-interaction.e2e.test.ts index 4ec7dda7d097..302b1b5ad52b 100644 --- a/ui/src/e2e/app-chrome-interaction.e2e.test.ts +++ b/ui/src/e2e/app-chrome-interaction.e2e.test.ts @@ -116,6 +116,7 @@ describeControlUiE2e("Control UI app chrome interaction mocked Gateway E2E", () expect(await dragAcross(page, transcript)).toContain("Selectable transcript"); await captureUiProof(page, "01-chat-selectable-transcript.png"); + await page.setViewportSize({ height: 650, width: 1440 }); await page.goto(`${server.baseUrl}settings/general`); const settingsSidebar = page.locator(".settings-sidebar"); const settingsTitle = settingsSidebar.locator(".settings-sidebar__title"); diff --git a/ui/src/e2e/approval-flow.e2e.test.ts b/ui/src/e2e/approval-flow.e2e.test.ts index 35682ff619e6..22e8d9726784 100644 --- a/ui/src/e2e/approval-flow.e2e.test.ts +++ b/ui/src/e2e/approval-flow.e2e.test.ts @@ -108,7 +108,7 @@ describeControlUiE2e("Control UI approval flow", () => { await currentPage.getByRole("button", { name: "Stop generating" }).waitFor(); await composer.fill("/approve approval-123 allow-once"); - await currentPage.getByRole("button", { name: "Steer into the active run" }).click(); + await currentPage.getByRole("button", { name: "Send message" }).click(); await expect .poll(async () => (await gateway.getRequests("chat.send")).length, { timeout: 10_000 }) diff --git a/ui/src/e2e/browser-talk-start-stop.e2e.test.ts b/ui/src/e2e/browser-talk-start-stop.e2e.test.ts index 7c8765b79dcd..572099c629b3 100644 --- a/ui/src/e2e/browser-talk-start-stop.e2e.test.ts +++ b/ui/src/e2e/browser-talk-start-stop.e2e.test.ts @@ -20,6 +20,15 @@ let server: ControlUiE2eServer; // Browser contexts preserve test isolation; keep one process warm for this file. let browser: Browser; +function videoTalkCatalog(activeProvider: "google" | "openai") { + return { + realtime: { + activeProvider, + providers: [{ id: activeProvider, label: activeProvider, supportsVideoFrames: true }], + }, + }; +} + async function installTalkBrowserFixtures(page: Page) { await page.addInitScript(() => { type InputProcessor = { @@ -474,11 +483,12 @@ describeControlUiE2e("Control UI browser Talk", () => { } }); - it("starts OpenAI Video Talk, previews a fake camera, and submits describe_view", async () => { + it("starts OpenAI Talk, enables a fake camera, and submits describe_view", async () => { const context = await browser.newContext({ permissions: ["camera", "microphone"] }); const page = await context.newPage(); const gateway = await installMockGateway(page, { methodResponses: { + "talk.catalog": videoTalkCatalog("openai"), "talk.client.create": { provider: "openai", transport: "webrtc", @@ -528,14 +538,25 @@ describeControlUiE2e("Control UI browser Talk", () => { super(); ( window as Window & { - openclawVideoTalkE2e?: { peer: FakePeerConnection }; + openclawVideoTalkE2e?: { + dataChannelCreated: boolean; + peer: FakePeerConnection; + }; } - ).openclawVideoTalkE2e = { peer: this }; + ).openclawVideoTalkE2e = { dataChannelCreated: false, peer: this }; } addTrack() {} createDataChannel() { + const harness = ( + window as Window & { + openclawVideoTalkE2e?: { dataChannelCreated: boolean }; + } + ).openclawVideoTalkE2e; + if (harness) { + harness.dataChannelCreated = true; + } return this.channel; } @@ -570,13 +591,36 @@ describeControlUiE2e("Control UI browser Talk", () => { await page.goto(`${server.baseUrl}chat`); await captureVideoTalkProof(page, "01-before-video-talk.png"); - await page.getByRole("button", { name: "Start video talk" }).click(); + await page.getByRole("button", { name: "Start voice input" }).click(); const request = await gateway.waitForRequest("talk.client.create"); expect(request.params).toMatchObject({ - capabilities: ["camera-frame"], sessionKey: "main", }); console.info("[video-talk-e2e] session=provider:openai,transport:webrtc"); + await expect + .poll(() => + page.evaluate(() => + Boolean( + ( + window as Window & { + openclawVideoTalkE2e?: { dataChannelCreated: boolean }; + } + ).openclawVideoTalkE2e?.dataChannelCreated, + ), + ), + ) + .toBe(true); + await page.evaluate(() => { + const channel = ( + window as Window & { + openclawVideoTalkE2e?: { peer: { channel: EventTarget } }; + } + ).openclawVideoTalkE2e?.peer.channel; + channel?.dispatchEvent(new Event("open")); + }); + const turnCameraOn = page.getByRole("button", { name: "Turn camera on" }); + await expect.poll(() => turnCameraOn.isEnabled()).toBe(true); + await turnCameraOn.click(); const preview = page.locator('video[aria-label="Camera preview"]'); await expect.poll(() => preview.isVisible()).toBe(true); await expect @@ -591,14 +635,6 @@ describeControlUiE2e("Control UI browser Talk", () => { console.info( `[video-talk-e2e] preview=live,width:${dimensions.width},height:${dimensions.height}`, ); - await page.evaluate(() => { - const channel = ( - window as Window & { - openclawVideoTalkE2e?: { peer: { channel: EventTarget } }; - } - ).openclawVideoTalkE2e?.peer.channel; - channel?.dispatchEvent(new Event("open")); - }); await captureVideoTalkProof(page, "02-live-camera-preview.png"); await page.evaluate(() => { @@ -647,7 +683,10 @@ describeControlUiE2e("Control UI browser Talk", () => { const talkRequests = (await gateway.getRequests()).filter((entry) => entry.method.startsWith("talk."), ); - expect(talkRequests.map((entry) => entry.method)).toEqual(["talk.client.create"]); + expect(talkRequests.map((entry) => entry.method)).toEqual([ + "talk.catalog", + "talk.client.create", + ]); console.info( "[video-talk-e2e] describe_view=input_image+function_output+response_create,gateway_frame_requests:0", ); @@ -670,11 +709,12 @@ describeControlUiE2e("Control UI browser Talk", () => { } }); - it("starts Gemini Live Video Talk, streams a fake camera directly, and handles describe_view", async () => { + it("starts Gemini Live Talk, enables a fake camera, and handles describe_view", async () => { const context = await browser.newContext({ permissions: ["camera", "microphone"] }); const page = await context.newPage(); const gateway = await installMockGateway(page, { methodResponses: { + "talk.catalog": videoTalkCatalog("google"), "talk.client.create": { provider: "google", transport: "provider-websocket", @@ -740,12 +780,14 @@ describeControlUiE2e("Control UI browser Talk", () => { try { await page.setViewportSize({ width: 1366, height: 900 }); await page.goto(`${server.baseUrl}chat`); - await page.getByRole("button", { name: "Start video talk" }).click(); + await page.getByRole("button", { name: "Start voice input" }).click(); const request = await gateway.waitForRequest("talk.client.create"); expect(request.params).toMatchObject({ - capabilities: ["camera-frame"], sessionKey: "main", }); + const turnCameraOn = page.getByRole("button", { name: "Turn camera on" }); + await expect.poll(() => turnCameraOn.isEnabled()).toBe(true); + await turnCameraOn.click(); const preview = page.locator('video[aria-label="Camera preview"]'); await expect.poll(() => preview.isVisible()).toBe(true); await expect @@ -786,7 +828,10 @@ describeControlUiE2e("Control UI browser Talk", () => { const talkRequests = (await gateway.getRequests()).filter((entry) => entry.method.startsWith("talk."), ); - expect(talkRequests.map((entry) => entry.method)).toEqual(["talk.client.create"]); + expect(talkRequests.map((entry) => entry.method)).toEqual([ + "talk.catalog", + "talk.client.create", + ]); await captureVideoTalkProof(page, "05-gemini-live-camera-preview.png"); console.info( "[video-talk-e2e] gemini=realtimeInput.video+functionResponse,gateway_frame_requests:0", @@ -814,6 +859,7 @@ describeControlUiE2e("Control UI browser Talk", () => { const page = await context.newPage(); const gateway = await installMockGateway(page, { methodResponses: { + "talk.catalog": videoTalkCatalog("google"), "talk.client.create": { provider: "google", transport: "provider-websocket", @@ -831,19 +877,32 @@ describeControlUiE2e("Control UI browser Talk", () => { }, }, }); + await page.routeWebSocket("wss://generativelanguage.googleapis.com/**", (ws) => { + ws.onMessage((message) => { + const parsed = JSON.parse(typeof message === "string" ? message : message.toString()) as { + setup?: unknown; + }; + if (parsed.setup) { + ws.send(JSON.stringify({ setupComplete: {} })); + } + }); + }); await installBlockedVideoTalkFixture(page); try { await page.setViewportSize({ width: 1366, height: 900 }); await page.goto(`${server.baseUrl}chat`); - await page.getByRole("button", { name: "Start video talk" }).click(); + await page.getByRole("button", { name: "Start voice input" }).click(); await gateway.waitForRequest("talk.client.create"); + const turnCameraOn = page.getByRole("button", { name: "Turn camera on" }); + await expect.poll(() => turnCameraOn.isEnabled()).toBe(true); + await turnCameraOn.click(); const alert = page.getByRole("alert"); await expect.poll(() => alert.textContent()).toContain("Camera access is blocked."); await expect.poll(() => page.locator('video[aria-label="Camera preview"]').count()).toBe(0); await expect - .poll(() => page.getByRole("button", { name: "Start video talk" }).isVisible()) + .poll(() => page.getByRole("button", { name: "Turn camera on" }).isVisible()) .toBe(true); await captureVideoTalkProof(page, "03-camera-permission-blocked.png"); console.info("[video-talk-e2e] camera_denial=actionable,no-audio-fallback"); diff --git a/ui/src/e2e/build-info-unicode.e2e.test.ts b/ui/src/e2e/build-info-unicode.e2e.test.ts index 613110812762..3a7fcef8e259 100644 --- a/ui/src/e2e/build-info-unicode.e2e.test.ts +++ b/ui/src/e2e/build-info-unicode.e2e.test.ts @@ -47,11 +47,9 @@ function containsBrokenSurrogate(value: string): boolean { } async function openBuildDetails(page: Page) { - const sidebar = page.locator("openclaw-app-sidebar"); - const agentMenu = sidebar.getByRole("button", { name: /Agent menu/ }); - await agentMenu.waitFor(); - await agentMenu.click(); - const buildLink = page.getByRole("link", { name: "Control UI build details", exact: true }); + const buildLink = page + .locator("openclaw-app-sidebar") + .getByRole("link", { name: "Control UI build details", exact: true }); await buildLink.waitFor(); const compactText = (await buildLink.textContent()) ?? ""; expect(compactText).toContain(`${COMPACT_BRANCH}@0123456`); diff --git a/ui/src/e2e/chat-flow.e2e.test.ts b/ui/src/e2e/chat-flow.e2e.test.ts index 819e25cab7c3..5eaba56ca1e7 100644 --- a/ui/src/e2e/chat-flow.e2e.test.ts +++ b/ui/src/e2e/chat-flow.e2e.test.ts @@ -377,13 +377,12 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => { // Keyboard focus on a header action marks the pane active. await headers.first().getByRole("button", { name: "Split down" }).focus(); - await expect.poll(() => headers.first().getAttribute("class")).toContain("--active"); - const cells = page.locator(".chat-split-view__cell"); + await expect.poll(() => cells.first().getAttribute("class")).toContain("--active"); + const lastPane = page.locator(".chat-split-view__pane").last(); await lastPane.click({ position: { x: 20, y: 80 } }); await expect.poll(() => cells.last().getAttribute("class")).toContain("--active"); - await expect.poll(() => headers.last().getAttribute("class")).toContain("--active"); const targetHeader = headers.first(); await expect .poll(() => @@ -722,6 +721,7 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => { viewport: { height: 900, width: 1280 }, }); const page = await context.newPage(); + const sessionKey = "main"; const gateway = await installMockGateway(page, { historyMessages: [ { @@ -734,28 +734,30 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => { "sessions.list": chatSessionListResponse([ { hasActiveRun: true, - key: "main", + key: "agent:main:main", kind: "direct", label: "Main", updatedAt: Date.now(), }, ]), }, + sessionKey, }); try { await page.goto(`${server.baseUrl}chat`); await page.getByText("Active run is waiting for steering.").waitFor({ timeout: 10_000 }); await gateway.waitForRequest("sessions.list"); + await page.getByRole("button", { name: "Stop generating" }).waitFor({ timeout: 10_000 }); await page .locator(".agent-chat__composer-combobox textarea") .fill("/steer use the smaller fix"); - await page.getByRole("button", { name: "Steer into the active run" }).click(); + await page.getByRole("button", { name: "Send message" }).click(); const steerRequest = await gateway.waitForRequest("chat.send"); const params = requireRecord(steerRequest.params); - expect(params.sessionKey).toBe("main"); + expect(params.sessionKey).toBe(sessionKey); expect(params.message).toBe("use the smaller fix"); expect(params.deliver).toBe(false); @@ -1865,13 +1867,11 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => { // The background hydrate must not take the shared sessions loading // flag, which would disable New thread for the whole request. - expect(await page.getByRole("button", { name: "New thread" }).first().isEnabled()).toBe(true); + const newThread = page.getByRole("button", { name: "New thread" }).first(); + expect(await newThread.isEnabled()).toBe(true); await gateway.resolveDeferred("sessions.list"); - await page - .locator(".sidebar-recent-session", { hasText: "Main" }) - .first() - .waitFor({ state: "visible", timeout: 10_000 }); + await expect.poll(() => newThread.isEnabled()).toBe(true); } finally { await closeBrowserContext(context); } @@ -2384,6 +2384,7 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => { viewport: { height: 900, width: 1280 }, }); const page = await context.newPage(); + const sessionKey = "main"; const runtimeConfig = { messages: { queue: { byChannel: { webchat: "steer" }, mode: "steer" } }, }; @@ -2400,7 +2401,7 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => { "sessions.list": chatSessionListResponse([ { effectiveQueueMode: "interrupt", - key: "main", + key: "agent:main:main", kind: "direct", label: "Main", queueMode: "interrupt", @@ -2408,6 +2409,14 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => { }, ]), }, + sessionInfo: { + effectiveQueueMode: "interrupt", + hasActiveRun: false, + key: "agent:main:main", + queueMode: "interrupt", + status: "done", + }, + sessionKey, }); try { @@ -2426,7 +2435,7 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => { expect(requireRecord(sends[1]?.params)).toMatchObject({ message: followUp, queueMode: "interrupt", - sessionKey: "main", + sessionKey, }); } finally { await closeBrowserContext(context); @@ -3450,7 +3459,7 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => { .waitFor({ timeout: 10_000, }); - await expect.poll(() => sidebarSessionOrder(page)).toEqual(createdOrder.slice(0, 10)); + await expect.poll(() => sidebarSessionOrder(page)).toEqual(createdOrder.slice(0, 11)); await page.getByRole("button", { name: "Load more" }).click(); await expect.poll(() => sidebarSessionOrder(page)).toEqual(createdOrder); @@ -3474,15 +3483,19 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => { .evaluate((label) => getComputedStyle(label).fontWeight); expect(activeWeight).toBe(inactiveWeight); - await page.getByRole("button", { name: "Sort threads" }).click(); + const sortThreads = page.getByRole("button", { name: "Sort threads" }); + await sortThreads.locator("..").hover(); + await sortThreads.click(); await page.getByRole("menuitemradio", { name: "Last updated" }).click(); await expect.poll(() => sidebarSessionOrder(page)).toEqual(updatedOrder); - await page.getByRole("button", { name: "Sort threads" }).click(); + await sortThreads.locator("..").hover(); + await sortThreads.click(); await page.getByRole("menuitemradio", { name: "Created" }).click(); await expect.poll(() => sidebarSessionOrder(page)).toEqual(createdOrder); - await page.getByRole("button", { name: "Sort threads" }).click(); + await sortThreads.locator("..").hover(); + await sortThreads.click(); await page.getByRole("main").click(); await expect.poll(() => page.getByRole("menuitemradio", { name: "Created" }).count()).toBe(0); } finally { @@ -3564,9 +3577,7 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => { expect(await row.getAttribute("aria-label")).toBeNull(); expect(await link.getAttribute("aria-label")).toBeNull(); expect(await link.getAttribute("aria-current")).toBe("page"); - const descriptionId = await link.getAttribute("aria-describedby"); - expect(descriptionId).toBeTruthy(); - expect(await page.locator(`[id="${descriptionId}"]`).textContent()).toBeTruthy(); + expect(await link.getAttribute("aria-describedby")).toBeNull(); expect(await link.ariaSnapshot()).toContain(`link "${readableTitle}"`); await captureSessionAccessibilityProof(page, "after-derived-title"); diff --git a/ui/src/e2e/chat-run-lifecycle.e2e.test.ts b/ui/src/e2e/chat-run-lifecycle.e2e.test.ts index ca6e41c9c57b..af502fe80c4f 100644 --- a/ui/src/e2e/chat-run-lifecycle.e2e.test.ts +++ b/ui/src/e2e/chat-run-lifecycle.e2e.test.ts @@ -115,7 +115,7 @@ describeControlUiE2e("Control UI chat run lifecycle", () => { const runId = params.idempotencyKey as string; await currentPage.getByRole("button", { name: "Stop generating" }).waitFor(); - const mainSession = currentPage.locator(".sidebar-recent-session").filter({ hasText: "Main" }); + const mainSession = currentPage.locator(".nav-item--home"); await mainSession.waitFor({ state: "visible" }); const sessionListsBeforeActive = (await gateway.getRequests("sessions.list")).length; await gateway.deferNext("sessions.list"); @@ -161,7 +161,7 @@ describeControlUiE2e("Control UI chat run lifecycle", () => { ], ts: activeUpdatedAt, }); - await currentPage.getByText(staleActiveLabel, { exact: true }).waitFor(); + await currentPage.locator(".chat-pane__session-title", { hasText: staleActiveLabel }).waitFor(); expect(await currentPage.getByRole("button", { name: "Stop generating" }).count()).toBe(0); await expect.poll(() => mainSession.locator(".session-run-spinner").count()).toBe(0); @@ -258,7 +258,7 @@ describeControlUiE2e("Control UI chat run lifecycle", () => { const runId = params.idempotencyKey as string; await currentPage.getByRole("button", { name: "Stop generating" }).waitFor(); - const mainSession = currentPage.locator(".sidebar-recent-session").filter({ hasText: "Main" }); + const mainSession = currentPage.locator(".nav-item--home"); await mainSession.waitFor({ state: "visible" }); const sessionListsBeforeActive = (await gateway.getRequests("sessions.list")).length; await gateway.deferNext("sessions.list"); diff --git a/ui/src/e2e/claude-sessions.e2e.test.ts b/ui/src/e2e/claude-sessions.e2e.test.ts index 8fc47afae888..b337c3d1b7ca 100644 --- a/ui/src/e2e/claude-sessions.e2e.test.ts +++ b/ui/src/e2e/claude-sessions.e2e.test.ts @@ -205,8 +205,17 @@ function hostGroupedNativeCatalogs() { return { catalogs: [catalog("claude", "Claude Code"), catalog("codex", "Codex")] }; } +async function expandCodingSection(page: Page) { + const toggle = page.locator('[data-session-section="work"] .sidebar-session-group-toggle'); + await toggle.waitFor({ state: "visible" }); + if ((await toggle.getAttribute("aria-expanded")) === "false") { + await toggle.click(); + } +} + async function openClaudeCatalogTerminal(page: Page) { await page.goto(`${server.baseUrl}chat`); + await expandCodingSection(page); const row = page.locator('[data-session-key^="catalog:"]').filter({ hasText: "Native Claude terminal", }); @@ -237,6 +246,7 @@ suite("Claude native session catalog", () => { try { await page.goto(`${server.baseUrl}chat`); + await expandCodingSection(page); for (const catalogId of ["claude", "codex"]) { const section = page.locator(`[data-session-section="catalog:${catalogId}"]`); const gatewayHost = section.locator('[data-session-catalog-host="gateway:local"]'); @@ -459,6 +469,7 @@ suite("Claude native session catalog", () => { }, }); await page.goto(`${server.baseUrl}chat`); + await expandCodingSection(page); await page.getByRole("button", { name: "Load more threads" }).click(); await page.getByText("Older remote review", { exact: true }).waitFor(); expect((await gateway.getRequests("sessions.catalog.list")).at(-1)?.params).toEqual({ @@ -508,7 +519,7 @@ suite("Claude native session catalog", () => { expectStableVirtualRowPrepend(anchor, await finishVirtualRowPrependProbe(thread)); expect(await page.locator(".agent-chat__composer-combobox > textarea").isDisabled()).toBe(true); await expect - .poll(() => page.getByText("This session is on a paired node and is view-only.").count()) + .poll(() => page.getByText("This thread is on a paired node and is view-only.").count()) .toBe(1); const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim(); const expectCenteredLayout = async (screenshotName: string) => { diff --git a/ui/src/e2e/codex-sessions.e2e.test.ts b/ui/src/e2e/codex-sessions.e2e.test.ts index 10f638f9f8d9..be7c7036171f 100644 --- a/ui/src/e2e/codex-sessions.e2e.test.ts +++ b/ui/src/e2e/codex-sessions.e2e.test.ts @@ -1,6 +1,6 @@ import { mkdir } from "node:fs/promises"; import path from "node:path"; -import { chromium, type Browser } from "playwright"; +import { chromium, type Browser, type Page } from "playwright"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import type { SessionsCatalogHostEvent } from "../../../packages/gateway-protocol/src/index.ts"; import { @@ -28,6 +28,14 @@ const uiProofArtifactDir = path.join( "native-session-discovery", ); +async function expandCodingSection(page: Page) { + const toggle = page.locator('[data-session-section="work"] .sidebar-session-group-toggle'); + await toggle.waitFor({ state: "visible" }); + if ((await toggle.getAttribute("aria-expanded")) === "false") { + await toggle.click(); + } +} + suite("Codex native session catalog", () => { beforeAll(async () => { if (!available) { @@ -135,6 +143,7 @@ suite("Codex native session catalog", () => { }, } satisfies SessionsCatalogHostEvent); + await expandCodingSection(page); await page.getByText("Progressive node result", { exact: true }).waitFor(); expect((await gateway.getRequests("sessions.catalog.list")).length).toBe(1); if (captureUiProofEnabled) { @@ -245,6 +254,7 @@ suite("Codex native session catalog", () => { try { await page.goto(`${server.baseUrl}chat`); + await expandCodingSection(page); const section = page.locator('[data-session-section="catalog:codex"]'); await section.waitFor({ state: "visible" }); await expect.poll(() => section.locator("[data-session-catalog-host]").count()).toBe(2); @@ -441,6 +451,7 @@ suite("Codex native session catalog", () => { try { await page.goto(`${server.baseUrl}chat`); + await expandCodingSection(page); const warning = page.locator( '[data-session-section="catalog:codex"] .sidebar-session-group-toggle', ); @@ -555,6 +566,7 @@ suite("Codex native session catalog", () => { try { await page.goto(`${server.baseUrl}chat`); + await expandCodingSection(page); await expect .poll(async () => (await gateway.getRequests("sessions.catalog.list")).length) .toBe(1); @@ -627,6 +639,7 @@ suite("Codex native session catalog", () => { }, }); await page.goto(`${server.baseUrl}chat`); + await expandCodingSection(page); await page.getByText("Release checklist", { exact: true }).click(); await expect.poll(() => page.getByText("prepare release", { exact: true }).count()).toBe(1); const composer = page.locator(".agent-chat__composer-combobox > textarea"); diff --git a/ui/src/e2e/config-form-guidance.e2e.test.ts b/ui/src/e2e/config-form-guidance.e2e.test.ts index 61198d599fd0..9eeaa7d3925f 100644 --- a/ui/src/e2e/config-form-guidance.e2e.test.ts +++ b/ui/src/e2e/config-form-guidance.e2e.test.ts @@ -90,10 +90,9 @@ describeControlUiE2e("Control UI config form guidance mocked Gateway E2E", () => }); try { - const response = await page.goto(`${server.baseUrl}settings/general`); + const response = await page.goto(`${server.baseUrl}settings/advanced`); expect(response?.status()).toBe(200); - await page.locator('.content-header wa-radio[value="advanced"]').click(); await page.getByRole("button", { name: "Core" }).click(); await page.getByRole("button", { name: "Updates", exact: true }).click(); diff --git a/ui/src/e2e/external-session-catalogs.e2e.test.ts b/ui/src/e2e/external-session-catalogs.e2e.test.ts index 41122cd11a29..eac9974aa3d6 100644 --- a/ui/src/e2e/external-session-catalogs.e2e.test.ts +++ b/ui/src/e2e/external-session-catalogs.e2e.test.ts @@ -116,6 +116,7 @@ suite("OpenCode and Pi external session catalogs", () => { }); await page.goto(`${server.baseUrl}chat`); + await page.locator('[data-session-section="work"] .sidebar-session-group-toggle').click(); await page.getByText("OpenCode release review", { exact: true }).click(); await expect.poll(() => page.getByText("OpenCode transcript loaded").count()).toBe(1); await page.getByText("Pi architecture notes", { exact: true }).click(); diff --git a/ui/src/e2e/new-session-page.e2e.test.ts b/ui/src/e2e/new-session-page.e2e.test.ts index 05ce50e5a062..59d9b1931eb7 100644 --- a/ui/src/e2e/new-session-page.e2e.test.ts +++ b/ui/src/e2e/new-session-page.e2e.test.ts @@ -38,6 +38,11 @@ const EXEC_ONLY_PICKED = "C:\\Users\\peter\\repo"; const ONE_PIXEL_PNG_B64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/woAAn8B9FD5fHAAAAAASUVORK5CYII="; +const SESSION_LIST_DEFAULTS = { + contextTokens: null, + model: "gpt-5.5", + modelProvider: "openai", +}; async function captureUiProof(page: Page, fileName: string) { if (!captureUiProofEnabled) { @@ -698,9 +703,11 @@ describeControlUiE2e("Control UI new-session page mocked Gateway E2E", () => { const page = await context.newPage(); const sessionKey = "agent:cloud:cloud-e2e"; const gateway = await installMockGateway(page, { + defaultAgentId: "cloud", deferredMethods: ["sessions.dispatch"], featureMethods: ["chat.metadata", "chat.startup", "sessions.reclaim"], workspaceGit: true, + sessionKey: "agent:cloud:neutral-e2e", methodResponses: { "agents.list": { agents: [ @@ -757,6 +764,12 @@ describeControlUiE2e("Control UI new-session page mocked Gateway E2E", () => { try { await page.goto(`${server.baseUrl}new`); + expect( + await page.evaluate(() => ({ + hasSubtleCrypto: Boolean(globalThis.crypto.subtle), + isSecureContext: globalThis.isSecureContext, + })), + ).toEqual({ hasSubtleCrypto: true, isSecureContext: true }); await gateway.waitForRequest("environments.list"); await page.locator("#new-session-where-trigger").click(); const where = page.locator("wa-popover.new-session-page__where-popover"); @@ -901,7 +914,7 @@ describeControlUiE2e("Control UI new-session page mocked Gateway E2E", () => { ]); await gateway.setMethodResponse("sessions.list", { - count: 2, + count: 4, path: "", defaults: {}, sessions: [ @@ -913,18 +926,36 @@ describeControlUiE2e("Control UI new-session page mocked Gateway E2E", () => { worktree: { id: "worktree-1", branch: "openclaw/cloud-e2e", repoRoot: WORKSPACE }, placement: { state: "active" }, }, + { + key: "agent:cloud:managed-e2e", + kind: "direct", + label: "Managed session", + updatedAt: Date.now() - 1, + placement: { state: "active" }, + }, { key: "agent:cloud:local-e2e", kind: "direct", label: "Local session", - updatedAt: Date.now() - 1, + updatedAt: Date.now() - 2, + placement: { state: "local" }, + }, + { + key: "agent:cloud:neutral-e2e", + kind: "direct", + label: "Neutral session", + updatedAt: Date.now() - 3, placement: { state: "local" }, }, ], ts: Date.now(), }); await gateway.emitGatewayEvent("sessions.changed", { sessionKey, reason: "dispatch" }); - const sessionRow = page.locator('[data-session-key="agent:cloud:cloud-e2e"]'); + await page.goto( + `${server.baseUrl}chat?session=${encodeURIComponent("agent:cloud:neutral-e2e")}`, + ); + const managedSessionKey = "agent:cloud:managed-e2e"; + const sessionRow = page.locator(`[data-session-key="${managedSessionKey}"]`); const localSessionRow = page.locator('[data-session-key="agent:cloud:local-e2e"]'); await sessionRow.waitFor(); await localSessionRow.waitFor(); @@ -943,7 +974,7 @@ describeControlUiE2e("Control UI new-session page mocked Gateway E2E", () => { page.once("dialog", (dialog) => void dialog.accept()); await stopWorker.click(); const reclaim = await gateway.waitForRequest("sessions.reclaim"); - expect(reclaim.params).toEqual({ key: sessionKey, agentId: "cloud" }); + expect(reclaim.params).toEqual({ key: managedSessionKey, agentId: "cloud" }); } finally { await context.close(); } @@ -1083,6 +1114,7 @@ describeControlUiE2e("Control UI new-session page mocked Gateway E2E", () => { }, "sessions.list": { count: 1, + defaults: SESSION_LIST_DEFAULTS, path: "", sessions: [{ key: sessionKey, kind: "direct", updatedAt: Date.now() }], ts: Date.now(), @@ -1507,6 +1539,7 @@ describeControlUiE2e("Control UI new-session page mocked Gateway E2E", () => { }, "sessions.list": { count: 1, + defaults: SESSION_LIST_DEFAULTS, path: "", sessions: [{ key: sessionKey, kind: "direct", updatedAt: Date.now() }], ts: Date.now(), @@ -1672,6 +1705,7 @@ describeControlUiE2e("Control UI new-session page mocked Gateway E2E", () => { const sessionKey = "agent:main:refresh-overlap-e2e"; const listResponse = { count: 0, + defaults: SESSION_LIST_DEFAULTS, path: "", sessions: [], ts: Date.now(), @@ -2412,6 +2446,7 @@ describeControlUiE2e("Control UI new-session page mocked Gateway E2E", () => { }, "sessions.list": { count: 0, + defaults: SESSION_LIST_DEFAULTS, path: "", sessions: [], ts: Date.now(), @@ -2575,6 +2610,7 @@ describeControlUiE2e("Control UI new-session page mocked Gateway E2E", () => { }, "sessions.list": { count: 1, + defaults: SESSION_LIST_DEFAULTS, path: "", sessions: [ { diff --git a/ui/src/e2e/session-management.e2e.test.ts b/ui/src/e2e/session-management.e2e.test.ts index ce1aed4ba794..0f09600553e7 100644 --- a/ui/src/e2e/session-management.e2e.test.ts +++ b/ui/src/e2e/session-management.e2e.test.ts @@ -431,7 +431,7 @@ describeControlUiE2e("Control UI session management mocked Gateway E2E", () => { chatRows.evaluateAll((rows) => rows.map((row) => row.querySelector(".sidebar-recent-session__name")?.textContent ?? ""), ); - await expect.poll(rowNames).toEqual(["Main", "Data migration", "Research notes"]); + await expect.poll(rowNames).toEqual(["Data migration", "Research notes"]); const sidebarMigration = sidebarRows.filter({ hasText: "Data migration" }); await expect .poll(() => sidebarMigration.locator(".session-run-spinner").isVisible()) @@ -488,9 +488,7 @@ describeControlUiE2e("Control UI session management mocked Gateway E2E", () => { const researchLink = sidebarResearch.locator("a").first(); await researchLink.click(); await expect.poll(() => page.url()).toContain("session=agent%3Amain%3Aresearch"); - await expect - .poll(rowNames) - .toEqual(["Main", "Release planning", "Data migration", "Research notes"]); + await expect.poll(rowNames).toEqual(["Release planning", "Data migration", "Research notes"]); await expect .poll(() => chatRows @@ -589,7 +587,9 @@ describeControlUiE2e("Control UI session management mocked Gateway E2E", () => { .locator(".sidebar-brand") .getByRole("button", { name: "Collapse sidebar" }); const expandButton = page.locator(".shell-nav-expand"); - const drawerToggle = page.locator(".topbar-nav-toggle"); + const drawerToggle = page + .locator(".topbar-nav-toggle:visible, .chat-pane__nav-toggle:visible") + .first(); const sessionMenu = page.getByRole("menu", { name: "Actions for Research notes" }); await row.waitFor({ state: "visible", timeout: 10_000 }); @@ -986,7 +986,7 @@ describeControlUiE2e("Control UI session management mocked Gateway E2E", () => { .poll(() => page.locator('[data-session-section="ungrouped"] .sidebar-recent-session').count(), ) - .toBe(3); + .toBe(2); // Group by "None" flattens the category sections into the plain list. const sortSessionsButton = page.locator( @@ -1003,7 +1003,7 @@ describeControlUiE2e("Control UI session management mocked Gateway E2E", () => { await sortSessionsButton.click(); await activateMenuItem(page.getByRole("menuitemradio", { name: "None" })); await expect.poll(() => groups.count()).toBe(1); - await expect.poll(() => groups.first().locator(".sidebar-recent-session").count()).toBe(4); + await expect.poll(() => groups.first().locator(".sidebar-recent-session").count()).toBe(3); } finally { await context.close(); } @@ -1229,12 +1229,13 @@ describeControlUiE2e("Control UI session management mocked Gateway E2E", () => { .getAttribute("aria-expanded"), ) .toBe("false"); - await expect.poll(() => page.locator(".sidebar-recent-session").count()).toBe(9); - await page.getByRole("button", { name: "Load more" }).click(); + await expect.poll(() => page.locator(".sidebar-recent-session").count()).toBe(10); + await page.getByRole("button", { name: "Load more threads" }).click(); await expect.poll(() => page.locator(".sidebar-recent-session").count()).toBe(11); const patchCountBeforeFlatDrag = (await gateway.getRequests("sessions.patch")).length; const sortSessionsButton = page.getByRole("button", { name: "Sort threads" }); + await sortSessionsButton.locator("..").hover(); await sortSessionsButton.click(); await activateMenuItem(page.getByRole("menuitemradio", { name: "None" })); const flatSection = page.locator('[data-session-section="ungrouped"]'); @@ -1268,9 +1269,6 @@ describeControlUiE2e("Control UI session management mocked Gateway E2E", () => { await page.goto(`${server.baseUrl}chat`); const firstGroup = page.locator('[data-session-section="category:First group"]'); await firstGroup.waitFor({ state: "visible" }); - await page.locator('[data-session-section="ungrouped"] .sidebar-recent-session').waitFor({ - state: "visible", - }); // A header-menu-created group starts empty and still gets a section. await firstGroup.locator(".sidebar-recent-sessions__head").hover(); @@ -1410,7 +1408,7 @@ describeControlUiE2e("Control UI session management mocked Gateway E2E", () => { } }); - it("keeps raw ids out of work rows and survives rows growing subtitles in place", async () => { + it("keeps raw ids out of work rows while their metadata grows in place", async () => { const baseTime = Date.parse("2026-07-01T16:00:00.000Z"); const nodeHash = "11c38726acc6fac280357576c87acc6fac280357"; const rows = (withWork: boolean) => { @@ -1447,42 +1445,35 @@ describeControlUiE2e("Control UI session management mocked Gateway E2E", () => { methodResponses: { "sessions.list": sessionsListResponse(rows(false)), }, - // Defer every list request; the test resolves them per phase so rows - // first paint single-line and only then grow a subtitle line in place. - deferredMethods: Array.from({ length: 12 }, () => "sessions.list"), sessionKey: "agent:main:main", }); - const resolveNextList = async (withWork: boolean) => { - try { - await gateway.resolveDeferred("sessions.list", sessionsListResponse(rows(withWork))); - } catch { - // No list request queued yet; the poll below retries. - } - }; try { await page.goto(`${server.baseUrl}chat`); await expect - .poll( - async () => { - await resolveNextList(false); - return page.locator(".sidebar-recent-session").count(); - }, - { timeout: 15_000 }, - ) + .poll(() => page.locator(".sidebar-recent-session").count(), { timeout: 15_000 }) .toBeGreaterThan(0); - // Rows grow a second subtitle line only after first layout: the WebKit - // overlap regression needs in-place growth, not a static two-line list. - await gateway.emitGatewayEvent("sessions.changed", {}); + // Add work metadata only after first layout so the WebKit overlap + // regression still exercises in-place row growth. + const listRequests = (await gateway.getRequests("sessions.list")).length; + await gateway.setMethodResponse("sessions.list", sessionsListResponse(rows(true))); + await gateway.emitGatewayEvent("sessions.changed", { + reason: "update", + sessionKey: "agent:main:dashboard:0f9d5c1e-6d0f-4c9a-9d84-1c2f3a4b5c6e", + }); await expect - .poll( - async () => { - await resolveNextList(true); - return page.locator(".sidebar-recent-session__subtitle").count(); - }, - { timeout: 15_000 }, - ) - .toBeGreaterThan(0); + .poll(async () => (await gateway.getRequests("sessions.list")).length) + .toBeGreaterThan(listRequests); + const codingToggle = page.locator( + '[data-session-section="work"] .sidebar-session-group-toggle', + ); + await codingToggle.waitFor({ state: "visible" }); + await expect.poll(() => codingToggle.getAttribute("aria-expanded")).toBe("false"); + await codingToggle.click(); + const namesLocator = page.locator(".sidebar-recent-session__name"); + await expect + .poll(() => trimmedTextContents(namesLocator)) + .toContain("clawdbot ⎇ wt-1 · …0357"); // Names and subtitles never show raw node ids or raw agent keys. const names = await trimmedTextContents(page.locator(".sidebar-recent-session__name")); @@ -1551,16 +1542,17 @@ describeControlUiE2e("Control UI session management mocked Gateway E2E", () => { }); try { await page.goto(`${server.baseUrl}chat`); - const seeMore = page.getByRole("button", { name: "Load more" }); - for (let pageIndex = 0; pageIndex < 3; pageIndex += 1) { - await seeMore.click(); + await page.locator('[data-session-section="work"] .sidebar-session-group-toggle').click(); + const loadMore = page.getByRole("button", { name: "Load more threads" }); + for (let pageIndex = 0; pageIndex < 3 && (await loadMore.isVisible()); pageIndex += 1) { + await loadMore.click(); } await page.locator(".sidebar-shell__body").evaluate((element) => { element.scrollTop = 0; }); await expect .poll(() => page.locator(".sidebar-recent-session").count(), { timeout: 15_000 }) - .toBeGreaterThanOrEqual(rows.length); + .toBe(rows.length); await captureUiProof(page, "short-window-session-sections.png"); // Sections must stack below each other, not paint over the rows above. diff --git a/ui/src/e2e/sidebar-customization.e2e.test.ts b/ui/src/e2e/sidebar-customization.e2e.test.ts index 07f90931afcf..2b69d1cb4324 100644 --- a/ui/src/e2e/sidebar-customization.e2e.test.ts +++ b/ui/src/e2e/sidebar-customization.e2e.test.ts @@ -300,6 +300,7 @@ describeControlUiE2e("Control UI sidebar customization mocked Gateway E2E", () = "About", "General", "Appearance", + "Notifications", ]); await captureSettingsSidebarProof(settingsSidebar, "01c-settings-search-group.png"); await holdUiProof(page); @@ -557,7 +558,7 @@ describeControlUiE2e("Control UI sidebar customization mocked Gateway E2E", () = } }); - it("opens the start screen from the sidebar brand without carrying the active session", async () => { + it("opens the start screen from the sidebar action without carrying the active session", async () => { const context = await browser.newContext({ locale: "en-US", serviceWorkers: "block", @@ -568,15 +569,13 @@ describeControlUiE2e("Control UI sidebar customization mocked Gateway E2E", () = try { await page.goto(`${server.baseUrl}chat?session=${encodeURIComponent("agent:main:work")}`); - const brand = page.locator("openclaw-app-sidebar").getByRole("link", { name: "New thread" }); - await expect.poll(() => brand.getAttribute("href")).toBe("/new"); - - await brand.click(); + await page.locator("openclaw-app-sidebar .sidebar-brand__new-thread").click(); await expect.poll(() => new URL(page.url()).pathname).toBe("/new"); - await expect.poll(() => new URL(page.url()).search).toBe(""); + await expect.poll(() => new URL(page.url()).searchParams.get("agent")).toBe("main"); + await expect.poll(() => new URL(page.url()).searchParams.has("session")).toBe(false); await expect.poll(() => page.locator(".new-session-page").isVisible()).toBe(true); - await captureUiProof(page, "07-brand-start-screen.png"); + await captureUiProof(page, "07-sidebar-start-screen.png"); } finally { await context.close(); } diff --git a/ui/src/pages/chat/run-lifecycle.test.ts b/ui/src/pages/chat/run-lifecycle.test.ts index 87fda08122ad..187a84537331 100644 --- a/ui/src/pages/chat/run-lifecycle.test.ts +++ b/ui/src/pages/chat/run-lifecycle.test.ts @@ -4,6 +4,7 @@ import type { SessionsListResult } from "../../api/types.ts"; import { isSessionRunActive } from "../../lib/session-run-state.ts"; import { CHAT_RUN_STATUS_TOAST_DURATION_MS, + hasAbortableSessionRun, reconcileChatRunFromCurrentSessionRow, reconcileChatRunFromSessionRow, reconcileChatRunLifecycle, @@ -23,6 +24,20 @@ function makeSessionsResult(rows: TestRow[]): SessionsListResult { return { sessions: rows } as unknown as SessionsListResult; } +describe("hasAbortableSessionRun", () => { + it("recognizes the canonical main row while chat uses its main alias", () => { + expect( + hasAbortableSessionRun({ + chatRunId: null, + sessionKey: "main", + sessionsResult: makeSessionsResult([ + { key: "agent:main:main", hasActiveRun: true, status: "running" }, + ]), + }), + ).toBe(true); + }); +}); + function makeHost(over: Partial = {}): ReconcileHost { return { sessionKey: "s1", diff --git a/ui/src/pages/chat/run-lifecycle.ts b/ui/src/pages/chat/run-lifecycle.ts index fe326fb3f720..a3240b533ed8 100644 --- a/ui/src/pages/chat/run-lifecycle.ts +++ b/ui/src/pages/chat/run-lifecycle.ts @@ -8,7 +8,10 @@ import { type SessionRunTerminal, type SessionScopeHost, } from "../../lib/sessions/index.ts"; -import { uiSessionRowMatchesSelectedChat } from "../../lib/sessions/session-key.ts"; +import { + areUiSessionKeysEquivalent, + uiSessionRowMatchesSelectedChat, +} from "../../lib/sessions/session-key.ts"; import { normalizeLowercaseStringOrEmpty } from "../../lib/string-coerce.ts"; import { formatConnectError } from "./connect-error.ts"; import { resetChatInputHistoryNavigation, type ChatInputHistoryState } from "./input-history.ts"; @@ -123,7 +126,8 @@ export function hasAbortableSessionRun(host: { } return Boolean( host.sessionsResult?.sessions.some( - (session) => session.key === host.sessionKey && isSessionRunActive(session), + (session) => + areUiSessionKeysEquivalent(session.key, host.sessionKey) && isSessionRunActive(session), ), ); } diff --git a/ui/src/pages/plugins/plugins.e2e.test.ts b/ui/src/pages/plugins/plugins.e2e.test.ts index 5578e6651956..c6162aced170 100644 --- a/ui/src/pages/plugins/plugins.e2e.test.ts +++ b/ui/src/pages/plugins/plugins.e2e.test.ts @@ -566,9 +566,9 @@ describeControlUiE2e("Control UI Plugins mocked Gateway E2E", () => { } const sidebar = page.locator("openclaw-app-sidebar"); await sidebar.waitFor({ state: "visible" }); - const moreButton = sidebar.getByRole("button", { name: "More" }); - if ((await moreButton.getAttribute("aria-expanded")) !== "true") { - await moreButton.click(); + const pagesButton = sidebar.locator(".sidebar-nav__head-action"); + if ((await pagesButton.getAttribute("aria-expanded")) !== "true") { + await pagesButton.click(); } const workboardMenuItem = sidebar .locator("wa-dropdown.sidebar-more-menu") diff --git a/ui/src/pages/skill-workshop/history-scan.e2e.test.ts b/ui/src/pages/skill-workshop/history-scan.e2e.test.ts index f18e15c5872d..74d69a4fe5cc 100644 --- a/ui/src/pages/skill-workshop/history-scan.e2e.test.ts +++ b/ui/src/pages/skill-workshop/history-scan.e2e.test.ts @@ -94,7 +94,7 @@ describeBrowser("Skill Workshop history scan browser flow", () => { } await findIdeas.click(); - await expect.poll(() => page.getByText("34 sessions reviewed").isVisible()).toBe(true); + await expect.poll(() => page.getByText("34 threads reviewed").isVisible()).toBe(true); await expect.poll(() => page.getByText("2 ideas found").isVisible()).toBe(true); await expect .poll(() => page.getByRole("button", { name: "Scan earlier work" }).isVisible()) diff --git a/ui/src/pages/workboard/workboard.e2e.test.ts b/ui/src/pages/workboard/workboard.e2e.test.ts index 4b24306e45a0..359acb7bb7fd 100644 --- a/ui/src/pages/workboard/workboard.e2e.test.ts +++ b/ui/src/pages/workboard/workboard.e2e.test.ts @@ -442,7 +442,7 @@ describeControlUiE2e("Control UI Workboard mocked Gateway E2E", () => { await expect.poll(() => createDialog.isVisible()).toBe(true); await createForm.getByLabel("Title").fill(createdCard.title); await createForm.getByLabel("Notes").fill(createdCard.notes ?? ""); - await chooseWorkboardSelectOption(createForm, "Session", linkedSessionName); + await chooseWorkboardSelectOption(createForm, "Thread", linkedSessionName); await createForm.getByLabel("Labels").fill("ui, proof"); await captureScreenshot(writable.page, artifacts, "02-create-dialog"); const createBefore = (await writableGateway.getRequests("workboard.cards.create")).length;