@cosmicdrift/kumiko-renderer 0.157.2 → 0.159.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer",
3
- "version": "0.157.2",
3
+ "version": "0.159.1",
4
4
  "description": "Platform-agnostic React renderer for Kumiko screens. Contains the shared logic — primitives-contract, hooks, KumikoScreen, navigation & SSE abstractions — that any platform-specific renderer (web, native) composes. No DOM, no EventSource, no react-dom.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -15,8 +15,8 @@
15
15
  }
16
16
  },
17
17
  "dependencies": {
18
- "@cosmicdrift/kumiko-framework": "0.157.2",
19
- "@cosmicdrift/kumiko-headless": "0.157.2",
18
+ "@cosmicdrift/kumiko-framework": "0.159.1",
19
+ "@cosmicdrift/kumiko-headless": "0.159.1",
20
20
  "react": "^19.2.6",
21
21
  "temporal-polyfill": "^0.3.2"
22
22
  },
@@ -0,0 +1,41 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { sortByAccessor } from "../sort-by-accessor";
3
+
4
+ type Row = { readonly name: string; readonly count: number };
5
+
6
+ const rows: readonly Row[] = [
7
+ { name: "b", count: 2 },
8
+ { name: "a", count: 3 },
9
+ { name: "c", count: 1 },
10
+ ];
11
+
12
+ const accessors = {
13
+ name: (r: Row) => r.name,
14
+ count: (r: Row) => r.count,
15
+ };
16
+
17
+ describe("sortByAccessor", () => {
18
+ test("sort === null returns rows unchanged (same reference)", () => {
19
+ expect(sortByAccessor(rows, null, accessors)).toBe(rows);
20
+ });
21
+
22
+ test("an unknown field returns rows unchanged (same reference)", () => {
23
+ expect(sortByAccessor(rows, { field: "nope", dir: "asc" }, accessors)).toBe(rows);
24
+ });
25
+
26
+ test("sorts ascending by the given accessor", () => {
27
+ const sorted = sortByAccessor(rows, { field: "name", dir: "asc" }, accessors);
28
+ expect(sorted.map((r) => r.name)).toEqual(["a", "b", "c"]);
29
+ });
30
+
31
+ test("sorts descending by the given accessor", () => {
32
+ const sorted = sortByAccessor(rows, { field: "count", dir: "desc" }, accessors);
33
+ expect(sorted.map((r) => r.count)).toEqual([3, 2, 1]);
34
+ });
35
+
36
+ test("does not mutate the input array", () => {
37
+ const original = [...rows];
38
+ sortByAccessor(rows, { field: "name", dir: "asc" }, accessors);
39
+ expect(rows).toEqual(original);
40
+ });
41
+ });
@@ -0,0 +1,40 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { ConfigEditScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
3
+ import { synthesizeConfigEditEntity, synthesizeConfigEditScreen } from "../config-edit-shim";
4
+
5
+ describe("synthesizeConfigEditEntity", () => {
6
+ test("wraps the inline fields as an EntityDefinition", () => {
7
+ const fields = { apiKey: { type: "text" } } as unknown as ConfigEditScreenDefinition["fields"];
8
+ expect(synthesizeConfigEditEntity(fields)).toEqual({ fields });
9
+ });
10
+ });
11
+
12
+ describe("synthesizeConfigEditScreen", () => {
13
+ test("stamps type: entityEdit and a pseudo entity name", () => {
14
+ const screen = {
15
+ id: "settings",
16
+ layout: { sections: [] },
17
+ } as unknown as ConfigEditScreenDefinition;
18
+
19
+ const result = synthesizeConfigEditScreen(screen);
20
+ expect(result.id).toBe("settings");
21
+ expect(result.type).toBe("entityEdit");
22
+ expect(result.entity).toBe("__config-edit__");
23
+ expect(result.layout).toBe(screen.layout);
24
+ expect(result).not.toHaveProperty("fieldLabels");
25
+ expect(result).not.toHaveProperty("access");
26
+ });
27
+
28
+ test("carries fieldLabels and access through when present", () => {
29
+ const screen = {
30
+ id: "settings",
31
+ layout: { sections: [] },
32
+ fieldLabels: { apiKey: "config.apiKey" },
33
+ access: { roles: ["Admin"] },
34
+ } as unknown as ConfigEditScreenDefinition;
35
+
36
+ const result = synthesizeConfigEditScreen(screen);
37
+ expect(result.fieldLabels).toEqual({ apiKey: "config.apiKey" });
38
+ expect(result.access).toEqual({ roles: ["Admin"] });
39
+ });
40
+ });
@@ -0,0 +1,24 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { screenAccessAllows } from "../kumiko-screen";
3
+
4
+ describe("screenAccessAllows", () => {
5
+ test("allows when no access rule is set", () => {
6
+ expect(screenAccessAllows(undefined, undefined)).toBe(true);
7
+ });
8
+
9
+ test("openToAll: true allows regardless of roles", () => {
10
+ expect(screenAccessAllows({ openToAll: true }, undefined)).toBe(true);
11
+ });
12
+
13
+ test("roles-gated allows a matching role", () => {
14
+ expect(screenAccessAllows({ roles: ["Admin"] }, ["Admin"])).toBe(true);
15
+ });
16
+
17
+ test("roles-gated denies with no matching role", () => {
18
+ expect(screenAccessAllows({ roles: ["Admin"] }, ["Member"])).toBe(false);
19
+ });
20
+
21
+ test("roles-gated denies when userRoles is undefined", () => {
22
+ expect(screenAccessAllows({ roles: ["Admin"] }, undefined)).toBe(false);
23
+ });
24
+ });
@@ -120,7 +120,7 @@ export function qualifyNavId(featureName: string, navId: string): string {
120
120
  // Duplicated instead of imported from framework/engine's hasAccess (pulls
121
121
  // server-side deps) — same bundle-purity reasoning as headless/nav's
122
122
  // resolve.ts:userCanSee, which this mirrors.
123
- function screenAccessAllows(
123
+ export function screenAccessAllows(
124
124
  access: AccessRule | undefined,
125
125
  userRoles: readonly string[] | undefined,
126
126
  ): boolean {