@multiplatform.one/frappe-ui 7.90.6 → 8.0.0

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 (261) hide show
  1. package/dist/cjs/FrappeListView.cjs +0 -2
  2. package/dist/cjs/FrappeListView.native.js +0 -2
  3. package/dist/cjs/FrappeListView.native.js.map +1 -1
  4. package/dist/cjs/desk/index.cjs +41 -0
  5. package/dist/cjs/desk/index.native.js +44 -0
  6. package/dist/cjs/desk/index.native.js.map +1 -0
  7. package/dist/cjs/fields/Link.cjs +0 -4
  8. package/dist/cjs/fields/Link.native.js +0 -4
  9. package/dist/cjs/fields/Link.native.js.map +1 -1
  10. package/dist/cjs/index.cjs +0 -2
  11. package/dist/cjs/index.gnome.cjs +203 -0
  12. package/dist/cjs/index.gnome.native.js +206 -0
  13. package/dist/cjs/index.gnome.native.js.map +1 -0
  14. package/dist/cjs/index.native.js +0 -2
  15. package/dist/cjs/index.native.js.map +1 -1
  16. package/dist/esm/ChartCard.native.js.map +1 -1
  17. package/dist/esm/FileBrowser/AttachLibrary.native.js.map +1 -1
  18. package/dist/esm/FileBrowser/folderTree.native.js.map +1 -1
  19. package/dist/esm/FileBrowser/index.native.js.map +1 -1
  20. package/dist/esm/FileBrowser/types.native.js.map +1 -1
  21. package/dist/esm/FileBrowser/viewPersistence.native.js.map +1 -1
  22. package/dist/esm/FormSidebar.native.js.map +1 -1
  23. package/dist/esm/FrappeAssignment.native.js.map +1 -1
  24. package/dist/esm/FrappeCalendar.native.js.map +1 -1
  25. package/dist/esm/FrappeChartCard.native.js.map +1 -1
  26. package/dist/esm/FrappeConnectionIndicator.native.js.map +1 -1
  27. package/dist/esm/FrappeDashboard.native.js.map +1 -1
  28. package/dist/esm/FrappeForm.native.js.map +1 -1
  29. package/dist/esm/FrappeFormActions.native.js.map +1 -1
  30. package/dist/esm/FrappeFormConnections.native.js.map +1 -1
  31. package/dist/esm/FrappeGantt.native.js.map +1 -1
  32. package/dist/esm/FrappeImageView.native.js.map +1 -1
  33. package/dist/esm/FrappeInsights.native.js.map +1 -1
  34. package/dist/esm/FrappeKPICard.native.js.map +1 -1
  35. package/dist/esm/FrappeKanban.native.js.map +1 -1
  36. package/dist/esm/FrappeLikedBy.native.js.map +1 -1
  37. package/dist/esm/FrappeList.native.js.map +1 -1
  38. package/dist/esm/FrappeListView.mjs +0 -2
  39. package/dist/esm/FrappeListView.mjs.map +1 -1
  40. package/dist/esm/FrappeListView.native.js +0 -2
  41. package/dist/esm/FrappeListView.native.js.map +1 -1
  42. package/dist/esm/FrappeNotConfigured.native.js.map +1 -1
  43. package/dist/esm/FrappeProvider.native.js.map +1 -1
  44. package/dist/esm/FrappeReconnectIndicator.native.js.map +1 -1
  45. package/dist/esm/FrappeRecordView.native.js.map +1 -1
  46. package/dist/esm/FrappeReportBuilder.native.js.map +1 -1
  47. package/dist/esm/FrappeReportView.native.js.map +1 -1
  48. package/dist/esm/FrappeRowsReader.native.js.map +1 -1
  49. package/dist/esm/FrappeTable.native.js.map +1 -1
  50. package/dist/esm/FrappeTreeView.native.js.map +1 -1
  51. package/dist/esm/SyncErrorNotice.native.js.map +1 -1
  52. package/dist/esm/commentActions.native.js.map +1 -1
  53. package/dist/esm/components/attachments.native.js.map +1 -1
  54. package/dist/esm/components/tags.native.js.map +1 -1
  55. package/dist/esm/components/timeline.native.js.map +1 -1
  56. package/dist/esm/dependsOn.native.js.map +1 -1
  57. package/dist/esm/desk/DeskFormPage.native.js.map +1 -1
  58. package/dist/esm/desk/DeskFormSidebar.native.js.map +1 -1
  59. package/dist/esm/desk/DeskListPage.native.js.map +1 -1
  60. package/dist/esm/desk/DeskShell.native.js.map +1 -1
  61. package/dist/esm/desk/DeskSidebar.native.js.map +1 -1
  62. package/dist/esm/desk/awesomebar.native.js.map +1 -1
  63. package/dist/esm/desk/index.mjs +8 -0
  64. package/dist/esm/desk/index.mjs.map +1 -0
  65. package/dist/esm/desk/index.native.js +8 -0
  66. package/dist/esm/desk/index.native.js.map +1 -0
  67. package/dist/esm/fieldOverrides.native.js.map +1 -1
  68. package/dist/esm/fieldRegistry.native.js.map +1 -1
  69. package/dist/esm/fields/Barcode/index.native.js.map +1 -1
  70. package/dist/esm/fields/Check/index.native.js.map +1 -1
  71. package/dist/esm/fields/CodeEditor/index.native.js.map +1 -1
  72. package/dist/esm/fields/Color/index.native.js.map +1 -1
  73. package/dist/esm/fields/Data/index.native.js.map +1 -1
  74. package/dist/esm/fields/Date/catalog.native.js.map +1 -1
  75. package/dist/esm/fields/Date/index.native.js.map +1 -1
  76. package/dist/esm/fields/Date/types.native.js.map +1 -1
  77. package/dist/esm/fields/Date/wire.native.js.map +1 -1
  78. package/dist/esm/fields/DateRange/index.native.js.map +1 -1
  79. package/dist/esm/fields/Duration/index.native.js.map +1 -1
  80. package/dist/esm/fields/DynamicLink.native.js.map +1 -1
  81. package/dist/esm/fields/FileUploadField/index.native.js.map +1 -1
  82. package/dist/esm/fields/Geolocation/index.native.js.map +1 -1
  83. package/dist/esm/fields/HTML/index.native.js.map +1 -1
  84. package/dist/esm/fields/Heading/index.native.js.map +1 -1
  85. package/dist/esm/fields/Icon/index.native.js.map +1 -1
  86. package/dist/esm/fields/Link.mjs +0 -4
  87. package/dist/esm/fields/Link.mjs.map +1 -1
  88. package/dist/esm/fields/Link.native.js +0 -4
  89. package/dist/esm/fields/Link.native.js.map +1 -1
  90. package/dist/esm/fields/LinkTree.native.js.map +1 -1
  91. package/dist/esm/fields/LongText/index.native.js.map +1 -1
  92. package/dist/esm/fields/MentionInput/index.native.js.map +1 -1
  93. package/dist/esm/fields/Numeric/index.native.js.map +1 -1
  94. package/dist/esm/fields/OTPInput/index.native.js.map +1 -1
  95. package/dist/esm/fields/Password/index.native.js.map +1 -1
  96. package/dist/esm/fields/Phone/index.native.js.map +1 -1
  97. package/dist/esm/fields/Progress/index.native.js.map +1 -1
  98. package/dist/esm/fields/RadioGroup/index.native.js.map +1 -1
  99. package/dist/esm/fields/Rating/index.native.js.map +1 -1
  100. package/dist/esm/fields/ReadOnly/index.native.js.map +1 -1
  101. package/dist/esm/fields/Select/index.native.js.map +1 -1
  102. package/dist/esm/fields/Signature/index.native.js.map +1 -1
  103. package/dist/esm/fields/Slider/index.native.js.map +1 -1
  104. package/dist/esm/fields/SmallText/index.native.js.map +1 -1
  105. package/dist/esm/fields/Switch/index.native.js.map +1 -1
  106. package/dist/esm/fields/TableMultiSelect.native.js.map +1 -1
  107. package/dist/esm/fields/TextEditor/types.native.js.map +1 -1
  108. package/dist/esm/fields/TimePicker/index.native.js.map +1 -1
  109. package/dist/esm/fields/ToggleGroup/index.native.js.map +1 -1
  110. package/dist/esm/fields/shared/EditorPreviewPanes.native.js.map +1 -1
  111. package/dist/esm/filterFields.native.js.map +1 -1
  112. package/dist/esm/formEngine/createPrefill.native.js.map +1 -1
  113. package/dist/esm/formEngine/dirtyTracking.native.js.map +1 -1
  114. package/dist/esm/formEngine/docLifecycle.native.js.map +1 -1
  115. package/dist/esm/formEngine/docPermissions.native.js.map +1 -1
  116. package/dist/esm/formEngine/doctypeMeta.native.js.map +1 -1
  117. package/dist/esm/formEngine/fetchFrom.native.js.map +1 -1
  118. package/dist/esm/formEngine/fieldPermissions.native.js.map +1 -1
  119. package/dist/esm/formEngine/formActions.native.js.map +1 -1
  120. package/dist/esm/formEngine/formLayout.native.js.map +1 -1
  121. package/dist/esm/formEngine/index.native.js.map +1 -1
  122. package/dist/esm/formEngine/useAmendedAs.native.js.map +1 -1
  123. package/dist/esm/formEngine/useDocRights.native.js.map +1 -1
  124. package/dist/esm/formEngine/useFormEngine.native.js.map +1 -1
  125. package/dist/esm/formEngine/useSessionRoles.native.js.map +1 -1
  126. package/dist/esm/frappeConnections.native.js.map +1 -1
  127. package/dist/esm/frappeEventColor.native.js.map +1 -1
  128. package/dist/esm/frappeNavigation.native.js.map +1 -1
  129. package/dist/esm/frappeStoryTarget.native.js.map +1 -1
  130. package/dist/esm/frappeUsers.native.js.map +1 -1
  131. package/dist/esm/index.gnome.mjs +59 -0
  132. package/dist/esm/index.gnome.mjs.map +1 -0
  133. package/dist/esm/index.gnome.native.js +59 -0
  134. package/dist/esm/index.gnome.native.js.map +1 -0
  135. package/dist/esm/index.mjs +1 -2
  136. package/dist/esm/index.mjs.map +1 -1
  137. package/dist/esm/index.native.js +1 -2
  138. package/dist/esm/index.native.js.map +1 -1
  139. package/dist/esm/insights/InsightsFacetTags.native.js.map +1 -1
  140. package/dist/esm/insights/InsightsKpiBand.native.js.map +1 -1
  141. package/dist/esm/insights/InsightsLeaders.native.js.map +1 -1
  142. package/dist/esm/insights/InsightsRecords.native.js.map +1 -1
  143. package/dist/esm/insights/InsightsTable.native.js.map +1 -1
  144. package/dist/esm/insights/insightsData.native.js.map +1 -1
  145. package/dist/esm/insights/insightsRecord.native.js.map +1 -1
  146. package/dist/esm/list/BandFilters.native.js.map +1 -1
  147. package/dist/esm/list/FacetBadge.native.js.map +1 -1
  148. package/dist/esm/list/FacetList.native.js.map +1 -1
  149. package/dist/esm/list/FacetRail.native.js.map +1 -1
  150. package/dist/esm/list/ImageWell.native.js.map +1 -1
  151. package/dist/esm/list/ListControls.native.js.map +1 -1
  152. package/dist/esm/list/ListControlsSheet.native.js.map +1 -1
  153. package/dist/esm/list/ListFilterPanel.native.js.map +1 -1
  154. package/dist/esm/list/ListFiltersButton.native.js.map +1 -1
  155. package/dist/esm/list/ListSidebarToolbar.native.js.map +1 -1
  156. package/dist/esm/list/ListToastHost.native.js.map +1 -1
  157. package/dist/esm/list/ListToolsSheet.native.js.map +1 -1
  158. package/dist/esm/list/RangeFilter.native.js.map +1 -1
  159. package/dist/esm/list/ShufflePanel.native.js.map +1 -1
  160. package/dist/esm/list/defaultProjection.native.js.map +1 -1
  161. package/dist/esm/list/facetBadgeSteps.native.js.map +1 -1
  162. package/dist/esm/list/facetReading.native.js.map +1 -1
  163. package/dist/esm/list/favorites.native.js.map +1 -1
  164. package/dist/esm/list/listColumns.native.js.map +1 -1
  165. package/dist/esm/list/listControls.fixture.native.js.map +1 -1
  166. package/dist/esm/list/listFields.native.js.map +1 -1
  167. package/dist/esm/list/listFormat.native.js.map +1 -1
  168. package/dist/esm/list/listState.native.js.map +1 -1
  169. package/dist/esm/list/recordLook.native.js.map +1 -1
  170. package/dist/esm/list/statusColors.native.js.map +1 -1
  171. package/dist/esm/list/useListControls.native.js.map +1 -1
  172. package/dist/esm/liveErrors.native.js.map +1 -1
  173. package/dist/esm/measures.native.js.map +1 -1
  174. package/dist/esm/record/RecordBars.native.js.map +1 -1
  175. package/dist/esm/record/RecordGallery.native.js.map +1 -1
  176. package/dist/esm/record/RecordHero.native.js.map +1 -1
  177. package/dist/esm/record/RecordPage.native.js.map +1 -1
  178. package/dist/esm/record/RecordPager.native.js.map +1 -1
  179. package/dist/esm/record/RecordRows.native.js.map +1 -1
  180. package/dist/esm/record/recordData.native.js.map +1 -1
  181. package/dist/esm/record/recordSections.native.js.map +1 -1
  182. package/dist/esm/recordShape.native.js.map +1 -1
  183. package/dist/esm/report/ReportGrid.native.js.map +1 -1
  184. package/dist/esm/report/ReportViewToolbar.native.js.map +1 -1
  185. package/dist/esm/report/exportCsv.native.js.map +1 -1
  186. package/dist/esm/report/queryReport.native.js.map +1 -1
  187. package/dist/esm/report/queryReportColumns.native.js.map +1 -1
  188. package/dist/esm/report/reportViewModel.native.js.map +1 -1
  189. package/dist/esm/report/savedReports.native.js.map +1 -1
  190. package/dist/esm/report/useFrappeReport.native.js.map +1 -1
  191. package/dist/esm/report/useQueryReport.native.js.map +1 -1
  192. package/dist/esm/resolveControl.native.js.map +1 -1
  193. package/dist/esm/schemaForm/SchemaWizard.native.js.map +1 -1
  194. package/dist/esm/schemaForm/applyFieldOrder.native.js.map +1 -1
  195. package/dist/esm/schemaForm/index.native.js.map +1 -1
  196. package/dist/esm/sessionUser.native.js.map +1 -1
  197. package/dist/esm/shared/devWarn.native.js.map +1 -1
  198. package/dist/esm/shared/useKeyboardInset.native.js.map +1 -1
  199. package/dist/esm/syncError.native.js.map +1 -1
  200. package/dist/esm/useConnectionFilters.native.js.map +1 -1
  201. package/dist/esm/useFrappeAggregate.native.js.map +1 -1
  202. package/dist/esm/useFrappeFacets.native.js.map +1 -1
  203. package/dist/esm/useFrappeForm.native.js.map +1 -1
  204. package/dist/esm/useFrappePagination.native.js.map +1 -1
  205. package/dist/esm/useFrappeRows.native.js.map +1 -1
  206. package/dist/jsx/FrappeListView.mjs +0 -2
  207. package/dist/jsx/FrappeListView.mjs.map +1 -1
  208. package/dist/jsx/FrappeListView.native.js +0 -2
  209. package/dist/jsx/FrappeListView.native.js.map +1 -1
  210. package/dist/jsx/desk/index.mjs +8 -0
  211. package/dist/jsx/desk/index.mjs.map +1 -0
  212. package/dist/jsx/desk/index.native.js +44 -0
  213. package/dist/jsx/desk/index.native.js.map +1 -0
  214. package/dist/jsx/fields/Link.mjs +0 -4
  215. package/dist/jsx/fields/Link.mjs.map +1 -1
  216. package/dist/jsx/fields/Link.native.js +0 -4
  217. package/dist/jsx/fields/Link.native.js.map +1 -1
  218. package/dist/jsx/index.gnome.mjs +59 -0
  219. package/dist/jsx/index.gnome.mjs.map +1 -0
  220. package/dist/jsx/index.gnome.native.js +206 -0
  221. package/dist/jsx/index.gnome.native.js.map +1 -0
  222. package/dist/jsx/index.js +1 -2
  223. package/dist/jsx/index.js.map +1 -1
  224. package/dist/jsx/index.mjs +1 -2
  225. package/dist/jsx/index.mjs.map +1 -1
  226. package/dist/jsx/index.native.js +0 -2
  227. package/dist/jsx/index.native.js.map +1 -1
  228. package/package.json +34 -15
  229. package/src/FrappeListView.tsx +0 -4
  230. package/src/desk/index.ts +18 -0
  231. package/src/fields/Link.tsx +0 -12
  232. package/src/formEngine/index.ts +2 -2
  233. package/src/index.gnome.ts +191 -0
  234. package/src/index.ts +2 -7
  235. package/types/FrappeListView.d.ts +0 -2
  236. package/types/FrappeListView.d.ts.map +1 -1
  237. package/types/desk/index.d.ts +12 -0
  238. package/types/desk/index.d.ts.map +1 -0
  239. package/types/fields/Link.d.ts +0 -8
  240. package/types/fields/Link.d.ts.map +1 -1
  241. package/types/formEngine/index.d.ts +2 -2
  242. package/types/index.d.ts +0 -1
  243. package/types/index.d.ts.map +1 -1
  244. package/types/index.gnome.d.ts +105 -0
  245. package/types/index.gnome.d.ts.map +1 -0
  246. package/dist/cjs/fields/FileUpload.cjs +0 -404
  247. package/dist/cjs/fields/FileUpload.native.js +0 -431
  248. package/dist/cjs/fields/FileUpload.native.js.map +0 -1
  249. package/dist/esm/fields/FileUpload.mjs +0 -379
  250. package/dist/esm/fields/FileUpload.mjs.map +0 -1
  251. package/dist/esm/fields/FileUpload.native.js +0 -403
  252. package/dist/esm/fields/FileUpload.native.js.map +0 -1
  253. package/dist/jsx/fields/FileUpload.mjs +0 -379
  254. package/dist/jsx/fields/FileUpload.mjs.map +0 -1
  255. package/dist/jsx/fields/FileUpload.native.js +0 -431
  256. package/dist/jsx/fields/FileUpload.native.js.map +0 -1
  257. package/src/fields/FileUpload.spec.tsx +0 -351
  258. package/src/fields/FileUpload.tsx +0 -472
  259. package/src/fields/FrappeFileUpload.stories.tsx +0 -139
  260. package/types/fields/FileUpload.d.ts +0 -27
  261. package/types/fields/FileUpload.d.ts.map +0 -1
@@ -1,351 +0,0 @@
1
- /**
2
- * Frappe FileUpload contract specs (the live-data uploader behind the
3
- * "FrappeUI/FileUpload" catalog entry — not the forms dropzone, and not the
4
- * FileUploadField registry adapter).
5
- *
6
- * Everything worth pinning here is transport state, because that is what the
7
- * component owns and what a screenshot cannot show: the size gate before a
8
- * byte is read, the progress channel, the failure row that keeps the existing
9
- * value instead of wiping it (DG-VAL-04), retry, and the abort that has to
10
- * actually reach the handler's AbortSignal.
11
- *
12
- * The default transport inserts into the File doctype, so every test injects
13
- * `onUpload` — the documented override — and drives it by hand.
14
- */
15
- import { renderWithProviders } from "@multiplatform.one/test-utils";
16
- import { act, cleanup, fireEvent, waitFor } from "@testing-library/react";
17
- import { afterEach, describe, expect, it, vi } from "vitest";
18
- import { FileUpload } from "./FileUpload";
19
-
20
- afterEach(cleanup);
21
-
22
- const hiddenInput = (container: HTMLElement) =>
23
- container.querySelector('input[type="file"]') as HTMLInputElement;
24
-
25
- function makeFile(name = "report.pdf", size = 1024, type = "application/pdf") {
26
- const file = new File(["x"], name, { type });
27
- Object.defineProperty(file, "size", { value: size });
28
- return file;
29
- }
30
-
31
- async function selectFile(container: HTMLElement, file: File) {
32
- await act(async () => {
33
- fireEvent.change(hiddenInput(container), { target: { files: [file] } });
34
- });
35
- }
36
-
37
- /** A handler the test resolves or rejects on demand. */
38
- function deferredUpload() {
39
- let resolve!: (url: string) => void;
40
- let reject!: (err: Error) => void;
41
- let progress!: (percent: number) => void;
42
- let signal!: AbortSignal;
43
- const handler = vi.fn(
44
- (_file: File, ctx: { onProgress: (p: number) => void; signal: AbortSignal }) => {
45
- progress = ctx.onProgress;
46
- signal = ctx.signal;
47
- return new Promise<string>((res, rej) => {
48
- resolve = res;
49
- reject = rej;
50
- });
51
- },
52
- );
53
- return {
54
- handler,
55
- resolve: (url: string) => resolve(url),
56
- reject: (err: Error) => reject(err),
57
- progress: (p: number) => progress(p),
58
- get signal() {
59
- return signal;
60
- },
61
- };
62
- }
63
-
64
- describe("FileUpload empty state", () => {
65
- it("offers a Choose File affordance and nothing else", () => {
66
- const result = renderWithProviders(<FileUpload label="Attachment" onChange={() => {}} />);
67
- expect(result.getButton("Choose File")).toBeTruthy();
68
- expect(result.container.querySelector('[data-file-upload="transfer"]')).toBeNull();
69
- });
70
-
71
- it("marks a required field on its label", () => {
72
- const result = renderWithProviders(
73
- <FileUpload label="Attachment" required onChange={() => {}} />,
74
- );
75
- expect(result.container.textContent).toContain("Attachment");
76
- expect(result.container.textContent).toContain("*");
77
- });
78
-
79
- it("skeleton branch offers no affordance at all", () => {
80
- const result = renderWithProviders(
81
- <FileUpload label="Attachment" skeleton onChange={() => {}} />,
82
- );
83
- expect(result.getButton("Choose File")).toBeUndefined();
84
- });
85
-
86
- it("shows helperText while nothing is in flight", () => {
87
- const result = renderWithProviders(
88
- <FileUpload label="Attachment" helperText="PDF or PNG, under 5MB" onChange={() => {}} />,
89
- );
90
- expect(result.container.textContent).toContain("PDF or PNG, under 5MB");
91
- });
92
- });
93
-
94
- describe("FileUpload size gate", () => {
95
- it("refuses a file over maxSize before the transport is ever called", async () => {
96
- const onUpload = vi.fn();
97
- const onChange = vi.fn();
98
- const result = renderWithProviders(
99
- <FileUpload label="Attachment" maxSize={1024} onUpload={onUpload} onChange={onChange} />,
100
- );
101
- await selectFile(result.container, makeFile("big.pdf", 2048));
102
- expect(onUpload).not.toHaveBeenCalled();
103
- expect(onChange).not.toHaveBeenCalled();
104
- expect(result.container.textContent).toContain("exceeds");
105
- });
106
-
107
- it("accepts a file at exactly maxSize", async () => {
108
- const onUpload = vi.fn().mockResolvedValue("/files/ok.pdf");
109
- const result = renderWithProviders(
110
- <FileUpload label="Attachment" maxSize={1024} onUpload={onUpload} onChange={() => {}} />,
111
- );
112
- await selectFile(result.container, makeFile("ok.pdf", 1024));
113
- expect(onUpload).toHaveBeenCalledTimes(1);
114
- });
115
- });
116
-
117
- /**
118
- * i18n interpolation is INERT under `renderWithProviders`: the provider hands
119
- * back the raw key, so `t("Retry {{name}}", { name })` renders the accessible
120
- * name literally as `Retry {{name}}` rather than `Retry report.pdf`. Buttons
121
- * are therefore looked up by the stable half of the label, and the per-file
122
- * parameterisation is asserted separately (`ariaNames` below) so a dropped
123
- * `aria-label` still fails. Recorded on MPO-123.
124
- */
125
- const buttonLabelled = (container: HTMLElement, prefix: string) =>
126
- Array.from(container.querySelectorAll('button, [role="button"]')).find((b) =>
127
- (b.getAttribute("aria-label") ?? "").startsWith(prefix),
128
- );
129
-
130
- describe("FileUpload transfer", () => {
131
- it("shows the file name and a live progressbar while uploading", async () => {
132
- const upload = deferredUpload();
133
- const result = renderWithProviders(
134
- <FileUpload label="Attachment" onUpload={upload.handler} onChange={() => {}} />,
135
- );
136
- await selectFile(result.container, makeFile("report.pdf"));
137
-
138
- const row = result.container.querySelector('[data-file-upload="transfer"]');
139
- expect(row?.getAttribute("data-transfer-status")).toBe("uploading");
140
- expect(result.container.textContent).toContain("report.pdf");
141
-
142
- const bar = result.container.querySelector('[data-file-upload="progress"]');
143
- expect(bar?.getAttribute("aria-valuenow")).toBe("0");
144
- await act(async () => {
145
- upload.progress(42);
146
- });
147
- expect(
148
- result.container
149
- .querySelector('[data-file-upload="progress"]')
150
- ?.getAttribute("aria-valuenow"),
151
- ).toBe("42");
152
- });
153
-
154
- it("clamps a nonsense progress report into 0..100", async () => {
155
- const upload = deferredUpload();
156
- const result = renderWithProviders(
157
- <FileUpload label="Attachment" onUpload={upload.handler} onChange={() => {}} />,
158
- );
159
- await selectFile(result.container, makeFile());
160
- await act(async () => {
161
- upload.progress(999);
162
- });
163
- expect(
164
- result.container
165
- .querySelector('[data-file-upload="progress"]')
166
- ?.getAttribute("aria-valuenow"),
167
- ).toBe("100");
168
- });
169
-
170
- it("emits the committed URL through onChange and clears the transfer row", async () => {
171
- const upload = deferredUpload();
172
- const onChange = vi.fn();
173
- const result = renderWithProviders(
174
- <FileUpload label="Attachment" onUpload={upload.handler} onChange={onChange} />,
175
- );
176
- await selectFile(result.container, makeFile());
177
- await act(async () => {
178
- upload.resolve("/files/report.pdf");
179
- });
180
- await waitFor(() => expect(onChange).toHaveBeenCalledWith("/files/report.pdf"));
181
- expect(result.container.querySelector('[data-file-upload="transfer"]')).toBeNull();
182
- });
183
-
184
- it("keeps the existing value when the transfer fails (DG-VAL-04)", async () => {
185
- const upload = deferredUpload();
186
- const onChange = vi.fn();
187
- const result = renderWithProviders(
188
- <FileUpload
189
- label="Attachment"
190
- value="/files/previous.pdf"
191
- onUpload={upload.handler}
192
- onChange={onChange}
193
- />,
194
- );
195
- await selectFile(result.container, makeFile());
196
- await act(async () => {
197
- upload.reject(new Error("network is down"));
198
- });
199
- await waitFor(() =>
200
- expect(
201
- result.container
202
- .querySelector('[data-file-upload="transfer"]')
203
- ?.getAttribute("data-transfer-status"),
204
- ).toBe("failed"),
205
- );
206
- expect(onChange).not.toHaveBeenCalled();
207
- expect(result.container.textContent).toContain("network is down");
208
- expect(result.container.textContent).toContain("previous.pdf");
209
- });
210
-
211
- it("offers Retry and Remove on a failed row, and Retry re-runs the transport", async () => {
212
- const upload = deferredUpload();
213
- const result = renderWithProviders(
214
- <FileUpload label="Attachment" onUpload={upload.handler} onChange={() => {}} />,
215
- );
216
- await selectFile(result.container, makeFile("report.pdf"));
217
- await act(async () => {
218
- upload.reject(new Error("boom"));
219
- });
220
- await waitFor(() => expect(buttonLabelled(result.container, "Retry ")).toBeTruthy());
221
- const remove = buttonLabelled(result.container, "Remove ");
222
- expect(remove).toBeTruthy();
223
- // Both affordances carry a per-file accessible name, so a screen reader
224
- // can tell one failed row's Retry from another's.
225
- expect(buttonLabelled(result.container, "Retry ")?.getAttribute("aria-label")).toMatch(
226
- /^Retry \S/,
227
- );
228
- expect(remove?.getAttribute("aria-label")).toMatch(/^Remove \S/);
229
- // LC-260: the button shows only its glyph; the name lives on the label.
230
- expect(buttonLabelled(result.container, "Retry ")?.textContent).toBe("");
231
- expect(buttonLabelled(result.container, "Retry ")?.querySelector("svg")).toBeTruthy();
232
-
233
- await act(async () => {
234
- fireEvent.click(buttonLabelled(result.container, "Retry ") as Element);
235
- });
236
- expect(upload.handler).toHaveBeenCalledTimes(2);
237
- });
238
-
239
- it("Cancel aborts the in-flight signal and drops the row", async () => {
240
- const upload = deferredUpload();
241
- const result = renderWithProviders(
242
- <FileUpload label="Attachment" onUpload={upload.handler} onChange={() => {}} />,
243
- );
244
- await selectFile(result.container, makeFile("report.pdf"));
245
- expect(upload.signal.aborted).toBe(false);
246
-
247
- await act(async () => {
248
- fireEvent.click(buttonLabelled(result.container, "Cancel ") as Element);
249
- });
250
- expect(upload.signal.aborted).toBe(true);
251
- expect(result.container.querySelector('[data-file-upload="transfer"]')).toBeNull();
252
- });
253
-
254
- it("an aborted transport never emits onChange", async () => {
255
- const upload = deferredUpload();
256
- const onChange = vi.fn();
257
- const result = renderWithProviders(
258
- <FileUpload label="Attachment" onUpload={upload.handler} onChange={onChange} />,
259
- );
260
- await selectFile(result.container, makeFile("report.pdf"));
261
- await act(async () => {
262
- fireEvent.click(buttonLabelled(result.container, "Cancel ") as Element);
263
- upload.resolve("/files/late.pdf");
264
- });
265
- expect(onChange).not.toHaveBeenCalled();
266
- });
267
- });
268
-
269
- describe("FileUpload with a value", () => {
270
- it("previews an image value as an <img> with a string src", () => {
271
- const result = renderWithProviders(
272
- <FileUpload label="Photo" isImage value="/files/photo.png" onChange={() => {}} />,
273
- );
274
- const img = result.container.querySelector("img");
275
- expect(img?.getAttribute("src")).toBe("/files/photo.png");
276
- });
277
-
278
- it("shows the file name for a non-image value", () => {
279
- const result = renderWithProviders(
280
- <FileUpload label="Attachment" value="/files/deep/report.pdf" onChange={() => {}} />,
281
- );
282
- expect(result.container.textContent).toContain("report.pdf");
283
- expect(result.container.querySelector("img")).toBeNull();
284
- });
285
-
286
- it("offers Replace and Remove, and Remove clears the value", () => {
287
- const onChange = vi.fn();
288
- const result = renderWithProviders(
289
- <FileUpload label="Attachment" value="/files/report.pdf" onChange={onChange} />,
290
- );
291
- expect(result.getButton("Replace")).toBeTruthy();
292
- fireEvent.click(result.getButton("Remove") as Element);
293
- expect(onChange).toHaveBeenCalledWith("");
294
- });
295
-
296
- it("an image's Replace and Remove are glyphs in one media capsule over it (LC-260, LC-176)", () => {
297
- const result = renderWithProviders(
298
- <FileUpload label="Photo" value="/files/sprite.png" isImage onChange={() => {}} />,
299
- );
300
- const actions = result.container.querySelector('[data-file-upload="preview-actions"]');
301
- expect(actions).toBeTruthy();
302
- const capsule = actions!.querySelector('[data-mp-icon-button-group="media"]');
303
- expect(capsule).toBeTruthy();
304
- const buttons = Array.from(capsule!.querySelectorAll("[data-mp-action]"));
305
- expect(buttons.map((b) => b.getAttribute("aria-label"))).toEqual(["Replace", "Remove"]);
306
- for (const button of buttons) {
307
- expect(button.getAttribute("data-mp-icon-button")).not.toBe("media");
308
- expect(button.textContent).toBe("");
309
- }
310
- });
311
-
312
- it("readOnly hides Replace and Remove — the value is visible, not editable", () => {
313
- const result = renderWithProviders(
314
- <FileUpload label="Attachment" value="/files/report.pdf" readOnly onChange={() => {}} />,
315
- );
316
- expect(result.getButton("Replace")).toBeUndefined();
317
- expect(result.getButton("Remove")).toBeUndefined();
318
- });
319
-
320
- it("disabled refuses a Remove even though the button is on screen", () => {
321
- const onChange = vi.fn();
322
- const result = renderWithProviders(
323
- <FileUpload label="Attachment" value="/files/report.pdf" disabled onChange={onChange} />,
324
- );
325
- fireEvent.click(result.getButton("Remove") as Element);
326
- expect(onChange).not.toHaveBeenCalled();
327
- });
328
-
329
- it("disabled refuses a file selection outright", async () => {
330
- const onUpload = vi.fn();
331
- const result = renderWithProviders(
332
- <FileUpload label="Attachment" disabled onUpload={onUpload} onChange={() => {}} />,
333
- );
334
- await selectFile(result.container, makeFile());
335
- expect(onUpload).not.toHaveBeenCalled();
336
- });
337
- });
338
-
339
- describe("FileUpload accept", () => {
340
- it("passes an explicit accept list to the native picker", () => {
341
- const result = renderWithProviders(
342
- <FileUpload label="Attachment" accept=".pdf,.docx" onChange={() => {}} />,
343
- );
344
- expect(hiddenInput(result.container).getAttribute("accept")).toBe(".pdf,.docx");
345
- });
346
-
347
- it("falls back to image/* when the field is an image field", () => {
348
- const result = renderWithProviders(<FileUpload label="Photo" isImage onChange={() => {}} />);
349
- expect(hiddenInput(result.container).getAttribute("accept")).toBe("image/*");
350
- });
351
- });