puty 0.0.1 → 0.0.2

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.
Files changed (3) hide show
  1. package/README.md +59 -0
  2. package/package.json +4 -1
  3. package/src/puty.js +139 -25
package/README.md CHANGED
@@ -26,6 +26,8 @@ It should run all the tests in the file.
26
26
 
27
27
  ## Usage
28
28
 
29
+ ### Testing Functions
30
+
29
31
  ```yaml
30
32
  file: './math.js'
31
33
  group: math
@@ -61,3 +63,60 @@ in:
61
63
  - 2
62
64
  out: 3
63
65
  ```
66
+
67
+ ### Testing Classes
68
+
69
+ Puty also supports testing classes with method calls and state assertions:
70
+
71
+ ```yaml
72
+ file: './calculator.js'
73
+ group: Calculator
74
+ suites: [basic-operations]
75
+ ---
76
+ suite: basic-operations
77
+ mode: 'class'
78
+ exportName: default
79
+ constructorArgs: [10] # Initial value
80
+ ---
81
+ case: add and multiply operations
82
+ executions:
83
+ - method: add
84
+ in: [5]
85
+ out: 15
86
+ asserts:
87
+ - property: value
88
+ op: eq
89
+ value: 15
90
+ - method: multiply
91
+ in: [2]
92
+ out: 30
93
+ asserts:
94
+ - property: value
95
+ op: eq
96
+ value: 30
97
+ - method: getValue
98
+ in: []
99
+ out: 30
100
+ ```
101
+
102
+ #### Class Test Structure
103
+
104
+ - `mode: 'class'` - Indicates this suite tests a class
105
+ - `constructorArgs` - Arguments passed to the class constructor
106
+ - `executions` - Array of method calls to execute in sequence
107
+ - `method` - Name of the method to call
108
+ - `in` - Arguments to pass to the method
109
+ - `out` - Expected return value (optional)
110
+ - `asserts` - Assertions to run after the method call
111
+ - Property assertions: Check instance properties
112
+ - Method assertions: Call methods and check their return values
113
+
114
+ ### Error Testing
115
+
116
+ You can test that functions or methods throw expected errors:
117
+
118
+ ```yaml
119
+ case: divide by zero
120
+ in: [10, 0]
121
+ throws: "Division by zero"
122
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "puty",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "A tooling function to test javascript functions and classes.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -19,5 +19,8 @@
19
19
  "lint": "bunx prettier src tests -c",
20
20
  "lint:fix": "bunx prettier src tests -w",
21
21
  "build": "bun run esbuild.js"
22
+ },
23
+ "devDependencies": {
24
+ "vitest": "^3.2.1"
22
25
  }
23
26
  }
package/src/puty.js CHANGED
@@ -36,12 +36,27 @@ export const parseYamlDocuments = (yamlContent) => {
36
36
  exportName: doc.exportName || doc.suite,
37
37
  cases: [],
38
38
  };
39
+ // Only add mode and constructorArgs if mode is explicitly 'class'
40
+ if (doc.mode === "class") {
41
+ currentSuite.mode = "class";
42
+ currentSuite.constructorArgs = doc.constructorArgs || [];
43
+ }
39
44
  } else if (doc.case && currentSuite) {
40
- currentSuite.cases.push({
45
+ const testCase = {
41
46
  name: doc.case,
42
- in: doc.in || [],
43
- out: doc.out,
44
- });
47
+ };
48
+
49
+ if (currentSuite.mode === "class") {
50
+ testCase.executions = doc.executions || [];
51
+ } else {
52
+ testCase.in = doc.in || [];
53
+ testCase.out = doc.out;
54
+ if (doc.throws) {
55
+ testCase.throws = doc.throws;
56
+ }
57
+ }
58
+
59
+ currentSuite.cases.push(testCase);
45
60
  }
46
61
  }
47
62
 
@@ -64,28 +79,111 @@ const setupTestSuite = (testConfig) => {
64
79
  describe(group, () => {
65
80
  for (const suite of suites) {
66
81
  describe(suite.name, () => {
67
- const { cases } = suite;
68
- for (const testCase of cases) {
69
- const {
70
- name,
71
- in: inArg,
72
- out: expectedOut,
73
- functionUnderTest,
74
- } = testCase;
75
- test(name, () => {
76
- const out = functionUnderTest(...(inArg || []));
77
- if (out instanceof Error) {
78
- expect(out.message).toEqual(out.message);
79
- } else {
80
- expect(out).toEqual(expectedOut);
81
- }
82
- });
82
+ const { cases, mode } = suite;
83
+
84
+ if (mode === "class") {
85
+ setupClassTests(suite);
86
+ } else {
87
+ setupFunctionTests(suite);
83
88
  }
84
89
  });
85
90
  }
86
91
  });
87
92
  };
88
93
 
94
+ const setupFunctionTests = (suite) => {
95
+ const { cases } = suite;
96
+ for (const testCase of cases) {
97
+ const {
98
+ name,
99
+ in: inArg,
100
+ out: expectedOut,
101
+ functionUnderTest,
102
+ throws,
103
+ } = testCase;
104
+ test(name, () => {
105
+ if (!functionUnderTest) {
106
+ throw new Error(`Function not found for test case: ${name}`);
107
+ }
108
+
109
+ if (throws) {
110
+ // Test expects an error to be thrown
111
+ expect(() => functionUnderTest(...(inArg || []))).toThrow(throws);
112
+ } else {
113
+ const out = functionUnderTest(...(inArg || []));
114
+ expect(out).toEqual(expectedOut);
115
+ }
116
+ });
117
+ }
118
+ };
119
+
120
+ const setupClassTests = (suite) => {
121
+ const { cases, ClassUnderTest, constructorArgs } = suite;
122
+ for (const testCase of cases) {
123
+ const { name, executions } = testCase;
124
+ test(name, () => {
125
+ if (!ClassUnderTest) {
126
+ throw new Error(`Class not found for test suite: ${suite.name}`);
127
+ }
128
+
129
+ const instance = new ClassUnderTest(...constructorArgs);
130
+
131
+ for (const execution of executions) {
132
+ const {
133
+ method,
134
+ in: inArg,
135
+ out: expectedOut,
136
+ throws,
137
+ asserts,
138
+ } = execution;
139
+
140
+ // Validate method exists
141
+ if (!instance[method] || typeof instance[method] !== "function") {
142
+ throw new Error(`Method '${method}' not found on class instance`);
143
+ }
144
+
145
+ // Execute the method and check its return value
146
+ if (throws) {
147
+ expect(() => instance[method](...(inArg || []))).toThrow(throws);
148
+ } else {
149
+ const result = instance[method](...(inArg || []));
150
+ if (expectedOut !== undefined) {
151
+ expect(result).toEqual(expectedOut);
152
+ }
153
+ }
154
+
155
+ // Run assertions
156
+ if (asserts) {
157
+ for (const assertion of asserts) {
158
+ if (assertion.property) {
159
+ // Property assertion
160
+ const actualValue = instance[assertion.property];
161
+ if (assertion.op === "eq") {
162
+ expect(actualValue).toEqual(assertion.value);
163
+ }
164
+ // Add more operators as needed
165
+ } else if (assertion.method) {
166
+ // Method assertion
167
+ if (
168
+ !instance[assertion.method] ||
169
+ typeof instance[assertion.method] !== "function"
170
+ ) {
171
+ throw new Error(
172
+ `Method '${assertion.method}' not found on class instance for assertion`,
173
+ );
174
+ }
175
+ const result = instance[assertion.method](
176
+ ...(assertion.in || []),
177
+ );
178
+ expect(result).toEqual(assertion.out);
179
+ }
180
+ }
181
+ }
182
+ }
183
+ });
184
+ }
185
+ };
186
+
89
187
  /**
90
188
  *
91
189
  * @param {*} module
@@ -97,11 +195,27 @@ export const injectFunctions = (module, originalTestConfig) => {
97
195
  let functionUnderTest = module[testConfig.exportName || "default"];
98
196
 
99
197
  for (const suite of testConfig.suites) {
100
- if (suite.exportName) {
101
- functionUnderTest = module[suite.exportName];
102
- }
103
- for (const testCase of suite.cases) {
104
- testCase.functionUnderTest = functionUnderTest;
198
+ if (suite.mode === "class") {
199
+ const exportName = suite.exportName || "default";
200
+ const exported = module[exportName];
201
+ if (!exported) {
202
+ throw new Error(
203
+ `Export '${exportName}' not found in module for class suite '${suite.name}'`,
204
+ );
205
+ }
206
+ suite.ClassUnderTest = exported;
207
+ } else {
208
+ if (suite.exportName) {
209
+ functionUnderTest = module[suite.exportName];
210
+ if (!functionUnderTest) {
211
+ throw new Error(
212
+ `Export '${suite.exportName}' not found in module for suite '${suite.name}'`,
213
+ );
214
+ }
215
+ }
216
+ for (const testCase of suite.cases) {
217
+ testCase.functionUnderTest = functionUnderTest;
218
+ }
105
219
  }
106
220
  }
107
221
  return testConfig;