2942ced3ec
The Codex tab strip wrapped onto a second row while Claude Code's stayed one scrollable row, so two panels that are meant to be twins read as different components. Both now render the same ConfigTabBar — the scroller lifted out of CcConfig, with the fade edges and chevrons that only appear when there is somewhere to scroll. The deferral note in CodexConfig that predicted this is gone with it. The bar also carries role="tablist"/"tab" and aria-selected now: Codex had them, Claude Code did not, and one of the two had to win. Cursor's placeholder tab goes away with the machinery that served only it — the optional Panel, the "soon" badge, the coming-soon card, and their four i18n keys. Element.scrollBy and friends move to test-setup: jsdom has none of them, and the tab bar now calls them from both pages.
133 lines
3.9 KiB
TypeScript
133 lines
3.9 KiB
TypeScript
/**
|
|
* @file Run.defaultCwd.test.tsx
|
|
* @description The Run page must default its working directory to the user's
|
|
* HOME directory (a neutral spawn location) rather than the dashboard's own
|
|
* cwd, which would make ad-hoc runs inherit this repo's project context
|
|
* (.claude/agents, skills, rules, CLAUDE.md, .mcp.json) — issue #202. Falls
|
|
* back to the dashboard cwd only when no home suggestion is available.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import { render, act, screen } from "@testing-library/react";
|
|
import { MemoryRouter } from "react-router-dom";
|
|
import i18n from "i18next";
|
|
|
|
const HOME = { kind: "home", path: "/Users/tester", label: "Home" };
|
|
const DASHBOARD = {
|
|
kind: "dashboard",
|
|
path: "/Users/tester/projects/dashboard",
|
|
label: "Dashboard server",
|
|
};
|
|
const RECENT = { kind: "recent", path: "/Users/tester/projects/other", label: "other" };
|
|
|
|
// Mutable per-test cwd suggestion payload, read by the api mock below.
|
|
let cwdItems: Array<{ kind: string; path: string; label: string }> = [];
|
|
|
|
vi.mock("../../lib/api", async (importOriginal) => {
|
|
const actual = await importOriginal<Record<string, unknown>>();
|
|
const r = (value: unknown) => vi.fn().mockResolvedValue(value);
|
|
return {
|
|
...actual,
|
|
api: {
|
|
run: {
|
|
list: r({ runs: [], items: [] }),
|
|
history: r({ items: [] }),
|
|
binary: r({ found: true, path: "/usr/bin/claude" }),
|
|
cwds: vi.fn().mockImplementation(() => Promise.resolve({ items: cwdItems })),
|
|
files: r({ items: [] }),
|
|
start: r({ id: "run-1", status: "running" }),
|
|
get: r({ id: "run-1", status: "running", messages: [], envelopes: [] }),
|
|
send: r({ messageId: "m-1" }),
|
|
kill: r({ ok: true }),
|
|
},
|
|
lanes: {
|
|
list: r({ lanes: [], counts: { total: 0, running: 0, needs_you: 0, dead: 0 } }),
|
|
pipelines: r({ pipelines: [] }),
|
|
},
|
|
ccConfig: {
|
|
commands: r({ items: [] }),
|
|
plugins: r({ plugins: [] }),
|
|
file: r({ content: "" }),
|
|
},
|
|
sessions: {
|
|
list: r({ sessions: [], total: 0, limit: 50, offset: 0 }),
|
|
transcript: r({ messages: [] }),
|
|
},
|
|
},
|
|
};
|
|
});
|
|
|
|
vi.mock("../../lib/eventBus", () => ({
|
|
eventBus: {
|
|
subscribe: () => () => {},
|
|
publish: () => {},
|
|
onConnection: () => () => {},
|
|
connected: true,
|
|
setConnected: () => {},
|
|
},
|
|
}));
|
|
|
|
import { Workspace as Run } from "../Workspace";
|
|
|
|
class ObserverStub {
|
|
observe() {}
|
|
unobserve() {}
|
|
disconnect() {}
|
|
takeRecords() {
|
|
return [];
|
|
}
|
|
}
|
|
globalThis.ResizeObserver =
|
|
globalThis.ResizeObserver || (ObserverStub as unknown as typeof ResizeObserver);
|
|
|
|
async function settle() {
|
|
await act(async () => {
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
});
|
|
}
|
|
|
|
async function renderRun() {
|
|
const utils = render(
|
|
<MemoryRouter initialEntries={["/run"]}>
|
|
<Run />
|
|
</MemoryRouter>
|
|
);
|
|
await settle();
|
|
return utils;
|
|
}
|
|
|
|
function cwdInput(): HTMLInputElement {
|
|
const placeholder = i18n.t("run:fields.cwdPlaceholder");
|
|
return screen.getByPlaceholderText(placeholder) as HTMLInputElement;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
i18n.changeLanguage("en");
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe("Run page — default working directory (issue #202)", () => {
|
|
it("pre-fills cwd with the HOME suggestion, not the dashboard cwd", async () => {
|
|
cwdItems = [DASHBOARD, HOME, RECENT];
|
|
await renderRun();
|
|
expect(cwdInput().value).toBe(HOME.path);
|
|
});
|
|
|
|
it("falls back to the dashboard cwd when no home suggestion exists", async () => {
|
|
cwdItems = [DASHBOARD, RECENT];
|
|
await renderRun();
|
|
expect(cwdInput().value).toBe(DASHBOARD.path);
|
|
});
|
|
|
|
it("leaves cwd empty when there are no suggestions at all", async () => {
|
|
cwdItems = [];
|
|
await renderRun();
|
|
expect(cwdInput().value).toBe("");
|
|
});
|
|
});
|