fragment-ts 1.0.17 ā 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/SETUP.md +570 -0
- package/changes/1.md +469 -0
- package/dist/cli/commands/init.command.js +4 -4
- package/dist/cli/commands/init.command.js.map +1 -1
- package/dist/cli/commands/test.command.d.ts +6 -0
- package/dist/cli/commands/test.command.d.ts.map +1 -0
- package/dist/cli/commands/test.command.js +311 -0
- package/dist/cli/commands/test.command.js.map +1 -0
- package/dist/cli/index.js +6 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/core/container/di-container.d.ts +5 -0
- package/dist/core/container/di-container.d.ts.map +1 -1
- package/dist/core/container/di-container.js +121 -21
- package/dist/core/container/di-container.js.map +1 -1
- package/dist/core/decorators/controller.decorator.js +5 -5
- package/dist/core/decorators/injection.decorators.d.ts +44 -1
- package/dist/core/decorators/injection.decorators.d.ts.map +1 -1
- package/dist/core/decorators/injection.decorators.js +92 -1
- package/dist/core/decorators/injection.decorators.js.map +1 -1
- package/dist/core/metadata/metadata-keys.d.ts +29 -17
- package/dist/core/metadata/metadata-keys.d.ts.map +1 -1
- package/dist/core/metadata/metadata-keys.js +35 -17
- package/dist/core/metadata/metadata-keys.js.map +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 +4 -4
- package/src/cli/commands/test.command.ts +289 -0
- package/src/cli/index.ts +16 -11
- package/src/core/container/di-container.ts +166 -31
- package/src/core/decorators/DECORATOR_USAGE.md +326 -0
- package/src/core/decorators/controller.decorator.ts +9 -9
- package/src/core/decorators/injection.decorators.ts +129 -5
- package/src/core/metadata/metadata-keys.ts +44 -18
- package/src/testing/TEST.md +321 -0
- package/src/testing/runner.ts +137 -24
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// user.service.spec.ts
|
|
3
|
+
// Example test file for UserService
|
|
4
|
+
// ============================================
|
|
5
|
+
|
|
6
|
+
import 'reflect-metadata';
|
|
7
|
+
import { UserService } from './user.service';
|
|
8
|
+
import { UserRepository } from './user.repository';
|
|
9
|
+
import { EmailService } from './email.service';
|
|
10
|
+
import { DIContainer } from 'fragment-ts';
|
|
11
|
+
|
|
12
|
+
describe('UserService', () => {
|
|
13
|
+
let userService: UserService;
|
|
14
|
+
let userRepository: UserRepository;
|
|
15
|
+
let emailService: EmailService;
|
|
16
|
+
let container: DIContainer;
|
|
17
|
+
|
|
18
|
+
// Setup before each test
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
// Create a fresh DI container for each test
|
|
21
|
+
container = DIContainer.getInstance();
|
|
22
|
+
container.reset();
|
|
23
|
+
|
|
24
|
+
// Mock dependencies
|
|
25
|
+
userRepository = {
|
|
26
|
+
findAll: async () => [],
|
|
27
|
+
findById: async (id: number) => null,
|
|
28
|
+
findByEmail: async (email: string) => null,
|
|
29
|
+
create: async (data: any) => ({ id: 1, ...data }),
|
|
30
|
+
update: async (id: number, data: any) => null,
|
|
31
|
+
delete: async (id: number) => true,
|
|
32
|
+
} as any;
|
|
33
|
+
|
|
34
|
+
emailService = {
|
|
35
|
+
sendWelcomeEmail: async (email: string, name: string) => {},
|
|
36
|
+
sendPasswordReset: async (email: string, token: string) => {},
|
|
37
|
+
} as any;
|
|
38
|
+
|
|
39
|
+
// Register mocks in container
|
|
40
|
+
container.register(UserRepository, userRepository);
|
|
41
|
+
container.register(EmailService, emailService);
|
|
42
|
+
|
|
43
|
+
// Create service instance (will auto-inject mocks)
|
|
44
|
+
userService = container.resolve(UserService);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should get all users', async () => {
|
|
48
|
+
const mockUsers = [
|
|
49
|
+
{ id: 1, name: 'John', email: 'john@example.com', isActive: true },
|
|
50
|
+
{ id: 2, name: 'Jane', email: 'jane@example.com', isActive: true },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
userRepository.findAll = async () => mockUsers;
|
|
54
|
+
|
|
55
|
+
const users = await userService.getAllUsers();
|
|
56
|
+
|
|
57
|
+
expect(users).toEqual(mockUsers);
|
|
58
|
+
expect(users).toHaveLength(2);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should get user by id', async () => {
|
|
62
|
+
const mockUser = {
|
|
63
|
+
id: 1,
|
|
64
|
+
name: 'John',
|
|
65
|
+
email: 'john@example.com',
|
|
66
|
+
isActive: true,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
userRepository.findById = async (id: number) =>
|
|
70
|
+
id === 1 ? mockUser : null;
|
|
71
|
+
|
|
72
|
+
const user = await userService.getUserById(1);
|
|
73
|
+
|
|
74
|
+
expect(user).toEqual(mockUser);
|
|
75
|
+
expect(user?.name).toBe('John');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should create user and send welcome email', async () => {
|
|
79
|
+
const newUser = {
|
|
80
|
+
name: 'John',
|
|
81
|
+
email: 'john@example.com',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
let emailSent = false;
|
|
85
|
+
emailService.sendWelcomeEmail = async () => {
|
|
86
|
+
emailSent = true;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const user = await userService.createUser(newUser);
|
|
90
|
+
|
|
91
|
+
expect(user.name).toBe('John');
|
|
92
|
+
expect(user.email).toBe('john@example.com');
|
|
93
|
+
expect(emailSent).toBeTruthy();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should throw error when creating duplicate email', async () => {
|
|
97
|
+
const existingUser = {
|
|
98
|
+
id: 1,
|
|
99
|
+
name: 'Existing',
|
|
100
|
+
email: 'test@example.com',
|
|
101
|
+
isActive: true,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
userRepository.findByEmail = async (email: string) =>
|
|
105
|
+
email === 'test@example.com' ? existingUser : null;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
await userService.createUser({
|
|
109
|
+
name: 'New User',
|
|
110
|
+
email: 'test@example.com',
|
|
111
|
+
});
|
|
112
|
+
throw new Error('Should have thrown error');
|
|
113
|
+
} catch (error: any) {
|
|
114
|
+
expect(error.message).toContain('already exists');
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should update user', async () => {
|
|
119
|
+
const existingUser = {
|
|
120
|
+
id: 1,
|
|
121
|
+
name: 'John',
|
|
122
|
+
email: 'john@example.com',
|
|
123
|
+
isActive: true,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
userRepository.findById = async (id: number) =>
|
|
127
|
+
id === 1 ? existingUser : null;
|
|
128
|
+
userRepository.update = async (id: number, data: any) => ({
|
|
129
|
+
...existingUser,
|
|
130
|
+
...data,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const updated = await userService.updateUser(1, { name: 'John Updated' });
|
|
134
|
+
|
|
135
|
+
expect(updated?.name).toBe('John Updated');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should delete user', async () => {
|
|
139
|
+
const existingUser = {
|
|
140
|
+
id: 1,
|
|
141
|
+
name: 'John',
|
|
142
|
+
email: 'john@example.com',
|
|
143
|
+
isActive: true,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
userRepository.findById = async (id: number) =>
|
|
147
|
+
id === 1 ? existingUser : null;
|
|
148
|
+
|
|
149
|
+
const deleted = await userService.deleteUser(1);
|
|
150
|
+
|
|
151
|
+
expect(deleted).toBeTruthy();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should throw error when deleting non-existent user', async () => {
|
|
155
|
+
userRepository.findById = async () => null;
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
await userService.deleteUser(999);
|
|
159
|
+
throw new Error('Should have thrown error');
|
|
160
|
+
} catch (error: any) {
|
|
161
|
+
expect(error.message).toContain('not found');
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should request password reset', async () => {
|
|
166
|
+
const existingUser = {
|
|
167
|
+
id: 1,
|
|
168
|
+
name: 'John',
|
|
169
|
+
email: 'john@example.com',
|
|
170
|
+
isActive: true,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
userRepository.findByEmail = async (email: string) =>
|
|
174
|
+
email === 'john@example.com' ? existingUser : null;
|
|
175
|
+
|
|
176
|
+
let resetEmailSent = false;
|
|
177
|
+
let resetToken = '';
|
|
178
|
+
|
|
179
|
+
emailService.sendPasswordReset = async (email: string, token: string) => {
|
|
180
|
+
resetEmailSent = true;
|
|
181
|
+
resetToken = token;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
await userService.requestPasswordReset('john@example.com');
|
|
185
|
+
|
|
186
|
+
expect(resetEmailSent).toBeTruthy();
|
|
187
|
+
expect(resetToken).toBeTruthy();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// ============================================
|
|
192
|
+
// di-container.spec.ts
|
|
193
|
+
// Example test file for DI Container
|
|
194
|
+
// ============================================
|
|
195
|
+
|
|
196
|
+
describe('DIContainer', () => {
|
|
197
|
+
let container: DIContainer;
|
|
198
|
+
|
|
199
|
+
beforeEach(() => {
|
|
200
|
+
container = DIContainer.getInstance();
|
|
201
|
+
container.reset();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should resolve singleton instances', () => {
|
|
205
|
+
class TestService {
|
|
206
|
+
counter = 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
container.register(TestService);
|
|
210
|
+
|
|
211
|
+
const instance1 = container.resolve(TestService);
|
|
212
|
+
const instance2 = container.resolve(TestService);
|
|
213
|
+
|
|
214
|
+
instance1.counter = 5;
|
|
215
|
+
|
|
216
|
+
expect(instance2.counter).toBe(5);
|
|
217
|
+
expect(instance1).toBe(instance2);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should inject constructor dependencies', () => {
|
|
221
|
+
@Injectable()
|
|
222
|
+
class DependencyService {
|
|
223
|
+
getValue() {
|
|
224
|
+
return 'dependency';
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
@Injectable()
|
|
229
|
+
class TestService {
|
|
230
|
+
constructor(private dep: DependencyService) {}
|
|
231
|
+
|
|
232
|
+
getDepValue() {
|
|
233
|
+
return this.dep.getValue();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
container.register(DependencyService);
|
|
238
|
+
container.register(TestService);
|
|
239
|
+
|
|
240
|
+
const service = container.resolve(TestService);
|
|
241
|
+
|
|
242
|
+
expect(service.getDepValue()).toBe('dependency');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should inject properties with @Autowired', () => {
|
|
246
|
+
@Injectable()
|
|
247
|
+
class DependencyService {
|
|
248
|
+
getValue() {
|
|
249
|
+
return 'autowired';
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
@Injectable()
|
|
254
|
+
class TestService {
|
|
255
|
+
@Autowired()
|
|
256
|
+
private dep!: DependencyService;
|
|
257
|
+
|
|
258
|
+
getDepValue() {
|
|
259
|
+
return this.dep.getValue();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
container.register(DependencyService);
|
|
264
|
+
container.register(TestService);
|
|
265
|
+
|
|
266
|
+
const service = container.resolve(TestService);
|
|
267
|
+
|
|
268
|
+
expect(service.getDepValue()).toBe('autowired');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should inject values with @Value', () => {
|
|
272
|
+
process.env.TEST_VALUE = '42';
|
|
273
|
+
|
|
274
|
+
@Injectable()
|
|
275
|
+
class TestService {
|
|
276
|
+
@Value('${TEST_VALUE}')
|
|
277
|
+
private value!: number;
|
|
278
|
+
|
|
279
|
+
getValue() {
|
|
280
|
+
return this.value;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
container.register(TestService);
|
|
285
|
+
const service = container.resolve(TestService);
|
|
286
|
+
|
|
287
|
+
expect(service.getValue()).toBe(42);
|
|
288
|
+
|
|
289
|
+
delete process.env.TEST_VALUE;
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should detect circular dependencies', () => {
|
|
293
|
+
@Injectable()
|
|
294
|
+
class ServiceA {
|
|
295
|
+
constructor(private b: ServiceB) {}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
@Injectable()
|
|
299
|
+
class ServiceB {
|
|
300
|
+
constructor(private a: ServiceA) {}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
container.register(ServiceA);
|
|
304
|
+
container.register(ServiceB);
|
|
305
|
+
|
|
306
|
+
expect(() => container.resolve(ServiceA)).toThrow('Circular dependency');
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// ============================================
|
|
311
|
+
// Helper functions for testing
|
|
312
|
+
// ============================================
|
|
313
|
+
|
|
314
|
+
// Mock functions for testing
|
|
315
|
+
global.beforeEach = (fn: () => void) => {
|
|
316
|
+
// Implement beforeEach hook
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
global.afterEach = (fn: () => void) => {
|
|
320
|
+
// Implement afterEach hook
|
|
321
|
+
};
|
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
|
}
|