@korso/shepherd-ui 0.2.0 → 0.2.1

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.
Files changed (61) hide show
  1. package/README.md +47 -47
  2. package/dist/ShepherdRoot.js +15 -57
  3. package/dist/components/Dashboard.d.ts +16 -2
  4. package/dist/components/Dashboard.js +76 -5
  5. package/dist/config/EmptyState.js +5 -1
  6. package/dist/lib/{Dashboard-CG6KeFQ3.js → Dashboard-B7nioe5N.js} +529 -457
  7. package/dist/lib/index.d.ts +15 -1
  8. package/dist/lib/index.js +196 -279
  9. package/dist/lib/selfhost.js +1 -1
  10. package/dist/lib/styles.css +217 -209
  11. package/dist/selfhost/assets/index-D6rTsbVM.js +40 -0
  12. package/dist/selfhost/assets/{index-BZmImfYZ.css → index-DnhlP_lc.css} +1 -1
  13. package/dist/selfhost/index.html +13 -13
  14. package/dist/useLandscapePolling.d.ts +7 -0
  15. package/dist/useLandscapePolling.js +6 -1
  16. package/package.json +67 -67
  17. package/dist/ShepherdProvider.d.ts +0 -12
  18. package/dist/ShepherdProvider.js +0 -33
  19. package/dist/ShepherdProvider.test.d.ts +0 -1
  20. package/dist/ShepherdProvider.test.js +0 -47
  21. package/dist/ShepherdRoot.routing.test.d.ts +0 -1
  22. package/dist/ShepherdRoot.routing.test.js +0 -61
  23. package/dist/ShepherdRoot.test.d.ts +0 -1
  24. package/dist/ShepherdRoot.test.js +0 -37
  25. package/dist/app/assets/index-CDOCIg6s.js +0 -11
  26. package/dist/app/index.html +0 -12
  27. package/dist/client.test.d.ts +0 -1
  28. package/dist/client.test.js +0 -298
  29. package/dist/config/ConnectAgent.test.d.ts +0 -1
  30. package/dist/config/ConnectAgent.test.js +0 -92
  31. package/dist/config/EmptyState.test.d.ts +0 -1
  32. package/dist/config/EmptyState.test.js +0 -32
  33. package/dist/config/Members.test.d.ts +0 -1
  34. package/dist/config/Members.test.js +0 -66
  35. package/dist/config/Workspaces.test.d.ts +0 -1
  36. package/dist/config/Workspaces.test.js +0 -90
  37. package/dist/index.cjs +0 -5289
  38. package/dist/index.cjs.map +0 -1
  39. package/dist/index.d.cts +0 -149
  40. package/dist/index.js.map +0 -1
  41. package/dist/selfhost/assets/index-CS268KSw.js +0 -40
  42. package/dist/test/setup.d.ts +0 -1
  43. package/dist/test/setup.js +0 -2
  44. package/dist/views/Chat.d.ts +0 -5
  45. package/dist/views/Chat.js +0 -60
  46. package/dist/views/Chat.test.d.ts +0 -1
  47. package/dist/views/Chat.test.js +0 -92
  48. package/dist/views/Config.d.ts +0 -12
  49. package/dist/views/Config.js +0 -6
  50. package/dist/views/Tasks.d.ts +0 -5
  51. package/dist/views/Tasks.js +0 -75
  52. package/dist/views/Tasks.test.d.ts +0 -1
  53. package/dist/views/Tasks.test.js +0 -115
  54. package/dist/views/useLandscape.d.ts +0 -14
  55. package/dist/views/useLandscape.js +0 -74
  56. package/dist/views/useLandscape.test.d.ts +0 -1
  57. package/dist/views/useLandscape.test.js +0 -101
  58. package/dist/views/wallboard.d.ts +0 -46
  59. package/dist/views/wallboard.js +0 -209
  60. package/dist/views/wallboard.test.d.ts +0 -1
  61. package/dist/views/wallboard.test.js +0 -143
@@ -1,12 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>Shepherd</title>
7
- <script type="module" crossorigin src="/assets/index-CDOCIg6s.js"></script>
8
- </head>
9
- <body>
10
- <div id="root"></div>
11
- </body>
12
- </html>
@@ -1 +0,0 @@
1
- export {};
@@ -1,298 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { createShepherdClient, ShepherdHttpError } from "./client.js";
3
- // ---------------------------------------------------------------------------
4
- // ShepherdClient — fetch-mock unit tests (DB-free; mocks global `fetch`).
5
- //
6
- // Each method asserts: the request URL (baseUrl + exact encoded path), the HTTP
7
- // method, that auth headers from getAuthHeader are merged (Bearer-string, header-
8
- // map, and undefined/no-op cases), Content-Type + body JSON on bodied requests,
9
- // and that the response is validated by the @shepherd/shared zod schema. A
10
- // malformed 2xx body throws; a non-2xx rejects with a typed ShepherdHttpError.
11
- // ---------------------------------------------------------------------------
12
- const BASE = "https://hub.example.com";
13
- /** Build a Response-like object for a mocked fetch resolution. */
14
- function jsonResponse(body, init) {
15
- const status = init?.status ?? 200;
16
- return {
17
- ok: status >= 200 && status < 300,
18
- status,
19
- statusText: init?.statusText ?? "OK",
20
- json: async () => body,
21
- text: async () => JSON.stringify(body),
22
- };
23
- }
24
- let fetchMock;
25
- beforeEach(() => {
26
- fetchMock = vi.fn();
27
- vi.stubGlobal("fetch", fetchMock);
28
- });
29
- afterEach(() => {
30
- vi.unstubAllGlobals();
31
- vi.restoreAllMocks();
32
- });
33
- /** The single call recorded on the fetch mock: [url, init]. */
34
- function lastCall() {
35
- const call = fetchMock.mock.calls[0];
36
- return [call[0], call[1]];
37
- }
38
- /** Read a header from a recorded RequestInit (headers stored as a plain record). */
39
- function header(init, name) {
40
- const h = init.headers;
41
- if (!h)
42
- return undefined;
43
- const key = Object.keys(h).find((k) => k.toLowerCase() === name.toLowerCase());
44
- return key ? h[key] : undefined;
45
- }
46
- // --- fixtures matching the @shepherd/shared schemas ------------------------
47
- const WORKSPACE_SUMMARY = { id: "ws_1", slug: "acme", name: "Acme", role: "admin" };
48
- const LANDSCAPE = {
49
- agents: [],
50
- tasks: [],
51
- announcements: [],
52
- serverTime: "2026-06-29T00:00:00.000Z",
53
- };
54
- describe("createShepherdClient", () => {
55
- let client;
56
- beforeEach(() => {
57
- client = createShepherdClient({ baseUrl: BASE });
58
- });
59
- describe("listWorkspaces", () => {
60
- it("GETs /workspaces and returns the validated body", async () => {
61
- fetchMock.mockResolvedValueOnce(jsonResponse({ workspaces: [WORKSPACE_SUMMARY] }));
62
- const out = await client.listWorkspaces();
63
- const [url, init] = lastCall();
64
- expect(url).toBe(`${BASE}/workspaces`);
65
- expect(init.method).toBe("GET");
66
- expect(out).toEqual({ workspaces: [WORKSPACE_SUMMARY] });
67
- });
68
- it("throws when the response body fails schema validation", async () => {
69
- fetchMock.mockResolvedValueOnce(jsonResponse({ workspaces: [{ id: "x" }] }));
70
- await expect(client.listWorkspaces()).rejects.toThrow();
71
- });
72
- it("rejects with a typed ShepherdHttpError carrying the status on non-2xx", async () => {
73
- fetchMock.mockResolvedValueOnce(jsonResponse({ error: "nope" }, { status: 401, statusText: "Unauthorized" }));
74
- await expect(client.listWorkspaces()).rejects.toBeInstanceOf(ShepherdHttpError);
75
- fetchMock.mockResolvedValueOnce(jsonResponse({ error: "nope" }, { status: 500, statusText: "Server Error" }));
76
- await expect(client.listWorkspaces()).rejects.toMatchObject({ status: 500 });
77
- });
78
- });
79
- describe("createWorkspace", () => {
80
- it("POSTs /workspaces with a JSON body and returns the WorkspaceSummary", async () => {
81
- fetchMock.mockResolvedValueOnce(jsonResponse(WORKSPACE_SUMMARY));
82
- const out = await client.createWorkspace({ name: "Acme" });
83
- const [url, init] = lastCall();
84
- expect(url).toBe(`${BASE}/workspaces`);
85
- expect(init.method).toBe("POST");
86
- expect(header(init, "Content-Type")).toBe("application/json");
87
- expect(JSON.parse(init.body)).toEqual({ name: "Acme" });
88
- expect(out).toEqual(WORKSPACE_SUMMARY);
89
- });
90
- });
91
- describe("mintToken", () => {
92
- it("POSTs /workspaces/:id/tokens and returns the MintTokenResponse", async () => {
93
- fetchMock.mockResolvedValueOnce(jsonResponse({ token: "shp_abc", id: "tok_1" }));
94
- const out = await client.mintToken("ws_1", { name: "ci" });
95
- const [url, init] = lastCall();
96
- expect(url).toBe(`${BASE}/workspaces/ws_1/tokens`);
97
- expect(init.method).toBe("POST");
98
- expect(JSON.parse(init.body)).toEqual({ name: "ci" });
99
- expect(out).toEqual({ token: "shp_abc", id: "tok_1" });
100
- });
101
- });
102
- describe("listTokens", () => {
103
- it("GETs /workspaces/:id/tokens and returns the ListTokensResponse", async () => {
104
- const token = {
105
- id: "tok_1",
106
- name: "ci",
107
- lastUsedAt: null,
108
- createdAt: "2026-06-29T00:00:00.000Z",
109
- revokedAt: null,
110
- };
111
- fetchMock.mockResolvedValueOnce(jsonResponse({ tokens: [token] }));
112
- const out = await client.listTokens("ws_1");
113
- const [url, init] = lastCall();
114
- expect(url).toBe(`${BASE}/workspaces/ws_1/tokens`);
115
- expect(init.method).toBe("GET");
116
- expect(out).toEqual({ tokens: [token] });
117
- });
118
- });
119
- describe("revokeToken", () => {
120
- it("DELETEs /workspaces/:id/tokens/:tokenId (no schema validation)", async () => {
121
- fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true }));
122
- await client.revokeToken("ws_1", "tok_1");
123
- const [url, init] = lastCall();
124
- expect(url).toBe(`${BASE}/workspaces/ws_1/tokens/tok_1`);
125
- expect(init.method).toBe("DELETE");
126
- });
127
- });
128
- describe("createInvite", () => {
129
- it("POSTs /workspaces/:id/invites and returns the InviteResponse", async () => {
130
- const invite = { code: "inv_abc", expiresAt: null, maxUses: 5, useCount: 0 };
131
- fetchMock.mockResolvedValueOnce(jsonResponse(invite));
132
- const out = await client.createInvite("ws_1", { maxUses: 5 });
133
- const [url, init] = lastCall();
134
- expect(url).toBe(`${BASE}/workspaces/ws_1/invites`);
135
- expect(init.method).toBe("POST");
136
- expect(JSON.parse(init.body)).toEqual({ maxUses: 5 });
137
- expect(out).toEqual(invite);
138
- });
139
- });
140
- describe("revokeInvite", () => {
141
- it("POSTs /workspaces/:id/invites/:code/revoke (encodes the code)", async () => {
142
- fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true }));
143
- await client.revokeInvite("ws_1", "a/b code");
144
- const [url, init] = lastCall();
145
- expect(url).toBe(`${BASE}/workspaces/ws_1/invites/a%2Fb%20code/revoke`);
146
- expect(init.method).toBe("POST");
147
- });
148
- });
149
- describe("redeemInvite", () => {
150
- it("POSTs /invites/:code/redeem and returns the RedeemInviteResponse", async () => {
151
- fetchMock.mockResolvedValueOnce(jsonResponse({ workspace: WORKSPACE_SUMMARY }));
152
- const out = await client.redeemInvite("inv_abc");
153
- const [url, init] = lastCall();
154
- expect(url).toBe(`${BASE}/invites/inv_abc/redeem`);
155
- expect(init.method).toBe("POST");
156
- expect(out).toEqual({ workspace: WORKSPACE_SUMMARY });
157
- });
158
- });
159
- describe("listMembers", () => {
160
- it("GETs /workspaces/:id/members and returns the ListMembersResponse", async () => {
161
- const member = {
162
- accountId: "acc_1",
163
- displayName: "Dana",
164
- githubLogin: "dana",
165
- avatarUrl: null,
166
- role: "member",
167
- };
168
- fetchMock.mockResolvedValueOnce(jsonResponse({ members: [member] }));
169
- const out = await client.listMembers("ws_1");
170
- const [url, init] = lastCall();
171
- expect(url).toBe(`${BASE}/workspaces/ws_1/members`);
172
- expect(init.method).toBe("GET");
173
- expect(out).toEqual({ members: [member] });
174
- });
175
- });
176
- describe("removeMember", () => {
177
- it("DELETEs /workspaces/:id/members/:accountId (encodes the accountId)", async () => {
178
- fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true }));
179
- await client.removeMember("ws_1", "acc/1");
180
- const [url, init] = lastCall();
181
- expect(url).toBe(`${BASE}/workspaces/ws_1/members/acc%2F1`);
182
- expect(init.method).toBe("DELETE");
183
- });
184
- });
185
- describe("leave", () => {
186
- it("POSTs /workspaces/:id/leave", async () => {
187
- fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true }));
188
- await client.leave("ws_1");
189
- const [url, init] = lastCall();
190
- expect(url).toBe(`${BASE}/workspaces/ws_1/leave`);
191
- expect(init.method).toBe("POST");
192
- });
193
- });
194
- describe("landscape", () => {
195
- it("GETs /workspaces/:id/landscape and returns the WorkspaceLandscapeResponse", async () => {
196
- fetchMock.mockResolvedValueOnce(jsonResponse(LANDSCAPE));
197
- const out = await client.landscape("ws_1");
198
- const [url, init] = lastCall();
199
- expect(url).toBe(`${BASE}/workspaces/ws_1/landscape`);
200
- expect(init.method).toBe("GET");
201
- expect(out).toEqual(LANDSCAPE);
202
- });
203
- });
204
- describe("announce", () => {
205
- it("POSTs /workspaces/:id/announce and returns the WorkspaceAnnounceResponse", async () => {
206
- fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true, announcementIds: [1, 2] }));
207
- const out = await client.announce("ws_1", { body: "hello", repo: "r" });
208
- const [url, init] = lastCall();
209
- expect(url).toBe(`${BASE}/workspaces/ws_1/announce`);
210
- expect(init.method).toBe("POST");
211
- expect(header(init, "Content-Type")).toBe("application/json");
212
- expect(JSON.parse(init.body)).toEqual({ body: "hello", repo: "r" });
213
- expect(out).toEqual({ ok: true, announcementIds: [1, 2] });
214
- });
215
- });
216
- // --- encodeURIComponent on path params ----------------------------------
217
- describe("path-param encoding", () => {
218
- it("encodes the workspaceId segment", async () => {
219
- fetchMock.mockResolvedValueOnce(jsonResponse(LANDSCAPE));
220
- await client.landscape("ws/1 special");
221
- const [url] = lastCall();
222
- expect(url).toBe(`${BASE}/workspaces/ws%2F1%20special/landscape`);
223
- });
224
- it("encodes a tokenId with a special char", async () => {
225
- fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true }));
226
- await client.revokeToken("ws_1", "tok/1");
227
- const [url] = lastCall();
228
- expect(url).toBe(`${BASE}/workspaces/ws_1/tokens/tok%2F1`);
229
- });
230
- });
231
- });
232
- // --- baseUrl joining --------------------------------------------------------
233
- describe("baseUrl joining", () => {
234
- it("does not double-slash when baseUrl has a trailing slash", async () => {
235
- const fm = vi.fn().mockResolvedValueOnce(jsonResponse({ workspaces: [] }));
236
- vi.stubGlobal("fetch", fm);
237
- const client = createShepherdClient({ baseUrl: `${BASE}/` });
238
- await client.listWorkspaces();
239
- expect(fm.mock.calls[0][0]).toBe(`${BASE}/workspaces`);
240
- vi.unstubAllGlobals();
241
- });
242
- it("supports same-origin ('') baseUrl producing a root-relative path", async () => {
243
- const fm = vi.fn().mockResolvedValueOnce(jsonResponse({ workspaces: [] }));
244
- vi.stubGlobal("fetch", fm);
245
- const client = createShepherdClient({ baseUrl: "" });
246
- await client.listWorkspaces();
247
- expect(fm.mock.calls[0][0]).toBe("/workspaces");
248
- vi.unstubAllGlobals();
249
- });
250
- });
251
- // --- getAuthHeader resolution: all three return shapes, sync + async --------
252
- describe("getAuthHeader injection", () => {
253
- beforeEach(() => {
254
- fetchMock = vi.fn().mockResolvedValue(jsonResponse({ workspaces: [] }));
255
- vi.stubGlobal("fetch", fetchMock);
256
- });
257
- afterEach(() => vi.unstubAllGlobals());
258
- it("merges a Bearer string into the Authorization header (sync)", async () => {
259
- const client = createShepherdClient({
260
- baseUrl: BASE,
261
- getAuthHeader: () => "Bearer team_tok",
262
- });
263
- await client.listWorkspaces();
264
- const [, init] = lastCall();
265
- expect(header(init, "Authorization")).toBe("Bearer team_tok");
266
- });
267
- it("merges a Bearer string returned asynchronously", async () => {
268
- const client = createShepherdClient({
269
- baseUrl: BASE,
270
- getAuthHeader: async () => "Bearer async_tok",
271
- });
272
- await client.listWorkspaces();
273
- const [, init] = lastCall();
274
- expect(header(init, "Authorization")).toBe("Bearer async_tok");
275
- });
276
- it("merges a header-map return", async () => {
277
- const client = createShepherdClient({
278
- baseUrl: BASE,
279
- getAuthHeader: () => ({ Authorization: "Bearer map_tok", "X-Extra": "1" }),
280
- });
281
- await client.listWorkspaces();
282
- const [, init] = lastCall();
283
- expect(header(init, "Authorization")).toBe("Bearer map_tok");
284
- expect(header(init, "X-Extra")).toBe("1");
285
- });
286
- it("sends no auth header when getAuthHeader is omitted (same-origin BFF)", async () => {
287
- const client = createShepherdClient({ baseUrl: BASE });
288
- await client.listWorkspaces();
289
- const [, init] = lastCall();
290
- expect(header(init, "Authorization")).toBeUndefined();
291
- });
292
- it("sends no auth header when getAuthHeader resolves to undefined", async () => {
293
- const client = createShepherdClient({ baseUrl: BASE, getAuthHeader: () => undefined });
294
- await client.listWorkspaces();
295
- const [, init] = lastCall();
296
- expect(header(init, "Authorization")).toBeUndefined();
297
- });
298
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,92 +0,0 @@
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 { ConnectAgent } from "./ConnectAgent.js";
7
- import { makeMockClient } from "../test/mockClient.js";
8
- // ---------------------------------------------------------------------------
9
- // ConnectAgent — install-command render + token mint/revoke. DB-free: the mock
10
- // ShepherdClient is injected via ShepherdProvider's `client` prop.
11
- // ---------------------------------------------------------------------------
12
- describe("ConnectAgent", () => {
13
- const WORKSPACE_ID = "ws_1";
14
- const HUB_URL = "https://hub.example.run.app";
15
- let client;
16
- beforeEach(() => {
17
- client = makeMockClient({
18
- listTokens: vi.fn().mockResolvedValue({ tokens: [] }),
19
- });
20
- });
21
- function renderConnect() {
22
- return render(_jsx(ShepherdProvider, { client: client, children: _jsx(ConnectAgent, { workspaceId: WORKSPACE_ID, hubUrl: HUB_URL }) }));
23
- }
24
- it("does not show a raw token until one is minted", async () => {
25
- renderConnect();
26
- await waitFor(() => expect(client.listTokens).toHaveBeenCalled());
27
- // The command shows a placeholder, not a real shp_ token, before minting.
28
- const cmd = screen.getByTestId("install-command").textContent ?? "";
29
- expect(cmd).not.toMatch(/shp_realtoken/);
30
- });
31
- it("renders the install command with the direct Hub URL after minting a token", async () => {
32
- client.mintToken = vi
33
- .fn()
34
- .mockResolvedValue({ token: "shp_realtoken123", id: "tok_1" });
35
- renderConnect();
36
- await waitFor(() => expect(client.listTokens).toHaveBeenCalled());
37
- await userEvent.click(screen.getByRole("button", { name: /generate token/i }));
38
- await waitFor(() => {
39
- const cmd = screen.getByTestId("install-command").textContent ?? "";
40
- expect(cmd).toContain("shp_realtoken123");
41
- expect(cmd).toContain(HUB_URL);
42
- });
43
- expect(client.mintToken).toHaveBeenCalledWith(WORKSPACE_ID, expect.any(Object));
44
- });
45
- it("surfaces the raw token exactly once (with a one-time notice)", async () => {
46
- client.mintToken = vi
47
- .fn()
48
- .mockResolvedValue({ token: "shp_onceonly", id: "tok_1" });
49
- renderConnect();
50
- await userEvent.click(screen.getByRole("button", { name: /generate token/i }));
51
- await waitFor(() => {
52
- expect(screen.getByTestId("install-command").textContent).toContain("shp_onceonly");
53
- });
54
- // The one-time warning is shown so the operator knows to copy it now.
55
- expect(screen.getByText(/won't be shown again|shown once|only.*once/i)).toBeInTheDocument();
56
- });
57
- it("switches the install command when a different tool is picked", async () => {
58
- client.mintToken = vi
59
- .fn()
60
- .mockResolvedValue({ token: "shp_tok", id: "tok_1" });
61
- renderConnect();
62
- await userEvent.click(screen.getByRole("button", { name: /generate token/i }));
63
- await waitFor(() => expect(screen.getByTestId("install-command").textContent).toContain("shp_tok"));
64
- const claudeCmd = screen.getByTestId("install-command").textContent ?? "";
65
- expect(claudeCmd).toMatch(/claude mcp add/);
66
- // Pick Codex.
67
- await userEvent.selectOptions(screen.getByLabelText(/tool/i), "codex");
68
- await waitFor(() => {
69
- const codexCmd = screen.getByTestId("install-command").textContent ?? "";
70
- expect(codexCmd).toMatch(/codex mcp add/);
71
- expect(codexCmd).toContain("shp_tok");
72
- });
73
- });
74
- it("lists existing tokens and revokes one", async () => {
75
- client.listTokens = vi.fn().mockResolvedValue({
76
- tokens: [
77
- {
78
- id: "tok_old",
79
- name: "laptop",
80
- lastUsedAt: null,
81
- createdAt: "2026-06-01T00:00:00.000Z",
82
- revokedAt: null,
83
- },
84
- ],
85
- });
86
- client.revokeToken = vi.fn().mockResolvedValue(undefined);
87
- renderConnect();
88
- await waitFor(() => expect(screen.getByText("laptop")).toBeInTheDocument());
89
- await userEvent.click(screen.getByRole("button", { name: /revoke/i }));
90
- await waitFor(() => expect(client.revokeToken).toHaveBeenCalledWith(WORKSPACE_ID, "tok_old"));
91
- });
92
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,32 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { describe, it, expect, vi } from "vitest";
3
- import { render, screen } from "@testing-library/react";
4
- import { userEvent } from "@testing-library/user-event";
5
- import { EmptyState } from "./EmptyState.js";
6
- // ---------------------------------------------------------------------------
7
- // EmptyState — the reusable "no workspace yet" prompt with an optional CTA.
8
- // ---------------------------------------------------------------------------
9
- describe("EmptyState", () => {
10
- it("renders the default title, copy, and CTA label", () => {
11
- render(_jsx(EmptyState, { onGetStarted: () => { } }));
12
- expect(screen.getByRole("heading", { name: /no workspace yet/i })).toBeInTheDocument();
13
- expect(screen.getByText(/create a workspace or join one/i)).toBeInTheDocument();
14
- expect(screen.getByRole("button", { name: /go to config/i })).toBeInTheDocument();
15
- });
16
- it("invokes onGetStarted when the CTA is clicked", async () => {
17
- const onGetStarted = vi.fn();
18
- render(_jsx(EmptyState, { onGetStarted: onGetStarted }));
19
- await userEvent.click(screen.getByRole("button", { name: /go to config/i }));
20
- expect(onGetStarted).toHaveBeenCalledTimes(1);
21
- });
22
- it("honours custom title, copy, and CTA label", () => {
23
- render(_jsx(EmptyState, { title: "Nothing here", ctaLabel: "Get going", onGetStarted: () => { }, children: "Custom supporting copy." }));
24
- expect(screen.getByRole("heading", { name: "Nothing here" })).toBeInTheDocument();
25
- expect(screen.getByText("Custom supporting copy.")).toBeInTheDocument();
26
- expect(screen.getByRole("button", { name: "Get going" })).toBeInTheDocument();
27
- });
28
- it("omits the CTA when no handler is supplied", () => {
29
- render(_jsx(EmptyState, {}));
30
- expect(screen.queryByRole("button")).not.toBeInTheDocument();
31
- });
32
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,66 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { describe, it, expect, vi, beforeEach, afterEach } 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 { Members } from "./Members.js";
7
- import { makeMockClient } from "../test/mockClient.js";
8
- // ---------------------------------------------------------------------------
9
- // Members — the workspace roster with an admin-only remove control. Fetches the
10
- // list on mount and removes a member through the client.
11
- // ---------------------------------------------------------------------------
12
- const WORKSPACE_ID = "ws_1";
13
- describe("Members", () => {
14
- let client;
15
- beforeEach(() => {
16
- client = makeMockClient();
17
- });
18
- afterEach(() => {
19
- vi.restoreAllMocks();
20
- });
21
- function renderMembers() {
22
- return render(_jsx(ShepherdProvider, { client: client, children: _jsx(Members, { workspaceId: WORKSPACE_ID }) }));
23
- }
24
- it("lists members fetched from the client", async () => {
25
- client.listMembers = vi.fn().mockResolvedValue({
26
- members: [
27
- {
28
- accountId: "acc_1",
29
- displayName: "Alice",
30
- githubLogin: "alice",
31
- avatarUrl: null,
32
- role: "admin",
33
- },
34
- ],
35
- });
36
- renderMembers();
37
- await waitFor(() => expect(client.listMembers).toHaveBeenCalledWith(WORKSPACE_ID));
38
- expect(await screen.findByText("Alice")).toBeInTheDocument();
39
- expect(screen.getByText("admin")).toBeInTheDocument();
40
- });
41
- it("shows an empty state when there are no members", async () => {
42
- client.listMembers = vi.fn().mockResolvedValue({ members: [] });
43
- renderMembers();
44
- expect(await screen.findByText(/no members/i)).toBeInTheDocument();
45
- });
46
- it("removes a member through the client", async () => {
47
- client.listMembers = vi.fn().mockResolvedValue({
48
- members: [
49
- {
50
- accountId: "acc_2",
51
- displayName: "Bob",
52
- githubLogin: "bob",
53
- avatarUrl: null,
54
- role: "member",
55
- },
56
- ],
57
- });
58
- client.removeMember = vi.fn().mockResolvedValue(undefined);
59
- renderMembers();
60
- expect(await screen.findByText("Bob")).toBeInTheDocument();
61
- await userEvent.click(screen.getByRole("button", { name: /remove/i }));
62
- await waitFor(() => expect(client.removeMember).toHaveBeenCalledWith(WORKSPACE_ID, "acc_2"));
63
- // Optimistically dropped from the list.
64
- await waitFor(() => expect(screen.queryByText("Bob")).not.toBeInTheDocument());
65
- });
66
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,90 +0,0 @@
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 { Workspaces } from "./Workspaces.js";
7
- import { makeMockClient } from "../test/mockClient.js";
8
- // ---------------------------------------------------------------------------
9
- // Workspaces — create / join, plus admin-only invite + member-admin controls.
10
- // Admin-ness is read from the selected workspace's `role` (WorkspaceSummary.role).
11
- // ---------------------------------------------------------------------------
12
- const ADMIN_WS = { id: "ws_1", slug: "acme", name: "Acme", role: "admin" };
13
- const MEMBER_WS = { id: "ws_2", slug: "beta", name: "Beta", role: "member" };
14
- describe("Workspaces", () => {
15
- let client;
16
- beforeEach(() => {
17
- client = makeMockClient();
18
- });
19
- function renderWorkspaces(props = {}) {
20
- return render(_jsx(ShepherdProvider, { client: client, children: _jsx(Workspaces, { workspaces: props.workspace ? [props.workspace] : [], selected: props.workspace ?? null, onChanged: props.onChanged ?? (() => { }) }) }));
21
- }
22
- it("creates a workspace via the client", async () => {
23
- const onChanged = vi.fn();
24
- renderWorkspaces({ onChanged });
25
- await userEvent.type(screen.getByLabelText(/workspace name/i), "My Team");
26
- await userEvent.click(screen.getByRole("button", { name: /create workspace/i }));
27
- await waitFor(() => expect(client.createWorkspace).toHaveBeenCalledWith({ name: "My Team" }));
28
- expect(onChanged).toHaveBeenCalled();
29
- });
30
- it("joins a workspace by redeeming an invite code", async () => {
31
- const onChanged = vi.fn();
32
- renderWorkspaces({ onChanged });
33
- await userEvent.type(screen.getByLabelText(/invite code/i), "INV-123");
34
- await userEvent.click(screen.getByRole("button", { name: /join/i }));
35
- await waitFor(() => expect(client.redeemInvite).toHaveBeenCalledWith("INV-123"));
36
- expect(onChanged).toHaveBeenCalled();
37
- });
38
- it("shows invite + member-admin controls for an admin", async () => {
39
- client.listMembers = vi.fn().mockResolvedValue({
40
- members: [
41
- {
42
- accountId: "acc_1",
43
- displayName: "Alice",
44
- githubLogin: "alice",
45
- avatarUrl: null,
46
- role: "admin",
47
- },
48
- ],
49
- });
50
- renderWorkspaces({ workspace: ADMIN_WS });
51
- expect(screen.getByRole("button", { name: /create invite/i })).toBeInTheDocument();
52
- await waitFor(() => expect(screen.getByText("Alice")).toBeInTheDocument());
53
- });
54
- it("hides invite + member-admin controls for a non-admin member", async () => {
55
- renderWorkspaces({ workspace: MEMBER_WS });
56
- expect(screen.queryByRole("button", { name: /create invite/i })).not.toBeInTheDocument();
57
- // The member list is not even fetched for a non-admin.
58
- expect(client.listMembers).not.toHaveBeenCalled();
59
- });
60
- it("creates an invite (admin) and shows the code", async () => {
61
- client.createInvite = vi.fn().mockResolvedValue({
62
- code: "INV-NEW",
63
- expiresAt: null,
64
- maxUses: 5,
65
- useCount: 0,
66
- });
67
- renderWorkspaces({ workspace: ADMIN_WS });
68
- await userEvent.click(screen.getByRole("button", { name: /create invite/i }));
69
- await waitFor(() => expect(client.createInvite).toHaveBeenCalledWith("ws_1", expect.any(Object)));
70
- await waitFor(() => expect(screen.getByText(/INV-NEW/)).toBeInTheDocument());
71
- });
72
- it("removes a member (admin)", async () => {
73
- client.listMembers = vi.fn().mockResolvedValue({
74
- members: [
75
- {
76
- accountId: "acc_2",
77
- displayName: "Bob",
78
- githubLogin: "bob",
79
- avatarUrl: null,
80
- role: "member",
81
- },
82
- ],
83
- });
84
- client.removeMember = vi.fn().mockResolvedValue(undefined);
85
- renderWorkspaces({ workspace: ADMIN_WS });
86
- await waitFor(() => expect(screen.getByText("Bob")).toBeInTheDocument());
87
- await userEvent.click(screen.getByRole("button", { name: /remove/i }));
88
- await waitFor(() => expect(client.removeMember).toHaveBeenCalledWith("ws_1", "acc_2"));
89
- });
90
- });