id-scanner-lib 1.5.0 → 1.6.2

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 (72) hide show
  1. package/README.md +378 -59
  2. package/dist/id-scanner-lib.esm.js +195 -10
  3. package/dist/id-scanner-lib.esm.js.map +1 -1
  4. package/dist/id-scanner-lib.js +4812 -14709
  5. package/dist/id-scanner-lib.js.map +1 -1
  6. package/dist/types/browser-image-compression.d.ts +19 -0
  7. package/dist/types/tesseract.d.ts +280 -0
  8. package/package.json +21 -11
  9. package/src/core/camera-manager.ts +16 -1
  10. package/src/core/config.ts +37 -0
  11. package/src/core/errors.ts +3 -3
  12. package/src/core/event-emitter.test.ts +42 -0
  13. package/src/core/loading-state.test.ts +67 -0
  14. package/src/core/loading-state.ts +156 -0
  15. package/src/core/logger.test.ts +49 -0
  16. package/src/core/module-manager.ts +2 -4
  17. package/src/core/scanner-factory.ts +8 -9
  18. package/src/index.ts +3 -2
  19. package/src/modules/face/face-detector.ts +123 -66
  20. package/src/modules/id-card/anti-fake-detector.ts +2 -2
  21. package/src/modules/id-card/ocr-worker.ts +1 -1
  22. package/src/modules/qrcode/qr-code-scanner.ts +2 -1
  23. package/src/modules/qrcode/types.ts +111 -7
  24. package/src/types/common.test.ts +99 -0
  25. package/src/types/common.ts +166 -0
  26. package/src/utils/camera.test.ts +30 -0
  27. package/src/utils/camera.ts +4 -1
  28. package/src/utils/error-handler.test.ts +137 -0
  29. package/src/utils/error-handler.ts +110 -0
  30. package/src/utils/index.test.ts +186 -0
  31. package/src/utils/index.ts +3 -0
  32. package/src/utils/retry.test.ts +142 -0
  33. package/src/utils/retry.ts +282 -0
  34. package/src/utils/utils.test.ts +171 -0
  35. package/src/version.ts +1 -1
  36. package/dist/types/core/base-module.d.ts +0 -44
  37. package/dist/types/core/camera-manager.d.ts +0 -258
  38. package/dist/types/core/config.d.ts +0 -88
  39. package/dist/types/core/errors.d.ts +0 -111
  40. package/dist/types/core/event-emitter.d.ts +0 -55
  41. package/dist/types/core/logger.d.ts +0 -277
  42. package/dist/types/core/module-manager.d.ts +0 -78
  43. package/dist/types/core/plugin-manager.d.ts +0 -158
  44. package/dist/types/core/resource-manager.d.ts +0 -246
  45. package/dist/types/core/result.d.ts +0 -83
  46. package/dist/types/core/scanner-factory.d.ts +0 -93
  47. package/dist/types/index.bundle.d.ts +0 -1303
  48. package/dist/types/index.d.ts +0 -86
  49. package/dist/types/interfaces/external-types.d.ts +0 -174
  50. package/dist/types/interfaces/face-detection.d.ts +0 -293
  51. package/dist/types/interfaces/scanner-module.d.ts +0 -280
  52. package/dist/types/modules/face/face-detector.d.ts +0 -170
  53. package/dist/types/modules/face/index.d.ts +0 -56
  54. package/dist/types/modules/face/liveness-detector.d.ts +0 -177
  55. package/dist/types/modules/face/types.d.ts +0 -136
  56. package/dist/types/modules/id-card/anti-fake-detector.d.ts +0 -170
  57. package/dist/types/modules/id-card/id-card-detector.d.ts +0 -131
  58. package/dist/types/modules/id-card/index.d.ts +0 -89
  59. package/dist/types/modules/id-card/ocr-processor.d.ts +0 -110
  60. package/dist/types/modules/id-card/ocr-worker.d.ts +0 -31
  61. package/dist/types/modules/id-card/types.d.ts +0 -181
  62. package/dist/types/modules/qrcode/index.d.ts +0 -51
  63. package/dist/types/modules/qrcode/qr-code-scanner.d.ts +0 -64
  64. package/dist/types/modules/qrcode/types.d.ts +0 -67
  65. package/dist/types/utils/camera.d.ts +0 -81
  66. package/dist/types/utils/image-processing.d.ts +0 -176
  67. package/dist/types/utils/index.d.ts +0 -175
  68. package/dist/types/utils/performance.d.ts +0 -81
  69. package/dist/types/utils/resource-manager.d.ts +0 -53
  70. package/dist/types/utils/types.d.ts +0 -166
  71. package/dist/types/utils/worker.d.ts +0 -52
  72. package/dist/types/version.d.ts +0 -7
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Type definitions for browser-image-compression
3
+ */
4
+
5
+ declare module 'browser-image-compression' {
6
+ export interface Options {
7
+ maxSizeMB?: number;
8
+ maxWidthOrHeight?: number;
9
+ useWebWorker?: boolean;
10
+ maxIteration?: number;
11
+ quality?: number;
12
+ fileType?: string;
13
+ onProgress?: (progress: number) => void;
14
+ }
15
+
16
+ function imageCompression(file: File, options?: Options): Promise<File>;
17
+
18
+ export default imageCompression;
19
+ }
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Type definitions for tesseract.js
3
+ */
4
+
5
+ declare module "tesseract.js" {
6
+ // Based on https://github.com/naptha/tesseract.js/blob/master/src/index.d.ts
7
+ // and https://github.com/naptha/tesseract.js/blob/master/docs/api.md
8
+
9
+ export interface Point {
10
+ x: number
11
+ y: number
12
+ }
13
+
14
+ export interface Bbox {
15
+ x0: number
16
+ y0: number
17
+ x1: number
18
+ y1: number
19
+ }
20
+
21
+ export interface Baseline {
22
+ x0: number
23
+ y0: number
24
+ x1: number
25
+ y1: number
26
+ has_descenders: boolean
27
+ has_ascenders: boolean
28
+ }
29
+
30
+ export interface Word {
31
+ symbols: Symbol[]
32
+ choices: Choice[]
33
+ text: string
34
+ confidence: number
35
+ baseline: Baseline
36
+ bbox: Bbox
37
+ is_numeric: boolean
38
+ in_dictionary: boolean
39
+ direction: string
40
+ language: string
41
+ is_from_dictionary: boolean
42
+ is_fuzzy: boolean
43
+ is_certain: boolean
44
+ is_bold: boolean
45
+ is_italic: boolean
46
+ is_underlined: boolean
47
+ is_monospace: boolean
48
+ is_serif: boolean
49
+ is_smallcaps: boolean
50
+ font_id: number
51
+ font_size: number
52
+ font_name: string
53
+ }
54
+
55
+ export interface Symbol {
56
+ choices: Choice[]
57
+ image: null | HTMLImageElement // Or string if it's a path/URL
58
+ text: string
59
+ confidence: number
60
+ baseline: Baseline
61
+ bbox: Bbox
62
+ is_superscript: boolean
63
+ is_subscript: boolean
64
+ is_dropcap: boolean
65
+ }
66
+
67
+ export interface Choice {
68
+ text: string
69
+ confidence: number
70
+ }
71
+
72
+ export interface Line {
73
+ words: Word[]
74
+ text: string
75
+ confidence: number
76
+ baseline: Baseline
77
+ bbox: Bbox
78
+ }
79
+
80
+ export interface Paragraph {
81
+ lines: Line[]
82
+ text: string
83
+ confidence: number
84
+ baseline: Baseline
85
+ bbox: Bbox
86
+ is_ltr: boolean
87
+ }
88
+
89
+ export interface Block {
90
+ paragraphs: Paragraph[]
91
+ lines: Line[]
92
+ words: Word[]
93
+ text: string
94
+ confidence: number
95
+ baseline: Baseline
96
+ bbox: Bbox
97
+ blocktype: string
98
+ polygon: Point[]
99
+ }
100
+
101
+ export interface Page {
102
+ blocks: Block[]
103
+ confidence: number
104
+ html: string // HTML representation of the page
105
+ jobId?: string
106
+ text: string
107
+ lines: Line[]
108
+ oem: string
109
+ operator: string
110
+ paragraphs: Paragraph[]
111
+ psm: string
112
+ symbols: Symbol[]
113
+ version: string
114
+ words: Word[]
115
+ hocr?: string // hOCR output
116
+ tsv?: string // TSV output
117
+ }
118
+
119
+ export interface LoggerMessage {
120
+ jobId?: string
121
+ workerId?: string
122
+ status: string
123
+ progress: number
124
+ userfriendlyText?: string
125
+ }
126
+
127
+ export interface WorkerOptions {
128
+ langPath?: string
129
+ corePath?: string
130
+ workerPath?: string
131
+ logger?: (message: LoggerMessage) => void // More specific type for logger message <mcreference index="1" link="https://github.com/naptha/tesseract.js/blob/master/docs/api.md"></mcreference>
132
+ errorHandler?: (error: Error) => void
133
+ // Add other options based on documentation if needed
134
+ [key: string]: any // For other less common or dynamic options
135
+ }
136
+
137
+ export interface RecognizeResult {
138
+ data: Page // Use the detailed Page interface
139
+ }
140
+
141
+ export interface DetectResult {
142
+ data: {
143
+ tesseract_script_id: number | null
144
+ script: string | null
145
+ script_confidence: number | null
146
+ orientation_degrees: number | null
147
+ orientation_confidence: number | null
148
+ }
149
+ jobId?: string
150
+ }
151
+
152
+ export interface ConfigResult {
153
+ data: null
154
+ jobId?: string
155
+ }
156
+
157
+ export type ImageLike =
158
+ | HTMLImageElement
159
+ | HTMLCanvasElement
160
+ | File
161
+ | string
162
+ | Buffer
163
+ | ImageData // Common image types
164
+
165
+ export interface Worker {
166
+ load(jobId?: string): Promise<ConfigResult> // <mcreference index="4" link="https://github.com/naptha/tesseract.js/blob/master/src/index.d.ts"></mcreference>
167
+ loadLanguage(
168
+ langs?: string | string[],
169
+ jobId?: string
170
+ ): Promise<ConfigResult> // <mcreference index="4" link="https://github.com/naptha/tesseract.js/blob/master/src/index.d.ts"></mcreference>
171
+ initialize(
172
+ langs?: string | string[],
173
+ oem?: OEM,
174
+ config?: string | Partial<InitOptions>,
175
+ jobId?: string
176
+ ): Promise<ConfigResult> // <mcreference index="4" link="https://github.com/naptha/tesseract.js/blob/master/src/index.d.ts"></mcreference>
177
+ setParameters(
178
+ params: Partial<Parameters>,
179
+ jobId?: string
180
+ ): Promise<ConfigResult> // <mcreference index="4" link="https://github.com/naptha/tesseract.js/blob/master/src/index.d.ts"></mcreference>
181
+ recognize(
182
+ image: ImageLike,
183
+ options?: Partial<RecognizeOptions>,
184
+ output?: Partial<OutputFormats>,
185
+ jobId?: string
186
+ ): Promise<RecognizeResult> // <mcreference index="4" link="https://github.com/naptha/tesseract.js/blob/master/src/index.d.ts"></mcreference>
187
+ detect(
188
+ image: ImageLike,
189
+ options?: Partial<WorkerOptions>,
190
+ jobId?: string
191
+ ): Promise<DetectResult> // <mcreference index="4" link="https://github.com/naptha/tesseract.js/blob/master/src/index.d.ts"></mcreference>
192
+ terminate(jobId?: string): Promise<ConfigResult> // <mcreference index="4" link="https://github.com/naptha/tesseract.js/blob/master/src/index.d.ts"></mcreference>
193
+ // Add other worker methods if present in the version you are targeting
194
+ // Example from docs for other methods like FS operations:
195
+ writeText?(
196
+ path: string,
197
+ text: string,
198
+ jobId?: string
199
+ ): Promise<ConfigResult>
200
+ readText?(path: string, jobId?: string): Promise<ConfigResult>
201
+ removeFile?(path: string, jobId?: string): Promise<ConfigResult> // Assuming removeFile also returns ConfigResult or similar
202
+ FS?(method: string, args: any[], jobId?: string): Promise<any> // FS is more generic
203
+ }
204
+
205
+ // Based on Tesseract's OEM and PSM enums
206
+ export enum OEM {
207
+ TESSERACT_ONLY = 0,
208
+ LSTM_ONLY = 1,
209
+ TESSERACT_LSTM_COMBINED = 2,
210
+ DEFAULT = 3,
211
+ }
212
+
213
+ export enum PSM {
214
+ OSD_ONLY = 0,
215
+ AUTO_OSD = 1,
216
+ AUTO_ONLY = 2,
217
+ AUTO = 3,
218
+ SINGLE_COLUMN = 4,
219
+ SINGLE_BLOCK_VERT_TEXT = 5,
220
+ SINGLE_BLOCK = 6,
221
+ SINGLE_LINE = 7,
222
+ SINGLE_WORD = 8,
223
+ CIRCLE_WORD = 9,
224
+ SINGLE_CHAR = 10,
225
+ SPARSE_TEXT = 11,
226
+ SPARSE_TEXT_OSD = 12,
227
+ RAW_LINE = 13,
228
+ }
229
+
230
+ export interface Parameters {
231
+ tessedit_char_whitelist?: string
232
+ tessedit_pageseg_mode?: PSM
233
+ // Add other Tesseract parameters as needed
234
+ [key: string]: any // For flexibility with other parameters
235
+ }
236
+
237
+ export interface RecognizeOptions {
238
+ rectangle?: Bbox // For recognizing a specific region
239
+ rectangles?: Bbox[] // For recognizing multiple regions
240
+ // Add other recognize specific options
241
+ [key: string]: any
242
+ }
243
+
244
+ export interface OutputFormats {
245
+ text?: boolean
246
+ blocks?: boolean
247
+ hocr?: boolean
248
+ tsv?: boolean
249
+ pdf?: boolean // If PDF output is supported
250
+ // Add other output formats
251
+ [key: string]: any
252
+ }
253
+
254
+ export interface InitOptions {
255
+ load_system_dawg?: boolean
256
+ load_freq_dawg?: boolean
257
+ load_punc_dawg?: boolean
258
+ load_number_dawg?: boolean
259
+ load_unambig_dawg?: boolean
260
+ load_bigram_dawg?: boolean
261
+ load_fixed_length_dawgs?: boolean
262
+ // Add other init-only parameters
263
+ [key: string]: any
264
+ }
265
+
266
+ export function createWorker(options?: Partial<WorkerOptions>): Worker // 修正返回类型为 Worker 而非 Promise<Worker>
267
+ export function setLogging(logging: boolean): void
268
+ export function recognize(
269
+ image: ImageLike,
270
+ langs?: string | string[],
271
+ options?: Partial<RecognizeOptions & WorkerOptions>
272
+ ): Promise<RecognizeResult>
273
+ export function detect(
274
+ image: ImageLike,
275
+ options?: Partial<WorkerOptions>
276
+ ): Promise<DetectResult>
277
+
278
+ // 正确导出 OEM 和 PSM 枚举
279
+ export { OEM, PSM }
280
+ }
package/package.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "name": "id-scanner-lib",
3
- "version": "1.5.0",
4
- "type": "module",
3
+ "version": "1.6.2",
5
4
  "description": "Browser-based ID card, QR code, and face recognition scanner with liveness detection",
6
5
  "main": "dist/id-scanner-lib.js",
7
6
  "module": "dist/id-scanner-lib.esm.js",
@@ -20,7 +19,7 @@
20
19
  },
21
20
  "scripts": {
22
21
  "build": "rimraf dist && rollup -c rollup.config.js",
23
- "dev": "rollup -c -w rollup.config.js",
22
+ "dev": "vite build --watch",
24
23
  "test": "jest",
25
24
  "lint": "eslint --ext .ts,.js src",
26
25
  "format": "prettier --write \"src/**/*.{ts,js}\"",
@@ -49,9 +48,9 @@
49
48
  "author": "Your Name",
50
49
  "license": "MIT",
51
50
  "bugs": {
52
- "url": "https://github.com/agions/id-scanner-lib/issues"
51
+ "url": "https://github.com/yourusername/id-scanner-lib/issues"
53
52
  },
54
- "homepage": "https://github.com/agions/id-scanner-lib#readme",
53
+ "homepage": "https://github.com/yourusername/id-scanner-lib#readme",
55
54
  "dependencies": {
56
55
  "@tensorflow/tfjs": "^4.16.0",
57
56
  "@vladmandic/face-api": "^1.7.13",
@@ -61,20 +60,25 @@
61
60
  "@babel/core": "^7.23.7",
62
61
  "@babel/preset-env": "^7.23.7",
63
62
  "@babel/preset-typescript": "^7.23.7",
63
+ "@eslint/js": "^10.0.1",
64
64
  "@rollup/plugin-babel": "^6.0.4",
65
65
  "@rollup/plugin-commonjs": "^25.0.7",
66
66
  "@rollup/plugin-json": "^6.0.1",
67
67
  "@rollup/plugin-node-resolve": "^15.2.3",
68
- "@rollup/plugin-terser": "^0.4.4",
68
+ "@rollup/plugin-terser": "^1.0.0",
69
69
  "@rollup/plugin-typescript": "^11.1.5",
70
+ "@testing-library/jest-dom": "^6.9.1",
70
71
  "@types/jest": "^29.5.11",
71
- "@typescript-eslint/eslint-plugin": "^6.19.1",
72
- "@typescript-eslint/parser": "^6.19.1",
72
+ "@types/node": "^20.0.0",
73
+ "@typescript-eslint/eslint-plugin": "^8.56.1",
74
+ "@typescript-eslint/parser": "^8.56.1",
73
75
  "dts-bundle-generator": "^9.2.4",
74
- "eslint": "^8.56.0",
76
+ "eslint": "^9.39.4",
75
77
  "eslint-config-prettier": "^9.1.0",
76
78
  "eslint-plugin-prettier": "^5.1.3",
79
+ "globals": "^17.4.0",
77
80
  "jest": "^29.7.0",
81
+ "jest-environment-jsdom": "^30.2.0",
78
82
  "prettier": "^3.2.4",
79
83
  "rimraf": "^5.0.5",
80
84
  "rollup": "^4.9.6",
@@ -87,10 +91,16 @@
87
91
  "ts-jest": "^29.1.1",
88
92
  "tslib": "^2.6.2",
89
93
  "typedoc": "^0.25.7",
90
- "typescript": "^5.3.3",
91
- "vitepress": "^1.0.0-rc.25"
94
+ "typescript": "^5.9.3",
95
+ "typescript-eslint": "^8.57.1",
96
+ "vite-plugin-dts": "^4.5.4",
97
+ "vitepress": "^1.6.4"
92
98
  },
93
99
  "engines": {
94
100
  "node": ">=14.0.0"
101
+ },
102
+ "overrides": {
103
+ "esbuild": "^0.27.0",
104
+ "vite": "^6.0.0"
95
105
  }
96
106
  }
@@ -671,13 +671,28 @@ export class CameraManager extends EventEmitter {
671
671
  dispose(): void {
672
672
  this.stop();
673
673
 
674
+ // 停止并释放媒体流
675
+ this.stopMediaStream();
676
+
674
677
  if (this.canvas) {
678
+ // 清空 canvas 内容
679
+ const ctx = this.canvas.getContext('2d');
680
+ if (ctx) ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
681
+ this.canvas.width = 0;
682
+ this.canvas.height = 0;
675
683
  this.canvas = null;
676
684
  this.canvasCtx = null;
677
685
  }
678
686
 
679
- this.videoElement = null;
687
+ // 释放 video 元素
688
+ if (this.videoElement) {
689
+ this.videoElement.srcObject = null;
690
+ this.videoElement.load();
691
+ this.videoElement = null;
692
+ }
693
+
680
694
  this.status = CameraStatus.NOT_INITIALIZED;
695
+ this.logger.debug('CameraManager', 'Camera resources disposed');
681
696
  }
682
697
 
683
698
  /**
@@ -254,6 +254,43 @@ export class ConfigManager {
254
254
  }
255
255
  }
256
256
  }
257
+
258
+ /**
259
+ * 检查模块是否启用
260
+ * @param moduleName 模块名称
261
+ */
262
+ public isModuleEnabled(moduleName: string): boolean {
263
+ const key = `modules.${moduleName}.enabled`;
264
+ return this.get(key) ?? false;
265
+ }
266
+ }
267
+
268
+ /**
269
+ * 全局配置接口
270
+ */
271
+ export interface GlobalConfig {
272
+ /** 版本号 */
273
+ version: string;
274
+ /** 调试模式 */
275
+ debug: boolean;
276
+ /** 日志级别 */
277
+ logLevel: string;
278
+ /** 摄像头配置 */
279
+ camera: {
280
+ resolution: { width: number; height: number };
281
+ frameRate: number;
282
+ facingMode: string;
283
+ };
284
+ /** 性能配置 */
285
+ performance: {
286
+ useCache: boolean;
287
+ };
288
+ /** 模块配置 */
289
+ modules: {
290
+ face: { enabled: boolean };
291
+ idcard: { enabled: boolean };
292
+ qrcode: { enabled: boolean };
293
+ };
257
294
  }
258
295
 
259
296
  /**
@@ -31,9 +31,9 @@ export class IDScannerError extends Error {
31
31
  // 设置错误原因
32
32
  this.cause = options?.cause;
33
33
 
34
- // 捕获堆栈
35
- if (Error.captureStackTrace) {
36
- Error.captureStackTrace(this, this.constructor);
34
+ // 捕获堆栈 (Node.js专有,浏览器环境忽略)
35
+ if (typeof (Error as any).captureStackTrace === 'function') {
36
+ (Error as any).captureStackTrace(this, this.constructor);
37
37
  }
38
38
  }
39
39
  }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @file EventEmitter 测试
3
+ * @description 测试事件发射器
4
+ */
5
+
6
+ import { EventEmitter } from './event-emitter';
7
+
8
+ describe('EventEmitter', () => {
9
+ let emitter: EventEmitter;
10
+
11
+ beforeEach(() => {
12
+ emitter = new EventEmitter();
13
+ });
14
+
15
+ afterEach(() => {
16
+ emitter.removeAllListeners();
17
+ });
18
+
19
+ it('should register and emit event', () => {
20
+ let called = false;
21
+ emitter.on('test', () => { called = true; });
22
+ emitter.emit('test');
23
+ expect(called).toBe(true);
24
+ });
25
+
26
+ it('should register one-time event', () => {
27
+ let count = 0;
28
+ emitter.once('test', () => { count++; });
29
+ emitter.emit('test');
30
+ emitter.emit('test');
31
+ expect(count).toBe(1);
32
+ });
33
+
34
+ it('should remove listener', () => {
35
+ let called = false;
36
+ const fn = () => { called = true; };
37
+ emitter.on('test', fn);
38
+ emitter.off('test', fn);
39
+ emitter.emit('test');
40
+ expect(called).toBe(false);
41
+ });
42
+ });
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @file 加载状态管理测试
3
+ * @description 测试 LoadingStateManager
4
+ */
5
+
6
+ import { LoadingStateManager, LoadingState } from './loading-state';
7
+
8
+ describe('LoadingStateManager', () => {
9
+ let manager: LoadingStateManager;
10
+
11
+ beforeEach(() => {
12
+ manager = new LoadingStateManager();
13
+ });
14
+
15
+ afterEach(() => {
16
+ manager.dispose();
17
+ });
18
+
19
+ it('should start with idle state', () => {
20
+ expect(manager.getState()).toBe(LoadingState.IDLE);
21
+ });
22
+
23
+ it('should start loading', () => {
24
+ manager.startLoading(5);
25
+
26
+ expect(manager.getState()).toBe(LoadingState.LOADING);
27
+ expect(manager.getProgress().progress).toBe(0);
28
+ });
29
+
30
+ it('should track model loading', () => {
31
+ manager.startLoading(5);
32
+ manager.startModelLoading('model1');
33
+
34
+ expect(manager.getProgress().loadingModel).toBe('model1');
35
+
36
+ manager.completeModelLoading('model1');
37
+ expect(manager.getProgress().loadedModels).toContain('model1');
38
+ });
39
+
40
+ it('should complete loading', () => {
41
+ manager.startLoading(2);
42
+ manager.completeModelLoading('model1');
43
+ manager.completeModelLoading('model2');
44
+ manager.complete();
45
+
46
+ expect(manager.getState()).toBe(LoadingState.READY);
47
+ expect(manager.isReady()).toBe(true);
48
+ });
49
+
50
+ it('should handle failure', () => {
51
+ manager.startLoading(2);
52
+ manager.fail('Network error');
53
+
54
+ expect(manager.getState()).toBe(LoadingState.ERROR);
55
+ expect(manager.getProgress().error).toBe('Network error');
56
+ expect(manager.hasError()).toBe(true);
57
+ });
58
+
59
+ it('should dispose correctly', () => {
60
+ manager.startLoading(2);
61
+ manager.complete();
62
+ manager.dispose();
63
+
64
+ expect(manager.getState()).toBe(LoadingState.DISPOSED);
65
+ expect(manager.isReady()).toBe(false);
66
+ });
67
+ });