fix: improve console workspace usability

This commit is contained in:
lda
2026-08-12 02:32:51 +07:00 Verified
parent b94b77cd39
commit b24ba6a92d
8 changed files with 70 additions and 42 deletions
@@ -3,13 +3,16 @@ import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it } from "vitest";
import { cleanup } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { readFileSync } from "node:fs";
import { initialState } from "../app/state.js";
import { ConsoleShell } from "./ConsoleShell.js";
const globalStyles = readFileSync("src/styles/global.css", "utf8");
afterEach(() => cleanup());
describe("ConsoleShell", () => {
it("renders the connection header, lifecycle rail, main content, and evidence surface", () => {
it("renders the connection header, lifecycle rail, and main content without a global evidence panel", () => {
render(
<MemoryRouter initialEntries={["/console/discover"]}>
<ConsoleShell
@@ -27,13 +30,21 @@ describe("ConsoleShell", () => {
expect(screen.getByRole("main", { name: "Console workspace" })).toHaveTextContent(
"Discover content",
);
expect(screen.getByRole("complementary", { name: "Operation evidence" })).toBeInTheDocument();
expect(screen.queryByRole("complementary", { name: "Operation evidence" })).toBeNull();
expect(screen.getByRole("link", { name: "Discover" })).toHaveAttribute(
"aria-current",
"page",
);
});
it("reserves the workspace width for navigation and route content", () => {
expect(globalStyles).toMatch(
/\.console-workspace\s*\{[\s\S]*?grid-template-columns:\s*minmax\(9rem, 13rem\)\s+minmax\(0, 1fr\)/,
);
expect(globalStyles).toContain('"nav main"');
expect(globalStyles).not.toContain('"nav main evidence"');
});
it("renders all lifecycle links with route destinations", () => {
render(
<MemoryRouter>
@@ -2,7 +2,6 @@ import type { ReactNode } from "react";
import { NavLink } from "react-router-dom";
import type { ConnectionState } from "../app/state.js";
import { ConnectionHeader } from "../components/ConnectionHeader.js";
import { EvidenceLedger } from "./EvidenceLedger.js";
type Props = {
readonly connection: ConnectionState;
@@ -59,9 +58,5 @@ export const ConsoleShell = ({
<main id="console-workspace-main" aria-label="Console workspace" tabIndex={-1}>
{children}
</main>
<aside aria-label="Operation evidence" className="console-workspace__evidence">
<h2>Operation evidence</h2>
<EvidenceLedger records={connection.evidence} />
</aside>
</div>
);
@@ -149,13 +149,13 @@ describe("ConsoleWorkspace", () => {
expect(screen.getByTestId("executor-state")).toHaveTextContent("available");
expect(screen.getByTestId("write-executor-state")).toHaveTextContent("available");
expect(screen.getByTestId("executor-stable")).toHaveTextContent("yes");
expect(screen.getAllByText("Health check")).toHaveLength(1);
expect(screen.getByTestId("evidence-ids")).toHaveTextContent("workflow.health-0");
expect(mockedCallOperation).not.toHaveBeenCalled();
await userEvent.click(screen.getByRole("button", { name: "Navigate to drafts" }));
expect(await screen.findByText("Drafts route")).toBeInTheDocument();
expect(screen.getAllByText("Health check")).toHaveLength(1);
expect(screen.getByTestId("evidence-ids")).toHaveTextContent("workflow.health-0");
expect(screen.getByTestId("connected-target")).toHaveTextContent("http://one.example/rpc");
});
@@ -273,6 +273,6 @@ describe("ConsoleWorkspace", () => {
"http://two.example/rpc",
);
});
expect(screen.getAllByText("Health check")).toHaveLength(1);
expect(screen.getByTestId("evidence-ids")).toHaveTextContent("workflow.health-0");
});
});
@@ -188,6 +188,21 @@ describe("DraftWorkbench", () => {
expect(within(inspector as HTMLElement).getByText("demo.collect")).toBeInTheDocument();
});
it("opens the context inspector when a route outcome is selected on mobile", async () => {
setViewport(390);
const user = userEvent.setup();
const { container } = render(<DraftWorkbench draft={workspace} />);
const routes = screen.getByLabelText("Route outcomes");
await user.click(within(routes).getByRole("button", { name: /collect.*ok/i }));
const inspector = container.querySelector("#draft-workbench-inspector");
expect(inspector).toHaveAttribute("open", "");
expect(within(inspector as HTMLElement).getByRole("heading", { name: "Route inspector" })).toBeInTheDocument();
expect(within(inspector as HTMLElement).getByRole("textbox", { name: "Source step" })).toHaveValue("collect");
expect(within(inspector as HTMLElement).getByRole("textbox", { name: "Outcome" })).toHaveValue("ok");
});
it("keeps a dirty inspector form mounted and intact across mobile close and reopen", async () => {
setViewport(390);
mockedUseAuthoringCapabilityDetail.mockReturnValue({
@@ -164,8 +164,11 @@ export const DraftWorkbench = ({
(nextSelection: WorkbenchSelection): void => {
controller.select(nextSelection);
onSelectionChange?.(nextSelection);
if (isMobile && (nextSelection.kind === "edge" || nextSelection.kind === "node")) {
setOpenSheet("inspector");
}
},
[controller, onSelectionChange],
[controller, isMobile, onSelectionChange],
);
return (
@@ -1,4 +1,5 @@
import { cleanup, render, screen, within } from "@testing-library/react";
import { readFileSync } from "node:fs";
import userEvent from "@testing-library/user-event";
import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -33,6 +34,7 @@ vi.mock("../domain/draft-authoring-client.js", () => ({
const mockedUseCapabilityDiscovery = vi.mocked(useCapabilityDiscovery);
const mockedUseDraftWorkspace = vi.mocked(useDraftWorkspace);
const globalStyles = readFileSync("src/styles/global.css", "utf8");
const mockedUseConsoleWorkspace = vi.mocked(useConsoleWorkspace);
const mockedCreateDraftAuthoringClient = vi.mocked(createDraftAuthoringClient);
@@ -335,4 +337,10 @@ describe("DiscoverRoute", () => {
),
).toBeInTheDocument();
});
it("keeps both discovery panes scrollable within the viewport", () => {
expect(globalStyles).toMatch(
/\.capability-discovery__results,\s*\.capability-discovery__detail,\s*\.capability-playground\s*\{[\s\S]*?max-height:\s*calc\(100vh - 10rem\);[\s\S]*?overflow-y:\s*auto/,
);
});
});
@@ -150,6 +150,12 @@ describe("DraftDetailRoute", () => {
);
});
it("keeps route outcomes scrollable inside the bounded graph panel", () => {
expect(globalStyles).toMatch(
/\.authoring-graph__routes\s*\{[\s\S]*?min-height:\s*0;[\s\S]*?overflow-y:\s*auto/,
);
});
it("explains when the full draft document was not returned", () => {
mockedUseDraftWorkspace.mockReturnValue(
controller({ selected: workspace({ draft: null }) }),