@cosmicdrift/kumiko-renderer 0.328.0 → 0.328.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.
Files changed (259) hide show
  1. package/dist/app/action-form-shim.d.ts +14 -0
  2. package/{src/app/action-form-shim.ts → dist/app/action-form-shim.js} +26 -46
  3. package/dist/app/app-features-context.d.ts +8 -0
  4. package/dist/app/app-features-context.js +16 -0
  5. package/dist/app/column-renderers.d.ts +30 -0
  6. package/dist/app/column-renderers.js +34 -0
  7. package/dist/app/config-edit-shim.d.ts +11 -0
  8. package/{src/app/config-edit-shim.ts → dist/app/config-edit-shim.js} +12 -25
  9. package/dist/app/content-editors.d.ts +38 -0
  10. package/dist/app/content-editors.js +26 -0
  11. package/dist/app/content-preview.d.ts +11 -0
  12. package/dist/app/content-preview.js +30 -0
  13. package/dist/app/custom-screens.d.ts +11 -0
  14. package/dist/app/custom-screens.js +20 -0
  15. package/dist/app/dashboard-body.d.ts +16 -0
  16. package/dist/app/dashboard-body.js +11 -0
  17. package/dist/app/extension-form-submit.d.ts +32 -0
  18. package/dist/app/extension-form-submit.js +73 -0
  19. package/dist/app/extension-sections.d.ts +72 -0
  20. package/dist/app/extension-sections.js +34 -0
  21. package/dist/app/feature-schema.d.ts +4 -0
  22. package/{src/app/feature-schema.ts → dist/app/feature-schema.js} +12 -22
  23. package/dist/app/form-schema.d.ts +4 -0
  24. package/dist/app/form-schema.js +85 -0
  25. package/dist/app/kumiko-screen.d.ts +43 -0
  26. package/dist/app/kumiko-screen.js +2206 -0
  27. package/dist/app/layout-fields.d.ts +3 -0
  28. package/dist/app/layout-fields.js +20 -0
  29. package/dist/app/list-facets.d.ts +48 -0
  30. package/dist/app/list-facets.js +185 -0
  31. package/dist/app/nav.d.ts +61 -0
  32. package/dist/app/nav.js +139 -0
  33. package/dist/app/projection-detail-shim.d.ts +8 -0
  34. package/dist/app/projection-detail-shim.js +71 -0
  35. package/dist/app/projection-list-shim.d.ts +13 -0
  36. package/{src/app/projection-list-shim.ts → dist/app/projection-list-shim.js} +26 -41
  37. package/dist/app/qn.d.ts +7 -0
  38. package/{src/app/qn.ts → dist/app/qn.js} +9 -11
  39. package/dist/app/qualify-screen-id.d.ts +2 -0
  40. package/{src/app/qualify-screen-id.ts → dist/app/qualify-screen-id.js} +5 -6
  41. package/dist/app/reference-facet-bridge.d.ts +10 -0
  42. package/dist/app/reference-facet-bridge.js +22 -0
  43. package/dist/app/return-to.d.ts +31 -0
  44. package/dist/app/return-to.js +188 -0
  45. package/dist/app/row-actions.d.ts +77 -0
  46. package/dist/app/row-actions.js +416 -0
  47. package/dist/app/screen-access.d.ts +8 -0
  48. package/dist/app/screen-access.js +48 -0
  49. package/dist/app/secret-mint-body.d.ts +10 -0
  50. package/dist/app/secret-mint-body.js +140 -0
  51. package/dist/app/secrets-edit-body.d.ts +8 -0
  52. package/dist/app/secrets-edit-body.js +76 -0
  53. package/dist/app/use-embedded-screen.d.ts +6 -0
  54. package/dist/app/use-embedded-screen.js +19 -0
  55. package/dist/app/variable-chips.d.ts +7 -0
  56. package/dist/app/variable-chips.js +16 -0
  57. package/dist/app/write-failed-error.d.ts +6 -0
  58. package/dist/app/write-failed-error.js +21 -0
  59. package/dist/components/embedded-list-field.d.ts +10 -0
  60. package/dist/components/embedded-list-field.js +259 -0
  61. package/dist/components/grid-cell-for-field.d.ts +25 -0
  62. package/dist/components/grid-cell-for-field.js +31 -0
  63. package/dist/components/multi-select-checkboxes.d.ts +14 -0
  64. package/dist/components/multi-select-checkboxes.js +36 -0
  65. package/dist/components/reference-create-dialog.d.ts +13 -0
  66. package/dist/components/reference-create-dialog.js +46 -0
  67. package/dist/components/related-list-section.d.ts +25 -0
  68. package/dist/components/related-list-section.js +273 -0
  69. package/dist/components/render-edit-action-button.d.ts +22 -0
  70. package/dist/components/render-edit-action-button.js +43 -0
  71. package/dist/components/render-edit-logic.d.ts +6 -0
  72. package/dist/components/render-edit-logic.js +63 -0
  73. package/dist/components/render-edit-types.d.ts +209 -0
  74. package/dist/components/render-edit-types.js +1 -0
  75. package/dist/components/render-edit.d.ts +6 -0
  76. package/dist/components/render-edit.js +1169 -0
  77. package/dist/components/render-field.d.ts +38 -0
  78. package/dist/components/render-field.js +544 -0
  79. package/dist/components/render-list.d.ts +117 -0
  80. package/dist/components/render-list.js +232 -0
  81. package/dist/components/write-form-section.d.ts +17 -0
  82. package/dist/components/write-form-section.js +104 -0
  83. package/dist/context/dispatcher-context.d.ts +10 -0
  84. package/{src/context/dispatcher-context.tsx → dist/context/dispatcher-context.js} +16 -29
  85. package/dist/context/draft-storage-context.d.ts +12 -0
  86. package/dist/context/draft-storage-context.js +18 -0
  87. package/dist/context/user-roles-context.d.ts +7 -0
  88. package/{src/context/user-roles-context.tsx → dist/context/user-roles-context.js} +7 -15
  89. package/dist/format-when.d.ts +1 -0
  90. package/{src/format-when.ts → dist/format-when.js} +7 -7
  91. package/dist/hooks/reference-limits.d.ts +3 -0
  92. package/{src/hooks/reference-limits.ts → dist/hooks/reference-limits.js} +0 -1
  93. package/dist/hooks/use-ai-text.d.ts +52 -0
  94. package/dist/hooks/use-ai-text.js +90 -0
  95. package/dist/hooks/use-disclosure.d.ts +7 -0
  96. package/dist/hooks/use-disclosure.js +8 -0
  97. package/dist/hooks/use-form.d.ts +7 -0
  98. package/dist/hooks/use-form.js +36 -0
  99. package/dist/hooks/use-list-url-state.d.ts +43 -0
  100. package/dist/hooks/use-list-url-state.js +131 -0
  101. package/dist/hooks/use-mutation.d.ts +9 -0
  102. package/dist/hooks/use-mutation.js +37 -0
  103. package/dist/hooks/use-query.d.ts +14 -0
  104. package/dist/hooks/use-query.js +118 -0
  105. package/dist/hooks/use-reference-lookup.d.ts +14 -0
  106. package/dist/hooks/use-reference-lookup.js +63 -0
  107. package/dist/hooks/use-store.d.ts +3 -0
  108. package/dist/hooks/use-store.js +38 -0
  109. package/dist/hooks/use-stream-handler.d.ts +14 -0
  110. package/dist/hooks/use-stream-handler.js +93 -0
  111. package/dist/i18n-defaults.d.ts +2 -0
  112. package/dist/i18n-defaults.js +201 -0
  113. package/dist/i18n.d.ts +64 -0
  114. package/dist/i18n.js +166 -0
  115. package/dist/index.d.ts +77 -0
  116. package/dist/index.js +55 -0
  117. package/dist/inside-drawer.d.ts +2 -0
  118. package/{src/inside-drawer.ts → dist/inside-drawer.js} +2 -5
  119. package/dist/page-header-slot.d.ts +2 -0
  120. package/{src/page-header-slot.ts → dist/page-header-slot.js} +2 -5
  121. package/dist/primitives.d.ts +1351 -0
  122. package/dist/primitives.js +73 -0
  123. package/dist/screen-fills-height.d.ts +3 -0
  124. package/dist/screen-fills-height.js +3 -0
  125. package/dist/share-links.d.ts +4 -0
  126. package/dist/share-links.js +11 -0
  127. package/dist/sort-by-accessor.d.ts +5 -0
  128. package/dist/sort-by-accessor.js +16 -0
  129. package/dist/sse/live-events.d.ts +23 -0
  130. package/dist/sse/live-events.js +28 -0
  131. package/dist/tokens.d.ts +83 -0
  132. package/dist/tokens.js +80 -0
  133. package/package.json +16 -9
  134. package/src/__tests__/error-i18n-defaults.test.ts +0 -82
  135. package/src/__tests__/format-when.test.ts +0 -12
  136. package/src/__tests__/i18n.test.tsx +0 -283
  137. package/src/__tests__/merge-search-params-into-initial.test.ts +0 -525
  138. package/src/__tests__/qn.test.ts +0 -83
  139. package/src/__tests__/share-links.test.ts +0 -26
  140. package/src/__tests__/sort-by-accessor.test.ts +0 -48
  141. package/src/__tests__/stringify-nav-params.test.ts +0 -33
  142. package/src/__tests__/use-list-url-state.test.tsx +0 -250
  143. package/src/app/__tests__/action-form-field-labels.test.tsx +0 -92
  144. package/src/app/__tests__/action-form-shim.test.ts +0 -137
  145. package/src/app/__tests__/build-list-query-payload.test.ts +0 -107
  146. package/src/app/__tests__/config-edit-shim.test.ts +0 -57
  147. package/src/app/__tests__/content-editors.test.tsx +0 -128
  148. package/src/app/__tests__/content-preview.test.tsx +0 -184
  149. package/src/app/__tests__/entity-edit-create-search-params.test.tsx +0 -236
  150. package/src/app/__tests__/entity-edit-fill-height.test.tsx +0 -118
  151. package/src/app/__tests__/entity-edit-jsonb-embedded-submit.test.tsx +0 -239
  152. package/src/app/__tests__/entity-list-create-singleton.test.tsx +0 -128
  153. package/src/app/__tests__/entity-list-row-action-entity-target.test.tsx +0 -424
  154. package/src/app/__tests__/entity-list-row-action-icon.test.tsx +0 -248
  155. package/src/app/__tests__/entity-list-row-action-kumiko-actions-view.test.tsx +0 -162
  156. package/src/app/__tests__/entity-list-row-action-refetch.test.tsx +0 -401
  157. package/src/app/__tests__/entity-list-screen-padding.test.tsx +0 -128
  158. package/src/app/__tests__/feature-schema.test.ts +0 -40
  159. package/src/app/__tests__/form-schema.test.ts +0 -247
  160. package/src/app/__tests__/list-daterange-facet.test.ts +0 -165
  161. package/src/app/__tests__/list-filter-facets.test.tsx +0 -521
  162. package/src/app/__tests__/nav.test.ts +0 -198
  163. package/src/app/__tests__/projection-detail-actions.test.tsx +0 -719
  164. package/src/app/__tests__/projection-detail-reference-field.test.tsx +0 -165
  165. package/src/app/__tests__/projection-detail-shim.test.ts +0 -188
  166. package/src/app/__tests__/projection-detail-singleton.test.tsx +0 -191
  167. package/src/app/__tests__/projection-list-bad-shape.test.tsx +0 -152
  168. package/src/app/__tests__/projection-list-reference-column.test.tsx +0 -219
  169. package/src/app/__tests__/projection-list-search-sort.test.tsx +0 -212
  170. package/src/app/__tests__/qualified-names.test.ts +0 -37
  171. package/src/app/__tests__/return-to.test.ts +0 -396
  172. package/src/app/__tests__/row-actions-navigate-tab.test.ts +0 -223
  173. package/src/app/__tests__/row-default-edit-action.test.tsx +0 -324
  174. package/src/app/__tests__/screen-access-allows.test.ts +0 -34
  175. package/src/app/__tests__/secret-mint-body.test.tsx +0 -404
  176. package/src/app/__tests__/variable-chips.test.tsx +0 -70
  177. package/src/app/app-features-context.tsx +0 -25
  178. package/src/app/column-renderers.tsx +0 -68
  179. package/src/app/content-editors.tsx +0 -85
  180. package/src/app/content-preview.tsx +0 -67
  181. package/src/app/custom-screens.tsx +0 -29
  182. package/src/app/dashboard-body.tsx +0 -33
  183. package/src/app/extension-form-submit.tsx +0 -123
  184. package/src/app/extension-sections.tsx +0 -111
  185. package/src/app/form-schema.ts +0 -91
  186. package/src/app/kumiko-screen.tsx +0 -3801
  187. package/src/app/layout-fields.ts +0 -31
  188. package/src/app/list-facets.ts +0 -259
  189. package/src/app/nav.tsx +0 -258
  190. package/src/app/projection-detail-shim.ts +0 -86
  191. package/src/app/reference-facet-bridge.tsx +0 -58
  192. package/src/app/return-to.tsx +0 -234
  193. package/src/app/row-actions.ts +0 -640
  194. package/src/app/screen-access.ts +0 -63
  195. package/src/app/secret-mint-body.tsx +0 -259
  196. package/src/app/secrets-edit-body.tsx +0 -194
  197. package/src/app/use-embedded-screen.ts +0 -27
  198. package/src/app/variable-chips.tsx +0 -39
  199. package/src/app/write-failed-error.ts +0 -27
  200. package/src/components/__tests__/embedded-list-field.test.tsx +0 -531
  201. package/src/components/__tests__/multi-select-checkboxes.test.tsx +0 -162
  202. package/src/components/__tests__/related-list-section-empty-state.test.tsx +0 -143
  203. package/src/components/__tests__/related-list-section.test.tsx +0 -1733
  204. package/src/components/__tests__/render-edit-action-button.test.tsx +0 -205
  205. package/src/components/__tests__/render-edit-hidden-section.test.tsx +0 -128
  206. package/src/components/__tests__/render-edit-logic.test.ts +0 -314
  207. package/src/components/__tests__/render-edit-screen-description.test.tsx +0 -152
  208. package/src/components/__tests__/render-edit-submit-actions.test.tsx +0 -460
  209. package/src/components/__tests__/render-field-app-locale.test.tsx +0 -142
  210. package/src/components/__tests__/render-field-enum-option.test.tsx +0 -99
  211. package/src/components/__tests__/render-field-icon-derivation.test.tsx +0 -119
  212. package/src/components/__tests__/render-field-json-format.test.tsx +0 -164
  213. package/src/components/__tests__/render-field-money-embedded-list-consistency.test.tsx +0 -212
  214. package/src/components/__tests__/render-field-money-roundtrip.test.tsx +0 -217
  215. package/src/components/__tests__/render-field-number-grouping.test.tsx +0 -76
  216. package/src/components/__tests__/render-field-number-unit.test.tsx +0 -227
  217. package/src/components/__tests__/render-field-password-format.test.tsx +0 -101
  218. package/src/components/__tests__/render-field-select-display.test.tsx +0 -134
  219. package/src/components/__tests__/render-field-unit-format.test.tsx +0 -107
  220. package/src/components/__tests__/render-field-unsupported-types.test.tsx +0 -195
  221. package/src/components/__tests__/render-list-toolbar-icon-only.test.tsx +0 -146
  222. package/src/components/__tests__/write-form-section.test.tsx +0 -375
  223. package/src/components/embedded-list-field.tsx +0 -354
  224. package/src/components/grid-cell-for-field.tsx +0 -88
  225. package/src/components/multi-select-checkboxes.tsx +0 -93
  226. package/src/components/reference-create-dialog.tsx +0 -91
  227. package/src/components/related-list-section.tsx +0 -491
  228. package/src/components/render-edit-action-button.tsx +0 -109
  229. package/src/components/render-edit-logic.ts +0 -85
  230. package/src/components/render-edit-types.ts +0 -226
  231. package/src/components/render-edit.tsx +0 -1871
  232. package/src/components/render-field.tsx +0 -951
  233. package/src/components/render-list.tsx +0 -593
  234. package/src/components/write-form-section.tsx +0 -198
  235. package/src/context/draft-storage-context.tsx +0 -39
  236. package/src/hooks/__tests__/use-ai-text.test.tsx +0 -177
  237. package/src/hooks/__tests__/use-disclosure.test.tsx +0 -25
  238. package/src/hooks/__tests__/use-form.test.tsx +0 -65
  239. package/src/hooks/__tests__/use-mutation.test.tsx +0 -77
  240. package/src/hooks/__tests__/use-query.test.tsx +0 -149
  241. package/src/hooks/__tests__/use-stream-handler.test.tsx +0 -107
  242. package/src/hooks/use-ai-text.ts +0 -172
  243. package/src/hooks/use-disclosure.ts +0 -20
  244. package/src/hooks/use-form.ts +0 -78
  245. package/src/hooks/use-list-url-state.ts +0 -207
  246. package/src/hooks/use-mutation.ts +0 -61
  247. package/src/hooks/use-query.ts +0 -166
  248. package/src/hooks/use-reference-lookup.ts +0 -76
  249. package/src/hooks/use-store.ts +0 -48
  250. package/src/hooks/use-stream-handler.ts +0 -134
  251. package/src/i18n-defaults.ts +0 -240
  252. package/src/i18n.tsx +0 -286
  253. package/src/index.ts +0 -279
  254. package/src/primitives.tsx +0 -1567
  255. package/src/screen-fills-height.ts +0 -3
  256. package/src/share-links.ts +0 -12
  257. package/src/sort-by-accessor.ts +0 -20
  258. package/src/sse/live-events.tsx +0 -56
  259. package/src/tokens.tsx +0 -170
@@ -1,212 +0,0 @@
1
- // kumiko-framework#1972: one form, two money contracts. A top-level money
2
- // field's write payload is `{amount, currency}` in MAJOR units
3
- // (render-field.tsx's moneyPayload) while an embedded-list row's money
4
- // sub-field is a bare number in MINOR units (embedded-list-field.tsx passes
5
- // the widget's minor-unit value straight through — currency lives on the
6
- // head aggregate, not the row). Both are correct in isolation; the risk is
7
- // a consumer that reads the combined payload and compares the two amounts
8
- // without converting units first (exactly the reported solon incident:
9
- // `linesSum:10000` vs `netTotal:100`).
10
- //
11
- // This test drives BOTH real conversion paths — RenderField's moneyPayload
12
- // for the top-level field, EmbeddedListField's handleCellChange for the
13
- // list rows — with a crooked amount (not a round number, which can hide a
14
- // factor-of-100 bug by coincidence), assembles the resulting wire payload
15
- // exactly like a real form submission would, and validates it against the
16
- // real server schema (buildInsertSchema + EmbeddedFieldDef.totalsMatch). No
17
- // hand-built wire-form fixture stands in for either conversion function.
18
-
19
- import { describe, expect, test } from "bun:test";
20
- import {
21
- buildInsertSchema,
22
- createEmbeddedListField,
23
- createEntity,
24
- createMoneyField,
25
- } from "@cosmicdrift/kumiko-framework/engine";
26
- import type {
27
- EntityDefinition,
28
- EntityEditScreenDefinition,
29
- } from "@cosmicdrift/kumiko-framework/ui-types";
30
- import { computeEditViewModel, type EditFieldViewModel } from "@cosmicdrift/kumiko-headless";
31
- import { render } from "@testing-library/react";
32
- import type { ComponentType, ReactNode } from "react";
33
- import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
34
- import {
35
- type CorePrimitives,
36
- type EmbeddedListInputProps,
37
- type InputProps,
38
- PrimitivesProvider,
39
- } from "../../primitives";
40
- import { RenderField } from "../render-field";
41
-
42
- let capturedInput: InputProps | undefined;
43
- let capturedList: EmbeddedListInputProps | undefined;
44
- let lastTotalPayload: unknown;
45
- let lastLinesPayload: unknown;
46
-
47
- const captureInput: ComponentType<InputProps> = (props) => {
48
- capturedInput = props;
49
- return null;
50
- };
51
- const captureEmbeddedListInput: ComponentType<EmbeddedListInputProps> = (props) => {
52
- capturedList = props;
53
- return null;
54
- };
55
- const onTotalChange = (v: unknown): void => {
56
- lastTotalPayload = v;
57
- };
58
- const onLinesChange = (v: unknown): void => {
59
- lastLinesPayload = v;
60
- };
61
- const noop = (): ReactNode => null;
62
- const passChildren = ({ children }: { readonly children?: ReactNode }): ReactNode => children;
63
-
64
- const testPrimitives: CorePrimitives = {
65
- Button: noop,
66
- Banner: noop,
67
- Field: passChildren,
68
- Input: captureInput,
69
- EmbeddedListInput: captureEmbeddedListInput,
70
- DataTable: noop,
71
- Form: passChildren,
72
- Section: passChildren,
73
- Card: passChildren,
74
- Grid: passChildren,
75
- GridCell: passChildren,
76
- Text: passChildren,
77
- Heading: noop,
78
- Dialog: noop,
79
- Modal: noop,
80
- Lightbox: noop,
81
- ConfigSourceBadge: noop,
82
- ConfigCascadeView: noop,
83
- Link: noop,
84
- };
85
-
86
- function invoiceEntity(): EntityDefinition {
87
- return createEntity({
88
- table: "Invoices",
89
- fields: {
90
- total: createMoneyField({ required: true }),
91
- lines: createEmbeddedListField(
92
- { amount: { type: "money", required: true } },
93
- { totalsMatch: { amount: "total" } },
94
- ),
95
- },
96
- defaultCurrency: "EUR",
97
- });
98
- }
99
-
100
- function invoiceScreen(): EntityEditScreenDefinition {
101
- return {
102
- id: "invoice-edit",
103
- type: "entityEdit",
104
- entity: "invoice",
105
- layout: { sections: [{ columns: 1, fields: ["total", "lines"] }] },
106
- } as EntityEditScreenDefinition;
107
- }
108
-
109
- function computeInvoiceFields(
110
- entity: EntityDefinition,
111
- values: Record<string, unknown>,
112
- ): { readonly total: EditFieldViewModel; readonly lines: EditFieldViewModel } {
113
- const vm = computeEditViewModel({
114
- screen: invoiceScreen(),
115
- entity,
116
- values,
117
- translate: (key) => key,
118
- featureName: "invoices",
119
- });
120
- const section = vm.sections[0];
121
- if (section === undefined || section.kind !== "fields") {
122
- throw new Error("expected a fields section");
123
- }
124
- const total = section.fields.find((f) => f.field === "total");
125
- const lines = section.fields.find((f) => f.field === "lines");
126
- if (total === undefined || lines === undefined) {
127
- throw new Error("expected total + lines fields");
128
- }
129
- return { total, lines };
130
- }
131
-
132
- // Mounts both fields fresh against the given values and re-captures their
133
- // widget props. Called again after every simulated edit — a real controlled
134
- // form re-renders with the previous onChange result fed back in the same
135
- // way, so this mirrors that instead of mutating one long-lived tree.
136
- function renderInvoiceForm(entity: EntityDefinition, values: Record<string, unknown>): void {
137
- capturedInput = undefined;
138
- capturedList = undefined;
139
- const { total, lines } = computeInvoiceFields(entity, values);
140
- render(
141
- <LocaleProvider resolver={createStaticLocaleResolver({ locale: "de-DE" })}>
142
- <PrimitivesProvider value={testPrimitives}>
143
- <RenderField field={total} onChange={onTotalChange} />
144
- <RenderField field={lines} onChange={onLinesChange} featureName="invoices" />
145
- </PrimitivesProvider>
146
- </LocaleProvider>,
147
- );
148
- if (capturedInput === undefined) {
149
- throw new Error("total field did not render an Input");
150
- }
151
- if (capturedList === undefined) {
152
- throw new Error("lines field did not render an EmbeddedListInput");
153
- }
154
- }
155
-
156
- // Mirrors what MoneyInput actually emits on blur: the widget's minor-units
157
- // value. `InputProps` is a discriminated union keyed by `kind`, so the
158
- // money-typed onChange only narrows to a callable signature once `kind` is
159
- // checked.
160
- function emitMoneyChange(minorUnits: number): void {
161
- if (capturedInput === undefined) throw new Error("no Input captured");
162
- if (capturedInput.kind !== "money") throw new Error("expected a money Input");
163
- capturedInput.onChange(minorUnits);
164
- }
165
-
166
- describe("RenderField money contract consistency across top-level + embedded-list (kumiko-framework#1972)", () => {
167
- test("a crooked invoice total (1234.56 EUR) whose line amounts sum to the same value validates end to end", () => {
168
- const entity = invoiceEntity();
169
-
170
- // Two rows, edited one at a time via the widget's real onCellChange
171
- // (minor units) — each edit re-renders with the previous result fed
172
- // back in as `lines`, exactly like a real controlled form.
173
- renderInvoiceForm(entity, { lines: [{}, {}] });
174
- capturedList?.onCellChange(0, "amount", 100_000); // 1000.00 EUR
175
- expect(lastLinesPayload).toEqual([{ amount: 100_000 }, {}]);
176
-
177
- renderInvoiceForm(entity, { lines: lastLinesPayload as Record<string, unknown>[] });
178
- capturedList?.onCellChange(1, "amount", 23_456); // 234.56 EUR
179
- expect(lastLinesPayload).toEqual([{ amount: 100_000 }, { amount: 23_456 }]);
180
-
181
- // Top-level total: the widget (MoneyInput) emits minor units on blur;
182
- // RenderField's moneyPayload converts that to the wire's major-unit
183
- // object.
184
- emitMoneyChange(123_456); // 1234.56 EUR, minor units
185
- expect(lastTotalPayload).toEqual({ amount: 1234.56, currency: "EUR" });
186
-
187
- const result = buildInsertSchema(entity).safeParse({
188
- total: lastTotalPayload,
189
- lines: lastLinesPayload,
190
- });
191
- expect(result.success).toBe(true);
192
- });
193
-
194
- test("a genuinely wrong total (mismatched, not just a unit mixup) is still rejected", () => {
195
- const entity = invoiceEntity();
196
- renderInvoiceForm(entity, { lines: [{}] });
197
- capturedList?.onCellChange(0, "amount", 100_000); // 1000.00 EUR
198
- expect(lastLinesPayload).toEqual([{ amount: 100_000 }]);
199
-
200
- emitMoneyChange(50_000); // user enters 500.00 EUR — doesn't match the line
201
- expect(lastTotalPayload).toEqual({ amount: 500, currency: "EUR" });
202
-
203
- const result = buildInsertSchema(entity).safeParse({
204
- total: lastTotalPayload,
205
- lines: lastLinesPayload,
206
- });
207
- expect(result.success).toBe(false);
208
- if (!result.success) {
209
- expect(result.error.issues.some((issue) => issue.path.join(".") === "lines")).toBe(true);
210
- }
211
- });
212
- });
@@ -1,217 +0,0 @@
1
- // kumiko-framework#1923: money didn't round-trip on the auto-wired
2
- // entityEdit path — create sent a bare number against the server's
3
- // `{amount, currency}` schema, update rendered the rehydrated read value as
4
- // NaN, and a naive fix would have been 100x off (minor vs. major units).
5
- //
6
- // This test walks the whole chain with real functions, no mocks:
7
- // computeEditViewModel (headless) → RenderField → simulated widget
8
- // onChange → buildInsertSchema/buildUpdateSchema (framework/engine). The
9
- // widget's own minor-units math (MoneyInput) is pinned separately in
10
- // packages/renderer-web/src/primitives/__tests__/money-input.test.tsx —
11
- // this test starts one step in, at the value RenderField hands to/from the
12
- // widget contract (`number | ""` minor units).
13
-
14
- import { describe, expect, test } from "bun:test";
15
- import { rehydrateMoney } from "@cosmicdrift/kumiko-framework/db";
16
- import { buildInsertSchema, buildUpdateSchema } from "@cosmicdrift/kumiko-framework/engine";
17
- import type {
18
- EntityDefinition,
19
- EntityEditScreenDefinition,
20
- } from "@cosmicdrift/kumiko-framework/ui-types";
21
- import { computeEditViewModel, type EditFieldViewModel } from "@cosmicdrift/kumiko-headless";
22
- import { render } from "@testing-library/react";
23
- import type { ComponentType, ReactNode } from "react";
24
- import { buildInitialValues } from "../../app/kumiko-screen";
25
- import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
26
- import { type CorePrimitives, type InputProps, PrimitivesProvider } from "../../primitives";
27
- import { RenderField } from "../render-field";
28
-
29
- let captured: InputProps | undefined;
30
- const captureInput: ComponentType<InputProps> = (props) => {
31
- captured = props;
32
- return null;
33
- };
34
- const noop = (): ReactNode => null;
35
- const passChildren = ({ children }: { readonly children?: ReactNode }): ReactNode => children;
36
-
37
- const testPrimitives: CorePrimitives = {
38
- Button: noop,
39
- Banner: noop,
40
- Field: passChildren,
41
- Input: captureInput,
42
- DataTable: noop,
43
- Form: noop,
44
- Section: noop,
45
- Card: noop,
46
- Grid: noop,
47
- GridCell: noop,
48
- Text: noop,
49
- Heading: noop,
50
- Dialog: noop,
51
- Modal: noop,
52
- Lightbox: noop,
53
- ConfigSourceBadge: noop,
54
- ConfigCascadeView: noop,
55
- Link: noop,
56
- };
57
-
58
- function buildEntity(defaultCurrency: string): EntityDefinition {
59
- return {
60
- fields: {
61
- price: { type: "money", required: true },
62
- },
63
- defaultCurrency,
64
- } as EntityDefinition;
65
- }
66
-
67
- function buildScreen(): EntityEditScreenDefinition {
68
- return {
69
- id: "product-edit",
70
- type: "entityEdit",
71
- entity: "product",
72
- layout: { sections: [{ columns: 1, fields: ["price"] }] },
73
- } as EntityEditScreenDefinition;
74
- }
75
-
76
- function priceField(entity: EntityDefinition, values: Record<string, unknown>): EditFieldViewModel {
77
- const vm = computeEditViewModel({
78
- screen: buildScreen(),
79
- entity,
80
- values,
81
- translate: (key) => key,
82
- featureName: "shop",
83
- });
84
- const section = vm.sections[0];
85
- if (section === undefined || section.kind !== "fields") {
86
- throw new Error("expected a fields section");
87
- }
88
- const field = section.fields[0];
89
- if (field === undefined) throw new Error("expected a price field");
90
- return field;
91
- }
92
-
93
- function renderMoneyField(
94
- entity: EntityDefinition,
95
- values: Record<string, unknown>,
96
- onChange: (v: unknown) => void,
97
- ): InputProps {
98
- captured = undefined;
99
- render(
100
- <LocaleProvider resolver={createStaticLocaleResolver({ locale: "de-DE" })}>
101
- <PrimitivesProvider value={testPrimitives}>
102
- <RenderField field={priceField(entity, values)} onChange={onChange} />
103
- </PrimitivesProvider>
104
- </LocaleProvider>,
105
- );
106
- if (captured === undefined) throw new Error("money field did not render an Input");
107
- return captured;
108
- }
109
-
110
- describe("RenderField money round-trip (kumiko-framework#1923)", () => {
111
- test("create: untouched required field, buildInitialValues default validates against buildInsertSchema", () => {
112
- const entity = buildEntity("USD");
113
- // Same default shape kumiko-screen.tsx's buildInitialValues produces
114
- // for a money field once a defaultCurrency is threaded through.
115
- const initialPrice = { amount: 0, currency: "USD" };
116
- const result = buildInsertSchema(entity).safeParse({ price: initialPrice });
117
- expect(result.success).toBe(true);
118
- });
119
-
120
- test("create: buildInitialValues default renders as 0.00 in the widget, not empty", () => {
121
- // Regression: the widget was tested with `values: {}` (no field.value at
122
- // all), which produces "" — but the real entityEdit create path always
123
- // feeds buildInitialValues(fields, defaultCurrency), which yields
124
- // {amount: 0, currency} (kumiko-screen.tsx buildInitialValues).
125
- const entity = buildEntity("USD");
126
- const initialValues = buildInitialValues(entity.fields, "USD");
127
- const field = renderMoneyField(entity, initialValues, () => {});
128
- expect(field.kind).toBe("money");
129
- if (field.kind !== "money") return;
130
- expect(field.value).toBe(0);
131
- });
132
-
133
- test("create: user-entered amount becomes a payload that validates against buildInsertSchema", () => {
134
- const entity = buildEntity("USD");
135
- let payload: unknown;
136
- const field = renderMoneyField(entity, {}, (v) => {
137
- payload = v;
138
- });
139
- expect(field.kind).toBe("money");
140
- if (field.kind !== "money") return;
141
- expect(field.value).toBe(""); // untouched — empty widget state, not NaN/0
142
- field.onChange(1299); // widget emits minor units: 12.99 USD
143
- expect(payload).toEqual({ amount: 12.99, currency: "USD" });
144
- const result = buildInsertSchema(entity).safeParse({ price: payload });
145
- expect(result.success).toBe(true);
146
- });
147
-
148
- test("update: server-rehydrated value renders as a real number, not NaN (regression)", () => {
149
- const entity = buildEntity("USD");
150
- const row = { price: 1299, priceCurrency: "USD" };
151
- const record = rehydrateMoney(row, entity);
152
- const field = renderMoneyField(entity, record, () => {});
153
- expect(field.kind).toBe("money");
154
- if (field.kind !== "money") return;
155
- expect(field.value).toBe(1299);
156
- expect(Number.isNaN(field.value)).toBe(false);
157
- });
158
-
159
- test("update: edited amount becomes a payload that validates against buildUpdateSchema", () => {
160
- const entity = buildEntity("USD");
161
- const row = { price: 1299, priceCurrency: "USD" };
162
- const record = rehydrateMoney(row, entity);
163
- let payload: unknown;
164
- const field = renderMoneyField(entity, record, (v) => {
165
- payload = v;
166
- });
167
- if (field.kind !== "money") throw new Error("expected money kind");
168
- field.onChange(1500); // user edits to 15.00
169
- expect(payload).toEqual({ amount: 15, currency: "USD" });
170
- const result = buildUpdateSchema(entity).safeParse({ price: payload });
171
- expect(result.success).toBe(true);
172
- });
173
-
174
- test("JPY (zero-decimal currency): amountMinor is not read, so the widget value is not 100x off", () => {
175
- const entity = buildEntity("JPY");
176
- // DB stores minor units at the framework's flat MINOR_UNIT_SCALE=100,
177
- // so ¥500 is row-stored as 50000 — rehydrateMoney's amountMinor mirrors
178
- // that scale, which disagrees with JPY's real 0 decimal places. Reading
179
- // amountMinor here would render 50000 instead of 500.
180
- const row = { price: 50000, priceCurrency: "JPY" };
181
- const record = rehydrateMoney(row, entity);
182
- let payload: unknown;
183
- const field = renderMoneyField(entity, record, (v) => {
184
- payload = v;
185
- });
186
- if (field.kind !== "money") throw new Error("expected money kind");
187
- expect(field.value).toBe(500);
188
- field.onChange(500); // user submits the same amount unchanged
189
- expect(payload).toEqual({ amount: 500, currency: "JPY" });
190
- const result = buildInsertSchema(entity).safeParse({ price: payload });
191
- expect(result.success).toBe(true);
192
- expect(result.data?.["price"]).toEqual({ amount: 500, currency: "JPY" });
193
- });
194
-
195
- test("record currency differs from entity.defaultCurrency (#1930)", () => {
196
- const entity = buildEntity("EUR");
197
- let payload: unknown;
198
- const field = renderMoneyField(entity, { price: { amount: 500, currency: "JPY" } }, (v) => {
199
- payload = v;
200
- });
201
- if (field.kind !== "money") throw new Error("expected money kind");
202
- // Prefer the value's own currency over entity.defaultCurrency (EUR=2dp).
203
- expect(field.currency).toBe("JPY");
204
- expect(field.value).toBe(500);
205
- field.onChange(500);
206
- expect(payload).toEqual({ amount: 500, currency: "JPY" });
207
- });
208
-
209
- test("server schema strips an unexpected amountMinor key instead of rejecting the payload", () => {
210
- const entity = buildEntity("USD");
211
- const result = buildUpdateSchema(entity).safeParse({
212
- price: { amount: 12.99, currency: "USD", amountMinor: 1299 },
213
- });
214
- expect(result.success).toBe(true);
215
- expect(result.data?.["price"]).toEqual({ amount: 12.99, currency: "USD" });
216
- });
217
- });
@@ -1,76 +0,0 @@
1
- // Read-only number display (`field.readOnly: true`, no own `renderer`) goes
2
- // through readOnlyDisplayText's own Intl.NumberFormat call, independent of
3
- // the headless `applyFormatSpec` path already covered by
4
- // format.test.tsx — `field.grouping` gates it the same way (fw#3234).
5
-
6
- import { describe, expect, test } from "bun:test";
7
- import type { EditFieldViewModel } from "@cosmicdrift/kumiko-headless";
8
- import { cleanup, render, screen } from "@testing-library/react";
9
- import type { ReactNode } from "react";
10
- import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
11
- import { type CorePrimitives, PrimitivesProvider, type TextProps } from "../../primitives";
12
- import { RenderField } from "../render-field";
13
-
14
- const noop = (): ReactNode => null;
15
- const passChildren = ({ children }: { readonly children?: ReactNode }): ReactNode => children;
16
- const DefaultText = ({ testId, children }: TextProps): ReactNode => (
17
- <span data-testid={testId}>{children}</span>
18
- );
19
-
20
- const testPrimitives: CorePrimitives = {
21
- Button: noop,
22
- Banner: noop,
23
- Field: passChildren,
24
- Input: noop,
25
- DataTable: noop,
26
- Form: noop,
27
- Section: noop,
28
- Card: noop,
29
- Grid: noop,
30
- GridCell: noop,
31
- Text: DefaultText,
32
- Heading: noop,
33
- Dialog: noop,
34
- Modal: noop,
35
- Lightbox: noop,
36
- ConfigSourceBadge: noop,
37
- ConfigCascadeView: noop,
38
- Link: noop,
39
- };
40
-
41
- function numberField(grouping: boolean | undefined): EditFieldViewModel {
42
- return {
43
- field: "population",
44
- label: "Population",
45
- type: "number",
46
- value: 2021,
47
- visible: true,
48
- readOnly: true,
49
- required: false,
50
- ...(grouping !== undefined && { grouping }),
51
- };
52
- }
53
-
54
- function renderReadOnly(locale: string, grouping: boolean | undefined): string {
55
- cleanup();
56
- render(
57
- <LocaleProvider resolver={createStaticLocaleResolver({ locale })}>
58
- <PrimitivesProvider value={testPrimitives}>
59
- <RenderField field={numberField(grouping)} onChange={() => {}} valueDisplay="text" />
60
- </PrimitivesProvider>
61
- </LocaleProvider>,
62
- );
63
- return screen.getByTestId("field-value-population").textContent ?? "";
64
- }
65
-
66
- describe("RenderField — read-only number grouping (fw#3234)", () => {
67
- test("groups thousands by default, locale-aware", () => {
68
- expect(renderReadOnly("en-US", undefined)).toBe("2,021");
69
- expect(renderReadOnly("de-DE", undefined)).toBe("2.021");
70
- });
71
-
72
- test("grouping: false renders the digits without a thousands separator", () => {
73
- expect(renderReadOnly("en-US", false)).toBe("2021");
74
- expect(renderReadOnly("de-DE", false)).toBe("2021");
75
- });
76
- });