@multiplatform.one/frappe-ui 7.28.1 → 7.29.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 (325) hide show
  1. package/dist/cjs/FrappeCalendar.cjs +3 -2
  2. package/dist/cjs/FrappeCalendar.native.js +3 -2
  3. package/dist/cjs/FrappeCalendar.native.js.map +1 -1
  4. package/dist/cjs/FrappeForm.cjs +148 -464
  5. package/dist/cjs/FrappeForm.native.js +152 -633
  6. package/dist/cjs/FrappeForm.native.js.map +1 -1
  7. package/dist/cjs/FrappeListView.cjs +5 -27
  8. package/dist/cjs/FrappeListView.native.js +11 -32
  9. package/dist/cjs/FrappeListView.native.js.map +1 -1
  10. package/dist/cjs/FrappeReportBuilder.cjs +523 -309
  11. package/dist/cjs/FrappeReportBuilder.native.js +658 -353
  12. package/dist/cjs/FrappeReportBuilder.native.js.map +1 -1
  13. package/dist/cjs/FrappeReportView.cjs +35 -234
  14. package/dist/cjs/FrappeReportView.native.js +39 -283
  15. package/dist/cjs/FrappeReportView.native.js.map +1 -1
  16. package/dist/cjs/dependsOn.cjs +2 -2
  17. package/dist/cjs/dependsOn.native.js +1 -2
  18. package/dist/cjs/dependsOn.native.js.map +1 -1
  19. package/dist/cjs/fields/shared/EditorPreviewPanes.cjs +2 -2
  20. package/dist/cjs/fields/shared/EditorPreviewPanes.native.js +2 -2
  21. package/dist/cjs/fields/shared/EditorPreviewPanes.native.js.map +1 -1
  22. package/dist/cjs/formEngine/docPermissions.cjs +20 -0
  23. package/dist/cjs/formEngine/docPermissions.native.js +69 -0
  24. package/dist/cjs/formEngine/docPermissions.native.js.map +1 -1
  25. package/dist/cjs/formEngine/index.cjs +2 -0
  26. package/dist/cjs/formEngine/index.native.js +2 -0
  27. package/dist/cjs/formEngine/index.native.js.map +1 -1
  28. package/dist/cjs/index.cjs +67 -7
  29. package/dist/cjs/index.native.js +67 -7
  30. package/dist/cjs/index.native.js.map +1 -1
  31. package/dist/cjs/liveErrors.cjs +8 -2
  32. package/dist/cjs/liveErrors.native.js +8 -2
  33. package/dist/cjs/liveErrors.native.js.map +1 -1
  34. package/dist/cjs/measures.cjs +31 -0
  35. package/dist/cjs/measures.native.js +34 -0
  36. package/dist/cjs/measures.native.js.map +1 -0
  37. package/dist/cjs/recordShape.cjs +103 -0
  38. package/dist/cjs/recordShape.native.js +115 -0
  39. package/dist/cjs/recordShape.native.js.map +1 -0
  40. package/dist/cjs/report/ReportGrid.cjs +453 -0
  41. package/dist/cjs/report/ReportGrid.native.js +510 -0
  42. package/dist/cjs/report/ReportGrid.native.js.map +1 -0
  43. package/dist/cjs/report/ReportViewToolbar.cjs +253 -0
  44. package/dist/cjs/report/ReportViewToolbar.native.js +303 -0
  45. package/dist/cjs/report/ReportViewToolbar.native.js.map +1 -0
  46. package/dist/cjs/report/queryReport.cjs +444 -0
  47. package/dist/cjs/report/queryReport.native.js +588 -0
  48. package/dist/cjs/report/queryReport.native.js.map +1 -0
  49. package/dist/cjs/report/queryReportColumns.cjs +131 -0
  50. package/dist/cjs/report/queryReportColumns.native.js +159 -0
  51. package/dist/cjs/report/queryReportColumns.native.js.map +1 -0
  52. package/dist/cjs/report/reportViewModel.cjs +386 -0
  53. package/dist/cjs/report/reportViewModel.native.js +657 -0
  54. package/dist/cjs/report/reportViewModel.native.js.map +1 -0
  55. package/dist/cjs/report/savedReports.cjs +137 -0
  56. package/dist/cjs/report/savedReports.native.js +169 -0
  57. package/dist/cjs/report/savedReports.native.js.map +1 -0
  58. package/dist/cjs/report/useFrappeReport.cjs +184 -0
  59. package/dist/cjs/report/useFrappeReport.native.js +214 -0
  60. package/dist/cjs/report/useFrappeReport.native.js.map +1 -0
  61. package/dist/cjs/report/useQueryReport.cjs +201 -0
  62. package/dist/cjs/report/useQueryReport.native.js +233 -0
  63. package/dist/cjs/report/useQueryReport.native.js.map +1 -0
  64. package/dist/cjs/useFrappeForm.cjs +558 -0
  65. package/dist/cjs/useFrappeForm.native.js +714 -0
  66. package/dist/cjs/useFrappeForm.native.js.map +1 -0
  67. package/dist/esm/FrappeCalendar.mjs +4 -3
  68. package/dist/esm/FrappeCalendar.mjs.map +1 -1
  69. package/dist/esm/FrappeCalendar.native.js +4 -3
  70. package/dist/esm/FrappeCalendar.native.js.map +1 -1
  71. package/dist/esm/FrappeForm.mjs +151 -467
  72. package/dist/esm/FrappeForm.mjs.map +1 -1
  73. package/dist/esm/FrappeForm.native.js +155 -636
  74. package/dist/esm/FrappeForm.native.js.map +1 -1
  75. package/dist/esm/FrappeListView.mjs +6 -28
  76. package/dist/esm/FrappeListView.mjs.map +1 -1
  77. package/dist/esm/FrappeListView.native.js +12 -33
  78. package/dist/esm/FrappeListView.native.js.map +1 -1
  79. package/dist/esm/FrappeReportBuilder.mjs +526 -312
  80. package/dist/esm/FrappeReportBuilder.mjs.map +1 -1
  81. package/dist/esm/FrappeReportBuilder.native.js +661 -356
  82. package/dist/esm/FrappeReportBuilder.native.js.map +1 -1
  83. package/dist/esm/FrappeReportView.mjs +30 -229
  84. package/dist/esm/FrappeReportView.mjs.map +1 -1
  85. package/dist/esm/FrappeReportView.native.js +34 -278
  86. package/dist/esm/FrappeReportView.native.js.map +1 -1
  87. package/dist/esm/dependsOn.mjs +2 -2
  88. package/dist/esm/dependsOn.mjs.map +1 -1
  89. package/dist/esm/dependsOn.native.js +1 -2
  90. package/dist/esm/dependsOn.native.js.map +1 -1
  91. package/dist/esm/fields/shared/EditorPreviewPanes.mjs +2 -2
  92. package/dist/esm/fields/shared/EditorPreviewPanes.mjs.map +1 -1
  93. package/dist/esm/fields/shared/EditorPreviewPanes.native.js +2 -2
  94. package/dist/esm/fields/shared/EditorPreviewPanes.native.js.map +1 -1
  95. package/dist/esm/formEngine/docPermissions.mjs +19 -1
  96. package/dist/esm/formEngine/docPermissions.mjs.map +1 -1
  97. package/dist/esm/formEngine/docPermissions.native.js +68 -1
  98. package/dist/esm/formEngine/docPermissions.native.js.map +1 -1
  99. package/dist/esm/formEngine/index.mjs +2 -2
  100. package/dist/esm/formEngine/index.mjs.map +1 -1
  101. package/dist/esm/formEngine/index.native.js +2 -2
  102. package/dist/esm/formEngine/index.native.js.map +1 -1
  103. package/dist/esm/index.mjs +16 -4
  104. package/dist/esm/index.mjs.map +1 -1
  105. package/dist/esm/index.native.js +16 -4
  106. package/dist/esm/index.native.js.map +1 -1
  107. package/dist/esm/liveErrors.mjs +8 -3
  108. package/dist/esm/liveErrors.mjs.map +1 -1
  109. package/dist/esm/liveErrors.native.js +8 -3
  110. package/dist/esm/liveErrors.native.js.map +1 -1
  111. package/dist/esm/measures.mjs +5 -0
  112. package/dist/esm/measures.mjs.map +1 -0
  113. package/dist/esm/measures.native.js +5 -0
  114. package/dist/esm/measures.native.js.map +1 -0
  115. package/dist/esm/recordShape.mjs +74 -0
  116. package/dist/esm/recordShape.mjs.map +1 -0
  117. package/dist/esm/recordShape.native.js +83 -0
  118. package/dist/esm/recordShape.native.js.map +1 -0
  119. package/dist/esm/report/ReportGrid.mjs +428 -0
  120. package/dist/esm/report/ReportGrid.mjs.map +1 -0
  121. package/dist/esm/report/ReportGrid.native.js +471 -0
  122. package/dist/esm/report/ReportGrid.native.js.map +1 -0
  123. package/dist/esm/report/ReportViewToolbar.mjs +228 -0
  124. package/dist/esm/report/ReportViewToolbar.mjs.map +1 -0
  125. package/dist/esm/report/ReportViewToolbar.native.js +264 -0
  126. package/dist/esm/report/ReportViewToolbar.native.js.map +1 -0
  127. package/dist/esm/report/queryReport.mjs +404 -0
  128. package/dist/esm/report/queryReport.mjs.map +1 -0
  129. package/dist/esm/report/queryReport.native.js +545 -0
  130. package/dist/esm/report/queryReport.native.js.map +1 -0
  131. package/dist/esm/report/queryReportColumns.mjs +105 -0
  132. package/dist/esm/report/queryReportColumns.mjs.map +1 -0
  133. package/dist/esm/report/queryReportColumns.native.js +119 -0
  134. package/dist/esm/report/queryReportColumns.native.js.map +1 -0
  135. package/dist/esm/report/reportViewModel.mjs +343 -0
  136. package/dist/esm/report/reportViewModel.mjs.map +1 -0
  137. package/dist/esm/report/reportViewModel.native.js +611 -0
  138. package/dist/esm/report/reportViewModel.native.js.map +1 -0
  139. package/dist/esm/report/savedReports.mjs +112 -0
  140. package/dist/esm/report/savedReports.mjs.map +1 -0
  141. package/dist/esm/report/savedReports.native.js +141 -0
  142. package/dist/esm/report/savedReports.native.js.map +1 -0
  143. package/dist/esm/report/useFrappeReport.mjs +158 -0
  144. package/dist/esm/report/useFrappeReport.mjs.map +1 -0
  145. package/dist/esm/report/useFrappeReport.native.js +185 -0
  146. package/dist/esm/report/useFrappeReport.native.js.map +1 -0
  147. package/dist/esm/report/useQueryReport.mjs +176 -0
  148. package/dist/esm/report/useQueryReport.mjs.map +1 -0
  149. package/dist/esm/report/useQueryReport.native.js +205 -0
  150. package/dist/esm/report/useQueryReport.native.js.map +1 -0
  151. package/dist/esm/useFrappeForm.mjs +533 -0
  152. package/dist/esm/useFrappeForm.mjs.map +1 -0
  153. package/dist/esm/useFrappeForm.native.js +686 -0
  154. package/dist/esm/useFrappeForm.native.js.map +1 -0
  155. package/dist/jsx/FrappeCalendar.mjs +4 -3
  156. package/dist/jsx/FrappeCalendar.mjs.map +1 -1
  157. package/dist/jsx/FrappeCalendar.native.js +3 -2
  158. package/dist/jsx/FrappeCalendar.native.js.map +1 -1
  159. package/dist/jsx/FrappeForm.mjs +151 -467
  160. package/dist/jsx/FrappeForm.mjs.map +1 -1
  161. package/dist/jsx/FrappeForm.native.js +152 -633
  162. package/dist/jsx/FrappeForm.native.js.map +1 -1
  163. package/dist/jsx/FrappeListView.mjs +6 -28
  164. package/dist/jsx/FrappeListView.mjs.map +1 -1
  165. package/dist/jsx/FrappeListView.native.js +11 -32
  166. package/dist/jsx/FrappeListView.native.js.map +1 -1
  167. package/dist/jsx/FrappeReportBuilder.mjs +526 -312
  168. package/dist/jsx/FrappeReportBuilder.mjs.map +1 -1
  169. package/dist/jsx/FrappeReportBuilder.native.js +658 -353
  170. package/dist/jsx/FrappeReportBuilder.native.js.map +1 -1
  171. package/dist/jsx/FrappeReportView.mjs +30 -229
  172. package/dist/jsx/FrappeReportView.mjs.map +1 -1
  173. package/dist/jsx/FrappeReportView.native.js +39 -283
  174. package/dist/jsx/FrappeReportView.native.js.map +1 -1
  175. package/dist/jsx/dependsOn.mjs +2 -2
  176. package/dist/jsx/dependsOn.mjs.map +1 -1
  177. package/dist/jsx/dependsOn.native.js +1 -2
  178. package/dist/jsx/dependsOn.native.js.map +1 -1
  179. package/dist/jsx/fields/shared/EditorPreviewPanes.mjs +2 -2
  180. package/dist/jsx/fields/shared/EditorPreviewPanes.mjs.map +1 -1
  181. package/dist/jsx/fields/shared/EditorPreviewPanes.native.js +2 -2
  182. package/dist/jsx/fields/shared/EditorPreviewPanes.native.js.map +1 -1
  183. package/dist/jsx/formEngine/docPermissions.mjs +19 -1
  184. package/dist/jsx/formEngine/docPermissions.mjs.map +1 -1
  185. package/dist/jsx/formEngine/docPermissions.native.js +69 -0
  186. package/dist/jsx/formEngine/docPermissions.native.js.map +1 -1
  187. package/dist/jsx/formEngine/index.mjs +2 -2
  188. package/dist/jsx/formEngine/index.mjs.map +1 -1
  189. package/dist/jsx/formEngine/index.native.js +2 -0
  190. package/dist/jsx/formEngine/index.native.js.map +1 -1
  191. package/dist/jsx/index.js +16 -4
  192. package/dist/jsx/index.js.map +1 -1
  193. package/dist/jsx/index.mjs +16 -4
  194. package/dist/jsx/index.mjs.map +1 -1
  195. package/dist/jsx/index.native.js +67 -7
  196. package/dist/jsx/index.native.js.map +1 -1
  197. package/dist/jsx/liveErrors.mjs +8 -3
  198. package/dist/jsx/liveErrors.mjs.map +1 -1
  199. package/dist/jsx/liveErrors.native.js +8 -2
  200. package/dist/jsx/liveErrors.native.js.map +1 -1
  201. package/dist/jsx/measures.mjs +5 -0
  202. package/dist/jsx/measures.mjs.map +1 -0
  203. package/dist/jsx/measures.native.js +34 -0
  204. package/dist/jsx/measures.native.js.map +1 -0
  205. package/dist/jsx/recordShape.mjs +74 -0
  206. package/dist/jsx/recordShape.mjs.map +1 -0
  207. package/dist/jsx/recordShape.native.js +115 -0
  208. package/dist/jsx/recordShape.native.js.map +1 -0
  209. package/dist/jsx/report/ReportGrid.mjs +428 -0
  210. package/dist/jsx/report/ReportGrid.mjs.map +1 -0
  211. package/dist/jsx/report/ReportGrid.native.js +510 -0
  212. package/dist/jsx/report/ReportGrid.native.js.map +1 -0
  213. package/dist/jsx/report/ReportViewToolbar.mjs +228 -0
  214. package/dist/jsx/report/ReportViewToolbar.mjs.map +1 -0
  215. package/dist/jsx/report/ReportViewToolbar.native.js +303 -0
  216. package/dist/jsx/report/ReportViewToolbar.native.js.map +1 -0
  217. package/dist/jsx/report/queryReport.mjs +404 -0
  218. package/dist/jsx/report/queryReport.mjs.map +1 -0
  219. package/dist/jsx/report/queryReport.native.js +588 -0
  220. package/dist/jsx/report/queryReport.native.js.map +1 -0
  221. package/dist/jsx/report/queryReportColumns.mjs +105 -0
  222. package/dist/jsx/report/queryReportColumns.mjs.map +1 -0
  223. package/dist/jsx/report/queryReportColumns.native.js +159 -0
  224. package/dist/jsx/report/queryReportColumns.native.js.map +1 -0
  225. package/dist/jsx/report/reportViewModel.mjs +343 -0
  226. package/dist/jsx/report/reportViewModel.mjs.map +1 -0
  227. package/dist/jsx/report/reportViewModel.native.js +657 -0
  228. package/dist/jsx/report/reportViewModel.native.js.map +1 -0
  229. package/dist/jsx/report/savedReports.mjs +112 -0
  230. package/dist/jsx/report/savedReports.mjs.map +1 -0
  231. package/dist/jsx/report/savedReports.native.js +169 -0
  232. package/dist/jsx/report/savedReports.native.js.map +1 -0
  233. package/dist/jsx/report/useFrappeReport.mjs +158 -0
  234. package/dist/jsx/report/useFrappeReport.mjs.map +1 -0
  235. package/dist/jsx/report/useFrappeReport.native.js +214 -0
  236. package/dist/jsx/report/useFrappeReport.native.js.map +1 -0
  237. package/dist/jsx/report/useQueryReport.mjs +176 -0
  238. package/dist/jsx/report/useQueryReport.mjs.map +1 -0
  239. package/dist/jsx/report/useQueryReport.native.js +233 -0
  240. package/dist/jsx/report/useQueryReport.native.js.map +1 -0
  241. package/dist/jsx/useFrappeForm.mjs +533 -0
  242. package/dist/jsx/useFrappeForm.mjs.map +1 -0
  243. package/dist/jsx/useFrappeForm.native.js +714 -0
  244. package/dist/jsx/useFrappeForm.native.js.map +1 -0
  245. package/package.json +16 -16
  246. package/src/FrappeCalendar.tsx +8 -2
  247. package/src/FrappeForm.stories.tsx +52 -0
  248. package/src/FrappeForm.tsx +176 -565
  249. package/src/FrappeListView.cards.stories.tsx +3 -6
  250. package/src/FrappeListView.tsx +15 -55
  251. package/src/FrappeReportBuilder.stories.tsx +34 -7
  252. package/src/FrappeReportBuilder.tsx +790 -380
  253. package/src/FrappeReportView.spec.tsx +10 -6
  254. package/src/FrappeReportView.stories.tsx +20 -1
  255. package/src/FrappeReportView.tsx +89 -415
  256. package/src/dependsOn.ts +4 -1
  257. package/src/fields/shared/EditorPreviewPanes.tsx +2 -2
  258. package/src/formEngine/docPermissions.spec.ts +36 -1
  259. package/src/formEngine/docPermissions.ts +34 -0
  260. package/src/formEngine/index.ts +8 -1
  261. package/src/frappeRemainder.designLaw.spec.ts +9 -1
  262. package/src/frappeRoutes.spec.tsx +9 -0
  263. package/src/frappeViews.designLaw.spec.ts +15 -2
  264. package/src/guestFallback.spec.tsx +236 -0
  265. package/src/index.ts +117 -16
  266. package/src/liveErrors.spec.ts +10 -0
  267. package/src/liveErrors.ts +16 -3
  268. package/src/measures.ts +11 -0
  269. package/src/recordShape.spec.ts +100 -0
  270. package/src/recordShape.ts +133 -0
  271. package/src/report/ReportGrid.spec.tsx +175 -0
  272. package/src/report/ReportGrid.tsx +530 -0
  273. package/src/report/ReportViewToolbar.spec.tsx +90 -0
  274. package/src/report/ReportViewToolbar.tsx +314 -0
  275. package/src/report/queryReport.spec.ts +483 -0
  276. package/src/report/queryReport.ts +722 -0
  277. package/src/report/queryReportColumns.tsx +123 -0
  278. package/src/report/reportViewModel.spec.ts +393 -0
  279. package/src/report/reportViewModel.ts +617 -0
  280. package/src/report/savedReports.ts +172 -0
  281. package/src/report/seam.spec.ts +216 -0
  282. package/src/report/useFrappeReport.ts +241 -0
  283. package/src/report/useQueryReport.spec.tsx +131 -0
  284. package/src/report/useQueryReport.ts +309 -0
  285. package/src/useFrappeForm.spec.tsx +209 -0
  286. package/src/useFrappeForm.ts +778 -0
  287. package/types/FrappeCalendar.d.ts.map +1 -1
  288. package/types/FrappeForm.d.ts +21 -1
  289. package/types/FrappeForm.d.ts.map +1 -1
  290. package/types/FrappeListView.d.ts.map +1 -1
  291. package/types/FrappeReportBuilder.d.ts +66 -16
  292. package/types/FrappeReportBuilder.d.ts.map +1 -1
  293. package/types/FrappeReportView.d.ts +9 -127
  294. package/types/FrappeReportView.d.ts.map +1 -1
  295. package/types/dependsOn.d.ts.map +1 -1
  296. package/types/formEngine/docPermissions.d.ts +14 -0
  297. package/types/formEngine/docPermissions.d.ts.map +1 -1
  298. package/types/formEngine/index.d.ts +1 -1
  299. package/types/formEngine/index.d.ts.map +1 -1
  300. package/types/index.d.ts +15 -3
  301. package/types/index.d.ts.map +1 -1
  302. package/types/liveErrors.d.ts +9 -1
  303. package/types/liveErrors.d.ts.map +1 -1
  304. package/types/measures.d.ts +3 -0
  305. package/types/measures.d.ts.map +1 -0
  306. package/types/recordShape.d.ts +45 -0
  307. package/types/recordShape.d.ts.map +1 -0
  308. package/types/report/ReportGrid.d.ts +20 -0
  309. package/types/report/ReportGrid.d.ts.map +1 -0
  310. package/types/report/ReportViewToolbar.d.ts +31 -0
  311. package/types/report/ReportViewToolbar.d.ts.map +1 -0
  312. package/types/report/queryReport.d.ts +181 -0
  313. package/types/report/queryReport.d.ts.map +1 -0
  314. package/types/report/queryReportColumns.d.ts +12 -0
  315. package/types/report/queryReportColumns.d.ts.map +1 -0
  316. package/types/report/reportViewModel.d.ts +146 -0
  317. package/types/report/reportViewModel.d.ts.map +1 -0
  318. package/types/report/savedReports.d.ts +27 -0
  319. package/types/report/savedReports.d.ts.map +1 -0
  320. package/types/report/useFrappeReport.d.ts +60 -0
  321. package/types/report/useFrappeReport.d.ts.map +1 -0
  322. package/types/report/useQueryReport.d.ts +37 -0
  323. package/types/report/useQueryReport.d.ts.map +1 -0
  324. package/types/useFrappeForm.d.ts +142 -0
  325. package/types/useFrappeForm.d.ts.map +1 -0
@@ -0,0 +1,131 @@
1
+ /**
2
+ * useQueryReport's live path (MPO-15, GL-28): the desk hook reads the Report
3
+ * doc and script, then runs the report through useFrappeReport, the
4
+ * transport FrappeReportView uses, and hands back the one model's columns and
5
+ * rows. A refused run keeps its HTTP status on the humanized error.
6
+ */
7
+ import { renderHook, waitFor } from "@testing-library/react";
8
+ import React, { type ReactNode } from "react";
9
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
10
+ import { FrappeProvider } from "../FrappeProvider";
11
+ import { useQueryReport } from "./useQueryReport";
12
+
13
+ interface Recorded {
14
+ method: string;
15
+ url: string;
16
+ body?: Record<string, unknown>;
17
+ }
18
+
19
+ function json(body: unknown, status = 200): Response {
20
+ return new Response(JSON.stringify(body), {
21
+ status,
22
+ headers: { "Content-Type": "application/json" },
23
+ });
24
+ }
25
+
26
+ let calls: Recorded[] = [];
27
+ let runResponse: () => Response;
28
+
29
+ beforeEach(() => {
30
+ calls = [];
31
+ runResponse = () =>
32
+ json({
33
+ message: {
34
+ columns: ["ID:Link/ToDo:120", { label: "Assigned To/Owner", fieldtype: "Data" }],
35
+ result: [
36
+ ["TD-1", "jane@example.com"],
37
+ { id: "TD-2", "assigned_to/owner": "joe@example.com" },
38
+ ],
39
+ execution_time: 0.25,
40
+ message: "Two open",
41
+ },
42
+ });
43
+ vi.stubGlobal(
44
+ "fetch",
45
+ vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
46
+ const url = typeof input === "string" ? input : input.toString();
47
+ const method = (init?.method ?? "GET").toUpperCase();
48
+ const body =
49
+ typeof init?.body === "string"
50
+ ? (JSON.parse(init.body) as Record<string, unknown>)
51
+ : undefined;
52
+ calls.push({ method, url, body });
53
+ if (url.includes("frappe.client.get")) {
54
+ return json({
55
+ message: {
56
+ name: "Open Work",
57
+ report_name: "Open Work",
58
+ filters: [{ fieldname: "owner", label: "Owner", fieldtype: "Data", default: "jane" }],
59
+ },
60
+ });
61
+ }
62
+ if (url.includes("frappe.desk.query_report.get_script")) return json({ message: {} });
63
+ if (url.includes("frappe.desk.query_report.run")) return runResponse();
64
+ return json({ message: null });
65
+ }),
66
+ );
67
+ });
68
+
69
+ afterEach(() => {
70
+ vi.unstubAllGlobals();
71
+ });
72
+
73
+ let run = 0;
74
+ function wrapper() {
75
+ run += 1;
76
+ const baseURL = `https://query-report-${run}.spec.test`;
77
+ return ({ children }: { children: ReactNode }) => (
78
+ <FrappeProvider baseURL={baseURL} realtime={false}>
79
+ {children}
80
+ </FrappeProvider>
81
+ );
82
+ }
83
+
84
+ describe("useQueryReport over the shared transport", () => {
85
+ it("seeds the doc's defaults, runs once, and reads the model's rows", async () => {
86
+ const { result } = renderHook(() => useQueryReport("Open Work"), { wrapper: wrapper() });
87
+ await waitFor(() => expect(result.current.loading).toBe(false), { timeout: 5000 });
88
+ expect(result.current.error).toBeUndefined();
89
+ expect(result.current.columns.map((column) => column.fieldname)).toEqual([
90
+ "id",
91
+ "assigned_to/owner",
92
+ ]);
93
+ expect(result.current.rows).toEqual([
94
+ { id: "TD-1", "assigned_to/owner": "jane@example.com" },
95
+ { id: "TD-2", "assigned_to/owner": "joe@example.com" },
96
+ ]);
97
+ expect(result.current.executionTime).toBe(0.25);
98
+ expect(result.current.serverMessage).toBe("Two open");
99
+
100
+ const runs = calls.filter((call) => call.url.includes("frappe.desk.query_report.run"));
101
+ expect(runs).toHaveLength(1);
102
+ expect(runs[0].method).toBe("POST");
103
+ expect(runs[0].body).toMatchObject({
104
+ report_name: "Open Work",
105
+ filters: { owner: "jane" },
106
+ ignore_prepared_report: 1,
107
+ are_default_filters: 0,
108
+ });
109
+ });
110
+
111
+ it("keeps the refused run's status on the humanized error", async () => {
112
+ runResponse = () =>
113
+ json(
114
+ {
115
+ exc_type: "PermissionError",
116
+ _server_messages: JSON.stringify([
117
+ JSON.stringify({ message: "You are not permitted to run Open Work" }),
118
+ ]),
119
+ },
120
+ 403,
121
+ );
122
+ const { result } = renderHook(() => useQueryReport("Open Work"), { wrapper: wrapper() });
123
+ await waitFor(() => expect(result.current.error).toBeDefined(), { timeout: 5000 });
124
+ const error = result.current.error as Error & { status?: number; exc_type?: string };
125
+ expect(error.message).toBe("You are not permitted to run Open Work");
126
+ expect(error.status).toBe(403);
127
+ expect(error.exc_type).toBe("PermissionError");
128
+ expect(result.current.rows).toEqual([]);
129
+ expect(result.current.loading).toBe(false);
130
+ });
131
+ });
@@ -0,0 +1,309 @@
1
+ // Data hook for frappe Query / Script Reports (the desk
2
+ // /app/query-report/<name> surface): loads the Report doc (title,
3
+ // ref_doctype, doc-level filters) plus the desk-side script module
4
+ // (frappe.desk.query_report.get_script) and runs the report through
5
+ // useFrappeReport, the transport FrappeReportView uses too. Wire-shape
6
+ // normalization (and the probed response documentation) lives in
7
+ // queryReport.ts; this file owns the desk's filter state and gating.
8
+ //
9
+ // GET /api/method/frappe.client.get {doctype:"Report", name}
10
+ // → {message: {report_name, ref_doctype, report_type, disabled,
11
+ // prepared_report, add_total_row, filters: [...], ...}}
12
+ // GET /api/method/frappe.desk.query_report.get_script {report_name}
13
+ // → {message: {script, html_format, execution_time, filters}}
14
+ //
15
+ // Filter meta sources (desk precedence, query_report.js setup): the script
16
+ // module's declarative `filters` win when the script declares any; the
17
+ // Report doc `filters` child table applies only when it doesn't. Script
18
+ // filters are extracted from the raw JS text without eval
19
+ // (queryReport.extractScriptFilters) — entries beyond the declarative
20
+ // subset (get_query functions, dynamic defaults, desk-only fieldtypes)
21
+ // surface as `scriptFilterGap` so the screen can render an honest note.
22
+ // `ignore_prepared_report` forces a synchronous run for prepared reports
23
+ // (desk's background Prepared Report queue is out of scope here).
24
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
25
+ import { useFrappeConfig } from "../FrappeProvider";
26
+ import { isPermissionError } from "../liveErrors";
27
+ import {
28
+ type QueryReportColumn,
29
+ type QueryReportFilterMeta,
30
+ type QueryReportFilters,
31
+ type QueryReportFixture,
32
+ type QueryReportMeta,
33
+ type QueryReportRow,
34
+ buildRunFilters,
35
+ defaultFilterValues,
36
+ extractScriptFilters,
37
+ humanizeFrappeError,
38
+ isEmptyFilterValue,
39
+ normalizeFilterMeta,
40
+ normalizeQueryReportColumns,
41
+ normalizeQueryReportRows,
42
+ } from "./queryReport";
43
+ import { reportCall, useFrappeReport, type FrappeReportConnection } from "./useFrappeReport";
44
+
45
+ const SCRIPT_METHOD = "frappe.desk.query_report.get_script";
46
+ const EMPTY_COLUMNS: QueryReportColumn[] = [];
47
+ const EMPTY_ROWS: QueryReportRow[] = [];
48
+
49
+ export interface UseQueryReportOptions {
50
+ fixture?: QueryReportFixture;
51
+ /** Filter values applied before the first run (report swaps re-apply them). */
52
+ initialFilters?: QueryReportFilters;
53
+ }
54
+
55
+ export interface UseQueryReportResult {
56
+ meta?: QueryReportMeta;
57
+ columns: QueryReportColumn[];
58
+ rows: QueryReportRow[];
59
+ /** True until the first meta+script+run cycle settles. */
60
+ loading: boolean;
61
+ /** True while a (re)run is in flight. */
62
+ running: boolean;
63
+ /** The failed run's server sentence; `status` and `exc_type` ride along. */
64
+ error?: Error;
65
+ /** The server refused the run: the session may not run this report (LC-96). */
66
+ refused: boolean;
67
+ /** The report's informational `message` payload (may embed HTML). */
68
+ serverMessage?: string;
69
+ /** Server-reported run time in seconds. */
70
+ executionTime?: number;
71
+ /** Merged filter meta: script-declared filters, else doc-level rows. */
72
+ filterMeta: QueryReportFilterMeta[];
73
+ /**
74
+ * True when the desk script declares filters this surface could not
75
+ * derive (non-declarative entries) — the honest "defined in code" note.
76
+ */
77
+ scriptFilterGap: boolean;
78
+ /** Mandatory filters currently unfilled — the run is skipped while non-empty. */
79
+ missingRequired: QueryReportFilterMeta[];
80
+ filters: QueryReportFilters;
81
+ setFilters: (next: QueryReportFilters) => void;
82
+ reload: () => void;
83
+ }
84
+
85
+ async function fetchReportMeta(
86
+ connection: FrappeReportConnection,
87
+ reportName: string,
88
+ ): Promise<QueryReportMeta> {
89
+ const res = await reportCall(connection).get<{ message?: QueryReportMeta }>("frappe.client.get", {
90
+ doctype: "Report",
91
+ name: reportName,
92
+ });
93
+ const doc = res.payload?.message;
94
+ if (!doc) throw new Error(`Report "${reportName}" not found`);
95
+ return doc;
96
+ }
97
+
98
+ async function fetchReportScript(
99
+ connection: FrappeReportConnection,
100
+ reportName: string,
101
+ ): Promise<string | undefined> {
102
+ const res = await reportCall(connection).get<{ message?: { script?: string | null } }>(
103
+ SCRIPT_METHOD,
104
+ { report_name: reportName },
105
+ );
106
+ const script = res.payload?.message?.script;
107
+ return typeof script === "string" ? script : undefined;
108
+ }
109
+
110
+ export function useQueryReport(
111
+ reportName: string,
112
+ options: UseQueryReportOptions = {},
113
+ ): UseQueryReportResult {
114
+ const { fixture } = options;
115
+ const config = useFrappeConfig();
116
+ const baseURL = config?.baseURL;
117
+ const auth = config?.auth;
118
+ const connection = useMemo<FrappeReportConnection | undefined>(
119
+ () => (baseURL ? { baseURL, auth } : undefined),
120
+ [baseURL, auth],
121
+ );
122
+
123
+ // Latest initialFilters without re-running effects on identity churn.
124
+ const initialFiltersRef = useRef(options.initialFilters);
125
+ initialFiltersRef.current = options.initialFilters;
126
+
127
+ const [meta, setMeta] = useState<QueryReportMeta | undefined>(undefined);
128
+ const [script, setScript] = useState<string | undefined>(undefined);
129
+ const [sourcesSettled, setSourcesSettled] = useState(false);
130
+ const [filters, setFiltersState] = useState<QueryReportFilters>(() => ({
131
+ ...initialFiltersRef.current,
132
+ }));
133
+ const seededFor = useRef<string | undefined>(undefined);
134
+
135
+ // The route component is reused across query-report navigations — a
136
+ // report swap resets filters and shows the loading state again.
137
+ const lastReport = useRef(reportName);
138
+ useEffect(() => {
139
+ if (lastReport.current === reportName) return;
140
+ lastReport.current = reportName;
141
+ setFiltersState({ ...initialFiltersRef.current });
142
+ setMeta(undefined);
143
+ setScript(undefined);
144
+ setSourcesSettled(false);
145
+ }, [reportName]);
146
+
147
+ // Report doc meta (title, ref_doctype, doc filters) + the desk script
148
+ // module, fetched together. Both are non-fatal niceties — the run below
149
+ // reports the real errors — but the run waits for them so declared
150
+ // defaults are seeded into the FIRST run (desk parity).
151
+ useEffect(() => {
152
+ if (fixture) return;
153
+ if (!connection) {
154
+ setSourcesSettled(true);
155
+ return;
156
+ }
157
+ let cancelled = false;
158
+ setSourcesSettled(false);
159
+ Promise.allSettled([
160
+ fetchReportMeta(connection, reportName),
161
+ fetchReportScript(connection, reportName),
162
+ ]).then(([metaResult, scriptResult]) => {
163
+ if (cancelled) return;
164
+ const doc = metaResult.status === "fulfilled" ? metaResult.value : undefined;
165
+ const scriptText = scriptResult.status === "fulfilled" ? scriptResult.value : undefined;
166
+ if (doc) setMeta(doc);
167
+ if (scriptText) setScript(scriptText);
168
+ // Seed filter defaults once per report; user edits (and deep-link
169
+ // initialFilters, already in state) win over defaults.
170
+ if (seededFor.current !== reportName) {
171
+ seededFor.current = reportName;
172
+ const extraction = extractScriptFilters(scriptText);
173
+ const merged = extraction.declared ? extraction.filters : normalizeFilterMeta(doc?.filters);
174
+ const defaults = defaultFilterValues(merged);
175
+ if (Object.keys(defaults).length > 0)
176
+ setFiltersState((previous) => ({ ...defaults, ...previous }));
177
+ }
178
+ setSourcesSettled(true);
179
+ });
180
+ return () => {
181
+ cancelled = true;
182
+ };
183
+ }, [connection, fixture, reportName]);
184
+
185
+ const scriptExtraction = useMemo(
186
+ () => extractScriptFilters(fixture ? fixture.script : script),
187
+ [fixture, script],
188
+ );
189
+ const docFilterMeta = useMemo(
190
+ () => normalizeFilterMeta(fixture ? fixture.meta?.filters : meta?.filters),
191
+ [fixture, meta],
192
+ );
193
+ // Desk precedence (query_report.js): the script module's filters apply
194
+ // when the script declares the key at all; doc rows only otherwise.
195
+ const filterMeta = useMemo(
196
+ () => (scriptExtraction.declared ? scriptExtraction.filters : docFilterMeta),
197
+ [scriptExtraction, docFilterMeta],
198
+ );
199
+ const scriptFilterGap = scriptExtraction.declaredCount > scriptExtraction.filters.length;
200
+
201
+ // Fixture branch seeds defaults too (no fetch effect runs for fixtures).
202
+ useEffect(() => {
203
+ if (!fixture) return;
204
+ if (seededFor.current === reportName) return;
205
+ seededFor.current = reportName;
206
+ const defaults = defaultFilterValues(filterMeta);
207
+ if (Object.keys(defaults).length > 0)
208
+ setFiltersState((previous) => ({ ...defaults, ...previous }));
209
+ }, [fixture, reportName, filterMeta]);
210
+
211
+ // Desk parity: defaults SEED the controls (state) once; after that the
212
+ // gate reads the live values only — clearing a defaulted mandatory filter
213
+ // re-gates instead of firing a doomed run (desk's get_filter_values reads
214
+ // control values, not declarations).
215
+ const missingRequired = useMemo(
216
+ () =>
217
+ filterMeta.filter(
218
+ (filter) => filter.mandatory === 1 && isEmptyFilterValue(filters[filter.fieldname]),
219
+ ),
220
+ [filterMeta, filters],
221
+ );
222
+ const hasMissingRequired = missingRequired.length > 0;
223
+ const metaDisabled = (fixture ? fixture.meta?.disabled : meta?.disabled) === 1;
224
+
225
+ const runFilters = useMemo(() => buildRunFilters(filterMeta, filters), [filterMeta, filters]);
226
+ const runEnabled =
227
+ !fixture && sourcesSettled && Boolean(connection) && !metaDisabled && !hasMissingRequired;
228
+ const run = useFrappeReport(connection, {
229
+ reportName,
230
+ filters: runFilters,
231
+ ignorePreparedReport: true,
232
+ debounceMs: 0,
233
+ enabled: runEnabled,
234
+ });
235
+ const { refresh } = run;
236
+
237
+ const setFilters = useCallback((next: QueryReportFilters) => {
238
+ setFiltersState(next);
239
+ }, []);
240
+
241
+ const reload = useCallback(() => {
242
+ if (runEnabled) refresh();
243
+ }, [runEnabled, refresh]);
244
+
245
+ const runError = useMemo(
246
+ () => (run.error ? humanizeFrappeError(run.error) : undefined),
247
+ [run.error],
248
+ );
249
+
250
+ // Fixture branch: static data, no RPCs (stories/tests).
251
+ const fixtureResult = useMemo(() => {
252
+ if (!fixture) return undefined;
253
+ const fixtureColumns = normalizeQueryReportColumns(fixture.columns);
254
+ return {
255
+ columns: fixtureColumns,
256
+ rows: normalizeQueryReportRows(fixture.rows, fixtureColumns),
257
+ meta: fixture.meta ? ({ name: reportName, ...fixture.meta } as QueryReportMeta) : undefined,
258
+ };
259
+ }, [fixture, reportName]);
260
+
261
+ if (fixtureResult) {
262
+ return {
263
+ meta: fixtureResult.meta,
264
+ columns: fixtureResult.columns,
265
+ rows: hasMissingRequired ? EMPTY_ROWS : fixtureResult.rows,
266
+ loading: false,
267
+ running: false,
268
+ error: undefined,
269
+ refused: false,
270
+ serverMessage: fixture?.message,
271
+ executionTime: undefined,
272
+ filterMeta,
273
+ scriptFilterGap,
274
+ missingRequired,
275
+ filters,
276
+ setFilters,
277
+ reload,
278
+ };
279
+ }
280
+
281
+ const noConnection = sourcesSettled && !connection;
282
+ // A settled run shows its result; a gated one (disabled report, missing
283
+ // mandatory filters) shows nothing rather than the last report's rows.
284
+ const showsRun = runEnabled && !runError;
285
+ const payload = showsRun ? run.payload : undefined;
286
+ const error = noConnection
287
+ ? new Error("No Frappe connection configured")
288
+ : runEnabled
289
+ ? runError
290
+ : undefined;
291
+ return {
292
+ meta,
293
+ columns: payload?.columns ?? EMPTY_COLUMNS,
294
+ rows: payload?.rows ?? EMPTY_ROWS,
295
+ loading: !sourcesSettled || (runEnabled && (run.isLoading || (!run.payload && !run.error))),
296
+ running: runEnabled && run.isFetching,
297
+ error,
298
+ refused: error !== undefined && isPermissionError(error),
299
+ serverMessage: payload?.message,
300
+ executionTime:
301
+ typeof payload?.raw.execution_time === "number" ? payload.raw.execution_time : undefined,
302
+ filterMeta,
303
+ scriptFilterGap,
304
+ missingRequired,
305
+ filters,
306
+ setFilters,
307
+ reload,
308
+ };
309
+ }
@@ -0,0 +1,209 @@
1
+ /**
2
+ * useFrappeForm is FrappeForm's own state, handed to its caller (MPO-15
3
+ * AC2): a page renders the fields with `<FrappeForm form={controller} />`
4
+ * and builds its chrome on the same controller, so the desk and a bare
5
+ * FrappeForm run one engine. A new record also starts from the doctype's
6
+ * defaults, the way desk create_new seeds it.
7
+ */
8
+
9
+ import { InMemoryFixtureProvider } from "@multiplatform.one/frappe";
10
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
11
+ import { act, cleanup, fireEvent, screen, waitFor } from "@testing-library/react";
12
+ import i18next from "i18next";
13
+ import { initReactI18next } from "react-i18next";
14
+ import { Text } from "tamagui";
15
+ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
16
+ import { assetLoanLifecycleFixtures } from "../tests/fixtures/assetLoanLifecycle";
17
+ import { setCreatePrefill } from "./formEngine/createPrefill";
18
+ import { FrappeForm } from "./FrappeForm";
19
+ import { FrappeProvider } from "./FrappeProvider";
20
+ import type { FieldOverride } from "./schemaForm";
21
+ import { useFrappeForm, type FrappeFormController } from "./useFrappeForm";
22
+
23
+ beforeAll(async () => {
24
+ await i18next.use(initReactI18next).init({
25
+ lng: "en",
26
+ interpolation: { escapeValue: false },
27
+ resources: { en: { translation: {} } },
28
+ });
29
+ });
30
+
31
+ let confirm: ReturnType<typeof vi.fn>;
32
+
33
+ beforeEach(() => {
34
+ confirm = vi.fn(() => true);
35
+ vi.stubGlobal("confirm", confirm);
36
+ });
37
+
38
+ afterEach(() => {
39
+ cleanup();
40
+ vi.unstubAllGlobals();
41
+ vi.restoreAllMocks();
42
+ });
43
+
44
+ type Doc = Record<string, unknown> & { name: string; doctype: string };
45
+
46
+ const probe: FieldOverride = (props) => {
47
+ const name = props.fieldMeta?.name ?? "";
48
+ const value = props.value === undefined || props.value === null ? "" : String(props.value);
49
+ return <Text testID={`probe-${name}`}>{`val=${value} err=${props.error ?? ""}`}</Text>;
50
+ };
51
+
52
+ const ticketMeta = {
53
+ name: "Desk Ticket",
54
+ doctype: "DocType",
55
+ autoname: "hash",
56
+ fields: [
57
+ { fieldname: "subject", fieldtype: "Data", label: "Subject", reqd: 1 },
58
+ { fieldname: "status", fieldtype: "Select", label: "Status", options: "Open\nClosed" },
59
+ { fieldname: "priority", fieldtype: "Data", label: "Priority", default: "Medium" },
60
+ { fieldname: "more_tab", fieldtype: "Tab Break", label: "More" },
61
+ { fieldname: "resolution", fieldtype: "Data", label: "Resolution", reqd: 1 },
62
+ ],
63
+ permissions: [{ role: "All", read: 1, write: 1, create: 1 }],
64
+ };
65
+
66
+ const ticketOverrides = { subject: probe, status: probe, priority: probe, resolution: probe };
67
+
68
+ function ticketFixtures() {
69
+ return new InMemoryFixtureProvider({
70
+ DocType: [ticketMeta],
71
+ "Desk Ticket": [
72
+ { name: "TICKET-1", doctype: "Desk Ticket", subject: "Printer", status: "Open" },
73
+ ],
74
+ });
75
+ }
76
+
77
+ let controller: FrappeFormController<Doc> | undefined;
78
+
79
+ function Host({ doctype, name }: { doctype: string; name?: string }) {
80
+ const form = useFrappeForm<Doc>({ doctype, name, roles: ["Loan Manager", "All"] });
81
+ controller = form;
82
+ return (
83
+ <>
84
+ <button type="button" data-testid="host-validate" onClick={() => revealFirst(form)}>
85
+ validate
86
+ </button>
87
+ <button type="button" data-testid="host-submit" onClick={() => void form.submitDocument()}>
88
+ submit
89
+ </button>
90
+ <FrappeForm<Doc>
91
+ form={form}
92
+ doctype={doctype}
93
+ name={name}
94
+ overrides={{ ...ticketOverrides, employee_name: probe }}
95
+ showToolbar={false}
96
+ showSidebar={false}
97
+ showTimeline={false}
98
+ showBanners={false}
99
+ />
100
+ </>
101
+ );
102
+ }
103
+
104
+ function revealFirst(form: FrappeFormController<Doc>) {
105
+ const errors = form.validate();
106
+ form.setErrors(errors);
107
+ Object.keys(errors).some((field) => form.revealField(field));
108
+ }
109
+
110
+ async function expectProbe(field: string, text: string): Promise<void> {
111
+ await waitFor(() => {
112
+ expect(screen.getByTestId(`probe-${field}`).textContent).toContain(text);
113
+ });
114
+ }
115
+
116
+ describe("FrappeForm seeds a new record from the doctype's defaults", () => {
117
+ it("a Data default and a Select's first option fill in", async () => {
118
+ renderWithProviders(
119
+ <FrappeProvider baseURL="http://test" fixtures={ticketFixtures()}>
120
+ <FrappeForm doctype="Desk Ticket" overrides={ticketOverrides} showSidebar={false} />
121
+ </FrappeProvider>,
122
+ );
123
+ await expectProbe("status", "val=Open");
124
+ await expectProbe("priority", "val=Medium");
125
+ await expectProbe("subject", "val=");
126
+ });
127
+
128
+ it("a stashed draft (?d=, Duplicate, Amend) wins over the defaults", async () => {
129
+ setCreatePrefill("Desk Ticket", { priority: "High", subject: "From the list" });
130
+ renderWithProviders(
131
+ <FrappeProvider baseURL="http://test" fixtures={ticketFixtures()}>
132
+ <FrappeForm doctype="Desk Ticket" overrides={ticketOverrides} showSidebar={false} />
133
+ </FrappeProvider>,
134
+ );
135
+ await expectProbe("priority", "val=High");
136
+ await expectProbe("subject", "val=From the list");
137
+ await expectProbe("status", "val=Open");
138
+ });
139
+ });
140
+
141
+ describe("a caller-owned FrappeForm", () => {
142
+ it("renders the fields without its own toolbar and lays out the same tabs", async () => {
143
+ renderWithProviders(
144
+ <FrappeProvider baseURL="http://test" fixtures={ticketFixtures()}>
145
+ <Host doctype="Desk Ticket" name="TICKET-1" />
146
+ </FrappeProvider>,
147
+ );
148
+ await expectProbe("subject", "val=Printer");
149
+ expect(screen.queryByText("Save")).toBeNull();
150
+ expect(controller?.fieldTargets.map((field) => [field.name, field.tabKey])).toEqual([
151
+ ["subject", "__details"],
152
+ ["status", "__details"],
153
+ ["priority", "__details"],
154
+ ["resolution", "more_tab"],
155
+ ]);
156
+ });
157
+
158
+ it("reveals the tab a failing required field sits on", async () => {
159
+ renderWithProviders(
160
+ <FrappeProvider baseURL="http://test" fixtures={ticketFixtures()}>
161
+ <Host doctype="Desk Ticket" name="TICKET-1" />
162
+ </FrappeProvider>,
163
+ );
164
+ await expectProbe("subject", "val=Printer");
165
+ expect(screen.queryByTestId("probe-resolution")).toBeNull();
166
+ fireEvent.click(screen.getByTestId("host-validate"));
167
+ await expectProbe("resolution", "err=Resolution is required");
168
+ expect(controller?.activeTab).toBe("more_tab");
169
+ });
170
+
171
+ it("submits without FrappeForm's own confirm, so the host owns the question", async () => {
172
+ const fixtures = assetLoanLifecycleFixtures();
173
+ renderWithProviders(
174
+ <FrappeProvider baseURL="http://test" fixtures={fixtures}>
175
+ <Host doctype="Asset Loan" name="LOAN-DRAFT" />
176
+ </FrappeProvider>,
177
+ );
178
+ await expectProbe("employee_name", "val=Ada Lovelace");
179
+ await waitFor(() => expect(controller?.rights.submit).toBe(true));
180
+ fireEvent.click(screen.getByTestId("host-submit"));
181
+ await waitFor(async () =>
182
+ expect((await fixtures.getDoc("Asset Loan", "LOAN-DRAFT"))?.docstatus).toBe(1),
183
+ );
184
+ await waitFor(() => expect(controller?.docstatus).toBe(1));
185
+ expect(confirm).not.toHaveBeenCalled();
186
+ });
187
+
188
+ it("save resolves with the created record's name", async () => {
189
+ const fixtures = ticketFixtures();
190
+ renderWithProviders(
191
+ <FrappeProvider baseURL="http://test" fixtures={fixtures}>
192
+ <Host doctype="Desk Ticket" />
193
+ </FrappeProvider>,
194
+ );
195
+ await expectProbe("priority", "val=Medium");
196
+ act(() => {
197
+ controller?.setValue("subject", "Scanner");
198
+ controller?.setValue("resolution", "Rebooted");
199
+ });
200
+ await waitFor(() => expect(controller?.doc.subject).toBe("Scanner"));
201
+ let saved: string | undefined;
202
+ await act(async () => {
203
+ saved = await controller?.save();
204
+ });
205
+ expect(saved).toBeTruthy();
206
+ const created = await fixtures.getDoc("Desk Ticket", String(saved));
207
+ expect(created).toMatchObject({ subject: "Scanner", priority: "Medium", status: "Open" });
208
+ });
209
+ });