puty 0.0.2 → 0.0.3
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 +197 -8
- package/package.json +1 -1
- package/src/index.js +9 -0
- package/src/puty.js +89 -12
- package/src/utils.js +200 -7
package/README.md
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
|
-
# Puty: Pure Unit Test in
|
|
1
|
+
# Puty: Pure Unit Test in YAML
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Puty is a declarative testing framework that allows you to write unit tests using YAML files instead of JavaScript code. It's built on top of [Vitest](https://vitest.dev/) and designed to make testing more accessible and maintainable by separating test data from test logic.
|
|
4
|
+
|
|
5
|
+
Puty is ideal for testing pure functions - functions that always return the same output for the same input and have no side effects. The declarative YAML format perfectly captures the essence of pure function testing: given these inputs, expect this output.
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Features](#features)
|
|
10
|
+
- [Installation](#installation)
|
|
11
|
+
- [Quick Start](#quick-start)
|
|
12
|
+
- [Usage](#usage)
|
|
13
|
+
- [Testing Functions](#testing-functions)
|
|
14
|
+
- [Testing Classes](#testing-classes)
|
|
15
|
+
- [Error Testing](#error-testing)
|
|
16
|
+
- [Using !include Directive](#using-include-directive)
|
|
17
|
+
- [YAML Structure](#yaml-structure)
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- 📝 Write tests in simple YAML format
|
|
22
|
+
- 📦 Modular test organization with `!include` directive
|
|
23
|
+
- 🎯 Clear separation of test data and test logic
|
|
24
|
+
- ⚡ Powered by Vitest for fast test execution
|
|
4
25
|
|
|
5
26
|
## Installation
|
|
6
27
|
|
|
@@ -8,26 +29,71 @@ Write unit test specifications using YAML files, powered by vitest as the test r
|
|
|
8
29
|
npm install puty
|
|
9
30
|
```
|
|
10
31
|
|
|
11
|
-
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
1. Create a test runner file `puty.test.js` in your project:
|
|
36
|
+
|
|
12
37
|
```js
|
|
13
|
-
import { setupTestSuiteFromYaml } from "
|
|
38
|
+
import { setupTestSuiteFromYaml } from "puty";
|
|
39
|
+
|
|
40
|
+
// Search for test files in the current directory
|
|
14
41
|
await setupTestSuiteFromYaml();
|
|
42
|
+
|
|
43
|
+
// Or specify a different directory
|
|
44
|
+
// await setupTestSuiteFromYaml("./tests");
|
|
15
45
|
```
|
|
16
46
|
|
|
17
|
-
|
|
47
|
+
**Note:** Puty uses ES module imports and requires your project to support ES modules. If you're using Node.js, make sure to add `"type": "module"` to your `package.json` or use `.mjs` file extensions.
|
|
18
48
|
|
|
19
|
-
run
|
|
20
49
|
|
|
50
|
+
2. Create your first test file `math.test.yaml`:
|
|
51
|
+
|
|
52
|
+
```yaml
|
|
53
|
+
file: './math.js'
|
|
54
|
+
group: math
|
|
55
|
+
suites: [add]
|
|
56
|
+
---
|
|
57
|
+
suite: add
|
|
58
|
+
exportName: add
|
|
59
|
+
---
|
|
60
|
+
case: add two numbers
|
|
61
|
+
in: [2, 3]
|
|
62
|
+
out: 5
|
|
21
63
|
```
|
|
22
|
-
|
|
64
|
+
|
|
65
|
+
3. Run your tests:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npx vitest
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Recommended Vitest Configuration
|
|
72
|
+
|
|
73
|
+
To enable automatic test reruns when YAML test files change, create a `vitest.config.js` file in your project root:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
import { defineConfig } from 'vitest/config'
|
|
77
|
+
|
|
78
|
+
export default defineConfig({
|
|
79
|
+
test: {
|
|
80
|
+
forceRerunTriggers: [
|
|
81
|
+
'**/*.js',
|
|
82
|
+
'**/*.{test,spec}.yaml',
|
|
83
|
+
'**/*.{test,spec}.yml'
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
});
|
|
23
87
|
```
|
|
24
88
|
|
|
25
|
-
|
|
89
|
+
This configuration ensures that Vitest will re-run your tests whenever you modify either your JavaScript source files or your YAML test files.
|
|
26
90
|
|
|
27
91
|
## Usage
|
|
28
92
|
|
|
29
93
|
### Testing Functions
|
|
30
94
|
|
|
95
|
+
Here's a complete example of testing JavaScript functions with Puty:
|
|
96
|
+
|
|
31
97
|
```yaml
|
|
32
98
|
file: './math.js'
|
|
33
99
|
group: math
|
|
@@ -64,6 +130,22 @@ in:
|
|
|
64
130
|
out: 3
|
|
65
131
|
```
|
|
66
132
|
|
|
133
|
+
This under the hood creates a test structure in Vitest like:
|
|
134
|
+
```js
|
|
135
|
+
describe('math', () => {
|
|
136
|
+
describe('add', () => {
|
|
137
|
+
it('add 1 and 2', () => { ... })
|
|
138
|
+
it('add 2 and 2', () => { ... })
|
|
139
|
+
})
|
|
140
|
+
describe('increment', () => {
|
|
141
|
+
it('increment 1', () => { ... })
|
|
142
|
+
it('increment 2', () => { ... })
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
See the [YAML Structure](#yaml-structure) section for detailed documentation of all available fields.
|
|
148
|
+
|
|
67
149
|
### Testing Classes
|
|
68
150
|
|
|
69
151
|
Puty also supports testing classes with method calls and state assertions:
|
|
@@ -120,3 +202,110 @@ case: divide by zero
|
|
|
120
202
|
in: [10, 0]
|
|
121
203
|
throws: "Division by zero"
|
|
122
204
|
```
|
|
205
|
+
|
|
206
|
+
### Using !include Directive
|
|
207
|
+
|
|
208
|
+
Puty supports the `!include` directive to modularize and reuse YAML test files. This is useful for:
|
|
209
|
+
- Sharing common test data across multiple test files
|
|
210
|
+
- Organizing large test suites into smaller, manageable files
|
|
211
|
+
- Reusing test cases for different modules
|
|
212
|
+
|
|
213
|
+
#### Basic Usage
|
|
214
|
+
|
|
215
|
+
You can include entire YAML documents:
|
|
216
|
+
|
|
217
|
+
```yaml
|
|
218
|
+
file: "./math.js"
|
|
219
|
+
group: math-tests
|
|
220
|
+
suites: [add]
|
|
221
|
+
---
|
|
222
|
+
!include ./suite-definition.yaml
|
|
223
|
+
---
|
|
224
|
+
!include ./test-cases.yaml
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
#### Including Values
|
|
228
|
+
|
|
229
|
+
You can also include specific values within a YAML document:
|
|
230
|
+
|
|
231
|
+
```yaml
|
|
232
|
+
case: test with shared data
|
|
233
|
+
in: !include ./test-data/input.yaml
|
|
234
|
+
out: !include ./test-data/expected-output.yaml
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
#### Recursive Includes
|
|
238
|
+
|
|
239
|
+
The `!include` directive supports recursive includes, allowing included files to include other files:
|
|
240
|
+
|
|
241
|
+
```yaml
|
|
242
|
+
# main.yaml
|
|
243
|
+
!include ./level1.yaml
|
|
244
|
+
|
|
245
|
+
# level1.yaml
|
|
246
|
+
suite: test
|
|
247
|
+
---
|
|
248
|
+
!include ./level2.yaml
|
|
249
|
+
|
|
250
|
+
# level2.yaml
|
|
251
|
+
case: nested test
|
|
252
|
+
in: []
|
|
253
|
+
out: "success"
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
#### Important Notes
|
|
257
|
+
|
|
258
|
+
- File paths in `!include` are relative to the YAML file containing the directive
|
|
259
|
+
- Circular dependencies are detected and will cause an error
|
|
260
|
+
- Missing include files will result in a clear error message
|
|
261
|
+
- Both single documents and multi-document YAML files can be included
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
## YAML Structure
|
|
265
|
+
|
|
266
|
+
Puty test files use multi-document YAML format with three types of documents:
|
|
267
|
+
|
|
268
|
+
### 1. Configuration Document (First document)
|
|
269
|
+
|
|
270
|
+
```yaml
|
|
271
|
+
file: './module.js' # Required: Path to JS file (relative to YAML file)
|
|
272
|
+
group: 'test-group' # Required: Test group name (or use 'name')
|
|
273
|
+
suites: ['suite1', 'suite2'] # Optional: List of suites to define
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### 2. Suite Definition Documents
|
|
277
|
+
|
|
278
|
+
```yaml
|
|
279
|
+
suite: 'suiteName' # Required: Suite name
|
|
280
|
+
exportName: 'functionName' # Optional: Export to test (defaults to suite name or 'default')
|
|
281
|
+
mode: 'class' # Optional: Set to 'class' for class testing
|
|
282
|
+
constructorArgs: [arg1] # Optional: Arguments for class constructor (class mode only)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### 3. Test Case Documents
|
|
286
|
+
|
|
287
|
+
For function tests:
|
|
288
|
+
```yaml
|
|
289
|
+
case: 'test description' # Required: Test case name
|
|
290
|
+
in: [arg1, arg2] # Required: Input arguments
|
|
291
|
+
out: expectedValue # Optional: Expected output (omit if testing for errors)
|
|
292
|
+
throws: 'Error message' # Optional: Expected error message
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
For class tests:
|
|
296
|
+
```yaml
|
|
297
|
+
case: 'test description'
|
|
298
|
+
executions:
|
|
299
|
+
- method: 'methodName'
|
|
300
|
+
in: [arg1]
|
|
301
|
+
out: expectedValue # Optional
|
|
302
|
+
throws: 'Error msg' # Optional
|
|
303
|
+
asserts:
|
|
304
|
+
- property: 'prop'
|
|
305
|
+
op: 'eq' # Currently only 'eq' is supported
|
|
306
|
+
value: expected
|
|
307
|
+
- method: 'getter'
|
|
308
|
+
in: []
|
|
309
|
+
out: expected
|
|
310
|
+
```
|
|
311
|
+
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Main entry point for the puty testing framework
|
|
3
|
+
* Exports the primary function for setting up YAML-driven test suites.
|
|
4
|
+
*/
|
|
5
|
+
|
|
1
6
|
import { setupTestSuiteFromYaml } from "./puty.js";
|
|
2
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Main entry point function for discovering and setting up test suites from YAML files
|
|
10
|
+
* @see {@link setupTestSuiteFromYaml} for detailed documentation
|
|
11
|
+
*/
|
|
3
12
|
export { setupTestSuiteFromYaml };
|
package/src/puty.js
CHANGED
|
@@ -1,14 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Main test framework functionality for YAML-driven testing
|
|
3
|
+
* This module provides functions for setting up test suites from YAML configurations,
|
|
4
|
+
* injecting functions/classes from modules, and executing tests using vitest.
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
import path from "node:path";
|
|
2
|
-
import fs from "node:fs";
|
|
3
8
|
import yaml from "js-yaml";
|
|
4
9
|
import { expect, test, describe } from "vitest";
|
|
5
10
|
|
|
6
|
-
import { traverseAllFiles } from "./utils.js";
|
|
11
|
+
import { traverseAllFiles, parseWithIncludes } from "./utils.js";
|
|
7
12
|
|
|
13
|
+
/**
|
|
14
|
+
* File extensions that are recognized as YAML test files
|
|
15
|
+
* @type {string[]}
|
|
16
|
+
*/
|
|
8
17
|
const extensions = [".test.yaml", ".test.yml", ".spec.yaml", ".spec.yml"];
|
|
9
18
|
|
|
10
19
|
/**
|
|
11
|
-
*
|
|
20
|
+
* Parses YAML content containing multiple documents separated by '---' into a structured test configuration
|
|
21
|
+
* @param {string} yamlContent - Raw YAML content string to parse
|
|
22
|
+
* @returns {Object} Structured test configuration object
|
|
23
|
+
* @returns {string|null} returns.file - Path to the JavaScript file being tested
|
|
24
|
+
* @returns {string|null} returns.group - Test group name
|
|
25
|
+
* @returns {string[]} [returns.suiteNames] - Array of suite names defined in config
|
|
26
|
+
* @returns {Object[]} returns.suites - Array of test suite objects
|
|
27
|
+
* @example
|
|
28
|
+
* const yamlContent = `
|
|
29
|
+
* file: './math.js'
|
|
30
|
+
* group: math
|
|
31
|
+
* ---
|
|
32
|
+
* suite: add
|
|
33
|
+
* exportName: add
|
|
34
|
+
* ---
|
|
35
|
+
* case: add 1 and 2
|
|
36
|
+
* in: [1, 2]
|
|
37
|
+
* out: 3
|
|
38
|
+
* `;
|
|
39
|
+
* const config = parseYamlDocuments(yamlContent);
|
|
12
40
|
*/
|
|
13
41
|
export const parseYamlDocuments = (yamlContent) => {
|
|
14
42
|
const docs = yaml.loadAll(yamlContent);
|
|
@@ -68,8 +96,19 @@ export const parseYamlDocuments = (yamlContent) => {
|
|
|
68
96
|
};
|
|
69
97
|
|
|
70
98
|
/**
|
|
71
|
-
*
|
|
72
|
-
* @param {
|
|
99
|
+
* Sets up and registers test suites with the testing framework (vitest)
|
|
100
|
+
* @param {Object} testConfig - Test configuration object containing suites and cases
|
|
101
|
+
* @param {string} testConfig.group - Name of the test group (used as describe block name)
|
|
102
|
+
* @param {Object[]} testConfig.suites - Array of test suite objects
|
|
103
|
+
* @param {boolean} [testConfig.skip] - Whether to skip this entire test suite
|
|
104
|
+
* @example
|
|
105
|
+
* setupTestSuite({
|
|
106
|
+
* group: 'math',
|
|
107
|
+
* suites: [{
|
|
108
|
+
* name: 'add',
|
|
109
|
+
* cases: [{ name: 'add 1+2', functionUnderTest: addFn, in: [1,2], out: 3 }]
|
|
110
|
+
* }]
|
|
111
|
+
* });
|
|
73
112
|
*/
|
|
74
113
|
const setupTestSuite = (testConfig) => {
|
|
75
114
|
const { group, suites, skip } = testConfig;
|
|
@@ -91,6 +130,16 @@ const setupTestSuite = (testConfig) => {
|
|
|
91
130
|
});
|
|
92
131
|
};
|
|
93
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Sets up individual test cases for function-based testing
|
|
135
|
+
* @param {Object} suite - Test suite configuration
|
|
136
|
+
* @param {Object[]} suite.cases - Array of test case objects
|
|
137
|
+
* @param {string} suite.cases[].name - Test case name
|
|
138
|
+
* @param {any[]} suite.cases[].in - Input arguments for the function
|
|
139
|
+
* @param {any} suite.cases[].out - Expected output value
|
|
140
|
+
* @param {Function} suite.cases[].functionUnderTest - The function to test
|
|
141
|
+
* @param {string|RegExp} [suite.cases[].throws] - Expected error message/pattern if function should throw
|
|
142
|
+
*/
|
|
94
143
|
const setupFunctionTests = (suite) => {
|
|
95
144
|
const { cases } = suite;
|
|
96
145
|
for (const testCase of cases) {
|
|
@@ -117,6 +166,15 @@ const setupFunctionTests = (suite) => {
|
|
|
117
166
|
}
|
|
118
167
|
};
|
|
119
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Sets up individual test cases for class-based testing
|
|
171
|
+
* @param {Object} suite - Test suite configuration for class testing
|
|
172
|
+
* @param {Object[]} suite.cases - Array of test case objects
|
|
173
|
+
* @param {string} suite.cases[].name - Test case name
|
|
174
|
+
* @param {Object[]} suite.cases[].executions - Array of method executions to perform
|
|
175
|
+
* @param {Function} suite.ClassUnderTest - The class constructor to test
|
|
176
|
+
* @param {any[]} suite.constructorArgs - Arguments to pass to class constructor
|
|
177
|
+
*/
|
|
120
178
|
const setupClassTests = (suite) => {
|
|
121
179
|
const { cases, ClassUnderTest, constructorArgs } = suite;
|
|
122
180
|
for (const testCase of cases) {
|
|
@@ -185,10 +243,19 @@ const setupClassTests = (suite) => {
|
|
|
185
243
|
};
|
|
186
244
|
|
|
187
245
|
/**
|
|
188
|
-
*
|
|
189
|
-
* @param {
|
|
190
|
-
* @param {
|
|
191
|
-
* @returns
|
|
246
|
+
* Injects functions and classes from an imported module into test configuration objects
|
|
247
|
+
* @param {Object} module - The imported JavaScript module containing functions/classes to test
|
|
248
|
+
* @param {Object} originalTestConfig - Original test configuration object
|
|
249
|
+
* @returns {Object} Test configuration with injected functions/classes ready for testing
|
|
250
|
+
* @throws {Error} When required exports are not found in the module
|
|
251
|
+
* @example
|
|
252
|
+
* // Import module and inject functions
|
|
253
|
+
* const module = await import('./math.js');
|
|
254
|
+
* const testConfig = {
|
|
255
|
+
* suites: [{ name: 'add', exportName: 'add', cases: [...] }]
|
|
256
|
+
* };
|
|
257
|
+
* const ready = injectFunctions(module, testConfig);
|
|
258
|
+
* // ready.suites[0].cases[0].functionUnderTest === module.add
|
|
192
259
|
*/
|
|
193
260
|
export const injectFunctions = (module, originalTestConfig) => {
|
|
194
261
|
const testConfig = structuredClone(originalTestConfig);
|
|
@@ -222,13 +289,23 @@ export const injectFunctions = (module, originalTestConfig) => {
|
|
|
222
289
|
};
|
|
223
290
|
|
|
224
291
|
/**
|
|
225
|
-
*
|
|
292
|
+
* Discovers and sets up test suites from all YAML test files in a directory and its subdirectories
|
|
293
|
+
* @param {string} dirname - Directory path to search for YAML test files
|
|
294
|
+
* @returns {Promise<void>} Promise that resolves when all test suites are set up
|
|
295
|
+
* @throws {Error} When YAML files cannot be parsed or modules cannot be imported
|
|
296
|
+
* @example
|
|
297
|
+
* // Set up all test suites from YAML files in the current directory
|
|
298
|
+
* await setupTestSuiteFromYaml('.');
|
|
299
|
+
*
|
|
300
|
+
* // Set up tests from a specific directory
|
|
301
|
+
* await setupTestSuiteFromYaml('./tests');
|
|
302
|
+
*
|
|
303
|
+
* // This will find all files matching: *.test.yaml, *.test.yml, *.spec.yaml, *.spec.yml
|
|
226
304
|
*/
|
|
227
305
|
export const setupTestSuiteFromYaml = async (dirname) => {
|
|
228
306
|
const testYamlFiles = traverseAllFiles(dirname, extensions);
|
|
229
307
|
for (const file of testYamlFiles) {
|
|
230
|
-
const
|
|
231
|
-
const testConfig = parseYamlDocuments(yamlContent);
|
|
308
|
+
const testConfig = parseWithIncludes(file);
|
|
232
309
|
const filepathRelativeToSpecFile = path.join(
|
|
233
310
|
path.dirname(file),
|
|
234
311
|
testConfig.file,
|
package/src/utils.js
CHANGED
|
@@ -1,23 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Utility functions for YAML processing with !include directive support
|
|
3
|
+
* This module provides functions for loading YAML files with include capabilities,
|
|
4
|
+
* directory traversal, and parsing YAML test configurations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
import fs from "node:fs";
|
|
2
8
|
import path, { join } from "node:path";
|
|
3
9
|
|
|
4
10
|
import yaml from "js-yaml";
|
|
5
11
|
|
|
6
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Loads a YAML file with support for !include directives and circular dependency detection
|
|
14
|
+
* @param {string} filePath - Absolute or relative path to the YAML file to load
|
|
15
|
+
* @param {Set<string>} [visitedFiles=new Set()] - Set of already visited file paths for circular dependency detection
|
|
16
|
+
* @returns {any|any[]} The parsed YAML content. Returns single object for single documents, array for multi-document YAML
|
|
17
|
+
* @throws {Error} When circular dependencies are detected, files are not found, or YAML parsing fails
|
|
18
|
+
* @example
|
|
19
|
+
* // Load a simple YAML file
|
|
20
|
+
* const config = loadYamlWithPath('./config.yaml');
|
|
21
|
+
*
|
|
22
|
+
* // Load YAML with includes
|
|
23
|
+
* const data = loadYamlWithPath('./main.yaml'); // main.yaml contains: data: !include ./data.yaml
|
|
24
|
+
*/
|
|
25
|
+
export const loadYamlWithPath = (filePath, visitedFiles = new Set()) => {
|
|
26
|
+
const absolutePath = path.resolve(filePath);
|
|
27
|
+
|
|
28
|
+
// Check for circular dependencies
|
|
29
|
+
if (visitedFiles.has(absolutePath)) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Circular dependency detected: ${absolutePath} is already being processed`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Add current file to visited set
|
|
36
|
+
visitedFiles.add(absolutePath);
|
|
37
|
+
|
|
7
38
|
const includeType = new yaml.Type("!include", {
|
|
8
39
|
kind: "scalar",
|
|
9
|
-
construct: function (
|
|
10
|
-
|
|
11
|
-
|
|
40
|
+
construct: function (includePath) {
|
|
41
|
+
// Resolve include path relative to the current file's directory
|
|
42
|
+
const currentDir = path.dirname(absolutePath);
|
|
43
|
+
const resolvedIncludePath = path.resolve(currentDir, includePath);
|
|
44
|
+
|
|
45
|
+
// Check if included file exists
|
|
46
|
+
if (!fs.existsSync(resolvedIncludePath)) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Include file not found: ${resolvedIncludePath} (included from ${absolutePath})`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Recursively load the included file with the same visited files set
|
|
53
|
+
return loadYamlWithPath(resolvedIncludePath, new Set(visitedFiles));
|
|
12
54
|
},
|
|
13
55
|
});
|
|
56
|
+
|
|
14
57
|
const schema = yaml.DEFAULT_SCHEMA.extend([includeType]);
|
|
15
|
-
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const content = fs.readFileSync(absolutePath, "utf8");
|
|
61
|
+
// Always use loadAll - it handles both single and multi-document YAML
|
|
62
|
+
const docs = yaml.loadAll(content, { schema });
|
|
63
|
+
// If only one document, return it directly (not as array)
|
|
64
|
+
return docs.length === 1 ? docs[0] : docs;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Error loading YAML file ${absolutePath}: ${error.message}`,
|
|
68
|
+
);
|
|
69
|
+
} finally {
|
|
70
|
+
// Remove current file from visited set when done processing
|
|
71
|
+
visitedFiles.delete(absolutePath);
|
|
72
|
+
}
|
|
16
73
|
};
|
|
17
74
|
|
|
18
75
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
76
|
+
* Recursively traverses a directory and returns all files matching the specified extensions
|
|
77
|
+
* @param {string} startPath - The directory path to start traversing from
|
|
78
|
+
* @param {string[]} extensions - Array of file extensions to match (e.g., ['.yaml', '.yml'])
|
|
79
|
+
* @returns {string[]} Array of absolute file paths for all matching files found
|
|
80
|
+
* @example
|
|
81
|
+
* // Find all YAML test files
|
|
82
|
+
* const testFiles = traverseAllFiles('./tests', ['.test.yaml', '.spec.yml']);
|
|
83
|
+
*
|
|
84
|
+
* // Find all JavaScript files
|
|
85
|
+
* const jsFiles = traverseAllFiles('./src', ['.js', '.ts']);
|
|
21
86
|
*/
|
|
22
87
|
export const traverseAllFiles = (startPath, extensions) => {
|
|
23
88
|
const results = [];
|
|
@@ -33,3 +98,131 @@ export const traverseAllFiles = (startPath, extensions) => {
|
|
|
33
98
|
}
|
|
34
99
|
return results;
|
|
35
100
|
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Recursively flattens nested arrays of YAML documents that result from multi-document includes
|
|
104
|
+
* @param {any|any[]} data - The data to flatten, can be a single document or nested arrays
|
|
105
|
+
* @returns {any[]} Flattened array of documents
|
|
106
|
+
* @example
|
|
107
|
+
* // Flatten nested document arrays
|
|
108
|
+
* const nested = [doc1, [doc2, doc3], doc4];
|
|
109
|
+
* const flat = flattenDocuments(nested); // [doc1, doc2, doc3, doc4]
|
|
110
|
+
*/
|
|
111
|
+
const flattenDocuments = (data) => {
|
|
112
|
+
if (!Array.isArray(data)) {
|
|
113
|
+
return [data];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const result = [];
|
|
117
|
+
for (const item of data) {
|
|
118
|
+
if (Array.isArray(item)) {
|
|
119
|
+
result.push(...flattenDocuments(item));
|
|
120
|
+
} else {
|
|
121
|
+
result.push(item);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Processes an array of YAML documents and converts them into a structured test configuration
|
|
129
|
+
* @param {any[]} docs - Array of YAML document objects to process
|
|
130
|
+
* @returns {Object} Structured test configuration object
|
|
131
|
+
* @returns {string|null} returns.file - Path to the JavaScript file being tested
|
|
132
|
+
* @returns {string|null} returns.group - Test group name
|
|
133
|
+
* @returns {string[]} [returns.suiteNames] - Array of suite names defined in config
|
|
134
|
+
* @returns {Object[]} returns.suites - Array of test suite objects
|
|
135
|
+
* @returns {string} returns.suites[].name - Suite name
|
|
136
|
+
* @returns {string} returns.suites[].exportName - Function/class export name to test
|
|
137
|
+
* @returns {Object[]} returns.suites[].cases - Array of test cases
|
|
138
|
+
* @returns {string} [returns.suites[].mode] - Test mode ('class' for class testing)
|
|
139
|
+
* @returns {any[]} [returns.suites[].constructorArgs] - Constructor arguments for class mode
|
|
140
|
+
* @example
|
|
141
|
+
* const docs = [
|
|
142
|
+
* { file: './math.js', group: 'math', suites: ['add'] },
|
|
143
|
+
* { suite: 'add', exportName: 'add' },
|
|
144
|
+
* { case: 'add 1+2', in: [1, 2], out: 3 }
|
|
145
|
+
* ];
|
|
146
|
+
* const config = processDocuments(docs);
|
|
147
|
+
*/
|
|
148
|
+
const processDocuments = (docs) => {
|
|
149
|
+
const config = {
|
|
150
|
+
file: null,
|
|
151
|
+
group: null,
|
|
152
|
+
suites: [],
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
let currentSuite = null;
|
|
156
|
+
|
|
157
|
+
for (const doc of docs) {
|
|
158
|
+
if (doc.file) {
|
|
159
|
+
config.file = doc.file;
|
|
160
|
+
config.group = doc.group || doc.name;
|
|
161
|
+
if (doc.suites) {
|
|
162
|
+
config.suiteNames = doc.suites;
|
|
163
|
+
}
|
|
164
|
+
} else if (doc.suite) {
|
|
165
|
+
if (currentSuite) {
|
|
166
|
+
config.suites.push(currentSuite);
|
|
167
|
+
}
|
|
168
|
+
currentSuite = {
|
|
169
|
+
name: doc.suite,
|
|
170
|
+
exportName: doc.exportName || doc.suite,
|
|
171
|
+
cases: [],
|
|
172
|
+
};
|
|
173
|
+
// Only add mode and constructorArgs if mode is explicitly 'class'
|
|
174
|
+
if (doc.mode === "class") {
|
|
175
|
+
currentSuite.mode = "class";
|
|
176
|
+
currentSuite.constructorArgs = doc.constructorArgs || [];
|
|
177
|
+
}
|
|
178
|
+
} else if (doc.case && currentSuite) {
|
|
179
|
+
const testCase = {
|
|
180
|
+
name: doc.case,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
if (currentSuite.mode === "class") {
|
|
184
|
+
testCase.executions = doc.executions || [];
|
|
185
|
+
} else {
|
|
186
|
+
testCase.in = doc.in || [];
|
|
187
|
+
testCase.out = doc.out;
|
|
188
|
+
if (doc.throws) {
|
|
189
|
+
testCase.throws = doc.throws;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
currentSuite.cases.push(testCase);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (currentSuite) {
|
|
198
|
+
config.suites.push(currentSuite);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return config;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Parses a YAML file with !include directive support and converts it into a structured test configuration
|
|
206
|
+
* @param {string} filePath - Path to the YAML file to parse
|
|
207
|
+
* @returns {Object} Structured test configuration object ready for test execution
|
|
208
|
+
* @throws {Error} When file cannot be found, YAML is invalid, or circular dependencies are detected
|
|
209
|
+
* @example
|
|
210
|
+
* // Parse a test configuration file with includes
|
|
211
|
+
* const config = parseWithIncludes('./tests/math.spec.yaml');
|
|
212
|
+
* // Returns: { file: './math.js', group: 'math', suites: [...] }
|
|
213
|
+
*
|
|
214
|
+
* // Works with files containing !include directives
|
|
215
|
+
* // main.yaml:
|
|
216
|
+
* // file: './lib.js'
|
|
217
|
+
* // ---
|
|
218
|
+
* // !include ./test-cases.yaml
|
|
219
|
+
* const config = parseWithIncludes('./main.yaml');
|
|
220
|
+
*/
|
|
221
|
+
export const parseWithIncludes = (filePath) => {
|
|
222
|
+
const yamlData = loadYamlWithPath(filePath);
|
|
223
|
+
|
|
224
|
+
// Flatten any nested arrays that come from multi-document includes
|
|
225
|
+
const flattenedDocs = flattenDocuments(yamlData);
|
|
226
|
+
|
|
227
|
+
return processDocuments(flattenedDocs);
|
|
228
|
+
};
|