@cosmicdrift/kumiko-renderer-web 0.14.0 → 0.16.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/package.json +2 -2
- package/src/__tests__/avatar.test.tsx +1 -2
- package/src/__tests__/combobox.test.tsx +1 -2
- package/src/__tests__/config-edit.test.tsx +5 -8
- package/src/__tests__/create-app.test.tsx +30 -10
- package/src/__tests__/date-input.test.tsx +2 -3
- package/src/__tests__/default-app-shell.test.tsx +1 -2
- package/src/__tests__/dispatcher-context.test.tsx +1 -2
- package/src/__tests__/dispatcher-status-wiring.test.tsx +5 -6
- package/src/__tests__/kumiko-screen.test.tsx +2 -3
- package/src/__tests__/language-switcher.test.tsx +2 -3
- package/src/__tests__/money-input.test.tsx +8 -9
- package/src/__tests__/nav-base-path.test.tsx +1 -2
- package/src/__tests__/nav-search-params.test.tsx +1 -2
- package/src/__tests__/nav-tree.test.tsx +1 -2
- package/src/__tests__/nav.test.tsx +16 -9
- package/src/__tests__/primitives.test.tsx +53 -54
- package/src/__tests__/render-edit.test.tsx +3 -4
- package/src/__tests__/render-list-column-renderer.test.tsx +3 -4
- package/src/__tests__/render-list-debounce.test.tsx +9 -10
- package/src/__tests__/render-list.test.tsx +3 -4
- package/src/__tests__/sidebar.test.tsx +1 -2
- package/src/__tests__/theme-toggle.test.tsx +4 -5
- package/src/__tests__/toast.test.tsx +1 -2
- package/src/__tests__/tokens.test.ts +8 -0
- package/src/__tests__/use-form.test.tsx +6 -7
- package/src/__tests__/use-query-live.test.tsx +1 -2
- package/src/__tests__/use-query.test.tsx +7 -8
- package/src/__tests__/use-store.test.tsx +2 -3
- package/src/__tests__/visual-tree-integration.test.tsx +1 -2
- package/src/__tests__/workspace-shell.test.tsx +2 -4
- package/src/app/create-app.tsx +5 -3
- package/src/layout/__tests__/target-url.test.ts +36 -0
- package/src/layout/__tests__/visual-tree.test.tsx +33 -30
- package/src/lib/__tests__/cn.test.ts +12 -0
- package/CHANGELOG.md +0 -485
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.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>",
|
|
@@ -75,4 +75,4 @@
|
|
|
75
75
|
"README.md",
|
|
76
76
|
"LICENSE"
|
|
77
77
|
]
|
|
78
|
-
}
|
|
78
|
+
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
|
|
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 =
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
75
|
-
//
|
|
76
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
78
|
+
const query = mock(async (type: string) => {
|
|
80
79
|
seenTypes.push(type);
|
|
81
80
|
return {
|
|
82
81
|
isSuccess: true,
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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) => {
|