fragment-ts 1.0.19 → 1.0.21
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/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 +132 -189
- package/dist/cli/commands/test.command.js.map +1 -1
- package/dist/testing/runner.d.ts +5 -2
- package/dist/testing/runner.d.ts.map +1 -1
- package/dist/testing/runner.js +110 -23
- 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 +184 -191
- package/src/testing/runner.ts +138 -23
- package/src/web/application.ts +1 -0
|
@@ -3,6 +3,7 @@ import chalk from "chalk";
|
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import { spawn } from "child_process";
|
|
5
5
|
import * as fs from "fs";
|
|
6
|
+
import chokidar from "chokidar";
|
|
6
7
|
|
|
7
8
|
export class TestCommand {
|
|
8
9
|
static register(program: Command): void {
|
|
@@ -17,6 +18,7 @@ export class TestCommand {
|
|
|
17
18
|
)
|
|
18
19
|
.option("--pattern <pattern>", "Test file pattern", "**/*.spec.ts")
|
|
19
20
|
.option("--coverage", "Generate coverage report")
|
|
21
|
+
.option("--no-color", "Disable colored output")
|
|
20
22
|
.action(async (options) => {
|
|
21
23
|
await this.runTests(options);
|
|
22
24
|
});
|
|
@@ -57,7 +59,7 @@ export class TestCommand {
|
|
|
57
59
|
useTypeScript = false;
|
|
58
60
|
mode = "production (dist/)";
|
|
59
61
|
basePath = "dist";
|
|
60
|
-
pattern = options.pattern.replace(
|
|
62
|
+
pattern = options.pattern.replace(/\.ts$/, ".js") || "**/*.spec.js";
|
|
61
63
|
} else {
|
|
62
64
|
// Auto-detect
|
|
63
65
|
if (hasSource) {
|
|
@@ -69,7 +71,7 @@ export class TestCommand {
|
|
|
69
71
|
useTypeScript = false;
|
|
70
72
|
mode = "auto-detected production (dist/)";
|
|
71
73
|
basePath = "dist";
|
|
72
|
-
pattern = options.pattern.replace(
|
|
74
|
+
pattern = options.pattern.replace(/\.ts$/, ".js") || "**/*.spec.js";
|
|
73
75
|
} else {
|
|
74
76
|
console.log(
|
|
75
77
|
chalk.red("No src/ or dist/ directory found. Run: fragment init"),
|
|
@@ -78,212 +80,203 @@ export class TestCommand {
|
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
|
|
81
|
-
console.log(chalk.gray(` Mode: ${mode}
|
|
83
|
+
console.log(chalk.gray(` Mode: ${mode}`));
|
|
84
|
+
console.log(chalk.gray(` Pattern: ${basePath}/${pattern}`));
|
|
82
85
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
// Check if we need to use ts-node for TypeScript files
|
|
87
|
+
const tsConfigPath = path.join(cwd, "tsconfig.json");
|
|
88
|
+
const hasTsConfig = fs.existsSync(tsConfigPath);
|
|
86
89
|
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
// Global test functions
|
|
100
|
-
global.describe = function(name, fn) {
|
|
101
|
-
const suite = { name, tests: [] };
|
|
102
|
-
testState.suites.push(suite);
|
|
103
|
-
testState.currentSuite = suite;
|
|
104
|
-
fn();
|
|
105
|
-
testState.currentSuite = null;
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
global.it = function(name, fn) {
|
|
109
|
-
if (!testState.currentSuite) {
|
|
110
|
-
throw new Error('it() must be called inside describe()');
|
|
111
|
-
}
|
|
112
|
-
testState.currentSuite.tests.push({ name, fn });
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
global.expect = function(actual) {
|
|
116
|
-
return {
|
|
117
|
-
toBe(expected) {
|
|
118
|
-
if (actual !== expected) {
|
|
119
|
-
throw new Error(\`Expected \${actual} to be \${expected}\`);
|
|
120
|
-
}
|
|
121
|
-
},
|
|
122
|
-
toEqual(expected) {
|
|
123
|
-
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
124
|
-
throw new Error(\`Expected \${JSON.stringify(actual)} to equal \${JSON.stringify(expected)}\`);
|
|
125
|
-
}
|
|
126
|
-
},
|
|
127
|
-
toBeTruthy() {
|
|
128
|
-
if (!actual) {
|
|
129
|
-
throw new Error(\`Expected \${actual} to be truthy\`);
|
|
130
|
-
}
|
|
131
|
-
},
|
|
132
|
-
toBeFalsy() {
|
|
133
|
-
if (actual) {
|
|
134
|
-
throw new Error(\`Expected \${actual} to be falsy\`);
|
|
135
|
-
}
|
|
136
|
-
},
|
|
137
|
-
toThrow(expectedError) {
|
|
138
|
-
let threw = false;
|
|
139
|
-
let error = null;
|
|
140
|
-
try {
|
|
141
|
-
actual();
|
|
142
|
-
} catch (e) {
|
|
143
|
-
threw = true;
|
|
144
|
-
error = e;
|
|
145
|
-
}
|
|
146
|
-
if (!threw) {
|
|
147
|
-
throw new Error('Expected function to throw');
|
|
148
|
-
}
|
|
149
|
-
if (expectedError && error.message !== expectedError) {
|
|
150
|
-
throw new Error(\`Expected error message "\${expectedError}" but got "\${error.message}"\`);
|
|
151
|
-
}
|
|
152
|
-
},
|
|
153
|
-
toBeInstanceOf(expected) {
|
|
154
|
-
if (!(actual instanceof expected)) {
|
|
155
|
-
throw new Error(\`Expected \${actual} to be instance of \${expected.name}\`);
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
toContain(expected) {
|
|
159
|
-
if (!actual.includes(expected)) {
|
|
160
|
-
throw new Error(\`Expected \${actual} to contain \${expected}\`);
|
|
161
|
-
}
|
|
162
|
-
},
|
|
163
|
-
toHaveProperty(property, value) {
|
|
164
|
-
if (!(property in actual)) {
|
|
165
|
-
throw new Error(\`Expected object to have property \${property}\`);
|
|
166
|
-
}
|
|
167
|
-
if (value !== undefined && actual[property] !== value) {
|
|
168
|
-
throw new Error(\`Expected property \${property} to be \${value} but got \${actual[property]}\`);
|
|
169
|
-
}
|
|
170
|
-
},
|
|
171
|
-
toBeNull() {
|
|
172
|
-
if (actual !== null) {
|
|
173
|
-
throw new Error(\`Expected \${actual} to be null\`);
|
|
174
|
-
}
|
|
175
|
-
},
|
|
176
|
-
toBeUndefined() {
|
|
177
|
-
if (actual !== undefined) {
|
|
178
|
-
throw new Error(\`Expected \${actual} to be undefined\`);
|
|
179
|
-
}
|
|
180
|
-
},
|
|
181
|
-
toHaveLength(expected) {
|
|
182
|
-
if (actual.length !== expected) {
|
|
183
|
-
throw new Error(\`Expected length \${expected} but got \${actual.length}\`);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
};
|
|
90
|
+
// Create a simple runner script that uses our test runner module
|
|
91
|
+
const scriptContent = this.generateRunnerScript({
|
|
92
|
+
useTypeScript,
|
|
93
|
+
basePath,
|
|
94
|
+
pattern,
|
|
95
|
+
hasTsConfig,
|
|
96
|
+
tsConfigPath,
|
|
97
|
+
watchMode: options.watch,
|
|
98
|
+
coverage: options.coverage,
|
|
99
|
+
noColor: options.color === false,
|
|
100
|
+
});
|
|
188
101
|
|
|
189
|
-
|
|
190
|
-
try {
|
|
191
|
-
// Find and load test files
|
|
192
|
-
const files = await glob('${basePath}/${pattern}', { cwd: process.cwd() });
|
|
193
|
-
|
|
194
|
-
if (files.length === 0) {
|
|
195
|
-
console.log('No test files found matching pattern: ${basePath}/${pattern}');
|
|
196
|
-
process.exit(0);
|
|
197
|
-
}
|
|
102
|
+
const scriptPath = path.join(cwd, ".fragment-test-runner.js");
|
|
198
103
|
|
|
199
|
-
|
|
104
|
+
try {
|
|
105
|
+
fs.writeFileSync(scriptPath, scriptContent);
|
|
106
|
+
fs.chmodSync(scriptPath, "755");
|
|
200
107
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
108
|
+
// -----------------------
|
|
109
|
+
// Watch / Coverage / Single Run
|
|
110
|
+
// -----------------------
|
|
111
|
+
if (options.watch) {
|
|
112
|
+
console.log(
|
|
113
|
+
chalk.green("\n👀 Watch mode enabled. Listening for changes...\n"),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const watcher = chokidar.watch(
|
|
117
|
+
`${basePath}/**/*.spec.${useTypeScript ? "ts" : "js"}`,
|
|
118
|
+
{
|
|
119
|
+
ignoreInitial: true,
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const runTests = () => {
|
|
124
|
+
const proc = spawn("node", [scriptPath], { stdio: "inherit" });
|
|
205
125
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
for (const test of suite.tests) {
|
|
211
|
-
try {
|
|
212
|
-
await test.fn();
|
|
213
|
-
console.log(\` ✓ \${test.name}\`);
|
|
214
|
-
testState.passed++;
|
|
215
|
-
} catch (error) {
|
|
216
|
-
console.log(\` ✗ \${test.name}\`);
|
|
217
|
-
console.error(\` \${error.message}\`);
|
|
218
|
-
testState.failed++;
|
|
219
|
-
testState.errors.push({
|
|
220
|
-
suite: suite.name,
|
|
221
|
-
test: test.name,
|
|
222
|
-
error: error.message,
|
|
223
|
-
stack: error.stack
|
|
126
|
+
proc.on("close", (code) => {
|
|
127
|
+
if (code !== 0)
|
|
128
|
+
console.log(chalk.red(`Test run exited with code ${code}`));
|
|
224
129
|
});
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
130
|
+
};
|
|
228
131
|
|
|
229
|
-
|
|
230
|
-
console.log(\`\\n\\n📊 Results: \${testState.passed} passed, \${testState.failed} failed\\n\`);
|
|
132
|
+
runTests(); // Initial run
|
|
231
133
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
134
|
+
watcher.on("all", (event, pathChanged) => {
|
|
135
|
+
console.log(
|
|
136
|
+
chalk.blue(`\n🔄 File changed (${event}): ${pathChanged}`),
|
|
137
|
+
);
|
|
138
|
+
runTests();
|
|
139
|
+
});
|
|
140
|
+
} else if (options.coverage) {
|
|
141
|
+
console.log(
|
|
142
|
+
chalk.green(
|
|
143
|
+
"\n📊 Coverage enabled. Running tests with coverage...\n",
|
|
144
|
+
),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const proc = spawn(
|
|
148
|
+
"npx",
|
|
149
|
+
["c8", "--reporter=lcov", "--reporter=text", "node", scriptPath],
|
|
150
|
+
{ stdio: "inherit" },
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
proc.on("close", (code) => process.exit(code || 0));
|
|
154
|
+
proc.on("error", (err) => {
|
|
155
|
+
console.error(chalk.red("Failed to run coverage:"), err);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
});
|
|
158
|
+
} else {
|
|
159
|
+
const proc = spawn("node", [scriptPath], { stdio: "inherit" });
|
|
160
|
+
|
|
161
|
+
proc.on("close", (code) => process.exit(code || 0));
|
|
162
|
+
proc.on("error", (err) => {
|
|
163
|
+
console.error(chalk.red("Failed to run tests:"), err);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
} catch (error: any) {
|
|
168
|
+
console.error(chalk.red("Failed to create test runner:"), error.message);
|
|
238
169
|
process.exit(1);
|
|
239
|
-
} else {
|
|
240
|
-
console.log('✅ All tests passed!\\n');
|
|
241
|
-
process.exit(0);
|
|
242
170
|
}
|
|
243
|
-
} catch (error) {
|
|
244
|
-
console.error('Error running tests:', error);
|
|
245
|
-
process.exit(1);
|
|
246
171
|
}
|
|
247
|
-
}
|
|
248
172
|
|
|
249
|
-
|
|
250
|
-
|
|
173
|
+
private static generateRunnerScript(options: {
|
|
174
|
+
useTypeScript: boolean;
|
|
175
|
+
basePath: string;
|
|
176
|
+
pattern: string;
|
|
177
|
+
hasTsConfig: boolean;
|
|
178
|
+
tsConfigPath: string;
|
|
179
|
+
watchMode: boolean;
|
|
180
|
+
coverage: boolean;
|
|
181
|
+
noColor: boolean;
|
|
182
|
+
}): string {
|
|
183
|
+
const {
|
|
184
|
+
useTypeScript,
|
|
185
|
+
basePath,
|
|
186
|
+
pattern,
|
|
187
|
+
hasTsConfig,
|
|
188
|
+
tsConfigPath,
|
|
189
|
+
noColor,
|
|
190
|
+
} = options;
|
|
251
191
|
|
|
252
|
-
|
|
192
|
+
let runnerImportPath = "fragment-ts";
|
|
253
193
|
|
|
254
|
-
|
|
255
|
-
|
|
194
|
+
const possiblePaths = [
|
|
195
|
+
path.join(process.cwd(), "node_modules", "fragment-ts", "testing"),
|
|
196
|
+
path.join(
|
|
197
|
+
process.cwd(),
|
|
198
|
+
"node_modules",
|
|
199
|
+
"fragment-ts",
|
|
200
|
+
"dist",
|
|
201
|
+
"testing",
|
|
202
|
+
),
|
|
203
|
+
path.join(__dirname, "..", "..", "testing"),
|
|
204
|
+
];
|
|
256
205
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const proc = spawn("node", nodeArgs, {
|
|
265
|
-
cwd,
|
|
266
|
-
stdio: "inherit",
|
|
267
|
-
env,
|
|
268
|
-
});
|
|
206
|
+
let resolvedRunnerPath = runnerImportPath;
|
|
207
|
+
for (const testPath of possiblePaths) {
|
|
208
|
+
if (fs.existsSync(testPath + ".ts") || fs.existsSync(testPath + ".js")) {
|
|
209
|
+
resolvedRunnerPath = testPath;
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
269
213
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
fs.unlinkSync(scriptPath);
|
|
273
|
-
} catch {}
|
|
274
|
-
process.exit(code || 0);
|
|
275
|
-
});
|
|
214
|
+
return `
|
|
215
|
+
"use strict";
|
|
276
216
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
217
|
+
${useTypeScript ? "require('ts-node/register/transpile-only');" : ""}
|
|
218
|
+
require('reflect-metadata');
|
|
219
|
+
|
|
220
|
+
process.env.NODE_ENV = 'test';
|
|
221
|
+
${noColor ? "process.env.FORCE_COLOR = '0';" : "process.env.FORCE_COLOR = '1';"}
|
|
222
|
+
|
|
223
|
+
${
|
|
224
|
+
hasTsConfig && useTypeScript
|
|
225
|
+
? `
|
|
226
|
+
try {
|
|
227
|
+
const tsNode = require('ts-node');
|
|
228
|
+
tsNode.register({
|
|
229
|
+
project: '${tsConfigPath}',
|
|
230
|
+
transpileOnly: true,
|
|
231
|
+
compilerOptions: {
|
|
232
|
+
module: 'commonjs',
|
|
233
|
+
target: 'ES2020',
|
|
234
|
+
esModuleInterop: true,
|
|
235
|
+
skipLibCheck: true
|
|
287
236
|
}
|
|
237
|
+
});
|
|
238
|
+
} catch (error) {}`
|
|
239
|
+
: ""
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let testRunner;
|
|
243
|
+
try {
|
|
244
|
+
const testModule = require('${resolvedRunnerPath}');
|
|
245
|
+
|
|
246
|
+
if (testModule.getTestRunner) {
|
|
247
|
+
testRunner = testModule.getTestRunner();
|
|
248
|
+
} else if (testModule.TestRunner) {
|
|
249
|
+
testRunner = new testModule.TestRunner();
|
|
250
|
+
} else if (testModule.default && testModule.default.getTestRunner) {
|
|
251
|
+
testRunner = testModule.default.getTestRunner();
|
|
252
|
+
} else {
|
|
253
|
+
testRunner = testModule;
|
|
254
|
+
}
|
|
255
|
+
} catch (error) {
|
|
256
|
+
console.error('Failed to load test runner:', error.message);
|
|
257
|
+
console.error('Make sure fragment-ts is installed as a dependency');
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function runTests() {
|
|
262
|
+
try {
|
|
263
|
+
console.log('Looking for test files...');
|
|
264
|
+
await testRunner.loadTestFiles('${basePath}/${pattern}');
|
|
265
|
+
await testRunner.run();
|
|
266
|
+
} catch (error) {
|
|
267
|
+
console.error('\\n❌ Error running tests:', error.message);
|
|
268
|
+
if (error.stack && process.env.DEBUG) console.error(error.stack);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
process.on('SIGINT', () => { console.log('\\n\\nTest run interrupted'); process.exit(130); });
|
|
274
|
+
process.on('SIGTERM', () => { console.log('\\n\\nTest run terminated'); process.exit(143); });
|
|
275
|
+
|
|
276
|
+
runTests().catch(error => {
|
|
277
|
+
console.error('Unhandled error:', error);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
});
|
|
280
|
+
`;
|
|
288
281
|
}
|
|
289
|
-
}
|
|
282
|
+
}
|
package/src/testing/runner.ts
CHANGED
|
@@ -36,6 +36,12 @@ export class TestRunner {
|
|
|
36
36
|
private suites: TestSuite[] = [];
|
|
37
37
|
private passed = 0;
|
|
38
38
|
private failed = 0;
|
|
39
|
+
private errors: Array<{
|
|
40
|
+
suite: string;
|
|
41
|
+
test: string;
|
|
42
|
+
error: string;
|
|
43
|
+
stack?: string;
|
|
44
|
+
}> = [];
|
|
39
45
|
|
|
40
46
|
describe(name: string, fn: () => void): void {
|
|
41
47
|
const suite: TestSuite = {
|
|
@@ -71,56 +77,144 @@ export class TestRunner {
|
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
async run(): Promise<void> {
|
|
74
|
-
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
|
+
}
|
|
75
86
|
|
|
76
87
|
for (const suite of this.suites) {
|
|
77
|
-
console.log(`\
|
|
88
|
+
console.log(`\n📦 ${suite.name}`);
|
|
89
|
+
|
|
90
|
+
if (suite.tests.length === 0) {
|
|
91
|
+
console.log(" (no tests)");
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
78
94
|
|
|
79
95
|
for (const test of suite.tests) {
|
|
80
96
|
try {
|
|
97
|
+
// Run beforeEach hooks
|
|
81
98
|
for (const hook of suite.beforeEachHooks) {
|
|
82
99
|
await hook();
|
|
83
100
|
}
|
|
84
101
|
|
|
102
|
+
// Run the test
|
|
85
103
|
await test.fn();
|
|
86
104
|
|
|
105
|
+
// Run afterEach hooks
|
|
87
106
|
for (const hook of suite.afterEachHooks) {
|
|
88
107
|
await hook();
|
|
89
108
|
}
|
|
90
109
|
|
|
91
|
-
console.log(`
|
|
110
|
+
console.log(` ✓ ${test.name}`);
|
|
92
111
|
this.passed++;
|
|
93
112
|
} catch (error: any) {
|
|
94
|
-
console.log(`
|
|
113
|
+
console.log(` ✗ ${test.name}`);
|
|
95
114
|
console.error(` ${error?.message ?? error}`);
|
|
96
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
|
+
});
|
|
97
122
|
}
|
|
98
123
|
}
|
|
99
124
|
}
|
|
100
125
|
|
|
101
|
-
|
|
126
|
+
// Print summary
|
|
127
|
+
console.log(
|
|
128
|
+
`\n\n📊 Results: ${this.passed} passed, ${this.failed} failed\n`,
|
|
129
|
+
);
|
|
102
130
|
|
|
103
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
|
+
});
|
|
104
137
|
process.exit(1);
|
|
138
|
+
} else {
|
|
139
|
+
console.log("✅ All tests passed!\n");
|
|
140
|
+
process.exit(0);
|
|
105
141
|
}
|
|
106
142
|
}
|
|
107
143
|
|
|
108
144
|
async loadTestFiles(pattern: string): Promise<void> {
|
|
109
|
-
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;
|
|
110
156
|
|
|
111
157
|
for (const file of files) {
|
|
112
|
-
|
|
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
|
+
}
|
|
113
169
|
}
|
|
114
170
|
}
|
|
115
171
|
}
|
|
116
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
|
+
|
|
117
203
|
/* ======================================================
|
|
118
204
|
* Global test helpers
|
|
119
205
|
* ====================================================== */
|
|
120
206
|
export function describe(name: string, fn: () => void): void {
|
|
121
207
|
if (!G.__testRunner) {
|
|
122
|
-
|
|
208
|
+
// Try to auto-initialize
|
|
209
|
+
getTestRunner();
|
|
123
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
|
+
|
|
124
218
|
G.__testRunner.describe(name, fn);
|
|
125
219
|
}
|
|
126
220
|
|
|
@@ -176,21 +270,29 @@ export function expect(actual: any) {
|
|
|
176
270
|
}
|
|
177
271
|
},
|
|
178
272
|
|
|
179
|
-
toThrow() {
|
|
273
|
+
toThrow(expectedError?: string) {
|
|
180
274
|
if (typeof actual !== "function") {
|
|
181
275
|
throw new Error("toThrow expects a function");
|
|
182
276
|
}
|
|
183
277
|
|
|
184
278
|
let threw = false;
|
|
279
|
+
let error = null;
|
|
185
280
|
try {
|
|
186
281
|
actual();
|
|
187
|
-
} catch {
|
|
282
|
+
} catch (e: any) {
|
|
188
283
|
threw = true;
|
|
284
|
+
error = e;
|
|
189
285
|
}
|
|
190
286
|
|
|
191
287
|
if (!threw) {
|
|
192
288
|
throw new Error("Expected function to throw an error");
|
|
193
289
|
}
|
|
290
|
+
|
|
291
|
+
if (expectedError && error?.message !== expectedError) {
|
|
292
|
+
throw new Error(
|
|
293
|
+
`Expected error message "${expectedError}" but got "${error?.message}"`,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
194
296
|
},
|
|
195
297
|
|
|
196
298
|
toBeInstanceOf(expected: any) {
|
|
@@ -209,9 +311,15 @@ export function expect(actual: any) {
|
|
|
209
311
|
}
|
|
210
312
|
},
|
|
211
313
|
|
|
212
|
-
toHaveProperty(
|
|
213
|
-
if (actual == null || !(
|
|
214
|
-
throw new Error(`Expected object to have property "${
|
|
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
|
+
);
|
|
215
323
|
}
|
|
216
324
|
},
|
|
217
325
|
|
|
@@ -240,17 +348,24 @@ export function expect(actual: any) {
|
|
|
240
348
|
}
|
|
241
349
|
|
|
242
350
|
/* ======================================================
|
|
243
|
-
* CLI entry
|
|
351
|
+
* CLI entry (when this file is run directly)
|
|
244
352
|
* ====================================================== */
|
|
245
353
|
if (require.main === module) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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";
|
|
363
|
+
|
|
364
|
+
await runner.loadTestFiles(pattern);
|
|
365
|
+
await runner.run();
|
|
366
|
+
} catch (err: any) {
|
|
367
|
+
console.error("Failed to run tests:", err.message);
|
|
254
368
|
process.exit(1);
|
|
255
|
-
}
|
|
369
|
+
}
|
|
370
|
+
})();
|
|
256
371
|
}
|