@cosmicdrift/kumiko-renderer-web 0.215.6 → 0.215.7
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 +5 -5
- package/src/__tests__/nav-tree.test.tsx +13 -0
- package/src/lib/__tests__/accept-attr.test.ts +28 -0
- package/src/lib/__tests__/clamp.test.ts +21 -0
- package/src/lib/__tests__/download.test.ts +82 -0
- package/src/primitives/__tests__/date-parse.test.ts +4 -0
- package/src/widgets/__tests__/feed-list.test.tsx +28 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer-web",
|
|
3
|
-
"version": "0.215.
|
|
3
|
+
"version": "0.215.7",
|
|
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.215.
|
|
20
|
-
"@cosmicdrift/kumiko-headless": "0.215.
|
|
21
|
-
"@cosmicdrift/kumiko-renderer": "0.215.
|
|
19
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.215.7",
|
|
20
|
+
"@cosmicdrift/kumiko-headless": "0.215.7",
|
|
21
|
+
"@cosmicdrift/kumiko-renderer": "0.215.7",
|
|
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",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"@types/react-dom": "^19.2.3",
|
|
65
65
|
"jsdom": "^29.1.1",
|
|
66
66
|
"tailwindcss": "^4.3.0",
|
|
67
|
-
"@cosmicdrift/kumiko-locale-de": "0.215.
|
|
67
|
+
"@cosmicdrift/kumiko-locale-de": "0.215.7"
|
|
68
68
|
},
|
|
69
69
|
"repository": {
|
|
70
70
|
"type": "git",
|
|
@@ -168,6 +168,7 @@ const NAV_ICON_LUCIDE_CLASS: Readonly<Record<string, string>> = {
|
|
|
168
168
|
share: "lucide-share-2",
|
|
169
169
|
send: "lucide-send",
|
|
170
170
|
"shield-check": "lucide-shield-check",
|
|
171
|
+
gauge: "lucide-gauge",
|
|
171
172
|
};
|
|
172
173
|
|
|
173
174
|
function expectNavIcons(container: HTMLElement, iconKeys: readonly string[]): void {
|
|
@@ -366,6 +367,18 @@ describe("NavTree", () => {
|
|
|
366
367
|
expect(warnSpy).not.toHaveBeenCalled();
|
|
367
368
|
});
|
|
368
369
|
|
|
370
|
+
test("gauge (cap-list nav) resolves to an icon without missing-icon path", () => {
|
|
371
|
+
const schema = {
|
|
372
|
+
featureName: "cap-counter",
|
|
373
|
+
entities: {},
|
|
374
|
+
screens: [{ id: "cap-list", type: "entityList", entity: "x", columns: [] }],
|
|
375
|
+
navs: [{ id: "cap-list", label: "Caps", screen: "cap-list", order: 60, icon: "gauge" }],
|
|
376
|
+
} as FeatureSchema;
|
|
377
|
+
const { container } = render(<NavTree schema={schema} />);
|
|
378
|
+
expectNavIcons(container, ["gauge"]);
|
|
379
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
380
|
+
});
|
|
381
|
+
|
|
369
382
|
test("unbekannter icon-Key fällt sauber auf den Dot zurück (kein svg), aber warnt sichtbar", () => {
|
|
370
383
|
const schema = {
|
|
371
384
|
featureName: "showcase",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { toAcceptAttr } from "../accept-attr";
|
|
3
|
+
|
|
4
|
+
describe("toAcceptAttr", () => {
|
|
5
|
+
test("bare extensions get a leading dot", () => {
|
|
6
|
+
expect(toAcceptAttr(["pdf"])).toBe(".pdf");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("extensions with a dot pass through", () => {
|
|
10
|
+
expect(toAcceptAttr([".pdf"])).toBe(".pdf");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("mime types pass through", () => {
|
|
14
|
+
expect(toAcceptAttr(["image/png"])).toBe("image/png");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("mixed entries are comma-joined", () => {
|
|
18
|
+
expect(toAcceptAttr(["pdf", ".docx", "image/*"])).toBe(".pdf,.docx,image/*");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("empty list yields undefined", () => {
|
|
22
|
+
expect(toAcceptAttr([])).toBeUndefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("missing list yields undefined", () => {
|
|
26
|
+
expect(toAcceptAttr(undefined)).toBeUndefined();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { clamp } from "../clamp";
|
|
3
|
+
|
|
4
|
+
describe("clamp", () => {
|
|
5
|
+
test("value inside range passes through", () => {
|
|
6
|
+
expect(clamp(5, 0, 10)).toBe(5);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("value below min → min", () => {
|
|
10
|
+
expect(clamp(-3, 0, 10)).toBe(0);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("value above max → max", () => {
|
|
14
|
+
expect(clamp(42, 0, 10)).toBe(10);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("boundary values are kept", () => {
|
|
18
|
+
expect(clamp(0, 0, 10)).toBe(0);
|
|
19
|
+
expect(clamp(10, 0, 10)).toBe(10);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import type { Dispatcher, DispatcherError, QueryResult } from "@cosmicdrift/kumiko-headless";
|
|
3
|
+
import { postWithDownload } from "../download";
|
|
4
|
+
|
|
5
|
+
function stubDispatcher(result: QueryResult<{ url?: string }>): Dispatcher {
|
|
6
|
+
return { query: async () => result } as unknown as Dispatcher;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Works in both bunfig (node) and bunfig.dom.toml (happy-dom) runs:
|
|
10
|
+
// - node env: install a minimal fake window for the duration of the test,
|
|
11
|
+
// then remove it again so later suites are unaffected.
|
|
12
|
+
// - dom env: patch only location.assign on the real window — swapping the
|
|
13
|
+
// whole window object corrupts every later DOM suite in the same process.
|
|
14
|
+
function trackAssignedUrl(): { url: () => string | undefined; restore: () => void } {
|
|
15
|
+
const g = globalThis as Record<string, unknown>;
|
|
16
|
+
const hadWindow = typeof g["window"] !== "undefined";
|
|
17
|
+
if (!hadWindow) {
|
|
18
|
+
g["window"] = { location: { assign(_url: string) {} } };
|
|
19
|
+
}
|
|
20
|
+
const win = g["window"] as { location: { assign: (u: string) => void } };
|
|
21
|
+
const originalAssign = win.location.assign;
|
|
22
|
+
let assignedUrl: string | undefined;
|
|
23
|
+
win.location.assign = (u: string) => {
|
|
24
|
+
assignedUrl = u;
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
url: () => assignedUrl,
|
|
28
|
+
restore: () => {
|
|
29
|
+
if (hadWindow) {
|
|
30
|
+
win.location.assign = originalAssign;
|
|
31
|
+
} else {
|
|
32
|
+
delete g["window"];
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("postWithDownload", () => {
|
|
39
|
+
let tracked: ReturnType<typeof trackAssignedUrl> | undefined;
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
tracked?.restore();
|
|
42
|
+
tracked = undefined;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("assigns the url and returns null on success", async () => {
|
|
46
|
+
tracked = trackAssignedUrl();
|
|
47
|
+
const dispatcher = stubDispatcher({
|
|
48
|
+
isSuccess: true,
|
|
49
|
+
data: { url: "https://cdn.example.com/f.pdf" },
|
|
50
|
+
});
|
|
51
|
+
const err = await postWithDownload(dispatcher, "files.signedUrl", {});
|
|
52
|
+
expect(err).toBeNull();
|
|
53
|
+
expect(tracked.url()).toBe("https://cdn.example.com/f.pdf");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("passes the dispatcher error through when not successful", async () => {
|
|
57
|
+
const dispatcherError: DispatcherError = {
|
|
58
|
+
code: "boom",
|
|
59
|
+
httpStatus: 500,
|
|
60
|
+
i18nKey: "errors.internal",
|
|
61
|
+
message: "boom",
|
|
62
|
+
};
|
|
63
|
+
const dispatcher = stubDispatcher({ isSuccess: false, error: dispatcherError });
|
|
64
|
+
const err = await postWithDownload(dispatcher, "files.signedUrl", {});
|
|
65
|
+
expect(err).toEqual(dispatcherError);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("returns download_url_missing error when url is absent", async () => {
|
|
69
|
+
tracked = trackAssignedUrl();
|
|
70
|
+
const err = await postWithDownload(
|
|
71
|
+
stubDispatcher({ isSuccess: true, data: {} }),
|
|
72
|
+
"files.signedUrl",
|
|
73
|
+
{},
|
|
74
|
+
);
|
|
75
|
+
expect(err).toMatchObject({
|
|
76
|
+
code: "download_url_missing",
|
|
77
|
+
httpStatus: 502,
|
|
78
|
+
i18nKey: "errors.download.urlMissing",
|
|
79
|
+
});
|
|
80
|
+
expect(tracked.url()).toBeUndefined();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -33,6 +33,10 @@ const enPlaceholderLetters = placeholderLetters("en");
|
|
|
33
33
|
describe("parseIso", () => {
|
|
34
34
|
test("valid yyyy-mm-dd → PlainDate (no TZ conversion)", () => {
|
|
35
35
|
const d = parseIso("2026-04-25");
|
|
36
|
+
// Cross-realm safe: bun and happy-dom can expose different Temporal
|
|
37
|
+
// copies, so toBeInstanceOf fails depending on file order. The ISO
|
|
38
|
+
// string proves both the type and the no-TZ-conversion property.
|
|
39
|
+
expect(String(d)).toBe("2026-04-25");
|
|
36
40
|
expect(d?.year).toBe(2026);
|
|
37
41
|
expect(d?.month).toBe(4);
|
|
38
42
|
expect(d?.day).toBe(25);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { FeedList, type FeedRow } from "../feed-list";
|
|
4
|
+
|
|
5
|
+
const rows: FeedRow[] = [
|
|
6
|
+
{ id: "r1", primary: "First row", trailing: "3 min" },
|
|
7
|
+
{ id: "r2", primary: "Second row" },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
describe("FeedList", () => {
|
|
11
|
+
test("renders one list item per row with primary text", () => {
|
|
12
|
+
render(<FeedList rows={rows} testId="feed" />);
|
|
13
|
+
expect(screen.getAllByRole("listitem")).toHaveLength(2);
|
|
14
|
+
expect(screen.getByText("First row")).toBeTruthy();
|
|
15
|
+
expect(screen.getByText("Second row")).toBeTruthy();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("renders trailing content only when defined", () => {
|
|
19
|
+
render(<FeedList rows={rows} testId="feed" />);
|
|
20
|
+
expect(screen.getByText("3 min")).toBeTruthy();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("renders the empty state when there are no rows", () => {
|
|
24
|
+
render(<FeedList rows={[]} testId="feed" emptyContent={<p>Nothing here</p>} />);
|
|
25
|
+
expect(screen.queryAllByRole("listitem")).toHaveLength(0);
|
|
26
|
+
expect(screen.getByText("Nothing here")).toBeTruthy();
|
|
27
|
+
});
|
|
28
|
+
});
|