@cosmicdrift/kumiko-renderer 0.327.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.
- package/dist/app/action-form-shim.d.ts +14 -0
- package/{src/app/action-form-shim.ts → dist/app/action-form-shim.js} +26 -44
- package/dist/app/app-features-context.d.ts +8 -0
- package/dist/app/app-features-context.js +16 -0
- package/dist/app/column-renderers.d.ts +30 -0
- package/dist/app/column-renderers.js +34 -0
- package/dist/app/config-edit-shim.d.ts +11 -0
- package/{src/app/config-edit-shim.ts → dist/app/config-edit-shim.js} +12 -25
- package/dist/app/content-editors.d.ts +38 -0
- package/dist/app/content-editors.js +26 -0
- package/dist/app/content-preview.d.ts +11 -0
- package/dist/app/content-preview.js +30 -0
- package/dist/app/custom-screens.d.ts +11 -0
- package/dist/app/custom-screens.js +20 -0
- package/dist/app/dashboard-body.d.ts +16 -0
- package/dist/app/dashboard-body.js +11 -0
- package/dist/app/extension-form-submit.d.ts +32 -0
- package/dist/app/extension-form-submit.js +73 -0
- package/dist/app/extension-sections.d.ts +72 -0
- package/dist/app/extension-sections.js +34 -0
- package/dist/app/feature-schema.d.ts +4 -0
- package/{src/app/feature-schema.ts → dist/app/feature-schema.js} +12 -22
- package/dist/app/form-schema.d.ts +4 -0
- package/dist/app/form-schema.js +85 -0
- package/dist/app/kumiko-screen.d.ts +43 -0
- package/dist/app/kumiko-screen.js +2206 -0
- package/dist/app/layout-fields.d.ts +3 -0
- package/dist/app/layout-fields.js +20 -0
- package/dist/app/list-facets.d.ts +48 -0
- package/dist/app/list-facets.js +185 -0
- package/dist/app/nav.d.ts +61 -0
- package/dist/app/nav.js +139 -0
- package/dist/app/projection-detail-shim.d.ts +8 -0
- package/dist/app/projection-detail-shim.js +71 -0
- package/dist/app/projection-list-shim.d.ts +13 -0
- package/{src/app/projection-list-shim.ts → dist/app/projection-list-shim.js} +26 -38
- package/dist/app/qn.d.ts +7 -0
- package/{src/app/qn.ts → dist/app/qn.js} +9 -11
- package/dist/app/qualify-screen-id.d.ts +2 -0
- package/{src/app/qualify-screen-id.ts → dist/app/qualify-screen-id.js} +5 -6
- package/dist/app/reference-facet-bridge.d.ts +10 -0
- package/dist/app/reference-facet-bridge.js +22 -0
- package/dist/app/return-to.d.ts +31 -0
- package/dist/app/return-to.js +188 -0
- package/dist/app/row-actions.d.ts +77 -0
- package/dist/app/row-actions.js +416 -0
- package/dist/app/screen-access.d.ts +8 -0
- package/dist/app/screen-access.js +48 -0
- package/dist/app/secret-mint-body.d.ts +10 -0
- package/dist/app/secret-mint-body.js +140 -0
- package/dist/app/secrets-edit-body.d.ts +8 -0
- package/dist/app/secrets-edit-body.js +76 -0
- package/dist/app/use-embedded-screen.d.ts +6 -0
- package/dist/app/use-embedded-screen.js +19 -0
- package/dist/app/variable-chips.d.ts +7 -0
- package/dist/app/variable-chips.js +16 -0
- package/dist/app/write-failed-error.d.ts +6 -0
- package/dist/app/write-failed-error.js +21 -0
- package/dist/components/embedded-list-field.d.ts +10 -0
- package/dist/components/embedded-list-field.js +259 -0
- package/dist/components/grid-cell-for-field.d.ts +25 -0
- package/dist/components/grid-cell-for-field.js +31 -0
- package/dist/components/multi-select-checkboxes.d.ts +14 -0
- package/dist/components/multi-select-checkboxes.js +36 -0
- package/dist/components/reference-create-dialog.d.ts +13 -0
- package/dist/components/reference-create-dialog.js +46 -0
- package/dist/components/related-list-section.d.ts +25 -0
- package/dist/components/related-list-section.js +273 -0
- package/dist/components/render-edit-action-button.d.ts +22 -0
- package/dist/components/render-edit-action-button.js +43 -0
- package/dist/components/render-edit-logic.d.ts +6 -0
- package/dist/components/render-edit-logic.js +63 -0
- package/dist/components/render-edit-types.d.ts +209 -0
- package/dist/components/render-edit-types.js +1 -0
- package/dist/components/render-edit.d.ts +6 -0
- package/dist/components/render-edit.js +1169 -0
- package/dist/components/render-field.d.ts +38 -0
- package/dist/components/render-field.js +544 -0
- package/dist/components/render-list.d.ts +117 -0
- package/dist/components/render-list.js +232 -0
- package/dist/components/write-form-section.d.ts +17 -0
- package/dist/components/write-form-section.js +104 -0
- package/dist/context/dispatcher-context.d.ts +10 -0
- package/{src/context/dispatcher-context.tsx → dist/context/dispatcher-context.js} +16 -29
- package/dist/context/draft-storage-context.d.ts +12 -0
- package/dist/context/draft-storage-context.js +18 -0
- package/dist/context/user-roles-context.d.ts +7 -0
- package/{src/context/user-roles-context.tsx → dist/context/user-roles-context.js} +7 -15
- package/dist/format-when.d.ts +1 -0
- package/{src/format-when.ts → dist/format-when.js} +7 -7
- package/dist/hooks/reference-limits.d.ts +3 -0
- package/{src/hooks/reference-limits.ts → dist/hooks/reference-limits.js} +0 -1
- package/dist/hooks/use-ai-text.d.ts +52 -0
- package/dist/hooks/use-ai-text.js +90 -0
- package/dist/hooks/use-disclosure.d.ts +7 -0
- package/dist/hooks/use-disclosure.js +8 -0
- package/dist/hooks/use-form.d.ts +7 -0
- package/dist/hooks/use-form.js +36 -0
- package/dist/hooks/use-list-url-state.d.ts +43 -0
- package/dist/hooks/use-list-url-state.js +131 -0
- package/dist/hooks/use-mutation.d.ts +9 -0
- package/dist/hooks/use-mutation.js +37 -0
- package/dist/hooks/use-query.d.ts +14 -0
- package/dist/hooks/use-query.js +118 -0
- package/dist/hooks/use-reference-lookup.d.ts +14 -0
- package/dist/hooks/use-reference-lookup.js +63 -0
- package/dist/hooks/use-store.d.ts +3 -0
- package/dist/hooks/use-store.js +38 -0
- package/dist/hooks/use-stream-handler.d.ts +14 -0
- package/dist/hooks/use-stream-handler.js +93 -0
- package/dist/i18n-defaults.d.ts +2 -0
- package/dist/i18n-defaults.js +201 -0
- package/dist/i18n.d.ts +64 -0
- package/dist/i18n.js +166 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.js +55 -0
- package/dist/inside-drawer.d.ts +2 -0
- package/dist/inside-drawer.js +9 -0
- package/dist/page-header-slot.d.ts +2 -0
- package/dist/page-header-slot.js +9 -0
- package/dist/primitives.d.ts +1351 -0
- package/dist/primitives.js +73 -0
- package/dist/screen-fills-height.d.ts +3 -0
- package/dist/screen-fills-height.js +3 -0
- package/dist/share-links.d.ts +4 -0
- package/dist/share-links.js +11 -0
- package/dist/sort-by-accessor.d.ts +5 -0
- package/dist/sort-by-accessor.js +16 -0
- package/dist/sse/live-events.d.ts +23 -0
- package/dist/sse/live-events.js +28 -0
- package/dist/tokens.d.ts +83 -0
- package/dist/tokens.js +80 -0
- package/package.json +16 -9
- package/src/changes.json +52 -0
- package/src/__tests__/error-i18n-defaults.test.ts +0 -82
- package/src/__tests__/format-when.test.ts +0 -12
- package/src/__tests__/i18n.test.tsx +0 -283
- package/src/__tests__/merge-search-params-into-initial.test.ts +0 -525
- package/src/__tests__/qn.test.ts +0 -83
- package/src/__tests__/share-links.test.ts +0 -26
- package/src/__tests__/sort-by-accessor.test.ts +0 -48
- package/src/__tests__/stringify-nav-params.test.ts +0 -33
- package/src/__tests__/use-list-url-state.test.tsx +0 -250
- package/src/app/__tests__/action-form-field-labels.test.tsx +0 -92
- package/src/app/__tests__/action-form-shim.test.ts +0 -137
- package/src/app/__tests__/build-list-query-payload.test.ts +0 -107
- package/src/app/__tests__/config-edit-shim.test.ts +0 -57
- package/src/app/__tests__/content-editors.test.tsx +0 -128
- package/src/app/__tests__/content-preview.test.tsx +0 -184
- package/src/app/__tests__/entity-edit-create-search-params.test.tsx +0 -236
- package/src/app/__tests__/entity-edit-jsonb-embedded-submit.test.tsx +0 -239
- package/src/app/__tests__/entity-list-create-singleton.test.tsx +0 -128
- package/src/app/__tests__/entity-list-row-action-entity-target.test.tsx +0 -424
- package/src/app/__tests__/entity-list-row-action-icon.test.tsx +0 -248
- package/src/app/__tests__/entity-list-row-action-kumiko-actions-view.test.tsx +0 -162
- package/src/app/__tests__/entity-list-row-action-refetch.test.tsx +0 -401
- package/src/app/__tests__/entity-list-screen-padding.test.tsx +0 -111
- package/src/app/__tests__/feature-schema.test.ts +0 -40
- package/src/app/__tests__/form-schema.test.ts +0 -247
- package/src/app/__tests__/list-daterange-facet.test.ts +0 -165
- package/src/app/__tests__/list-filter-facets.test.tsx +0 -521
- package/src/app/__tests__/nav.test.ts +0 -198
- package/src/app/__tests__/projection-detail-actions.test.tsx +0 -719
- package/src/app/__tests__/projection-detail-reference-field.test.tsx +0 -165
- package/src/app/__tests__/projection-detail-shim.test.ts +0 -188
- package/src/app/__tests__/projection-detail-singleton.test.tsx +0 -191
- package/src/app/__tests__/projection-list-bad-shape.test.tsx +0 -152
- package/src/app/__tests__/projection-list-reference-column.test.tsx +0 -219
- package/src/app/__tests__/projection-list-search-sort.test.tsx +0 -212
- package/src/app/__tests__/qualified-names.test.ts +0 -37
- package/src/app/__tests__/return-to.test.ts +0 -396
- package/src/app/__tests__/row-actions-navigate-tab.test.ts +0 -223
- package/src/app/__tests__/row-default-edit-action.test.tsx +0 -324
- package/src/app/__tests__/screen-access-allows.test.ts +0 -34
- package/src/app/__tests__/secret-mint-body.test.tsx +0 -404
- package/src/app/__tests__/variable-chips.test.tsx +0 -70
- package/src/app/app-features-context.tsx +0 -25
- package/src/app/column-renderers.tsx +0 -68
- package/src/app/content-editors.tsx +0 -85
- package/src/app/content-preview.tsx +0 -67
- package/src/app/custom-screens.tsx +0 -29
- package/src/app/dashboard-body.tsx +0 -33
- package/src/app/extension-form-submit.tsx +0 -123
- package/src/app/extension-sections.tsx +0 -111
- package/src/app/form-schema.ts +0 -91
- package/src/app/kumiko-screen.tsx +0 -3614
- package/src/app/layout-fields.ts +0 -31
- package/src/app/list-facets.ts +0 -259
- package/src/app/nav.tsx +0 -258
- package/src/app/projection-detail-shim.ts +0 -85
- package/src/app/reference-facet-bridge.tsx +0 -58
- package/src/app/return-to.tsx +0 -234
- package/src/app/row-actions.ts +0 -639
- package/src/app/screen-access.ts +0 -63
- package/src/app/secret-mint-body.tsx +0 -259
- package/src/app/secrets-edit-body.tsx +0 -194
- package/src/app/use-embedded-screen.ts +0 -27
- package/src/app/variable-chips.tsx +0 -39
- package/src/app/write-failed-error.ts +0 -27
- package/src/components/__tests__/embedded-list-field.test.tsx +0 -531
- package/src/components/__tests__/multi-select-checkboxes.test.tsx +0 -162
- package/src/components/__tests__/related-list-section-empty-state.test.tsx +0 -143
- package/src/components/__tests__/related-list-section.test.tsx +0 -1669
- package/src/components/__tests__/render-edit-action-button.test.tsx +0 -205
- package/src/components/__tests__/render-edit-hidden-section.test.tsx +0 -128
- package/src/components/__tests__/render-edit-logic.test.ts +0 -314
- package/src/components/__tests__/render-edit-screen-description.test.tsx +0 -152
- package/src/components/__tests__/render-edit-submit-actions.test.tsx +0 -460
- package/src/components/__tests__/render-field-app-locale.test.tsx +0 -142
- package/src/components/__tests__/render-field-enum-option.test.tsx +0 -99
- package/src/components/__tests__/render-field-icon-derivation.test.tsx +0 -119
- package/src/components/__tests__/render-field-json-format.test.tsx +0 -164
- package/src/components/__tests__/render-field-money-embedded-list-consistency.test.tsx +0 -212
- package/src/components/__tests__/render-field-money-roundtrip.test.tsx +0 -217
- package/src/components/__tests__/render-field-number-grouping.test.tsx +0 -76
- package/src/components/__tests__/render-field-number-unit.test.tsx +0 -227
- package/src/components/__tests__/render-field-password-format.test.tsx +0 -101
- package/src/components/__tests__/render-field-select-display.test.tsx +0 -134
- package/src/components/__tests__/render-field-unit-format.test.tsx +0 -107
- package/src/components/__tests__/render-field-unsupported-types.test.tsx +0 -195
- package/src/components/__tests__/render-list-toolbar-icon-only.test.tsx +0 -146
- package/src/components/__tests__/write-form-section.test.tsx +0 -375
- package/src/components/embedded-list-field.tsx +0 -354
- package/src/components/grid-cell-for-field.tsx +0 -60
- package/src/components/multi-select-checkboxes.tsx +0 -93
- package/src/components/reference-create-dialog.tsx +0 -91
- package/src/components/related-list-section.tsx +0 -467
- package/src/components/render-edit-action-button.tsx +0 -106
- package/src/components/render-edit-logic.ts +0 -85
- package/src/components/render-edit-types.ts +0 -216
- package/src/components/render-edit.tsx +0 -1700
- package/src/components/render-field.tsx +0 -942
- package/src/components/render-list.tsx +0 -561
- package/src/components/write-form-section.tsx +0 -199
- package/src/context/draft-storage-context.tsx +0 -39
- package/src/hooks/__tests__/use-ai-text.test.tsx +0 -177
- package/src/hooks/__tests__/use-disclosure.test.tsx +0 -25
- package/src/hooks/__tests__/use-form.test.tsx +0 -65
- package/src/hooks/__tests__/use-mutation.test.tsx +0 -77
- package/src/hooks/__tests__/use-stream-handler.test.tsx +0 -107
- package/src/hooks/use-ai-text.ts +0 -172
- package/src/hooks/use-disclosure.ts +0 -20
- package/src/hooks/use-form.ts +0 -78
- package/src/hooks/use-list-url-state.ts +0 -171
- package/src/hooks/use-mutation.ts +0 -61
- package/src/hooks/use-query.ts +0 -132
- package/src/hooks/use-reference-lookup.ts +0 -76
- package/src/hooks/use-store.ts +0 -48
- package/src/hooks/use-stream-handler.ts +0 -134
- package/src/i18n-defaults.ts +0 -220
- package/src/i18n.tsx +0 -286
- package/src/index.ts +0 -272
- package/src/primitives.tsx +0 -1446
- package/src/share-links.ts +0 -12
- package/src/sort-by-accessor.ts +0 -20
- package/src/sse/live-events.tsx +0 -56
- 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
|
-
});
|