@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.
- package/CLAUDE.md +206 -0
- package/README-zhCN.md +256 -0
- package/README.md +232 -0
- package/bin/webf.js +25 -0
- package/coverage/clover.xml +1295 -0
- package/coverage/coverage-final.json +12 -0
- package/coverage/lcov-report/IDLBlob.ts.html +142 -0
- package/coverage/lcov-report/analyzer.ts.html +2158 -0
- package/coverage/lcov-report/analyzer_original.ts.html +1450 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/commands.ts.html +700 -0
- package/coverage/lcov-report/dart.ts.html +490 -0
- package/coverage/lcov-report/declaration.ts.html +337 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/generator.ts.html +1171 -0
- package/coverage/lcov-report/index.html +266 -0
- package/coverage/lcov-report/logger.ts.html +424 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/react.ts.html +619 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +196 -0
- package/coverage/lcov-report/utils.ts.html +466 -0
- package/coverage/lcov-report/vue.ts.html +613 -0
- package/coverage/lcov.info +2149 -0
- package/global.d.ts +2 -0
- package/jest.config.js +24 -0
- package/package.json +36 -0
- package/src/IDLBlob.ts +20 -0
- package/src/analyzer.ts +692 -0
- package/src/commands.ts +645 -0
- package/src/dart.ts +170 -0
- package/src/declaration.ts +84 -0
- package/src/generator.ts +454 -0
- package/src/logger.ts +114 -0
- package/src/react.ts +186 -0
- package/src/utils.ts +127 -0
- package/src/vue.ts +176 -0
- package/templates/class.dart.tpl +86 -0
- package/templates/gitignore.tpl +2 -0
- package/templates/react.component.tsx.tpl +53 -0
- package/templates/react.createComponent.tpl +286 -0
- package/templates/react.index.ts.tpl +8 -0
- package/templates/react.package.json.tpl +26 -0
- package/templates/react.tsconfig.json.tpl +16 -0
- package/templates/react.tsup.config.ts.tpl +10 -0
- package/templates/tsconfig.json.tpl +8 -0
- package/templates/vue.component.partial.tpl +31 -0
- package/templates/vue.components.d.ts.tpl +49 -0
- package/templates/vue.package.json.tpl +11 -0
- package/templates/vue.tsconfig.json.tpl +15 -0
- package/test/IDLBlob.test.ts +75 -0
- package/test/analyzer.test.ts +370 -0
- package/test/commands.test.ts +1253 -0
- package/test/generator.test.ts +460 -0
- package/test/logger.test.ts +215 -0
- package/test/react.test.ts +49 -0
- package/test/utils.test.ts +316 -0
- 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
|
+
});
|