fragment-ts 1.0.18 → 1.0.19
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/changes/1.md +113 -64
- package/dist/cli/commands/init.command.js +1 -1
- package/dist/testing/runner.d.ts +10 -0
- package/dist/testing/runner.d.ts.map +1 -1
- package/dist/testing/runner.js +109 -16
- package/dist/testing/runner.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/init.command.ts +1 -1
- package/src/testing/runner.ts +137 -24
package/changes/1.md
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## Issues Fixed
|
|
4
4
|
|
|
5
|
-
### 1.
|
|
5
|
+
### 1. Test Command Created
|
|
6
|
+
|
|
6
7
|
**Problem:** No test command existed.
|
|
7
8
|
|
|
8
9
|
**Solution:** Created `TestCommand` class with full testing support:
|
|
10
|
+
|
|
9
11
|
- Runs `.spec.ts` files
|
|
10
12
|
- Supports watch mode
|
|
11
13
|
- Auto-detects dev vs prod environment
|
|
@@ -13,6 +15,7 @@
|
|
|
13
15
|
- Shows detailed test results with pass/fail counts
|
|
14
16
|
|
|
15
17
|
**Usage:**
|
|
18
|
+
|
|
16
19
|
```bash
|
|
17
20
|
fragment test # Run all tests
|
|
18
21
|
fragment test --watch # Watch mode
|
|
@@ -21,74 +24,84 @@ fragment test --pattern "**/*.spec.ts" # Custom pattern
|
|
|
21
24
|
|
|
22
25
|
---
|
|
23
26
|
|
|
24
|
-
### 2.
|
|
27
|
+
### 2. @Autowired Decorator Fixed
|
|
28
|
+
|
|
25
29
|
**Problem:** @Autowired wasn't injecting dependencies properly.
|
|
26
30
|
|
|
27
31
|
**Root Causes:**
|
|
32
|
+
|
|
28
33
|
- Metadata wasn't being stored correctly on the prototype
|
|
29
34
|
- DI container wasn't checking the prototype chain for metadata
|
|
30
35
|
- Properties were being checked on instance instead of prototype
|
|
31
36
|
|
|
32
37
|
**Solution:**
|
|
38
|
+
|
|
33
39
|
- Fixed `Autowired()` decorator to store metadata on prototype
|
|
34
40
|
- Updated DI container to traverse prototype chain with `getAllPropertyKeys()`
|
|
35
41
|
- Added proper type checking from TypeScript's `design:type` metadata
|
|
36
42
|
- Improved error messages for missing emitDecoratorMetadata
|
|
37
43
|
|
|
38
44
|
**Now Works:**
|
|
45
|
+
|
|
39
46
|
```typescript
|
|
40
47
|
@Service()
|
|
41
48
|
export class UserService {
|
|
42
49
|
@Autowired()
|
|
43
|
-
private userRepository!: UserRepository;
|
|
44
|
-
|
|
50
|
+
private userRepository!: UserRepository; // Auto-injected
|
|
51
|
+
|
|
45
52
|
@Autowired()
|
|
46
|
-
private emailService!: EmailService;
|
|
53
|
+
private emailService!: EmailService; // Auto-injected
|
|
47
54
|
}
|
|
48
55
|
```
|
|
49
56
|
|
|
50
57
|
---
|
|
51
58
|
|
|
52
|
-
### 3.
|
|
59
|
+
### 3. Repository TypeORM Integration Fixed
|
|
60
|
+
|
|
53
61
|
**Problem:** Repositories couldn't access TypeORM repositories automatically.
|
|
54
62
|
|
|
55
63
|
**Solution:** Created `@InjectRepository(Entity)` decorator:
|
|
64
|
+
|
|
56
65
|
- New metadata key: `INJECT_REPOSITORY`
|
|
57
66
|
- DI container resolves TypeORM repositories via `TypeORMModule.getDataSource()`
|
|
58
67
|
- Automatic entity-to-repository mapping
|
|
59
68
|
|
|
60
69
|
**Now Works:**
|
|
70
|
+
|
|
61
71
|
```typescript
|
|
62
72
|
@Repository()
|
|
63
73
|
export class UserRepository {
|
|
64
74
|
@InjectRepository(User)
|
|
65
|
-
private repo!: Repository<User>;
|
|
66
|
-
|
|
75
|
+
private repo!: Repository<User>; // TypeORM repo injected
|
|
76
|
+
|
|
67
77
|
async findAll() {
|
|
68
|
-
return this.repo.find();
|
|
78
|
+
return this.repo.find(); // Direct TypeORM access
|
|
69
79
|
}
|
|
70
80
|
}
|
|
71
81
|
```
|
|
72
82
|
|
|
73
83
|
---
|
|
74
84
|
|
|
75
|
-
### 4.
|
|
85
|
+
### 4. All Injectable Decorators Working
|
|
86
|
+
|
|
76
87
|
**Previously Working:**
|
|
77
|
-
|
|
78
|
-
-
|
|
79
|
-
-
|
|
88
|
+
|
|
89
|
+
- @Controller
|
|
90
|
+
- @Service
|
|
91
|
+
- HTTP decorators (@Get, @Post, etc.)
|
|
80
92
|
|
|
81
93
|
**Now Also Working:**
|
|
82
|
-
|
|
83
|
-
-
|
|
84
|
-
-
|
|
85
|
-
-
|
|
86
|
-
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
-
|
|
94
|
+
|
|
95
|
+
- @Injectable - General purpose components
|
|
96
|
+
- @Repository - Data access with TypeORM integration
|
|
97
|
+
- @Autowired - Property injection by type
|
|
98
|
+
- @InjectRepository - TypeORM repository injection
|
|
99
|
+
- @Inject - Token-based injection
|
|
100
|
+
- @Value - Environment variable injection
|
|
101
|
+
- @Qualifier - Bean qualifier
|
|
102
|
+
- @PostConstruct - Lifecycle hook
|
|
103
|
+
- @PreDestroy - Cleanup hook
|
|
104
|
+
- @AutoConfiguration - Conditional beans
|
|
92
105
|
|
|
93
106
|
---
|
|
94
107
|
|
|
@@ -97,12 +110,14 @@ export class UserRepository {
|
|
|
97
110
|
### DI Container Enhancements
|
|
98
111
|
|
|
99
112
|
1. **Circular Dependency Detection**
|
|
113
|
+
|
|
100
114
|
```typescript
|
|
101
115
|
private constructing: Set<any> = new Set();
|
|
102
116
|
// Detects and throws error for circular deps
|
|
103
117
|
```
|
|
104
118
|
|
|
105
119
|
2. **Prototype Chain Traversal**
|
|
120
|
+
|
|
106
121
|
```typescript
|
|
107
122
|
private getAllPropertyKeys(obj: any): string[] {
|
|
108
123
|
// Walks entire prototype chain to find all decorated properties
|
|
@@ -110,6 +125,7 @@ export class UserRepository {
|
|
|
110
125
|
```
|
|
111
126
|
|
|
112
127
|
3. **TypeORM Repository Resolution**
|
|
128
|
+
|
|
113
129
|
```typescript
|
|
114
130
|
private resolveRepository(entity: any): any {
|
|
115
131
|
const dataSource = TypeORMModule.getDataSource();
|
|
@@ -124,7 +140,7 @@ export class UserRepository {
|
|
|
124
140
|
|
|
125
141
|
5. **Post-Construction Hooks**
|
|
126
142
|
```typescript
|
|
127
|
-
if (typeof instance.onInit ===
|
|
143
|
+
if (typeof instance.onInit === "function") {
|
|
128
144
|
instance.onInit();
|
|
129
145
|
}
|
|
130
146
|
```
|
|
@@ -134,12 +150,14 @@ export class UserRepository {
|
|
|
134
150
|
## New Decorators Added
|
|
135
151
|
|
|
136
152
|
### @InjectRepository
|
|
153
|
+
|
|
137
154
|
```typescript
|
|
138
155
|
@InjectRepository(User)
|
|
139
156
|
private userRepo!: Repository<User>;
|
|
140
157
|
```
|
|
141
158
|
|
|
142
159
|
### @Optional
|
|
160
|
+
|
|
143
161
|
```typescript
|
|
144
162
|
@Optional()
|
|
145
163
|
@Autowired()
|
|
@@ -147,6 +165,7 @@ private cache?: CacheService; // Won't throw if not found
|
|
|
147
165
|
```
|
|
148
166
|
|
|
149
167
|
### @Lazy
|
|
168
|
+
|
|
150
169
|
```typescript
|
|
151
170
|
@Lazy()
|
|
152
171
|
@Autowired()
|
|
@@ -154,6 +173,7 @@ private heavyService!: HeavyService; // Created on first access
|
|
|
154
173
|
```
|
|
155
174
|
|
|
156
175
|
### @PostConstruct
|
|
176
|
+
|
|
157
177
|
```typescript
|
|
158
178
|
@PostConstruct()
|
|
159
179
|
init() {
|
|
@@ -162,6 +182,7 @@ init() {
|
|
|
162
182
|
```
|
|
163
183
|
|
|
164
184
|
### @PreDestroy
|
|
185
|
+
|
|
165
186
|
```typescript
|
|
166
187
|
@PreDestroy()
|
|
167
188
|
cleanup() {
|
|
@@ -174,21 +195,23 @@ cleanup() {
|
|
|
174
195
|
## Testing Infrastructure
|
|
175
196
|
|
|
176
197
|
### Test Runner Features
|
|
177
|
-
|
|
178
|
-
-
|
|
179
|
-
-
|
|
180
|
-
-
|
|
181
|
-
-
|
|
198
|
+
|
|
199
|
+
- Automatic test file discovery
|
|
200
|
+
- TypeScript support (ts-node)
|
|
201
|
+
- Watch mode
|
|
202
|
+
- Detailed error reporting
|
|
203
|
+
- Test isolation (fresh DI container per test)
|
|
182
204
|
|
|
183
205
|
### Test API
|
|
206
|
+
|
|
184
207
|
```typescript
|
|
185
208
|
describe('UserService', () => {
|
|
186
209
|
let service: UserService;
|
|
187
|
-
|
|
210
|
+
|
|
188
211
|
beforeEach(() => {
|
|
189
212
|
// Setup
|
|
190
213
|
});
|
|
191
|
-
|
|
214
|
+
|
|
192
215
|
it('should create user', async () => {
|
|
193
216
|
const user = await service.createUser({...});
|
|
194
217
|
expect(user.name).toBe('Test');
|
|
@@ -197,6 +220,7 @@ describe('UserService', () => {
|
|
|
197
220
|
```
|
|
198
221
|
|
|
199
222
|
### Assertions
|
|
223
|
+
|
|
200
224
|
- `toBe()` - Strict equality
|
|
201
225
|
- `toEqual()` - Deep equality
|
|
202
226
|
- `toBeTruthy()` / `toBeFalsy()`
|
|
@@ -212,19 +236,26 @@ describe('UserService', () => {
|
|
|
212
236
|
## Complete Example
|
|
213
237
|
|
|
214
238
|
```typescript
|
|
215
|
-
import
|
|
216
|
-
import {
|
|
217
|
-
Controller,
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
239
|
+
import "reflect-metadata";
|
|
240
|
+
import {
|
|
241
|
+
Controller,
|
|
242
|
+
Service,
|
|
243
|
+
Repository,
|
|
244
|
+
Get,
|
|
245
|
+
Post,
|
|
246
|
+
Body,
|
|
247
|
+
Param,
|
|
248
|
+
Autowired,
|
|
249
|
+
InjectRepository,
|
|
250
|
+
Value,
|
|
251
|
+
} from "fragment-ts";
|
|
221
252
|
|
|
222
253
|
// Entity
|
|
223
254
|
@Entity()
|
|
224
255
|
class User {
|
|
225
256
|
@PrimaryGeneratedColumn()
|
|
226
257
|
id!: number;
|
|
227
|
-
|
|
258
|
+
|
|
228
259
|
@Column()
|
|
229
260
|
name!: string;
|
|
230
261
|
}
|
|
@@ -234,7 +265,7 @@ class User {
|
|
|
234
265
|
class UserRepository {
|
|
235
266
|
@InjectRepository(User)
|
|
236
267
|
private repo!: Repository<User>;
|
|
237
|
-
|
|
268
|
+
|
|
238
269
|
async findAll() {
|
|
239
270
|
return this.repo.find();
|
|
240
271
|
}
|
|
@@ -245,22 +276,22 @@ class UserRepository {
|
|
|
245
276
|
class UserService {
|
|
246
277
|
@Autowired()
|
|
247
278
|
private userRepository!: UserRepository;
|
|
248
|
-
|
|
249
|
-
@Value(
|
|
279
|
+
|
|
280
|
+
@Value("${MAX_USERS:100}")
|
|
250
281
|
private maxUsers!: number;
|
|
251
|
-
|
|
282
|
+
|
|
252
283
|
async getUsers() {
|
|
253
284
|
return this.userRepository.findAll();
|
|
254
285
|
}
|
|
255
286
|
}
|
|
256
287
|
|
|
257
288
|
// Controller with service
|
|
258
|
-
@Controller(
|
|
289
|
+
@Controller("/api/users")
|
|
259
290
|
class UserController {
|
|
260
291
|
@Autowired()
|
|
261
292
|
private userService!: UserService;
|
|
262
|
-
|
|
263
|
-
@Get(
|
|
293
|
+
|
|
294
|
+
@Get("/")
|
|
264
295
|
async getAll() {
|
|
265
296
|
return this.userService.getUsers();
|
|
266
297
|
}
|
|
@@ -272,24 +303,27 @@ class UserController {
|
|
|
272
303
|
## Migration Guide
|
|
273
304
|
|
|
274
305
|
### Before (Broken)
|
|
306
|
+
|
|
275
307
|
```typescript
|
|
276
308
|
@Service()
|
|
277
309
|
class MyService {
|
|
278
310
|
@Autowired()
|
|
279
|
-
private repo: MyRepo;
|
|
311
|
+
private repo: MyRepo; // ❌ undefined
|
|
280
312
|
}
|
|
281
313
|
```
|
|
282
314
|
|
|
283
315
|
### After (Fixed)
|
|
316
|
+
|
|
284
317
|
```typescript
|
|
285
318
|
@Service()
|
|
286
319
|
class MyService {
|
|
287
320
|
@Autowired()
|
|
288
|
-
private repo!: MyRepo;
|
|
321
|
+
private repo!: MyRepo; // Auto-injected (note the !)
|
|
289
322
|
}
|
|
290
323
|
```
|
|
291
324
|
|
|
292
325
|
### Key Changes Required:
|
|
326
|
+
|
|
293
327
|
1. Add `!` for definite assignment: `private repo!: MyRepo`
|
|
294
328
|
2. Ensure `emitDecoratorMetadata: true` in tsconfig.json
|
|
295
329
|
3. Import `reflect-metadata` at app entry point
|
|
@@ -303,25 +337,25 @@ class MyService {
|
|
|
303
337
|
fragment-ts/
|
|
304
338
|
├── src/
|
|
305
339
|
│ ├── cli/
|
|
306
|
-
│ │ ├── cli.ts #
|
|
340
|
+
│ │ ├── cli.ts # Updated with test command
|
|
307
341
|
│ │ └── commands/
|
|
308
|
-
│ │ ├── test.command.ts #
|
|
342
|
+
│ │ ├── test.command.ts # NEW
|
|
309
343
|
│ │ ├── diagnostics.command.ts
|
|
310
344
|
│ │ └── ...
|
|
311
345
|
│ ├── core/
|
|
312
346
|
│ │ ├── container/
|
|
313
|
-
│ │ │ └── di-container.ts #
|
|
347
|
+
│ │ │ └── di-container.ts # FIXED
|
|
314
348
|
│ │ ├── decorators/
|
|
315
|
-
│ │ │ ├── injectable.decorator.ts #
|
|
349
|
+
│ │ │ ├── injectable.decorator.ts # FIXED
|
|
316
350
|
│ │ │ ├── repository.decorator.ts
|
|
317
351
|
│ │ │ └── ...
|
|
318
352
|
│ │ └── metadata/
|
|
319
|
-
│ │ └── metadata-keys.ts #
|
|
353
|
+
│ │ └── metadata-keys.ts # UPDATED
|
|
320
354
|
│ └── typeorm/
|
|
321
355
|
│ └── typeorm-module.ts
|
|
322
356
|
└── examples/
|
|
323
|
-
├── complete-app/ #
|
|
324
|
-
└── tests/ #
|
|
357
|
+
├── complete-app/ # NEW
|
|
358
|
+
└── tests/ # NEW
|
|
325
359
|
```
|
|
326
360
|
|
|
327
361
|
---
|
|
@@ -342,40 +376,52 @@ Before running tests, ensure:
|
|
|
342
376
|
## Common Issues & Solutions
|
|
343
377
|
|
|
344
378
|
### Issue: "Cannot resolve dependency"
|
|
379
|
+
|
|
345
380
|
**Solution:** Make sure the dependency is registered:
|
|
381
|
+
|
|
346
382
|
```typescript
|
|
347
383
|
container.register(MyService);
|
|
348
384
|
```
|
|
349
385
|
|
|
350
386
|
### Issue: "@Autowired returns undefined"
|
|
387
|
+
|
|
351
388
|
**Solution:** Add `!` for definite assignment and check tsconfig:
|
|
389
|
+
|
|
352
390
|
```typescript
|
|
353
391
|
@Autowired()
|
|
354
392
|
private service!: MyService; // Note the !
|
|
355
393
|
```
|
|
356
394
|
|
|
357
395
|
### Issue: "TypeORM repository is undefined"
|
|
396
|
+
|
|
358
397
|
**Solution:** Use @InjectRepository instead of @Autowired:
|
|
398
|
+
|
|
359
399
|
```typescript
|
|
360
400
|
@InjectRepository(User) // Not @Autowired()
|
|
361
401
|
private repo!: Repository<User>;
|
|
362
402
|
```
|
|
363
403
|
|
|
364
404
|
### Issue: "Circular dependency detected"
|
|
405
|
+
|
|
365
406
|
**Solution:** Refactor to break the cycle:
|
|
407
|
+
|
|
366
408
|
```typescript
|
|
367
409
|
// Before (circular)
|
|
368
|
-
class A {
|
|
369
|
-
|
|
410
|
+
class A {
|
|
411
|
+
constructor(private b: B) {}
|
|
412
|
+
}
|
|
413
|
+
class B {
|
|
414
|
+
constructor(private a: A) {}
|
|
415
|
+
}
|
|
370
416
|
|
|
371
417
|
// After (inject via property)
|
|
372
|
-
class A {
|
|
373
|
-
@Autowired()
|
|
374
|
-
private b!: B;
|
|
418
|
+
class A {
|
|
419
|
+
@Autowired()
|
|
420
|
+
private b!: B;
|
|
375
421
|
}
|
|
376
|
-
class B {
|
|
377
|
-
@Autowired()
|
|
378
|
-
private a!: A;
|
|
422
|
+
class B {
|
|
423
|
+
@Autowired()
|
|
424
|
+
private a!: A;
|
|
379
425
|
}
|
|
380
426
|
```
|
|
381
427
|
|
|
@@ -384,16 +430,19 @@ class B {
|
|
|
384
430
|
## Next Steps
|
|
385
431
|
|
|
386
432
|
1. **Run Tests**
|
|
433
|
+
|
|
387
434
|
```bash
|
|
388
435
|
fragment test
|
|
389
436
|
```
|
|
390
437
|
|
|
391
438
|
2. **Check All Beans**
|
|
439
|
+
|
|
392
440
|
```bash
|
|
393
441
|
fragment beans
|
|
394
442
|
```
|
|
395
443
|
|
|
396
444
|
3. **Verify Routes**
|
|
445
|
+
|
|
397
446
|
```bash
|
|
398
447
|
fragment routes
|
|
399
448
|
```
|
|
@@ -415,6 +464,6 @@ All requested features are now working:
|
|
|
415
464
|
✅ @InjectRepository provides TypeORM repository access
|
|
416
465
|
✅ All decorators working (@Controller, @Service, @Repository, @Injectable, etc.)
|
|
417
466
|
✅ Comprehensive examples and documentation
|
|
418
|
-
✅ Complete testing infrastructure
|
|
467
|
+
✅ Complete testing infrastructure
|
|
419
468
|
|
|
420
|
-
The framework is now fully functional and ready for development!
|
|
469
|
+
The framework is now fully functional and ready for development!
|
package/dist/testing/runner.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export interface TestSuite {
|
|
2
2
|
name: string;
|
|
3
3
|
tests: Test[];
|
|
4
|
+
beforeEachHooks: Array<() => void | Promise<void>>;
|
|
5
|
+
afterEachHooks: Array<() => void | Promise<void>>;
|
|
4
6
|
}
|
|
5
7
|
export interface Test {
|
|
6
8
|
name: string;
|
|
@@ -16,11 +18,19 @@ export declare class TestRunner {
|
|
|
16
18
|
}
|
|
17
19
|
export declare function describe(name: string, fn: () => void): void;
|
|
18
20
|
export declare function it(name: string, fn: () => void | Promise<void>): void;
|
|
21
|
+
export declare function beforeEach(fn: () => void | Promise<void>): void;
|
|
22
|
+
export declare function afterEach(fn: () => void | Promise<void>): void;
|
|
19
23
|
export declare function expect(actual: any): {
|
|
20
24
|
toBe(expected: any): void;
|
|
21
25
|
toEqual(expected: any): void;
|
|
22
26
|
toBeTruthy(): void;
|
|
23
27
|
toBeFalsy(): void;
|
|
24
28
|
toThrow(): void;
|
|
29
|
+
toBeInstanceOf(expected: any): void;
|
|
30
|
+
toContain(expected: any): void;
|
|
31
|
+
toHaveProperty(prop: string): void;
|
|
32
|
+
toBeNull(): void;
|
|
33
|
+
toBeUndefined(): void;
|
|
34
|
+
toHaveLength(expected: number): void;
|
|
25
35
|
};
|
|
26
36
|
//# sourceMappingURL=runner.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/testing/runner.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/testing/runner.ts"],"names":[],"mappings":"AAmBA,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,eAAe,EAAE,KAAK,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACnD,cAAc,EAAE,KAAK,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;CACnD;AAED,MAAM,WAAW,IAAI;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChC;AAKD,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,MAAM,CAAK;IAEnB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI;IAiCtC,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IAmCpB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAOpD;AAKD,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAK3D;AAED,wBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAKrE;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAK/D;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAK9D;AAKD,wBAAgB,MAAM,CAAC,MAAM,EAAE,GAAG;mBAEf,GAAG;sBAMA,GAAG;;;;6BAqCI,GAAG;wBAMR,GAAG;yBAUF,MAAM;;;2BAkBJ,MAAM;EAUhC"}
|
package/dist/testing/runner.js
CHANGED
|
@@ -36,6 +36,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.TestRunner = void 0;
|
|
37
37
|
exports.describe = describe;
|
|
38
38
|
exports.it = it;
|
|
39
|
+
exports.beforeEach = beforeEach;
|
|
40
|
+
exports.afterEach = afterEach;
|
|
39
41
|
exports.expect = expect;
|
|
40
42
|
const path = __importStar(require("path"));
|
|
41
43
|
const glob_1 = require("glob");
|
|
@@ -50,34 +52,54 @@ class TestRunner {
|
|
|
50
52
|
this.failed = 0;
|
|
51
53
|
}
|
|
52
54
|
describe(name, fn) {
|
|
53
|
-
const suite = {
|
|
55
|
+
const suite = {
|
|
56
|
+
name,
|
|
57
|
+
tests: [],
|
|
58
|
+
beforeEachHooks: [],
|
|
59
|
+
afterEachHooks: [],
|
|
60
|
+
};
|
|
54
61
|
this.suites.push(suite);
|
|
55
|
-
const currentSuite = suite;
|
|
56
62
|
const originalIt = G.it;
|
|
63
|
+
const originalBeforeEach = G.beforeEach;
|
|
64
|
+
const originalAfterEach = G.afterEach;
|
|
57
65
|
G.it = (testName, testFn) => {
|
|
58
|
-
|
|
66
|
+
suite.tests.push({ name: testName, fn: testFn });
|
|
67
|
+
};
|
|
68
|
+
G.beforeEach = (hook) => {
|
|
69
|
+
suite.beforeEachHooks.push(hook);
|
|
70
|
+
};
|
|
71
|
+
G.afterEach = (hook) => {
|
|
72
|
+
suite.afterEachHooks.push(hook);
|
|
59
73
|
};
|
|
60
74
|
fn();
|
|
61
75
|
G.it = originalIt;
|
|
76
|
+
G.beforeEach = originalBeforeEach;
|
|
77
|
+
G.afterEach = originalAfterEach;
|
|
62
78
|
}
|
|
63
79
|
async run() {
|
|
64
|
-
console.log(
|
|
80
|
+
console.log("\nRunning Fragment Tests\n");
|
|
65
81
|
for (const suite of this.suites) {
|
|
66
|
-
console.log(`\
|
|
82
|
+
console.log(`\nSuite: ${suite.name}`);
|
|
67
83
|
for (const test of suite.tests) {
|
|
68
84
|
try {
|
|
85
|
+
for (const hook of suite.beforeEachHooks) {
|
|
86
|
+
await hook();
|
|
87
|
+
}
|
|
69
88
|
await test.fn();
|
|
70
|
-
|
|
89
|
+
for (const hook of suite.afterEachHooks) {
|
|
90
|
+
await hook();
|
|
91
|
+
}
|
|
92
|
+
console.log(` PASS ${test.name}`);
|
|
71
93
|
this.passed++;
|
|
72
94
|
}
|
|
73
95
|
catch (error) {
|
|
74
|
-
console.log(`
|
|
75
|
-
console.error(` ${error}`);
|
|
96
|
+
console.log(` FAIL ${test.name}`);
|
|
97
|
+
console.error(` ${error?.message ?? error}`);
|
|
76
98
|
this.failed++;
|
|
77
99
|
}
|
|
78
100
|
}
|
|
79
101
|
}
|
|
80
|
-
console.log(`\
|
|
102
|
+
console.log(`\nResults: ${this.passed} passed, ${this.failed} failed\n`);
|
|
81
103
|
if (this.failed > 0) {
|
|
82
104
|
process.exit(1);
|
|
83
105
|
}
|
|
@@ -94,11 +116,32 @@ exports.TestRunner = TestRunner;
|
|
|
94
116
|
* Global test helpers
|
|
95
117
|
* ====================================================== */
|
|
96
118
|
function describe(name, fn) {
|
|
97
|
-
G.__testRunner
|
|
119
|
+
if (!G.__testRunner) {
|
|
120
|
+
throw new Error("TestRunner not initialized");
|
|
121
|
+
}
|
|
122
|
+
G.__testRunner.describe(name, fn);
|
|
98
123
|
}
|
|
99
124
|
function it(name, fn) {
|
|
100
|
-
|
|
125
|
+
if (!G.it) {
|
|
126
|
+
throw new Error('"it" must be called inside describe()');
|
|
127
|
+
}
|
|
128
|
+
G.it(name, fn);
|
|
129
|
+
}
|
|
130
|
+
function beforeEach(fn) {
|
|
131
|
+
if (!G.beforeEach) {
|
|
132
|
+
throw new Error('"beforeEach" must be called inside describe()');
|
|
133
|
+
}
|
|
134
|
+
G.beforeEach(fn);
|
|
101
135
|
}
|
|
136
|
+
function afterEach(fn) {
|
|
137
|
+
if (!G.afterEach) {
|
|
138
|
+
throw new Error('"afterEach" must be called inside describe()');
|
|
139
|
+
}
|
|
140
|
+
G.afterEach(fn);
|
|
141
|
+
}
|
|
142
|
+
/* ======================================================
|
|
143
|
+
* Expect / Assertions
|
|
144
|
+
* ====================================================== */
|
|
102
145
|
function expect(actual) {
|
|
103
146
|
return {
|
|
104
147
|
toBe(expected) {
|
|
@@ -107,8 +150,10 @@ function expect(actual) {
|
|
|
107
150
|
}
|
|
108
151
|
},
|
|
109
152
|
toEqual(expected) {
|
|
110
|
-
|
|
111
|
-
|
|
153
|
+
const a = JSON.stringify(actual);
|
|
154
|
+
const e = JSON.stringify(expected);
|
|
155
|
+
if (a !== e) {
|
|
156
|
+
throw new Error(`Expected ${a} to equal ${e}`);
|
|
112
157
|
}
|
|
113
158
|
},
|
|
114
159
|
toBeTruthy() {
|
|
@@ -122,12 +167,54 @@ function expect(actual) {
|
|
|
122
167
|
}
|
|
123
168
|
},
|
|
124
169
|
toThrow() {
|
|
170
|
+
if (typeof actual !== "function") {
|
|
171
|
+
throw new Error("toThrow expects a function");
|
|
172
|
+
}
|
|
173
|
+
let threw = false;
|
|
125
174
|
try {
|
|
126
175
|
actual();
|
|
127
|
-
throw new Error('Expected function to throw');
|
|
128
176
|
}
|
|
129
177
|
catch {
|
|
130
|
-
|
|
178
|
+
threw = true;
|
|
179
|
+
}
|
|
180
|
+
if (!threw) {
|
|
181
|
+
throw new Error("Expected function to throw an error");
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
toBeInstanceOf(expected) {
|
|
185
|
+
if (!(actual instanceof expected)) {
|
|
186
|
+
throw new Error(`Expected object to be instance of ${expected?.name}`);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
toContain(expected) {
|
|
190
|
+
if (!Array.isArray(actual) && typeof actual !== "string") {
|
|
191
|
+
throw new Error("toContain works only on arrays or strings");
|
|
192
|
+
}
|
|
193
|
+
if (!actual.includes(expected)) {
|
|
194
|
+
throw new Error(`Expected ${actual} to contain ${expected}`);
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
toHaveProperty(prop) {
|
|
198
|
+
if (actual == null || !(prop in actual)) {
|
|
199
|
+
throw new Error(`Expected object to have property "${prop}"`);
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
toBeNull() {
|
|
203
|
+
if (actual !== null) {
|
|
204
|
+
throw new Error(`Expected ${actual} to be null`);
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
toBeUndefined() {
|
|
208
|
+
if (actual !== undefined) {
|
|
209
|
+
throw new Error(`Expected ${actual} to be undefined`);
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
toHaveLength(expected) {
|
|
213
|
+
if (actual == null || typeof actual.length !== "number") {
|
|
214
|
+
throw new Error("toHaveLength works only on arrays or strings");
|
|
215
|
+
}
|
|
216
|
+
if (actual.length !== expected) {
|
|
217
|
+
throw new Error(`Expected length ${expected}, got ${actual.length}`);
|
|
131
218
|
}
|
|
132
219
|
},
|
|
133
220
|
};
|
|
@@ -138,6 +225,12 @@ function expect(actual) {
|
|
|
138
225
|
if (require.main === module) {
|
|
139
226
|
const runner = new TestRunner();
|
|
140
227
|
G.__testRunner = runner;
|
|
141
|
-
runner
|
|
228
|
+
runner
|
|
229
|
+
.loadTestFiles("dist/**/*.spec.js")
|
|
230
|
+
.then(() => runner.run())
|
|
231
|
+
.catch((err) => {
|
|
232
|
+
console.error("Failed to run tests:", err);
|
|
233
|
+
process.exit(1);
|
|
234
|
+
});
|
|
142
235
|
}
|
|
143
236
|
//# sourceMappingURL=runner.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../../src/testing/runner.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../../src/testing/runner.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/testing/runner.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as fs from
|
|
2
|
-
import * as path from
|
|
3
|
-
import { glob } from
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { glob } from "glob";
|
|
4
4
|
|
|
5
5
|
/* ======================================================
|
|
6
6
|
* Global augmentation
|
|
@@ -8,6 +8,8 @@ import { glob } from 'glob';
|
|
|
8
8
|
type GlobalWithTestRunner = typeof globalThis & {
|
|
9
9
|
__testRunner?: TestRunner;
|
|
10
10
|
it?: (name: string, fn: () => void | Promise<void>) => void;
|
|
11
|
+
beforeEach?: (fn: () => void | Promise<void>) => void;
|
|
12
|
+
afterEach?: (fn: () => void | Promise<void>) => void;
|
|
11
13
|
};
|
|
12
14
|
|
|
13
15
|
const G = global as GlobalWithTestRunner;
|
|
@@ -18,6 +20,8 @@ const G = global as GlobalWithTestRunner;
|
|
|
18
20
|
export interface TestSuite {
|
|
19
21
|
name: string;
|
|
20
22
|
tests: Test[];
|
|
23
|
+
beforeEachHooks: Array<() => void | Promise<void>>;
|
|
24
|
+
afterEachHooks: Array<() => void | Promise<void>>;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
export interface Test {
|
|
@@ -34,43 +38,67 @@ export class TestRunner {
|
|
|
34
38
|
private failed = 0;
|
|
35
39
|
|
|
36
40
|
describe(name: string, fn: () => void): void {
|
|
37
|
-
const suite: TestSuite = {
|
|
41
|
+
const suite: TestSuite = {
|
|
42
|
+
name,
|
|
43
|
+
tests: [],
|
|
44
|
+
beforeEachHooks: [],
|
|
45
|
+
afterEachHooks: [],
|
|
46
|
+
};
|
|
47
|
+
|
|
38
48
|
this.suites.push(suite);
|
|
39
49
|
|
|
40
|
-
const currentSuite = suite;
|
|
41
50
|
const originalIt = G.it;
|
|
51
|
+
const originalBeforeEach = G.beforeEach;
|
|
52
|
+
const originalAfterEach = G.afterEach;
|
|
42
53
|
|
|
43
54
|
G.it = (testName: string, testFn: () => void | Promise<void>) => {
|
|
44
|
-
|
|
55
|
+
suite.tests.push({ name: testName, fn: testFn });
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
G.beforeEach = (hook: () => void | Promise<void>) => {
|
|
59
|
+
suite.beforeEachHooks.push(hook);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
G.afterEach = (hook: () => void | Promise<void>) => {
|
|
63
|
+
suite.afterEachHooks.push(hook);
|
|
45
64
|
};
|
|
46
65
|
|
|
47
66
|
fn();
|
|
48
67
|
|
|
49
68
|
G.it = originalIt;
|
|
69
|
+
G.beforeEach = originalBeforeEach;
|
|
70
|
+
G.afterEach = originalAfterEach;
|
|
50
71
|
}
|
|
51
72
|
|
|
52
73
|
async run(): Promise<void> {
|
|
53
|
-
console.log(
|
|
74
|
+
console.log("\nRunning Fragment Tests\n");
|
|
54
75
|
|
|
55
76
|
for (const suite of this.suites) {
|
|
56
|
-
console.log(`\
|
|
77
|
+
console.log(`\nSuite: ${suite.name}`);
|
|
57
78
|
|
|
58
79
|
for (const test of suite.tests) {
|
|
59
80
|
try {
|
|
81
|
+
for (const hook of suite.beforeEachHooks) {
|
|
82
|
+
await hook();
|
|
83
|
+
}
|
|
84
|
+
|
|
60
85
|
await test.fn();
|
|
61
|
-
|
|
86
|
+
|
|
87
|
+
for (const hook of suite.afterEachHooks) {
|
|
88
|
+
await hook();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log(` PASS ${test.name}`);
|
|
62
92
|
this.passed++;
|
|
63
|
-
} catch (error) {
|
|
64
|
-
console.log(`
|
|
65
|
-
console.error(` ${error}`);
|
|
93
|
+
} catch (error: any) {
|
|
94
|
+
console.log(` FAIL ${test.name}`);
|
|
95
|
+
console.error(` ${error?.message ?? error}`);
|
|
66
96
|
this.failed++;
|
|
67
97
|
}
|
|
68
98
|
}
|
|
69
99
|
}
|
|
70
100
|
|
|
71
|
-
console.log(
|
|
72
|
-
`\n\n📊 Results: ${this.passed} passed, ${this.failed} failed\n`,
|
|
73
|
-
);
|
|
101
|
+
console.log(`\nResults: ${this.passed} passed, ${this.failed} failed\n`);
|
|
74
102
|
|
|
75
103
|
if (this.failed > 0) {
|
|
76
104
|
process.exit(1);
|
|
@@ -90,13 +118,36 @@ export class TestRunner {
|
|
|
90
118
|
* Global test helpers
|
|
91
119
|
* ====================================================== */
|
|
92
120
|
export function describe(name: string, fn: () => void): void {
|
|
93
|
-
G.__testRunner
|
|
121
|
+
if (!G.__testRunner) {
|
|
122
|
+
throw new Error("TestRunner not initialized");
|
|
123
|
+
}
|
|
124
|
+
G.__testRunner.describe(name, fn);
|
|
94
125
|
}
|
|
95
126
|
|
|
96
127
|
export function it(name: string, fn: () => void | Promise<void>): void {
|
|
97
|
-
|
|
128
|
+
if (!G.it) {
|
|
129
|
+
throw new Error('"it" must be called inside describe()');
|
|
130
|
+
}
|
|
131
|
+
G.it(name, fn);
|
|
98
132
|
}
|
|
99
133
|
|
|
134
|
+
export function beforeEach(fn: () => void | Promise<void>): void {
|
|
135
|
+
if (!G.beforeEach) {
|
|
136
|
+
throw new Error('"beforeEach" must be called inside describe()');
|
|
137
|
+
}
|
|
138
|
+
G.beforeEach(fn);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function afterEach(fn: () => void | Promise<void>): void {
|
|
142
|
+
if (!G.afterEach) {
|
|
143
|
+
throw new Error('"afterEach" must be called inside describe()');
|
|
144
|
+
}
|
|
145
|
+
G.afterEach(fn);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* ======================================================
|
|
149
|
+
* Expect / Assertions
|
|
150
|
+
* ====================================================== */
|
|
100
151
|
export function expect(actual: any) {
|
|
101
152
|
return {
|
|
102
153
|
toBe(expected: any) {
|
|
@@ -104,29 +155,85 @@ export function expect(actual: any) {
|
|
|
104
155
|
throw new Error(`Expected ${actual} to be ${expected}`);
|
|
105
156
|
}
|
|
106
157
|
},
|
|
158
|
+
|
|
107
159
|
toEqual(expected: any) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
);
|
|
160
|
+
const a = JSON.stringify(actual);
|
|
161
|
+
const e = JSON.stringify(expected);
|
|
162
|
+
if (a !== e) {
|
|
163
|
+
throw new Error(`Expected ${a} to equal ${e}`);
|
|
112
164
|
}
|
|
113
165
|
},
|
|
166
|
+
|
|
114
167
|
toBeTruthy() {
|
|
115
168
|
if (!actual) {
|
|
116
169
|
throw new Error(`Expected ${actual} to be truthy`);
|
|
117
170
|
}
|
|
118
171
|
},
|
|
172
|
+
|
|
119
173
|
toBeFalsy() {
|
|
120
174
|
if (actual) {
|
|
121
175
|
throw new Error(`Expected ${actual} to be falsy`);
|
|
122
176
|
}
|
|
123
177
|
},
|
|
178
|
+
|
|
124
179
|
toThrow() {
|
|
180
|
+
if (typeof actual !== "function") {
|
|
181
|
+
throw new Error("toThrow expects a function");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let threw = false;
|
|
125
185
|
try {
|
|
126
186
|
actual();
|
|
127
|
-
throw new Error('Expected function to throw');
|
|
128
187
|
} catch {
|
|
129
|
-
|
|
188
|
+
threw = true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!threw) {
|
|
192
|
+
throw new Error("Expected function to throw an error");
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
toBeInstanceOf(expected: any) {
|
|
197
|
+
if (!(actual instanceof expected)) {
|
|
198
|
+
throw new Error(`Expected object to be instance of ${expected?.name}`);
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
toContain(expected: any) {
|
|
203
|
+
if (!Array.isArray(actual) && typeof actual !== "string") {
|
|
204
|
+
throw new Error("toContain works only on arrays or strings");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!actual.includes(expected)) {
|
|
208
|
+
throw new Error(`Expected ${actual} to contain ${expected}`);
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
toHaveProperty(prop: string) {
|
|
213
|
+
if (actual == null || !(prop in actual)) {
|
|
214
|
+
throw new Error(`Expected object to have property "${prop}"`);
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
toBeNull() {
|
|
219
|
+
if (actual !== null) {
|
|
220
|
+
throw new Error(`Expected ${actual} to be null`);
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
toBeUndefined() {
|
|
225
|
+
if (actual !== undefined) {
|
|
226
|
+
throw new Error(`Expected ${actual} to be undefined`);
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
toHaveLength(expected: number) {
|
|
231
|
+
if (actual == null || typeof actual.length !== "number") {
|
|
232
|
+
throw new Error("toHaveLength works only on arrays or strings");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (actual.length !== expected) {
|
|
236
|
+
throw new Error(`Expected length ${expected}, got ${actual.length}`);
|
|
130
237
|
}
|
|
131
238
|
},
|
|
132
239
|
};
|
|
@@ -139,5 +246,11 @@ if (require.main === module) {
|
|
|
139
246
|
const runner = new TestRunner();
|
|
140
247
|
G.__testRunner = runner;
|
|
141
248
|
|
|
142
|
-
runner
|
|
249
|
+
runner
|
|
250
|
+
.loadTestFiles("dist/**/*.spec.js")
|
|
251
|
+
.then(() => runner.run())
|
|
252
|
+
.catch((err) => {
|
|
253
|
+
console.error("Failed to run tests:", err);
|
|
254
|
+
process.exit(1);
|
|
255
|
+
});
|
|
143
256
|
}
|