@openwebf/webf 0.1.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 (60) hide show
  1. package/CLAUDE.md +206 -0
  2. package/README-zhCN.md +256 -0
  3. package/README.md +232 -0
  4. package/bin/webf.js +25 -0
  5. package/coverage/clover.xml +1295 -0
  6. package/coverage/coverage-final.json +12 -0
  7. package/coverage/lcov-report/IDLBlob.ts.html +142 -0
  8. package/coverage/lcov-report/analyzer.ts.html +2158 -0
  9. package/coverage/lcov-report/analyzer_original.ts.html +1450 -0
  10. package/coverage/lcov-report/base.css +224 -0
  11. package/coverage/lcov-report/block-navigation.js +87 -0
  12. package/coverage/lcov-report/commands.ts.html +700 -0
  13. package/coverage/lcov-report/dart.ts.html +490 -0
  14. package/coverage/lcov-report/declaration.ts.html +337 -0
  15. package/coverage/lcov-report/favicon.png +0 -0
  16. package/coverage/lcov-report/generator.ts.html +1171 -0
  17. package/coverage/lcov-report/index.html +266 -0
  18. package/coverage/lcov-report/logger.ts.html +424 -0
  19. package/coverage/lcov-report/prettify.css +1 -0
  20. package/coverage/lcov-report/prettify.js +2 -0
  21. package/coverage/lcov-report/react.ts.html +619 -0
  22. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  23. package/coverage/lcov-report/sorter.js +196 -0
  24. package/coverage/lcov-report/utils.ts.html +466 -0
  25. package/coverage/lcov-report/vue.ts.html +613 -0
  26. package/coverage/lcov.info +2149 -0
  27. package/global.d.ts +2 -0
  28. package/jest.config.js +24 -0
  29. package/package.json +36 -0
  30. package/src/IDLBlob.ts +20 -0
  31. package/src/analyzer.ts +692 -0
  32. package/src/commands.ts +645 -0
  33. package/src/dart.ts +170 -0
  34. package/src/declaration.ts +84 -0
  35. package/src/generator.ts +454 -0
  36. package/src/logger.ts +114 -0
  37. package/src/react.ts +186 -0
  38. package/src/utils.ts +127 -0
  39. package/src/vue.ts +176 -0
  40. package/templates/class.dart.tpl +86 -0
  41. package/templates/gitignore.tpl +2 -0
  42. package/templates/react.component.tsx.tpl +53 -0
  43. package/templates/react.createComponent.tpl +286 -0
  44. package/templates/react.index.ts.tpl +8 -0
  45. package/templates/react.package.json.tpl +26 -0
  46. package/templates/react.tsconfig.json.tpl +16 -0
  47. package/templates/react.tsup.config.ts.tpl +10 -0
  48. package/templates/tsconfig.json.tpl +8 -0
  49. package/templates/vue.component.partial.tpl +31 -0
  50. package/templates/vue.components.d.ts.tpl +49 -0
  51. package/templates/vue.package.json.tpl +11 -0
  52. package/templates/vue.tsconfig.json.tpl +15 -0
  53. package/test/IDLBlob.test.ts +75 -0
  54. package/test/analyzer.test.ts +370 -0
  55. package/test/commands.test.ts +1253 -0
  56. package/test/generator.test.ts +460 -0
  57. package/test/logger.test.ts +215 -0
  58. package/test/react.test.ts +49 -0
  59. package/test/utils.test.ts +316 -0
  60. package/tsconfig.json +30 -0
@@ -0,0 +1,460 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { glob } from 'glob';
4
+ import { dartGen, reactGen, vueGen, clearAllCaches } from '../src/generator';
5
+ import * as analyzer from '../src/analyzer';
6
+ import * as dartGenerator from '../src/dart';
7
+ import * as reactGenerator from '../src/react';
8
+ import * as vueGenerator from '../src/vue';
9
+ import { ClassObject } from '../src/declaration';
10
+
11
+ // Mock dependencies
12
+ jest.mock('fs');
13
+ jest.mock('glob');
14
+ jest.mock('../src/analyzer');
15
+ jest.mock('../src/dart');
16
+ jest.mock('../src/react');
17
+ jest.mock('../src/vue');
18
+ jest.mock('../src/logger', () => ({
19
+ logger: {
20
+ setLogLevel: jest.fn(),
21
+ debug: jest.fn(),
22
+ info: jest.fn(),
23
+ success: jest.fn(),
24
+ warn: jest.fn(),
25
+ error: jest.fn(),
26
+ group: jest.fn(),
27
+ progress: jest.fn(),
28
+ time: jest.fn(),
29
+ timeEnd: jest.fn(),
30
+ },
31
+ debug: jest.fn(),
32
+ info: jest.fn(),
33
+ success: jest.fn(),
34
+ warn: jest.fn(),
35
+ error: jest.fn(),
36
+ group: jest.fn(),
37
+ progress: jest.fn(),
38
+ time: jest.fn(),
39
+ timeEnd: jest.fn(),
40
+ LogLevel: {
41
+ DEBUG: 0,
42
+ INFO: 1,
43
+ WARN: 2,
44
+ ERROR: 3,
45
+ SILENT: 4,
46
+ },
47
+ }));
48
+
49
+ const mockFs = fs as jest.Mocked<typeof fs>;
50
+ const mockGlob = glob as jest.Mocked<typeof glob>;
51
+ const mockAnalyzer = analyzer as jest.Mocked<typeof analyzer>;
52
+ const mockDartGenerator = dartGenerator as jest.Mocked<typeof dartGenerator>;
53
+ const mockReactGenerator = reactGenerator as jest.Mocked<typeof reactGenerator>;
54
+ const mockVueGenerator = vueGenerator as jest.Mocked<typeof vueGenerator>;
55
+
56
+ describe('Generator', () => {
57
+ beforeEach(() => {
58
+ jest.clearAllMocks();
59
+ clearAllCaches();
60
+ ClassObject.globalClassMap = Object.create(null);
61
+
62
+ // Setup default mocks
63
+ mockFs.existsSync.mockReturnValue(true);
64
+ mockFs.readFileSync.mockReturnValue('test content');
65
+ mockFs.writeFileSync.mockImplementation(() => undefined);
66
+ mockFs.mkdirSync.mockImplementation(() => undefined);
67
+
68
+ mockGlob.globSync.mockReturnValue(['test.d.ts', 'component.d.ts']);
69
+
70
+ mockAnalyzer.analyzer.mockImplementation(() => undefined);
71
+ mockAnalyzer.clearCaches.mockImplementation(() => undefined);
72
+
73
+ mockDartGenerator.generateDartClass.mockReturnValue('dart code');
74
+ mockReactGenerator.generateReactComponent.mockReturnValue('react component');
75
+ mockReactGenerator.generateReactIndex.mockReturnValue('export * from "./test"');
76
+ mockVueGenerator.generateVueTypings.mockReturnValue('vue typings');
77
+ });
78
+
79
+ describe('dartGen', () => {
80
+ it('should generate Dart code for type files', async () => {
81
+ await dartGen({
82
+ source: '/test/source',
83
+ target: '/test/target',
84
+ command: 'test command'
85
+ });
86
+
87
+ expect(mockGlob.globSync).toHaveBeenCalledWith('**/*.d.ts', {
88
+ cwd: '/test/source',
89
+ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**']
90
+ });
91
+
92
+ expect(mockAnalyzer.analyzer).toHaveBeenCalledTimes(2); // For each file
93
+ expect(mockDartGenerator.generateDartClass).toHaveBeenCalledTimes(2);
94
+ expect(mockFs.writeFileSync).toHaveBeenCalled();
95
+ });
96
+
97
+ it('should handle absolute and relative paths', async () => {
98
+ await dartGen({
99
+ source: './relative/source',
100
+ target: './relative/target',
101
+ command: 'test command'
102
+ });
103
+
104
+ expect(mockGlob.globSync).toHaveBeenCalledWith('**/*.d.ts', {
105
+ cwd: expect.stringContaining('relative/source'),
106
+ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**']
107
+ });
108
+ });
109
+
110
+ it('should filter out global.d.ts files', async () => {
111
+ mockGlob.globSync.mockReturnValue(['test.d.ts', 'global.d.ts', 'component.d.ts']);
112
+
113
+ await dartGen({
114
+ source: '/test/source',
115
+ target: '/test/target',
116
+ command: 'test command'
117
+ });
118
+
119
+ // Should only process 2 files (excluding global.d.ts)
120
+ expect(mockAnalyzer.analyzer).toHaveBeenCalledTimes(2);
121
+ expect(mockDartGenerator.generateDartClass).toHaveBeenCalledTimes(2);
122
+ });
123
+
124
+ it('should handle empty type files', async () => {
125
+ mockGlob.globSync.mockReturnValue([]);
126
+
127
+ await dartGen({
128
+ source: '/test/source',
129
+ target: '/test/target',
130
+ command: 'test command'
131
+ });
132
+
133
+ expect(mockAnalyzer.analyzer).not.toHaveBeenCalled();
134
+ expect(mockDartGenerator.generateDartClass).not.toHaveBeenCalled();
135
+ });
136
+
137
+ it('should continue processing if one file fails', async () => {
138
+ mockGlob.globSync.mockReturnValue(['test1.d.ts', 'test2.d.ts']);
139
+ mockAnalyzer.analyzer
140
+ .mockImplementationOnce(() => { throw new Error('Parse error'); })
141
+ .mockImplementationOnce(() => undefined);
142
+
143
+ await dartGen({
144
+ source: '/test/source',
145
+ target: '/test/target',
146
+ command: 'test command'
147
+ });
148
+
149
+ expect(mockDartGenerator.generateDartClass).toHaveBeenCalledTimes(2);
150
+ });
151
+
152
+ it('should validate source path exists', async () => {
153
+ mockFs.existsSync.mockImplementation((path) =>
154
+ !path.toString().includes('/test/source')
155
+ );
156
+
157
+ await expect(dartGen({
158
+ source: '/test/source',
159
+ target: '/test/target',
160
+ command: 'test command'
161
+ })).rejects.toThrow('Source path does not exist');
162
+ });
163
+
164
+ it('should cache file content', async () => {
165
+ const firstRunReadCount = mockFs.readFileSync.mock.calls.length;
166
+
167
+ await dartGen({
168
+ source: '/test/source',
169
+ target: '/test/target',
170
+ command: 'test command'
171
+ });
172
+
173
+ const afterFirstRunReadCount = mockFs.readFileSync.mock.calls.length;
174
+ const firstRunReads = afterFirstRunReadCount - firstRunReadCount;
175
+
176
+ // Run again
177
+ await dartGen({
178
+ source: '/test/source',
179
+ target: '/test/target',
180
+ command: 'test command'
181
+ });
182
+
183
+ const afterSecondRunReadCount = mockFs.readFileSync.mock.calls.length;
184
+ const secondRunReads = afterSecondRunReadCount - afterFirstRunReadCount;
185
+
186
+ // Second run should read less due to caching
187
+ expect(secondRunReads).toBeLessThan(firstRunReads);
188
+ });
189
+
190
+ it('should only write changed files', async () => {
191
+ // First run
192
+ await dartGen({
193
+ source: '/test/source',
194
+ target: '/test/target',
195
+ command: 'test command'
196
+ });
197
+
198
+ mockFs.writeFileSync.mockClear();
199
+
200
+ // Second run with same content
201
+ mockFs.existsSync.mockReturnValue(true);
202
+ mockFs.readFileSync.mockImplementation((path) => {
203
+ if (path.toString().endsWith('_bindings_generated.dart')) {
204
+ return 'dart code'; // Same as generated
205
+ }
206
+ return 'test content';
207
+ });
208
+
209
+ await dartGen({
210
+ source: '/test/source',
211
+ target: '/test/target',
212
+ command: 'test command'
213
+ });
214
+
215
+ expect(mockFs.writeFileSync).not.toHaveBeenCalled();
216
+ });
217
+
218
+ it('should generate index.d.ts with references and exports', async () => {
219
+ mockGlob.globSync.mockReturnValue(['components/button.d.ts', 'widgets/card.d.ts']);
220
+ mockFs.readFileSync.mockReturnValue('interface Test {}');
221
+ mockFs.existsSync.mockImplementation((path) => {
222
+ // Source directory exists
223
+ if (path.toString() === '/test/source') return true;
224
+ return false;
225
+ });
226
+ mockDartGenerator.generateDartClass.mockReturnValue('generated dart code');
227
+
228
+ await dartGen({
229
+ source: '/test/source',
230
+ target: '/test/target',
231
+ command: 'test command'
232
+ });
233
+
234
+ // Check that index.d.ts was written
235
+ const writeFileCalls = mockFs.writeFileSync.mock.calls;
236
+ const indexDtsCall = writeFileCalls.find(call =>
237
+ call[0].toString().endsWith('index.d.ts')
238
+ );
239
+
240
+ expect(indexDtsCall).toBeDefined();
241
+ expect(indexDtsCall![1]).toContain('/// <reference path="./global.d.ts" />');
242
+ expect(indexDtsCall![1]).toContain('/// <reference path="./components/button.d.ts" />');
243
+ expect(indexDtsCall![1]).toContain('/// <reference path="./widgets/card.d.ts" />');
244
+ expect(indexDtsCall![1]).toContain("export * from './components/button';");
245
+ expect(indexDtsCall![1]).toContain("export * from './widgets/card';");
246
+ expect(indexDtsCall![1]).toContain('TypeScript Definitions');
247
+ });
248
+
249
+ it('should copy original .d.ts files to output directory', async () => {
250
+ mockGlob.globSync.mockReturnValue(['test.d.ts']);
251
+ const originalContent = 'interface Original {}';
252
+ mockFs.readFileSync.mockReturnValue(originalContent);
253
+ mockFs.existsSync.mockImplementation((path) => {
254
+ // Source directory exists
255
+ if (path.toString() === '/test/source') return true;
256
+ return false;
257
+ });
258
+ mockDartGenerator.generateDartClass.mockReturnValue('generated dart code');
259
+
260
+ await dartGen({
261
+ source: '/test/source',
262
+ target: '/test/target',
263
+ command: 'test command'
264
+ });
265
+
266
+ // Check that .d.ts file was copied
267
+ expect(mockFs.writeFileSync).toHaveBeenCalledWith(
268
+ expect.stringContaining('test.d.ts'),
269
+ originalContent,
270
+ 'utf-8'
271
+ );
272
+ });
273
+ });
274
+
275
+ describe('reactGen', () => {
276
+ it('should generate React components', async () => {
277
+ await reactGen({
278
+ source: '/test/source',
279
+ target: '/test/target',
280
+ command: 'test command'
281
+ });
282
+
283
+ expect(mockReactGenerator.generateReactComponent).toHaveBeenCalledTimes(2);
284
+ expect(mockReactGenerator.generateReactIndex).toHaveBeenCalled();
285
+ expect(mockFs.writeFileSync).toHaveBeenCalled();
286
+ });
287
+
288
+ it('should use the exact target directory specified', async () => {
289
+ await reactGen({
290
+ source: '/test/source',
291
+ target: 'MyReactComponents',
292
+ command: 'test command'
293
+ });
294
+
295
+ const writeCalls = mockFs.writeFileSync.mock.calls;
296
+ const componentPath = writeCalls.find(call =>
297
+ call[0].toString().includes('.tsx')
298
+ );
299
+ expect(componentPath?.[0]).toContain('MyReactComponents');
300
+ });
301
+
302
+ it('should create src directory if it does not exist', async () => {
303
+ mockFs.existsSync.mockImplementation((path) =>
304
+ !path.toString().includes('/src')
305
+ );
306
+
307
+ await reactGen({
308
+ source: '/test/source',
309
+ target: '/test/target',
310
+ command: 'test command'
311
+ });
312
+
313
+ expect(mockFs.mkdirSync).toHaveBeenCalledWith(
314
+ expect.stringContaining('/src'),
315
+ { recursive: true }
316
+ );
317
+ });
318
+
319
+ it('should generate index file', async () => {
320
+ await reactGen({
321
+ source: '/test/source',
322
+ target: '/test/target',
323
+ command: 'test command'
324
+ });
325
+
326
+ expect(mockReactGenerator.generateReactIndex).toHaveBeenCalledWith(
327
+ expect.arrayContaining([
328
+ expect.objectContaining({ filename: 'test' }),
329
+ expect.objectContaining({ filename: 'component' })
330
+ ])
331
+ );
332
+
333
+ expect(mockFs.writeFileSync).toHaveBeenCalledWith(
334
+ expect.stringContaining('index.ts'),
335
+ 'export * from "./test"',
336
+ 'utf-8'
337
+ );
338
+ });
339
+ });
340
+
341
+ describe('vueGen', () => {
342
+ it('should generate Vue typings', async () => {
343
+ await vueGen({
344
+ source: '/test/source',
345
+ target: '/test/target',
346
+ command: 'test command'
347
+ });
348
+
349
+ expect(mockVueGenerator.generateVueTypings).toHaveBeenCalled();
350
+ expect(mockFs.writeFileSync).toHaveBeenCalledWith(
351
+ expect.stringContaining('index.d.ts'),
352
+ 'vue typings',
353
+ 'utf-8'
354
+ );
355
+ });
356
+
357
+ it('should use the exact target directory specified', async () => {
358
+ await vueGen({
359
+ source: '/test/source',
360
+ target: 'MyVueComponents',
361
+ command: 'test command'
362
+ });
363
+
364
+ const writeCalls = mockFs.writeFileSync.mock.calls;
365
+ const typingsPath = writeCalls.find(call =>
366
+ call[0].toString().includes('index.d.ts')
367
+ );
368
+ expect(typingsPath?.[0]).toContain('MyVueComponents');
369
+ });
370
+
371
+ it('should only generate one index.d.ts file', async () => {
372
+ mockGlob.globSync.mockReturnValue(['comp1.d.ts', 'comp2.d.ts', 'comp3.d.ts']);
373
+
374
+ await vueGen({
375
+ source: '/test/source',
376
+ target: '/test/target',
377
+ command: 'test command'
378
+ });
379
+
380
+ const writeCalls = mockFs.writeFileSync.mock.calls.filter(call =>
381
+ call[0].toString().includes('index.d.ts')
382
+ );
383
+ expect(writeCalls).toHaveLength(1);
384
+ });
385
+ });
386
+
387
+ describe('Error handling', () => {
388
+ it('should handle glob errors', async () => {
389
+ mockGlob.globSync.mockImplementation(() => {
390
+ throw new Error('Glob error');
391
+ });
392
+
393
+ await expect(dartGen({
394
+ source: '/test/source',
395
+ target: '/test/target',
396
+ command: 'test command'
397
+ })).rejects.toThrow('Failed to scan type files');
398
+ });
399
+
400
+ it('should handle file read errors', async () => {
401
+ mockFs.readFileSync.mockImplementation((path) => {
402
+ if (path.toString().endsWith('.d.ts')) {
403
+ throw new Error('Read error');
404
+ }
405
+ return 'content';
406
+ });
407
+
408
+ await expect(dartGen({
409
+ source: '/test/source',
410
+ target: '/test/target',
411
+ command: 'test command'
412
+ })).rejects.toThrow('Read error');
413
+ });
414
+
415
+ it('should handle generator errors gracefully', async () => {
416
+ mockDartGenerator.generateDartClass
417
+ .mockImplementationOnce(() => { throw new Error('Generate error'); })
418
+ .mockImplementationOnce(() => 'dart code');
419
+
420
+ await dartGen({
421
+ source: '/test/source',
422
+ target: '/test/target',
423
+ command: 'test command'
424
+ });
425
+
426
+ // First file fails (no writes), second file succeeds (1 dart + 1 .d.ts), plus index.d.ts = 3 total
427
+ // But since the error happens in dartGen, the .d.ts copy might not happen
428
+ const writeCalls = mockFs.writeFileSync.mock.calls;
429
+ // Should have at least written the successful dart file and index.d.ts
430
+ expect(writeCalls.length).toBeGreaterThanOrEqual(2);
431
+ });
432
+ });
433
+
434
+ describe('clearAllCaches', () => {
435
+ it('should clear all caches', async () => {
436
+ // First run to populate caches
437
+ await dartGen({
438
+ source: '/test/source',
439
+ target: '/test/target',
440
+ command: 'test command'
441
+ });
442
+
443
+ mockFs.readFileSync.mockClear();
444
+
445
+ // Clear caches
446
+ clearAllCaches();
447
+
448
+ // Run again
449
+ await dartGen({
450
+ source: '/test/source',
451
+ target: '/test/target',
452
+ command: 'test command'
453
+ });
454
+
455
+ // Should read files again
456
+ expect(mockFs.readFileSync).toHaveBeenCalled();
457
+ expect(mockAnalyzer.clearCaches).toHaveBeenCalled();
458
+ });
459
+ });
460
+ });
@@ -0,0 +1,215 @@
1
+ import { logger, LogLevel } from '../src/logger';
2
+
3
+ describe('Logger', () => {
4
+ let consoleLogSpy: jest.SpyInstance;
5
+ let consoleWarnSpy: jest.SpyInstance;
6
+ let consoleErrorSpy: jest.SpyInstance;
7
+ let consoleTimeSpy: jest.SpyInstance;
8
+ let consoleTimeEndSpy: jest.SpyInstance;
9
+ let stdoutWriteSpy: jest.SpyInstance;
10
+
11
+ beforeEach(() => {
12
+ // Reset logger to default state
13
+ logger.setLogLevel(LogLevel.INFO);
14
+
15
+ // Mock console methods
16
+ consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
17
+ consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
18
+ consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
19
+ consoleTimeSpy = jest.spyOn(console, 'time').mockImplementation();
20
+ consoleTimeEndSpy = jest.spyOn(console, 'timeEnd').mockImplementation();
21
+ stdoutWriteSpy = jest.spyOn(process.stdout, 'write').mockImplementation();
22
+ });
23
+
24
+ afterEach(() => {
25
+ jest.restoreAllMocks();
26
+ delete process.env.WEBF_DEBUG;
27
+ delete process.env.DEBUG;
28
+ });
29
+
30
+ describe('Log levels', () => {
31
+ it('should respect log level for debug messages', () => {
32
+ logger.setLogLevel(LogLevel.DEBUG);
33
+ logger.debug('Debug message');
34
+ expect(consoleLogSpy).toHaveBeenCalledWith('[DEBUG] Debug message');
35
+
36
+ consoleLogSpy.mockClear();
37
+ logger.setLogLevel(LogLevel.INFO);
38
+ logger.debug('Debug message');
39
+ expect(consoleLogSpy).not.toHaveBeenCalled();
40
+ });
41
+
42
+ it('should respect log level for info messages', () => {
43
+ logger.setLogLevel(LogLevel.INFO);
44
+ logger.info('Info message');
45
+ expect(consoleLogSpy).toHaveBeenCalledWith('ℹ Info message');
46
+
47
+ consoleLogSpy.mockClear();
48
+ logger.setLogLevel(LogLevel.WARN);
49
+ logger.info('Info message');
50
+ expect(consoleLogSpy).not.toHaveBeenCalled();
51
+ });
52
+
53
+ it('should respect log level for warn messages', () => {
54
+ logger.setLogLevel(LogLevel.WARN);
55
+ logger.warn('Warning message');
56
+ expect(consoleWarnSpy).toHaveBeenCalledWith('⚠ Warning message');
57
+
58
+ consoleWarnSpy.mockClear();
59
+ logger.setLogLevel(LogLevel.ERROR);
60
+ logger.warn('Warning message');
61
+ expect(consoleWarnSpy).not.toHaveBeenCalled();
62
+ });
63
+
64
+ it('should respect log level for error messages', () => {
65
+ logger.setLogLevel(LogLevel.ERROR);
66
+ logger.error('Error message');
67
+ expect(consoleErrorSpy).toHaveBeenCalledWith('✗ Error message');
68
+
69
+ consoleErrorSpy.mockClear();
70
+ logger.setLogLevel(LogLevel.SILENT);
71
+ logger.error('Error message');
72
+ expect(consoleErrorSpy).not.toHaveBeenCalled();
73
+ });
74
+ });
75
+
76
+ describe('Debug mode', () => {
77
+ it('should enable debug level when WEBF_DEBUG is true', () => {
78
+ // Since logger is a singleton, we can't test env var changes
79
+ // Just verify that the functionality exists
80
+ process.env.WEBF_DEBUG = 'true';
81
+ logger.setLogLevel(LogLevel.DEBUG);
82
+
83
+ logger.debug('Debug message');
84
+ expect(consoleLogSpy).toHaveBeenCalledWith('[DEBUG] Debug message');
85
+ });
86
+
87
+ it('should enable debug level when DEBUG is true', () => {
88
+ process.env.DEBUG = 'true';
89
+ logger.setLogLevel(LogLevel.DEBUG);
90
+
91
+ logger.debug('Debug message');
92
+ expect(consoleLogSpy).toHaveBeenCalledWith('[DEBUG] Debug message');
93
+ });
94
+ });
95
+
96
+ describe('Message formatting', () => {
97
+ it('should pass additional arguments to console methods', () => {
98
+ logger.info('Message', 'arg1', { key: 'value' });
99
+ expect(consoleLogSpy).toHaveBeenCalledWith('ℹ Message', 'arg1', { key: 'value' });
100
+ });
101
+
102
+ it('should format success messages', () => {
103
+ logger.success('Operation completed');
104
+ expect(consoleLogSpy).toHaveBeenCalledWith('✓ Operation completed');
105
+ });
106
+
107
+ it('should format group headers', () => {
108
+ logger.group('Test Section');
109
+ expect(consoleLogSpy).toHaveBeenCalledWith('\nTest Section');
110
+ expect(consoleLogSpy).toHaveBeenCalledWith('─'.repeat('Test Section'.length));
111
+ });
112
+ });
113
+
114
+ describe('Error handling', () => {
115
+ it('should log error object with stack trace', () => {
116
+ const error = new Error('Test error');
117
+ error.stack = 'Error: Test error\n at test.js:10';
118
+
119
+ logger.error('Operation failed', error);
120
+ expect(consoleErrorSpy).toHaveBeenCalledWith('✗ Operation failed');
121
+ expect(consoleErrorSpy).toHaveBeenCalledWith(error.stack);
122
+ });
123
+
124
+ it('should log error message if no stack trace', () => {
125
+ const error = new Error('Test error');
126
+ delete error.stack;
127
+
128
+ logger.error('Operation failed', error);
129
+ expect(consoleErrorSpy).toHaveBeenCalledWith('✗ Operation failed');
130
+ expect(consoleErrorSpy).toHaveBeenCalledWith('Test error');
131
+ });
132
+
133
+ it('should handle non-Error objects', () => {
134
+ logger.error('Operation failed', 'String error');
135
+ expect(consoleErrorSpy).toHaveBeenCalledWith('✗ Operation failed');
136
+ expect(consoleErrorSpy).toHaveBeenCalledWith('String error');
137
+ });
138
+
139
+ it('should handle undefined error', () => {
140
+ logger.error('Operation failed');
141
+ expect(consoleErrorSpy).toHaveBeenCalledWith('✗ Operation failed');
142
+ expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
143
+ });
144
+ });
145
+
146
+ describe('Progress bar', () => {
147
+ it('should display progress bar', () => {
148
+ logger.progress(5, 10, 'Processing');
149
+ expect(stdoutWriteSpy).toHaveBeenCalledWith(
150
+ expect.stringContaining('[██████████░░░░░░░░░░] 50% - Processing')
151
+ );
152
+ });
153
+
154
+ it('should add newline when complete', () => {
155
+ logger.progress(10, 10, 'Complete');
156
+ expect(stdoutWriteSpy).toHaveBeenCalledWith(
157
+ expect.stringContaining('\n')
158
+ );
159
+ });
160
+
161
+ it('should handle zero total', () => {
162
+ logger.progress(0, 0, 'Empty');
163
+ // When total is 0, percentage will be NaN, so just check it was called
164
+ expect(stdoutWriteSpy).toHaveBeenCalled();
165
+ });
166
+
167
+ it('should calculate percentage correctly', () => {
168
+ logger.progress(3, 4, 'Three quarters');
169
+ expect(stdoutWriteSpy).toHaveBeenCalledWith(
170
+ expect.stringContaining('75%')
171
+ );
172
+ });
173
+
174
+ it('should respect log level', () => {
175
+ logger.setLogLevel(LogLevel.WARN);
176
+ logger.progress(5, 10, 'Processing');
177
+ expect(stdoutWriteSpy).not.toHaveBeenCalled();
178
+ });
179
+ });
180
+
181
+ describe('Timing', () => {
182
+ it('should start timer with debug level', () => {
183
+ logger.setLogLevel(LogLevel.DEBUG);
184
+ logger.time('operation');
185
+ expect(consoleTimeSpy).toHaveBeenCalledWith('[TIMER] operation');
186
+ });
187
+
188
+ it('should end timer with debug level', () => {
189
+ logger.setLogLevel(LogLevel.DEBUG);
190
+ logger.timeEnd('operation');
191
+ expect(consoleTimeEndSpy).toHaveBeenCalledWith('[TIMER] operation');
192
+ });
193
+
194
+ it('should not time with higher log levels', () => {
195
+ logger.setLogLevel(LogLevel.INFO);
196
+ logger.time('operation');
197
+ logger.timeEnd('operation');
198
+ expect(consoleTimeSpy).not.toHaveBeenCalled();
199
+ expect(consoleTimeEndSpy).not.toHaveBeenCalled();
200
+ });
201
+ });
202
+
203
+ describe('Exported convenience functions', () => {
204
+ it('should export bound functions', () => {
205
+ const { debug, info, success, warn, error, group, progress, time, timeEnd } = require('../src/logger');
206
+
207
+ // Test that they work correctly
208
+ info('Test info');
209
+ expect(consoleLogSpy).toHaveBeenCalledWith('ℹ Test info');
210
+
211
+ warn('Test warn');
212
+ expect(consoleWarnSpy).toHaveBeenCalledWith('⚠ Test warn');
213
+ });
214
+ });
215
+ });