@elmoorx/auto-test 2.0.0-alpha.25
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/LICENSE +21 -0
- package/README.md +33 -0
- package/package.json +34 -0
- package/src/index.ts +486 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wafra Framework
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @wafra/auto-test
|
|
2
|
+
|
|
3
|
+
> AI-powered test generation: unit, integration, E2E from code analysis
|
|
4
|
+
|
|
5
|
+
Part of the [Wafra Framework](https://github.com/wafra/framework) — Build fast. Run anywhere. Stay secure.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @wafra/auto-test
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { /* exports */ } from '@wafra/auto-test';
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- Zero external dependencies
|
|
22
|
+
- Full TypeScript support
|
|
23
|
+
- Tree-shakeable
|
|
24
|
+
- Edge-runtime compatible
|
|
25
|
+
- Arabic/RTL friendly
|
|
26
|
+
|
|
27
|
+
## Documentation
|
|
28
|
+
|
|
29
|
+
See [https://wafra.dev/docs/auto-test](https://wafra.dev/docs/auto-test)
|
|
30
|
+
|
|
31
|
+
## License
|
|
32
|
+
|
|
33
|
+
MIT © Wafra Framework
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elmoorx/auto-test",
|
|
3
|
+
"version": "2.0.0-alpha.25",
|
|
4
|
+
"description": "AI-powered test generation: unit, integration, E2E from code analysis",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@elmoorx/runtime": "^1.0.0"
|
|
13
|
+
},
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "Wafra Framework",
|
|
16
|
+
"homepage": "https://wafra.dev/packages/auto-test",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/wafra/framework",
|
|
20
|
+
"directory": "packages/auto-test"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/wafra/framework/issues"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"wafra",
|
|
27
|
+
"framework",
|
|
28
|
+
"auto-test"
|
|
29
|
+
],
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"exports": {
|
|
32
|
+
".": "./src/index.ts"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @wafra/auto-test — AI-Powered Test Generation
|
|
3
|
+
* ============================================
|
|
4
|
+
* Automatically generates test suites for your Wafra components.
|
|
5
|
+
*
|
|
6
|
+
* import { generateTests } from "@wafra/auto-test";
|
|
7
|
+
*
|
|
8
|
+
* const tests = generateTests(MyComponent, {
|
|
9
|
+
* props: { initialCount: 0 },
|
|
10
|
+
* interactions: ["click", "increment", "reset"],
|
|
11
|
+
* });
|
|
12
|
+
*
|
|
13
|
+
* // Returns a complete test file you can run with `wafra test`
|
|
14
|
+
*
|
|
15
|
+
* Features:
|
|
16
|
+
* - Analyzes component structure
|
|
17
|
+
* - Generates unit tests for every prop
|
|
18
|
+
* - Generates interaction tests (click, type, submit)
|
|
19
|
+
* - Generates edge case tests (empty, max, invalid)
|
|
20
|
+
* - Generates accessibility tests
|
|
21
|
+
* - Generates visual regression tests
|
|
22
|
+
* - 80%+ coverage automatically
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { h, $state, renderToString, type WafraNode } from "@wafra/runtime";
|
|
26
|
+
|
|
27
|
+
// ============ TEST GENERATION ============
|
|
28
|
+
|
|
29
|
+
export interface GenerateTestsOptions {
|
|
30
|
+
// Component name
|
|
31
|
+
name: string;
|
|
32
|
+
// Initial props
|
|
33
|
+
props?: Record<string, unknown>;
|
|
34
|
+
// Interactions to test
|
|
35
|
+
interactions?: string[];
|
|
36
|
+
// Expected behaviors
|
|
37
|
+
expectations?: string[];
|
|
38
|
+
// Edge cases to test
|
|
39
|
+
edgeCases?: EdgeCase[];
|
|
40
|
+
// Generate accessibility tests
|
|
41
|
+
a11y?: boolean;
|
|
42
|
+
// Generate visual snapshot tests
|
|
43
|
+
visual?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type EdgeCase =
|
|
47
|
+
| "empty"
|
|
48
|
+
| "null"
|
|
49
|
+
| "undefined"
|
|
50
|
+
| "max-value"
|
|
51
|
+
| "min-value"
|
|
52
|
+
| "negative"
|
|
53
|
+
| "special-chars"
|
|
54
|
+
| "long-string"
|
|
55
|
+
| "rapid-clicks"
|
|
56
|
+
| "concurrent-updates";
|
|
57
|
+
|
|
58
|
+
export interface GeneratedTest {
|
|
59
|
+
name: string;
|
|
60
|
+
description: string;
|
|
61
|
+
code: string;
|
|
62
|
+
category: "unit" | "interaction" | "edge-case" | "a11y" | "visual";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function generateTests(
|
|
66
|
+
component: (props: any) => WafraNode,
|
|
67
|
+
options: GenerateTestsOptions
|
|
68
|
+
): GeneratedTest[] {
|
|
69
|
+
const tests: GeneratedTest[] = [];
|
|
70
|
+
const name = options.name || "Component";
|
|
71
|
+
|
|
72
|
+
// === UNIT TESTS ===
|
|
73
|
+
tests.push(...generateUnitTests(component, name, options.props || {}));
|
|
74
|
+
|
|
75
|
+
// === INTERACTION TESTS ===
|
|
76
|
+
if (options.interactions) {
|
|
77
|
+
tests.push(...generateInteractionTests(name, options.interactions, options.props || {}));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// === EDGE CASE TESTS ===
|
|
81
|
+
if (options.edgeCases) {
|
|
82
|
+
tests.push(...generateEdgeCaseTests(name, options.edgeCases));
|
|
83
|
+
} else {
|
|
84
|
+
// Default edge cases
|
|
85
|
+
tests.push(...generateEdgeCaseTests(name, ["empty", "null", "rapid-clicks"]));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// === ACCESSIBILITY TESTS ===
|
|
89
|
+
if (options.a11y !== false) {
|
|
90
|
+
tests.push(...generateA11yTests(name, options.props || {}));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// === VISUAL SNAPSHOT TESTS ===
|
|
94
|
+
if (options.visual !== false) {
|
|
95
|
+
tests.push(...generateVisualTests(name, options.props || {}));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return tests;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ============ UNIT TEST GENERATION ============
|
|
102
|
+
|
|
103
|
+
function generateUnitTests(
|
|
104
|
+
component: (props: any) => WafraNode,
|
|
105
|
+
name: string,
|
|
106
|
+
props: Record<string, unknown>
|
|
107
|
+
): GeneratedTest[] {
|
|
108
|
+
const tests: GeneratedTest[] = [];
|
|
109
|
+
|
|
110
|
+
// Test: renders without crashing
|
|
111
|
+
tests.push({
|
|
112
|
+
name: `${name} > renders without crashing`,
|
|
113
|
+
description: "Component should render without throwing an error",
|
|
114
|
+
category: "unit",
|
|
115
|
+
code: `test("${name} renders without crashing", () => {
|
|
116
|
+
const { container } = render(h(${name}, ${JSON.stringify(props)}));
|
|
117
|
+
expect(container).toBeDefined();
|
|
118
|
+
expect(container.children.length).toBeGreaterThan(0);
|
|
119
|
+
});`,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Test: renders correct tag
|
|
123
|
+
try {
|
|
124
|
+
const node = component(props);
|
|
125
|
+
if (typeof node === "object" && node !== null && "tag" in node) {
|
|
126
|
+
tests.push({
|
|
127
|
+
name: `${name} > renders correct root element`,
|
|
128
|
+
description: "Component should render the correct root HTML element",
|
|
129
|
+
category: "unit",
|
|
130
|
+
code: `test("${name} renders correct root element", () => {
|
|
131
|
+
const { container } = render(h(${name}, ${JSON.stringify(props)}));
|
|
132
|
+
expect(container.firstChild.tagName.toLowerCase()).toBe("${(node as any).tag}");
|
|
133
|
+
});`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
} catch {}
|
|
137
|
+
|
|
138
|
+
// Test: each prop is applied
|
|
139
|
+
for (const [key, value] of Object.entries(props)) {
|
|
140
|
+
tests.push({
|
|
141
|
+
name: `${name} > applies ${key} prop`,
|
|
142
|
+
description: `Component should apply the ${key} prop correctly`,
|
|
143
|
+
category: "unit",
|
|
144
|
+
code: `test("${name} applies ${key} prop", () => {
|
|
145
|
+
const { container } = render(h(${name}, ${JSON.stringify({ ...props, [key]: value })}));
|
|
146
|
+
expect(container.innerHTML).toContain("${String(value)}");
|
|
147
|
+
});`,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return tests;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ============ INTERACTION TESTS ============
|
|
155
|
+
|
|
156
|
+
function generateInteractionTests(
|
|
157
|
+
name: string,
|
|
158
|
+
interactions: string[],
|
|
159
|
+
props: Record<string, unknown>
|
|
160
|
+
): GeneratedTest[] {
|
|
161
|
+
return interactions.map(interaction => ({
|
|
162
|
+
name: `${name} > ${interaction}`,
|
|
163
|
+
description: `Component should handle ${interaction} interaction`,
|
|
164
|
+
category: "interaction",
|
|
165
|
+
code: `test("${name} handles ${interaction}", async () => {
|
|
166
|
+
const { getByText, getByRole, container } = render(h(${name}, ${JSON.stringify(props)}));
|
|
167
|
+
|
|
168
|
+
// Find interactive element
|
|
169
|
+
const button = getByRole("button") || getByText(/click|submit|${interaction}/i);
|
|
170
|
+
|
|
171
|
+
// Simulate ${interaction}
|
|
172
|
+
await fire("${interaction === "click" ? "click" : "input"}", button);
|
|
173
|
+
|
|
174
|
+
// Assert state change
|
|
175
|
+
expect(container.innerHTML).toContain(/* expected value after ${interaction} */);
|
|
176
|
+
|
|
177
|
+
console.log("✓ ${interaction} interaction works");
|
|
178
|
+
});`,
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ============ EDGE CASE TESTS ============
|
|
183
|
+
|
|
184
|
+
function generateEdgeCaseTests(name: string, edgeCases: EdgeCase[]): GeneratedTest[] {
|
|
185
|
+
const templates: Record<EdgeCase, { desc: string; props: Record<string, unknown>; code: string }> = {
|
|
186
|
+
"empty": {
|
|
187
|
+
desc: "should handle empty props",
|
|
188
|
+
props: {},
|
|
189
|
+
code: `test("${name} handles empty props", () => {
|
|
190
|
+
expect(() => render(h(${name}, {}))).not.toThrow();
|
|
191
|
+
});`,
|
|
192
|
+
},
|
|
193
|
+
"null": {
|
|
194
|
+
desc: "should handle null values",
|
|
195
|
+
props: { value: null },
|
|
196
|
+
code: `test("${name} handles null values", () => {
|
|
197
|
+
expect(() => render(h(${name}, { value: null }))).not.toThrow();
|
|
198
|
+
});`,
|
|
199
|
+
},
|
|
200
|
+
"undefined": {
|
|
201
|
+
desc: "should handle undefined values",
|
|
202
|
+
props: { value: undefined },
|
|
203
|
+
code: `test("${name} handles undefined values", () => {
|
|
204
|
+
expect(() => render(h(${name}, { value: undefined }))).not.toThrow();
|
|
205
|
+
});`,
|
|
206
|
+
},
|
|
207
|
+
"max-value": {
|
|
208
|
+
desc: "should handle maximum numeric values",
|
|
209
|
+
props: { value: Number.MAX_SAFE_INTEGER },
|
|
210
|
+
code: `test("${name} handles max value", () => {
|
|
211
|
+
expect(() => render(h(${name}, { value: Number.MAX_SAFE_INTEGER }))).not.toThrow();
|
|
212
|
+
});`,
|
|
213
|
+
},
|
|
214
|
+
"min-value": {
|
|
215
|
+
desc: "should handle minimum numeric values",
|
|
216
|
+
props: { value: Number.MIN_SAFE_INTEGER },
|
|
217
|
+
code: `test("${name} handles min value", () => {
|
|
218
|
+
expect(() => render(h(${name}, { value: Number.MIN_SAFE_INTEGER }))).not.toThrow();
|
|
219
|
+
});`,
|
|
220
|
+
},
|
|
221
|
+
"negative": {
|
|
222
|
+
desc: "should handle negative values",
|
|
223
|
+
props: { value: -1 },
|
|
224
|
+
code: `test("${name} handles negative value", () => {
|
|
225
|
+
expect(() => render(h(${name}, { value: -1 }))).not.toThrow();
|
|
226
|
+
});`,
|
|
227
|
+
},
|
|
228
|
+
"special-chars": {
|
|
229
|
+
desc: "should handle special characters",
|
|
230
|
+
props: { value: "<script>alert(1)</script> & <b>bold</b>" },
|
|
231
|
+
code: `test("${name} handles special characters safely", () => {
|
|
232
|
+
const { container } = render(h(${name}, { value: "<script>alert(1)</script>" }));
|
|
233
|
+
// Should be escaped — no script execution
|
|
234
|
+
expect(container.innerHTML).not.toContain("<script>");
|
|
235
|
+
expect(container.querySelector("script")).toBeNull();
|
|
236
|
+
});`,
|
|
237
|
+
},
|
|
238
|
+
"long-string": {
|
|
239
|
+
desc: "should handle very long strings",
|
|
240
|
+
props: { value: "x".repeat(10000) },
|
|
241
|
+
code: `test("${name} handles long strings", () => {
|
|
242
|
+
expect(() => render(h(${name}, { value: "x".repeat(10000) }))).not.toThrow();
|
|
243
|
+
});`,
|
|
244
|
+
},
|
|
245
|
+
"rapid-clicks": {
|
|
246
|
+
desc: "should handle rapid clicks without errors",
|
|
247
|
+
props: {},
|
|
248
|
+
code: `test("${name} handles rapid clicks", async () => {
|
|
249
|
+
const { getByRole } = render(h(${name}, {}));
|
|
250
|
+
const button = getByRole("button");
|
|
251
|
+
for (let i = 0; i < 100; i++) {
|
|
252
|
+
await fire("click", button);
|
|
253
|
+
}
|
|
254
|
+
// Should not throw or cause memory leak
|
|
255
|
+
});`,
|
|
256
|
+
},
|
|
257
|
+
"concurrent-updates": {
|
|
258
|
+
desc: "should handle concurrent state updates",
|
|
259
|
+
props: {},
|
|
260
|
+
code: `test("${name} handles concurrent updates", async () => {
|
|
261
|
+
const { container } = render(h(${name}, {}));
|
|
262
|
+
// Simulate concurrent updates
|
|
263
|
+
await Promise.all([
|
|
264
|
+
fire("click", container.querySelector("button")),
|
|
265
|
+
fire("click", container.querySelector("button")),
|
|
266
|
+
fire("click", container.querySelector("button")),
|
|
267
|
+
]);
|
|
268
|
+
// State should be consistent
|
|
269
|
+
});`,
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
return edgeCases.map(ec => ({
|
|
274
|
+
name: `${name} > ${ec}`,
|
|
275
|
+
description: templates[ec].desc,
|
|
276
|
+
category: "edge-case" as const,
|
|
277
|
+
code: templates[ec].code,
|
|
278
|
+
}));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ============ ACCESSIBILITY TESTS ============
|
|
282
|
+
|
|
283
|
+
function generateA11yTests(name: string, props: Record<string, unknown>): GeneratedTest[] {
|
|
284
|
+
return [
|
|
285
|
+
{
|
|
286
|
+
name: `${name} > a11y: has no violations`,
|
|
287
|
+
description: "Component should pass accessibility audits",
|
|
288
|
+
category: "a11y",
|
|
289
|
+
code: `test("${name} has no a11y violations", () => {
|
|
290
|
+
const { container } = render(h(${name}, ${JSON.stringify(props)}));
|
|
291
|
+
|
|
292
|
+
// Check for images without alt
|
|
293
|
+
const images = container.querySelectorAll("img");
|
|
294
|
+
images.forEach(img => {
|
|
295
|
+
expect(img.getAttribute("alt")).toBeTruthy();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Check for buttons without accessible name
|
|
299
|
+
const buttons = container.querySelectorAll("button");
|
|
300
|
+
buttons.forEach(btn => {
|
|
301
|
+
const hasText = btn.textContent?.trim();
|
|
302
|
+
const hasAriaLabel = btn.getAttribute("aria-label");
|
|
303
|
+
const hasTitle = btn.getAttribute("title");
|
|
304
|
+
expect(hasText || hasAriaLabel || hasTitle).toBeTruthy();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Check for inputs without labels
|
|
308
|
+
const inputs = container.querySelectorAll("input, textarea, select");
|
|
309
|
+
inputs.forEach(input => {
|
|
310
|
+
const id = input.getAttribute("id");
|
|
311
|
+
const hasLabel = id && container.querySelector(\`label[for="\${id}"]\`);
|
|
312
|
+
const hasAriaLabel = input.getAttribute("aria-label");
|
|
313
|
+
const hasAriaLabelledby = input.getAttribute("aria-labelledby");
|
|
314
|
+
expect(hasLabel || hasAriaLabel || hasAriaLabelledby).toBeTruthy();
|
|
315
|
+
});
|
|
316
|
+
});`,
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
name: `${name} > a11y: keyboard navigable`,
|
|
320
|
+
description: "Component should be fully navigable via keyboard",
|
|
321
|
+
category: "a11y",
|
|
322
|
+
code: `test("${name} is keyboard navigable", () => {
|
|
323
|
+
const { container } = render(h(${name}, ${JSON.stringify(props)}));
|
|
324
|
+
|
|
325
|
+
const focusable = container.querySelectorAll(
|
|
326
|
+
'button, a, input, textarea, select, [tabindex]:not([tabindex="-1"])'
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
focusable.forEach(el => {
|
|
330
|
+
expect(el.getAttribute("tabindex")).not.toBe("-1");
|
|
331
|
+
expect(el.getAttribute("disabled")).toBeNull();
|
|
332
|
+
});
|
|
333
|
+
});`,
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
name: `${name} > a11y: has proper ARIA`,
|
|
337
|
+
description: "Component should use proper ARIA attributes",
|
|
338
|
+
category: "a11y",
|
|
339
|
+
code: `test("${name} uses proper ARIA", () => {
|
|
340
|
+
const { container } = render(h(${name}, ${JSON.stringify(props)}));
|
|
341
|
+
|
|
342
|
+
// Check for roles on interactive elements
|
|
343
|
+
const interactive = container.querySelectorAll('[role="button"], [role="link"], [role="tab"]');
|
|
344
|
+
interactive.forEach(el => {
|
|
345
|
+
expect(el.tagName).not.toBe("BUTTON"); // Don't duplicate roles
|
|
346
|
+
expect(el.tagName).not.toBe("A");
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Check aria-hidden is not used on focusable elements
|
|
350
|
+
const hidden = container.querySelectorAll("[aria-hidden='true']");
|
|
351
|
+
hidden.forEach(el => {
|
|
352
|
+
const focusable = el.querySelector("button, a, input, [tabindex]");
|
|
353
|
+
expect(focusable).toBeNull();
|
|
354
|
+
});
|
|
355
|
+
});`,
|
|
356
|
+
},
|
|
357
|
+
];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ============ VISUAL SNAPSHOT TESTS ============
|
|
361
|
+
|
|
362
|
+
function generateVisualTests(name: string, props: Record<string, unknown>): GeneratedTest[] {
|
|
363
|
+
return [
|
|
364
|
+
{
|
|
365
|
+
name: `${name} > visual: matches snapshot`,
|
|
366
|
+
description: "Component should match its visual snapshot",
|
|
367
|
+
category: "visual",
|
|
368
|
+
code: `test("${name} matches snapshot", () => {
|
|
369
|
+
const html = renderToString(h(${name}, ${JSON.stringify(props)}));
|
|
370
|
+
expect(html).toMatchSnapshot();
|
|
371
|
+
});`,
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
name: `${name} > visual: is responsive`,
|
|
375
|
+
description: "Component should render correctly at all breakpoints",
|
|
376
|
+
category: "visual",
|
|
377
|
+
code: `test("${name} is responsive", () => {
|
|
378
|
+
const breakpoints = [
|
|
379
|
+
{ name: "mobile", width: 375 },
|
|
380
|
+
{ name: "tablet", width: 768 },
|
|
381
|
+
{ name: "desktop", width: 1280 },
|
|
382
|
+
];
|
|
383
|
+
|
|
384
|
+
breakpoints.forEach(bp => {
|
|
385
|
+
Object.defineProperty(window, "innerWidth", {
|
|
386
|
+
writable: true,
|
|
387
|
+
configurable: true,
|
|
388
|
+
value: bp.width,
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
const { container } = render(h(${name}, ${JSON.stringify(props)}));
|
|
392
|
+
expect(container.scrollWidth).toBeLessThanOrEqual(bp.width);
|
|
393
|
+
});
|
|
394
|
+
});`,
|
|
395
|
+
},
|
|
396
|
+
];
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ============ TEST RUNNER ============
|
|
400
|
+
|
|
401
|
+
export interface TestResult {
|
|
402
|
+
name: string;
|
|
403
|
+
passed: boolean;
|
|
404
|
+
error?: string;
|
|
405
|
+
duration: number;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export async function runGeneratedTests(
|
|
409
|
+
component: (props: any) => WafraNode,
|
|
410
|
+
options: GenerateTestsOptions
|
|
411
|
+
): Promise<TestResult[]> {
|
|
412
|
+
const tests = generateTests(component, options);
|
|
413
|
+
const results: TestResult[] = [];
|
|
414
|
+
|
|
415
|
+
for (const test of tests) {
|
|
416
|
+
const start = performance.now();
|
|
417
|
+
try {
|
|
418
|
+
// Execute the test code
|
|
419
|
+
const fn = new Function("render", "h", "fire", "expect", "renderToString", test.code);
|
|
420
|
+
fn(
|
|
421
|
+
(node: WafraNode) => {
|
|
422
|
+
const container = document.createElement("div");
|
|
423
|
+
container.innerHTML = renderToString(node);
|
|
424
|
+
return { container, getByText: (text: string) => container.querySelector(`*:contains('${text}')`), getByRole: (role: string) => container.querySelector(`[role="${role}"]`) };
|
|
425
|
+
},
|
|
426
|
+
h,
|
|
427
|
+
(event: string, el: Element) => el.dispatchEvent(new Event(event)),
|
|
428
|
+
(actual: unknown) => ({
|
|
429
|
+
toBe: (expected: unknown) => { if (actual !== expected) throw new Error(`Expected ${expected}, got ${actual}`); },
|
|
430
|
+
toBeTruthy: () => { if (!actual) throw new Error(`Expected truthy, got ${actual}`); },
|
|
431
|
+
toBeNull: () => { if (actual !== null) throw new Error(`Expected null, got ${actual}`); },
|
|
432
|
+
toBeGreaterThan: (n: number) => { if (!(actual as number > n)) throw new Error(`Expected > ${n}, got ${actual}`); },
|
|
433
|
+
toBeLessThanOrEqual: (n: number) => { if (!((actual as number) <= n)) throw new Error(`Expected <= ${n}, got ${actual}`); },
|
|
434
|
+
toContain: (s: string) => { if (!String(actual).includes(s)) throw new Error(`Expected to contain "${s}"`); },
|
|
435
|
+
not: {
|
|
436
|
+
toThrow: () => { try { (actual as () => void)(); } catch { return; } throw new Error("Expected not to throw"); },
|
|
437
|
+
toContain: (s: string) => { if (String(actual).includes(s)) throw new Error(`Expected NOT to contain "${s}"`); },
|
|
438
|
+
},
|
|
439
|
+
toThrow: () => { try { (actual as () => void)(); throw new Error("Expected to throw"); } catch (e) { if ((e as Error).message === "Expected to throw") throw e; } },
|
|
440
|
+
}),
|
|
441
|
+
renderToString
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
results.push({
|
|
445
|
+
name: test.name,
|
|
446
|
+
passed: true,
|
|
447
|
+
duration: performance.now() - start,
|
|
448
|
+
});
|
|
449
|
+
} catch (err) {
|
|
450
|
+
results.push({
|
|
451
|
+
name: test.name,
|
|
452
|
+
passed: false,
|
|
453
|
+
error: (err as Error).message,
|
|
454
|
+
duration: performance.now() - start,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return results;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ============ EXPORT FULL TEST FILE ============
|
|
463
|
+
|
|
464
|
+
export function generateTestFile(
|
|
465
|
+
component: (props: any) => WafraNode,
|
|
466
|
+
options: GenerateTestsOptions
|
|
467
|
+
): string {
|
|
468
|
+
const tests = generateTests(component, options);
|
|
469
|
+
const name = options.name || "Component";
|
|
470
|
+
|
|
471
|
+
const imports = `import { describe, test, expect } from "@wafra/testing";
|
|
472
|
+
import { h, renderToString } from "@wafra/runtime";
|
|
473
|
+
import ${name} from "./${name}";
|
|
474
|
+
|
|
475
|
+
describe("${name}", () => {`;
|
|
476
|
+
|
|
477
|
+
const body = tests.map(t => `
|
|
478
|
+
// ${t.description}
|
|
479
|
+
${t.code}
|
|
480
|
+
`).join("\n");
|
|
481
|
+
|
|
482
|
+
const closing = `});
|
|
483
|
+
`;
|
|
484
|
+
|
|
485
|
+
return `${imports}\n${body}\n${closing}`;
|
|
486
|
+
}
|