@cosmicdrift/kumiko-renderer 0.328.0 → 0.328.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/dist/app/action-form-shim.d.ts +14 -0
  2. package/{src/app/action-form-shim.ts → dist/app/action-form-shim.js} +26 -46
  3. package/dist/app/app-features-context.d.ts +8 -0
  4. package/dist/app/app-features-context.js +16 -0
  5. package/dist/app/column-renderers.d.ts +30 -0
  6. package/dist/app/column-renderers.js +34 -0
  7. package/dist/app/config-edit-shim.d.ts +11 -0
  8. package/{src/app/config-edit-shim.ts → dist/app/config-edit-shim.js} +12 -25
  9. package/dist/app/content-editors.d.ts +38 -0
  10. package/dist/app/content-editors.js +26 -0
  11. package/dist/app/content-preview.d.ts +11 -0
  12. package/dist/app/content-preview.js +30 -0
  13. package/dist/app/custom-screens.d.ts +11 -0
  14. package/dist/app/custom-screens.js +20 -0
  15. package/dist/app/dashboard-body.d.ts +16 -0
  16. package/dist/app/dashboard-body.js +11 -0
  17. package/dist/app/extension-form-submit.d.ts +32 -0
  18. package/dist/app/extension-form-submit.js +73 -0
  19. package/dist/app/extension-sections.d.ts +72 -0
  20. package/dist/app/extension-sections.js +34 -0
  21. package/dist/app/feature-schema.d.ts +4 -0
  22. package/{src/app/feature-schema.ts → dist/app/feature-schema.js} +12 -22
  23. package/dist/app/form-schema.d.ts +4 -0
  24. package/dist/app/form-schema.js +85 -0
  25. package/dist/app/kumiko-screen.d.ts +43 -0
  26. package/dist/app/kumiko-screen.js +2206 -0
  27. package/dist/app/layout-fields.d.ts +3 -0
  28. package/dist/app/layout-fields.js +20 -0
  29. package/dist/app/list-facets.d.ts +48 -0
  30. package/dist/app/list-facets.js +185 -0
  31. package/dist/app/nav.d.ts +61 -0
  32. package/dist/app/nav.js +139 -0
  33. package/dist/app/projection-detail-shim.d.ts +8 -0
  34. package/dist/app/projection-detail-shim.js +71 -0
  35. package/dist/app/projection-list-shim.d.ts +13 -0
  36. package/{src/app/projection-list-shim.ts → dist/app/projection-list-shim.js} +26 -41
  37. package/dist/app/qn.d.ts +7 -0
  38. package/{src/app/qn.ts → dist/app/qn.js} +9 -11
  39. package/dist/app/qualify-screen-id.d.ts +2 -0
  40. package/{src/app/qualify-screen-id.ts → dist/app/qualify-screen-id.js} +5 -6
  41. package/dist/app/reference-facet-bridge.d.ts +10 -0
  42. package/dist/app/reference-facet-bridge.js +22 -0
  43. package/dist/app/return-to.d.ts +31 -0
  44. package/dist/app/return-to.js +188 -0
  45. package/dist/app/row-actions.d.ts +77 -0
  46. package/dist/app/row-actions.js +416 -0
  47. package/dist/app/screen-access.d.ts +8 -0
  48. package/dist/app/screen-access.js +48 -0
  49. package/dist/app/secret-mint-body.d.ts +10 -0
  50. package/dist/app/secret-mint-body.js +140 -0
  51. package/dist/app/secrets-edit-body.d.ts +8 -0
  52. package/dist/app/secrets-edit-body.js +76 -0
  53. package/dist/app/use-embedded-screen.d.ts +6 -0
  54. package/dist/app/use-embedded-screen.js +19 -0
  55. package/dist/app/variable-chips.d.ts +7 -0
  56. package/dist/app/variable-chips.js +16 -0
  57. package/dist/app/write-failed-error.d.ts +6 -0
  58. package/dist/app/write-failed-error.js +21 -0
  59. package/dist/components/embedded-list-field.d.ts +10 -0
  60. package/dist/components/embedded-list-field.js +259 -0
  61. package/dist/components/grid-cell-for-field.d.ts +25 -0
  62. package/dist/components/grid-cell-for-field.js +31 -0
  63. package/dist/components/multi-select-checkboxes.d.ts +14 -0
  64. package/dist/components/multi-select-checkboxes.js +36 -0
  65. package/dist/components/reference-create-dialog.d.ts +13 -0
  66. package/dist/components/reference-create-dialog.js +46 -0
  67. package/dist/components/related-list-section.d.ts +25 -0
  68. package/dist/components/related-list-section.js +273 -0
  69. package/dist/components/render-edit-action-button.d.ts +22 -0
  70. package/dist/components/render-edit-action-button.js +43 -0
  71. package/dist/components/render-edit-logic.d.ts +6 -0
  72. package/dist/components/render-edit-logic.js +63 -0
  73. package/dist/components/render-edit-types.d.ts +209 -0
  74. package/dist/components/render-edit-types.js +1 -0
  75. package/dist/components/render-edit.d.ts +6 -0
  76. package/dist/components/render-edit.js +1169 -0
  77. package/dist/components/render-field.d.ts +38 -0
  78. package/dist/components/render-field.js +544 -0
  79. package/dist/components/render-list.d.ts +117 -0
  80. package/dist/components/render-list.js +232 -0
  81. package/dist/components/write-form-section.d.ts +17 -0
  82. package/dist/components/write-form-section.js +104 -0
  83. package/dist/context/dispatcher-context.d.ts +10 -0
  84. package/{src/context/dispatcher-context.tsx → dist/context/dispatcher-context.js} +16 -29
  85. package/dist/context/draft-storage-context.d.ts +12 -0
  86. package/dist/context/draft-storage-context.js +18 -0
  87. package/dist/context/user-roles-context.d.ts +7 -0
  88. package/{src/context/user-roles-context.tsx → dist/context/user-roles-context.js} +7 -15
  89. package/dist/format-when.d.ts +1 -0
  90. package/{src/format-when.ts → dist/format-when.js} +7 -7
  91. package/dist/hooks/reference-limits.d.ts +3 -0
  92. package/{src/hooks/reference-limits.ts → dist/hooks/reference-limits.js} +0 -1
  93. package/dist/hooks/use-ai-text.d.ts +52 -0
  94. package/dist/hooks/use-ai-text.js +90 -0
  95. package/dist/hooks/use-disclosure.d.ts +7 -0
  96. package/dist/hooks/use-disclosure.js +8 -0
  97. package/dist/hooks/use-form.d.ts +7 -0
  98. package/dist/hooks/use-form.js +36 -0
  99. package/dist/hooks/use-list-url-state.d.ts +43 -0
  100. package/dist/hooks/use-list-url-state.js +131 -0
  101. package/dist/hooks/use-mutation.d.ts +9 -0
  102. package/dist/hooks/use-mutation.js +37 -0
  103. package/dist/hooks/use-query.d.ts +14 -0
  104. package/dist/hooks/use-query.js +118 -0
  105. package/dist/hooks/use-reference-lookup.d.ts +14 -0
  106. package/dist/hooks/use-reference-lookup.js +63 -0
  107. package/dist/hooks/use-store.d.ts +3 -0
  108. package/dist/hooks/use-store.js +38 -0
  109. package/dist/hooks/use-stream-handler.d.ts +14 -0
  110. package/dist/hooks/use-stream-handler.js +93 -0
  111. package/dist/i18n-defaults.d.ts +2 -0
  112. package/dist/i18n-defaults.js +201 -0
  113. package/dist/i18n.d.ts +64 -0
  114. package/dist/i18n.js +166 -0
  115. package/dist/index.d.ts +77 -0
  116. package/dist/index.js +55 -0
  117. package/dist/inside-drawer.d.ts +2 -0
  118. package/{src/inside-drawer.ts → dist/inside-drawer.js} +2 -5
  119. package/dist/page-header-slot.d.ts +2 -0
  120. package/{src/page-header-slot.ts → dist/page-header-slot.js} +2 -5
  121. package/dist/primitives.d.ts +1351 -0
  122. package/dist/primitives.js +73 -0
  123. package/dist/screen-fills-height.d.ts +3 -0
  124. package/dist/screen-fills-height.js +3 -0
  125. package/dist/share-links.d.ts +4 -0
  126. package/dist/share-links.js +11 -0
  127. package/dist/sort-by-accessor.d.ts +5 -0
  128. package/dist/sort-by-accessor.js +16 -0
  129. package/dist/sse/live-events.d.ts +23 -0
  130. package/dist/sse/live-events.js +28 -0
  131. package/dist/tokens.d.ts +83 -0
  132. package/dist/tokens.js +80 -0
  133. package/package.json +16 -9
  134. package/src/__tests__/error-i18n-defaults.test.ts +0 -82
  135. package/src/__tests__/format-when.test.ts +0 -12
  136. package/src/__tests__/i18n.test.tsx +0 -283
  137. package/src/__tests__/merge-search-params-into-initial.test.ts +0 -525
  138. package/src/__tests__/qn.test.ts +0 -83
  139. package/src/__tests__/share-links.test.ts +0 -26
  140. package/src/__tests__/sort-by-accessor.test.ts +0 -48
  141. package/src/__tests__/stringify-nav-params.test.ts +0 -33
  142. package/src/__tests__/use-list-url-state.test.tsx +0 -250
  143. package/src/app/__tests__/action-form-field-labels.test.tsx +0 -92
  144. package/src/app/__tests__/action-form-shim.test.ts +0 -137
  145. package/src/app/__tests__/build-list-query-payload.test.ts +0 -107
  146. package/src/app/__tests__/config-edit-shim.test.ts +0 -57
  147. package/src/app/__tests__/content-editors.test.tsx +0 -128
  148. package/src/app/__tests__/content-preview.test.tsx +0 -184
  149. package/src/app/__tests__/entity-edit-create-search-params.test.tsx +0 -236
  150. package/src/app/__tests__/entity-edit-fill-height.test.tsx +0 -118
  151. package/src/app/__tests__/entity-edit-jsonb-embedded-submit.test.tsx +0 -239
  152. package/src/app/__tests__/entity-list-create-singleton.test.tsx +0 -128
  153. package/src/app/__tests__/entity-list-row-action-entity-target.test.tsx +0 -424
  154. package/src/app/__tests__/entity-list-row-action-icon.test.tsx +0 -248
  155. package/src/app/__tests__/entity-list-row-action-kumiko-actions-view.test.tsx +0 -162
  156. package/src/app/__tests__/entity-list-row-action-refetch.test.tsx +0 -401
  157. package/src/app/__tests__/entity-list-screen-padding.test.tsx +0 -128
  158. package/src/app/__tests__/feature-schema.test.ts +0 -40
  159. package/src/app/__tests__/form-schema.test.ts +0 -247
  160. package/src/app/__tests__/list-daterange-facet.test.ts +0 -165
  161. package/src/app/__tests__/list-filter-facets.test.tsx +0 -521
  162. package/src/app/__tests__/nav.test.ts +0 -198
  163. package/src/app/__tests__/projection-detail-actions.test.tsx +0 -719
  164. package/src/app/__tests__/projection-detail-reference-field.test.tsx +0 -165
  165. package/src/app/__tests__/projection-detail-shim.test.ts +0 -188
  166. package/src/app/__tests__/projection-detail-singleton.test.tsx +0 -191
  167. package/src/app/__tests__/projection-list-bad-shape.test.tsx +0 -152
  168. package/src/app/__tests__/projection-list-reference-column.test.tsx +0 -219
  169. package/src/app/__tests__/projection-list-search-sort.test.tsx +0 -212
  170. package/src/app/__tests__/qualified-names.test.ts +0 -37
  171. package/src/app/__tests__/return-to.test.ts +0 -396
  172. package/src/app/__tests__/row-actions-navigate-tab.test.ts +0 -223
  173. package/src/app/__tests__/row-default-edit-action.test.tsx +0 -324
  174. package/src/app/__tests__/screen-access-allows.test.ts +0 -34
  175. package/src/app/__tests__/secret-mint-body.test.tsx +0 -404
  176. package/src/app/__tests__/variable-chips.test.tsx +0 -70
  177. package/src/app/app-features-context.tsx +0 -25
  178. package/src/app/column-renderers.tsx +0 -68
  179. package/src/app/content-editors.tsx +0 -85
  180. package/src/app/content-preview.tsx +0 -67
  181. package/src/app/custom-screens.tsx +0 -29
  182. package/src/app/dashboard-body.tsx +0 -33
  183. package/src/app/extension-form-submit.tsx +0 -123
  184. package/src/app/extension-sections.tsx +0 -111
  185. package/src/app/form-schema.ts +0 -91
  186. package/src/app/kumiko-screen.tsx +0 -3801
  187. package/src/app/layout-fields.ts +0 -31
  188. package/src/app/list-facets.ts +0 -259
  189. package/src/app/nav.tsx +0 -258
  190. package/src/app/projection-detail-shim.ts +0 -86
  191. package/src/app/reference-facet-bridge.tsx +0 -58
  192. package/src/app/return-to.tsx +0 -234
  193. package/src/app/row-actions.ts +0 -640
  194. package/src/app/screen-access.ts +0 -63
  195. package/src/app/secret-mint-body.tsx +0 -259
  196. package/src/app/secrets-edit-body.tsx +0 -194
  197. package/src/app/use-embedded-screen.ts +0 -27
  198. package/src/app/variable-chips.tsx +0 -39
  199. package/src/app/write-failed-error.ts +0 -27
  200. package/src/components/__tests__/embedded-list-field.test.tsx +0 -531
  201. package/src/components/__tests__/multi-select-checkboxes.test.tsx +0 -162
  202. package/src/components/__tests__/related-list-section-empty-state.test.tsx +0 -143
  203. package/src/components/__tests__/related-list-section.test.tsx +0 -1733
  204. package/src/components/__tests__/render-edit-action-button.test.tsx +0 -205
  205. package/src/components/__tests__/render-edit-hidden-section.test.tsx +0 -128
  206. package/src/components/__tests__/render-edit-logic.test.ts +0 -314
  207. package/src/components/__tests__/render-edit-screen-description.test.tsx +0 -152
  208. package/src/components/__tests__/render-edit-submit-actions.test.tsx +0 -460
  209. package/src/components/__tests__/render-field-app-locale.test.tsx +0 -142
  210. package/src/components/__tests__/render-field-enum-option.test.tsx +0 -99
  211. package/src/components/__tests__/render-field-icon-derivation.test.tsx +0 -119
  212. package/src/components/__tests__/render-field-json-format.test.tsx +0 -164
  213. package/src/components/__tests__/render-field-money-embedded-list-consistency.test.tsx +0 -212
  214. package/src/components/__tests__/render-field-money-roundtrip.test.tsx +0 -217
  215. package/src/components/__tests__/render-field-number-grouping.test.tsx +0 -76
  216. package/src/components/__tests__/render-field-number-unit.test.tsx +0 -227
  217. package/src/components/__tests__/render-field-password-format.test.tsx +0 -101
  218. package/src/components/__tests__/render-field-select-display.test.tsx +0 -134
  219. package/src/components/__tests__/render-field-unit-format.test.tsx +0 -107
  220. package/src/components/__tests__/render-field-unsupported-types.test.tsx +0 -195
  221. package/src/components/__tests__/render-list-toolbar-icon-only.test.tsx +0 -146
  222. package/src/components/__tests__/write-form-section.test.tsx +0 -375
  223. package/src/components/embedded-list-field.tsx +0 -354
  224. package/src/components/grid-cell-for-field.tsx +0 -88
  225. package/src/components/multi-select-checkboxes.tsx +0 -93
  226. package/src/components/reference-create-dialog.tsx +0 -91
  227. package/src/components/related-list-section.tsx +0 -491
  228. package/src/components/render-edit-action-button.tsx +0 -109
  229. package/src/components/render-edit-logic.ts +0 -85
  230. package/src/components/render-edit-types.ts +0 -226
  231. package/src/components/render-edit.tsx +0 -1871
  232. package/src/components/render-field.tsx +0 -951
  233. package/src/components/render-list.tsx +0 -593
  234. package/src/components/write-form-section.tsx +0 -198
  235. package/src/context/draft-storage-context.tsx +0 -39
  236. package/src/hooks/__tests__/use-ai-text.test.tsx +0 -177
  237. package/src/hooks/__tests__/use-disclosure.test.tsx +0 -25
  238. package/src/hooks/__tests__/use-form.test.tsx +0 -65
  239. package/src/hooks/__tests__/use-mutation.test.tsx +0 -77
  240. package/src/hooks/__tests__/use-query.test.tsx +0 -149
  241. package/src/hooks/__tests__/use-stream-handler.test.tsx +0 -107
  242. package/src/hooks/use-ai-text.ts +0 -172
  243. package/src/hooks/use-disclosure.ts +0 -20
  244. package/src/hooks/use-form.ts +0 -78
  245. package/src/hooks/use-list-url-state.ts +0 -207
  246. package/src/hooks/use-mutation.ts +0 -61
  247. package/src/hooks/use-query.ts +0 -166
  248. package/src/hooks/use-reference-lookup.ts +0 -76
  249. package/src/hooks/use-store.ts +0 -48
  250. package/src/hooks/use-stream-handler.ts +0 -134
  251. package/src/i18n-defaults.ts +0 -240
  252. package/src/i18n.tsx +0 -286
  253. package/src/index.ts +0 -279
  254. package/src/primitives.tsx +0 -1567
  255. package/src/screen-fills-height.ts +0 -3
  256. package/src/share-links.ts +0 -12
  257. package/src/sort-by-accessor.ts +0 -20
  258. package/src/sse/live-events.tsx +0 -56
  259. package/src/tokens.tsx +0 -170
@@ -1,205 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { fireEvent, render, screen as rtlScreen, waitFor } from "@testing-library/react";
3
- import type { ComponentType } from "react";
4
- import type { ButtonProps, DialogProps } from "../../primitives";
5
- import { RenderEditActionButton } from "../render-edit-action-button";
6
- import type { RenderEditAction } from "../render-edit-types";
7
-
8
- const TestButton: ComponentType<ButtonProps> = ({ children, onClick, testId, type, loading }) => (
9
- <button
10
- type={type ?? "button"}
11
- data-testid={testId}
12
- data-loading={loading ? "1" : "0"}
13
- onClick={() => {
14
- void onClick?.();
15
- }}
16
- >
17
- {children}
18
- </button>
19
- );
20
-
21
- const TestDialog: ComponentType<DialogProps> = ({
22
- open,
23
- onOpenChange,
24
- title,
25
- description,
26
- confirmLabel,
27
- variant,
28
- onConfirm,
29
- testId,
30
- }) =>
31
- open ? (
32
- <div data-testid={testId} data-variant={variant ?? "default"}>
33
- <span data-testid={`${testId}-title`}>{title}</span>
34
- {description !== undefined && (
35
- <span data-testid={`${testId}-description`}>{description}</span>
36
- )}
37
- <button
38
- type="button"
39
- data-testid={`${testId}-confirm`}
40
- onClick={() => {
41
- void (async () => {
42
- await onConfirm();
43
- onOpenChange(false);
44
- })();
45
- }}
46
- >
47
- {confirmLabel ?? "Confirm"}
48
- </button>
49
- <button type="button" data-testid={`${testId}-cancel`} onClick={() => onOpenChange(false)}>
50
- Cancel
51
- </button>
52
- </div>
53
- ) : null;
54
-
55
- function renderAction(action: RenderEditAction, onError: (text: string | null) => void = () => {}) {
56
- return render(
57
- <RenderEditActionButton
58
- action={action}
59
- Button={TestButton}
60
- Dialog={TestDialog}
61
- onError={onError}
62
- />,
63
- );
64
- }
65
-
66
- describe("RenderEditActionButton", () => {
67
- test("secondary action without confirm runs onPress immediately", async () => {
68
- let pressed = 0;
69
- renderAction({
70
- id: "ping",
71
- label: "Ping",
72
- onPress: async () => {
73
- pressed += 1;
74
- },
75
- });
76
-
77
- expect(rtlScreen.queryByTestId("render-edit-action-ping-dialog")).toBeNull();
78
- fireEvent.click(rtlScreen.getByTestId("render-edit-action-ping"));
79
- await waitFor(() => expect(pressed).toBe(1));
80
- });
81
-
82
- test("explicit confirm text opens dialog; confirm runs onPress, cancel does not", async () => {
83
- let pressed = 0;
84
- renderAction({
85
- id: "archive",
86
- label: "Archive",
87
- confirm: "Really archive?",
88
- confirmLabel: "Yes, archive",
89
- onPress: async () => {
90
- pressed += 1;
91
- },
92
- });
93
-
94
- fireEvent.click(rtlScreen.getByTestId("render-edit-action-archive"));
95
- expect(rtlScreen.getByTestId("render-edit-action-archive-dialog")).toBeTruthy();
96
- expect(rtlScreen.getByTestId("render-edit-action-archive-dialog-description").textContent).toBe(
97
- "Really archive?",
98
- );
99
- expect(rtlScreen.getByTestId("render-edit-action-archive-dialog-confirm").textContent).toBe(
100
- "Yes, archive",
101
- );
102
-
103
- fireEvent.click(rtlScreen.getByTestId("render-edit-action-archive-dialog-cancel"));
104
- expect(rtlScreen.queryByTestId("render-edit-action-archive-dialog")).toBeNull();
105
- expect(pressed).toBe(0);
106
-
107
- fireEvent.click(rtlScreen.getByTestId("render-edit-action-archive"));
108
- fireEvent.click(rtlScreen.getByTestId("render-edit-action-archive-dialog-confirm"));
109
- await waitFor(() => expect(pressed).toBe(1));
110
- await waitFor(() =>
111
- expect(rtlScreen.queryByTestId("render-edit-action-archive-dialog")).toBeNull(),
112
- );
113
- });
114
-
115
- test("danger style forces confirm dialog even without confirm text", async () => {
116
- let pressed = 0;
117
- renderAction({
118
- id: "delete",
119
- label: "Delete",
120
- style: "danger",
121
- onPress: async () => {
122
- pressed += 1;
123
- },
124
- });
125
-
126
- fireEvent.click(rtlScreen.getByTestId("render-edit-action-delete"));
127
- const dialog = rtlScreen.getByTestId("render-edit-action-delete-dialog");
128
- expect(dialog.getAttribute("data-variant")).toBe("danger");
129
- expect(rtlScreen.queryByTestId("render-edit-action-delete-dialog-description")).toBeNull();
130
- expect(pressed).toBe(0);
131
-
132
- expect(rtlScreen.getByTestId("render-edit-action-delete-dialog-confirm").textContent).toBe(
133
- "Delete",
134
- );
135
-
136
- fireEvent.click(rtlScreen.getByTestId("render-edit-action-delete-dialog-confirm"));
137
- await waitFor(() => expect(pressed).toBe(1));
138
- });
139
-
140
- // fw#2752: schema-driven navigate/drawer actions set confirmRequired: false
141
- // to opt a danger-styled action out of the forced dialog — the colour still
142
- // marks it destructive, but the target form is itself the confirmation.
143
- test("danger style with confirmRequired=false fires onPress directly, no dialog", async () => {
144
- let pressed = 0;
145
- renderAction({
146
- id: "open-terminate-form",
147
- label: "Terminate",
148
- style: "danger",
149
- confirmRequired: false,
150
- onPress: async () => {
151
- pressed += 1;
152
- },
153
- });
154
-
155
- fireEvent.click(rtlScreen.getByTestId("render-edit-action-open-terminate-form"));
156
- expect(rtlScreen.queryByTestId("render-edit-action-open-terminate-form-dialog")).toBeNull();
157
- await waitFor(() => expect(pressed).toBe(1));
158
- });
159
-
160
- test("onPress failure reports via onError", async () => {
161
- const errors: Array<string | null> = [];
162
- renderAction(
163
- {
164
- id: "boom",
165
- label: "Boom",
166
- onPress: async () => {
167
- throw new Error("action exploded");
168
- },
169
- },
170
- (text) => {
171
- errors.push(text);
172
- },
173
- );
174
-
175
- fireEvent.click(rtlScreen.getByTestId("render-edit-action-boom"));
176
- await waitFor(() => expect(errors).toContain("action exploded"));
177
- // Cleared at the start of trigger, then set on failure.
178
- expect(errors[0]).toBeNull();
179
- });
180
-
181
- test("sets loading while onPress is in flight", async () => {
182
- let resolvePress!: () => void;
183
- const pressPromise = new Promise<void>((resolve) => {
184
- resolvePress = resolve;
185
- });
186
- renderAction({
187
- id: "slow",
188
- label: "Slow",
189
- onPress: () => pressPromise,
190
- });
191
-
192
- fireEvent.click(rtlScreen.getByTestId("render-edit-action-slow"));
193
- await waitFor(() =>
194
- expect(rtlScreen.getByTestId("render-edit-action-slow").getAttribute("data-loading")).toBe(
195
- "1",
196
- ),
197
- );
198
- resolvePress();
199
- await waitFor(() =>
200
- expect(rtlScreen.getByTestId("render-edit-action-slow").getAttribute("data-loading")).toBe(
201
- "0",
202
- ),
203
- );
204
- });
205
- });
@@ -1,128 +0,0 @@
1
- // Regression coverage for the hidden-section guards in render-edit.tsx
2
- // (kumiko-framework#1731, from PR-review finding #1690): nothing previously
3
- // asserted that a fully-hidden section actually stays out of the DOM.
4
- //
5
- // Two cases, verified by mutation-testing each guard in isolation:
6
- // - "Hidden": a "fields" section whose only field is condition-hidden — the
7
- // issue's literal ask. Redundantly guarded: both the
8
- // `filterEditSections(...).filter(...)` pass and the render loop's
9
- // `if (!section.visible) return null;` independently suppress it, so this
10
- // case only goes red if BOTH guards are removed.
11
- // - "Empty": a "fields" section declared with zero fields. fw#1901 keeps it
12
- // past the filter above on purpose (it isn't "hidden", it just has
13
- // nothing to hide), so its `visible` stays vacuously false and
14
- // `if (!section.visible) return null;` is the ONLY thing nulling it out —
15
- // this case is what actually pins that line, verified red when it's
16
- // neutralized. Not a shape a booted app can produce (boot-validator
17
- // rejects `fields.length === 0`), but RenderEdit's props aren't
18
- // boot-validated, so it's a real call shape worth guarding.
19
-
20
- import { describe, expect, test } from "bun:test";
21
- import type {
22
- EntityDefinition,
23
- EntityEditScreenDefinition,
24
- } from "@cosmicdrift/kumiko-framework/ui-types";
25
- import { type RenderResult, render } from "@testing-library/react";
26
- import type { ComponentType, ReactNode } from "react";
27
- import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
28
- import { kumikoDefaultTranslations } from "../../i18n-defaults";
29
- import { type CorePrimitives, PrimitivesProvider, type SectionProps } from "../../primitives";
30
- import { RenderEdit } from "../render-edit";
31
-
32
- const noop = (): ReactNode => null;
33
- const passChildren = ({ children }: { readonly children?: ReactNode }): ReactNode => children;
34
- const renderSection: ComponentType<SectionProps> = ({ testId, children }) => (
35
- <div data-testid={testId}>{children}</div>
36
- );
37
-
38
- function testPrimitives(): CorePrimitives {
39
- return {
40
- Button: noop,
41
- Banner: noop,
42
- Field: passChildren,
43
- Input: noop,
44
- DataTable: noop,
45
- Form: passChildren,
46
- Section: renderSection,
47
- Card: passChildren,
48
- Grid: passChildren,
49
- GridCell: passChildren,
50
- Text: passChildren,
51
- Heading: noop,
52
- Dialog: noop,
53
- Modal: noop,
54
- Lightbox: noop,
55
- ConfigSourceBadge: noop,
56
- ConfigCascadeView: noop,
57
- Link: noop,
58
- };
59
- }
60
-
61
- function buildEntity(): EntityDefinition {
62
- return {
63
- fields: {
64
- name: { type: "text", maxLength: 200, required: false, searchable: false, sortable: false },
65
- secret: { type: "text", maxLength: 200, required: false, searchable: false, sortable: false },
66
- },
67
- };
68
- }
69
-
70
- function renderEdit(screen: EntityEditScreenDefinition): RenderResult {
71
- return render(
72
- <LocaleProvider
73
- resolver={createStaticLocaleResolver({ locale: "en-US" })}
74
- fallbackBundles={[kumikoDefaultTranslations]}
75
- >
76
- <PrimitivesProvider value={testPrimitives()}>
77
- <RenderEdit
78
- screen={screen}
79
- entity={buildEntity()}
80
- featureName="widgets"
81
- initial={{ name: "", secret: "" }}
82
- />
83
- </PrimitivesProvider>
84
- </LocaleProvider>,
85
- );
86
- }
87
-
88
- describe("RenderEdit — section with every field condition-hidden", () => {
89
- test("the visible section renders, the fully-hidden one does not reach the DOM", () => {
90
- const screen: EntityEditScreenDefinition = {
91
- id: "widget-edit",
92
- type: "entityEdit",
93
- entity: "widget",
94
- layout: {
95
- sections: [
96
- { title: "Visible", fields: ["name"] },
97
- { title: "Hidden", fields: [{ field: "secret", visible: false }] },
98
- ],
99
- },
100
- };
101
-
102
- const { getByTestId, queryByTestId } = renderEdit(screen);
103
-
104
- expect(getByTestId("section-Visible")).toBeDefined();
105
- expect(queryByTestId("section-Hidden")).toBeNull();
106
- });
107
- });
108
-
109
- describe("RenderEdit — section declared with zero fields", () => {
110
- test("the visible section renders, the empty one does not reach the DOM", () => {
111
- const screen: EntityEditScreenDefinition = {
112
- id: "widget-edit",
113
- type: "entityEdit",
114
- entity: "widget",
115
- layout: {
116
- sections: [
117
- { title: "Visible", fields: ["name"] },
118
- { title: "Empty", fields: [] },
119
- ],
120
- },
121
- };
122
-
123
- const { getByTestId, queryByTestId } = renderEdit(screen);
124
-
125
- expect(getByTestId("section-Visible")).toBeDefined();
126
- expect(queryByTestId("section-Empty")).toBeNull();
127
- });
128
- });
@@ -1,314 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import type {
3
- DispatcherError,
4
- EditFieldViewModel,
5
- EditSectionViewModel,
6
- SubmitResult,
7
- } from "@cosmicdrift/kumiko-headless";
8
- import {
9
- filterEditSections,
10
- hasEditableSection,
11
- hasIssueWithoutRenderedField,
12
- resolveExtensionEntityId,
13
- shouldNotifyCaller,
14
- } from "../render-edit-logic";
15
-
16
- const error: DispatcherError = {
17
- code: "internal_error",
18
- httpStatus: 500,
19
- i18nKey: "kumiko.errors.internal",
20
- message: "boom",
21
- };
22
-
23
- const success: SubmitResult<unknown> = { validationBlocked: false, isSuccess: true, data: {} };
24
- const writeFailure: SubmitResult<unknown> = { validationBlocked: false, isSuccess: false, error };
25
- const validationBlocked: SubmitResult<unknown> = { validationBlocked: true, isSuccess: false };
26
-
27
- describe("resolveExtensionEntityId", () => {
28
- test("explicit id prop wins over vm.id", () => {
29
- expect(resolveExtensionEntityId("entity-42", "row-7")).toBe("entity-42");
30
- });
31
-
32
- // The #345/1 regression: mount fell back to vm.id but persist did not, so a
33
- // custom-fields section mounted editable against the existing row yet saved
34
- // to null → silent data loss. Both sites now resolve this same value.
35
- test("undefined prop → vm.id fallback (update form carries the row id)", () => {
36
- expect(resolveExtensionEntityId(undefined, "row-42")).toBe("row-42");
37
- });
38
-
39
- test("undefined prop + no vm.id (create mode) → null", () => {
40
- expect(resolveExtensionEntityId(undefined, null)).toBeNull();
41
- });
42
-
43
- test("explicit null prop forces null even when vm.id is present", () => {
44
- expect(resolveExtensionEntityId(null, "row-42")).toBeNull();
45
- });
46
- });
47
-
48
- describe("shouldNotifyCaller", () => {
49
- // The one case the #345/1 fix changes: a successful entity write whose
50
- // extension persist failed must NOT notify (caller would navigate away and
51
- // unmount the extension-error banner → silent custom-field data loss). The
52
- // pre-fix code notified unconditionally, so this row flips false↔true.
53
- test("entity success + extension persist failed → suppress callback", () => {
54
- expect(shouldNotifyCaller(success, false)).toBe(false);
55
- });
56
-
57
- test("entity success + extensions persisted → notify", () => {
58
- expect(shouldNotifyCaller(success, true)).toBe(true);
59
- });
60
-
61
- test("entity write failure → notify (caller needs the error result)", () => {
62
- expect(shouldNotifyCaller(writeFailure, true)).toBe(true);
63
- // extensions never run on a failed entity write, but the flag must not
64
- // swallow the failure callback regardless of its value.
65
- expect(shouldNotifyCaller(writeFailure, false)).toBe(true);
66
- });
67
-
68
- test("validation blocked → notify", () => {
69
- expect(shouldNotifyCaller(validationBlocked, true)).toBe(true);
70
- });
71
- });
72
-
73
- const field = (readOnly: boolean): EditFieldViewModel => ({
74
- field: "f",
75
- label: "F",
76
- type: "text",
77
- value: "",
78
- visible: true,
79
- readOnly,
80
- required: false,
81
- });
82
- const fieldsSection = (...readOnly: boolean[]): EditSectionViewModel => ({
83
- kind: "fields",
84
- columns: 1,
85
- visible: true,
86
- fields: readOnly.map(field),
87
- });
88
- const extensionSection: EditSectionViewModel = {
89
- kind: "extension",
90
- title: "Custom",
91
- component: {},
92
- contributesToFormSubmit: false,
93
- };
94
- const composedExtensionSection: EditSectionViewModel = {
95
- ...extensionSection,
96
- contributesToFormSubmit: true,
97
- };
98
-
99
- describe("hasEditableSection", () => {
100
- // A read-only inspector detail (export-job/download-attempt) marks every field
101
- // readOnly + has no create/delete — there is nothing to submit, so the Save
102
- // button must not render at all (a disabled one reads as a broken control).
103
- test("every field readOnly → false (no Save button)", () => {
104
- expect(hasEditableSection([fieldsSection(true, true, true)])).toBe(false);
105
- });
106
-
107
- test("one editable field → true", () => {
108
- expect(hasEditableSection([fieldsSection(true, false)])).toBe(true);
109
- });
110
-
111
- test("editable field in a later section → true", () => {
112
- expect(hasEditableSection([fieldsSection(true), fieldsSection(false)])).toBe(true);
113
- });
114
-
115
- test("extension section without form-submit opt-in → false", () => {
116
- expect(hasEditableSection([extensionSection])).toBe(false);
117
- });
118
-
119
- test("extension section with contributesToFormSubmit → true", () => {
120
- expect(hasEditableSection([composedExtensionSection])).toBe(true);
121
- });
122
-
123
- test("no sections → false", () => {
124
- expect(hasEditableSection([])).toBe(false);
125
- });
126
-
127
- // writeForm owns its own submit button (write-form-section-submit) —
128
- // counting it here would additionally render the screen-level Save button
129
- // (render-edit-submit) on a projectionDetail, which has no form submit to
130
- // wire it to.
131
- test("writeForm section → false (has its own submit, not the screen's)", () => {
132
- const writeFormSection: EditSectionViewModel = {
133
- kind: "writeForm",
134
- columns: 1,
135
- handler: "orders:write:add-note",
136
- fields: [field(false)],
137
- };
138
- expect(hasEditableSection([writeFormSection])).toBe(false);
139
- });
140
-
141
- test("hidden section with editable field → false (user cannot see the field)", () => {
142
- const hiddenSectionWithEditableField: EditSectionViewModel = {
143
- kind: "fields",
144
- columns: 1,
145
- visible: false,
146
- fields: [field(false)],
147
- };
148
- expect(hasEditableSection([hiddenSectionWithEditableField])).toBe(false);
149
- });
150
-
151
- test("visible section with only a field-hidden editable field → false", () => {
152
- const section: EditSectionViewModel = {
153
- kind: "fields",
154
- columns: 1,
155
- visible: true,
156
- fields: [
157
- {
158
- field: "f",
159
- label: "F",
160
- type: "text",
161
- value: "",
162
- visible: false,
163
- readOnly: false,
164
- required: false,
165
- },
166
- ],
167
- };
168
- expect(hasEditableSection([section])).toBe(false);
169
- });
170
-
171
- // The #1689 regression: the section is visible because field A is visible
172
- // (readOnly), but the only editable field B is hidden by its own
173
- // FieldCondition. Old code checked section.visible + !readOnly only, so it
174
- // saw B's !readOnly and returned true — Save appeared over zero editable
175
- // visible fields.
176
- test("visible section, visible readOnly field + hidden editable field → false", () => {
177
- const section: EditSectionViewModel = {
178
- kind: "fields",
179
- columns: 1,
180
- visible: true,
181
- fields: [
182
- {
183
- field: "a",
184
- label: "A",
185
- type: "text",
186
- value: "",
187
- visible: true,
188
- readOnly: true,
189
- required: false,
190
- },
191
- {
192
- field: "b",
193
- label: "B",
194
- type: "text",
195
- value: "",
196
- visible: false,
197
- readOnly: false,
198
- required: false,
199
- },
200
- ],
201
- };
202
- expect(hasEditableSection([section])).toBe(false);
203
- });
204
- });
205
-
206
- const namedField = (name: string): EditFieldViewModel => ({
207
- field: name,
208
- label: name,
209
- type: "text",
210
- value: "",
211
- visible: true,
212
- readOnly: false,
213
- required: false,
214
- });
215
- const namedFieldsSection = (...names: string[]): EditSectionViewModel => ({
216
- kind: "fields",
217
- columns: 1,
218
- visible: true,
219
- fields: names.map(namedField),
220
- });
221
-
222
- describe("filterEditSections", () => {
223
- test("fieldsFilter undefined → returns the same array reference (unchanged behavior)", () => {
224
- const sections = [namedFieldsSection("a", "b")];
225
- expect(filterEditSections(sections, undefined)).toBe(sections);
226
- });
227
-
228
- test("mixed section → only the filtered-in fields remain in section.fields", () => {
229
- const sections = [namedFieldsSection("a", "b", "c")];
230
- const result = filterEditSections(sections, ["a", "c"]);
231
- expect(result).toHaveLength(1);
232
- expect(result[0]?.kind).toBe("fields");
233
- expect(result[0]?.kind === "fields" ? result[0].fields.map((f) => f.field) : []).toEqual([
234
- "a",
235
- "c",
236
- ]);
237
- });
238
-
239
- test("section whose filtered field list becomes empty is dropped entirely, not rendered empty", () => {
240
- const sections = [namedFieldsSection("a", "b"), namedFieldsSection("c")];
241
- const result = filterEditSections(sections, ["c"]);
242
- expect(result).toHaveLength(1);
243
- expect(result[0]?.kind === "fields" ? result[0].fields.map((f) => f.field) : []).toEqual(["c"]);
244
- });
245
-
246
- test("extension section always survives the filter regardless of its content", () => {
247
- const sections = [extensionSection, namedFieldsSection("a")];
248
- const result = filterEditSections(sections, ["zzz"]);
249
- expect(result).toEqual([extensionSection]);
250
- });
251
- });
252
-
253
- describe("hasIssueWithoutRenderedField", () => {
254
- test("issue path matches a rendered field → false", () => {
255
- expect(hasIssueWithoutRenderedField(["title"], [namedFieldsSection("title", "body")])).toBe(
256
- false,
257
- );
258
- });
259
-
260
- // Root-level refine issues (`version`/`id`/`changes`) never match a rendered
261
- // field name — the banner is the only place that can show them.
262
- test("root-level refine path (version) → true", () => {
263
- expect(hasIssueWithoutRenderedField(["version"], [namedFieldsSection("title")])).toBe(true);
264
- });
265
-
266
- test("(root) path → true", () => {
267
- expect(hasIssueWithoutRenderedField(["(root)"], [namedFieldsSection("title")])).toBe(true);
268
- });
269
-
270
- test("path matches a field hidden by its own condition → true", () => {
271
- const section: EditSectionViewModel = {
272
- kind: "fields",
273
- columns: 1,
274
- visible: true,
275
- fields: [
276
- {
277
- field: "a",
278
- label: "A",
279
- type: "text",
280
- value: "",
281
- visible: true,
282
- readOnly: false,
283
- required: false,
284
- },
285
- {
286
- field: "b",
287
- label: "B",
288
- type: "text",
289
- value: "",
290
- visible: false,
291
- readOnly: false,
292
- required: false,
293
- },
294
- ],
295
- };
296
- expect(hasIssueWithoutRenderedField(["b"], [section])).toBe(true);
297
- });
298
-
299
- test("dotted path (tasks.2.title) matches on its first segment → false", () => {
300
- expect(hasIssueWithoutRenderedField(["tasks.2.title"], [namedFieldsSection("tasks")])).toBe(
301
- false,
302
- );
303
- });
304
-
305
- test("mixed: one matched, one unmatched → true", () => {
306
- expect(hasIssueWithoutRenderedField(["title", "version"], [namedFieldsSection("title")])).toBe(
307
- true,
308
- );
309
- });
310
-
311
- test("empty issue list → true (single-section path always shows the banner then)", () => {
312
- expect(hasIssueWithoutRenderedField([], [namedFieldsSection("title")])).toBe(true);
313
- });
314
- });