@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.
|
|
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.
|
|
19
|
-
"@cosmicdrift/kumiko-headless": "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({
|
|
582
|
-
|
|
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.
|