@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.
- package/dist/app/action-form-shim.d.ts +14 -0
- package/{src/app/action-form-shim.ts → dist/app/action-form-shim.js} +26 -46
- 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 -41
- 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/{src/inside-drawer.ts → dist/inside-drawer.js} +2 -5
- package/dist/page-header-slot.d.ts +2 -0
- package/{src/page-header-slot.ts → dist/page-header-slot.js} +2 -5
- 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/__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-fill-height.test.tsx +0 -118
- 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 -128
- 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 -3801
- 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 -86
- package/src/app/reference-facet-bridge.tsx +0 -58
- package/src/app/return-to.tsx +0 -234
- package/src/app/row-actions.ts +0 -640
- 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 -1733
- 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 -88
- 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 -491
- package/src/components/render-edit-action-button.tsx +0 -109
- package/src/components/render-edit-logic.ts +0 -85
- package/src/components/render-edit-types.ts +0 -226
- package/src/components/render-edit.tsx +0 -1871
- package/src/components/render-field.tsx +0 -951
- package/src/components/render-list.tsx +0 -593
- package/src/components/write-form-section.tsx +0 -198
- 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-query.test.tsx +0 -149
- 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 -207
- package/src/hooks/use-mutation.ts +0 -61
- package/src/hooks/use-query.ts +0 -166
- 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 -240
- package/src/i18n.tsx +0 -286
- package/src/index.ts +0 -279
- package/src/primitives.tsx +0 -1567
- package/src/screen-fills-height.ts +0 -3
- 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,531 +0,0 @@
|
|
|
1
|
-
// EmbeddedListField Tests — RTL + happy-dom, style follows
|
|
2
|
-
// render-field-app-locale.test.tsx: mount under real Locale/Dispatcher/
|
|
3
|
-
// Primitives providers, capture the props the (mocked) EmbeddedListInput
|
|
4
|
-
// primitive receives, then invoke its callbacks the way the real
|
|
5
|
-
// primitive would (a user clicking/typing) and assert the resulting
|
|
6
|
-
// onChange call.
|
|
7
|
-
|
|
8
|
-
import { describe, expect, test } from "bun:test";
|
|
9
|
-
import type { Dispatcher, EditFieldViewModel, FieldIssue } from "@cosmicdrift/kumiko-headless";
|
|
10
|
-
import { act, render, waitFor } from "@testing-library/react";
|
|
11
|
-
import type { ComponentType, ReactNode } from "react";
|
|
12
|
-
import { DispatcherProvider } from "../../context/dispatcher-context";
|
|
13
|
-
import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
|
|
14
|
-
import { kumikoDefaultTranslations } from "../../i18n-defaults";
|
|
15
|
-
import {
|
|
16
|
-
type CorePrimitives,
|
|
17
|
-
type EmbeddedListInputProps,
|
|
18
|
-
PrimitivesProvider,
|
|
19
|
-
} from "../../primitives";
|
|
20
|
-
import { EmbeddedListField } from "../embedded-list-field";
|
|
21
|
-
import { RenderField, type RenderFieldProps } from "../render-field";
|
|
22
|
-
|
|
23
|
-
let captured: EmbeddedListInputProps | undefined;
|
|
24
|
-
const captureEmbeddedListInput: ComponentType<EmbeddedListInputProps> = (props) => {
|
|
25
|
-
captured = {
|
|
26
|
-
...props,
|
|
27
|
-
onCellChange: (...args) => act(() => props.onCellChange(...args)),
|
|
28
|
-
onAddRow: () => act(() => props.onAddRow()),
|
|
29
|
-
onRemoveRow: (rowIndex) => act(() => props.onRemoveRow(rowIndex)),
|
|
30
|
-
onDuplicateRow: (rowIndex) => act(() => props.onDuplicateRow(rowIndex)),
|
|
31
|
-
onMoveRow: (fromIndex, toIndex) => act(() => props.onMoveRow(fromIndex, toIndex)),
|
|
32
|
-
...(props.onPasteCells !== undefined && {
|
|
33
|
-
onPasteCells: (...args: Parameters<NonNullable<EmbeddedListInputProps["onPasteCells"]>>) =>
|
|
34
|
-
act(() => props.onPasteCells?.(...args)),
|
|
35
|
-
}),
|
|
36
|
-
};
|
|
37
|
-
return null;
|
|
38
|
-
};
|
|
39
|
-
const noop = (): ReactNode => null;
|
|
40
|
-
const passChildren = ({ children }: { readonly children?: ReactNode }): ReactNode => children;
|
|
41
|
-
|
|
42
|
-
function testPrimitives(): CorePrimitives {
|
|
43
|
-
return {
|
|
44
|
-
Button: noop,
|
|
45
|
-
Banner: noop,
|
|
46
|
-
Field: passChildren,
|
|
47
|
-
Input: noop,
|
|
48
|
-
EmbeddedListInput: captureEmbeddedListInput,
|
|
49
|
-
DataTable: noop,
|
|
50
|
-
Form: passChildren,
|
|
51
|
-
Section: passChildren,
|
|
52
|
-
Card: passChildren,
|
|
53
|
-
Grid: passChildren,
|
|
54
|
-
GridCell: passChildren,
|
|
55
|
-
Text: passChildren,
|
|
56
|
-
Heading: noop,
|
|
57
|
-
Dialog: noop,
|
|
58
|
-
Modal: noop,
|
|
59
|
-
Lightbox: noop,
|
|
60
|
-
ConfigSourceBadge: noop,
|
|
61
|
-
ConfigCascadeView: noop,
|
|
62
|
-
Link: noop,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
type ProductRow = { readonly id: string; readonly name: string };
|
|
67
|
-
|
|
68
|
-
function stubDispatcher(productRows: readonly ProductRow[] = []): Dispatcher {
|
|
69
|
-
return {
|
|
70
|
-
write: (async () => ({ isSuccess: true, data: {} })) as unknown as Dispatcher["write"],
|
|
71
|
-
query: (async (type: string) => {
|
|
72
|
-
if (type === "invoices:query:product:list") {
|
|
73
|
-
return { isSuccess: true, data: { rows: productRows } };
|
|
74
|
-
}
|
|
75
|
-
return { isSuccess: true, data: { rows: [] } };
|
|
76
|
-
}) as unknown as Dispatcher["query"],
|
|
77
|
-
batch: (async () => ({ isSuccess: true, results: [] })) as unknown as Dispatcher["batch"],
|
|
78
|
-
statusStore: {
|
|
79
|
-
getState: () => "online",
|
|
80
|
-
subscribe: () => () => {},
|
|
81
|
-
} as unknown as Dispatcher["statusStore"],
|
|
82
|
-
async *stream() {},
|
|
83
|
-
pendingWrites: () => [],
|
|
84
|
-
pendingFiles: () => [],
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function invoiceLinesField(overrides: Partial<EditFieldViewModel> = {}): EditFieldViewModel {
|
|
89
|
-
return {
|
|
90
|
-
field: "lines",
|
|
91
|
-
label: "Lines",
|
|
92
|
-
type: "embedded",
|
|
93
|
-
value: [],
|
|
94
|
-
visible: true,
|
|
95
|
-
readOnly: false,
|
|
96
|
-
required: true,
|
|
97
|
-
embeddedListCells: [
|
|
98
|
-
{
|
|
99
|
-
field: "product",
|
|
100
|
-
label: "Product",
|
|
101
|
-
type: "reference",
|
|
102
|
-
required: true,
|
|
103
|
-
refEntity: "product",
|
|
104
|
-
refFeature: "invoices",
|
|
105
|
-
refLabelField: "name",
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
field: "unit",
|
|
109
|
-
label: "Unit",
|
|
110
|
-
type: "select",
|
|
111
|
-
required: true,
|
|
112
|
-
options: ["pcs", "hours", "kg"],
|
|
113
|
-
},
|
|
114
|
-
{ field: "quantity", label: "Qty", type: "number", required: true },
|
|
115
|
-
{ field: "unitPrice", label: "Unit Price", type: "money", required: true },
|
|
116
|
-
{ field: "amount", label: "Amount", type: "money", required: false },
|
|
117
|
-
],
|
|
118
|
-
embeddedListDerived: { amount: { op: "multiply", from: ["quantity", "unitPrice"] } },
|
|
119
|
-
embeddedListTotals: ["amount"],
|
|
120
|
-
embeddedListMinItems: 1,
|
|
121
|
-
embeddedListMaxItems: 5,
|
|
122
|
-
...overrides,
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
async function renderEmbeddedListField(
|
|
127
|
-
field: EditFieldViewModel,
|
|
128
|
-
onChange: (v: unknown) => void,
|
|
129
|
-
allIssues: Readonly<Record<string, readonly FieldIssue[]>> = {},
|
|
130
|
-
productRows: readonly ProductRow[] = [],
|
|
131
|
-
): Promise<void> {
|
|
132
|
-
captured = undefined;
|
|
133
|
-
render(
|
|
134
|
-
<LocaleProvider
|
|
135
|
-
resolver={createStaticLocaleResolver()}
|
|
136
|
-
fallbackBundles={[kumikoDefaultTranslations]}
|
|
137
|
-
>
|
|
138
|
-
<DispatcherProvider dispatcher={stubDispatcher(productRows)}>
|
|
139
|
-
<PrimitivesProvider value={testPrimitives()}>
|
|
140
|
-
<EmbeddedListField
|
|
141
|
-
field={field}
|
|
142
|
-
id="kumiko-edit-lines"
|
|
143
|
-
onChange={onChange}
|
|
144
|
-
allIssues={allIssues}
|
|
145
|
-
featureName="invoices"
|
|
146
|
-
/>
|
|
147
|
-
</PrimitivesProvider>
|
|
148
|
-
</DispatcherProvider>
|
|
149
|
-
</LocaleProvider>,
|
|
150
|
-
);
|
|
151
|
-
await act(async () => {});
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
describe("EmbeddedListField — cell change recomputes derived", () => {
|
|
155
|
-
test("changing quantity updates the cell and recomputes amount in the same row", async () => {
|
|
156
|
-
const rows = [{ product: "p1", unit: "pcs", quantity: 2, unitPrice: 500, amount: 1000 }];
|
|
157
|
-
let lastValue: unknown;
|
|
158
|
-
await renderEmbeddedListField(invoiceLinesField({ value: rows }), (v) => {
|
|
159
|
-
lastValue = v;
|
|
160
|
-
});
|
|
161
|
-
captured?.onCellChange(0, "quantity", 4);
|
|
162
|
-
expect(lastValue).toEqual([
|
|
163
|
-
{ product: "p1", unit: "pcs", quantity: 4, unitPrice: 500, amount: 2000 },
|
|
164
|
-
]);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test("a fractional product on the money-typed amount cell shows the rounded live preview, not the raw fraction", async () => {
|
|
168
|
-
const rows = [{ product: "p1", unit: "pcs", quantity: 2, unitPrice: 923, amount: 1846 }];
|
|
169
|
-
let lastValue: unknown;
|
|
170
|
-
await renderEmbeddedListField(invoiceLinesField({ value: rows }), (v) => {
|
|
171
|
-
lastValue = v;
|
|
172
|
-
});
|
|
173
|
-
// 2.5 * 923 = 2307.5 — a fractional minor-unit amount the money target
|
|
174
|
-
// can't store as-is. The live preview must show the rounded value
|
|
175
|
-
// (2308), matching what the server would compute and persist.
|
|
176
|
-
captured?.onCellChange(0, "quantity", 2.5);
|
|
177
|
-
expect(lastValue).toEqual([
|
|
178
|
-
{ product: "p1", unit: "pcs", quantity: 2.5, unitPrice: 923, amount: 2308 },
|
|
179
|
-
]);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
// Regression: withRecomputedDerived used to read source values from
|
|
183
|
-
// `result` (mutated in-place during the loop), so a chained derived cell
|
|
184
|
-
// (gross depends on net, net depends on raw qty/price) got the FRESH net
|
|
185
|
-
// when "net" happened to iterate before "gross" in embeddedListDerived's
|
|
186
|
-
// key order, but the STALE pre-edit net otherwise — the client preview
|
|
187
|
-
// drifted from the server (withDerivedCells always reads the untouched
|
|
188
|
-
// source row) purely based on object-key order. Fixed by reading from a
|
|
189
|
-
// frozen `source` snapshot, same as the server. Two field configs with
|
|
190
|
-
// "net"/"gross" declared in opposite key order must therefore produce the
|
|
191
|
-
// identical, order-independent result.
|
|
192
|
-
test("chained derived cells (gross depends on net) recompute the same value regardless of the derived-map's key order", async () => {
|
|
193
|
-
const cells: EditFieldViewModel["embeddedListCells"] = [
|
|
194
|
-
{ field: "qty", label: "Qty", type: "number", required: true },
|
|
195
|
-
{ field: "price", label: "Price", type: "number", required: true },
|
|
196
|
-
{ field: "tax", label: "Tax", type: "number", required: true },
|
|
197
|
-
{ field: "net", label: "Net", type: "number", required: false },
|
|
198
|
-
{ field: "gross", label: "Gross", type: "number", required: false },
|
|
199
|
-
];
|
|
200
|
-
// Pre-edit, already-settled row: net = 2*3 = 6, gross = 6+1 = 7.
|
|
201
|
-
const rows = [{ qty: 2, price: 3, tax: 1, net: 6, gross: 7 }];
|
|
202
|
-
// A fixed reference calculation for "qty changes from 2 to 4": net
|
|
203
|
-
// recomputes from the fresh raw inputs (qty=4, price=3) to 12; gross
|
|
204
|
-
// reads net from the row as it was BEFORE this edit (still 6, same as
|
|
205
|
-
// the server would), giving 6+1=7 — not 12+1=13.
|
|
206
|
-
const expected = { qty: 4, price: 3, tax: 1, net: 12, gross: 7 };
|
|
207
|
-
|
|
208
|
-
let netFirstResult: unknown;
|
|
209
|
-
await renderEmbeddedListField(
|
|
210
|
-
{
|
|
211
|
-
...invoiceLinesField({ value: rows }),
|
|
212
|
-
embeddedListCells: cells,
|
|
213
|
-
embeddedListDerived: {
|
|
214
|
-
net: { op: "multiply", from: ["qty", "price"] },
|
|
215
|
-
gross: { op: "sum", from: ["net", "tax"] },
|
|
216
|
-
},
|
|
217
|
-
},
|
|
218
|
-
(v) => {
|
|
219
|
-
netFirstResult = v;
|
|
220
|
-
},
|
|
221
|
-
);
|
|
222
|
-
captured?.onCellChange(0, "qty", 4);
|
|
223
|
-
|
|
224
|
-
let grossFirstResult: unknown;
|
|
225
|
-
await renderEmbeddedListField(
|
|
226
|
-
{
|
|
227
|
-
...invoiceLinesField({ value: rows }),
|
|
228
|
-
embeddedListCells: cells,
|
|
229
|
-
embeddedListDerived: {
|
|
230
|
-
gross: { op: "sum", from: ["net", "tax"] },
|
|
231
|
-
net: { op: "multiply", from: ["qty", "price"] },
|
|
232
|
-
},
|
|
233
|
-
},
|
|
234
|
-
(v) => {
|
|
235
|
-
grossFirstResult = v;
|
|
236
|
-
},
|
|
237
|
-
);
|
|
238
|
-
captured?.onCellChange(0, "qty", 4);
|
|
239
|
-
|
|
240
|
-
expect(netFirstResult).toEqual([expected]);
|
|
241
|
-
expect(grossFirstResult).toEqual([expected]);
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
describe("EmbeddedListField — row operations", () => {
|
|
246
|
-
const rows = [
|
|
247
|
-
{ product: "p1", unit: "pcs", quantity: 1, unitPrice: 100, amount: 100 },
|
|
248
|
-
{ product: "p2", unit: "hours", quantity: 2, unitPrice: 200, amount: 400 },
|
|
249
|
-
];
|
|
250
|
-
|
|
251
|
-
test("onAddRow appends an empty row with derived recomputed", async () => {
|
|
252
|
-
let lastValue: unknown;
|
|
253
|
-
await renderEmbeddedListField(invoiceLinesField({ value: rows }), (v) => {
|
|
254
|
-
lastValue = v;
|
|
255
|
-
});
|
|
256
|
-
captured?.onAddRow();
|
|
257
|
-
expect(lastValue).toEqual([...rows, { amount: undefined }]);
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
test("onRemoveRow removes exactly the targeted row", async () => {
|
|
261
|
-
let lastValue: unknown;
|
|
262
|
-
await renderEmbeddedListField(invoiceLinesField({ value: rows }), (v) => {
|
|
263
|
-
lastValue = v;
|
|
264
|
-
});
|
|
265
|
-
captured?.onRemoveRow(0);
|
|
266
|
-
expect(lastValue).toEqual([rows[1]]);
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
test("onDuplicateRow inserts a copy right after the source row", async () => {
|
|
270
|
-
let lastValue: unknown;
|
|
271
|
-
await renderEmbeddedListField(invoiceLinesField({ value: rows }), (v) => {
|
|
272
|
-
lastValue = v;
|
|
273
|
-
});
|
|
274
|
-
captured?.onDuplicateRow(0);
|
|
275
|
-
expect(lastValue).toEqual([rows[0], rows[0], rows[1]]);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
test("onDuplicateRow is a no-op for an out-of-range index", async () => {
|
|
279
|
-
let called = false;
|
|
280
|
-
await renderEmbeddedListField(invoiceLinesField({ value: rows }), () => {
|
|
281
|
-
called = true;
|
|
282
|
-
});
|
|
283
|
-
captured?.onDuplicateRow(99);
|
|
284
|
-
expect(called).toBe(false);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
test("onMoveRow moves an element from one index to another, immutably", async () => {
|
|
288
|
-
let lastValue: unknown;
|
|
289
|
-
await renderEmbeddedListField(invoiceLinesField({ value: rows }), (v) => {
|
|
290
|
-
lastValue = v;
|
|
291
|
-
});
|
|
292
|
-
captured?.onMoveRow(1, 0);
|
|
293
|
-
expect(lastValue).toEqual([rows[1], rows[0]]);
|
|
294
|
-
// original array must stay untouched
|
|
295
|
-
expect(rows).toEqual([
|
|
296
|
-
{ product: "p1", unit: "pcs", quantity: 1, unitPrice: 100, amount: 100 },
|
|
297
|
-
{ product: "p2", unit: "hours", quantity: 2, unitPrice: 200, amount: 400 },
|
|
298
|
-
]);
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
test("onMoveRow is a no-op for an out-of-range target index", async () => {
|
|
302
|
-
let called = false;
|
|
303
|
-
await renderEmbeddedListField(invoiceLinesField({ value: rows }), () => {
|
|
304
|
-
called = true;
|
|
305
|
-
});
|
|
306
|
-
captured?.onMoveRow(0, 5);
|
|
307
|
-
expect(called).toBe(false);
|
|
308
|
-
});
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
describe("EmbeddedListField — paste coercion", () => {
|
|
312
|
-
test("pastes number/money/select cells with correct coercion per column type", async () => {
|
|
313
|
-
const rows = [{ product: "p1", unit: "pcs", quantity: 1, unitPrice: 100, amount: 100 }];
|
|
314
|
-
let lastValue: unknown;
|
|
315
|
-
await renderEmbeddedListField(invoiceLinesField({ value: rows }), (v) => {
|
|
316
|
-
lastValue = v;
|
|
317
|
-
});
|
|
318
|
-
// Columns in order: product, unit, quantity, unitPrice, amount.
|
|
319
|
-
// Paste starting at column index 1 ("unit") for row 0: unit, quantity, unitPrice.
|
|
320
|
-
captured?.onPasteCells?.(0, 1, [["hours", "3", "12,50"]]);
|
|
321
|
-
expect(lastValue).toEqual([
|
|
322
|
-
{ product: "p1", unit: "hours", quantity: 3, unitPrice: 1250, amount: 3750 },
|
|
323
|
-
]);
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
// kumiko-framework#1972: a hardcoded ×100 in the paste path diverged from
|
|
327
|
-
// the typed-in path's currencyDecimals-based scaling for any non-2-decimal
|
|
328
|
-
// currency — JPY (0 decimals) pasted "1234" would have landed on 123400
|
|
329
|
-
// (100x too large) instead of 1234.
|
|
330
|
-
test("pastes a money cell for a zero-decimal currency (JPY) without the paste-path ×100 bug", async () => {
|
|
331
|
-
const rows = [{ product: "p1", unit: "pcs", quantity: 1, unitPrice: 0, amount: 0 }];
|
|
332
|
-
let lastValue: unknown;
|
|
333
|
-
await renderEmbeddedListField(
|
|
334
|
-
invoiceLinesField({ value: rows, embeddedListCurrency: "JPY" }),
|
|
335
|
-
(v) => {
|
|
336
|
-
lastValue = v;
|
|
337
|
-
},
|
|
338
|
-
);
|
|
339
|
-
// Columns in order: product, unit, quantity, unitPrice, amount. Paste
|
|
340
|
-
// starting at "unitPrice" (index 3).
|
|
341
|
-
captured?.onPasteCells?.(0, 3, [["1234"]]);
|
|
342
|
-
const result = lastValue as readonly Record<string, unknown>[];
|
|
343
|
-
expect(result[0]?.["unitPrice"]).toBe(1234);
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
test("paste beyond the current rows appends new rows but never past maxItems", async () => {
|
|
347
|
-
const rows = [{ product: "p1", unit: "pcs", quantity: 1, unitPrice: 100, amount: 100 }];
|
|
348
|
-
let lastValue: unknown;
|
|
349
|
-
await renderEmbeddedListField(
|
|
350
|
-
invoiceLinesField({ value: rows, embeddedListMaxItems: 2 }),
|
|
351
|
-
(v) => {
|
|
352
|
-
lastValue = v;
|
|
353
|
-
},
|
|
354
|
-
);
|
|
355
|
-
captured?.onPasteCells?.(0, 2, [
|
|
356
|
-
["1", "100"],
|
|
357
|
-
["2", "200"],
|
|
358
|
-
["3", "300"],
|
|
359
|
-
]);
|
|
360
|
-
const result = lastValue as readonly Record<string, unknown>[];
|
|
361
|
-
expect(result.length).toBe(2);
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
test("paste beyond maxItems surfaces a listIssue with the dropped-row count", async () => {
|
|
365
|
-
const rows = [{ product: "p1", unit: "pcs", quantity: 1, unitPrice: 100, amount: 100 }];
|
|
366
|
-
await renderEmbeddedListField(
|
|
367
|
-
invoiceLinesField({ value: rows, embeddedListMaxItems: 2 }),
|
|
368
|
-
() => {},
|
|
369
|
-
);
|
|
370
|
-
act(() => {
|
|
371
|
-
captured?.onPasteCells?.(0, 2, [
|
|
372
|
-
["1", "100"],
|
|
373
|
-
["2", "200"],
|
|
374
|
-
["3", "300"],
|
|
375
|
-
]);
|
|
376
|
-
});
|
|
377
|
-
await waitFor(() => {
|
|
378
|
-
expect(captured?.listIssues).toEqual([
|
|
379
|
-
{
|
|
380
|
-
path: "lines",
|
|
381
|
-
code: "paste-rows-truncated",
|
|
382
|
-
i18nKey: "kumiko.field.embedded-list.paste-rows-truncated",
|
|
383
|
-
params: { count: 1 },
|
|
384
|
-
},
|
|
385
|
-
]);
|
|
386
|
-
});
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
test("a select paste value with no matching option leaves the cell unchanged and surfaces a listIssue", async () => {
|
|
390
|
-
const rows = [{ product: "p1", unit: "pcs", quantity: 1, unitPrice: 100, amount: 100 }];
|
|
391
|
-
let lastValue: unknown;
|
|
392
|
-
await renderEmbeddedListField(invoiceLinesField({ value: rows }), (v) => {
|
|
393
|
-
lastValue = v;
|
|
394
|
-
});
|
|
395
|
-
// Columns in order: product, unit, quantity, unitPrice, amount. Column
|
|
396
|
-
// index 1 = "unit" (select, options ["pcs","hours","kg"]) — "not-a-unit"
|
|
397
|
-
// matches none of them.
|
|
398
|
-
act(() => {
|
|
399
|
-
captured?.onPasteCells?.(0, 1, [["not-a-unit"]]);
|
|
400
|
-
});
|
|
401
|
-
expect(lastValue).toEqual([rows[0]]);
|
|
402
|
-
await waitFor(() => {
|
|
403
|
-
expect(captured?.listIssues).toEqual([
|
|
404
|
-
{
|
|
405
|
-
path: "lines",
|
|
406
|
-
code: "paste-cells-unmatched",
|
|
407
|
-
i18nKey: "kumiko.field.embedded-list.paste-cells-unmatched",
|
|
408
|
-
params: { count: 1 },
|
|
409
|
-
},
|
|
410
|
-
]);
|
|
411
|
-
});
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
// kumiko-framework#1838: a naive `replace(",", ".")` on a DE-locale paste
|
|
415
|
-
// with a thousands separator turned "1.234,56" into "1.234.56" → NaN →
|
|
416
|
-
// the cell silently cleared instead of keeping the pasted amount.
|
|
417
|
-
test("pastes a DE-locale money value with a thousands separator (1.234,56)", async () => {
|
|
418
|
-
const rows = [{ product: "p1", unit: "pcs", quantity: 1, unitPrice: 100, amount: 100 }];
|
|
419
|
-
let lastValue: unknown;
|
|
420
|
-
await renderEmbeddedListField(invoiceLinesField({ value: rows }), (v) => {
|
|
421
|
-
lastValue = v;
|
|
422
|
-
});
|
|
423
|
-
// Column index 3 = "unitPrice".
|
|
424
|
-
captured?.onPasteCells?.(0, 3, [["1.234,56"]]);
|
|
425
|
-
const result = lastValue as readonly Record<string, unknown>[];
|
|
426
|
-
expect(result[0]?.["unitPrice"]).toBe(123456);
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
test("pastes an en-US-locale money value with a thousands separator (1,234.56)", async () => {
|
|
430
|
-
const rows = [{ product: "p1", unit: "pcs", quantity: 1, unitPrice: 100, amount: 100 }];
|
|
431
|
-
let lastValue: unknown;
|
|
432
|
-
await renderEmbeddedListField(invoiceLinesField({ value: rows }), (v) => {
|
|
433
|
-
lastValue = v;
|
|
434
|
-
});
|
|
435
|
-
captured?.onPasteCells?.(0, 3, [["1,234.56"]]);
|
|
436
|
-
const result = lastValue as readonly Record<string, unknown>[];
|
|
437
|
-
expect(result[0]?.["unitPrice"]).toBe(123456);
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
test("an unparseable money paste leaves the cell unchanged and surfaces a listIssue instead of clearing it", async () => {
|
|
441
|
-
const rows = [{ product: "p1", unit: "pcs", quantity: 1, unitPrice: 100, amount: 100 }];
|
|
442
|
-
let lastValue: unknown;
|
|
443
|
-
await renderEmbeddedListField(invoiceLinesField({ value: rows }), (v) => {
|
|
444
|
-
lastValue = v;
|
|
445
|
-
});
|
|
446
|
-
act(() => {
|
|
447
|
-
captured?.onPasteCells?.(0, 3, [["not-a-number"]]);
|
|
448
|
-
});
|
|
449
|
-
const result = lastValue as readonly Record<string, unknown>[];
|
|
450
|
-
expect(result[0]?.["unitPrice"]).toBe(100);
|
|
451
|
-
await waitFor(() => {
|
|
452
|
-
expect(captured?.listIssues).toEqual([
|
|
453
|
-
{
|
|
454
|
-
path: "lines",
|
|
455
|
-
code: "paste-cells-unmatched",
|
|
456
|
-
i18nKey: "kumiko.field.embedded-list.paste-cells-unmatched",
|
|
457
|
-
params: { count: 1 },
|
|
458
|
-
},
|
|
459
|
-
]);
|
|
460
|
-
});
|
|
461
|
-
});
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
describe("EmbeddedListField — reference column populated via useQuery", () => {
|
|
465
|
-
test("referenceOptions come from the product list query", async () => {
|
|
466
|
-
await renderEmbeddedListField(invoiceLinesField({ value: [] }), () => {}, {}, [
|
|
467
|
-
{ id: "p1", name: "Widget A" },
|
|
468
|
-
]);
|
|
469
|
-
await waitFor(() => {
|
|
470
|
-
const productColumn = captured?.columns.find((c) => c.field === "product");
|
|
471
|
-
expect(productColumn?.referenceOptions).toEqual([{ value: "p1", label: "Widget A" }]);
|
|
472
|
-
});
|
|
473
|
-
});
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
describe("EmbeddedListField — issue grouping wiring", () => {
|
|
477
|
-
test("a lines.0.amount issue is routed as a cellIssue at 0.amount", async () => {
|
|
478
|
-
const rows = [{ product: "p1", unit: "pcs", quantity: 1, unitPrice: 100, amount: 100 }];
|
|
479
|
-
const issue: FieldIssue = { path: "lines.0.amount", code: "custom", i18nKey: "Bad amount" };
|
|
480
|
-
await renderEmbeddedListField(invoiceLinesField({ value: rows }), () => {}, {
|
|
481
|
-
"lines.0.amount": [issue],
|
|
482
|
-
});
|
|
483
|
-
expect(captured?.cellIssues?.["0.amount"]).toEqual([issue]);
|
|
484
|
-
expect(captured?.rowIssues ?? {}).toEqual({});
|
|
485
|
-
expect(captured?.listIssues ?? []).toEqual([]);
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
test("a lines-level issue is routed as a listIssue", async () => {
|
|
489
|
-
const issue: FieldIssue = { path: "lines", code: "custom", i18nKey: "Too few lines" };
|
|
490
|
-
await renderEmbeddedListField(invoiceLinesField({ value: [] }), () => {}, {
|
|
491
|
-
lines: [issue],
|
|
492
|
-
});
|
|
493
|
-
expect(captured?.listIssues).toEqual([issue]);
|
|
494
|
-
});
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
async function renderFieldWithEmbeddedList(
|
|
498
|
-
field: EditFieldViewModel,
|
|
499
|
-
allIssues: RenderFieldProps["allIssues"],
|
|
500
|
-
): Promise<void> {
|
|
501
|
-
captured = undefined;
|
|
502
|
-
render(
|
|
503
|
-
<LocaleProvider
|
|
504
|
-
resolver={createStaticLocaleResolver()}
|
|
505
|
-
fallbackBundles={[kumikoDefaultTranslations]}
|
|
506
|
-
>
|
|
507
|
-
<DispatcherProvider dispatcher={stubDispatcher()}>
|
|
508
|
-
<PrimitivesProvider value={testPrimitives()}>
|
|
509
|
-
<RenderField
|
|
510
|
-
field={field}
|
|
511
|
-
onChange={() => {}}
|
|
512
|
-
allIssues={allIssues}
|
|
513
|
-
featureName="invoices"
|
|
514
|
-
/>
|
|
515
|
-
</PrimitivesProvider>
|
|
516
|
-
</DispatcherProvider>
|
|
517
|
-
</LocaleProvider>,
|
|
518
|
-
);
|
|
519
|
-
await act(async () => {});
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
describe("RenderField — routes embedded-list fields to EmbeddedListField (plumbing)", () => {
|
|
523
|
-
test("field.embeddedListCells set → RenderField mounts EmbeddedListField, allIssues propagate", async () => {
|
|
524
|
-
const rows = [{ product: "p1", unit: "pcs", quantity: 1, unitPrice: 100, amount: 100 }];
|
|
525
|
-
const issue: FieldIssue = { path: "lines.0.amount", code: "custom", i18nKey: "Bad amount" };
|
|
526
|
-
await renderFieldWithEmbeddedList(invoiceLinesField({ value: rows }), {
|
|
527
|
-
"lines.0.amount": [issue],
|
|
528
|
-
});
|
|
529
|
-
expect(captured?.cellIssues?.["0.amount"]).toEqual([issue]);
|
|
530
|
-
});
|
|
531
|
-
});
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
// display: "checkboxes" on a multiSelect field renders a checkbox grid
|
|
2
|
-
// (MultiSelectCheckboxes) instead of the combobox dropdown. Style follows
|
|
3
|
-
// render-field-app-locale.test.tsx: mount RenderField under real Locale +
|
|
4
|
-
// Primitives providers with capturing stubs, then invoke the captured
|
|
5
|
-
// callbacks the way a real primitive implementation would.
|
|
6
|
-
|
|
7
|
-
import { describe, expect, test } from "bun:test";
|
|
8
|
-
import type { EditFieldViewModel } from "@cosmicdrift/kumiko-headless";
|
|
9
|
-
import { render } from "@testing-library/react";
|
|
10
|
-
import type { ComponentType, ReactNode } from "react";
|
|
11
|
-
import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
|
|
12
|
-
import { kumikoDefaultTranslations } from "../../i18n-defaults";
|
|
13
|
-
import {
|
|
14
|
-
type ButtonProps,
|
|
15
|
-
type CorePrimitives,
|
|
16
|
-
type GridProps,
|
|
17
|
-
type InputProps,
|
|
18
|
-
PrimitivesProvider,
|
|
19
|
-
} from "../../primitives";
|
|
20
|
-
import { RenderField } from "../render-field";
|
|
21
|
-
|
|
22
|
-
let capturedInputs: InputProps[] = [];
|
|
23
|
-
let capturedButton: ButtonProps | undefined;
|
|
24
|
-
let capturedGrid: GridProps | undefined;
|
|
25
|
-
|
|
26
|
-
const captureInput: ComponentType<InputProps> = (props) => {
|
|
27
|
-
capturedInputs.push(props);
|
|
28
|
-
return null;
|
|
29
|
-
};
|
|
30
|
-
const captureButton: ComponentType<ButtonProps> = (props) => {
|
|
31
|
-
capturedButton = props;
|
|
32
|
-
return null;
|
|
33
|
-
};
|
|
34
|
-
const captureGrid: ComponentType<GridProps> = (props) => {
|
|
35
|
-
capturedGrid = props;
|
|
36
|
-
return <>{props.children}</>;
|
|
37
|
-
};
|
|
38
|
-
const passChildren = ({ children }: { readonly children?: ReactNode }): ReactNode => children;
|
|
39
|
-
const noop = (): ReactNode => null;
|
|
40
|
-
|
|
41
|
-
const testPrimitives: CorePrimitives = {
|
|
42
|
-
Button: captureButton,
|
|
43
|
-
Banner: noop,
|
|
44
|
-
Field: passChildren,
|
|
45
|
-
Input: captureInput,
|
|
46
|
-
DataTable: noop,
|
|
47
|
-
Form: noop,
|
|
48
|
-
Section: noop,
|
|
49
|
-
Card: noop,
|
|
50
|
-
Grid: captureGrid,
|
|
51
|
-
GridCell: passChildren,
|
|
52
|
-
Text: noop,
|
|
53
|
-
Heading: noop,
|
|
54
|
-
Dialog: noop,
|
|
55
|
-
Modal: noop,
|
|
56
|
-
Lightbox: noop,
|
|
57
|
-
ConfigSourceBadge: noop,
|
|
58
|
-
ConfigCascadeView: noop,
|
|
59
|
-
Link: noop,
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
function languagesField(overrides: Partial<EditFieldViewModel> = {}): EditFieldViewModel {
|
|
63
|
-
return {
|
|
64
|
-
field: "languages",
|
|
65
|
-
label: "Languages",
|
|
66
|
-
type: "multiSelect",
|
|
67
|
-
value: [],
|
|
68
|
-
visible: true,
|
|
69
|
-
readOnly: false,
|
|
70
|
-
required: false,
|
|
71
|
-
options: ["en", "de", "es"],
|
|
72
|
-
optionLabels: { en: "English", de: "German", es: "Spanish" },
|
|
73
|
-
display: "checkboxes",
|
|
74
|
-
...overrides,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
let lastOnChange: unknown;
|
|
79
|
-
function renderField(field: EditFieldViewModel): void {
|
|
80
|
-
capturedInputs = [];
|
|
81
|
-
capturedButton = undefined;
|
|
82
|
-
capturedGrid = undefined;
|
|
83
|
-
lastOnChange = undefined;
|
|
84
|
-
render(
|
|
85
|
-
<LocaleProvider
|
|
86
|
-
resolver={createStaticLocaleResolver()}
|
|
87
|
-
fallbackBundles={[kumikoDefaultTranslations]}
|
|
88
|
-
>
|
|
89
|
-
<PrimitivesProvider value={testPrimitives}>
|
|
90
|
-
<RenderField field={field} onChange={(v) => (lastOnChange = v)} />
|
|
91
|
-
</PrimitivesProvider>
|
|
92
|
-
</LocaleProvider>,
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
describe("RenderField — multiSelect display: checkboxes", () => {
|
|
97
|
-
test("renders one boolean checkbox per option, no combobox", () => {
|
|
98
|
-
renderField(languagesField());
|
|
99
|
-
expect(capturedInputs).toHaveLength(3);
|
|
100
|
-
for (const input of capturedInputs) {
|
|
101
|
-
expect(input.kind).toBe("boolean");
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test("without display, the field still renders the combobox (back-compat)", () => {
|
|
106
|
-
renderField(languagesField({ display: undefined }));
|
|
107
|
-
expect(capturedInputs).toHaveLength(1);
|
|
108
|
-
expect(capturedInputs[0]?.kind).toBe("combobox");
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test("clicking one checkbox emits the full array including previously-set values, in options order", () => {
|
|
112
|
-
renderField(languagesField({ value: ["en"] }));
|
|
113
|
-
const deInput = capturedInputs.find(
|
|
114
|
-
(i) => i.kind === "boolean" && i.id === "kumiko-edit-languages-de",
|
|
115
|
-
);
|
|
116
|
-
expect(deInput?.kind).toBe("boolean");
|
|
117
|
-
if (deInput?.kind === "boolean") deInput.onChange(true);
|
|
118
|
-
expect(lastOnChange).toEqual(["en", "de"]);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("unchecking a checkbox drops only that value, keeping options order", () => {
|
|
122
|
-
renderField(languagesField({ value: ["en", "de", "es"] }));
|
|
123
|
-
const deInput = capturedInputs.find(
|
|
124
|
-
(i) => i.kind === "boolean" && i.id === "kumiko-edit-languages-de",
|
|
125
|
-
);
|
|
126
|
-
if (deInput?.kind === "boolean") deInput.onChange(false);
|
|
127
|
-
expect(lastOnChange).toEqual(["en", "es"]);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test("select-all toggle shows 'Select all' when nothing is selected, and selects everything in options order", () => {
|
|
131
|
-
renderField(languagesField({ value: [] }));
|
|
132
|
-
expect(capturedButton?.children).toBe("Select all");
|
|
133
|
-
capturedButton?.onClick?.();
|
|
134
|
-
expect(lastOnChange).toEqual(["en", "de", "es"]);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test("select-all toggle flips to 'Deselect all' once everything is selected, and clears on click", () => {
|
|
138
|
-
renderField(languagesField({ value: ["en", "de", "es"] }));
|
|
139
|
-
expect(capturedButton?.children).toBe("Deselect all");
|
|
140
|
-
capturedButton?.onClick?.();
|
|
141
|
-
expect(lastOnChange).toEqual([]);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test("disabled (readOnly) disables every checkbox and the select-all toggle", () => {
|
|
145
|
-
renderField(languagesField({ readOnly: true }));
|
|
146
|
-
expect(capturedButton?.disabled).toBe(true);
|
|
147
|
-
for (const input of capturedInputs) {
|
|
148
|
-
expect(input.disabled).toBe(true);
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test("columns and maxRows pass through to the Grid primitive", () => {
|
|
153
|
-
renderField(languagesField({ columns: 2, maxRows: 3 }));
|
|
154
|
-
expect(capturedGrid?.columns).toBe(2);
|
|
155
|
-
expect(capturedGrid?.maxRows).toBe(3);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test("omitting columns/maxRows keeps the default layout (no maxRows on the Grid)", () => {
|
|
159
|
-
renderField(languagesField());
|
|
160
|
-
expect(capturedGrid?.maxRows).toBeUndefined();
|
|
161
|
-
});
|
|
162
|
-
});
|