@cosmicdrift/kumiko-renderer 0.215.7 → 0.216.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.215.7",
3
+ "version": "0.216.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.215.7",
19
- "@cosmicdrift/kumiko-headless": "0.215.7",
18
+ "@cosmicdrift/kumiko-framework": "0.216.0",
19
+ "@cosmicdrift/kumiko-headless": "0.216.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.215.7"
28
+ "@cosmicdrift/kumiko-locale-de": "0.216.0"
29
29
  },
30
30
  "repository": {
31
31
  "type": "git",
@@ -3,10 +3,11 @@ import type {
3
3
  EntityDefinition,
4
4
  EntityEditScreenDefinition,
5
5
  } from "@cosmicdrift/kumiko-framework/ui-types";
6
- import type { DispatcherError, SubmitResult } from "@cosmicdrift/kumiko-headless";
6
+ import type { Dispatcher, DispatcherError, SubmitResult } from "@cosmicdrift/kumiko-headless";
7
7
  import { fireEvent, render, screen as rtlScreen, waitFor } from "@testing-library/react";
8
8
  import type { ComponentType, ReactNode } from "react";
9
9
  import { buildFormSchema } from "../../app/form-schema";
10
+ import { DispatcherProvider } from "../../context/dispatcher-context";
10
11
  import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
11
12
  import { kumikoDefaultTranslations } from "../../i18n-defaults";
12
13
  import {
@@ -114,12 +115,37 @@ function buildEntity(required = false): EntityDefinition {
114
115
  };
115
116
  }
116
117
 
118
+ function stubDispatcher(writeImpl?: Dispatcher["write"]): {
119
+ dispatcher: Dispatcher;
120
+ writes: Array<{ type: string; payload: unknown }>;
121
+ } {
122
+ const writes: Array<{ type: string; payload: unknown }> = [];
123
+ const dispatcher: Dispatcher = {
124
+ write: (async (type, payload) => {
125
+ writes.push({ type, payload });
126
+ if (writeImpl) return writeImpl(type, payload);
127
+ return { isSuccess: true, data: { id: "n1" } };
128
+ }) as Dispatcher["write"],
129
+ query: (async () => ({ isSuccess: true, data: {} })) as Dispatcher["query"],
130
+ batch: (async () => ({ isSuccess: true, results: [] })) as Dispatcher["batch"],
131
+ statusStore: {
132
+ getState: () => "online",
133
+ subscribe: () => () => {},
134
+ } as unknown as Dispatcher["statusStore"],
135
+ async *stream() {},
136
+ pendingWrites: () => [],
137
+ pendingFiles: () => [],
138
+ };
139
+ return { dispatcher, writes };
140
+ }
141
+
117
142
  function renderEdit(
118
143
  screen: EntityEditScreenDefinition,
119
144
  overrides: Partial<RenderEditProps<Values>> = {},
120
145
  entity: EntityDefinition = buildEntity(),
146
+ dispatcher?: Dispatcher,
121
147
  ) {
122
- return render(
148
+ const body = (
123
149
  <LocaleProvider
124
150
  resolver={createStaticLocaleResolver({ locale: "en-US" })}
125
151
  fallbackBundles={[kumikoDefaultTranslations]}
@@ -133,7 +159,14 @@ function renderEdit(
133
159
  {...overrides}
134
160
  />
135
161
  </PrimitivesProvider>
136
- </LocaleProvider>,
162
+ </LocaleProvider>
163
+ );
164
+ return render(
165
+ dispatcher !== undefined ? (
166
+ <DispatcherProvider dispatcher={dispatcher}>{body}</DispatcherProvider>
167
+ ) : (
168
+ body
169
+ ),
137
170
  );
138
171
  }
139
172
 
@@ -298,3 +331,61 @@ describe("RenderEdit — custom actions", () => {
298
331
  );
299
332
  });
300
333
  });
334
+
335
+ describe("RenderEdit — writeCommand path", () => {
336
+ test("successful writeCommand dispatches and notifies onSubmit", async () => {
337
+ const { dispatcher, writes } = stubDispatcher();
338
+ let submitted: SubmitResult<unknown> | undefined;
339
+ renderEdit(
340
+ oneFieldScreen,
341
+ {
342
+ writeCommand: "contacts:write:contact:update",
343
+ onSubmit: (result) => {
344
+ submitted = result;
345
+ },
346
+ },
347
+ buildEntity(),
348
+ dispatcher,
349
+ );
350
+
351
+ fireEvent.change(rtlScreen.getByLabelText(/name/i), { target: { value: "Ada" } });
352
+ fireEvent.click(rtlScreen.getByTestId("render-edit-submit"));
353
+
354
+ await waitFor(() => expect(submitted).toBeDefined());
355
+ expect(submitted?.isSuccess).toBe(true);
356
+ expect(writes).toHaveLength(1);
357
+ expect(writes[0]?.type).toBe("contacts:write:contact:update");
358
+ expect(writes[0]?.payload).toMatchObject({ name: "Ada" });
359
+ });
360
+
361
+ test("failed writeCommand without field issues shows form-error banner", async () => {
362
+ const writeFailure: DispatcherError = {
363
+ code: "conflict",
364
+ httpStatus: 409,
365
+ i18nKey: "errors.conflict",
366
+ message: "conflict",
367
+ };
368
+ const { dispatcher } = stubDispatcher(async () => ({
369
+ isSuccess: false,
370
+ error: writeFailure,
371
+ }));
372
+ let submitted: SubmitResult<unknown> | undefined;
373
+ renderEdit(
374
+ oneFieldScreen,
375
+ {
376
+ writeCommand: "contacts:write:contact:update",
377
+ onSubmit: (result) => {
378
+ submitted = result;
379
+ },
380
+ },
381
+ buildEntity(),
382
+ dispatcher,
383
+ );
384
+
385
+ fireEvent.change(rtlScreen.getByLabelText(/name/i), { target: { value: "Ada" } });
386
+ fireEvent.click(rtlScreen.getByTestId("render-edit-submit"));
387
+
388
+ await waitFor(() => expect(rtlScreen.getByTestId("render-edit-form-error")).toBeTruthy());
389
+ expect(submitted?.isSuccess).toBe(false);
390
+ });
391
+ });