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.
- package/changes/1.md +113 -64
- package/dist/cli/commands/init.command.js +1 -1
- package/dist/cli/commands/test.command.d.ts +1 -0
- package/dist/cli/commands/test.command.d.ts.map +1 -1
- package/dist/cli/commands/test.command.js +149 -173
- package/dist/cli/commands/test.command.js.map +1 -1
- package/dist/testing/runner.d.ts +14 -1
- package/dist/testing/runner.d.ts.map +1 -1
- package/dist/testing/runner.js +199 -19
- package/dist/testing/runner.js.map +1 -1
- package/dist/web/application.d.ts.map +1 -1
- package/dist/web/application.js +1 -0
- package/dist/web/application.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/init.command.ts +1 -1
- package/src/cli/commands/test.command.ts +188 -173
- package/src/testing/runner.ts +253 -25
- package/src/web/application.ts +1 -0
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 {
|
|
@@ -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 = {
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
}
|