@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,525 +0,0 @@
|
|
|
1
|
-
import { describe, expect, spyOn, test } from "bun:test";
|
|
2
|
-
import type { EntityEditScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
3
|
-
import {
|
|
4
|
-
buildInitialValues,
|
|
5
|
-
literalCurrencyOverrides,
|
|
6
|
-
mergeSearchParamsIntoInitial,
|
|
7
|
-
} from "../app/kumiko-screen";
|
|
8
|
-
import { layoutFieldNames } from "../app/layout-fields";
|
|
9
|
-
|
|
10
|
-
type FieldDef = {
|
|
11
|
-
type?: string;
|
|
12
|
-
default?: unknown;
|
|
13
|
-
sensitive?: boolean;
|
|
14
|
-
format?: string;
|
|
15
|
-
options?: readonly string[];
|
|
16
|
-
multiple?: boolean;
|
|
17
|
-
schema?: Record<string, { type?: string; options?: readonly string[] }>;
|
|
18
|
-
maxItems?: number;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
function mergeWithAllFieldsUrlPrefillable(
|
|
22
|
-
fields: Record<string, FieldDef>,
|
|
23
|
-
searchParams: Record<string, string>,
|
|
24
|
-
renderableFields?: ReadonlySet<string>,
|
|
25
|
-
defaultCurrency?: string,
|
|
26
|
-
): Record<string, unknown> {
|
|
27
|
-
return mergeSearchParamsIntoInitial(fields, {
|
|
28
|
-
searchParams,
|
|
29
|
-
urlPrefillFields: Object.keys(fields),
|
|
30
|
-
...(renderableFields !== undefined && { renderableFields }),
|
|
31
|
-
...(defaultCurrency !== undefined && { defaultCurrency }),
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
describe("mergeSearchParamsIntoInitial — urlPrefillFields allowlist", () => {
|
|
36
|
-
const fields: Record<string, FieldDef> = {
|
|
37
|
-
leaseId: { type: "text" },
|
|
38
|
-
iban: { type: "text", default: "DE00" },
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
test("a URL param for a declared field prefills it", () => {
|
|
42
|
-
const result = mergeSearchParamsIntoInitial(fields, {
|
|
43
|
-
searchParams: { leaseId: "l-1" },
|
|
44
|
-
urlPrefillFields: ["leaseId"],
|
|
45
|
-
});
|
|
46
|
-
expect(result["leaseId"]).toBe("l-1");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("a crafted link cannot prefill a field no navigate params declares", () => {
|
|
50
|
-
const result = mergeSearchParamsIntoInitial(fields, {
|
|
51
|
-
searchParams: { leaseId: "l-1", iban: "attacker-iban" },
|
|
52
|
-
urlPrefillFields: ["leaseId"],
|
|
53
|
-
});
|
|
54
|
-
expect(result["leaseId"]).toBe("l-1");
|
|
55
|
-
expect(result["iban"]).toBe("DE00");
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("no urlPrefillFields (screen not from buildAppSchema) prefills nothing from the URL", () => {
|
|
59
|
-
const result = mergeSearchParamsIntoInitial(fields, {
|
|
60
|
-
searchParams: { leaseId: "l-1", iban: "attacker-iban" },
|
|
61
|
-
urlPrefillFields: undefined,
|
|
62
|
-
});
|
|
63
|
-
expect(result["leaseId"]).toBe("");
|
|
64
|
-
expect(result["iban"]).toBe("DE00");
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test("sensitive and password fields stay blocked even when allowlisted", () => {
|
|
68
|
-
const gated: Record<string, FieldDef> = {
|
|
69
|
-
secret: { type: "text", sensitive: true },
|
|
70
|
-
apiToken: { type: "text", format: "password", default: "unset" },
|
|
71
|
-
};
|
|
72
|
-
const result = mergeSearchParamsIntoInitial(gated, {
|
|
73
|
-
searchParams: { secret: "s", apiToken: "kpat_leak" },
|
|
74
|
-
urlPrefillFields: ["secret", "apiToken"],
|
|
75
|
-
});
|
|
76
|
-
expect(result["secret"]).toBe("");
|
|
77
|
-
expect(result["apiToken"]).toBe("unset");
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
describe("mergeSearchParamsIntoInitial — handoffValues", () => {
|
|
82
|
-
const fields: Record<string, FieldDef> = {
|
|
83
|
-
iban: { type: "text" },
|
|
84
|
-
amount: { type: "number", default: 0 },
|
|
85
|
-
secret: { type: "text", sensitive: true },
|
|
86
|
-
apiToken: { type: "text", format: "password" },
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
test("handed-off values prefill fields outside urlPrefillFields, strings coerced by field type", () => {
|
|
90
|
-
const result = mergeSearchParamsIntoInitial(fields, {
|
|
91
|
-
searchParams: {},
|
|
92
|
-
urlPrefillFields: [],
|
|
93
|
-
handoffValues: { iban: "DE12", amount: "42" },
|
|
94
|
-
});
|
|
95
|
-
expect(result["iban"]).toBe("DE12");
|
|
96
|
-
expect(result["amount"]).toBe(42);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test("handed-off values never reach sensitive or password fields", () => {
|
|
100
|
-
const result = mergeSearchParamsIntoInitial(fields, {
|
|
101
|
-
searchParams: {},
|
|
102
|
-
urlPrefillFields: [],
|
|
103
|
-
handoffValues: { secret: "s", apiToken: "kpat_leak" },
|
|
104
|
-
});
|
|
105
|
-
expect(result["secret"]).toBe("");
|
|
106
|
-
expect(result["apiToken"]).toBe("");
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("drawer overrides win over a handoff for the same field", () => {
|
|
110
|
-
const result = mergeSearchParamsIntoInitial(fields, {
|
|
111
|
-
searchParams: {},
|
|
112
|
-
urlPrefillFields: [],
|
|
113
|
-
drawerOverrides: { iban: "from-row" },
|
|
114
|
-
handoffValues: { iban: "from-handoff" },
|
|
115
|
-
});
|
|
116
|
-
expect(result["iban"]).toBe("from-row");
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
describe("mergeSearchParamsIntoInitial — coercion (every field URL-prefillable)", () => {
|
|
121
|
-
test("raw string param merges in as-is for a text field", () => {
|
|
122
|
-
const fields: Record<string, FieldDef> = { name: { type: "text" } };
|
|
123
|
-
const result = mergeWithAllFieldsUrlPrefillable(fields, { name: "Alice" });
|
|
124
|
-
expect(result["name"]).toBe("Alice");
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
test("number-type field coerces a numeric string", () => {
|
|
128
|
-
const fields: Record<string, FieldDef> = { age: { type: "number" } };
|
|
129
|
-
const result = mergeWithAllFieldsUrlPrefillable(fields, { age: "42" });
|
|
130
|
-
expect(result["age"]).toBe(42);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test("invalid number string falls back to field default", () => {
|
|
134
|
-
const fields: Record<string, FieldDef> = { count: { type: "number", default: 7 } };
|
|
135
|
-
const result = mergeWithAllFieldsUrlPrefillable(fields, { count: "not-a-number" });
|
|
136
|
-
expect(result["count"]).toBe(7);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test("boolean field coerces 'true' and 'false'", () => {
|
|
140
|
-
const fields: Record<string, FieldDef> = { active: { type: "boolean" } };
|
|
141
|
-
expect(mergeWithAllFieldsUrlPrefillable(fields, { active: "true" })["active"]).toBe(true);
|
|
142
|
-
expect(mergeWithAllFieldsUrlPrefillable(fields, { active: "false" })["active"]).toBe(false);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test("sensitive field is skipped even when a matching searchParam exists", () => {
|
|
146
|
-
const fields: Record<string, FieldDef> = { password: { type: "text", sensitive: true } };
|
|
147
|
-
const result = mergeWithAllFieldsUrlPrefillable(fields, { password: "secret" });
|
|
148
|
-
expect(result["password"]).toBe("");
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test("password-format field is skipped even when a matching searchParam exists (fw#2548)", () => {
|
|
152
|
-
const fields: Record<string, FieldDef> = {
|
|
153
|
-
apiToken: { type: "text", format: "password", default: "unset" },
|
|
154
|
-
};
|
|
155
|
-
const result = mergeWithAllFieldsUrlPrefillable(fields, { apiToken: "kpat_leak" });
|
|
156
|
-
expect(result["apiToken"]).toBe("unset");
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
test("field with no matching searchParam keeps its buildInitialValues default", () => {
|
|
160
|
-
const fields: Record<string, FieldDef> = { total: { type: "number", default: 100 } };
|
|
161
|
-
const result = mergeWithAllFieldsUrlPrefillable(fields, {});
|
|
162
|
-
expect(result["total"]).toBe(100);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
test("money-type field without a defaultCurrency coerces a bare numeric string (legacy callers, e.g. config-edit/action-form)", () => {
|
|
166
|
-
const fields: Record<string, FieldDef> = { price: { type: "money" } };
|
|
167
|
-
const result = mergeWithAllFieldsUrlPrefillable(fields, { price: "19.99" });
|
|
168
|
-
expect(result["price"]).toBe(19.99);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
test("money-type field WITH a defaultCurrency merges the entityEdit payload shape (#1923)", () => {
|
|
172
|
-
const fields: Record<string, FieldDef> = { price: { type: "money" } };
|
|
173
|
-
const result = mergeWithAllFieldsUrlPrefillable(fields, { price: "19.99" }, undefined, "USD");
|
|
174
|
-
expect(result["price"]).toEqual({ amount: 19.99, currency: "USD" });
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test("money-type field WITH a defaultCurrency but no matching searchParam still defaults to the object shape", () => {
|
|
178
|
-
const fields: Record<string, FieldDef> = { price: { type: "money" } };
|
|
179
|
-
const result = mergeWithAllFieldsUrlPrefillable(fields, {}, undefined, "USD");
|
|
180
|
-
expect(result["price"]).toEqual({ amount: 0, currency: "USD" });
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
test("renderableFields set given: searchParam for a non-rendered field is ignored (#1708)", () => {
|
|
184
|
-
const fields: Record<string, FieldDef> = {
|
|
185
|
-
status: { type: "text", default: "draft" },
|
|
186
|
-
ownerId: { type: "text" },
|
|
187
|
-
};
|
|
188
|
-
const result = mergeWithAllFieldsUrlPrefillable(
|
|
189
|
-
fields,
|
|
190
|
-
{ status: "approved", ownerId: "user-123" },
|
|
191
|
-
new Set(["status"]),
|
|
192
|
-
);
|
|
193
|
-
expect(result["status"]).toBe("approved");
|
|
194
|
-
expect(result["ownerId"]).toBe("");
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
test("renderableFields from a groups-only layout: searchParam on a group field is kept", () => {
|
|
198
|
-
const screen: EntityEditScreenDefinition = {
|
|
199
|
-
id: "s",
|
|
200
|
-
type: "entityEdit",
|
|
201
|
-
entity: "e",
|
|
202
|
-
layout: { sections: [{ fields: [], groups: [{ title: "g", fields: ["status"] }] }] },
|
|
203
|
-
};
|
|
204
|
-
const fields: Record<string, FieldDef> = { status: { type: "text", default: "draft" } };
|
|
205
|
-
const result = mergeWithAllFieldsUrlPrefillable(
|
|
206
|
-
fields,
|
|
207
|
-
{ status: "approved" },
|
|
208
|
-
layoutFieldNames(screen),
|
|
209
|
-
);
|
|
210
|
-
expect(result["status"]).toBe("approved");
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
test("no renderableFields set given (undefined): behaves as before, all fields eligible", () => {
|
|
214
|
-
const fields: Record<string, FieldDef> = { ownerId: { type: "text" } };
|
|
215
|
-
const result = mergeWithAllFieldsUrlPrefillable(fields, { ownerId: "user-123" });
|
|
216
|
-
expect(result["ownerId"]).toBe("user-123");
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
test("multiSelect coerces comma-separated searchParam to string[]", () => {
|
|
220
|
-
const fields: Record<string, FieldDef> = { roles: { type: "multiSelect" } };
|
|
221
|
-
expect(mergeWithAllFieldsUrlPrefillable(fields, { roles: "TenantAdmin" })["roles"]).toEqual([
|
|
222
|
-
"TenantAdmin",
|
|
223
|
-
]);
|
|
224
|
-
expect(mergeWithAllFieldsUrlPrefillable(fields, { roles: "Admin,User" })["roles"]).toEqual([
|
|
225
|
-
"Admin",
|
|
226
|
-
"User",
|
|
227
|
-
]);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
test("multiSelect coerces JSON-array searchParam to string[]", () => {
|
|
231
|
-
const fields: Record<string, FieldDef> = { roles: { type: "multiSelect" } };
|
|
232
|
-
expect(
|
|
233
|
-
mergeWithAllFieldsUrlPrefillable(fields, { roles: JSON.stringify(["Admin", "Editor"]) })[
|
|
234
|
-
"roles"
|
|
235
|
-
],
|
|
236
|
-
).toEqual(["Admin", "Editor"]);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
test("multiSelect defaults to [] when unset", () => {
|
|
240
|
-
const fields: Record<string, FieldDef> = { roles: { type: "multiSelect" } };
|
|
241
|
-
expect(mergeWithAllFieldsUrlPrefillable(fields, {})["roles"]).toEqual([]);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
test("multiSelect prefers JSON parse even without '[' prefix for quoted strings", () => {
|
|
245
|
-
const fields: Record<string, FieldDef> = { tags: { type: "multiSelect" } };
|
|
246
|
-
expect(
|
|
247
|
-
mergeWithAllFieldsUrlPrefillable(fields, { tags: JSON.stringify("Berlin, Germany") })["tags"],
|
|
248
|
-
).toEqual(["Berlin, Germany"]);
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
test("multiSelect filters unknown option values when options are set", () => {
|
|
252
|
-
const fields: Record<string, FieldDef> = {
|
|
253
|
-
roles: { type: "multiSelect", options: ["Admin", "User"] },
|
|
254
|
-
};
|
|
255
|
-
expect(
|
|
256
|
-
mergeWithAllFieldsUrlPrefillable(fields, { roles: JSON.stringify(["Admin", "Hacker"]) })[
|
|
257
|
-
"roles"
|
|
258
|
-
],
|
|
259
|
-
).toEqual(["Admin"]);
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
test("money-type field parses a JSON {amount, currency} param without a defaultCurrency (fw#2763)", () => {
|
|
263
|
-
const fields: Record<string, FieldDef> = { price: { type: "money" } };
|
|
264
|
-
const result = mergeWithAllFieldsUrlPrefillable(fields, {
|
|
265
|
-
price: JSON.stringify({ amount: 19.99, currency: "CHF" }),
|
|
266
|
-
});
|
|
267
|
-
expect(result["price"]).toEqual({ amount: 19.99, currency: "CHF" });
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
test("money-type field: explicit JSON currency wins over defaultCurrency", () => {
|
|
271
|
-
const fields: Record<string, FieldDef> = { price: { type: "money" } };
|
|
272
|
-
const result = mergeWithAllFieldsUrlPrefillable(
|
|
273
|
-
fields,
|
|
274
|
-
{ price: JSON.stringify({ amount: 19.99, currency: "CHF" }) },
|
|
275
|
-
undefined,
|
|
276
|
-
"USD",
|
|
277
|
-
);
|
|
278
|
-
expect(result["price"]).toEqual({ amount: 19.99, currency: "CHF" });
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
test("money-type field: bare number without a defaultCurrency keeps the number and warns (fw#2763)", () => {
|
|
282
|
-
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
283
|
-
try {
|
|
284
|
-
const fields: Record<string, FieldDef> = { price: { type: "money" } };
|
|
285
|
-
const result = mergeWithAllFieldsUrlPrefillable(fields, { price: "19.99" });
|
|
286
|
-
expect(result["price"]).toBe(19.99);
|
|
287
|
-
expect(warnSpy).toHaveBeenCalled();
|
|
288
|
-
expect(warnSpy.mock.calls[0]?.[0]).toContain("price");
|
|
289
|
-
} finally {
|
|
290
|
-
warnSpy.mockRestore();
|
|
291
|
-
}
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
test("money-type field: JSON object with a broken shape falls back to the default and warns", () => {
|
|
295
|
-
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
296
|
-
try {
|
|
297
|
-
const fields: Record<string, FieldDef> = {
|
|
298
|
-
price: { type: "money", default: { amount: 0, currency: "EUR" } },
|
|
299
|
-
};
|
|
300
|
-
const result = mergeWithAllFieldsUrlPrefillable(fields, {
|
|
301
|
-
price: JSON.stringify({ amount: 5 }),
|
|
302
|
-
});
|
|
303
|
-
expect(result["price"]).toEqual({ amount: 0, currency: "EUR" });
|
|
304
|
-
expect(warnSpy).toHaveBeenCalled();
|
|
305
|
-
} finally {
|
|
306
|
-
warnSpy.mockRestore();
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
test("money-type field: JSON object with a malformed currency code falls back to the default and warns", () => {
|
|
311
|
-
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
312
|
-
try {
|
|
313
|
-
const fields: Record<string, FieldDef> = {
|
|
314
|
-
price: { type: "money", default: { amount: 0, currency: "EUR" } },
|
|
315
|
-
};
|
|
316
|
-
const result = mergeWithAllFieldsUrlPrefillable(fields, {
|
|
317
|
-
price: JSON.stringify({ amount: 5, currency: "<script>" }),
|
|
318
|
-
});
|
|
319
|
-
expect(result["price"]).toEqual({ amount: 0, currency: "EUR" });
|
|
320
|
-
expect(warnSpy).toHaveBeenCalled();
|
|
321
|
-
} finally {
|
|
322
|
-
warnSpy.mockRestore();
|
|
323
|
-
}
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
test("money-type field: a non-finite bare number falls back to the default (fw#2763)", () => {
|
|
327
|
-
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
328
|
-
try {
|
|
329
|
-
const fields: Record<string, FieldDef> = {
|
|
330
|
-
price: { type: "money", default: { amount: 0, currency: "EUR" } },
|
|
331
|
-
};
|
|
332
|
-
const result = mergeWithAllFieldsUrlPrefillable(fields, { price: "1e999" });
|
|
333
|
-
expect(result["price"]).toEqual({ amount: 0, currency: "EUR" });
|
|
334
|
-
} finally {
|
|
335
|
-
warnSpy.mockRestore();
|
|
336
|
-
}
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
describe("embeddedList prefill (fw#2764)", () => {
|
|
340
|
-
const linesField = (extra: Partial<FieldDef> = {}): Record<string, FieldDef> => ({
|
|
341
|
-
lines: {
|
|
342
|
-
type: "embedded",
|
|
343
|
-
multiple: true,
|
|
344
|
-
schema: {
|
|
345
|
-
accountId: { type: "text" },
|
|
346
|
-
amount: { type: "money" },
|
|
347
|
-
qty: { type: "number" },
|
|
348
|
-
posted: { type: "boolean" },
|
|
349
|
-
kind: { type: "select", options: ["debit", "credit"] },
|
|
350
|
-
},
|
|
351
|
-
...extra,
|
|
352
|
-
},
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
test("a JSON row list from params.map arrives complete in the field", () => {
|
|
356
|
-
const rows = [
|
|
357
|
-
{ accountId: "bank", amount: 1299, qty: 2, posted: true, kind: "debit" },
|
|
358
|
-
{ accountId: "cash", amount: -1299, qty: 1, posted: false, kind: "credit" },
|
|
359
|
-
];
|
|
360
|
-
const result = mergeWithAllFieldsUrlPrefillable(linesField(), {
|
|
361
|
-
lines: JSON.stringify(rows),
|
|
362
|
-
});
|
|
363
|
-
expect(result["lines"]).toEqual(rows);
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
test("money cells stay signed minor-unit integers", () => {
|
|
367
|
-
const result = mergeWithAllFieldsUrlPrefillable(linesField(), {
|
|
368
|
-
lines: JSON.stringify([{ accountId: "bank", amount: -4200 }]),
|
|
369
|
-
});
|
|
370
|
-
expect(result["lines"]).toEqual([{ accountId: "bank", amount: -4200 }]);
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
test("a fractional money cell rejects the whole prefill and warns", () => {
|
|
374
|
-
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
375
|
-
try {
|
|
376
|
-
const result = mergeWithAllFieldsUrlPrefillable(linesField(), {
|
|
377
|
-
lines: JSON.stringify([{ accountId: "bank", amount: 12.99 }]),
|
|
378
|
-
});
|
|
379
|
-
expect(result["lines"]).toEqual([]);
|
|
380
|
-
expect(warnSpy).toHaveBeenCalled();
|
|
381
|
-
expect(warnSpy.mock.calls[0]?.[0]).toContain("lines");
|
|
382
|
-
} finally {
|
|
383
|
-
warnSpy.mockRestore();
|
|
384
|
-
}
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
test("a non-JSON param leaves the field empty and warns", () => {
|
|
388
|
-
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
389
|
-
try {
|
|
390
|
-
const result = mergeWithAllFieldsUrlPrefillable(linesField(), { lines: "not-json-at-all" });
|
|
391
|
-
expect(result["lines"]).toEqual([]);
|
|
392
|
-
expect(warnSpy).toHaveBeenCalled();
|
|
393
|
-
} finally {
|
|
394
|
-
warnSpy.mockRestore();
|
|
395
|
-
}
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
test("a JSON object instead of an array leaves the field empty and warns", () => {
|
|
399
|
-
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
400
|
-
try {
|
|
401
|
-
const result = mergeWithAllFieldsUrlPrefillable(linesField(), {
|
|
402
|
-
lines: JSON.stringify({ accountId: "bank" }),
|
|
403
|
-
});
|
|
404
|
-
expect(result["lines"]).toEqual([]);
|
|
405
|
-
expect(warnSpy).toHaveBeenCalled();
|
|
406
|
-
} finally {
|
|
407
|
-
warnSpy.mockRestore();
|
|
408
|
-
}
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
test("a row that is not an object leaves the field empty and warns", () => {
|
|
412
|
-
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
413
|
-
try {
|
|
414
|
-
const result = mergeWithAllFieldsUrlPrefillable(linesField(), {
|
|
415
|
-
lines: JSON.stringify([1, 2]),
|
|
416
|
-
});
|
|
417
|
-
expect(result["lines"]).toEqual([]);
|
|
418
|
-
expect(warnSpy).toHaveBeenCalled();
|
|
419
|
-
} finally {
|
|
420
|
-
warnSpy.mockRestore();
|
|
421
|
-
}
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
test("a select cell outside the declared options rejects the prefill and warns", () => {
|
|
425
|
-
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
426
|
-
try {
|
|
427
|
-
const result = mergeWithAllFieldsUrlPrefillable(linesField(), {
|
|
428
|
-
lines: JSON.stringify([{ accountId: "bank", kind: "sudo" }]),
|
|
429
|
-
});
|
|
430
|
-
expect(result["lines"]).toEqual([]);
|
|
431
|
-
expect(warnSpy).toHaveBeenCalled();
|
|
432
|
-
} finally {
|
|
433
|
-
warnSpy.mockRestore();
|
|
434
|
-
}
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
test("undeclared row keys are dropped instead of reaching the form", () => {
|
|
438
|
-
const result = mergeWithAllFieldsUrlPrefillable(linesField(), {
|
|
439
|
-
lines: JSON.stringify([{ accountId: "bank", secretFlag: "yes" }]),
|
|
440
|
-
});
|
|
441
|
-
expect(result["lines"]).toEqual([{ accountId: "bank" }]);
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
test("a __proto__ key in a row pollutes nothing", () => {
|
|
445
|
-
const result = mergeWithAllFieldsUrlPrefillable(linesField(), {
|
|
446
|
-
lines: '[{"accountId":"bank","__proto__":{"polluted":true}}]',
|
|
447
|
-
});
|
|
448
|
-
expect(result["lines"]).toEqual([{ accountId: "bank" }]);
|
|
449
|
-
expect(({} as Record<string, unknown>)["polluted"]).toBeUndefined();
|
|
450
|
-
expect(Object.getPrototypeOf((result["lines"] as unknown[])[0])).toBe(Object.prototype);
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
test("more rows than maxItems leaves the field empty and warns", () => {
|
|
454
|
-
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
455
|
-
try {
|
|
456
|
-
const result = mergeWithAllFieldsUrlPrefillable(linesField({ maxItems: 2 }), {
|
|
457
|
-
lines: JSON.stringify([{ accountId: "a" }, { accountId: "b" }, { accountId: "c" }]),
|
|
458
|
-
});
|
|
459
|
-
expect(result["lines"]).toEqual([]);
|
|
460
|
-
expect(warnSpy).toHaveBeenCalled();
|
|
461
|
-
} finally {
|
|
462
|
-
warnSpy.mockRestore();
|
|
463
|
-
}
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
test("an embedded list without a matching param defaults to an empty array", () => {
|
|
467
|
-
const result = mergeWithAllFieldsUrlPrefillable(linesField(), {});
|
|
468
|
-
expect(result["lines"]).toEqual([]);
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
test("a sensitive embedded list ignores the param", () => {
|
|
472
|
-
const result = mergeWithAllFieldsUrlPrefillable(linesField({ sensitive: true }), {
|
|
473
|
-
lines: JSON.stringify([{ accountId: "bank" }]),
|
|
474
|
-
});
|
|
475
|
-
expect(result["lines"]).toEqual([]);
|
|
476
|
-
});
|
|
477
|
-
});
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
// fw#2839: an entity-less form screen passes no `defaultCurrency`, so before
|
|
481
|
-
// this the money branch fell through to a bare `0` the handler's zod schema
|
|
482
|
-
// rejected. `literalCurrencyOverrides` resolves a declared literal source with
|
|
483
|
-
// no query; configEdit's plain-number contract depends on getting neither.
|
|
484
|
-
describe("money currency sources on an entity-less form (fw#2839)", () => {
|
|
485
|
-
const amountField = (currency?: unknown): Record<string, FieldDef> => ({
|
|
486
|
-
amount: { type: "money", ...(currency !== undefined && { currency }) } as FieldDef,
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
test("a literal source seeds { amount: 0, currency } without any defaultCurrency", () => {
|
|
490
|
-
const fields = amountField({ kind: "literal", code: "CHF" });
|
|
491
|
-
const result = mergeSearchParamsIntoInitial(fields, {
|
|
492
|
-
searchParams: {},
|
|
493
|
-
urlPrefillFields: [],
|
|
494
|
-
moneyCurrencyOverrides: literalCurrencyOverrides(fields),
|
|
495
|
-
});
|
|
496
|
-
expect(result["amount"]).toEqual({ amount: 0, currency: "CHF" });
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
test("a literal source also carries into a URL-prefilled amount (fw#2763 path)", () => {
|
|
500
|
-
const fields = amountField({ kind: "literal", code: "CHF" });
|
|
501
|
-
const result = mergeSearchParamsIntoInitial(fields, {
|
|
502
|
-
searchParams: { amount: "42.50" },
|
|
503
|
-
urlPrefillFields: ["amount"],
|
|
504
|
-
moneyCurrencyOverrides: literalCurrencyOverrides(fields),
|
|
505
|
-
});
|
|
506
|
-
expect(result["amount"]).toEqual({ amount: 42.5, currency: "CHF" });
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
test("literalCurrencyOverrides ignores tenant-declared and undeclared money fields", () => {
|
|
510
|
-
expect(literalCurrencyOverrides(amountField({ kind: "tenant" }))).toEqual({});
|
|
511
|
-
expect(literalCurrencyOverrides(amountField())).toEqual({});
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
test("configEdit stays on the plain-number contract: no overrides, no currency wrapper", () => {
|
|
515
|
-
const fields = amountField({ kind: "literal", code: "CHF" });
|
|
516
|
-
expect(buildInitialValues(fields)["amount"]).toBe(0);
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
test("entityEdit is unchanged: defaultCurrency still wins where no source is declared", () => {
|
|
520
|
-
expect(buildInitialValues(amountField(), "USD")["amount"]).toEqual({
|
|
521
|
-
amount: 0,
|
|
522
|
-
currency: "USD",
|
|
523
|
-
});
|
|
524
|
-
});
|
|
525
|
-
});
|
package/src/__tests__/qn.test.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { toKebab as serverToKebab } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
-
import { lastSegment, toKebab } from "../app/qn";
|
|
4
|
-
|
|
5
|
-
describe("lastSegment", () => {
|
|
6
|
-
test("strips feature-prefix from screen-QN", () => {
|
|
7
|
-
expect(lastSegment("publicstatus:screen:component-edit")).toBe("component-edit");
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
test("strips feature-prefix from nav-QN", () => {
|
|
11
|
-
expect(lastSegment("shop:nav:catalog")).toBe("catalog");
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
test("strips feature-prefix from workspace-QN", () => {
|
|
15
|
-
expect(lastSegment("admin:workspace:disposition")).toBe("disposition");
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test("returns short-form input unchanged", () => {
|
|
19
|
-
// Defensive default — an Author who already passes a short id
|
|
20
|
-
// shouldn't get a misformatted result.
|
|
21
|
-
expect(lastSegment("component-edit")).toBe("component-edit");
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test("strips only the LAST segment, not the first", () => {
|
|
25
|
-
// The QN convention is `<feature>:<kind>:<short-id>`. lastIndexOf
|
|
26
|
-
// ensures kebab-ids that themselves contain colons (defensive —
|
|
27
|
-
// kebab spec rejects them, but the helper shouldn't break).
|
|
28
|
-
expect(lastSegment("a:b:c:d")).toBe("d");
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test("handles empty string", () => {
|
|
32
|
-
expect(lastSegment("")).toBe("");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test("handles trailing colon as empty segment", () => {
|
|
36
|
-
// E.g. when registry stamping is buggy and writes `feature:screen:`
|
|
37
|
-
// — the helper returns "" rather than throwing, the caller's
|
|
38
|
-
// navigate-then-not-found banner makes the bug visible.
|
|
39
|
-
expect(lastSegment("publicstatus:screen:")).toBe("");
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
describe("toKebab", () => {
|
|
44
|
-
test("camelCase entity ids match server qualifyEntityName", () => {
|
|
45
|
-
expect(toKebab("driverModel")).toBe("driver-model");
|
|
46
|
-
expect(toKebab("statementUpload")).toBe("statement-upload");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("already kebab unchanged", () => {
|
|
50
|
-
expect(toKebab("driver-model")).toBe("driver-model");
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("preserves colon segments", () => {
|
|
54
|
-
expect(toKebab("driverModel:list")).toBe("driver-model:list");
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("consecutive uppercase (acronym boundary)", () => {
|
|
58
|
-
expect(toKebab("SSEBroadcast")).toBe("sse-broadcast");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("dot separators become dashes", () => {
|
|
62
|
-
expect(toKebab("billing-period.create")).toBe("billing-period-create");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
// Drift guard: this file is a byte-identical copy of the server's toKebab
|
|
66
|
-
// (packages/framework/src/engine/qualified-name.ts) kept in sync only by
|
|
67
|
-
// convention/comment, not by import (avoids pulling server deps into the
|
|
68
|
-
// browser bundle). Table-driven against the server's own doc examples so a
|
|
69
|
-
// future edit to either copy that breaks parity fails loudly here.
|
|
70
|
-
test("matches the server implementation for its documented examples", () => {
|
|
71
|
-
const cases = [
|
|
72
|
-
"task.create",
|
|
73
|
-
"ticketAssigned",
|
|
74
|
-
"billing-period.create",
|
|
75
|
-
"monthlyReport",
|
|
76
|
-
"SSEBroadcast",
|
|
77
|
-
"driverModel:list",
|
|
78
|
-
];
|
|
79
|
-
for (const input of cases) {
|
|
80
|
-
expect(toKebab(input)).toBe(serverToKebab(input));
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
});
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { buildWhatsAppShareUrl } from "../share-links";
|
|
3
|
-
|
|
4
|
-
describe("buildWhatsAppShareUrl", () => {
|
|
5
|
-
test("without a phone number the link opens the contact picker with the text prefilled", () => {
|
|
6
|
-
expect(buildWhatsAppShareUrl("Hallo Welt")).toBe("https://wa.me/?text=Hallo%20Welt");
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
test("encodes line breaks, umlauts and reserved characters so the text arrives unchanged", () => {
|
|
10
|
-
const text = "Škoda für 17.490 €\nProbefahrt? & mehr #1";
|
|
11
|
-
const url = new URL(buildWhatsAppShareUrl(text));
|
|
12
|
-
expect(url.searchParams.get("text")).toBe(text);
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
test("normalizes a formatted international number to digits only", () => {
|
|
16
|
-
expect(buildWhatsAppShareUrl("x", "+49 (171) 234-5678")).toBe(
|
|
17
|
-
"https://wa.me/491712345678?text=x",
|
|
18
|
-
);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test("treats a 00 prefix like +", () => {
|
|
22
|
-
expect(buildWhatsAppShareUrl("x", "0049 171 2345678")).toBe(
|
|
23
|
-
"https://wa.me/491712345678?text=x",
|
|
24
|
-
);
|
|
25
|
-
});
|
|
26
|
-
});
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { sortByAccessor } from "../sort-by-accessor";
|
|
3
|
-
|
|
4
|
-
type Row = { readonly name: string; readonly count: number };
|
|
5
|
-
|
|
6
|
-
function makeRows(): readonly Row[] {
|
|
7
|
-
return [
|
|
8
|
-
{ name: "b", count: 2 },
|
|
9
|
-
{ name: "a", count: 3 },
|
|
10
|
-
{ name: "c", count: 1 },
|
|
11
|
-
];
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const accessors = {
|
|
15
|
-
name: (r: Row) => r.name,
|
|
16
|
-
count: (r: Row) => r.count,
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
describe("sortByAccessor", () => {
|
|
20
|
-
test("sort === null returns rows unchanged (same reference)", () => {
|
|
21
|
-
const rows = makeRows();
|
|
22
|
-
expect(sortByAccessor(rows, null, accessors)).toBe(rows);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
test("an unknown field returns rows unchanged (same reference)", () => {
|
|
26
|
-
const rows = makeRows();
|
|
27
|
-
expect(sortByAccessor(rows, { field: "nope", dir: "asc" }, accessors)).toBe(rows);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test("sorts ascending by the given accessor", () => {
|
|
31
|
-
const rows = makeRows();
|
|
32
|
-
const sorted = sortByAccessor(rows, { field: "name", dir: "asc" }, accessors);
|
|
33
|
-
expect(sorted.map((r) => r.name)).toEqual(["a", "b", "c"]);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test("sorts descending by the given accessor", () => {
|
|
37
|
-
const rows = makeRows();
|
|
38
|
-
const sorted = sortByAccessor(rows, { field: "count", dir: "desc" }, accessors);
|
|
39
|
-
expect(sorted.map((r) => r.count)).toEqual([3, 2, 1]);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("does not mutate the input array", () => {
|
|
43
|
-
const rows = makeRows();
|
|
44
|
-
const original = [...rows];
|
|
45
|
-
sortByAccessor(rows, { field: "name", dir: "asc" }, accessors);
|
|
46
|
-
expect(rows).toEqual(original);
|
|
47
|
-
});
|
|
48
|
-
});
|