@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
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
2
2
|
import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
|
|
3
3
|
import { DispatcherProvider, useForm } from "@cosmicdrift/kumiko-renderer";
|
|
4
4
|
import type { ReactNode } from "react";
|
|
5
|
-
import { describe, expect, test, vi } from "vitest";
|
|
6
5
|
import { z } from "zod";
|
|
7
6
|
import { act, createMockDispatcher, renderHook } from "./test-utils";
|
|
8
7
|
|
|
@@ -40,7 +39,7 @@ describe("useForm", () => {
|
|
|
40
39
|
});
|
|
41
40
|
|
|
42
41
|
test("submit dispatches to the context dispatcher when no explicit one is passed", async () => {
|
|
43
|
-
const write =
|
|
42
|
+
const write = mock(async () => ({ isSuccess: true, data: { id: "123" } }) as never);
|
|
44
43
|
const dispatcher = makeDispatcher(write);
|
|
45
44
|
const { result } = renderHook(
|
|
46
45
|
() =>
|
|
@@ -57,13 +56,13 @@ describe("useForm", () => {
|
|
|
57
56
|
submitResult = await result.current.controller.submit();
|
|
58
57
|
});
|
|
59
58
|
|
|
60
|
-
expect(write).
|
|
59
|
+
expect(write).toHaveBeenCalledTimes(1);
|
|
61
60
|
expect(write).toHaveBeenCalledWith("x:create", expect.anything());
|
|
62
61
|
expect((submitResult as { isSuccess: boolean }).isSuccess).toBe(true);
|
|
63
62
|
});
|
|
64
63
|
|
|
65
64
|
test("zod schema failure blocks submit; no network call fires", async () => {
|
|
66
|
-
const write =
|
|
65
|
+
const write = mock();
|
|
67
66
|
const dispatcher = makeDispatcher(write as unknown as Dispatcher["write"]);
|
|
68
67
|
const schema = z.object({ title: z.string().min(1), count: z.number().optional() });
|
|
69
68
|
const { result } = renderHook(
|
|
@@ -87,8 +86,8 @@ describe("useForm", () => {
|
|
|
87
86
|
});
|
|
88
87
|
|
|
89
88
|
test("explicit dispatcher on submit wins over context dispatcher", async () => {
|
|
90
|
-
const contextWrite =
|
|
91
|
-
const overrideWrite =
|
|
89
|
+
const contextWrite = mock(async () => ({ isSuccess: true, data: {} }) as never);
|
|
90
|
+
const overrideWrite = mock(async () => ({ isSuccess: true, data: {} }) as never);
|
|
92
91
|
const contextDispatcher = makeDispatcher(contextWrite);
|
|
93
92
|
const overrideDispatcher = makeDispatcher(overrideWrite);
|
|
94
93
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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 =
|
|
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).
|
|
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 =
|
|
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 =
|
|
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).
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
238
|
+
const onSelect = mock();
|
|
241
239
|
render(
|
|
242
240
|
<WorkspaceSwitcher
|
|
243
241
|
workspaces={[
|
package/src/app/create-app.tsx
CHANGED
|
@@ -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 = {}):
|
|
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)
|
|
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
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
clearTargetSearchParams,
|
|
4
|
+
parseTargetFromSearchParams,
|
|
5
|
+
serializeTarget,
|
|
6
|
+
} from "../target-url";
|
|
7
|
+
|
|
8
|
+
describe("serializeTarget / parseTargetFromSearchParams", () => {
|
|
9
|
+
test("round-trips target + string args", () => {
|
|
10
|
+
const updates = serializeTarget(
|
|
11
|
+
{ featureId: "text-content", action: "edit", args: { slug: "imprint", lang: "de" } },
|
|
12
|
+
{},
|
|
13
|
+
);
|
|
14
|
+
const params = Object.fromEntries(
|
|
15
|
+
Object.entries(updates).filter(([, v]) => v !== null) as [string, string][],
|
|
16
|
+
);
|
|
17
|
+
expect(parseTargetFromSearchParams(params)).toEqual({
|
|
18
|
+
featureId: "text-content",
|
|
19
|
+
action: "edit",
|
|
20
|
+
args: { slug: "imprint", lang: "de" },
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("returns undefined when t param missing", () => {
|
|
25
|
+
expect(parseTargetFromSearchParams({})).toBeUndefined();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("clearTargetSearchParams", () => {
|
|
30
|
+
test("clears t and all a_* keys", () => {
|
|
31
|
+
expect(clearTargetSearchParams({ t: "x:y", a_slug: "imprint", keep: "1" })).toEqual({
|
|
32
|
+
t: null,
|
|
33
|
+
a_slug: null,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { cn } from "../cn";
|
|
3
|
+
|
|
4
|
+
describe("cn", () => {
|
|
5
|
+
test("merges conditional classes and resolves tailwind conflicts", () => {
|
|
6
|
+
expect(cn("px-2", false && "hidden", "px-4")).toBe("px-4");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("joins unrelated classes", () => {
|
|
10
|
+
expect(cn("text-sm", "font-bold")).toBe("text-sm font-bold");
|
|
11
|
+
});
|
|
12
|
+
});
|