@nocobase/client-v2 2.2.0-beta.8 → 2.2.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 (271) hide show
  1. package/es/APIClient.d.ts +13 -3
  2. package/es/BaseApplication.d.ts +3 -0
  3. package/es/RouteRepository.d.ts +8 -0
  4. package/es/RouterManager.d.ts +15 -0
  5. package/es/authRedirect.d.ts +1 -0
  6. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  7. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  8. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  9. package/es/components/category-tabs/index.d.ts +9 -0
  10. package/es/components/form/ScanInput/useCodeScanner.d.ts +3 -2
  11. package/es/components/form/TypedVariableInput.d.ts +31 -5
  12. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  13. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  14. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  15. package/es/components/form/filter/index.d.ts +2 -0
  16. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  17. package/es/components/index.d.ts +1 -0
  18. package/es/entry-actions/EntryActionManager.d.ts +24 -0
  19. package/es/entry-actions/index.d.ts +9 -0
  20. package/es/flow/actions/index.d.ts +1 -1
  21. package/es/flow/actions/linkageRules.d.ts +27 -0
  22. package/es/flow/admin-shell/BaseLayoutModel.d.ts +6 -0
  23. package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +7 -0
  24. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
  25. package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +2 -1
  26. package/es/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.d.ts +24 -0
  27. package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
  28. package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
  29. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  30. package/es/flow/components/FieldAssignValueInput.d.ts +7 -24
  31. package/es/flow/components/FlowRoute.d.ts +2 -2
  32. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  33. package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
  34. package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +31 -0
  35. package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
  36. package/es/flow/components/field-value-variable/index.d.ts +11 -0
  37. package/es/flow/components/filter/FilterGroup.d.ts +1 -0
  38. package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
  39. package/es/flow/components/placeholders/BlockPlaceholder.d.ts +1 -0
  40. package/es/flow/index.d.ts +1 -0
  41. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  42. package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -2
  43. package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
  44. package/es/flow/models/base/ActionModelCore.d.ts +6 -2
  45. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  46. package/es/flow/models/base/CollectionBlockModel.d.ts +3 -0
  47. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  48. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  49. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  50. package/es/flow/models/blocks/details/DetailsBlockModel.d.ts +3 -0
  51. package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
  52. package/es/flow/models/blocks/filter-form/FilterFormSubmitActionModel.d.ts +3 -1
  53. package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
  54. package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
  55. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  56. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
  57. package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
  58. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  59. package/es/flow/models/blocks/table/TableColumnModel.d.ts +12 -0
  60. package/es/flow/models/blocks/table/utils.d.ts +1 -0
  61. package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +2 -1
  62. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +0 -17
  63. package/es/flow/models/fields/AssociationFieldModel/recordSelectShared.d.ts +41 -0
  64. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  65. package/es/flow/routeTransientInputArgs.d.ts +14 -0
  66. package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
  67. package/es/flow-compat/passwordUtils.d.ts +1 -1
  68. package/es/flow-compat/routeTypes.d.ts +1 -0
  69. package/es/index.d.ts +7 -0
  70. package/es/index.mjs +301 -177
  71. package/es/ui-operation/index.d.ts +15 -0
  72. package/es/ui-operation/ui-operation-codec.d.ts +10 -0
  73. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  74. package/es/utils/index.d.ts +1 -0
  75. package/es/utils/markdownSanitize.d.ts +11 -0
  76. package/lib/index.js +313 -189
  77. package/lib/locale/languageCodes.js +3 -1
  78. package/package.json +9 -7
  79. package/src/APIClient.ts +90 -7
  80. package/src/Application.tsx +7 -1
  81. package/src/BaseApplication.tsx +13 -6
  82. package/src/RouteRepository.ts +25 -0
  83. package/src/RouterManager.tsx +142 -1
  84. package/src/__tests__/APIClient.test.tsx +58 -0
  85. package/src/__tests__/RouteRepository.test.ts +23 -0
  86. package/src/__tests__/RouterManager.test.ts +125 -0
  87. package/src/__tests__/app.test.tsx +73 -0
  88. package/src/__tests__/authRedirect.test.ts +17 -0
  89. package/src/__tests__/browserChecker.test.ts +61 -0
  90. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  91. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +35 -2
  92. package/src/__tests__/settings-center.test.tsx +270 -6
  93. package/src/acl/ACLProvider.tsx +2 -0
  94. package/src/acl/__tests__/ACLProvider.test.tsx +92 -0
  95. package/src/authRedirect.ts +38 -0
  96. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  97. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  98. package/src/collection-manager/field-validation.ts +1 -1
  99. package/src/components/README.md +7 -1
  100. package/src/components/README.zh-CN.md +6 -1
  101. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  102. package/src/components/category-tabs/index.ts +10 -0
  103. package/src/components/form/JsonTextArea.tsx +4 -0
  104. package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
  105. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  106. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
  107. package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
  108. package/src/components/form/TypedVariableInput.tsx +452 -101
  109. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  110. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  111. package/src/components/form/filter/CollectionFilterItem.tsx +36 -9
  112. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  113. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +55 -0
  114. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  115. package/src/components/form/filter/index.ts +2 -0
  116. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  117. package/src/components/form/table/Table.tsx +81 -6
  118. package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
  119. package/src/components/index.ts +1 -0
  120. package/src/entry-actions/EntryActionManager.ts +76 -0
  121. package/src/entry-actions/index.ts +10 -0
  122. package/src/flow/FlowPage.tsx +38 -3
  123. package/src/flow/__tests__/FlowPage.test.tsx +201 -27
  124. package/src/flow/__tests__/FlowRoute.test.tsx +675 -12
  125. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  126. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  127. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +62 -0
  128. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  129. package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
  130. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +72 -0
  131. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  132. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  133. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +80 -20
  134. package/src/flow/actions/dataScopeFilter.ts +10 -1
  135. package/src/flow/actions/dateTimeFormat.tsx +2 -2
  136. package/src/flow/actions/index.ts +2 -0
  137. package/src/flow/actions/linkageRules.tsx +144 -26
  138. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  139. package/src/flow/actions/openView.tsx +21 -1
  140. package/src/flow/admin-shell/BaseLayoutModel.tsx +124 -0
  141. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +184 -42
  142. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1016 -119
  143. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +41 -5
  144. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +177 -1
  145. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +20 -0
  146. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
  147. package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
  148. package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
  149. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
  150. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
  151. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +436 -2
  152. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +48 -0
  153. package/src/flow/admin-shell/admin-layout/index.ts +1 -0
  154. package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
  155. package/src/flow/common/Markdown/Display.tsx +14 -3
  156. package/src/flow/common/Markdown/Edit.tsx +19 -7
  157. package/src/flow/common/Markdown/style.ts +4 -0
  158. package/src/flow/components/BlockItemCard.tsx +2 -2
  159. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  160. package/src/flow/components/FieldAssignValueInput.tsx +72 -584
  161. package/src/flow/components/FlowRoute.tsx +204 -46
  162. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  163. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
  164. package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
  165. package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +326 -0
  166. package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +380 -0
  167. package/src/flow/components/field-value-variable/dateValue.ts +223 -0
  168. package/src/flow/components/field-value-variable/index.ts +12 -0
  169. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  170. package/src/flow/components/filter/VariableFilterItem.tsx +170 -13
  171. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  172. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  173. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +195 -1
  174. package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
  175. package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
  176. package/src/flow/index.ts +3 -0
  177. package/src/flow/internal/components/Markdown/util.ts +2 -1
  178. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  179. package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
  180. package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
  181. package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
  182. package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
  183. package/src/flow/models/base/ActionModel.tsx +10 -8
  184. package/src/flow/models/base/ActionModelCore.tsx +16 -2
  185. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  186. package/src/flow/models/base/CollectionBlockModel.tsx +38 -3
  187. package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
  188. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  189. package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
  190. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
  191. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  192. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  193. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
  194. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  195. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  196. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  197. package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
  198. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +28 -53
  199. package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
  200. package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
  201. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +28 -3
  202. package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
  203. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +155 -25
  204. package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
  205. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
  206. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +395 -1
  207. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
  208. package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
  209. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +66 -4
  210. package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
  211. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
  212. package/src/flow/models/blocks/form/FormBlockModel.tsx +47 -0
  213. package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
  214. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  215. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +8 -2
  216. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  217. package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
  218. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
  219. package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
  220. package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
  221. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  222. package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
  223. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
  224. package/src/flow/models/blocks/table/TableBlockModel.tsx +29 -3
  225. package/src/flow/models/blocks/table/TableColumnModel.tsx +54 -11
  226. package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
  227. package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
  228. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  229. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +84 -0
  230. package/src/flow/models/blocks/table/utils.ts +7 -0
  231. package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
  232. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +10 -1
  233. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
  234. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
  235. package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +24 -1
  236. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +54 -149
  237. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +167 -3
  238. package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
  239. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  240. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  241. package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
  242. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  243. package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
  244. package/src/flow/models/fields/TextareaFieldModel.tsx +42 -19
  245. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  246. package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
  247. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  248. package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
  249. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +32 -1
  250. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  251. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +26 -10
  252. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +51 -14
  253. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
  254. package/src/flow/models/topbar/TopbarActionModel.tsx +78 -3
  255. package/src/flow/routeTransientInputArgs.ts +27 -0
  256. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +42 -0
  257. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  258. package/src/flow-compat/fieldValidationConstants.ts +1 -1
  259. package/src/flow-compat/routeTypes.ts +1 -0
  260. package/src/index.ts +7 -0
  261. package/src/layout-manager/LayoutContentRoute.tsx +2 -1
  262. package/src/layout-manager/LayoutRoute.tsx +2 -1
  263. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
  264. package/src/locale/languageCodes.ts +3 -1
  265. package/src/nocobase-buildin-plugin/index.tsx +19 -1
  266. package/src/settings-center/AdminSettingsLayout.tsx +23 -2
  267. package/src/ui-operation/index.ts +33 -0
  268. package/src/ui-operation/ui-operation-codec.ts +54 -0
  269. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  270. package/src/utils/index.tsx +1 -0
  271. package/src/utils/markdownSanitize.ts +88 -0
@@ -7,13 +7,15 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { render, waitFor } from '@testing-library/react';
10
+ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
11
11
  import React, { useCallback } from 'react';
12
- import { beforeEach, describe, expect, it, vi } from 'vitest';
13
- import { DEFAULT_CODE_FORMATS, getCodeScanBoxSize, useCodeScanner } from '../useCodeScanner';
12
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
13
+ import { DEFAULT_CODE_FORMATS, getCodeScanBoxSize, scanQrVideoFrame, useCodeScanner } from '../useCodeScanner';
14
14
 
15
15
  type MockScannerInstance = {
16
+ applyVideoConstraints: ReturnType<typeof vi.fn>;
16
17
  clear: ReturnType<typeof vi.fn>;
18
+ getRunningTrackCapabilities: ReturnType<typeof vi.fn>;
17
19
  getState: ReturnType<typeof vi.fn>;
18
20
  scanFileV2: ReturnType<typeof vi.fn>;
19
21
  start: ReturnType<typeof vi.fn>;
@@ -24,18 +26,24 @@ const mocks = vi.hoisted(() => {
24
26
  const start = vi.fn().mockResolvedValue(null);
25
27
  const stop = vi.fn().mockResolvedValue(null);
26
28
  const clear = vi.fn();
29
+ const applyVideoConstraints = vi.fn().mockResolvedValue(undefined);
30
+ const getRunningTrackCapabilities = vi.fn(() => ({}));
27
31
  const getState = vi.fn(() => 1);
28
32
  const scanFileV2 = vi.fn();
29
33
  const Html5Qrcode = vi.fn(function MockHtml5Qrcode(this: MockScannerInstance) {
30
34
  this.start = start;
31
35
  this.stop = stop;
32
36
  this.clear = clear;
37
+ this.applyVideoConstraints = applyVideoConstraints;
38
+ this.getRunningTrackCapabilities = getRunningTrackCapabilities;
33
39
  this.getState = getState;
34
40
  this.scanFileV2 = scanFileV2;
35
41
  });
36
42
 
37
43
  return {
44
+ applyVideoConstraints,
38
45
  clear,
46
+ getRunningTrackCapabilities,
39
47
  getState,
40
48
  Html5Qrcode,
41
49
  scanFileV2,
@@ -44,6 +52,12 @@ const mocks = vi.hoisted(() => {
44
52
  };
45
53
  });
46
54
 
55
+ const jsQrMocks = vi.hoisted(() => {
56
+ return {
57
+ default: vi.fn(),
58
+ };
59
+ });
60
+
47
61
  vi.mock('html5-qrcode', () => ({
48
62
  Html5Qrcode: mocks.Html5Qrcode,
49
63
  Html5QrcodeScannerState: {
@@ -66,22 +80,101 @@ vi.mock('html5-qrcode', () => ({
66
80
  },
67
81
  }));
68
82
 
69
- function ScannerHost({ scanBoxSize }: { scanBoxSize?: { width: number; height: number } } = {}) {
83
+ vi.mock('jsqr', () => jsQrMocks);
84
+
85
+ function ScannerHost({
86
+ onCameraStartFailure,
87
+ onScanFailure,
88
+ }: {
89
+ onCameraStartFailure?: (error: unknown) => void;
90
+ onScanFailure?: () => void;
91
+ } = {}) {
70
92
  const handleScanSuccess = useCallback(() => undefined, []);
71
93
 
72
94
  useCodeScanner({
73
95
  elementId: 'scanner',
74
96
  enabled: true,
75
- scanBoxSize,
97
+ onCameraStartFailure,
98
+ onScanFailure,
76
99
  onScanSuccess: handleScanSuccess,
77
100
  });
78
101
 
79
102
  return <div id="scanner" />;
80
103
  }
81
104
 
105
+ function FileScannerHost({ onScanSuccess }: { onScanSuccess: (text: string) => void }) {
106
+ const { startScanFile } = useCodeScanner({
107
+ elementId: 'scanner',
108
+ enabled: true,
109
+ onScanSuccess,
110
+ });
111
+
112
+ return (
113
+ <>
114
+ <div id="scanner" />
115
+ <button onClick={() => startScanFile(new File(['qr'], 'qr.png', { type: 'image/png' }))}>scan file</button>
116
+ </>
117
+ );
118
+ }
119
+
120
+ function stubSafariBrowser() {
121
+ vi.spyOn(window.navigator, 'vendor', 'get').mockReturnValue('Apple Computer, Inc.');
122
+ vi.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue(
123
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15',
124
+ );
125
+ }
126
+
127
+ function stubImageElement(size: { width: number; height: number } = { width: 600, height: 400 }) {
128
+ vi.stubGlobal(
129
+ 'Image',
130
+ class MockImage {
131
+ onabort: ((event: Event) => void) | null = null;
132
+ onerror: ((event: Event) => void) | null = null;
133
+ onload: (() => void) | null = null;
134
+ height = size.height;
135
+ naturalHeight = size.height;
136
+ naturalWidth = size.width;
137
+ width = size.width;
138
+
139
+ set src(_value: string) {
140
+ this.onload?.();
141
+ }
142
+ },
143
+ );
144
+ vi.stubGlobal('URL', {
145
+ ...URL,
146
+ createObjectURL: vi.fn(() => 'blob:qr'),
147
+ revokeObjectURL: vi.fn(),
148
+ });
149
+ }
150
+
151
+ function stubCanvas() {
152
+ const canvasContext = {
153
+ drawImage: vi.fn(),
154
+ getImageData: vi.fn((_x: number, _y: number, width: number, height: number) => ({
155
+ data: new Uint8ClampedArray(width * height * 4),
156
+ height,
157
+ width,
158
+ })),
159
+ } as unknown as CanvasRenderingContext2D;
160
+
161
+ vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(canvasContext);
162
+ }
163
+
82
164
  describe('useCodeScanner', () => {
83
165
  beforeEach(() => {
84
166
  vi.clearAllMocks();
167
+ mocks.applyVideoConstraints.mockResolvedValue(undefined);
168
+ mocks.getRunningTrackCapabilities.mockReturnValue({});
169
+ mocks.start.mockResolvedValue(null);
170
+ mocks.stop.mockResolvedValue(null);
171
+ mocks.getState.mockReturnValue(1);
172
+ jsQrMocks.default.mockReturnValue(undefined);
173
+ });
174
+
175
+ afterEach(() => {
176
+ vi.unstubAllGlobals();
177
+ vi.restoreAllMocks();
85
178
  });
86
179
 
87
180
  it('starts scanning with QR code and barcode formats by default', async () => {
@@ -105,19 +198,199 @@ describe('useCodeScanner', () => {
105
198
  | { qrbox?: (width: number, height: number) => { width: number; height: number } }
106
199
  | undefined;
107
200
 
108
- expect(config?.qrbox?.(390, 844)).toEqual({ width: 319, height: 240 });
109
- expect(getCodeScanBoxSize(390, 844)).toEqual({ width: 319, height: 240 });
201
+ expect(config?.qrbox?.(1280, 720)).toEqual({ width: 1152, height: 504 });
202
+ expect(getCodeScanBoxSize(1280, 720)).toEqual({ width: 1152, height: 504 });
110
203
  });
111
204
 
112
- it('can use the visible viewport scan box when the camera video is wider than the viewport', async () => {
113
- render(<ScannerHost scanBoxSize={{ width: 319, height: 240 }} />);
205
+ it('uses optimized camera constraints without requiring a user-selected scan mode', async () => {
206
+ render(<ScannerHost />);
114
207
 
115
208
  await waitFor(() => expect(mocks.start).toHaveBeenCalled());
116
209
 
117
210
  const config = mocks.start.mock.calls[0]?.[1] as
118
- | { qrbox?: (width: number, height: number) => { width: number; height: number } }
211
+ | {
212
+ disableFlip?: boolean;
213
+ fps?: number;
214
+ qrbox?: (width: number, height: number) => { width: number; height: number };
215
+ videoConstraints?: MediaTrackConstraints;
216
+ }
119
217
  | undefined;
120
218
 
121
- expect(config?.qrbox?.(1200, 844)).toEqual({ width: 319, height: 240 });
219
+ expect(config?.qrbox?.(1280, 720)).toEqual({ width: 1152, height: 504 });
220
+ expect(config).toMatchObject({
221
+ disableFlip: false,
222
+ fps: 8,
223
+ videoConstraints: {
224
+ facingMode: { ideal: 'environment' },
225
+ width: { ideal: 1920 },
226
+ height: { ideal: 1080 },
227
+ frameRate: { ideal: 30 },
228
+ },
229
+ });
230
+ expect(mocks.Html5Qrcode).toHaveBeenCalledWith('scanner', {
231
+ formatsToSupport: DEFAULT_CODE_FORMATS,
232
+ verbose: false,
233
+ });
234
+ });
235
+
236
+ it('decodes a centered QR code from the raw camera frame fast path', () => {
237
+ jsQrMocks.default.mockReturnValueOnce({ data: 'FAST-QR' });
238
+ stubCanvas();
239
+ const video = document.createElement('video');
240
+ Object.defineProperties(video, {
241
+ clientHeight: { value: 720 },
242
+ clientWidth: { value: 1280 },
243
+ readyState: { value: HTMLMediaElement.HAVE_CURRENT_DATA },
244
+ videoHeight: { value: 1080 },
245
+ videoWidth: { value: 1920 },
246
+ });
247
+ const canvas = document.createElement('canvas');
248
+
249
+ expect(scanQrVideoFrame(video, canvas)).toBe('FAST-QR');
250
+
251
+ const context = canvas.getContext('2d');
252
+ expect(context?.drawImage).toHaveBeenCalledWith(video, 96, 162, 1728, 756, 0, 0, 960, 420);
253
+ expect(jsQrMocks.default).toHaveBeenCalledWith(expect.any(Uint8ClampedArray), 960, 420, {
254
+ inversionAttempts: 'dontInvert',
255
+ });
256
+ });
257
+
258
+ it('enables continuous camera focus when the device supports it', async () => {
259
+ mocks.getRunningTrackCapabilities.mockReturnValue({ focusMode: ['manual', 'continuous'] });
260
+
261
+ render(<ScannerHost />);
262
+
263
+ await waitFor(() =>
264
+ expect(mocks.applyVideoConstraints).toHaveBeenCalledWith({
265
+ advanced: [{ focusMode: 'continuous' }],
266
+ }),
267
+ );
268
+ });
269
+
270
+ it('reports camera start failures through the dedicated handler', async () => {
271
+ const cameraStartError = Object.assign(new Error('Camera failed'), { name: 'NotAllowedError' });
272
+ const handleCameraStartFailure = vi.fn();
273
+ const handleScanFailure = vi.fn();
274
+ mocks.start.mockRejectedValueOnce(cameraStartError);
275
+
276
+ render(<ScannerHost onCameraStartFailure={handleCameraStartFailure} onScanFailure={handleScanFailure} />);
277
+
278
+ await waitFor(() => expect(handleCameraStartFailure).toHaveBeenCalledWith(cameraStartError));
279
+ expect(handleScanFailure).not.toHaveBeenCalled();
280
+ });
281
+
282
+ it('stops a camera that finishes starting after the scanner unmounts', async () => {
283
+ let resolveStart: ((value: null) => void) | undefined;
284
+ mocks.start.mockReturnValueOnce(
285
+ new Promise<null>((resolve) => {
286
+ resolveStart = resolve;
287
+ }),
288
+ );
289
+ mocks.getState.mockReturnValueOnce(1).mockReturnValue(2);
290
+
291
+ const { unmount } = render(<ScannerHost />);
292
+ await waitFor(() => expect(mocks.start).toHaveBeenCalled());
293
+
294
+ unmount();
295
+ await act(async () => {
296
+ resolveStart?.(null);
297
+ });
298
+
299
+ await waitFor(() => expect(mocks.stop).toHaveBeenCalledTimes(1));
300
+ expect(mocks.clear).toHaveBeenCalled();
301
+ });
302
+
303
+ it('ignores a camera start failure after the scanner unmounts', async () => {
304
+ let rejectStart: ((error: Error) => void) | undefined;
305
+ mocks.start.mockReturnValueOnce(
306
+ new Promise<null>((_resolve, reject) => {
307
+ rejectStart = reject;
308
+ }),
309
+ );
310
+ const handleCameraStartFailure = vi.fn();
311
+ const handleScanFailure = vi.fn();
312
+
313
+ const { unmount } = render(
314
+ <ScannerHost onCameraStartFailure={handleCameraStartFailure} onScanFailure={handleScanFailure} />,
315
+ );
316
+ await waitFor(() => expect(mocks.start).toHaveBeenCalled());
317
+
318
+ unmount();
319
+ await act(async () => {
320
+ rejectStart?.(new Error('Camera start canceled'));
321
+ });
322
+
323
+ expect(handleCameraStartFailure).not.toHaveBeenCalled();
324
+ expect(handleScanFailure).not.toHaveBeenCalled();
325
+ });
326
+
327
+ it('uses jsQR first for Safari uploaded QR images', async () => {
328
+ const handleScanSuccess = vi.fn();
329
+ jsQrMocks.default.mockReturnValueOnce({ data: 'JSQR-CODE' });
330
+ stubSafariBrowser();
331
+ stubImageElement();
332
+ stubCanvas();
333
+
334
+ render(<FileScannerHost onScanSuccess={handleScanSuccess} />);
335
+ await waitFor(() => expect(mocks.start).toHaveBeenCalled());
336
+
337
+ fireEvent.click(screen.getByText('scan file'));
338
+
339
+ await waitFor(() => expect(handleScanSuccess).toHaveBeenCalledWith('JSQR-CODE'));
340
+ expect(jsQrMocks.default).toHaveBeenCalledWith(expect.any(Uint8ClampedArray), 600, 400, {
341
+ inversionAttempts: 'attemptBoth',
342
+ });
343
+ expect(mocks.scanFileV2).not.toHaveBeenCalled();
344
+ });
345
+
346
+ it('falls back to html5-qrcode for Safari uploaded files when jsQR does not decode a QR code', async () => {
347
+ const handleScanSuccess = vi.fn();
348
+ mocks.scanFileV2.mockResolvedValueOnce({ decodedText: 'BARCODE-CODE' });
349
+ stubSafariBrowser();
350
+ stubImageElement();
351
+ stubCanvas();
352
+
353
+ render(<FileScannerHost onScanSuccess={handleScanSuccess} />);
354
+ await waitFor(() => expect(mocks.start).toHaveBeenCalled());
355
+
356
+ fireEvent.click(screen.getByText('scan file'));
357
+
358
+ await waitFor(() => expect(handleScanSuccess).toHaveBeenCalledWith('BARCODE-CODE'));
359
+ expect(jsQrMocks.default).toHaveBeenCalled();
360
+ expect(mocks.scanFileV2).toHaveBeenCalled();
361
+ });
362
+
363
+ it('tries multiple image scales before falling back from Safari jsQR scanning', async () => {
364
+ const handleScanSuccess = vi.fn();
365
+ Array.from({ length: 8 }).forEach((_, index) => {
366
+ jsQrMocks.default.mockReturnValueOnce(index === 7 ? { data: 'SCALED-CODE' } : undefined);
367
+ });
368
+ stubSafariBrowser();
369
+ stubImageElement({ width: 4000, height: 3000 });
370
+ stubCanvas();
371
+
372
+ render(<FileScannerHost onScanSuccess={handleScanSuccess} />);
373
+ await waitFor(() => expect(mocks.start).toHaveBeenCalled());
374
+
375
+ fireEvent.click(screen.getByText('scan file'));
376
+
377
+ await waitFor(() => expect(handleScanSuccess).toHaveBeenCalledWith('SCALED-CODE'));
378
+ expect(jsQrMocks.default).toHaveBeenCalledTimes(8);
379
+ });
380
+
381
+ it('tries enhanced grayscale QR images for blurry Safari uploads', async () => {
382
+ const handleScanSuccess = vi.fn();
383
+ jsQrMocks.default.mockReturnValueOnce(undefined).mockReturnValueOnce({ data: 'ENHANCED-CODE' });
384
+ stubSafariBrowser();
385
+ stubImageElement();
386
+ stubCanvas();
387
+
388
+ render(<FileScannerHost onScanSuccess={handleScanSuccess} />);
389
+ await waitFor(() => expect(mocks.start).toHaveBeenCalled());
390
+
391
+ fireEvent.click(screen.getByText('scan file'));
392
+
393
+ await waitFor(() => expect(handleScanSuccess).toHaveBeenCalledWith('ENHANCED-CODE'));
394
+ expect(jsQrMocks.default).toHaveBeenCalledTimes(2);
122
395
  });
123
396
  });