@cosmicdrift/kumiko-renderer-web 0.235.4 → 0.236.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-web",
3
- "version": "0.235.4",
3
+ "version": "0.236.1",
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.235.4",
20
- "@cosmicdrift/kumiko-headless": "0.235.4",
21
- "@cosmicdrift/kumiko-renderer": "0.235.4",
19
+ "@cosmicdrift/kumiko-dispatcher-live": "0.236.1",
20
+ "@cosmicdrift/kumiko-headless": "0.236.1",
21
+ "@cosmicdrift/kumiko-renderer": "0.236.1",
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.235.4"
67
+ "@cosmicdrift/kumiko-locale-de": "0.236.1"
68
68
  },
69
69
  "repository": {
70
70
  "type": "git",
@@ -203,9 +203,18 @@ describe("KumikoScreen: singleton entityEdit", () => {
203
203
  </DispatcherProvider>,
204
204
  );
205
205
  await waitFor(() => expect(screen.queryByTestId("kumiko-screen-loading")).toBeNull());
206
+ await waitFor(() => expect(screen.getByTestId("field-title")).toBeTruthy());
206
207
 
207
208
  const titleInput = screen.getByTestId("field-title").querySelector("input") as HTMLInputElement;
208
209
  fireEvent.change(titleInput, { target: { value: "Created title" } });
210
+ // Re-query (not a held reference, which a re-render could have detached)
211
+ // and confirm the button is actually clickable before clicking it — a
212
+ // disabled native button swallows fireEvent.click silently, which reads
213
+ // identically to "the handler never ran" at the write-count assertion below.
214
+ await waitFor(() => {
215
+ const button = screen.getByTestId("render-edit-submit") as HTMLButtonElement;
216
+ expect(button.disabled).toBe(false);
217
+ });
209
218
  fireEvent.click(screen.getByTestId("render-edit-submit"));
210
219
 
211
220
  await waitFor(() => expect(write).toHaveBeenCalledTimes(1));
@@ -228,6 +237,10 @@ describe("KumikoScreen: singleton entityEdit", () => {
228
237
  fireEvent.change(screen.getByTestId("field-title").querySelector("input") as HTMLInputElement, {
229
238
  target: { value: "Edited again" },
230
239
  });
240
+ await waitFor(() => {
241
+ const button = screen.getByTestId("render-edit-submit") as HTMLButtonElement;
242
+ expect(button.disabled).toBe(false);
243
+ });
231
244
  fireEvent.click(screen.getByTestId("render-edit-submit"));
232
245
  // Exactly one create (the first submit) followed by an update, never a
233
246
  // second create — the singleton wrapper must have flipped branches.
@@ -58,6 +58,17 @@ const schema: FeatureSchema = {
58
58
  screens: [editScreen, listScreen, restrictedListScreen],
59
59
  };
60
60
 
61
+ // Re-queries the button (a held reference may have been detached by a
62
+ // re-render) and waits for it to actually be clickable before firing the
63
+ // click — a disabled native button swallows fireEvent.click silently.
64
+ async function clickSubmitOnceEnabled(): Promise<void> {
65
+ await waitFor(() => {
66
+ const button = screen.getByTestId("render-edit-submit") as HTMLButtonElement;
67
+ expect(button.disabled).toBe(false);
68
+ });
69
+ fireEvent.click(screen.getByTestId("render-edit-submit"));
70
+ }
71
+
61
72
  function makeDispatcher(overrides: Partial<Dispatcher> = {}): Dispatcher {
62
73
  const base = createMockDispatcher({
63
74
  query: (async () => ({
@@ -276,7 +287,7 @@ describe("KumikoScreen", () => {
276
287
 
277
288
  // Edit + submit → write command trägt { id, version, changes: {title} }
278
289
  fireEvent.change(titleInput, { target: { value: "edited-title" } });
279
- fireEvent.click(screen.getByTestId("render-edit-submit"));
290
+ await clickSubmitOnceEnabled();
280
291
 
281
292
  await waitFor(() => expect(writeCalls.length).toBe(1));
282
293
  const [call] = writeCalls;
@@ -538,7 +549,7 @@ describe("KumikoScreen", () => {
538
549
 
539
550
  // Dirty machen damit der Submit überhaupt feuert.
540
551
  fireEvent.change(titleInput, { target: { value: "edited" } });
541
- fireEvent.click(screen.getByTestId("render-edit-submit"));
552
+ await clickSubmitOnceEnabled();
542
553
 
543
554
  // Banner zeigt die ÜBERSETZTE version-conflict-Message (kein roher i18nKey)
544
555
  // und bietet einen Reload-Button. Gegen das Default-Bundle assertet, damit
@@ -1341,7 +1352,7 @@ describe("KumikoScreen", () => {
1341
1352
  const noteInput = screen.getByTestId("field-note").querySelector("input");
1342
1353
  if (noteInput === null) throw new Error("expected an <input> inside field-note");
1343
1354
  fireEvent.change(noteInput, { target: { value: "hello" } });
1344
- fireEvent.click(screen.getByTestId("render-edit-submit"));
1355
+ await clickSubmitOnceEnabled();
1345
1356
 
1346
1357
  await waitFor(() => expect(writeCalls.length).toBe(1));
1347
1358
  expect(writeCalls[0]?.type).toBe("tasks:write:task:note");
@@ -1566,7 +1577,7 @@ describe("KumikoScreen", () => {
1566
1577
  expect(titleInput).toBeTruthy();
1567
1578
  fireEvent.change(titleInput, { target: { value: "New Task" } });
1568
1579
 
1569
- fireEvent.click(screen.getByTestId("render-edit-submit"));
1580
+ await clickSubmitOnceEnabled();
1570
1581
  await waitFor(() => expect(writeCalls.length).toBe(1));
1571
1582
  expect(writeCalls[0]?.type).toBe("tasks:write:task:quick-add");
1572
1583
  // payloadMode="values" — alle Form-Werte landen im Payload, nicht
@@ -1615,7 +1626,7 @@ describe("KumikoScreen", () => {
1615
1626
 
1616
1627
  const titleInput = screen.getByTestId("field-title").querySelector("input") as HTMLInputElement;
1617
1628
  fireEvent.change(titleInput, { target: { value: "go" } });
1618
- fireEvent.click(screen.getByTestId("render-edit-submit"));
1629
+ await clickSubmitOnceEnabled();
1619
1630
  await waitFor(() => expect(navigateCalls.length).toBe(1));
1620
1631
  // #2419: entityList targets ignore entityId — do not attach a handler id.
1621
1632
  expect(navigateCalls[0]).toEqual({ screenId: "task-list" });
@@ -1674,7 +1685,7 @@ describe("KumikoScreen", () => {
1674
1685
 
1675
1686
  const titleInput = screen.getByTestId("field-title").querySelector("input") as HTMLInputElement;
1676
1687
  fireEvent.change(titleInput, { target: { value: "go" } });
1677
- fireEvent.click(screen.getByTestId("render-edit-submit"));
1688
+ await clickSubmitOnceEnabled();
1678
1689
  await waitFor(() => expect(navigateCalls.length).toBe(1));
1679
1690
  // #2485: the cross-feature target now resolves (over all mounted
1680
1691
  // features) — it just happens to be an entityList, which never carries
@@ -1739,7 +1750,7 @@ describe("KumikoScreen", () => {
1739
1750
 
1740
1751
  const vinInput = screen.getByTestId("field-vin").querySelector("input") as HTMLInputElement;
1741
1752
  fireEvent.change(vinInput, { target: { value: "1HGCM82633A004352" } });
1742
- fireEvent.click(screen.getByTestId("render-edit-submit"));
1753
+ await clickSubmitOnceEnabled();
1743
1754
  await waitFor(() => expect(navigateCalls.length).toBe(1));
1744
1755
  // #2485: redirect crosses a feature boundary (vehicle-vin-decode →
1745
1756
  // vehicles) — the target is only found by searching all mounted
@@ -1819,7 +1830,7 @@ describe("KumikoScreen", () => {
1819
1830
 
1820
1831
  const vinInput = screen.getByTestId("field-vin").querySelector("input") as HTMLInputElement;
1821
1832
  fireEvent.change(vinInput, { target: { value: "1HGCM82633A004352" } });
1822
- fireEvent.click(screen.getByTestId("render-edit-submit"));
1833
+ await clickSubmitOnceEnabled();
1823
1834
  await waitFor(() => expect(navigateCalls.length).toBe(1));
1824
1835
  // The QN names "vehicles" explicitly — the decoy feature's same-id
1825
1836
  // screen (mounted first) must not win just because it comes first.
@@ -1873,7 +1884,7 @@ describe("KumikoScreen", () => {
1873
1884
 
1874
1885
  const titleInput = screen.getByTestId("field-title").querySelector("input") as HTMLInputElement;
1875
1886
  fireEvent.change(titleInput, { target: { value: "go" } });
1876
- fireEvent.click(screen.getByTestId("render-edit-submit"));
1887
+ await clickSubmitOnceEnabled();
1877
1888
  await waitFor(() => expect(navigateCalls.length).toBe(1));
1878
1889
  expect(navigateCalls[0]).toEqual({ screenId: "task-edit", entityId: "x" });
1879
1890
  });
@@ -1924,7 +1935,7 @@ describe("KumikoScreen", () => {
1924
1935
 
1925
1936
  const titleInput = screen.getByTestId("field-title").querySelector("input") as HTMLInputElement;
1926
1937
  fireEvent.change(titleInput, { target: { value: "go" } });
1927
- fireEvent.click(screen.getByTestId("render-edit-submit"));
1938
+ await clickSubmitOnceEnabled();
1928
1939
  await waitFor(() => expect(navigateCalls.length).toBe(1));
1929
1940
  expect(navigateCalls[0]).toEqual({ screenId: "task-list" });
1930
1941
  expect(Object.hasOwn(navigateCalls[0] ?? {}, "entityId")).toBe(false);
@@ -2186,7 +2197,7 @@ describe("KumikoScreen", () => {
2186
2197
 
2187
2198
  const titleInput = screen.getByTestId("field-title").querySelector("input") as HTMLInputElement;
2188
2199
  fireEvent.change(titleInput, { target: { value: "stay" } });
2189
- fireEvent.click(screen.getByTestId("render-edit-submit"));
2200
+ await clickSubmitOnceEnabled();
2190
2201
  // Auf den Write-Call warten — sonst racet der Test gegen den
2191
2202
  // async submit und prüft navigate-Calls bevor handleSubmitted
2192
2203
  // überhaupt gerufen wurde (waitFor auf "render-edit-form" wäre