Files
kandev/apps/web/components/task/task-top-bar-plugin-actions.test.tsx
T
João Salavisa 545cd0b599 fix(ui): move task plugins into mobile menu
Move task plugin actions and destinations into the shared mobile Plugins menu, preserving a compact task header and accessible touch targets. Add focused mobile coverage, plugin authoring documentation, and process-probe stability handling.
2026-09-23 08:14:51 +01:00

153 lines
5.1 KiB
TypeScript

import { act, cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { pluginRegistry } from "@/lib/plugins/registry";
import {
TaskTopBarPluginActions,
type ChatTopBarSlotProps,
useHasTaskTopBarPluginActions,
} from "./task-top-bar-plugin-actions";
const SLOT = "chat-top-bar";
// Minimal store: the wrapper only reads taskSessionsByTask.itemsByTaskId.
const mockState = {
taskSessionsByTask: {
itemsByTaskId: {
t1: [
{ id: "s1", task_id: "t1" },
{ id: "s2", task_id: "t1" },
],
},
},
};
const originalSessions = mockState.taskSessionsByTask.itemsByTaskId.t1;
vi.mock("@/components/state-provider", () => ({
useOptionalAppStore: (selector: (s: typeof mockState) => unknown) => selector(mockState),
}));
afterEach(() => {
cleanup();
pluginRegistry.unregisterPlugin("plugin-a");
mockState.taskSessionsByTask.itemsByTaskId.t1 = originalSessions;
});
describe("TaskTopBarPluginActions", () => {
it("renders nothing when no plugin registered a chat-top-bar component", () => {
const { container } = render(
<TaskTopBarPluginActions sessionId="s1" taskId="t1" taskTitle="Demo" workspaceId="w1" />,
);
expect(container.innerHTML).toBe("");
});
it("forwards task/workspace context, the active session, and all session ids", () => {
pluginRegistry.forPlugin("plugin-a").registerComponent(SLOT, ({ slotProps }) => {
const ctx = slotProps as ChatTopBarSlotProps;
return (
<div data-testid="plugin-topbar">
{`${ctx.taskId}|${ctx.workspaceId}|${ctx.activeSessionId}|${ctx.sessionIds.join(",")}|${ctx.presentation}`}
</div>
);
});
render(
<TaskTopBarPluginActions sessionId="s2" taskId="t1" taskTitle="Demo" workspaceId="w1" />,
);
expect(screen.getByTestId("plugin-topbar").textContent).toBe("t1|w1|s2|s1,s2|desktop");
});
it("includes the active session id even when the store list omits it", () => {
pluginRegistry.forPlugin("plugin-a").registerComponent(SLOT, ({ slotProps }) => {
const ctx = slotProps as ChatTopBarSlotProps;
return <div data-testid="plugin-topbar">{ctx.sessionIds.join(",")}</div>;
});
// taskId with no store entry -> only the active session propagates.
render(
<TaskTopBarPluginActions
sessionId="s9"
taskId="t-unknown"
taskTitle="Demo"
workspaceId="w1"
/>,
);
expect(screen.getByTestId("plugin-topbar").textContent).toBe("s9");
});
it("propagates only the active session when taskId is null", () => {
pluginRegistry.forPlugin("plugin-a").registerComponent(SLOT, ({ slotProps }) => {
const ctx = slotProps as ChatTopBarSlotProps;
return (
<div data-testid="plugin-topbar">{`${ctx.taskId}|${ctx.activeSessionId}|${ctx.sessionIds.join(",")}`}</div>
);
});
render(<TaskTopBarPluginActions sessionId="s9" taskId={null} workspaceId={null} />);
expect(screen.getByTestId("plugin-topbar").textContent).toBe("null|s9|s9");
});
it("does not rerender the contribution for equal session ids", () => {
const pluginRender = vi.fn();
pluginRegistry.forPlugin("plugin-a").registerComponent(SLOT, () => {
pluginRender();
return null;
});
const { rerender } = render(
<TaskTopBarPluginActions sessionId="s2" taskId="t1" taskTitle="Demo" workspaceId="w1" />,
);
mockState.taskSessionsByTask.itemsByTaskId.t1 = originalSessions.map((session) => ({
...session,
state: "finished",
}));
rerender(
<TaskTopBarPluginActions sessionId="s2" taskId="t1" taskTitle="Demo" workspaceId="w1" />,
);
expect(pluginRender).toHaveBeenCalledTimes(1);
});
});
describe("responsive task top-bar plugin actions", () => {
it("identifies and contains the mobile menu presentation", () => {
pluginRegistry.forPlugin("plugin-a").registerComponent(SLOT, ({ slotProps }) => {
const ctx = slotProps as ChatTopBarSlotProps;
return <button data-testid="mobile-plugin-action">{ctx.presentation}</button>;
});
render(
<TaskTopBarPluginActions
sessionId="s2"
taskId="t1"
taskTitle="Demo"
workspaceId="w1"
presentation="mobile"
/>,
);
const wrapper = screen.getByTestId("mobile-chat-top-bar-plugin-actions");
expect(wrapper.contains(screen.getByTestId("mobile-plugin-action"))).toBe(true);
expect(screen.getByTestId("mobile-plugin-action").textContent).toBe("mobile");
});
it("reacts when the first or last chat-top-bar registration changes", () => {
function Presence() {
return <span data-testid="slot-presence">{String(useHasTaskTopBarPluginActions())}</span>;
}
render(<Presence />);
expect(screen.getByTestId("slot-presence").textContent).toBe("false");
act(() => {
pluginRegistry.forPlugin("plugin-a").registerComponent(SLOT, () => null);
});
expect(screen.getByTestId("slot-presence").textContent).toBe("true");
act(() => pluginRegistry.unregisterPlugin("plugin-a"));
expect(screen.getByTestId("slot-presence").textContent).toBe("false");
});
});