mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(ui): discussion button stays open on second click (#112671)
* fix(ui): toggle discussion sidebar from header * test(ui): cover discussion sidebar toggle
This commit is contained in:
committed by
GitHub
parent
b56006babf
commit
96d16c3c46
@@ -0,0 +1,111 @@
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { chromium, type Browser, type BrowserContext } from "playwright";
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||
import {
|
||||
canRunPlaywrightChromium,
|
||||
installMockGateway,
|
||||
resolvePlaywrightChromiumExecutablePath,
|
||||
startControlUiE2eServer,
|
||||
type ControlUiE2eServer,
|
||||
} from "../test-helpers/control-ui-e2e.ts";
|
||||
|
||||
const chromiumExecutablePath = resolvePlaywrightChromiumExecutablePath(chromium.executablePath());
|
||||
const chromiumAvailable = canRunPlaywrightChromium(chromiumExecutablePath);
|
||||
const allowMissingChromium = process.env.OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM === "1";
|
||||
const describeControlUiE2e = chromiumAvailable || !allowMissingChromium ? describe : describe.skip;
|
||||
const captureUiProof = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const proofDir = path.join(
|
||||
process.cwd(),
|
||||
".artifacts",
|
||||
"control-ui-e2e",
|
||||
"session-discussion-toggle",
|
||||
);
|
||||
|
||||
let server: ControlUiE2eServer;
|
||||
let browser: Browser;
|
||||
const openContexts = new Set<BrowserContext>();
|
||||
|
||||
async function closeOpenContexts(): Promise<void> {
|
||||
const contexts = Array.from(openContexts);
|
||||
openContexts.clear();
|
||||
await Promise.all(contexts.map((context) => context.close()));
|
||||
}
|
||||
|
||||
describeControlUiE2e("session discussion toggle", () => {
|
||||
beforeAll(async () => {
|
||||
if (!chromiumAvailable) {
|
||||
throw new Error(`Playwright Chromium is unavailable at ${chromiumExecutablePath}`);
|
||||
}
|
||||
browser = await chromium.launch({ executablePath: chromiumExecutablePath });
|
||||
server = await startControlUiE2eServer();
|
||||
if (captureUiProof) {
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(closeOpenContexts);
|
||||
|
||||
afterAll(async () => {
|
||||
await closeOpenContexts();
|
||||
await browser?.close();
|
||||
await server?.close();
|
||||
});
|
||||
|
||||
it("opens and closes the sidebar from the same header action", async () => {
|
||||
const context = await browser.newContext({
|
||||
...(captureUiProof
|
||||
? { recordVideo: { dir: proofDir, size: { height: 720, width: 1280 } } }
|
||||
: {}),
|
||||
viewport: { height: 720, width: 1280 },
|
||||
});
|
||||
openContexts.add(context);
|
||||
const page = await context.newPage();
|
||||
const sessionKey = "agent:main:discussion-proof";
|
||||
const gateway = await installMockGateway(page, {
|
||||
featureMethods: ["session.discussion.info", "session.discussion.open"],
|
||||
historyMessages: [
|
||||
{
|
||||
content: [{ type: "text", text: "Discussion toggle proof." }],
|
||||
role: "assistant",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
],
|
||||
methodResponses: {
|
||||
"session.discussion.info": { state: "available" },
|
||||
"session.discussion.open": {
|
||||
openUrl: "https://discussion.example/session",
|
||||
state: "open",
|
||||
},
|
||||
},
|
||||
sessionKey,
|
||||
});
|
||||
|
||||
await page.goto(`${server.baseUrl}chat?session=${encodeURIComponent(sessionKey)}`);
|
||||
await gateway.waitForRequest("session.discussion.info");
|
||||
|
||||
const showDiscussion = page.getByRole("button", { name: "Show discussion" });
|
||||
await expect.poll(() => showDiscussion.isVisible()).toBe(true);
|
||||
await expect.poll(() => showDiscussion.getAttribute("aria-pressed")).toBe("false");
|
||||
|
||||
await showDiscussion.click();
|
||||
|
||||
const hideDiscussion = page.getByRole("button", { name: "Hide discussion" });
|
||||
const closeSidebar = page.getByRole("button", { name: "Close sidebar" });
|
||||
await expect.poll(() => hideDiscussion.getAttribute("aria-pressed")).toBe("true");
|
||||
await expect.poll(() => closeSidebar.isVisible()).toBe(true);
|
||||
expect(await gateway.getRequests("session.discussion.open")).toHaveLength(1);
|
||||
if (captureUiProof) {
|
||||
await page.screenshot({ path: path.join(proofDir, "discussion-open.png") });
|
||||
}
|
||||
|
||||
await hideDiscussion.click();
|
||||
|
||||
await expect.poll(() => closeSidebar.isVisible()).toBe(false);
|
||||
await expect.poll(() => showDiscussion.getAttribute("aria-pressed")).toBe("false");
|
||||
expect(await gateway.getRequests("session.discussion.open")).toHaveLength(1);
|
||||
if (captureUiProof) {
|
||||
await page.screenshot({ path: path.join(proofDir, "discussion-closed.png") });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -4357,6 +4357,7 @@ export const en: TranslationMap = {
|
||||
sessionDiscussion: {
|
||||
title: "Discussion",
|
||||
show: "Show discussion",
|
||||
hide: "Hide discussion",
|
||||
disconnected: "Gateway is disconnected.",
|
||||
loading: "Loading discussion…",
|
||||
opening: "Opening discussion…",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render } from "lit";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { SessionDiscussionInfo } from "../../../../packages/gateway-protocol/src/index.js";
|
||||
import type { GatewayBrowserClient } from "../../api/gateway.ts";
|
||||
@@ -10,6 +11,7 @@ import "./components/chat-sidebar.ts";
|
||||
|
||||
type DiscussionTestPane = TestChatPane & {
|
||||
probeSessionDiscussion: (sessionKey: string) => Promise<void>;
|
||||
renderSessionDiscussionAction: () => unknown;
|
||||
};
|
||||
|
||||
const SESSION_KEY = "agent:main:current";
|
||||
@@ -35,9 +37,13 @@ function createDiscussionPane(params: {
|
||||
state.sidebarContent = content;
|
||||
state.sidebarOpen = true;
|
||||
});
|
||||
const handleCloseSidebar = vi.fn(() => {
|
||||
state.sidebarOpen = false;
|
||||
});
|
||||
state.handleOpenSidebar = handleOpenSidebar;
|
||||
state.handleCloseSidebar = handleCloseSidebar;
|
||||
state.sidebarOpen = params.sidebarOpen ?? false;
|
||||
return { pane, state, handleOpenSidebar, request };
|
||||
return { pane, state, handleOpenSidebar, handleCloseSidebar, request };
|
||||
}
|
||||
|
||||
describe("chat pane session discussion auto-show", () => {
|
||||
@@ -100,6 +106,33 @@ describe("chat pane session discussion auto-show", () => {
|
||||
expect(handleOpenSidebar).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses the header action to open and close the discussion sidebar", async () => {
|
||||
const { pane, handleOpenSidebar, handleCloseSidebar } = createDiscussionPane({
|
||||
info: { state: "available" },
|
||||
});
|
||||
const container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
|
||||
await pane.probeSessionDiscussion(SESSION_KEY);
|
||||
render(pane.renderSessionDiscussionAction(), container);
|
||||
|
||||
let action = container.querySelector<HTMLButtonElement>(".chat-session-discussion-toggle");
|
||||
expect(action?.ariaLabel).toBe("Show discussion");
|
||||
expect(action?.getAttribute("aria-pressed")).toBe("false");
|
||||
action?.click();
|
||||
expect(handleOpenSidebar).toHaveBeenCalledTimes(1);
|
||||
|
||||
render(pane.renderSessionDiscussionAction(), container);
|
||||
action = container.querySelector<HTMLButtonElement>(".chat-session-discussion-toggle");
|
||||
expect(action?.ariaLabel).toBe("Hide discussion");
|
||||
expect(action?.getAttribute("aria-pressed")).toBe("true");
|
||||
action?.click();
|
||||
expect(handleCloseSidebar).toHaveBeenCalledTimes(1);
|
||||
expect(handleOpenSidebar).toHaveBeenCalledTimes(1);
|
||||
|
||||
container.remove();
|
||||
});
|
||||
|
||||
it("does not steal a sidebar that is already open", async () => {
|
||||
const { pane, handleOpenSidebar } = createDiscussionPane({
|
||||
info: { state: "open", embedUrl: "https://clack.example/embed/c1" },
|
||||
|
||||
@@ -3124,7 +3124,7 @@ class ChatPane extends OpenClawLightDomElement {
|
||||
state.sidebarOpen &&
|
||||
state.sidebarContent?.kind === "session-discussion" &&
|
||||
state.sidebarContent.sessionKey === sessionKey;
|
||||
const label = t("chat.sessionDiscussion.show");
|
||||
const label = t(active ? "chat.sessionDiscussion.hide" : "chat.sessionDiscussion.show");
|
||||
return html`
|
||||
<openclaw-tooltip .content=${label}>
|
||||
<button
|
||||
@@ -3132,7 +3132,7 @@ class ChatPane extends OpenClawLightDomElement {
|
||||
type="button"
|
||||
aria-label=${label}
|
||||
aria-pressed=${String(active)}
|
||||
@click=${() => state.handleOpenSidebar(content)}
|
||||
@click=${() => (active ? state.handleCloseSidebar() : state.handleOpenSidebar(content))}
|
||||
>
|
||||
${icons.messageSquare}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user