@korso/shepherd-ui 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ShepherdProvider.d.ts +12 -0
- package/dist/ShepherdProvider.js +33 -0
- package/dist/ShepherdProvider.test.d.ts +1 -0
- package/dist/ShepherdProvider.test.js +47 -0
- package/dist/ShepherdRoot.d.ts +9 -0
- package/dist/ShepherdRoot.js +88 -0
- package/dist/ShepherdRoot.routing.test.d.ts +1 -0
- package/dist/ShepherdRoot.routing.test.js +61 -0
- package/dist/ShepherdRoot.test.d.ts +1 -0
- package/dist/ShepherdRoot.test.js +37 -0
- package/dist/app/assets/index-CDOCIg6s.js +11 -0
- package/dist/app/index.html +12 -0
- package/dist/client.d.ts +107 -0
- package/dist/client.js +238 -0
- package/dist/client.test.d.ts +1 -0
- package/dist/client.test.js +298 -0
- package/dist/components/ActiveList.d.ts +22 -0
- package/dist/components/ActiveList.js +59 -0
- package/dist/components/Chat.d.ts +30 -0
- package/dist/components/Chat.js +61 -0
- package/dist/components/Composer.d.ts +37 -0
- package/dist/components/Composer.js +146 -0
- package/dist/components/Crew.d.ts +23 -0
- package/dist/components/Crew.js +31 -0
- package/dist/components/Dashboard.d.ts +34 -0
- package/dist/components/Dashboard.js +209 -0
- package/dist/components/DoneList.d.ts +29 -0
- package/dist/components/DoneList.js +50 -0
- package/dist/components/RepoSelect.d.ts +38 -0
- package/dist/components/RepoSelect.js +56 -0
- package/dist/components/Territory.d.ts +21 -0
- package/dist/components/Territory.js +21 -0
- package/dist/config/ConnectAgent.d.ts +10 -0
- package/dist/config/ConnectAgent.js +119 -0
- package/dist/config/ConnectAgent.test.d.ts +1 -0
- package/dist/config/ConnectAgent.test.js +92 -0
- package/dist/config/EmptyState.d.ts +12 -0
- package/dist/config/EmptyState.js +4 -0
- package/dist/config/EmptyState.test.d.ts +1 -0
- package/dist/config/EmptyState.test.js +32 -0
- package/dist/config/Members.d.ts +10 -0
- package/dist/config/Members.js +83 -0
- package/dist/config/Members.test.d.ts +1 -0
- package/dist/config/Members.test.js +66 -0
- package/dist/config/Workspaces.d.ts +12 -0
- package/dist/config/Workspaces.js +96 -0
- package/dist/config/Workspaces.test.d.ts +1 -0
- package/dist/config/Workspaces.test.js +90 -0
- package/dist/config/index.d.ts +8 -0
- package/dist/config/index.js +6 -0
- package/dist/context.d.ts +28 -0
- package/dist/context.js +36 -0
- package/dist/index.cjs +5289 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +149 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/Dashboard-CG6KeFQ3.js +1214 -0
- package/dist/lib/index.d.ts +438 -16
- package/dist/lib/index.js +455 -6
- package/dist/lib/selfhost.js +9 -9
- package/dist/lib/styles.css +27 -0
- package/dist/logic.d.ts +208 -0
- package/dist/logic.js +372 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +11 -0
- package/dist/selfhost/assets/{index-tSyzirZa.css → index-BZmImfYZ.css} +1 -1
- package/dist/selfhost/assets/index-CS268KSw.js +40 -0
- package/dist/selfhost/index.html +2 -2
- package/dist/selfhost.d.ts +22 -0
- package/dist/selfhost.js +97 -0
- package/dist/test/mockClient.d.ts +6 -0
- package/dist/test/mockClient.js +45 -0
- package/dist/test/setup.d.ts +1 -0
- package/dist/test/setup.js +2 -0
- package/dist/useLandscapePolling.d.ts +63 -0
- package/dist/useLandscapePolling.js +129 -0
- package/dist/views/Chat.d.ts +5 -0
- package/dist/views/Chat.js +60 -0
- package/dist/views/Chat.test.d.ts +1 -0
- package/dist/views/Chat.test.js +92 -0
- package/dist/views/Config.d.ts +12 -0
- package/dist/views/Config.js +6 -0
- package/dist/views/Tasks.d.ts +5 -0
- package/dist/views/Tasks.js +75 -0
- package/dist/views/Tasks.test.d.ts +1 -0
- package/dist/views/Tasks.test.js +115 -0
- package/dist/views/useLandscape.d.ts +14 -0
- package/dist/views/useLandscape.js +74 -0
- package/dist/views/useLandscape.test.d.ts +1 -0
- package/dist/views/useLandscape.test.js +101 -0
- package/dist/views/wallboard.d.ts +46 -0
- package/dist/views/wallboard.js +209 -0
- package/dist/views/wallboard.test.d.ts +1 -0
- package/dist/views/wallboard.test.js +143 -0
- package/package.json +1 -1
- package/dist/lib/Dashboard-CB6SL-J2.js +0 -1064
- package/dist/selfhost/assets/index-BBx3vo2Y.js +0 -40
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { ShepherdClient, ShepherdClientConfig } from "./client.js";
|
|
3
|
+
export interface ShepherdProviderProps {
|
|
4
|
+
/** A pre-built client. Mutually exclusive with `config`. */
|
|
5
|
+
client?: ShepherdClient;
|
|
6
|
+
/** Config to build a client from, when one is not supplied directly. */
|
|
7
|
+
config?: ShepherdClientConfig;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
export declare function ShepherdProvider({ client, config, children }: ShepherdProviderProps): import("react").JSX.Element;
|
|
11
|
+
/** Access the ShepherdClient from context. Throws if used outside a provider. */
|
|
12
|
+
export declare function useShepherdClient(): ShepherdClient;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useMemo } from "react";
|
|
3
|
+
import { createShepherdClient } from "./client.js";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// ShepherdProvider — supplies a ShepherdClient to the view tree via context.
|
|
6
|
+
//
|
|
7
|
+
// Auth-agnostic: the provider takes either a ready-made `client` or a `config`
|
|
8
|
+
// (baseUrl + optional getAuthHeader) and builds one. It imports no auth library.
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
const ShepherdContext = createContext(null);
|
|
11
|
+
export function ShepherdProvider({ client, config, children }) {
|
|
12
|
+
// Memoise on STABLE PRIMITIVE deps (baseUrl + getAuthHeader) rather than the
|
|
13
|
+
// whole `config` object. A consumer passing an inline `config={{ … }}` mints a
|
|
14
|
+
// new object every render; keying on it would rebuild the client each render and
|
|
15
|
+
// re-fire every client-dep effect (e.g. re-subscribing useLandscape's 5s poll).
|
|
16
|
+
// A directly-supplied `client` still takes precedence.
|
|
17
|
+
const baseUrl = config?.baseUrl;
|
|
18
|
+
const getAuthHeader = config?.getAuthHeader;
|
|
19
|
+
const value = useMemo(() => {
|
|
20
|
+
if (client)
|
|
21
|
+
return client;
|
|
22
|
+
return createShepherdClient({ baseUrl: baseUrl ?? "", getAuthHeader });
|
|
23
|
+
}, [client, baseUrl, getAuthHeader]);
|
|
24
|
+
return _jsx(ShepherdContext.Provider, { value: value, children: children });
|
|
25
|
+
}
|
|
26
|
+
/** Access the ShepherdClient from context. Throws if used outside a provider. */
|
|
27
|
+
export function useShepherdClient() {
|
|
28
|
+
const client = useContext(ShepherdContext);
|
|
29
|
+
if (!client) {
|
|
30
|
+
throw new Error("useShepherdClient must be used within a <ShepherdProvider>.");
|
|
31
|
+
}
|
|
32
|
+
return client;
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
3
|
+
import { render, renderHook, act } from "@testing-library/react";
|
|
4
|
+
import { ShepherdProvider, useShepherdClient } from "./ShepherdProvider.js";
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// ShepherdProvider — supplies the client via context. Covers the throw-outside-
|
|
7
|
+
// provider guard and the stable-deps memoisation (P2.5): an inline `config`
|
|
8
|
+
// object must not rebuild the client every render.
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
describe("useShepherdClient", () => {
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
vi.restoreAllMocks();
|
|
13
|
+
});
|
|
14
|
+
it("throws a clear error when used outside a <ShepherdProvider>", () => {
|
|
15
|
+
// Silence the expected React error-boundary console noise.
|
|
16
|
+
const spy = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
17
|
+
expect(() => renderHook(() => useShepherdClient())).toThrow(/must be used within a <ShepherdProvider>/);
|
|
18
|
+
spy.mockRestore();
|
|
19
|
+
});
|
|
20
|
+
it("returns the same client across renders when config props are stable", () => {
|
|
21
|
+
const getAuthHeader = () => "Bearer t";
|
|
22
|
+
const baseUrl = "https://hub.example.com";
|
|
23
|
+
function wrapper({ children }) {
|
|
24
|
+
// A NEW inline config object each render — identity changes every time.
|
|
25
|
+
return (_jsx(ShepherdProvider, { config: { baseUrl, getAuthHeader }, children: children }));
|
|
26
|
+
}
|
|
27
|
+
const { result, rerender } = renderHook(() => useShepherdClient(), { wrapper });
|
|
28
|
+
const first = result.current;
|
|
29
|
+
rerender();
|
|
30
|
+
// Memoised on primitive deps (baseUrl + getAuthHeader), so the client is
|
|
31
|
+
// stable even though the `config` object identity changed.
|
|
32
|
+
expect(result.current).toBe(first);
|
|
33
|
+
});
|
|
34
|
+
it("prefers a directly-supplied client over config", () => {
|
|
35
|
+
const client = { baseUrl: "x" };
|
|
36
|
+
const config = { baseUrl: "ignored" };
|
|
37
|
+
let seen;
|
|
38
|
+
function Probe() {
|
|
39
|
+
seen = useShepherdClient();
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
act(() => {
|
|
43
|
+
render(_jsx(ShepherdProvider, { client: client, config: config, children: _jsx(Probe, {}) }));
|
|
44
|
+
});
|
|
45
|
+
expect(seen).toBe(client);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface ShepherdRootProps {
|
|
2
|
+
/**
|
|
3
|
+
* The DIRECT Hub URL embedded in the agent install command (planning decision
|
|
4
|
+
* #2: the headless agent connects straight to the Hub, not the BFF). Defaults
|
|
5
|
+
* to the client's baseUrl, which is correct for self-host.
|
|
6
|
+
*/
|
|
7
|
+
hubUrl?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function ShepherdRoot({ hubUrl }: ShepherdRootProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { Dashboard } from "./components/Dashboard.js";
|
|
4
|
+
import { Workspaces } from "./config/Workspaces.js";
|
|
5
|
+
import { ConnectAgent } from "./config/ConnectAgent.js";
|
|
6
|
+
import { EmptyState } from "./config/EmptyState.js";
|
|
7
|
+
import { useShepherdClient } from "./context.js";
|
|
8
|
+
import { describeError } from "./client.js";
|
|
9
|
+
const VIEWS = [
|
|
10
|
+
{ id: "board", label: "Board" },
|
|
11
|
+
{ id: "config", label: "Config" },
|
|
12
|
+
];
|
|
13
|
+
export function ShepherdRoot({ hubUrl }) {
|
|
14
|
+
const client = useShepherdClient();
|
|
15
|
+
const [load, setLoad] = useState({ status: "loading" });
|
|
16
|
+
const [selectedId, setSelectedId] = useState(null);
|
|
17
|
+
const [view, setView] = useState("board");
|
|
18
|
+
// Roving-tabindex refs for the tablist: each tab button registers itself by
|
|
19
|
+
// index so arrow/Home/End navigation can move DOM focus to the target tab.
|
|
20
|
+
const tabRefs = useRef([]);
|
|
21
|
+
// WAI-ARIA "tabs with automatic activation": Arrow keys (wrap-around),
|
|
22
|
+
// Home, End change the active view AND move focus to the matching tab.
|
|
23
|
+
const onTabKeyDown = useCallback((e, index) => {
|
|
24
|
+
const last = VIEWS.length - 1;
|
|
25
|
+
let next = null;
|
|
26
|
+
switch (e.key) {
|
|
27
|
+
case "ArrowRight":
|
|
28
|
+
next = index === last ? 0 : index + 1;
|
|
29
|
+
break;
|
|
30
|
+
case "ArrowLeft":
|
|
31
|
+
next = index === 0 ? last : index - 1;
|
|
32
|
+
break;
|
|
33
|
+
case "Home":
|
|
34
|
+
next = 0;
|
|
35
|
+
break;
|
|
36
|
+
case "End":
|
|
37
|
+
next = last;
|
|
38
|
+
break;
|
|
39
|
+
default:
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
setView(VIEWS[next].id);
|
|
44
|
+
tabRefs.current[next]?.focus();
|
|
45
|
+
}, []);
|
|
46
|
+
// The landing view (Config vs Board) is decided exactly once, on the first
|
|
47
|
+
// resolved list (isInitial), so a later refresh (after create/join) never
|
|
48
|
+
// re-routes the user off whatever tab they're on.
|
|
49
|
+
const fetchWorkspaces = useCallback(async (isInitial) => {
|
|
50
|
+
try {
|
|
51
|
+
const res = await client.listWorkspaces();
|
|
52
|
+
setLoad({ status: "ready", workspaces: res.workspaces });
|
|
53
|
+
// Keep the current selection if it still exists; else pick the first.
|
|
54
|
+
setSelectedId((prev) => prev && res.workspaces.some((w) => w.id === prev)
|
|
55
|
+
? prev
|
|
56
|
+
: (res.workspaces[0]?.id ?? null));
|
|
57
|
+
if (isInitial) {
|
|
58
|
+
setView(res.workspaces.length === 0 ? "config" : "board");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
setLoad({
|
|
63
|
+
status: "error",
|
|
64
|
+
message: describeError(err),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}, [client]);
|
|
68
|
+
// Initial load + landing decision (runs once on mount).
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
void fetchWorkspaces(true);
|
|
71
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
72
|
+
}, []);
|
|
73
|
+
const refresh = useCallback(() => fetchWorkspaces(false), [fetchWorkspaces]);
|
|
74
|
+
const workspaces = load.status === "ready" ? load.workspaces : [];
|
|
75
|
+
const selected = workspaces.find((w) => w.id === selectedId) ?? null;
|
|
76
|
+
if (load.status === "loading") {
|
|
77
|
+
return (_jsx("div", { className: "shepherd-root", children: _jsx("p", { role: "status", children: "Loading\u2026" }) }));
|
|
78
|
+
}
|
|
79
|
+
if (load.status === "error") {
|
|
80
|
+
return (_jsx("div", { className: "shepherd-root", children: _jsx("p", { role: "alert", children: load.message }) }));
|
|
81
|
+
}
|
|
82
|
+
const hasWorkspace = workspaces.length > 0;
|
|
83
|
+
return (_jsxs("div", { className: "shepherd-root", children: [_jsxs("header", { children: [_jsx("h1", { className: "brand", style: { margin: 0, font: "inherit" }, children: "Shepherd" }), _jsx("nav", { className: "tabs", role: "tablist", "aria-label": "Shepherd views", children: VIEWS.map(({ id, label }, index) => (_jsx("button", { ref: (el) => {
|
|
84
|
+
tabRefs.current[index] = el;
|
|
85
|
+
}, type: "button", role: "tab", id: `tab-${id}`, "aria-controls": `panel-${id}`, "aria-selected": view === id, tabIndex: view === id ? 0 : -1, onClick: () => setView(id), onKeyDown: (e) => onTabKeyDown(e, index), children: label }, id))) })] }), _jsxs("main", { children: [view === "board" && (_jsx("div", { role: "tabpanel", id: "panel-board", "aria-labelledby": "tab-board", tabIndex: 0, children: hasWorkspace && selected ? (_jsx(Dashboard, { workspaceId: selected.id })) : (_jsx(EmptyState, { onGetStarted: () => setView("config") })) })), view === "config" && (_jsx("div", { role: "tabpanel", id: "panel-config", "aria-labelledby": "tab-config", tabIndex: 0, children: _jsxs("section", { className: "shepherd-config", "aria-labelledby": "config-heading", children: [_jsx("h2", { id: "config-heading", children: "Configuration" }), _jsx(Workspaces, { workspaces: workspaces, selected: selected, onChanged: () => {
|
|
86
|
+
void refresh();
|
|
87
|
+
}, onSelect: setSelectedId }), selected && _jsx(ConnectAgent, { workspaceId: selected.id, hubUrl: hubUrl })] }) }))] })] }));
|
|
88
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
3
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
4
|
+
import { userEvent } from "@testing-library/user-event";
|
|
5
|
+
import { ShepherdProvider } from "./ShepherdProvider.js";
|
|
6
|
+
import { ShepherdRoot } from "./ShepherdRoot.js";
|
|
7
|
+
import { makeMockClient } from "./test/mockClient.js";
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// ShepherdRoot — routing / landing + empty-state gating (Task 6.3).
|
|
10
|
+
//
|
|
11
|
+
// The route guard keys off listWorkspaces() being empty:
|
|
12
|
+
// - no workspace → land on Config; Tasks/Chat render an EmptyState prompt.
|
|
13
|
+
// - >= 1 workspace → land on Tasks for the first (selected) workspace.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
const WS = { id: "ws_1", slug: "acme", name: "Acme", role: "admin" };
|
|
16
|
+
describe("ShepherdRoot routing / landing", () => {
|
|
17
|
+
let client;
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
client = makeMockClient();
|
|
20
|
+
});
|
|
21
|
+
function renderRoot() {
|
|
22
|
+
return render(_jsx(ShepherdProvider, { client: client, children: _jsx(ShepherdRoot, { hubUrl: "https://hub.example.run.app" }) }));
|
|
23
|
+
}
|
|
24
|
+
it("lands on Tasks when the account has at least one workspace", async () => {
|
|
25
|
+
client.listWorkspaces = vi.fn().mockResolvedValue({ workspaces: [WS] });
|
|
26
|
+
renderRoot();
|
|
27
|
+
await waitFor(() => expect(screen.getByRole("tab", { name: "Tasks" })).toHaveAttribute("aria-selected", "true"));
|
|
28
|
+
expect(screen.getByRole("heading", { name: "Tasks" })).toBeInTheDocument();
|
|
29
|
+
});
|
|
30
|
+
it("passes the selected workspaceId into the Tasks view", async () => {
|
|
31
|
+
client.listWorkspaces = vi.fn().mockResolvedValue({ workspaces: [WS] });
|
|
32
|
+
renderRoot();
|
|
33
|
+
await waitFor(() => expect(screen.getByTestId("tasks-workspace-id")).toHaveTextContent("ws_1"));
|
|
34
|
+
});
|
|
35
|
+
it("lands on Config when the account has no workspace", async () => {
|
|
36
|
+
client.listWorkspaces = vi.fn().mockResolvedValue({ workspaces: [] });
|
|
37
|
+
renderRoot();
|
|
38
|
+
await waitFor(() => expect(screen.getByRole("tab", { name: "Config" })).toHaveAttribute("aria-selected", "true"));
|
|
39
|
+
});
|
|
40
|
+
it("shows an EmptyState (not an error) on Tasks when there is no workspace", async () => {
|
|
41
|
+
client.listWorkspaces = vi.fn().mockResolvedValue({ workspaces: [] });
|
|
42
|
+
renderRoot();
|
|
43
|
+
// Wait for the load to resolve (Config becomes active).
|
|
44
|
+
await waitFor(() => expect(screen.getByRole("tab", { name: "Config" })).toHaveAttribute("aria-selected", "true"));
|
|
45
|
+
await userEvent.click(screen.getByRole("tab", { name: "Tasks" }));
|
|
46
|
+
// The empty-state prompt renders, never a hard wall / crash.
|
|
47
|
+
expect(screen.getByRole("heading", { name: /no workspace yet|get started/i })).toBeInTheDocument();
|
|
48
|
+
// The real wallboard scaffold heading is NOT present.
|
|
49
|
+
expect(screen.queryByTestId("tasks-workspace-id")).not.toBeInTheDocument();
|
|
50
|
+
});
|
|
51
|
+
it("shows a loading indicator before the workspace list resolves", async () => {
|
|
52
|
+
let resolve;
|
|
53
|
+
client.listWorkspaces = vi.fn().mockReturnValue(new Promise((r) => {
|
|
54
|
+
resolve = r;
|
|
55
|
+
}));
|
|
56
|
+
renderRoot();
|
|
57
|
+
expect(screen.getByText(/loading/i)).toBeInTheDocument();
|
|
58
|
+
resolve({ workspaces: [WS] });
|
|
59
|
+
await waitFor(() => expect(screen.getByRole("heading", { name: "Tasks" })).toBeInTheDocument());
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { describe, it, expect, vi } from "vitest";
|
|
3
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
4
|
+
import { userEvent } from "@testing-library/user-event";
|
|
5
|
+
import { ShepherdProvider } from "./ShepherdProvider.js";
|
|
6
|
+
import { ShepherdRoot } from "./ShepherdRoot.js";
|
|
7
|
+
import { makeMockClient } from "./test/mockClient.js";
|
|
8
|
+
// Proves the jsdom + Testing Library harness works for downstream tasks: mount
|
|
9
|
+
// the shell inside the provider and confirm the three-view tab skeleton renders
|
|
10
|
+
// and switches. Task 6.3 made the shell async (it lands after listWorkspaces()
|
|
11
|
+
// resolves), so we inject a mock client with a workspace and wait for the load.
|
|
12
|
+
describe("ShepherdRoot", () => {
|
|
13
|
+
const WS = { id: "ws_1", slug: "acme", name: "Acme", role: "admin" };
|
|
14
|
+
function renderRoot() {
|
|
15
|
+
const client = makeMockClient({
|
|
16
|
+
listWorkspaces: vi.fn().mockResolvedValue({ workspaces: [WS] }),
|
|
17
|
+
});
|
|
18
|
+
return render(_jsx(ShepherdProvider, { client: client, children: _jsx(ShepherdRoot, {}) }));
|
|
19
|
+
}
|
|
20
|
+
it("mounts and shows the three-view tab shell", async () => {
|
|
21
|
+
renderRoot();
|
|
22
|
+
expect(await screen.findByRole("tab", { name: "Tasks" })).toBeInTheDocument();
|
|
23
|
+
expect(screen.getByRole("tab", { name: "Chat" })).toBeInTheDocument();
|
|
24
|
+
expect(screen.getByRole("tab", { name: "Config" })).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
it("defaults to the Tasks view when a workspace exists", async () => {
|
|
27
|
+
renderRoot();
|
|
28
|
+
await waitFor(() => expect(screen.getByRole("tab", { name: "Tasks" })).toHaveAttribute("aria-selected", "true"));
|
|
29
|
+
expect(screen.getByRole("heading", { name: "Tasks" })).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
it("switches to the Config view when its tab is clicked", async () => {
|
|
32
|
+
renderRoot();
|
|
33
|
+
await userEvent.click(await screen.findByRole("tab", { name: "Config" }));
|
|
34
|
+
expect(screen.getByRole("tab", { name: "Config" })).toHaveAttribute("aria-selected", "true");
|
|
35
|
+
expect(screen.getByRole("heading", { name: "Config" })).toBeInTheDocument();
|
|
36
|
+
});
|
|
37
|
+
});
|