@cosmicdrift/kumiko-renderer 0.200.0 → 0.201.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.200.0",
3
+ "version": "0.201.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.200.0",
19
- "@cosmicdrift/kumiko-headless": "0.200.0",
18
+ "@cosmicdrift/kumiko-framework": "0.201.0",
19
+ "@cosmicdrift/kumiko-headless": "0.201.0",
20
20
  "react": "^19.2.6",
21
21
  "temporal-polyfill": "^0.3.2",
22
22
  "zod": "^4.4.3"
@@ -0,0 +1,128 @@
1
+ // Regression coverage for the hidden-section guards in render-edit.tsx
2
+ // (kumiko-framework#1731, from PR-review finding #1690): nothing previously
3
+ // asserted that a fully-hidden section actually stays out of the DOM.
4
+ //
5
+ // Two cases, verified by mutation-testing each guard in isolation:
6
+ // - "Hidden": a "fields" section whose only field is condition-hidden — the
7
+ // issue's literal ask. Redundantly guarded: both the
8
+ // `filterEditSections(...).filter(...)` pass and the render loop's
9
+ // `if (!section.visible) return null;` independently suppress it, so this
10
+ // case only goes red if BOTH guards are removed.
11
+ // - "Empty": a "fields" section declared with zero fields. fw#1901 keeps it
12
+ // past the filter above on purpose (it isn't "hidden", it just has
13
+ // nothing to hide), so its `visible` stays vacuously false and
14
+ // `if (!section.visible) return null;` is the ONLY thing nulling it out —
15
+ // this case is what actually pins that line, verified red when it's
16
+ // neutralized. Not a shape a booted app can produce (boot-validator
17
+ // rejects `fields.length === 0`), but RenderEdit's props aren't
18
+ // boot-validated, so it's a real call shape worth guarding.
19
+
20
+ import { describe, expect, test } from "bun:test";
21
+ import type {
22
+ EntityDefinition,
23
+ EntityEditScreenDefinition,
24
+ } from "@cosmicdrift/kumiko-framework/ui-types";
25
+ import { type RenderResult, render } from "@testing-library/react";
26
+ import type { ComponentType, ReactNode } from "react";
27
+ import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
28
+ import { kumikoDefaultTranslations } from "../../i18n-defaults";
29
+ import { type CorePrimitives, PrimitivesProvider, type SectionProps } from "../../primitives";
30
+ import { RenderEdit } from "../render-edit";
31
+
32
+ const noop = (): ReactNode => null;
33
+ const passChildren = ({ children }: { readonly children?: ReactNode }): ReactNode => children;
34
+ const renderSection: ComponentType<SectionProps> = ({ testId, children }) => (
35
+ <div data-testid={testId}>{children}</div>
36
+ );
37
+
38
+ function testPrimitives(): CorePrimitives {
39
+ return {
40
+ Button: noop,
41
+ Banner: noop,
42
+ Field: passChildren,
43
+ Input: noop,
44
+ DataTable: noop,
45
+ Form: passChildren,
46
+ Section: renderSection,
47
+ Card: passChildren,
48
+ Grid: passChildren,
49
+ GridCell: passChildren,
50
+ Text: passChildren,
51
+ Heading: noop,
52
+ Dialog: noop,
53
+ Modal: noop,
54
+ Lightbox: noop,
55
+ ConfigSourceBadge: noop,
56
+ ConfigCascadeView: noop,
57
+ Link: noop,
58
+ };
59
+ }
60
+
61
+ function buildEntity(): EntityDefinition {
62
+ return {
63
+ fields: {
64
+ name: { type: "text", maxLength: 200, required: false, searchable: false, sortable: false },
65
+ secret: { type: "text", maxLength: 200, required: false, searchable: false, sortable: false },
66
+ },
67
+ };
68
+ }
69
+
70
+ function renderEdit(screen: EntityEditScreenDefinition): RenderResult {
71
+ return render(
72
+ <LocaleProvider
73
+ resolver={createStaticLocaleResolver({ locale: "en-US" })}
74
+ fallbackBundles={[kumikoDefaultTranslations]}
75
+ >
76
+ <PrimitivesProvider value={testPrimitives()}>
77
+ <RenderEdit
78
+ screen={screen}
79
+ entity={buildEntity()}
80
+ featureName="widgets"
81
+ initial={{ name: "", secret: "" }}
82
+ />
83
+ </PrimitivesProvider>
84
+ </LocaleProvider>,
85
+ );
86
+ }
87
+
88
+ describe("RenderEdit — section with every field condition-hidden", () => {
89
+ test("the visible section renders, the fully-hidden one does not reach the DOM", () => {
90
+ const screen: EntityEditScreenDefinition = {
91
+ id: "widget-edit",
92
+ type: "entityEdit",
93
+ entity: "widget",
94
+ layout: {
95
+ sections: [
96
+ { title: "Visible", fields: ["name"] },
97
+ { title: "Hidden", fields: [{ field: "secret", visible: false }] },
98
+ ],
99
+ },
100
+ };
101
+
102
+ const { getByTestId, queryByTestId } = renderEdit(screen);
103
+
104
+ expect(getByTestId("section-Visible")).toBeDefined();
105
+ expect(queryByTestId("section-Hidden")).toBeNull();
106
+ });
107
+ });
108
+
109
+ describe("RenderEdit — section declared with zero fields", () => {
110
+ test("the visible section renders, the empty one does not reach the DOM", () => {
111
+ const screen: EntityEditScreenDefinition = {
112
+ id: "widget-edit",
113
+ type: "entityEdit",
114
+ entity: "widget",
115
+ layout: {
116
+ sections: [
117
+ { title: "Visible", fields: ["name"] },
118
+ { title: "Empty", fields: [] },
119
+ ],
120
+ },
121
+ };
122
+
123
+ const { getByTestId, queryByTestId } = renderEdit(screen);
124
+
125
+ expect(getByTestId("section-Visible")).toBeDefined();
126
+ expect(queryByTestId("section-Empty")).toBeNull();
127
+ });
128
+ });
@@ -210,6 +210,7 @@ export type RenderEditChangeState<TValues extends FormValues> = {
210
210
  readonly changes: Partial<TValues>;
211
211
  readonly dirty: boolean;
212
212
  readonly valid: boolean;
213
+ readonly submitting: boolean;
213
214
  };
214
215
 
215
216
  export type RenderEditControls<TValues extends FormValues> = {
@@ -578,8 +579,14 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
578
579
  const currentSchema = schemaRef.current;
579
580
  const valid =
580
581
  currentSchema === undefined ? true : currentSchema.safeParse(snapshot.values).success;
581
- cb({ values: snapshot.values, changes: snapshot.changes, dirty: snapshot.isDirty, valid });
582
- }, [snapshot]);
582
+ cb({
583
+ values: snapshot.values,
584
+ changes: snapshot.changes,
585
+ dirty: snapshot.isDirty,
586
+ valid,
587
+ submitting: isSubmitting,
588
+ });
589
+ }, [snapshot, isSubmitting]);
583
590
 
584
591
  useEffect(() => {
585
592
  // skip: this screen does not persist a draft.