@cosmicdrift/kumiko-renderer-web 0.14.0 → 0.15.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.
Files changed (33) hide show
  1. package/package.json +2 -2
  2. package/src/__tests__/avatar.test.tsx +1 -2
  3. package/src/__tests__/combobox.test.tsx +1 -2
  4. package/src/__tests__/config-edit.test.tsx +5 -8
  5. package/src/__tests__/create-app.test.tsx +30 -10
  6. package/src/__tests__/date-input.test.tsx +2 -3
  7. package/src/__tests__/default-app-shell.test.tsx +1 -2
  8. package/src/__tests__/dispatcher-context.test.tsx +1 -2
  9. package/src/__tests__/dispatcher-status-wiring.test.tsx +5 -6
  10. package/src/__tests__/kumiko-screen.test.tsx +2 -3
  11. package/src/__tests__/language-switcher.test.tsx +2 -3
  12. package/src/__tests__/money-input.test.tsx +8 -9
  13. package/src/__tests__/nav-base-path.test.tsx +1 -2
  14. package/src/__tests__/nav-search-params.test.tsx +1 -2
  15. package/src/__tests__/nav-tree.test.tsx +1 -2
  16. package/src/__tests__/nav.test.tsx +16 -9
  17. package/src/__tests__/primitives.test.tsx +53 -54
  18. package/src/__tests__/render-edit.test.tsx +3 -4
  19. package/src/__tests__/render-list-column-renderer.test.tsx +3 -4
  20. package/src/__tests__/render-list-debounce.test.tsx +9 -10
  21. package/src/__tests__/render-list.test.tsx +3 -4
  22. package/src/__tests__/sidebar.test.tsx +1 -2
  23. package/src/__tests__/theme-toggle.test.tsx +4 -5
  24. package/src/__tests__/toast.test.tsx +1 -2
  25. package/src/__tests__/use-form.test.tsx +6 -7
  26. package/src/__tests__/use-query-live.test.tsx +1 -2
  27. package/src/__tests__/use-query.test.tsx +7 -8
  28. package/src/__tests__/use-store.test.tsx +2 -3
  29. package/src/__tests__/visual-tree-integration.test.tsx +1 -2
  30. package/src/__tests__/workspace-shell.test.tsx +2 -4
  31. package/src/app/create-app.tsx +5 -3
  32. package/src/layout/__tests__/visual-tree.test.tsx +33 -30
  33. package/CHANGELOG.md +0 -485
@@ -1,4 +1,4 @@
1
- // @vitest-environment jsdom
1
+ import { describe, expect, test } from "bun:test";
2
2
  import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
3
3
  import {
4
4
  DispatcherProvider,
@@ -8,7 +8,6 @@ import {
8
8
  useQuery,
9
9
  } from "@cosmicdrift/kumiko-renderer";
10
10
  import type { ReactNode } from "react";
11
- import { describe, expect, test } from "vitest";
12
11
  import { act, createMockDispatcher, render, waitFor } from "./test-utils";
13
12
 
14
13
  // Test-Helper: fake LiveEventSubscriber. Sammelt alle Subscriber, das
@@ -1,8 +1,7 @@
1
- // @vitest-environment jsdom
1
+ import { describe, expect, mock, test } from "bun:test";
2
2
  import type { Dispatcher, DispatcherError } from "@cosmicdrift/kumiko-headless";
3
3
  import { DispatcherProvider, useQuery } from "@cosmicdrift/kumiko-renderer";
4
4
  import type { ReactNode } from "react";
5
- import { describe, expect, test, vi } from "vitest";
6
5
  import { act, createMockDispatcher, renderHook, waitFor } from "./test-utils";
7
6
 
8
7
  function makeDispatcher(queryFn?: Dispatcher["query"]): Dispatcher {
@@ -17,7 +16,7 @@ function wrap(dispatcher: Dispatcher) {
17
16
 
18
17
  describe("useQuery", () => {
19
18
  test("loads on mount; populates data, flips loading to false", async () => {
20
- const query = vi.fn(
19
+ const query = mock(
21
20
  async () => ({ isSuccess: true, data: [{ id: "1" }, { id: "2" }] }) as never,
22
21
  );
23
22
  const dispatcher = makeDispatcher(query as unknown as Dispatcher["query"]);
@@ -32,7 +31,7 @@ describe("useQuery", () => {
32
31
  await waitFor(() => expect(result.current.loading).toBe(false));
33
32
  expect(result.current.data).toEqual([{ id: "1" }, { id: "2" }]);
34
33
  expect(result.current.error).toBeNull();
35
- expect(query).toHaveBeenCalledOnce();
34
+ expect(query).toHaveBeenCalledTimes(1);
36
35
  });
37
36
 
38
37
  test("server error surfaces via `error`; data stays null", async () => {
@@ -42,7 +41,7 @@ describe("useQuery", () => {
42
41
  i18nKey: "errors.not_found",
43
42
  message: "no",
44
43
  };
45
- const query = vi.fn(async () => ({ isSuccess: false, error: err }) as never);
44
+ const query = mock(async () => ({ isSuccess: false, error: err }) as never);
46
45
  const { result } = renderHook(() => useQuery("task:list", {}), {
47
46
  wrapper: wrap(makeDispatcher(query as unknown as Dispatcher["query"])),
48
47
  });
@@ -53,7 +52,7 @@ describe("useQuery", () => {
53
52
  });
54
53
 
55
54
  test("enabled:false skips the auto-fetch until refetch is called", async () => {
56
- const query = vi.fn(async () => ({ isSuccess: true, data: ["hi"] }) as never);
55
+ const query = mock(async () => ({ isSuccess: true, data: ["hi"] }) as never);
57
56
  const { result } = renderHook(() => useQuery("task:list", {}, { enabled: false }), {
58
57
  wrapper: wrap(makeDispatcher(query as unknown as Dispatcher["query"])),
59
58
  });
@@ -65,13 +64,13 @@ describe("useQuery", () => {
65
64
  await act(async () => {
66
65
  await result.current.refetch();
67
66
  });
68
- expect(query).toHaveBeenCalledOnce();
67
+ expect(query).toHaveBeenCalledTimes(1);
69
68
  expect(result.current.data).toEqual(["hi"]);
70
69
  });
71
70
 
72
71
  test("refetch re-runs and replaces data (after-mutation reload pattern)", async () => {
73
72
  let callCount = 0;
74
- const query = vi.fn(async () => {
73
+ const query = mock(async () => {
75
74
  callCount += 1;
76
75
  return { isSuccess: true, data: [callCount] } as never;
77
76
  });
@@ -1,7 +1,6 @@
1
- // @vitest-environment jsdom
1
+ import { describe, expect, mock, test } from "bun:test";
2
2
  import { createStore, shallowEqual } from "@cosmicdrift/kumiko-headless";
3
3
  import { useStore, useStoreSelector } from "@cosmicdrift/kumiko-renderer";
4
- import { describe, expect, test, vi } from "vitest";
5
4
  import { act, renderHook } from "./test-utils";
6
5
 
7
6
  describe("useStore", () => {
@@ -116,7 +115,7 @@ describe("useStoreSelector", () => {
116
115
 
117
116
  test("custom equals receives previous and current selected values", () => {
118
117
  const store = createStore({ count: 0 });
119
- const equals = vi.fn((_a: number, _b: number) => false); // never equal
118
+ const equals = mock((_a: number, _b: number) => false); // never equal
120
119
  let renderCount = 0;
121
120
  renderHook(() => {
122
121
  renderCount += 1;
@@ -1,4 +1,3 @@
1
- // @vitest-environment jsdom
2
1
  //
3
2
  // V.1.1-D Integration-Test — End-to-End-Beweis für die Schleife
4
3
  // `clientFeatures.treeProvider → useTreeProviders → VisualTree →
@@ -22,6 +21,7 @@
22
21
  // setupTestStack kommt mit V.1.2 wenn text-content's Slug-Liste durch
23
22
  // die Server-Pipeline geht.
24
23
 
24
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
25
25
  import type { TreeChildrenSubscribe, TreeNode } from "@cosmicdrift/kumiko-framework/engine";
26
26
  import type { FeatureSchema, WorkspaceSchema } from "@cosmicdrift/kumiko-renderer";
27
27
  import {
@@ -32,7 +32,6 @@ import {
32
32
  } from "@cosmicdrift/kumiko-renderer";
33
33
  import { act, fireEvent, render, screen } from "@testing-library/react";
34
34
  import type { ReactNode } from "react";
35
- import { afterEach, beforeEach, describe, expect, test } from "vitest";
36
35
  import { useBrowserNavApi } from "../app/nav";
37
36
  import { TreeProvidersProvider } from "../app/tree-providers-context";
38
37
  import { setDispatchListener } from "../layout/target-resolver-stub";
@@ -1,5 +1,4 @@
1
- // @vitest-environment jsdom
2
-
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
3
2
  import type { WorkspaceSchema } from "@cosmicdrift/kumiko-renderer";
4
3
  import {
5
4
  createStaticLocaleResolver,
@@ -9,7 +8,6 @@ import {
9
8
  } from "@cosmicdrift/kumiko-renderer";
10
9
  import { render as _render, act } from "@testing-library/react";
11
10
  import type { ReactNode } from "react";
12
- import { beforeEach, describe, expect, test, vi } from "vitest";
13
11
  import { useBrowserNavApi } from "../app/nav";
14
12
  import {
15
13
  filterByAccess,
@@ -237,7 +235,7 @@ describe("WorkspaceSwitcher", () => {
237
235
  });
238
236
 
239
237
  test("clicking a tab calls onSelect with that workspace id", () => {
240
- const onSelect = vi.fn();
238
+ const onSelect = mock();
241
239
  render(
242
240
  <WorkspaceSwitcher
243
241
  workspaces={[
@@ -27,7 +27,7 @@ import {
27
27
  useNav,
28
28
  } from "@cosmicdrift/kumiko-renderer";
29
29
  import { type ComponentType, type ReactNode, useMemo } from "react";
30
- import { createRoot } from "react-dom/client";
30
+ import { createRoot, type Root } from "react-dom/client";
31
31
  import { lastSegment } from "../layout/nav-tree";
32
32
  import { defaultPrimitives } from "../primitives";
33
33
  import { ToastProvider } from "../primitives/toast";
@@ -111,7 +111,7 @@ function readInjectedSchema(): AppSchema | FeatureSchema | undefined {
111
111
  return w.__KUMIKO_SCHEMA__;
112
112
  }
113
113
 
114
- export function createKumikoApp(options: CreateKumikoAppOptions = {}): void {
114
+ export function createKumikoApp(options: CreateKumikoAppOptions = {}): { readonly root: Root } {
115
115
  const rootId = options.rootId ?? "root";
116
116
  const container = document.getElementById(rootId);
117
117
  if (!container) {
@@ -272,7 +272,9 @@ export function createKumikoApp(options: CreateKumikoAppOptions = {}): void {
272
272
  </TokensBoot>
273
273
  );
274
274
 
275
- createRoot(container).render(tree);
275
+ const root = createRoot(container);
276
+ root.render(tree);
277
+ return { root };
276
278
  }
277
279
 
278
280
  // TokensBoot nutzt den browser-backed TokensApi-Hook (class-based
@@ -1,10 +1,8 @@
1
- // @vitest-environment jsdom
2
-
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
3
2
  import type { TreeChildrenSubscribe, TreeNode } from "@cosmicdrift/kumiko-framework/engine";
4
3
  import { NavProvider } from "@cosmicdrift/kumiko-renderer";
5
4
  import { act, fireEvent, render, screen } from "@testing-library/react";
6
5
  import type { ReactNode } from "react";
7
- import { afterEach, beforeEach, describe, expect, test } from "vitest";
8
6
  import { useBrowserNavApi } from "../../app/nav";
9
7
  import { TreeProvidersProvider } from "../../app/tree-providers-context";
10
8
  import { setDispatchListener } from "../target-resolver-stub";
@@ -43,9 +41,9 @@ function makeMutableProvider(initial: readonly TreeNode[]): {
43
41
  };
44
42
  }
45
43
 
46
- function renderTree(
44
+ async function renderTree(
47
45
  providers: ReadonlyMap<string, TreeChildrenSubscribe>,
48
- ): ReturnType<typeof render> {
46
+ ): Promise<ReturnType<typeof render>> {
49
47
  function Wrapper({ children }: { readonly children: ReactNode }): ReactNode {
50
48
  // V.1.4b: TreeNodeRenderer + ActionButton nutzen useDispatchTarget,
51
49
  // das useNav greift — Tests brauchen NavProvider. Browser-nav reset
@@ -57,7 +55,12 @@ function renderTree(
57
55
  </NavProvider>
58
56
  );
59
57
  }
60
- return render(<VisualTree workspaceId="test-ws" />, { wrapper: Wrapper });
58
+ const result = render(<VisualTree workspaceId="test-ws" />, { wrapper: Wrapper });
59
+ // Asynchrone React-Effects (useEffect mit setTimeout/Promise) in
60
+ // act() abfangen. Ohne das feuern State-Updates außerhalb von act
61
+ // und produzieren "not wrapped in act"-Warnungen.
62
+ await act(async () => {});
63
+ return result;
61
64
  }
62
65
 
63
66
  // vitest+Bun-Runtime liefert nur ein partielles localStorage (kein
@@ -90,15 +93,15 @@ beforeEach(() => {
90
93
  });
91
94
 
92
95
  describe("VisualTree — Empty-State", () => {
93
- test("ohne registrierte Provider rendert sichtbaren Empty-Hint", () => {
94
- renderTree(new Map());
96
+ test("ohne registrierte Provider rendert sichtbaren Empty-Hint", async () => {
97
+ await renderTree(new Map());
95
98
  expect(screen.getByLabelText("Visual Tree (no providers)")).toBeTruthy();
96
99
  expect(screen.getByText(/Keine Tree-Provider aktiv/)).toBeTruthy();
97
100
  });
98
101
 
99
- test("Provider-Map vorhanden + emittet leere TreeNode[]: kein Empty-State, kein NavTree-Fallback", () => {
102
+ test("Provider-Map vorhanden + emittet leere TreeNode[]: kein Empty-State, kein NavTree-Fallback", async () => {
100
103
  const providers = new Map([["text-content", makeStaticProvider([])]]);
101
- renderTree(providers);
104
+ await renderTree(providers);
102
105
  // Nicht im Empty-State (es gibt einen registrierten Provider, der
103
106
  // hat nur kein Knoten emittet). Stattdessen rendert die ProviderBranch
104
107
  // mit dem featureName als Label im aria-tree.
@@ -108,39 +111,39 @@ describe("VisualTree — Empty-State", () => {
108
111
  });
109
112
 
110
113
  describe("VisualTree — Provider-Iteration", () => {
111
- test("Single-Provider mit static-children rendert Top-Level-Knoten", () => {
114
+ test("Single-Provider mit static-children rendert Top-Level-Knoten", async () => {
112
115
  const providers = new Map([
113
116
  ["text-content", makeStaticProvider([{ label: "Marketing" }, { label: "Legal" }])],
114
117
  ]);
115
- renderTree(providers);
118
+ await renderTree(providers);
116
119
  expect(screen.getByText("Marketing")).toBeTruthy();
117
120
  expect(screen.getByText("Legal")).toBeTruthy();
118
121
  });
119
122
 
120
- test("Multi-Provider alphabetisch sortiert nach featureName", () => {
123
+ test("Multi-Provider alphabetisch sortiert nach featureName", async () => {
121
124
  // legal-pages kommt alphabetisch vor text-content
122
125
  const providers = new Map<string, TreeChildrenSubscribe>([
123
126
  ["text-content", makeStaticProvider([{ label: "Marketing" }])],
124
127
  ["legal-pages", makeStaticProvider([{ label: "Imprint" }])],
125
128
  ]);
126
- renderTree(providers);
129
+ await renderTree(providers);
127
130
  const branches = document.querySelectorAll("[data-kumiko-tree-branch]");
128
131
  expect(branches[0]?.getAttribute("data-kumiko-tree-branch")).toBe("legal-pages");
129
132
  expect(branches[1]?.getAttribute("data-kumiko-tree-branch")).toBe("text-content");
130
133
  });
131
134
 
132
- test("Provider der nicht emittet bleibt im loading-State sichtbar", () => {
135
+ test("Provider der nicht emittet bleibt im loading-State sichtbar", async () => {
133
136
  // Provider ruft emit nie auf (z.B. async-fetch noch im Flug)
134
137
  const noopProvider: TreeChildrenSubscribe = () => () => () => {};
135
138
  const providers = new Map([["slow-feature", noopProvider]]);
136
- renderTree(providers);
139
+ await renderTree(providers);
137
140
  expect(screen.getByText("slow-feature: lädt …")).toBeTruthy();
138
141
  });
139
142
 
140
- test("Subscribe-Update: zweiter Emit re-rendert die Liste", () => {
143
+ test("Subscribe-Update: zweiter Emit re-rendert die Liste", async () => {
141
144
  const { provider, emit } = makeMutableProvider([{ label: "Hero" }]);
142
145
  const providers = new Map([["text-content", provider]]);
143
- renderTree(providers);
146
+ await renderTree(providers);
144
147
 
145
148
  expect(screen.getByText("Hero")).toBeTruthy();
146
149
 
@@ -153,7 +156,7 @@ describe("VisualTree — Provider-Iteration", () => {
153
156
  expect(screen.getByText("Hero")).toBeTruthy();
154
157
  });
155
158
 
156
- test("Provider-Unsubscribe wird beim Unmount gecallt", () => {
159
+ test("Provider-Unsubscribe wird beim Unmount gecallt", async () => {
157
160
  let unsubscribed = false;
158
161
  const provider: TreeChildrenSubscribe = () => (emit) => {
159
162
  emit([{ label: "Foo" }]);
@@ -162,7 +165,7 @@ describe("VisualTree — Provider-Iteration", () => {
162
165
  };
163
166
  };
164
167
  const providers = new Map([["test", provider]]);
165
- const result = renderTree(providers);
168
+ const result = await renderTree(providers);
166
169
 
167
170
  expect(unsubscribed).toBe(false);
168
171
  result.unmount();
@@ -177,7 +180,7 @@ describe("VisualTree — Click-Dispatch", () => {
177
180
  cleanup = undefined;
178
181
  });
179
182
 
180
- test("Click auf Knoten mit target ruft dispatchTarget mit dem TargetRef", () => {
183
+ test("Click auf Knoten mit target ruft dispatchTarget mit dem TargetRef", async () => {
181
184
  const dispatched: unknown[] = [];
182
185
  cleanup = setDispatchListener((target) => {
183
186
  dispatched.push(target);
@@ -194,7 +197,7 @@ describe("VisualTree — Click-Dispatch", () => {
194
197
  ]),
195
198
  ],
196
199
  ]);
197
- renderTree(providers);
200
+ await renderTree(providers);
198
201
 
199
202
  fireEvent.click(screen.getByText("Hero"));
200
203
 
@@ -203,7 +206,7 @@ describe("VisualTree — Click-Dispatch", () => {
203
206
  ]);
204
207
  });
205
208
 
206
- test('Skeleton-Affordance: state="empty" + createAction rendert + Button und dispatcht createAction.target', () => {
209
+ test('Skeleton-Affordance: state="empty" + createAction rendert + Button und dispatcht createAction.target', async () => {
207
210
  // D3-Validation aus visual-tree.md V.1.1-Decisions: Provider-explizit
208
211
  // createAction-Field auf TreeNode mit state="empty" → Tree-Component
209
212
  // zeigt automatisch ein "+"-Icon und dispatcht createAction.target
@@ -229,7 +232,7 @@ describe("VisualTree — Click-Dispatch", () => {
229
232
  ]),
230
233
  ],
231
234
  ]);
232
- renderTree(providers);
235
+ await renderTree(providers);
233
236
 
234
237
  // + Button greifbar via aria-label aus createAction.label
235
238
  const addButton = screen.getByLabelText("Add section");
@@ -238,7 +241,7 @@ describe("VisualTree — Click-Dispatch", () => {
238
241
  expect(dispatched).toEqual([{ featureId: "sections", action: "create" }]);
239
242
  });
240
243
 
241
- test("Click auf Container-Knoten (mit children) toggled expand statt Dispatch", () => {
244
+ test("Click auf Container-Knoten (mit children) toggled expand statt Dispatch", async () => {
242
245
  const dispatched: unknown[] = [];
243
246
  cleanup = setDispatchListener((target) => {
244
247
  dispatched.push(target);
@@ -255,7 +258,7 @@ describe("VisualTree — Click-Dispatch", () => {
255
258
  ]),
256
259
  ],
257
260
  ]);
258
- renderTree(providers);
261
+ await renderTree(providers);
259
262
 
260
263
  // Initial collapsed: Hero nicht sichtbar
261
264
  expect(screen.queryByText("Hero")).toBeNull();
@@ -267,11 +270,11 @@ describe("VisualTree — Click-Dispatch", () => {
267
270
  });
268
271
 
269
272
  describe("VisualTree — localStorage-Persistenz", () => {
270
- test("Toggle persistiert expanded-Set ins localStorage pro Workspace", () => {
273
+ test("Toggle persistiert expanded-Set ins localStorage pro Workspace", async () => {
271
274
  const providers = new Map([
272
275
  ["text-content", makeStaticProvider([{ label: "Marketing", children: [{ label: "Hero" }] }])],
273
276
  ]);
274
- renderTree(providers);
277
+ await renderTree(providers);
275
278
 
276
279
  fireEvent.click(screen.getByText("Marketing"));
277
280
 
@@ -282,7 +285,7 @@ describe("VisualTree — localStorage-Persistenz", () => {
282
285
  expect(parsed[0]).toContain("Marketing");
283
286
  });
284
287
 
285
- test("Re-mount restored expanded-Set aus localStorage", () => {
288
+ test("Re-mount restored expanded-Set aus localStorage", async () => {
286
289
  // Setup: persistierter expanded-Set für test-ws-Workspace
287
290
  window.localStorage.setItem(
288
291
  "kumiko:visual-tree:expanded:test-ws",
@@ -292,7 +295,7 @@ describe("VisualTree — localStorage-Persistenz", () => {
292
295
  const providers = new Map([
293
296
  ["text-content", makeStaticProvider([{ label: "Marketing", children: [{ label: "Hero" }] }])],
294
297
  ]);
295
- renderTree(providers);
298
+ await renderTree(providers);
296
299
 
297
300
  // Marketing ist expandiert → Hero sichtbar ohne Click
298
301
  expect(screen.getByText("Hero")).toBeTruthy();