@cosmicdrift/kumiko-renderer-web 0.13.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 +5 -5
  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 -477
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer-web",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Web-platform bindings for @cosmicdrift/kumiko-renderer. HTML default-primitives, browser history-based navigation, EventSource-backed live events, and a one-call createKumikoApp that mounts the whole stack via react-dom.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -16,9 +16,9 @@
16
16
  "./styles.css": "./src/styles.css"
17
17
  },
18
18
  "dependencies": {
19
- "@cosmicdrift/kumiko-dispatcher-live": "0.13.0",
20
- "@cosmicdrift/kumiko-headless": "0.13.0",
21
- "@cosmicdrift/kumiko-renderer": "0.13.0",
19
+ "@cosmicdrift/kumiko-dispatcher-live": "0.14.0",
20
+ "@cosmicdrift/kumiko-headless": "0.14.0",
21
+ "@cosmicdrift/kumiko-renderer": "0.14.0",
22
22
  "@radix-ui/react-dialog": "^1.1.15",
23
23
  "@radix-ui/react-dropdown-menu": "^2.1.16",
24
24
  "@radix-ui/react-label": "^2.1.8",
@@ -75,4 +75,4 @@
75
75
  "README.md",
76
76
  "LICENSE"
77
77
  ]
78
- }
78
+ }
@@ -1,5 +1,4 @@
1
- // @vitest-environment jsdom
2
- import { describe, expect, test } from "vitest";
1
+ import { describe, expect, test } from "bun:test";
3
2
  import { Avatar } from "../layout/avatar";
4
3
  import { render, screen } from "./test-utils";
5
4
 
@@ -1,8 +1,7 @@
1
- // @vitest-environment jsdom
1
+ import { describe, expect, test } from "bun:test";
2
2
  import { fireEvent } from "@testing-library/react";
3
3
  import userEvent from "@testing-library/user-event";
4
4
  import { act } from "react";
5
- import { describe, expect, test } from "vitest";
6
5
  import { ComboboxInput } from "../primitives/combobox";
7
6
  import { render, screen } from "./test-utils";
8
7
 
@@ -1,4 +1,3 @@
1
- // @vitest-environment jsdom
2
1
  //
3
2
  // Unit-Tests für den configEdit-Screen-Type. Decken die Pfade ab die
4
3
  // Integration + E2E nur indirekt sehen:
@@ -8,12 +7,12 @@
8
7
  // - Save-Button Greying via controller.rebase nach Success
9
8
  // - Loading-State während config:query:values noch läuft
10
9
 
10
+ import { describe, expect, mock, test } from "bun:test";
11
11
  import type { ConfigEditScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
12
12
  import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
13
13
  import type { FeatureSchema } from "@cosmicdrift/kumiko-renderer";
14
14
  import { DispatcherProvider, KumikoScreen } from "@cosmicdrift/kumiko-renderer";
15
15
  import userEvent from "@testing-library/user-event";
16
- import { describe, expect, test, vi } from "vitest";
17
16
  import { createMockDispatcher, render, screen, waitFor } from "./test-utils";
18
17
 
19
18
  const settingsScreen: ConfigEditScreenDefinition = {
@@ -91,12 +90,10 @@ describe("KumikoScreen / configEdit", () => {
91
90
  });
92
91
 
93
92
  test("submit dispatches one /api/batch with one command per changed field", async () => {
94
- const batchSpy = vi.fn(
95
- async (_commands: ReadonlyArray<{ type: string; payload: unknown }>) => ({
96
- isSuccess: true as const,
97
- results: [],
98
- }),
99
- );
93
+ const batchSpy = mock(async (_commands: ReadonlyArray<{ type: string; payload: unknown }>) => ({
94
+ isSuccess: true as const,
95
+ results: [],
96
+ }));
100
97
  const dispatcher: Dispatcher = createMockDispatcher({
101
98
  query: (async () => ({
102
99
  isSuccess: true,
@@ -1,4 +1,4 @@
1
- // @vitest-environment jsdom
1
+ import { afterAll, afterEach, describe, expect, spyOn, test } from "bun:test";
2
2
  import type {
3
3
  EntityDefinition,
4
4
  EntityEditScreenDefinition,
@@ -8,7 +8,6 @@ import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
8
8
  import type { ColumnRendererProps, FeatureSchema, NavApi } from "@cosmicdrift/kumiko-renderer";
9
9
  import { act, screen, waitFor } from "@testing-library/react";
10
10
  import type { ReactNode } from "react";
11
- import { beforeEach, describe, expect, type MockInstance, test, vi } from "vitest";
12
11
  import type { ClientFeatureDefinition } from "../app/client-plugin";
13
12
  import { type CreateKumikoAppOptions, createKumikoApp } from "../app/create-app";
14
13
  import { createMockDispatcher } from "./test-utils";
@@ -62,18 +61,41 @@ const baseSchema: FeatureSchema = {
62
61
  // im Test-Modus als "outside act()" flaggt. Produktions-Code muss nicht in
63
62
  // act() wissen; der Test übernimmt das Wrapping an der einzigen
64
63
  // Test-eigenen Aufrufstelle. async weil der erste useEffect-Tick in
65
- // KumikoScreen (useQuery) ebenfalls flushed werden muss.
64
+ // KumikoScreen (useQuery) ebenfalls geflusht werden muss.
65
+ let appRoot: { unmount: () => void } | undefined;
66
+
66
67
  async function mountApp(options: CreateKumikoAppOptions): Promise<void> {
67
68
  await act(async () => {
68
- createKumikoApp(options);
69
+ const result = createKumikoApp(options);
70
+ appRoot = result.root;
69
71
  });
70
72
  }
71
73
 
72
74
  describe("createKumikoApp", () => {
73
75
  // createKumikoApp mounts via createRoot into document.body. Reset
74
- // between tests so a previous test's mount doesn't leak through
75
- // and fool the next one into finding stale markup.
76
- beforeEach(() => {
76
+ // between tests: unmount den React-Root (damit keine pending Effects
77
+ // in nachfolgende Tests leaken) + leere den body.
78
+ afterEach(() => {
79
+ if (appRoot !== undefined) {
80
+ act(() => {
81
+ // biome-ignore lint/style/noNonNullAssertion: TS can't narrow inside act() callback
82
+ appRoot!.unmount();
83
+ });
84
+ appRoot = undefined;
85
+ }
86
+ while (document.body.firstChild) {
87
+ document.body.removeChild(document.body.firstChild);
88
+ }
89
+ });
90
+
91
+ afterAll(() => {
92
+ if (appRoot !== undefined) {
93
+ act(() => {
94
+ // biome-ignore lint/style/noNonNullAssertion: TS can't narrow inside act() callback
95
+ appRoot!.unmount();
96
+ });
97
+ appRoot = undefined;
98
+ }
77
99
  while (document.body.firstChild) {
78
100
  document.body.removeChild(document.body.firstChild);
79
101
  }
@@ -131,9 +153,7 @@ describe("createKumikoApp", () => {
131
153
  // create-app warnt und behält den späteren Eintrag (Last-Wins).
132
154
  // Beweist dass das bewusste Override-Verhalten nicht silent
133
155
  // wegrutscht falls jemand auf "first-wins" refactored.
134
- const warnSpy: MockInstance<typeof console.warn> = vi
135
- .spyOn(console, "warn")
136
- .mockImplementation(() => {});
156
+ const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
137
157
 
138
158
  function FirstSwatch({ value }: ColumnRendererProps): ReactNode {
139
159
  return <span data-testid="ca-first">{String(value)}</span>;
@@ -1,13 +1,12 @@
1
- // @vitest-environment jsdom
2
1
  //
3
2
  // DateInput pinnt: Trigger zeigt formatiertes Datum (locale-aware),
4
3
  // Popover öffnet das DayPicker, Auswahl gibt ISO-yyyy-mm-dd zurück.
5
4
  // Wert-Roundtrip (ISO → Date → ISO) muss tag-stable sein, sonst
6
5
  // zeigt der Calendar je nach Timezone den Vortag.
7
6
 
7
+ import { describe, expect, mock, test } from "bun:test";
8
8
  import { fireEvent, render, screen } from "@testing-library/react";
9
9
  import userEvent from "@testing-library/user-event";
10
- import { describe, expect, test, vi } from "vitest";
11
10
  import { DateInput } from "../primitives/date-input";
12
11
 
13
12
  describe("DateInput", () => {
@@ -79,7 +78,7 @@ describe("DateInput", () => {
79
78
 
80
79
  test("Tag-Auswahl im Calendar: onChange feuert ISO yyyy-mm-dd", async () => {
81
80
  const user = userEvent.setup();
82
- const onChange = vi.fn();
81
+ const onChange = mock();
83
82
  render(<DateInput id="d" name="d" value="2026-04-23" onChange={onChange} locale="en-US" />);
84
83
  await user.click(screen.getByRole("button"));
85
84
  // react-day-picker rendert jeden Tag als gridcell. Der 25. April
@@ -1,4 +1,3 @@
1
- // @vitest-environment jsdom
2
1
  //
3
2
  // DefaultAppShell — pinnt dass user-prop an NavTree durchgereicht wird.
4
3
  //
@@ -7,8 +6,8 @@
7
6
  // fehlende user als anonymous → alle role-gated navs ausgeblendet). Test
8
7
  // pinst dass DefaultAppShell user nun akzeptiert UND durchreicht.
9
8
 
9
+ import { describe, expect, test } from "bun:test";
10
10
  import type { FeatureSchema } from "@cosmicdrift/kumiko-renderer";
11
- import { describe, expect, test } from "vitest";
12
11
  import { DefaultAppShell } from "../layout/default-app-shell";
13
12
  import { render, screen } from "./test-utils";
14
13
 
@@ -1,4 +1,4 @@
1
- // @vitest-environment jsdom
1
+ import { describe, expect, test } from "bun:test";
2
2
  import type { Dispatcher, DispatcherStatus } from "@cosmicdrift/kumiko-headless";
3
3
  import {
4
4
  DispatcherProvider,
@@ -7,7 +7,6 @@ import {
7
7
  useOptionalDispatcher,
8
8
  } from "@cosmicdrift/kumiko-renderer";
9
9
  import type { ReactNode } from "react";
10
- import { describe, expect, test } from "vitest";
11
10
  import { act, createMockDispatcher, render, renderHook } from "./test-utils";
12
11
 
13
12
  // Minimal fake dispatcher: write/query/batch throwen, damit klar wird
@@ -1,4 +1,3 @@
1
- // @vitest-environment jsdom
2
1
  //
3
2
  // Verdrahtungs-Test: beweist dass die ganze UI-Store-Kette zusammenhält:
4
3
  // createLiveDispatcher → dispatcher.statusStore → DispatcherProvider →
@@ -17,10 +16,10 @@
17
16
  // lassen alles darüber echt laufen. Im Sinne von CLAUDE.md ist das ein
18
17
  // "Full-Stack des Frontend-Stacks", nicht ein Full-Stack-mit-API.
19
18
 
19
+ import { describe, expect, mock, test } from "bun:test";
20
20
  import { createLiveDispatcher } from "@cosmicdrift/kumiko-dispatcher-live";
21
21
  import { DispatcherProvider, useDispatcherStatus } from "@cosmicdrift/kumiko-renderer";
22
22
  import type { ReactNode } from "react";
23
- import { describe, expect, test, vi } from "vitest";
24
23
  import { act, render, screen, waitFor } from "./test-utils";
25
24
 
26
25
  function StatusProbe(): ReactNode {
@@ -30,7 +29,7 @@ function StatusProbe(): ReactNode {
30
29
 
31
30
  describe("UI-Store Verdrahtung: Dispatcher → statusStore → useDispatcherStatus", () => {
32
31
  test("initial-status: Probe rendert 'online' nach Provider-Mount", () => {
33
- const fetch = vi.fn() as unknown as typeof globalThis.fetch;
32
+ const fetch = mock() as unknown as typeof globalThis.fetch;
34
33
  const dispatcher = createLiveDispatcher({ fetch, readCsrf: () => "t" });
35
34
 
36
35
  render(
@@ -43,7 +42,7 @@ describe("UI-Store Verdrahtung: Dispatcher → statusStore → useDispatcherStat
43
42
  });
44
43
 
45
44
  test("network-fail flippt Probe von 'online' nach 'offline'", async () => {
46
- const fetch = vi.fn(async () => {
45
+ const fetch = mock(async () => {
47
46
  throw new Error("ECONNREFUSED");
48
47
  }) as unknown as typeof globalThis.fetch;
49
48
  const dispatcher = createLiveDispatcher({ fetch, readCsrf: () => "t" });
@@ -67,7 +66,7 @@ describe("UI-Store Verdrahtung: Dispatcher → statusStore → useDispatcherStat
67
66
 
68
67
  test("recovery flippt Probe zurück auf 'online'", async () => {
69
68
  let failNext = true;
70
- const fetch = vi.fn(async () => {
69
+ const fetch = mock(async () => {
71
70
  if (failNext) {
72
71
  failNext = false;
73
72
  throw new Error("boom");
@@ -100,7 +99,7 @@ describe("UI-Store Verdrahtung: Dispatcher → statusStore → useDispatcherStat
100
99
  });
101
100
 
102
101
  test("statusStore ist read-only auf dem public Dispatcher-Type", () => {
103
- const fetch = vi.fn() as unknown as typeof globalThis.fetch;
102
+ const fetch = mock() as unknown as typeof globalThis.fetch;
104
103
  const dispatcher = createLiveDispatcher({ fetch, readCsrf: () => "t" });
105
104
 
106
105
  // Der Dispatcher-Contract exponiert statusStore als Store<T> (nicht
@@ -1,4 +1,4 @@
1
- // @vitest-environment jsdom
1
+ import { describe, expect, mock, test } from "bun:test";
2
2
  import type {
3
3
  ActionFormScreenDefinition,
4
4
  EntityDefinition,
@@ -9,7 +9,6 @@ import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
9
9
  import type { FeatureSchema } from "@cosmicdrift/kumiko-renderer";
10
10
  import { DispatcherProvider, KumikoScreen } from "@cosmicdrift/kumiko-renderer";
11
11
  import userEvent from "@testing-library/user-event";
12
- import { describe, expect, test, vi } from "vitest";
13
12
  import { createMockDispatcher, fireEvent, render, screen, waitFor } from "./test-utils";
14
13
 
15
14
  const taskEntity = {
@@ -76,7 +75,7 @@ describe("KumikoScreen", () => {
76
75
 
77
76
  test("entityList → fires useQuery with derived query QN and renders RenderList", async () => {
78
77
  const seenTypes: string[] = [];
79
- const query = vi.fn(async (type: string) => {
78
+ const query = mock(async (type: string) => {
80
79
  seenTypes.push(type);
81
80
  return {
82
81
  isSuccess: true,
@@ -1,10 +1,9 @@
1
- // @vitest-environment jsdom
1
+ import { describe, expect, mock, test } from "bun:test";
2
2
  import type { LocaleResolver } from "@cosmicdrift/kumiko-headless";
3
3
  import { createStaticLocaleResolver, LocaleProvider } from "@cosmicdrift/kumiko-renderer";
4
4
  import { render as _render, screen, waitFor } from "@testing-library/react";
5
5
  import userEvent from "@testing-library/user-event";
6
6
  import type { ReactNode } from "react";
7
- import { describe, expect, test, vi } from "vitest";
8
7
  import { LanguageSwitcher } from "../layout/language-switcher";
9
8
 
10
9
  // Tests greifen den LanguageSwitcher mit einem stateful Stub-Resolver
@@ -26,7 +25,7 @@ function makeStatefulResolver(initial: string): LocaleResolver {
26
25
  listeners.delete(l);
27
26
  };
28
27
  },
29
- setLocale: vi.fn((next: string) => {
28
+ setLocale: mock((next: string) => {
30
29
  current = next;
31
30
  for (const l of listeners) l();
32
31
  }),
@@ -1,4 +1,3 @@
1
- // @vitest-environment jsdom
2
1
  //
3
2
  // MoneyInput hat genug Custom-Logik (focus-aware Format, +/- Buttons,
4
3
  // Locale-Parse), dass der Switch-Case-Test in primitives.test nicht
@@ -9,8 +8,8 @@
9
8
  // - Blur mit Müll-Input verwirft den Wert (kein corrupt-set).
10
9
  // - Verschiedene Currencies → korrekte Decimal-Stellen (JPY=0).
11
10
 
11
+ import { describe, expect, mock, test } from "bun:test";
12
12
  import { fireEvent, render, screen } from "@testing-library/react";
13
- import { describe, expect, test, vi } from "vitest";
14
13
  import { MoneyInput, parseLocaleNumber } from "../primitives/money-input";
15
14
 
16
15
  describe("MoneyInput", () => {
@@ -80,7 +79,7 @@ describe("MoneyInput", () => {
80
79
  });
81
80
 
82
81
  test("blur mit gültigem Edit-String: onChange feuert Cents", () => {
83
- const onChange = vi.fn();
82
+ const onChange = mock();
84
83
  render(
85
84
  <MoneyInput
86
85
  id="eur"
@@ -99,7 +98,7 @@ describe("MoneyInput", () => {
99
98
  });
100
99
 
101
100
  test("blur mit leerem String: onChange(undefined) räumt den Wert", () => {
102
- const onChange = vi.fn();
101
+ const onChange = mock();
103
102
  render(
104
103
  <MoneyInput
105
104
  id="eur"
@@ -118,7 +117,7 @@ describe("MoneyInput", () => {
118
117
  });
119
118
 
120
119
  test("blur mit korruptem Input (Buchstaben): onChange wird NICHT gerufen", () => {
121
- const onChange = vi.fn();
120
+ const onChange = mock();
122
121
  render(
123
122
  <MoneyInput
124
123
  id="eur"
@@ -137,7 +136,7 @@ describe("MoneyInput", () => {
137
136
  });
138
137
 
139
138
  test("+ Button: addiert 1 Major-Unit (=100 cents bei EUR) zum Canonical-Wert", () => {
140
- const onChange = vi.fn();
139
+ const onChange = mock();
141
140
  render(
142
141
  <MoneyInput
143
142
  id="eur"
@@ -153,7 +152,7 @@ describe("MoneyInput", () => {
153
152
  });
154
153
 
155
154
  test("− Button: subtrahiert 1 Major-Unit", () => {
156
- const onChange = vi.fn();
155
+ const onChange = mock();
157
156
  render(
158
157
  <MoneyInput
159
158
  id="eur"
@@ -169,7 +168,7 @@ describe("MoneyInput", () => {
169
168
  });
170
169
 
171
170
  test("+ Button bei leerem Wert: startet bei 0 + 1 Major-Unit", () => {
172
- const onChange = vi.fn();
171
+ const onChange = mock();
173
172
  render(
174
173
  <MoneyInput id="eur" name="eur" value="" onChange={onChange} currency="EUR" locale="de-DE" />,
175
174
  );
@@ -178,7 +177,7 @@ describe("MoneyInput", () => {
178
177
  });
179
178
 
180
179
  test("+ Button bei JPY: addiert 1 Yen (1 cent, weil JPY 0 decimals hat)", () => {
181
- const onChange = vi.fn();
180
+ const onChange = mock();
182
181
  render(
183
182
  <MoneyInput
184
183
  id="jpy"
@@ -1,4 +1,3 @@
1
- // @vitest-environment jsdom
2
1
  //
3
2
  // useBrowserNavApi({ basePath }) — Read-Pfad strippt den Prefix vor
4
3
  // parsePath, Write-Pfad prepend'd ihn vor pushState/replaceState/hrefFor.
@@ -9,10 +8,10 @@
9
8
  // Bereich unter /admin, Embedded-App unter /embed, Workspace-Routing
10
9
  // unter Prefix) sind hier mit Beispiel-URLs durchgespielt.
11
10
 
11
+ import { beforeEach, describe, expect, test } from "bun:test";
12
12
  import { NavProvider, useNav } from "@cosmicdrift/kumiko-renderer";
13
13
  import { act, fireEvent, render, screen } from "@testing-library/react";
14
14
  import type { ReactNode } from "react";
15
- import { beforeEach, describe, expect, test } from "vitest";
16
15
  import { KumikoLink, useBrowserNavApi } from "../app/nav";
17
16
 
18
17
  function AdminBrowserNav({ children }: { readonly children: ReactNode }): ReactNode {
@@ -1,12 +1,11 @@
1
- // @vitest-environment jsdom
2
1
  //
3
2
  // useBrowserNavApi Lese-/Schreib-Pfad für searchParams. Vor dieser
4
3
  // Suite war das Mapping `window.location.search ↔ NavApi.searchParams`
5
4
  // nur über useListUrlState mit Mock-NavApi indirekt getestet — der
6
5
  // echte URLSearchParams-Parse + replaceState-Roundtrip war ungetestet.
7
6
 
7
+ import { describe, expect, test } from "bun:test";
8
8
  import { act, renderHook } from "@testing-library/react";
9
- import { describe, expect, test } from "vitest";
10
9
  import { useBrowserNavApi } from "../app/nav";
11
10
 
12
11
  function setLocation(pathname: string, search: string): void {
@@ -1,4 +1,3 @@
1
- // @vitest-environment jsdom
2
1
  //
3
2
  // NavTree: Sidebar-Navigation aus dem Schema. Pinnt zwei Verträge:
4
3
  // 1. Section-Header (parent ohne screen) plus children-Collapse
@@ -6,8 +5,8 @@
6
5
  // 2. Active-State greift auf node mit screen wenn nav.route's
7
6
  // screenId matcht (Standard-Sidebar-Verhalten).
8
7
 
8
+ import { describe, expect, test } from "bun:test";
9
9
  import type { FeatureSchema } from "@cosmicdrift/kumiko-renderer";
10
- import { describe, expect, test } from "vitest";
11
10
  import { NavTree } from "../layout/nav-tree";
12
11
  import { fireEvent, render, screen } from "./test-utils";
13
12
 
@@ -1,8 +1,7 @@
1
- // @vitest-environment jsdom
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
2
  import { formatPath, NavProvider, parsePath, useNav } from "@cosmicdrift/kumiko-renderer";
3
3
  import { act, fireEvent, render, screen } from "@testing-library/react";
4
4
  import type { ReactNode } from "react";
5
- import { beforeEach, describe, expect, test } from "vitest";
6
5
  import { KumikoLink, useBrowserNavApi } from "../app/nav";
7
6
 
8
7
  describe("parsePath", () => {
@@ -124,23 +123,25 @@ describe("useBrowserNavApi + NavProvider", () => {
124
123
  );
125
124
  }
126
125
 
127
- test("initial-route aus window.location.pathname", () => {
126
+ test("initial-route aus window.location.pathname", async () => {
128
127
  window.history.replaceState(null, "", "/task-list");
129
128
  render(
130
129
  <BrowserNav>
131
130
  <Probe />
132
131
  </BrowserNav>,
133
132
  );
133
+ await act(async () => {});
134
134
  expect(screen.getByTestId("screen-id").textContent).toBe("task-list");
135
135
  expect(screen.getByTestId("entity-id").textContent).toBe("(none)");
136
136
  });
137
137
 
138
- test("navigate() aktualisiert location + re-rendert", () => {
138
+ test("navigate() aktualisiert location + re-rendert", async () => {
139
139
  render(
140
140
  <BrowserNav>
141
141
  <Probe />
142
142
  </BrowserNav>,
143
143
  );
144
+ await act(async () => {});
144
145
  expect(screen.getByTestId("screen-id").textContent).toBe("(none)");
145
146
 
146
147
  act(() => {
@@ -152,12 +153,14 @@ describe("useBrowserNavApi + NavProvider", () => {
152
153
  expect(screen.getByTestId("entity-id").textContent).toBe("xyz");
153
154
  });
154
155
 
155
- test("replace() aktualisiert location ohne History-Eintrag", () => {
156
+ test("replace() aktualisiert location ohne History-Eintrag", async () => {
156
157
  render(
157
158
  <BrowserNav>
158
159
  <Probe />
159
160
  </BrowserNav>,
160
161
  );
162
+ await act(async () => {});
163
+ expect(screen.getByTestId("screen-id").textContent).toBe("(none)");
161
164
  const before = window.history.length;
162
165
  act(() => {
163
166
  fireEvent.click(screen.getByTestId("replace-list"));
@@ -171,12 +174,13 @@ describe("useBrowserNavApi + NavProvider", () => {
171
174
  expect(window.history.length).toBe(before);
172
175
  });
173
176
 
174
- test("popstate (Browser-Back) re-rendert die aktuelle Route", () => {
177
+ test("popstate (Browser-Back) re-rendert die aktuelle Route", async () => {
175
178
  render(
176
179
  <BrowserNav>
177
180
  <Probe />
178
181
  </BrowserNav>,
179
182
  );
183
+ await act(async () => {});
180
184
  act(() => {
181
185
  fireEvent.click(screen.getByTestId("go-list"));
182
186
  });
@@ -198,35 +202,38 @@ describe("KumikoLink", () => {
198
202
  window.history.replaceState(null, "", "/");
199
203
  });
200
204
 
201
- test("rendert <a> mit korrekter href", () => {
205
+ test("rendert <a> mit korrekter href", async () => {
202
206
  render(
203
207
  <BrowserNav>
204
208
  <KumikoLink to={{ screenId: "task-edit", entityId: "xyz" }}>Edit</KumikoLink>
205
209
  </BrowserNav>,
206
210
  );
211
+ await act(async () => {});
207
212
  const anchor = screen.getByText("Edit") as HTMLAnchorElement;
208
213
  expect(anchor.tagName).toBe("A");
209
214
  expect(anchor.getAttribute("href")).toBe("/task-edit/xyz");
210
215
  });
211
216
 
212
- test("left-click wird abgefangen → navigate() statt full reload", () => {
217
+ test("left-click wird abgefangen → navigate() statt full reload", async () => {
213
218
  render(
214
219
  <BrowserNav>
215
220
  <KumikoLink to={{ screenId: "task-list" }}>Liste</KumikoLink>
216
221
  </BrowserNav>,
217
222
  );
223
+ await act(async () => {});
218
224
  act(() => {
219
225
  fireEvent.click(screen.getByText("Liste"), { button: 0 });
220
226
  });
221
227
  expect(window.location.pathname).toBe("/task-list");
222
228
  });
223
229
 
224
- test("meta-click (Cmd/Ctrl) wird NICHT abgefangen — Browser öffnet in neuem Tab", () => {
230
+ test("meta-click (Cmd/Ctrl) wird NICHT abgefangen — Browser öffnet in neuem Tab", async () => {
225
231
  render(
226
232
  <BrowserNav>
227
233
  <KumikoLink to={{ screenId: "task-list" }}>Liste</KumikoLink>
228
234
  </BrowserNav>,
229
235
  );
236
+ await act(async () => {});
230
237
  const anchor = screen.getByText("Liste") as HTMLAnchorElement;
231
238
  let kumikoLinkPreventedDefault: boolean | undefined;
232
239
  const observer = (e: Event) => {