fragment-ts 1.0.18 → 1.0.20

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.
@@ -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 {
@@ -32,71 +36,212 @@ export class TestRunner {
32
36
  private suites: TestSuite[] = [];
33
37
  private passed = 0;
34
38
  private failed = 0;
39
+ private errors: Array<{
40
+ suite: string;
41
+ test: string;
42
+ error: string;
43
+ stack?: string;
44
+ }> = [];
35
45
 
36
46
  describe(name: string, fn: () => void): void {
37
- const suite: TestSuite = { name, tests: [] };
47
+ const suite: TestSuite = {
48
+ name,
49
+ tests: [],
50
+ beforeEachHooks: [],
51
+ afterEachHooks: [],
52
+ };
53
+
38
54
  this.suites.push(suite);
39
55
 
40
- const currentSuite = suite;
41
56
  const originalIt = G.it;
57
+ const originalBeforeEach = G.beforeEach;
58
+ const originalAfterEach = G.afterEach;
42
59
 
43
60
  G.it = (testName: string, testFn: () => void | Promise<void>) => {
44
- currentSuite.tests.push({ name: testName, fn: testFn });
61
+ suite.tests.push({ name: testName, fn: testFn });
62
+ };
63
+
64
+ G.beforeEach = (hook: () => void | Promise<void>) => {
65
+ suite.beforeEachHooks.push(hook);
66
+ };
67
+
68
+ G.afterEach = (hook: () => void | Promise<void>) => {
69
+ suite.afterEachHooks.push(hook);
45
70
  };
46
71
 
47
72
  fn();
48
73
 
49
74
  G.it = originalIt;
75
+ G.beforeEach = originalBeforeEach;
76
+ G.afterEach = originalAfterEach;
50
77
  }
51
78
 
52
79
  async run(): Promise<void> {
53
- console.log('\n🧪 Running Fragment Tests\n');
80
+ console.log("\n🧪 Running Fragment Tests\n");
81
+
82
+ if (this.suites.length === 0) {
83
+ console.log("No test suites found");
84
+ return;
85
+ }
54
86
 
55
87
  for (const suite of this.suites) {
56
88
  console.log(`\n📦 ${suite.name}`);
57
89
 
90
+ if (suite.tests.length === 0) {
91
+ console.log(" (no tests)");
92
+ continue;
93
+ }
94
+
58
95
  for (const test of suite.tests) {
59
96
  try {
97
+ // Run beforeEach hooks
98
+ for (const hook of suite.beforeEachHooks) {
99
+ await hook();
100
+ }
101
+
102
+ // Run the test
60
103
  await test.fn();
104
+
105
+ // Run afterEach hooks
106
+ for (const hook of suite.afterEachHooks) {
107
+ await hook();
108
+ }
109
+
61
110
  console.log(` ✓ ${test.name}`);
62
111
  this.passed++;
63
- } catch (error) {
112
+ } catch (error: any) {
64
113
  console.log(` ✗ ${test.name}`);
65
- console.error(` ${error}`);
114
+ console.error(` ${error?.message ?? error}`);
66
115
  this.failed++;
116
+ this.errors.push({
117
+ suite: suite.name,
118
+ test: test.name,
119
+ error: error?.message ?? String(error),
120
+ stack: error?.stack,
121
+ });
67
122
  }
68
123
  }
69
124
  }
70
125
 
126
+ // Print summary
71
127
  console.log(
72
128
  `\n\n📊 Results: ${this.passed} passed, ${this.failed} failed\n`,
73
129
  );
74
130
 
75
131
  if (this.failed > 0) {
132
+ console.log("❌ Failed Tests:\n");
133
+ this.errors.forEach((err) => {
134
+ console.log(` ${err.suite} > ${err.test}`);
135
+ console.log(` ${err.error}\n`);
136
+ });
76
137
  process.exit(1);
138
+ } else {
139
+ console.log("✅ All tests passed!\n");
140
+ process.exit(0);
77
141
  }
78
142
  }
79
143
 
80
144
  async loadTestFiles(pattern: string): Promise<void> {
81
- const files = await glob(pattern);
145
+ const files = await glob(pattern, { cwd: process.cwd() });
146
+
147
+ if (files.length === 0) {
148
+ console.log(`No test files found matching pattern: ${pattern}`);
149
+ return;
150
+ }
151
+
152
+ console.log(`Found ${files.length} test file(s)\n`);
153
+
154
+ // Set the global runner before loading any files
155
+ G.__testRunner = this;
82
156
 
83
157
  for (const file of files) {
84
- require(path.resolve(file));
158
+ const fullPath = path.resolve(file);
159
+
160
+ // Clear require cache for this file to allow hot reloading
161
+ delete require.cache[fullPath];
162
+
163
+ try {
164
+ require(fullPath);
165
+ } catch (error: any) {
166
+ console.error(`Error loading test file ${file}:`, error.message);
167
+ throw error;
168
+ }
85
169
  }
86
170
  }
87
171
  }
88
172
 
173
+ /* ======================================================
174
+ * Singleton instance
175
+ * ====================================================== */
176
+ let runnerInstance: TestRunner | null = null;
177
+
178
+ export function getTestRunner(): TestRunner {
179
+ if (!runnerInstance) {
180
+ runnerInstance = new TestRunner();
181
+ G.__testRunner = runnerInstance;
182
+ }
183
+ return runnerInstance;
184
+ }
185
+
186
+ export function initTestRunner(): TestRunner {
187
+ return getTestRunner();
188
+ }
189
+
190
+ /* ======================================================
191
+ * Auto-initialization for test environment
192
+ * ====================================================== */
193
+ // Auto-initialize when in test environment
194
+ if (
195
+ process.env.NODE_ENV === "test" ||
196
+ process.argv.some((arg) => arg.includes("test"))
197
+ ) {
198
+ if (!G.__testRunner) {
199
+ getTestRunner();
200
+ }
201
+ }
202
+
89
203
  /* ======================================================
90
204
  * Global test helpers
91
205
  * ====================================================== */
92
206
  export function describe(name: string, fn: () => void): void {
93
- G.__testRunner?.describe(name, fn);
207
+ if (!G.__testRunner) {
208
+ // Try to auto-initialize
209
+ getTestRunner();
210
+ }
211
+
212
+ if (!G.__testRunner) {
213
+ throw new Error(
214
+ "TestRunner not initialized. Make sure to call initTestRunner() or run tests via 'fragment test' command",
215
+ );
216
+ }
217
+
218
+ G.__testRunner.describe(name, fn);
94
219
  }
95
220
 
96
221
  export function it(name: string, fn: () => void | Promise<void>): void {
97
- // Implemented dynamically by TestRunner.describe
222
+ if (!G.it) {
223
+ throw new Error('"it" must be called inside describe()');
224
+ }
225
+ G.it(name, fn);
226
+ }
227
+
228
+ export function beforeEach(fn: () => void | Promise<void>): void {
229
+ if (!G.beforeEach) {
230
+ throw new Error('"beforeEach" must be called inside describe()');
231
+ }
232
+ G.beforeEach(fn);
233
+ }
234
+
235
+ export function afterEach(fn: () => void | Promise<void>): void {
236
+ if (!G.afterEach) {
237
+ throw new Error('"afterEach" must be called inside describe()');
238
+ }
239
+ G.afterEach(fn);
98
240
  }
99
241
 
242
+ /* ======================================================
243
+ * Expect / Assertions
244
+ * ====================================================== */
100
245
  export function expect(actual: any) {
101
246
  return {
102
247
  toBe(expected: any) {
@@ -104,40 +249,123 @@ export function expect(actual: any) {
104
249
  throw new Error(`Expected ${actual} to be ${expected}`);
105
250
  }
106
251
  },
252
+
107
253
  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
- );
254
+ const a = JSON.stringify(actual);
255
+ const e = JSON.stringify(expected);
256
+ if (a !== e) {
257
+ throw new Error(`Expected ${a} to equal ${e}`);
112
258
  }
113
259
  },
260
+
114
261
  toBeTruthy() {
115
262
  if (!actual) {
116
263
  throw new Error(`Expected ${actual} to be truthy`);
117
264
  }
118
265
  },
266
+
119
267
  toBeFalsy() {
120
268
  if (actual) {
121
269
  throw new Error(`Expected ${actual} to be falsy`);
122
270
  }
123
271
  },
124
- toThrow() {
272
+
273
+ toThrow(expectedError?: string) {
274
+ if (typeof actual !== "function") {
275
+ throw new Error("toThrow expects a function");
276
+ }
277
+
278
+ let threw = false;
279
+ let error = null;
125
280
  try {
126
281
  actual();
127
- throw new Error('Expected function to throw');
128
- } catch {
129
- /* expected */
282
+ } catch (e: any) {
283
+ threw = true;
284
+ error = e;
285
+ }
286
+
287
+ if (!threw) {
288
+ throw new Error("Expected function to throw an error");
289
+ }
290
+
291
+ if (expectedError && error?.message !== expectedError) {
292
+ throw new Error(
293
+ `Expected error message "${expectedError}" but got "${error?.message}"`,
294
+ );
295
+ }
296
+ },
297
+
298
+ toBeInstanceOf(expected: any) {
299
+ if (!(actual instanceof expected)) {
300
+ throw new Error(`Expected object to be instance of ${expected?.name}`);
301
+ }
302
+ },
303
+
304
+ toContain(expected: any) {
305
+ if (!Array.isArray(actual) && typeof actual !== "string") {
306
+ throw new Error("toContain works only on arrays or strings");
307
+ }
308
+
309
+ if (!actual.includes(expected)) {
310
+ throw new Error(`Expected ${actual} to contain ${expected}`);
311
+ }
312
+ },
313
+
314
+ toHaveProperty(property: string, value?: any) {
315
+ if (actual == null || !(property in actual)) {
316
+ throw new Error(`Expected object to have property "${property}"`);
317
+ }
318
+
319
+ if (value !== undefined && actual[property] !== value) {
320
+ throw new Error(
321
+ `Expected property "${property}" to be ${value} but got ${actual[property]}`,
322
+ );
323
+ }
324
+ },
325
+
326
+ toBeNull() {
327
+ if (actual !== null) {
328
+ throw new Error(`Expected ${actual} to be null`);
329
+ }
330
+ },
331
+
332
+ toBeUndefined() {
333
+ if (actual !== undefined) {
334
+ throw new Error(`Expected ${actual} to be undefined`);
335
+ }
336
+ },
337
+
338
+ toHaveLength(expected: number) {
339
+ if (actual == null || typeof actual.length !== "number") {
340
+ throw new Error("toHaveLength works only on arrays or strings");
341
+ }
342
+
343
+ if (actual.length !== expected) {
344
+ throw new Error(`Expected length ${expected}, got ${actual.length}`);
130
345
  }
131
346
  },
132
347
  };
133
348
  }
134
349
 
135
350
  /* ======================================================
136
- * CLI entry
351
+ * CLI entry (when this file is run directly)
137
352
  * ====================================================== */
138
353
  if (require.main === module) {
139
- const runner = new TestRunner();
140
- G.__testRunner = runner;
354
+ (async () => {
355
+ try {
356
+ const runner = getTestRunner();
357
+
358
+ // Auto-detect TypeScript based on arguments
359
+ const isTsNode = process.argv.some(
360
+ (arg) => arg.includes("ts-node") || arg.includes("ts-node/register"),
361
+ );
362
+ const pattern = isTsNode ? "src/**/*.spec.ts" : "dist/**/*.spec.js";
141
363
 
142
- runner.loadTestFiles('dist/**/*.spec.js').then(() => runner.run());
364
+ await runner.loadTestFiles(pattern);
365
+ await runner.run();
366
+ } catch (err: any) {
367
+ console.error("Failed to run tests:", err.message);
368
+ process.exit(1);
369
+ }
370
+ })();
143
371
  }
@@ -20,6 +20,7 @@ export class FragmentWebApplication {
20
20
  this.container = DIContainer.getInstance();
21
21
  this.metadataStorage = MetadataStorage.getInstance();
22
22
  this.setupMiddleware();
23
+ this
23
24
  }
24
25
 
25
26
  private setupMiddleware(): void {