puty 0.0.3 โ†’ 0.0.4-rc2

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/README.md CHANGED
@@ -13,6 +13,7 @@ Puty is ideal for testing pure functions - functions that always return the same
13
13
  - [Testing Functions](#testing-functions)
14
14
  - [Testing Classes](#testing-classes)
15
15
  - [Error Testing](#error-testing)
16
+ - [Using Mocks](#using-mocks)
16
17
  - [Using !include Directive](#using-include-directive)
17
18
  - [YAML Structure](#yaml-structure)
18
19
 
@@ -21,6 +22,7 @@ Puty is ideal for testing pure functions - functions that always return the same
21
22
  - ๐Ÿ“ Write tests in simple YAML format
22
23
  - ๐Ÿ“ฆ Modular test organization with `!include` directive
23
24
  - ๐ŸŽฏ Clear separation of test data and test logic
25
+ - ๐Ÿงช Mock support for testing functions with dependencies
24
26
  - โšก Powered by Vitest for fast test execution
25
27
 
26
28
  ## Installation
@@ -186,12 +188,12 @@ executions:
186
188
  - `mode: 'class'` - Indicates this suite tests a class
187
189
  - `constructorArgs` - Arguments passed to the class constructor
188
190
  - `executions` - Array of method calls to execute in sequence
189
- - `method` - Name of the method to call
191
+ - `method` - Name of the method to call (supports nested: `user.api.getData`)
190
192
  - `in` - Arguments to pass to the method
191
193
  - `out` - Expected return value (optional)
192
194
  - `asserts` - Assertions to run after the method call
193
- - Property assertions: Check instance properties
194
- - Method assertions: Call methods and check their return values
195
+ - Property assertions: Check instance properties (supports nested: `user.profile.name`)
196
+ - Method assertions: Call methods and check their return values (supports nested: `settings.getTheme`)
195
197
 
196
198
  ### Error Testing
197
199
 
@@ -203,6 +205,79 @@ in: [10, 0]
203
205
  throws: "Division by zero"
204
206
  ```
205
207
 
208
+ ### Using Mocks
209
+
210
+ Puty supports mocking dependencies using the `$mock:` syntax. This is useful for testing functions that have external dependencies like loggers, API clients, or callbacks.
211
+
212
+ #### Basic Mock Example
213
+
214
+ ```yaml
215
+ file: './calculator.js'
216
+ group: calculator
217
+ ---
218
+ suite: calculate
219
+ exportName: calculateWithLogger
220
+ ---
221
+ case: test with mock logger
222
+ in:
223
+ - 10
224
+ - 5
225
+ - $mock:logger
226
+ out: 15
227
+ mocks:
228
+ logger:
229
+ calls:
230
+ - in: ['Calculating 10 + 5']
231
+ - in: ['Result: 15']
232
+ ```
233
+
234
+ #### Mock Hierarchy
235
+
236
+ Mocks can be defined at three levels (case overrides suite, suite overrides global):
237
+
238
+ ```yaml
239
+ file: './service.js'
240
+ group: service
241
+ mocks:
242
+ globalApi: # Global mock - available to all suites
243
+ calls:
244
+ - in: ['/default']
245
+ out: { status: 200 }
246
+ ---
247
+ suite: userService
248
+ mocks:
249
+ api: # Suite mock - available to all cases in this suite
250
+ calls:
251
+ - in: ['/users']
252
+ out: { users: [] }
253
+ ---
254
+ case: get user with mock
255
+ in: [123, $mock:api]
256
+ out: { id: 123, name: 'John' }
257
+ mocks:
258
+ api: # Case mock - overrides suite mock
259
+ calls:
260
+ - in: ['/users/123']
261
+ out: { id: 123, name: 'John' }
262
+ ```
263
+
264
+ #### Testing Callbacks
265
+
266
+ Mocks are perfect for testing event-driven code:
267
+
268
+ ```yaml
269
+ case: test event emitter
270
+ executions:
271
+ - method: on
272
+ in: ['data', $mock:callback]
273
+ - method: emit
274
+ in: ['data', 'hello']
275
+ mocks:
276
+ callback:
277
+ calls:
278
+ - in: ['hello']
279
+ ```
280
+
206
281
  ### Using !include Directive
207
282
 
208
283
  Puty supports the `!include` directive to modularize and reuse YAML test files. This is useful for:
@@ -271,6 +346,11 @@ Puty test files use multi-document YAML format with three types of documents:
271
346
  file: './module.js' # Required: Path to JS file (relative to YAML file)
272
347
  group: 'test-group' # Required: Test group name (or use 'name')
273
348
  suites: ['suite1', 'suite2'] # Optional: List of suites to define
349
+ mocks: # Optional: Global mocks available to all suites
350
+ mockName:
351
+ calls:
352
+ - in: [args]
353
+ out: result
274
354
  ```
275
355
 
276
356
  ### 2. Suite Definition Documents
@@ -280,6 +360,11 @@ suite: 'suiteName' # Required: Suite name
280
360
  exportName: 'functionName' # Optional: Export to test (defaults to suite name or 'default')
281
361
  mode: 'class' # Optional: Set to 'class' for class testing
282
362
  constructorArgs: [arg1] # Optional: Arguments for class constructor (class mode only)
363
+ mocks: # Optional: Suite-level mocks for all cases in this suite
364
+ mockName:
365
+ calls:
366
+ - in: [args]
367
+ out: result
283
368
  ```
284
369
 
285
370
  ### 3. Test Case Documents
@@ -287,25 +372,58 @@ constructorArgs: [arg1] # Optional: Arguments for class constructor (class mo
287
372
  For function tests:
288
373
  ```yaml
289
374
  case: 'test description' # Required: Test case name
290
- in: [arg1, arg2] # Required: Input arguments
375
+ in: [arg1, arg2] # Required: Input arguments (use $mock:name for mocks)
291
376
  out: expectedValue # Optional: Expected output (omit if testing for errors)
292
377
  throws: 'Error message' # Optional: Expected error message
378
+ mocks: # Optional: Case-specific mocks
379
+ mockName:
380
+ calls: # Array of expected calls
381
+ - in: [args] # Expected arguments
382
+ out: result # Optional: Return value
383
+ throws: 'error' # Optional: Throw error instead
293
384
  ```
294
385
 
295
386
  For class tests:
296
387
  ```yaml
297
388
  case: 'test description'
298
389
  executions:
299
- - method: 'methodName'
390
+ - method: 'methodName' # Supports nested: 'user.api.getData'
300
391
  in: [arg1]
301
- out: expectedValue # Optional
302
- throws: 'Error msg' # Optional
392
+ out: expectedValue # Optional
393
+ throws: 'Error msg' # Optional
303
394
  asserts:
304
- - property: 'prop'
305
- op: 'eq' # Currently only 'eq' is supported
395
+ - property: 'prop' # Supports nested: 'user.profile.name'
396
+ op: 'eq' # Currently only 'eq' is supported
306
397
  value: expected
307
- - method: 'getter'
398
+ - method: 'getter' # Supports nested: 'settings.ui.getTheme'
308
399
  in: []
309
400
  out: expected
401
+ mocks: # Optional: Mocks for the entire test case
402
+ mockName:
403
+ calls:
404
+ - in: [args]
405
+ out: result
406
+ ```
407
+
408
+ #### Nested Properties and Methods
409
+
410
+ Puty supports accessing nested properties and calling nested methods using dot notation:
411
+
412
+ ```yaml
413
+ case: 'test nested access'
414
+ executions:
415
+ - method: 'settings.ui.setTheme' # Call nested method
416
+ in: ['dark']
417
+ out: 'dark'
418
+ asserts:
419
+ - property: 'user.profile.name' # Access nested property
420
+ op: eq
421
+ value: 'John Doe'
422
+ - property: 'user.account.balance' # Deep nested property
423
+ op: eq
424
+ value: 100.50
425
+ - method: 'api.client.get' # Call nested method
426
+ in: ['/users/123']
427
+ out: 'GET /users/123'
310
428
  ```
311
429
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "puty",
3
- "version": "0.0.3",
3
+ "version": "0.0.4-rc2",
4
4
  "description": "A tooling function to test javascript functions and classes.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -16,6 +16,8 @@
16
16
  "js-yaml": "~4.1.0"
17
17
  },
18
18
  "scripts": {
19
+ "test": "vitest run",
20
+ "test:watch": "vitest run",
19
21
  "lint": "bunx prettier src tests -c",
20
22
  "lint:fix": "bunx prettier src tests -w",
21
23
  "build": "bun run esbuild.js"
@@ -0,0 +1,152 @@
1
+ /**
2
+ * @fileoverview Mock resolution and processing utilities
3
+ * This module provides functions for resolving mock references, creating mock functions,
4
+ * and validating mock calls in YAML-driven tests.
5
+ */
6
+
7
+ import { vi } from "vitest";
8
+
9
+ /**
10
+ * Deep equality check for mock argument validation
11
+ * @param {any} a - First value to compare
12
+ * @param {any} b - Second value to compare
13
+ * @returns {boolean} True if values are deeply equal
14
+ */
15
+ const deepEqual = (a, b) => {
16
+ if (a === b) return true;
17
+ if (a == null || b == null) return false;
18
+ if (Array.isArray(a) && Array.isArray(b)) {
19
+ if (a.length !== b.length) return false;
20
+ for (let i = 0; i < a.length; i++) {
21
+ if (!deepEqual(a[i], b[i])) return false;
22
+ }
23
+ return true;
24
+ }
25
+ if (typeof a === "object" && typeof b === "object") {
26
+ const keysA = Object.keys(a);
27
+ const keysB = Object.keys(b);
28
+ if (keysA.length !== keysB.length) return false;
29
+ for (const key of keysA) {
30
+ if (!keysB.includes(key) || !deepEqual(a[key], b[key])) return false;
31
+ }
32
+ return true;
33
+ }
34
+ return false;
35
+ };
36
+
37
+ /**
38
+ * Resolves mock references following hierarchy: case -> suite -> global
39
+ * @param {Object} caseMocks - Case-level mock definitions
40
+ * @param {Object} suiteMocks - Suite-level mock definitions
41
+ * @param {Object} globalMocks - Global-level mock definitions
42
+ * @returns {Object} Resolved mock definitions with hierarchy applied
43
+ */
44
+ export const resolveMocks = (caseMocks = {}, suiteMocks = {}, globalMocks = {}) => {
45
+ const resolved = {};
46
+
47
+ // Apply hierarchy: global -> suite -> case (case overrides suite, suite overrides global)
48
+ Object.assign(resolved, globalMocks, suiteMocks, caseMocks);
49
+
50
+ return resolved;
51
+ };
52
+
53
+ /**
54
+ * Recursively processes values, replacing $mock: references with mock functions
55
+ * @param {any} value - Value to process (can be string, array, object, or primitive)
56
+ * @param {Object} mockFunctions - Map of mock name to mock function wrapper
57
+ * @returns {any} Processed value with $mock: references replaced
58
+ */
59
+ export const processMockReferences = (value, mockFunctions) => {
60
+ if (typeof value === 'string' && value.startsWith('$mock:')) {
61
+ const mockName = value.substring(6); // Remove '$mock:' prefix
62
+ if (!mockFunctions[mockName]) {
63
+ throw new Error(`Mock '${mockName}' is referenced but not defined`);
64
+ }
65
+ return mockFunctions[mockName].mockFunction;
66
+ }
67
+
68
+ if (Array.isArray(value)) {
69
+ return value.map(item => processMockReferences(item, mockFunctions));
70
+ }
71
+
72
+ if (value && typeof value === 'object') {
73
+ const processed = {};
74
+ for (const [key, val] of Object.entries(value)) {
75
+ processed[key] = processMockReferences(val, mockFunctions);
76
+ }
77
+ return processed;
78
+ }
79
+
80
+ return value;
81
+ };
82
+
83
+ /**
84
+ * Creates a mock function with call tracking and validation
85
+ * @param {string} mockName - Name of the mock for error reporting
86
+ * @param {Object} mockDefinition - Mock definition with calls array
87
+ * @param {Array} mockDefinition.calls - Array of expected calls with in/out/throws
88
+ * @returns {Object} Mock function wrapper with validation methods
89
+ */
90
+ export const createMockFunction = (mockName, mockDefinition) => {
91
+ const { calls } = mockDefinition;
92
+ let callIndex = 0;
93
+
94
+ const mockFn = vi.fn().mockImplementation((...args) => {
95
+ if (callIndex >= calls.length) {
96
+ throw new Error(`Mock '${mockName}' was called ${callIndex + 1} time(s) but expected exactly ${calls.length} calls`);
97
+ }
98
+
99
+ const expectedCall = calls[callIndex];
100
+
101
+ // Validate input arguments
102
+ if (!deepEqual(args, expectedCall.in)) {
103
+ throw new Error(`Expected ${mockName}(${JSON.stringify(expectedCall.in)}) but got ${mockName}(${JSON.stringify(args)})`);
104
+ }
105
+
106
+ callIndex++;
107
+
108
+ if (expectedCall.throws) {
109
+ throw new Error(expectedCall.throws);
110
+ }
111
+
112
+ return expectedCall.out;
113
+ });
114
+
115
+ return {
116
+ mockFunction: mockFn,
117
+ expectedCalls: calls.length,
118
+ actualCalls: () => callIndex,
119
+ validate: () => {
120
+ if (callIndex !== calls.length) {
121
+ throw new Error(`Mock '${mockName}' was called ${callIndex} time(s) but expected exactly ${calls.length} calls`);
122
+ }
123
+ },
124
+ mockName
125
+ };
126
+ };
127
+
128
+ /**
129
+ * Validates all mocks were called as expected
130
+ * @param {Object} mockFunctions - Map of mock name to mock function wrapper
131
+ * @throws {Error} If any mock validation fails
132
+ */
133
+ export const validateMockCalls = (mockFunctions) => {
134
+ for (const mockWrapper of Object.values(mockFunctions)) {
135
+ mockWrapper.validate();
136
+ }
137
+ };
138
+
139
+ /**
140
+ * Creates mock functions from resolved mock definitions
141
+ * @param {Object} resolvedMocks - Resolved mock definitions
142
+ * @returns {Object} Map of mock name to mock function wrapper
143
+ */
144
+ export const createMockFunctions = (resolvedMocks) => {
145
+ const mockFunctions = {};
146
+
147
+ for (const [mockName, mockDef] of Object.entries(resolvedMocks)) {
148
+ mockFunctions[mockName] = createMockFunction(mockName, mockDef);
149
+ }
150
+
151
+ return mockFunctions;
152
+ };
package/src/puty.js CHANGED
@@ -9,6 +9,71 @@ import yaml from "js-yaml";
9
9
  import { expect, test, describe } from "vitest";
10
10
 
11
11
  import { traverseAllFiles, parseWithIncludes } from "./utils.js";
12
+ import { resolveMocks, processMockReferences, createMockFunctions, validateMockCalls } from "./mockResolver.js";
13
+
14
+ /**
15
+ * Resolves a nested property path on an object (e.g., "user.profile.name")
16
+ * @param {Object} obj - The object to traverse
17
+ * @param {string} path - The property path (e.g., "user.profile.name")
18
+ * @returns {any} The value at the path
19
+ * @throws {Error} If any part of the path doesn't exist
20
+ */
21
+ const getNestedProperty = (obj, path) => {
22
+ const parts = path.split('.');
23
+ let current = obj;
24
+
25
+ for (let i = 0; i < parts.length; i++) {
26
+ if (current == null) {
27
+ throw new Error(`Cannot access property '${parts[i]}' of ${current} in path '${path}'`);
28
+ }
29
+ if (!(parts[i] in current)) {
30
+ throw new Error(`Property '${parts[i]}' not found in path '${path}'`);
31
+ }
32
+ current = current[parts[i]];
33
+ }
34
+
35
+ return current;
36
+ };
37
+
38
+ /**
39
+ * Resolves a nested method path and calls it (e.g., "user.api.getData")
40
+ * @param {Object} obj - The object to traverse
41
+ * @param {string} path - The method path (e.g., "user.api.getData")
42
+ * @param {Array} args - Arguments to pass to the method
43
+ * @returns {any} The result of the method call
44
+ * @throws {Error} If any part of the path doesn't exist or final part is not a function
45
+ */
46
+ const callNestedMethod = (obj, path, args = []) => {
47
+ const parts = path.split('.');
48
+ const methodName = parts.pop();
49
+
50
+ let current = obj;
51
+ const parentPath = parts.join('.');
52
+
53
+ // Navigate to the parent object
54
+ for (const part of parts) {
55
+ if (current == null) {
56
+ throw new Error(`Cannot access property '${part}' of ${current} in path '${path}'`);
57
+ }
58
+ if (!(part in current)) {
59
+ throw new Error(`Property '${part}' not found in path '${path}'`);
60
+ }
61
+ current = current[part];
62
+ }
63
+
64
+ // Check if the method exists and is a function
65
+ if (current == null) {
66
+ throw new Error(`Cannot access method '${methodName}' of ${current} in path '${path}'`);
67
+ }
68
+ if (!(methodName in current)) {
69
+ throw new Error(`Method '${methodName}' not found in path '${path}'`);
70
+ }
71
+ if (typeof current[methodName] !== 'function') {
72
+ throw new Error(`'${methodName}' is not a function in path '${path}'`);
73
+ }
74
+
75
+ return current[methodName](...args);
76
+ };
12
77
 
13
78
  /**
14
79
  * File extensions that are recognized as YAML test files
@@ -43,6 +108,7 @@ export const parseYamlDocuments = (yamlContent) => {
43
108
  const config = {
44
109
  file: null,
45
110
  group: null,
111
+ mocks: {},
46
112
  suites: [],
47
113
  };
48
114
 
@@ -52,6 +118,7 @@ export const parseYamlDocuments = (yamlContent) => {
52
118
  if (doc.file) {
53
119
  config.file = doc.file;
54
120
  config.group = doc.group || doc.name;
121
+ config.mocks = doc.mocks || {};
55
122
  if (doc.suites) {
56
123
  config.suiteNames = doc.suites;
57
124
  }
@@ -62,6 +129,7 @@ export const parseYamlDocuments = (yamlContent) => {
62
129
  currentSuite = {
63
130
  name: doc.suite,
64
131
  exportName: doc.exportName || doc.suite,
132
+ mocks: doc.mocks || {},
65
133
  cases: [],
66
134
  };
67
135
  // Only add mode and constructorArgs if mode is explicitly 'class'
@@ -72,6 +140,8 @@ export const parseYamlDocuments = (yamlContent) => {
72
140
  } else if (doc.case && currentSuite) {
73
141
  const testCase = {
74
142
  name: doc.case,
143
+ mocks: doc.mocks || {},
144
+ resolvedMocks: null,
75
145
  };
76
146
 
77
147
  if (currentSuite.mode === "class") {
@@ -149,18 +219,31 @@ const setupFunctionTests = (suite) => {
149
219
  out: expectedOut,
150
220
  functionUnderTest,
151
221
  throws,
222
+ mockFunctions,
152
223
  } = testCase;
153
224
  test(name, () => {
154
225
  if (!functionUnderTest) {
155
226
  throw new Error(`Function not found for test case: ${name}`);
156
227
  }
157
228
 
158
- if (throws) {
159
- // Test expects an error to be thrown
160
- expect(() => functionUnderTest(...(inArg || []))).toThrow(throws);
161
- } else {
162
- const out = functionUnderTest(...(inArg || []));
163
- expect(out).toEqual(expectedOut);
229
+ try {
230
+ if (throws) {
231
+ // Test expects an error to be thrown
232
+ expect(() => functionUnderTest(...(inArg || []))).toThrow(throws);
233
+ } else {
234
+ const out = functionUnderTest(...(inArg || []));
235
+ expect(out).toEqual(expectedOut);
236
+ }
237
+
238
+ // Validate mock calls after test execution
239
+ if (mockFunctions && Object.keys(mockFunctions).length > 0) {
240
+ validateMockCalls(mockFunctions);
241
+ }
242
+ } finally {
243
+ // Cleanup mocks after test
244
+ if (mockFunctions) {
245
+ Object.values(mockFunctions).forEach(mock => mock.mockFunction.mockClear?.());
246
+ }
164
247
  }
165
248
  });
166
249
  }
@@ -178,7 +261,7 @@ const setupFunctionTests = (suite) => {
178
261
  const setupClassTests = (suite) => {
179
262
  const { cases, ClassUnderTest, constructorArgs } = suite;
180
263
  for (const testCase of cases) {
181
- const { name, executions } = testCase;
264
+ const { name, executions, mockFunctions } = testCase;
182
265
  test(name, () => {
183
266
  if (!ClassUnderTest) {
184
267
  throw new Error(`Class not found for test suite: ${suite.name}`);
@@ -186,57 +269,54 @@ const setupClassTests = (suite) => {
186
269
 
187
270
  const instance = new ClassUnderTest(...constructorArgs);
188
271
 
189
- for (const execution of executions) {
190
- const {
191
- method,
192
- in: inArg,
193
- out: expectedOut,
194
- throws,
195
- asserts,
196
- } = execution;
197
-
198
- // Validate method exists
199
- if (!instance[method] || typeof instance[method] !== "function") {
200
- throw new Error(`Method '${method}' not found on class instance`);
201
- }
272
+ try {
273
+ for (const execution of executions) {
274
+ const {
275
+ method,
276
+ in: inArg,
277
+ out: expectedOut,
278
+ throws,
279
+ asserts,
280
+ } = execution;
202
281
 
203
- // Execute the method and check its return value
204
- if (throws) {
205
- expect(() => instance[method](...(inArg || []))).toThrow(throws);
206
- } else {
207
- const result = instance[method](...(inArg || []));
208
- if (expectedOut !== undefined) {
209
- expect(result).toEqual(expectedOut);
282
+ // Execute the method and check its return value - supports nested methods
283
+ if (throws) {
284
+ expect(() => callNestedMethod(instance, method, inArg || [])).toThrow(throws);
285
+ } else {
286
+ const result = callNestedMethod(instance, method, inArg || []);
287
+ if (expectedOut !== undefined) {
288
+ expect(result).toEqual(expectedOut);
289
+ }
210
290
  }
211
- }
212
291
 
213
- // Run assertions
214
- if (asserts) {
215
- for (const assertion of asserts) {
216
- if (assertion.property) {
217
- // Property assertion
218
- const actualValue = instance[assertion.property];
219
- if (assertion.op === "eq") {
220
- expect(actualValue).toEqual(assertion.value);
221
- }
222
- // Add more operators as needed
223
- } else if (assertion.method) {
224
- // Method assertion
225
- if (
226
- !instance[assertion.method] ||
227
- typeof instance[assertion.method] !== "function"
228
- ) {
229
- throw new Error(
230
- `Method '${assertion.method}' not found on class instance for assertion`,
231
- );
292
+ // Run assertions
293
+ if (asserts) {
294
+ for (const assertion of asserts) {
295
+ if (assertion.property) {
296
+ // Property assertion - supports nested properties like "user.profile.name"
297
+ const actualValue = getNestedProperty(instance, assertion.property);
298
+ if (assertion.op === "eq") {
299
+ expect(actualValue).toEqual(assertion.value);
300
+ }
301
+ // Add more operators as needed
302
+ } else if (assertion.method) {
303
+ // Method assertion - supports nested methods like "user.api.getData"
304
+ const result = callNestedMethod(instance, assertion.method, assertion.in || []);
305
+ expect(result).toEqual(assertion.out);
232
306
  }
233
- const result = instance[assertion.method](
234
- ...(assertion.in || []),
235
- );
236
- expect(result).toEqual(assertion.out);
237
307
  }
238
308
  }
239
309
  }
310
+
311
+ // Validate mock calls after test execution
312
+ if (mockFunctions && Object.keys(mockFunctions).length > 0) {
313
+ validateMockCalls(mockFunctions);
314
+ }
315
+ } finally {
316
+ // Cleanup mocks after test
317
+ if (mockFunctions) {
318
+ Object.values(mockFunctions).forEach(mock => mock.mockFunction.mockClear?.());
319
+ }
240
320
  }
241
321
  });
242
322
  }
@@ -262,6 +342,38 @@ export const injectFunctions = (module, originalTestConfig) => {
262
342
  let functionUnderTest = module[testConfig.exportName || "default"];
263
343
 
264
344
  for (const suite of testConfig.suites) {
345
+ for (const testCase of suite.cases) {
346
+ // Resolve mocks for this test case using hierarchy
347
+ testCase.resolvedMocks = resolveMocks(
348
+ testCase.mocks,
349
+ suite.mocks,
350
+ testConfig.mocks
351
+ );
352
+
353
+ // Create mock functions from resolved mock definitions
354
+ testCase.mockFunctions = createMockFunctions(testCase.resolvedMocks);
355
+
356
+ // Process mock references in test inputs and outputs
357
+ if (testCase.in) {
358
+ testCase.in = processMockReferences(testCase.in, testCase.mockFunctions);
359
+ }
360
+ if (testCase.out) {
361
+ testCase.out = processMockReferences(testCase.out, testCase.mockFunctions);
362
+ }
363
+
364
+ // Process mock references in class test executions
365
+ if (testCase.executions) {
366
+ for (const execution of testCase.executions) {
367
+ if (execution.in) {
368
+ execution.in = processMockReferences(execution.in, testCase.mockFunctions);
369
+ }
370
+ if (execution.out) {
371
+ execution.out = processMockReferences(execution.out, testCase.mockFunctions);
372
+ }
373
+ }
374
+ }
375
+ }
376
+
265
377
  if (suite.mode === "class") {
266
378
  const exportName = suite.exportName || "default";
267
379
  const exported = module[exportName];
@@ -305,15 +417,19 @@ export const injectFunctions = (module, originalTestConfig) => {
305
417
  export const setupTestSuiteFromYaml = async (dirname) => {
306
418
  const testYamlFiles = traverseAllFiles(dirname, extensions);
307
419
  for (const file of testYamlFiles) {
308
- const testConfig = parseWithIncludes(file);
309
- const filepathRelativeToSpecFile = path.join(
310
- path.dirname(file),
311
- testConfig.file,
312
- );
420
+ try {
421
+ const testConfig = parseWithIncludes(file);
422
+ const filepathRelativeToSpecFile = path.join(
423
+ path.dirname(file),
424
+ testConfig.file,
425
+ );
313
426
 
314
- // testConfig.file is relative to the spec file
315
- const module = await import(filepathRelativeToSpecFile);
316
- const testConfigWithInjectedFunctions = injectFunctions(module, testConfig);
317
- setupTestSuite(testConfigWithInjectedFunctions);
427
+ // testConfig.file is relative to the spec file
428
+ const module = await import(filepathRelativeToSpecFile);
429
+ const testConfigWithInjectedFunctions = injectFunctions(module, testConfig);
430
+ setupTestSuite(testConfigWithInjectedFunctions);
431
+ } catch (error) {
432
+ throw error;
433
+ }
318
434
  }
319
435
  };
package/src/utils.js CHANGED
@@ -149,6 +149,7 @@ const processDocuments = (docs) => {
149
149
  const config = {
150
150
  file: null,
151
151
  group: null,
152
+ mocks: {},
152
153
  suites: [],
153
154
  };
154
155
 
@@ -158,6 +159,7 @@ const processDocuments = (docs) => {
158
159
  if (doc.file) {
159
160
  config.file = doc.file;
160
161
  config.group = doc.group || doc.name;
162
+ config.mocks = doc.mocks || {};
161
163
  if (doc.suites) {
162
164
  config.suiteNames = doc.suites;
163
165
  }
@@ -168,6 +170,7 @@ const processDocuments = (docs) => {
168
170
  currentSuite = {
169
171
  name: doc.suite,
170
172
  exportName: doc.exportName || doc.suite,
173
+ mocks: doc.mocks || {},
171
174
  cases: [],
172
175
  };
173
176
  // Only add mode and constructorArgs if mode is explicitly 'class'
@@ -178,6 +181,8 @@ const processDocuments = (docs) => {
178
181
  } else if (doc.case && currentSuite) {
179
182
  const testCase = {
180
183
  name: doc.case,
184
+ mocks: doc.mocks || {},
185
+ resolvedMocks: null,
181
186
  };
182
187
 
183
188
  if (currentSuite.mode === "class") {