@cosmicdrift/kumiko-renderer 0.232.0 → 0.234.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer",
3
- "version": "0.232.0",
3
+ "version": "0.234.0",
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.232.0",
19
- "@cosmicdrift/kumiko-headless": "0.232.0",
18
+ "@cosmicdrift/kumiko-framework": "0.234.0",
19
+ "@cosmicdrift/kumiko-headless": "0.234.0",
20
20
  "react": "^19.2.6",
21
21
  "temporal-polyfill": "^0.3.2",
22
22
  "zod": "^4.4.3"
@@ -25,7 +25,7 @@
25
25
  "@testing-library/react": "^16.3.2",
26
26
  "@types/react": "^19.2.14",
27
27
  "jsdom": "^29.1.1",
28
- "@cosmicdrift/kumiko-locale-de": "0.232.0"
28
+ "@cosmicdrift/kumiko-locale-de": "0.234.0"
29
29
  },
30
30
  "repository": {
31
31
  "type": "git",
@@ -292,10 +292,13 @@ const TestButton: ComponentType<ButtonProps> = ({ children, onClick, testId }) =
292
292
  </button>
293
293
  );
294
294
 
295
- const FormWithActions: ComponentType<FormProps> = ({ children, actions }) => (
295
+ const FormWithActions: ComponentType<FormProps> = ({ children, actions, secondaryActions }) => (
296
296
  <>
297
297
  <div data-testid="form-body">{children}</div>
298
- <div data-testid="form-actions">{actions}</div>
298
+ <div data-testid="form-actions">
299
+ {secondaryActions}
300
+ {actions}
301
+ </div>
299
302
  </>
300
303
  );
301
304
 
@@ -0,0 +1,169 @@
1
+ // fw-ui-defaults: RowAction had no icon field at all, so every framework-
2
+ // generated action button rendered as a same-looking text button. This
3
+ // renders the real entityList pipeline (KumikoScreen → EntityListBody) and
4
+ // asserts on the resolved `icon` KumikoScreen attaches to each
5
+ // DataTableRowAction — the id-derived default (ACTION_ICON_BY_ID in
6
+ // kumiko-screen.tsx) for an action with no declared icon, and the declared
7
+ // icon overriding that default for one that has it.
8
+
9
+ import { describe, expect, test } from "bun:test";
10
+ import type {
11
+ EntityDefinition,
12
+ EntityListScreenDefinition,
13
+ } from "@cosmicdrift/kumiko-framework/ui-types";
14
+ import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
15
+ import { render, waitFor } from "@testing-library/react";
16
+ import type { ComponentType, ReactNode } from "react";
17
+ import { DispatcherProvider } from "../../context/dispatcher-context";
18
+ import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
19
+ import { kumikoDefaultTranslations } from "../../i18n-defaults";
20
+ import {
21
+ type CorePrimitives,
22
+ type DataTableProps,
23
+ type DataTableRowAction,
24
+ PrimitivesProvider,
25
+ } from "../../primitives";
26
+ import type { FeatureSchema } from "../feature-schema";
27
+ import { KumikoScreen } from "../kumiko-screen";
28
+ import { NavProvider } from "../nav";
29
+
30
+ function stubDispatcher(): Dispatcher {
31
+ return {
32
+ write: (async () => ({ isSuccess: true, data: {} })) as unknown as Dispatcher["write"],
33
+ query: (async () => ({
34
+ isSuccess: true,
35
+ data: { rows: [{ id: "row-1" }], nextCursor: null, total: 1 },
36
+ })) as unknown as Dispatcher["query"],
37
+ batch: (async () => ({ isSuccess: true, results: [] })) as unknown as Dispatcher["batch"],
38
+ statusStore: {
39
+ getState: () => "online",
40
+ subscribe: () => () => {},
41
+ } as unknown as Dispatcher["statusStore"],
42
+ async *stream() {},
43
+ pendingWrites: () => [],
44
+ pendingFiles: () => [],
45
+ };
46
+ }
47
+
48
+ let capturedRowActions: readonly DataTableRowAction[] | undefined;
49
+ const captureDataTable: ComponentType<DataTableProps> = (props) => {
50
+ capturedRowActions = props.rowActions;
51
+ return null;
52
+ };
53
+ const noop = (): ReactNode => null;
54
+ const passChildren = ({ children }: { readonly children?: ReactNode }): ReactNode => children;
55
+
56
+ const testPrimitives: CorePrimitives = {
57
+ Button: noop,
58
+ Banner: passChildren,
59
+ Field: passChildren,
60
+ Input: noop,
61
+ DataTable: captureDataTable,
62
+ Form: passChildren,
63
+ Section: passChildren,
64
+ Card: passChildren,
65
+ Grid: passChildren,
66
+ GridCell: passChildren,
67
+ Text: passChildren,
68
+ Heading: noop,
69
+ Dialog: noop,
70
+ Modal: noop,
71
+ Lightbox: noop,
72
+ ConfigSourceBadge: noop,
73
+ ConfigCascadeView: noop,
74
+ Link: noop,
75
+ };
76
+
77
+ function buildSchema(): FeatureSchema {
78
+ const entity: EntityDefinition = {
79
+ fields: {
80
+ name: { type: "text", maxLength: 50, required: false, searchable: false, sortable: false },
81
+ },
82
+ };
83
+ const listScreen: EntityListScreenDefinition = {
84
+ id: "widget-list",
85
+ type: "entityList",
86
+ entity: "widget",
87
+ columns: ["name"],
88
+ rowActions: [
89
+ // No declared icon — must fall back to ACTION_ICON_BY_ID["delete"].
90
+ {
91
+ id: "delete",
92
+ label: "Delete",
93
+ handler: "icon-fixture:write:widget:delete",
94
+ },
95
+ // Declared icon on an id that DOES have an id-derived default
96
+ // ("edit" -> pencil) — the declared icon must win.
97
+ {
98
+ kind: "navigate",
99
+ id: "edit",
100
+ label: "Edit",
101
+ screen: "widget-edit",
102
+ icon: "archive",
103
+ },
104
+ ],
105
+ };
106
+ return {
107
+ featureName: "icon-fixture",
108
+ entities: { widget: entity },
109
+ screens: [listScreen],
110
+ } as FeatureSchema;
111
+ }
112
+
113
+ function renderListScreen(): void {
114
+ render(
115
+ <LocaleProvider
116
+ resolver={createStaticLocaleResolver({ locale: "en" })}
117
+ fallbackBundles={[kumikoDefaultTranslations]}
118
+ >
119
+ <DispatcherProvider dispatcher={stubDispatcher()}>
120
+ <NavProvider
121
+ value={{
122
+ route: { screenId: "icon-fixture:screen:widget-list" },
123
+ navigate: () => {},
124
+ replace: () => {},
125
+ hrefFor: () => "",
126
+ searchParams: {},
127
+ setSearchParams: () => {},
128
+ }}
129
+ >
130
+ <PrimitivesProvider value={testPrimitives}>
131
+ <KumikoScreen schema={buildSchema()} qn="icon-fixture:screen:widget-list" />
132
+ </PrimitivesProvider>
133
+ </NavProvider>
134
+ </DispatcherProvider>
135
+ </LocaleProvider>,
136
+ );
137
+ }
138
+
139
+ function requireAction(id: string): DataTableRowAction {
140
+ const action = capturedRowActions?.find((a) => a.id === id);
141
+ if (!action) throw new Error(`expected the '${id}' row action to be captured`);
142
+ return action;
143
+ }
144
+
145
+ describe("entityList rowActions resolve an icon (fw-ui-defaults)", () => {
146
+ test("an action with no declared icon derives it from its id ('delete' -> trash)", async () => {
147
+ capturedRowActions = undefined;
148
+
149
+ renderListScreen();
150
+
151
+ await waitFor(() => {
152
+ expect(capturedRowActions).toBeDefined();
153
+ });
154
+
155
+ expect(requireAction("delete").icon).toBe("trash");
156
+ });
157
+
158
+ test("a declared icon overrides the id-derived default ('edit' would derive pencil)", async () => {
159
+ capturedRowActions = undefined;
160
+
161
+ renderListScreen();
162
+
163
+ await waitFor(() => {
164
+ expect(capturedRowActions).toBeDefined();
165
+ });
166
+
167
+ expect(requireAction("edit").icon).toBe("archive");
168
+ });
169
+ });
@@ -43,14 +43,18 @@ const TestButton: ComponentType<ButtonProps> = ({ children, onClick, testId }) =
43
43
  </button>
44
44
  );
45
45
 
46
- // Form's `actions` slot carries the header buttons under test — passChildren
47
- // alone would drop it. Wrapped in distinct testid'd containers so a test can
48
- // assert a banner rendered as a Form CHILD (formError/actionError region)
49
- // rather than inside the actions row — the review finding this proves.
50
- const FormWithActions: ComponentType<FormProps> = ({ children, actions }) => (
46
+ // Form's `actions`/`secondaryActions` slots carry the header buttons under
47
+ // test — passChildren alone would drop them. Wrapped in distinct testid'd
48
+ // containers so a test can assert a banner rendered as a Form CHILD
49
+ // (formError/actionError region) rather than inside the actions row — the
50
+ // review finding this proves.
51
+ const FormWithActions: ComponentType<FormProps> = ({ children, actions, secondaryActions }) => (
51
52
  <>
52
53
  <div data-testid="form-body">{children}</div>
53
- <div data-testid="form-actions">{actions}</div>
54
+ <div data-testid="form-actions">
55
+ {secondaryActions}
56
+ {actions}
57
+ </div>
54
58
  </>
55
59
  );
56
60