@nocobase/client-v2 2.2.0-beta.9 → 3.0.0-alpha.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 (208) hide show
  1. package/es/APIClient.d.ts +1 -3
  2. package/es/BaseApplication.d.ts +1 -0
  3. package/es/acl/ACLProvider.d.ts +10 -0
  4. package/es/authRedirect.d.ts +5 -0
  5. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  6. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  7. package/es/components/AttachmentUpload.d.ts +51 -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 +2 -0
  18. package/es/flow/actions/index.d.ts +1 -1
  19. package/es/flow/actions/linkageRules.d.ts +3 -0
  20. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
  21. package/es/flow/admin-shell/admin-layout/NocoBaseLogo.d.ts +10 -0
  22. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  23. package/es/flow/components/FieldAssignValueInput.d.ts +9 -0
  24. package/es/flow/components/FlowRoute.d.ts +2 -2
  25. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  26. package/es/flow/components/filter/FilterGroup.d.ts +1 -0
  27. package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
  28. package/es/flow/index.d.ts +1 -0
  29. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  30. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  31. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  32. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  33. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  34. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  35. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  36. package/es/flow/models/blocks/table/TableColumnModel.d.ts +1 -0
  37. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  38. package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
  39. package/es/flow-compat/routeTypes.d.ts +1 -0
  40. package/es/index.d.ts +8 -1
  41. package/es/index.mjs +190 -141
  42. package/es/nocobase-buildin-plugin/currentUserAuthStatus.d.ts +11 -0
  43. package/es/nocobase-buildin-plugin/index.d.ts +9 -0
  44. package/es/settings-app/SettingsApplication.d.ts +18 -0
  45. package/es/settings-app/SettingsBrand.d.ts +16 -0
  46. package/es/settings-app/SettingsBuildInPlugin.d.ts +9 -0
  47. package/es/settings-app/SettingsDocumentRedirect.d.ts +10 -0
  48. package/es/settings-app/SettingsGroupNav.d.ts +17 -0
  49. package/es/settings-app/SettingsPluginSettingsManager.d.ts +14 -0
  50. package/es/settings-app/SettingsRouterManager.d.ts +15 -0
  51. package/es/settings-app/SettingsSearch.d.ts +17 -0
  52. package/es/settings-app/SettingsShell.d.ts +10 -0
  53. package/es/settings-app/isSettingsApp.d.ts +18 -0
  54. package/es/settings-app/runtimePaths.d.ts +18 -0
  55. package/es/settings-app/settingsDocumentPath.d.ts +14 -0
  56. package/es/settings-app/settingsTheme.d.ts +87 -0
  57. package/es/settings-app/useSettingsThemeConfig.d.ts +22 -0
  58. package/es/settings-center/AdminSettingsLayout.d.ts +2 -2
  59. package/es/settings-center/groups.d.ts +66 -0
  60. package/es/settings-center/useSettingsGroups.d.ts +36 -0
  61. package/es/settings-center/useSettingsSearch.d.ts +35 -0
  62. package/es/settings-center/utils.d.ts +24 -0
  63. package/es/theme/index.d.ts +1 -0
  64. package/es/theme/itemActive.d.ts +24 -0
  65. package/es/theme/type.d.ts +9 -0
  66. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  67. package/es/utils/index.d.ts +1 -0
  68. package/lib/index.js +195 -146
  69. package/lib/locale/languageCodes.js +3 -1
  70. package/package.json +9 -7
  71. package/src/APIClient.ts +1 -10
  72. package/src/Application.tsx +4 -0
  73. package/src/BaseApplication.tsx +11 -6
  74. package/src/__tests__/app.test.tsx +73 -0
  75. package/src/__tests__/authRedirect.test.ts +137 -0
  76. package/src/__tests__/browserChecker.test.ts +185 -6
  77. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  78. package/src/__tests__/mockSettingsApplication.ts +29 -0
  79. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +79 -10
  80. package/src/__tests__/plugin-manager.test.tsx +7 -6
  81. package/src/__tests__/settings-application.test.ts +164 -0
  82. package/src/__tests__/settings-auth-routes.test.ts +71 -0
  83. package/src/__tests__/settings-center.test.tsx +38 -37
  84. package/src/__tests__/settings-layout-root.test.tsx +260 -0
  85. package/src/__tests__/settings-runtime-paths.test.ts +99 -0
  86. package/src/__tests__/settings-shell.test.tsx +200 -0
  87. package/src/acl/ACLProvider.tsx +21 -0
  88. package/src/authRedirect.ts +72 -3
  89. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  90. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  91. package/src/collection-manager/field-validation.ts +1 -1
  92. package/src/components/AttachmentUpload.tsx +275 -0
  93. package/src/components/README.md +7 -1
  94. package/src/components/README.zh-CN.md +6 -1
  95. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  96. package/src/components/category-tabs/index.ts +10 -0
  97. package/src/components/form/JsonTextArea.tsx +4 -0
  98. package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
  99. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  100. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
  101. package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
  102. package/src/components/form/TypedVariableInput.tsx +452 -101
  103. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  104. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  105. package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
  106. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  107. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
  108. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  109. package/src/components/form/filter/index.ts +2 -0
  110. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  111. package/src/components/index.ts +2 -0
  112. package/src/flow/FlowPage.tsx +9 -1
  113. package/src/flow/__tests__/FlowPage.test.tsx +50 -3
  114. package/src/flow/__tests__/FlowRoute.test.tsx +128 -10
  115. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  116. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  117. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  118. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  119. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  120. package/src/flow/actions/index.ts +2 -0
  121. package/src/flow/actions/linkageRules.tsx +111 -16
  122. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  123. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -57
  124. package/src/flow/admin-shell/admin-layout/NocoBaseLogo.tsx +77 -0
  125. package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +37 -14
  126. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
  127. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +77 -13
  128. package/src/flow/common/Markdown/style.ts +4 -0
  129. package/src/flow/components/BlockItemCard.tsx +2 -2
  130. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  131. package/src/flow/components/FieldAssignValueInput.tsx +91 -20
  132. package/src/flow/components/FlowRoute.tsx +78 -32
  133. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  134. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
  135. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  136. package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
  137. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  138. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  139. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
  140. package/src/flow/index.ts +3 -0
  141. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  142. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  143. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  144. package/src/flow/models/base/CollectionBlockModel.tsx +6 -2
  145. package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
  146. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  147. package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
  148. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
  149. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  150. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  151. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
  152. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  153. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  154. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  155. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +28 -3
  156. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
  157. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
  158. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +66 -4
  159. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  160. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  161. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  162. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
  163. package/src/flow/models/blocks/table/TableBlockModel.tsx +4 -2
  164. package/src/flow/models/blocks/table/TableColumnModel.tsx +30 -1
  165. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  166. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  167. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +2 -1
  168. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  169. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  170. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  171. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  172. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  173. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  174. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
  175. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
  176. package/src/flow/models/topbar/TopbarActionModel.tsx +52 -180
  177. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  178. package/src/flow-compat/fieldValidationConstants.ts +1 -1
  179. package/src/flow-compat/routeTypes.ts +1 -0
  180. package/src/index.ts +14 -1
  181. package/src/locale/languageCodes.ts +3 -1
  182. package/src/nocobase-buildin-plugin/currentUserAuthStatus.ts +58 -0
  183. package/src/nocobase-buildin-plugin/index.tsx +110 -70
  184. package/src/settings-app/SettingsApplication.ts +31 -0
  185. package/src/settings-app/SettingsBrand.tsx +108 -0
  186. package/src/settings-app/SettingsBuildInPlugin.ts +10 -0
  187. package/src/settings-app/SettingsDocumentRedirect.tsx +26 -0
  188. package/src/settings-app/SettingsGroupNav.tsx +124 -0
  189. package/src/settings-app/SettingsPluginSettingsManager.ts +38 -0
  190. package/src/settings-app/SettingsRouterManager.ts +92 -0
  191. package/src/settings-app/SettingsSearch.tsx +238 -0
  192. package/src/settings-app/SettingsShell.tsx +167 -0
  193. package/src/settings-app/isSettingsApp.ts +21 -0
  194. package/src/settings-app/runtimePaths.ts +104 -0
  195. package/src/settings-app/settingsDocumentPath.ts +66 -0
  196. package/src/settings-app/settingsTheme.ts +301 -0
  197. package/src/settings-app/useSettingsThemeConfig.ts +100 -0
  198. package/src/settings-center/AdminSettingsLayout.tsx +122 -141
  199. package/src/settings-center/SystemSettingsPage.tsx +4 -170
  200. package/src/settings-center/groups.ts +108 -0
  201. package/src/settings-center/useSettingsGroups.ts +105 -0
  202. package/src/settings-center/useSettingsSearch.ts +173 -0
  203. package/src/settings-center/utils.tsx +50 -1
  204. package/src/theme/index.tsx +1 -0
  205. package/src/theme/itemActive.ts +31 -0
  206. package/src/theme/type.ts +10 -0
  207. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  208. package/src/utils/index.tsx +1 -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
  });