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 CHANGED
@@ -2,10 +2,12 @@
2
2
 
3
3
  ## Issues Fixed
4
4
 
5
- ### 1. Test Command Created
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. @Autowired Decorator Fixed
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; // Auto-injected
44
-
50
+ private userRepository!: UserRepository; // Auto-injected
51
+
45
52
  @Autowired()
46
- private emailService!: EmailService; // Auto-injected
53
+ private emailService!: EmailService; // Auto-injected
47
54
  }
48
55
  ```
49
56
 
50
57
  ---
51
58
 
52
- ### 3. Repository TypeORM Integration Fixed
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>; // TypeORM repo injected
66
-
75
+ private repo!: Repository<User>; // TypeORM repo injected
76
+
67
77
  async findAll() {
68
- return this.repo.find(); // Direct TypeORM access
78
+ return this.repo.find(); // Direct TypeORM access
69
79
  }
70
80
  }
71
81
  ```
72
82
 
73
83
  ---
74
84
 
75
- ### 4. All Injectable Decorators Working
85
+ ### 4. All Injectable Decorators Working
86
+
76
87
  **Previously Working:**
77
- - ✅ @Controller
78
- - @Service
79
- - ✅ HTTP decorators (@Get, @Post, etc.)
88
+
89
+ - @Controller
90
+ - @Service
91
+ - HTTP decorators (@Get, @Post, etc.)
80
92
 
81
93
  **Now Also Working:**
82
- - ✅ @Injectable - General purpose components
83
- - @Repository - Data access with TypeORM integration
84
- - @Autowired - Property injection by type
85
- - @InjectRepository - TypeORM repository injection
86
- - @Inject - Token-based injection
87
- - @Value - Environment variable injection
88
- - @Qualifier - Bean qualifier
89
- - @PostConstruct - Lifecycle hook
90
- - @PreDestroy - Cleanup hook
91
- - @AutoConfiguration - Conditional beans
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 === 'function') {
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
- - ✅ Automatic test file discovery
178
- - TypeScript support (ts-node)
179
- - Watch mode
180
- - Detailed error reporting
181
- - Test isolation (fresh DI container per test)
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 'reflect-metadata';
216
- import {
217
- Controller, Service, Repository,
218
- Get, Post, Body, Param,
219
- Autowired, InjectRepository, Value
220
- } from 'fragment-ts';
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('${MAX_USERS:100}')
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('/api/users')
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; // ❌ undefined
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; // Auto-injected (note the !)
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 # Updated with test command
340
+ │ │ ├── cli.ts # Updated with test command
307
341
  │ │ └── commands/
308
- │ │ ├── test.command.ts # NEW
342
+ │ │ ├── test.command.ts # NEW
309
343
  │ │ ├── diagnostics.command.ts
310
344
  │ │ └── ...
311
345
  │ ├── core/
312
346
  │ │ ├── container/
313
- │ │ │ └── di-container.ts # FIXED
347
+ │ │ │ └── di-container.ts # FIXED
314
348
  │ │ ├── decorators/
315
- │ │ │ ├── injectable.decorator.ts # FIXED
349
+ │ │ │ ├── injectable.decorator.ts # FIXED
316
350
  │ │ │ ├── repository.decorator.ts
317
351
  │ │ │ └── ...
318
352
  │ │ └── metadata/
319
- │ │ └── metadata-keys.ts # UPDATED
353
+ │ │ └── metadata-keys.ts # UPDATED
320
354
  │ └── typeorm/
321
355
  │ └── typeorm-module.ts
322
356
  └── examples/
323
- ├── complete-app/ # NEW
324
- └── tests/ # NEW
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 { constructor(private b: B) {} }
369
- class B { constructor(private a: A) {} }
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!
@@ -177,7 +177,7 @@ class InitCommand {
177
177
  "migrate:revert": "fragment migrate:revert",
178
178
  },
179
179
  dependencies: {
180
- "fragment-ts": "^1.0.18",
180
+ "fragment-ts": "^1.0.19",
181
181
  "reflect-metadata": "^0.1.13",
182
182
  },
183
183
  devDependencies: {
@@ -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":"AAiBA,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,IAAI,EAAE,CAAC;CACf;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;IAgBtC,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IA4BpB,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,CAE3D;AAED,wBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAErE;AAED,wBAAgB,MAAM,CAAC,MAAM,EAAE,GAAG;mBAEf,GAAG;sBAKA,GAAG;;;;EA0BxB"}
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"}
@@ -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 = { name, tests: [] };
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
- currentSuite.tests.push({ name: testName, fn: testFn });
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('\n🧪 Running Fragment Tests\n');
80
+ console.log("\nRunning Fragment Tests\n");
65
81
  for (const suite of this.suites) {
66
- console.log(`\n📦 ${suite.name}`);
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
- console.log(` ✓ ${test.name}`);
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(` ${test.name}`);
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(`\n\n📊 Results: ${this.passed} passed, ${this.failed} failed\n`);
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?.describe(name, fn);
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
- // Implemented dynamically by TestRunner.describe
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
- if (JSON.stringify(actual) !== JSON.stringify(expected)) {
111
- throw new Error(`Expected ${JSON.stringify(actual)} to equal ${JSON.stringify(expected)}`);
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
- /* expected */
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.loadTestFiles('dist/**/*.spec.js').then(() => runner.run());
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":"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"}
1
+ {"version":3,"file":"runner.js","sourceRoot":"","sources":["../../src/testing/runner.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fragment-ts",
3
- "version": "1.0.18",
3
+ "version": "1.0.19",
4
4
  "description": "Spring Boot-style framework for TypeScript with Express and TypeORM",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -169,7 +169,7 @@ export class InitCommand {
169
169
  "migrate:revert": "fragment migrate:revert",
170
170
  },
171
171
  dependencies: {
172
- "fragment-ts": "^1.0.18",
172
+ "fragment-ts": "^1.0.19",
173
173
  "reflect-metadata": "^0.1.13",
174
174
  },
175
175
  devDependencies: {
@@ -1,6 +1,6 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- import { glob } from 'glob';
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 = { name, tests: [] };
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
- currentSuite.tests.push({ name: testName, fn: testFn });
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('\n🧪 Running Fragment Tests\n');
74
+ console.log("\nRunning Fragment Tests\n");
54
75
 
55
76
  for (const suite of this.suites) {
56
- console.log(`\n📦 ${suite.name}`);
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
- console.log(` ✓ ${test.name}`);
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(` ${test.name}`);
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?.describe(name, fn);
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
- // Implemented dynamically by TestRunner.describe
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
- if (JSON.stringify(actual) !== JSON.stringify(expected)) {
109
- throw new Error(
110
- `Expected ${JSON.stringify(actual)} to equal ${JSON.stringify(expected)}`,
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
- /* expected */
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.loadTestFiles('dist/**/*.spec.js').then(() => runner.run());
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
  }