specforge-mcp 0.2.2 → 0.4.0
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/dist/engine/skill-generator/sections-skills.d.ts +4 -0
- package/dist/engine/skill-generator/sections-skills.d.ts.map +1 -0
- package/dist/engine/skill-generator/sections-skills.js +51 -0
- package/dist/engine/skill-generator/sections-skills.js.map +1 -0
- package/dist/engine/skill-generator/sections.d.ts +1 -0
- package/dist/engine/skill-generator/sections.d.ts.map +1 -1
- package/dist/engine/skill-generator/sections.js +2 -1
- package/dist/engine/skill-generator/sections.js.map +1 -1
- package/dist/engine/skill-generator.d.ts +3 -1
- package/dist/engine/skill-generator.d.ts.map +1 -1
- package/dist/engine/skill-generator.js +11 -2
- package/dist/engine/skill-generator.js.map +1 -1
- package/dist/engine/test-plan-generator.d.ts +3 -0
- package/dist/engine/test-plan-generator.d.ts.map +1 -0
- package/dist/engine/test-plan-generator.js +166 -0
- package/dist/engine/test-plan-generator.js.map +1 -0
- package/dist/engine/test-spec-generator.d.ts +8 -0
- package/dist/engine/test-spec-generator.d.ts.map +1 -0
- package/dist/engine/test-spec-generator.js +348 -0
- package/dist/engine/test-spec-generator.js.map +1 -0
- package/dist/tools/generate-rules.d.ts.map +1 -1
- package/dist/tools/generate-rules.js +20 -0
- package/dist/tools/generate-rules.js.map +1 -1
- package/dist/tools/generate-tests/generators/database-test-generator.d.ts +11 -0
- package/dist/tools/generate-tests/generators/database-test-generator.d.ts.map +1 -0
- package/dist/tools/generate-tests/generators/database-test-generator.js +329 -0
- package/dist/tools/generate-tests/generators/database-test-generator.js.map +1 -0
- package/dist/tools/generate-tests/generators/graphql-test-generator.d.ts +17 -0
- package/dist/tools/generate-tests/generators/graphql-test-generator.d.ts.map +1 -0
- package/dist/tools/generate-tests/generators/graphql-test-generator.js +235 -0
- package/dist/tools/generate-tests/generators/graphql-test-generator.js.map +1 -0
- package/dist/tools/generate-tests/generators/grpc-test-generator.d.ts +17 -0
- package/dist/tools/generate-tests/generators/grpc-test-generator.d.ts.map +1 -0
- package/dist/tools/generate-tests/generators/grpc-test-generator.js +283 -0
- package/dist/tools/generate-tests/generators/grpc-test-generator.js.map +1 -0
- package/dist/tools/generate-tests/generators/microservices-test-generator.d.ts +10 -0
- package/dist/tools/generate-tests/generators/microservices-test-generator.d.ts.map +1 -0
- package/dist/tools/generate-tests/generators/microservices-test-generator.js +341 -0
- package/dist/tools/generate-tests/generators/microservices-test-generator.js.map +1 -0
- package/dist/tools/generate-tests/generators/security-test-generator.d.ts +11 -0
- package/dist/tools/generate-tests/generators/security-test-generator.d.ts.map +1 -0
- package/dist/tools/generate-tests/generators/security-test-generator.js +318 -0
- package/dist/tools/generate-tests/generators/security-test-generator.js.map +1 -0
- package/dist/tools/generate-tests/generators/visual-regression-generator.d.ts +19 -0
- package/dist/tools/generate-tests/generators/visual-regression-generator.d.ts.map +1 -0
- package/dist/tools/generate-tests/generators/visual-regression-generator.js +304 -0
- package/dist/tools/generate-tests/generators/visual-regression-generator.js.map +1 -0
- package/dist/tools/generate-tests/generators/websocket-test-generator.d.ts +17 -0
- package/dist/tools/generate-tests/generators/websocket-test-generator.d.ts.map +1 -0
- package/dist/tools/generate-tests/generators/websocket-test-generator.js +243 -0
- package/dist/tools/generate-tests/generators/websocket-test-generator.js.map +1 -0
- package/dist/tools/generate-tests/plan-mode-handler.d.ts +3 -0
- package/dist/tools/generate-tests/plan-mode-handler.d.ts.map +1 -0
- package/dist/tools/generate-tests/plan-mode-handler.js +54 -0
- package/dist/tools/generate-tests/plan-mode-handler.js.map +1 -0
- package/dist/tools/generate-tests/spec-dispatcher.d.ts.map +1 -1
- package/dist/tools/generate-tests/spec-dispatcher.js +46 -0
- package/dist/tools/generate-tests/spec-dispatcher.js.map +1 -1
- package/dist/tools/generate-tests/test-helpers.d.ts +8 -0
- package/dist/tools/generate-tests/test-helpers.d.ts.map +1 -0
- package/dist/tools/generate-tests/test-helpers.js +120 -0
- package/dist/tools/generate-tests/test-helpers.js.map +1 -0
- package/dist/tools/generate-tests.d.ts.map +1 -1
- package/dist/tools/generate-tests.js +6 -118
- package/dist/tools/generate-tests.js.map +1 -1
- package/dist/tools/init-project/handler.d.ts.map +1 -1
- package/dist/tools/init-project/handler.js +29 -0
- package/dist/tools/init-project/handler.js.map +1 -1
- package/dist/types/stack/recommend.d.ts +2 -0
- package/dist/types/stack/recommend.d.ts.map +1 -1
- package/dist/types/testing.d.ts +51 -0
- package/dist/types/testing.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/i18n/messages/en.json +333 -0
- package/src/i18n/messages/es.json +333 -0
- package/src/i18n/messages/pt.json +333 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
// tools/generate-tests/generators/security-test-generator.ts — SPEC-058b Section G
|
|
2
|
+
// Generates security test scaffolds:
|
|
3
|
+
// OWASP Top 10, auth flows, file upload, API rate limiting.
|
|
4
|
+
const AUTH_SIGNALS = [
|
|
5
|
+
'jwt',
|
|
6
|
+
'oauth',
|
|
7
|
+
'passport',
|
|
8
|
+
'keycloak',
|
|
9
|
+
'auth0',
|
|
10
|
+
'clerk',
|
|
11
|
+
'supabase',
|
|
12
|
+
'session',
|
|
13
|
+
'cookie-session',
|
|
14
|
+
'express-session',
|
|
15
|
+
'next-auth',
|
|
16
|
+
'lucia',
|
|
17
|
+
'better-auth',
|
|
18
|
+
];
|
|
19
|
+
const UPLOAD_SIGNALS = [
|
|
20
|
+
'multer',
|
|
21
|
+
'formidable',
|
|
22
|
+
'busboy',
|
|
23
|
+
'sharp',
|
|
24
|
+
'jimp',
|
|
25
|
+
'imagemagick',
|
|
26
|
+
'file-upload',
|
|
27
|
+
'multipart',
|
|
28
|
+
];
|
|
29
|
+
export function detectAuth(knowledge) {
|
|
30
|
+
const stackLower = knowledge.stack.map((s) => s.toLowerCase());
|
|
31
|
+
for (const sig of AUTH_SIGNALS) {
|
|
32
|
+
if (stackLower.some((s) => s.includes(sig))) {
|
|
33
|
+
return sig;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
export function detectFileUpload(knowledge) {
|
|
39
|
+
const stackLower = knowledge.stack.map((s) => s.toLowerCase());
|
|
40
|
+
for (const sig of UPLOAD_SIGNALS) {
|
|
41
|
+
if (stackLower.some((s) => s.includes(sig))) {
|
|
42
|
+
return sig;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
export function hasPublicApi(knowledge) {
|
|
48
|
+
if (knowledge.apiContracts.length > 0) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return knowledge.apps.some((a) => a.type === 'backend');
|
|
52
|
+
}
|
|
53
|
+
export function isSecurityTestProject(knowledge) {
|
|
54
|
+
if (detectAuth(knowledge) !== null) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
if (hasPublicApi(knowledge)) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
return knowledge.apps.some((a) => a.type === 'backend' || a.type === 'frontend');
|
|
61
|
+
}
|
|
62
|
+
export function generateSecurityTestDefs(title, testDir, _testExt, knowledge) {
|
|
63
|
+
const defs = [];
|
|
64
|
+
if (hasPublicApi(knowledge)) {
|
|
65
|
+
defs.push({
|
|
66
|
+
name: `${title} — OWASP Top 10: injection, XSS, CSRF, auth bypass`,
|
|
67
|
+
type: 'integration',
|
|
68
|
+
file: `${testDir}/security/`,
|
|
69
|
+
description: 'Verify protection against OWASP Top 10 vulnerabilities on public endpoints',
|
|
70
|
+
priority: 'critical',
|
|
71
|
+
automatable: true,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (detectAuth(knowledge) !== null) {
|
|
75
|
+
defs.push({
|
|
76
|
+
name: `${title} — auth: token expiration, refresh flow, privilege escalation`,
|
|
77
|
+
type: 'integration',
|
|
78
|
+
file: `${testDir}/security/`,
|
|
79
|
+
description: 'Verify auth token lifecycle, refresh flow, and privilege escalation prevention',
|
|
80
|
+
priority: 'critical',
|
|
81
|
+
automatable: true,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
if (detectFileUpload(knowledge) !== null) {
|
|
85
|
+
defs.push({
|
|
86
|
+
name: `${title} — file upload: type validation, size limits, path traversal`,
|
|
87
|
+
type: 'integration',
|
|
88
|
+
file: `${testDir}/security/`,
|
|
89
|
+
description: 'Verify file upload security: type validation, size limits, path traversal prevention',
|
|
90
|
+
priority: 'high',
|
|
91
|
+
automatable: true,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (hasPublicApi(knowledge)) {
|
|
95
|
+
defs.push({
|
|
96
|
+
name: `${title} — API security: rate limiting, CORS, API key rotation`,
|
|
97
|
+
type: 'integration',
|
|
98
|
+
file: `${testDir}/security/`,
|
|
99
|
+
description: 'Verify rate limiting effectiveness, CORS policy, and API key rotation support',
|
|
100
|
+
priority: 'high',
|
|
101
|
+
automatable: true,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return defs;
|
|
105
|
+
}
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Scaffold builders
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
function buildTsSecurityTest(title, auth, upload, publicApi) {
|
|
110
|
+
const owaspSection = publicApi
|
|
111
|
+
? `
|
|
112
|
+
describe('OWASP Top 10', () => {
|
|
113
|
+
it('rejects SQL injection in query parameters', async () => {
|
|
114
|
+
// const res = await request(app).get("/api/users?id=' OR 1=1 --");
|
|
115
|
+
// expect(res.status).toBe(400);
|
|
116
|
+
expect(true).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('escapes XSS payloads in response', async () => {
|
|
120
|
+
// const res = await request(app).post('/api/comments')
|
|
121
|
+
// .send({ body: '<script>alert("xss")</script>' });
|
|
122
|
+
// expect(res.body.body).not.toContain('<script>');
|
|
123
|
+
expect(true).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('validates CSRF token on state-changing requests', async () => {
|
|
127
|
+
expect(true).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('rejects auth bypass attempts', async () => {
|
|
131
|
+
expect(true).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
`
|
|
135
|
+
: '';
|
|
136
|
+
const authSection = auth
|
|
137
|
+
? `
|
|
138
|
+
describe('Authentication (${auth})', () => {
|
|
139
|
+
it('rejects expired token with 401', async () => {
|
|
140
|
+
// const expiredToken = signToken({ exp: Math.floor(Date.now()/1000) - 60 });
|
|
141
|
+
// const res = await request(app).get('/api/me').set('Authorization', \`Bearer \${expiredToken}\`);
|
|
142
|
+
// expect(res.status).toBe(401);
|
|
143
|
+
expect(true).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('refresh token returns new access token', async () => {
|
|
147
|
+
expect(true).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('prevents privilege escalation (user cannot access admin)', async () => {
|
|
151
|
+
expect(true).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('prevents session fixation attack', async () => {
|
|
155
|
+
expect(true).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
`
|
|
159
|
+
: '';
|
|
160
|
+
const uploadSection = upload
|
|
161
|
+
? `
|
|
162
|
+
describe('File upload (${upload})', () => {
|
|
163
|
+
it('rejects disallowed file types', async () => {
|
|
164
|
+
// const res = await request(app).post('/api/upload')
|
|
165
|
+
// .attach('file', Buffer.from('#!/bin/sh'), { filename: 'evil.sh' });
|
|
166
|
+
// expect(res.status).toBe(400);
|
|
167
|
+
expect(true).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('enforces maximum file size', async () => {
|
|
171
|
+
expect(true).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('prevents path traversal in filename', async () => {
|
|
175
|
+
// filename: '../../etc/passwd' → should be sanitized
|
|
176
|
+
expect(true).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('scans for malicious content', async () => {
|
|
180
|
+
expect(true).toBe(true);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
`
|
|
184
|
+
: '';
|
|
185
|
+
const apiSection = publicApi
|
|
186
|
+
? `
|
|
187
|
+
describe('API security', () => {
|
|
188
|
+
it('rate limiter blocks excessive requests', async () => {
|
|
189
|
+
// for (let i = 0; i < 101; i++) await request(app).get('/api/data');
|
|
190
|
+
// const res = await request(app).get('/api/data');
|
|
191
|
+
// expect(res.status).toBe(429);
|
|
192
|
+
expect(true).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('CORS rejects requests from unauthorized origins', async () => {
|
|
196
|
+
expect(true).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('supports API key rotation without downtime', async () => {
|
|
200
|
+
expect(true).toBe(true);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
`
|
|
204
|
+
: '';
|
|
205
|
+
return `// Security tests for: ${title}
|
|
206
|
+
// Generated by SpecForge SDD MCP Server (SPEC-058b)
|
|
207
|
+
import { describe, it, expect } from 'vitest';
|
|
208
|
+
|
|
209
|
+
describe('${title} — Security Tests', () => {
|
|
210
|
+
${owaspSection}${authSection}${uploadSection}${apiSection}});
|
|
211
|
+
`;
|
|
212
|
+
}
|
|
213
|
+
function buildPythonSecurityTest(title, auth, upload, publicApi) {
|
|
214
|
+
const cls = title.replace(/[^a-zA-Z0-9]/g, '');
|
|
215
|
+
const owaspSection = publicApi
|
|
216
|
+
? `
|
|
217
|
+
def test_sql_injection_rejected(self) -> None:
|
|
218
|
+
"""SQL injection in query parameters is rejected."""
|
|
219
|
+
# response = client.get("/api/users?id=' OR 1=1 --")
|
|
220
|
+
# assert response.status_code == 400
|
|
221
|
+
pass
|
|
222
|
+
|
|
223
|
+
def test_xss_payload_escaped(self) -> None:
|
|
224
|
+
"""XSS payloads are escaped in response."""
|
|
225
|
+
pass
|
|
226
|
+
|
|
227
|
+
def test_csrf_token_validated(self) -> None:
|
|
228
|
+
"""CSRF token is required on state-changing requests."""
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
def test_auth_bypass_rejected(self) -> None:
|
|
232
|
+
"""Auth bypass attempts are rejected."""
|
|
233
|
+
pass
|
|
234
|
+
`
|
|
235
|
+
: '';
|
|
236
|
+
const authSection = auth
|
|
237
|
+
? `
|
|
238
|
+
def test_expired_token_returns_401(self) -> None:
|
|
239
|
+
"""Expired token is rejected with 401."""
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
def test_refresh_token_returns_new_access_token(self) -> None:
|
|
243
|
+
"""Refresh token flow issues a new access token."""
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
def test_privilege_escalation_prevented(self) -> None:
|
|
247
|
+
"""User cannot access admin-only endpoints."""
|
|
248
|
+
pass
|
|
249
|
+
|
|
250
|
+
def test_session_fixation_prevented(self) -> None:
|
|
251
|
+
"""Session fixation attack is prevented."""
|
|
252
|
+
pass
|
|
253
|
+
`
|
|
254
|
+
: '';
|
|
255
|
+
const uploadSection = upload
|
|
256
|
+
? `
|
|
257
|
+
def test_disallowed_file_type_rejected(self) -> None:
|
|
258
|
+
"""Disallowed file types are rejected on upload."""
|
|
259
|
+
pass
|
|
260
|
+
|
|
261
|
+
def test_max_file_size_enforced(self) -> None:
|
|
262
|
+
"""Files exceeding size limit are rejected."""
|
|
263
|
+
pass
|
|
264
|
+
|
|
265
|
+
def test_path_traversal_in_filename_prevented(self) -> None:
|
|
266
|
+
"""Path traversal in filename is sanitized."""
|
|
267
|
+
pass
|
|
268
|
+
|
|
269
|
+
def test_malicious_content_detected(self) -> None:
|
|
270
|
+
"""Malicious file content is detected and rejected."""
|
|
271
|
+
pass
|
|
272
|
+
`
|
|
273
|
+
: '';
|
|
274
|
+
const apiSection = publicApi
|
|
275
|
+
? `
|
|
276
|
+
def test_rate_limiter_blocks_excess(self) -> None:
|
|
277
|
+
"""Rate limiter returns 429 after threshold."""
|
|
278
|
+
pass
|
|
279
|
+
|
|
280
|
+
def test_cors_rejects_unauthorized_origin(self) -> None:
|
|
281
|
+
"""CORS policy rejects requests from unauthorized origins."""
|
|
282
|
+
pass
|
|
283
|
+
|
|
284
|
+
def test_api_key_rotation_supported(self) -> None:
|
|
285
|
+
"""API key rotation works without downtime."""
|
|
286
|
+
pass
|
|
287
|
+
`
|
|
288
|
+
: '';
|
|
289
|
+
return `"""Security tests for: ${title}
|
|
290
|
+
Generated by SpecForge SDD MCP Server (SPEC-058b).
|
|
291
|
+
"""
|
|
292
|
+
import pytest
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class Test${cls}Security:
|
|
296
|
+
"""Security test suite for ${title}."""
|
|
297
|
+
${owaspSection}${authSection}${uploadSection}${apiSection}`;
|
|
298
|
+
}
|
|
299
|
+
export function generateSecurityTestFiles(spec, testDir, framework, language, testExt, autoGenerate, knowledge) {
|
|
300
|
+
const auth = detectAuth(knowledge);
|
|
301
|
+
const upload = detectFileUpload(knowledge);
|
|
302
|
+
const publicApi = hasPublicApi(knowledge);
|
|
303
|
+
const isPython = language === 'python';
|
|
304
|
+
const ext = isPython ? 'py' : testExt;
|
|
305
|
+
const fw = isPython ? 'pytest' : framework;
|
|
306
|
+
const content = isPython
|
|
307
|
+
? buildPythonSecurityTest(spec.title, auth, upload, publicApi)
|
|
308
|
+
: buildTsSecurityTest(spec.title, auth, upload, publicApi);
|
|
309
|
+
return [
|
|
310
|
+
{
|
|
311
|
+
path: `${testDir}/security/${spec.slug}.security.test.${ext}`,
|
|
312
|
+
framework: fw,
|
|
313
|
+
content,
|
|
314
|
+
ready: autoGenerate,
|
|
315
|
+
},
|
|
316
|
+
];
|
|
317
|
+
}
|
|
318
|
+
//# sourceMappingURL=security-test-generator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security-test-generator.js","sourceRoot":"","sources":["../../../../src/tools/generate-tests/generators/security-test-generator.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,qCAAqC;AACrC,4DAA4D;AAI5D,MAAM,YAAY,GAAG;IACnB,KAAK;IACL,OAAO;IACP,UAAU;IACV,UAAU;IACV,OAAO;IACP,OAAO;IACP,UAAU;IACV,SAAS;IACT,gBAAgB;IAChB,iBAAiB;IACjB,WAAW;IACX,OAAO;IACP,aAAa;CACd,CAAC;AACF,MAAM,cAAc,GAAG;IACrB,QAAQ;IACR,YAAY;IACZ,QAAQ;IACR,OAAO;IACP,MAAM;IACN,aAAa;IACb,aAAa;IACb,WAAW;CACZ,CAAC;AAEF,MAAM,UAAU,UAAU,CAAC,SAA2B;IACpD,MAAM,UAAU,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;IAC/D,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;QAC/B,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YAC5C,OAAO,GAAG,CAAC;QACb,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,SAA2B;IAC1D,MAAM,UAAU,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;IAC/D,KAAK,MAAM,GAAG,IAAI,cAAc,EAAE,CAAC;QACjC,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YAC5C,OAAO,GAAG,CAAC;QACb,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,SAA2B;IACtD,IAAI,SAAS,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC;AAC1D,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,SAA2B;IAC/D,IAAI,UAAU,CAAC,SAAS,CAAC,KAAK,IAAI,EAAE,CAAC;QACnC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,YAAY,CAAC,SAAS,CAAC,EAAE,CAAC;QAC5B,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC;AACnF,CAAC;AAED,MAAM,UAAU,wBAAwB,CACtC,KAAa,EACb,OAAe,EACf,QAAgB,EAChB,SAA2B;IAE3B,MAAM,IAAI,GAAqB,EAAE,CAAC;IAElC,IAAI,YAAY,CAAC,SAAS,CAAC,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI,CAAC;YACR,IAAI,EAAE,GAAG,KAAK,oDAAoD;YAClE,IAAI,EAAE,aAAa;YACnB,IAAI,EAAE,GAAG,OAAO,YAAY;YAC5B,WAAW,EAAE,4EAA4E;YACzF,QAAQ,EAAE,UAAU;YACpB,WAAW,EAAE,IAAI;SAClB,CAAC,CAAC;IACL,CAAC;IAED,IAAI,UAAU,CAAC,SAAS,CAAC,KAAK,IAAI,EAAE,CAAC;QACnC,IAAI,CAAC,IAAI,CAAC;YACR,IAAI,EAAE,GAAG,KAAK,+DAA+D;YAC7E,IAAI,EAAE,aAAa;YACnB,IAAI,EAAE,GAAG,OAAO,YAAY;YAC5B,WAAW,EAAE,gFAAgF;YAC7F,QAAQ,EAAE,UAAU;YACpB,WAAW,EAAE,IAAI;SAClB,CAAC,CAAC;IACL,CAAC;IAED,IAAI,gBAAgB,CAAC,SAAS,CAAC,KAAK,IAAI,EAAE,CAAC;QACzC,IAAI,CAAC,IAAI,CAAC;YACR,IAAI,EAAE,GAAG,KAAK,8DAA8D;YAC5E,IAAI,EAAE,aAAa;YACnB,IAAI,EAAE,GAAG,OAAO,YAAY;YAC5B,WAAW,EACT,sFAAsF;YACxF,QAAQ,EAAE,MAAM;YAChB,WAAW,EAAE,IAAI;SAClB,CAAC,CAAC;IACL,CAAC;IAED,IAAI,YAAY,CAAC,SAAS,CAAC,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI,CAAC;YACR,IAAI,EAAE,GAAG,KAAK,wDAAwD;YACtE,IAAI,EAAE,aAAa;YACnB,IAAI,EAAE,GAAG,OAAO,YAAY;YAC5B,WAAW,EAAE,+EAA+E;YAC5F,QAAQ,EAAE,MAAM;YAChB,WAAW,EAAE,IAAI;SAClB,CAAC,CAAC;IACL,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,8EAA8E;AAC9E,oBAAoB;AACpB,8EAA8E;AAE9E,SAAS,mBAAmB,CAC1B,KAAa,EACb,IAAmB,EACnB,MAAqB,EACrB,SAAkB;IAElB,MAAM,YAAY,GAAG,SAAS;QAC5B,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;CAuBL;QACG,CAAC,CAAC,EAAE,CAAC;IAEP,MAAM,WAAW,GAAG,IAAI;QACtB,CAAC,CAAC;8BACwB,IAAI;;;;;;;;;;;;;;;;;;;;CAoBjC;QACG,CAAC,CAAC,EAAE,CAAC;IAEP,MAAM,aAAa,GAAG,MAAM;QAC1B,CAAC,CAAC;2BACqB,MAAM;;;;;;;;;;;;;;;;;;;;;CAqBhC;QACG,CAAC,CAAC,EAAE,CAAC;IAEP,MAAM,UAAU,GAAG,SAAS;QAC1B,CAAC,CAAC;;;;;;;;;;;;;;;;;CAiBL;QACG,CAAC,CAAC,EAAE,CAAC;IAEP,OAAO,0BAA0B,KAAK;;;;YAI5B,KAAK;EACf,YAAY,GAAG,WAAW,GAAG,aAAa,GAAG,UAAU;CACxD,CAAC;AACF,CAAC;AAED,SAAS,uBAAuB,CAC9B,KAAa,EACb,IAAmB,EACnB,MAAqB,EACrB,SAAkB;IAElB,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;IAC/C,MAAM,YAAY,GAAG,SAAS;QAC5B,CAAC,CAAC;;;;;;;;;;;;;;;;;;CAkBL;QACG,CAAC,CAAC,EAAE,CAAC;IAEP,MAAM,WAAW,GAAG,IAAI;QACtB,CAAC,CAAC;;;;;;;;;;;;;;;;CAgBL;QACG,CAAC,CAAC,EAAE,CAAC;IAEP,MAAM,aAAa,GAAG,MAAM;QAC1B,CAAC,CAAC;;;;;;;;;;;;;;;;CAgBL;QACG,CAAC,CAAC,EAAE,CAAC;IAEP,MAAM,UAAU,GAAG,SAAS;QAC1B,CAAC,CAAC;;;;;;;;;;;;CAYL;QACG,CAAC,CAAC,EAAE,CAAC;IAEP,OAAO,0BAA0B,KAAK;;;;;;YAM5B,GAAG;iCACkB,KAAK;EACpC,YAAY,GAAG,WAAW,GAAG,aAAa,GAAG,UAAU,EAAE,CAAC;AAC5D,CAAC;AAED,MAAM,UAAU,yBAAyB,CACvC,IAAqC,EACrC,OAAe,EACf,SAAiB,EACjB,QAAgB,EAChB,OAAe,EACf,YAAqB,EACrB,SAA2B;IAE3B,MAAM,IAAI,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC;IACnC,MAAM,MAAM,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAC;IAC3C,MAAM,SAAS,GAAG,YAAY,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,QAAQ,GAAG,QAAQ,KAAK,QAAQ,CAAC;IACvC,MAAM,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC;IACtC,MAAM,EAAE,GAAG,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;IAC3C,MAAM,OAAO,GAAG,QAAQ;QACtB,CAAC,CAAC,uBAAuB,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC;QAC9D,CAAC,CAAC,mBAAmB,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;IAE7D,OAAO;QACL;YACE,IAAI,EAAE,GAAG,OAAO,aAAa,IAAI,CAAC,IAAI,kBAAkB,GAAG,EAAE;YAC7D,SAAS,EAAE,EAAE;YACb,OAAO;YACP,KAAK,EAAE,YAAY;SACpB;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ProjectKnowledge, TestDefinition, TestFile } from '../../../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Returns true when the project has at least one frontend/fullstack app with a UI framework.
|
|
4
|
+
*/
|
|
5
|
+
export declare function isVisualRegressionProject(knowledge: ProjectKnowledge): boolean;
|
|
6
|
+
declare function hasDarkModeSupport(knowledge: ProjectKnowledge): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Generate visual regression test definitions.
|
|
9
|
+
*/
|
|
10
|
+
export declare function generateVisualRegressionTestDefs(title: string, testDir: string, _testExt: string): TestDefinition[];
|
|
11
|
+
/**
|
|
12
|
+
* Generate visual regression test files for the given spec.
|
|
13
|
+
*/
|
|
14
|
+
export declare function generateVisualRegressionTestFiles(spec: {
|
|
15
|
+
title: string;
|
|
16
|
+
slug: string;
|
|
17
|
+
}, testDir: string, framework: string, language: string, testExt: string, autoGenerate: boolean): TestFile[];
|
|
18
|
+
export { hasDarkModeSupport };
|
|
19
|
+
//# sourceMappingURL=visual-regression-generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visual-regression-generator.d.ts","sourceRoot":"","sources":["../../../../src/tools/generate-tests/generators/visual-regression-generator.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAuC1F;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,gBAAgB,GAAG,OAAO,CAiB9E;AAED,iBAAS,kBAAkB,CAAC,SAAS,EAAE,gBAAgB,GAAG,OAAO,CAGhE;AAED;;GAEG;AACH,wBAAgB,gCAAgC,CAC9C,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,cAAc,EAAE,CA4ClB;AAsMD;;GAEG;AACH,wBAAgB,iCAAiC,CAC/C,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACrC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,OAAO,GACpB,QAAQ,EAAE,CAaZ;AAGD,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// tools/generate-tests/generators/visual-regression-generator.ts — SPEC-058a Section C
|
|
2
|
+
// Generates visual regression and frontend screenshot test scaffolds.
|
|
3
|
+
const UI_FRAMEWORKS = [
|
|
4
|
+
'react',
|
|
5
|
+
'vue',
|
|
6
|
+
'angular',
|
|
7
|
+
'svelte',
|
|
8
|
+
'next',
|
|
9
|
+
'nextjs',
|
|
10
|
+
'next.js',
|
|
11
|
+
'nuxt',
|
|
12
|
+
'sveltekit',
|
|
13
|
+
'svelte-kit',
|
|
14
|
+
'solid',
|
|
15
|
+
'solidjs',
|
|
16
|
+
'preact',
|
|
17
|
+
'qwik',
|
|
18
|
+
'remix',
|
|
19
|
+
'gatsby',
|
|
20
|
+
'astro',
|
|
21
|
+
];
|
|
22
|
+
const DARK_MODE_SIGNALS = [
|
|
23
|
+
'tailwind',
|
|
24
|
+
'shadcn',
|
|
25
|
+
'radix',
|
|
26
|
+
'mui',
|
|
27
|
+
'chakra',
|
|
28
|
+
'mantine',
|
|
29
|
+
'daisy',
|
|
30
|
+
'daisyui',
|
|
31
|
+
'styled-components',
|
|
32
|
+
'emotion',
|
|
33
|
+
];
|
|
34
|
+
const BP_MOBILE = { label: 'mobile', width: 375, height: 812 };
|
|
35
|
+
const BP_TABLET = { label: 'tablet', width: 768, height: 1024 };
|
|
36
|
+
const BP_DESKTOP = { label: 'desktop', width: 1280, height: 800 };
|
|
37
|
+
/**
|
|
38
|
+
* Returns true when the project has at least one frontend/fullstack app with a UI framework.
|
|
39
|
+
*/
|
|
40
|
+
export function isVisualRegressionProject(knowledge) {
|
|
41
|
+
const frontendApps = knowledge.apps.filter((app) => app.type === 'frontend');
|
|
42
|
+
for (const app of frontendApps) {
|
|
43
|
+
const fw = (app.framework ?? '').toLowerCase();
|
|
44
|
+
if (UI_FRAMEWORKS.some((uif) => fw.includes(uif))) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Fall back to top-level framework when no frontend apps matched
|
|
49
|
+
if (frontendApps.length === 0) {
|
|
50
|
+
const topFw = (knowledge.framework ?? '').toLowerCase();
|
|
51
|
+
return UI_FRAMEWORKS.some((uif) => topFw.includes(uif));
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
function hasDarkModeSupport(knowledge) {
|
|
56
|
+
const stackLower = knowledge.stack.map((s) => s.toLowerCase());
|
|
57
|
+
return DARK_MODE_SIGNALS.some((sig) => stackLower.some((s) => s.includes(sig)));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Generate visual regression test definitions.
|
|
61
|
+
*/
|
|
62
|
+
export function generateVisualRegressionTestDefs(title, testDir, _testExt) {
|
|
63
|
+
const defs = [
|
|
64
|
+
{
|
|
65
|
+
name: `${title} — Visual: screenshot baseline matches approved snapshot`,
|
|
66
|
+
type: 'e2e',
|
|
67
|
+
file: `${testDir}/visual/`,
|
|
68
|
+
description: 'Capture full-page screenshot and compare against approved baseline snapshot — fails on unexpected visual changes',
|
|
69
|
+
priority: 'critical',
|
|
70
|
+
automatable: true,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: `${title} — Visual: responsive layout at 375px, 768px, 1280px`,
|
|
74
|
+
type: 'e2e',
|
|
75
|
+
file: `${testDir}/visual/`,
|
|
76
|
+
description: 'Verify layout integrity across mobile (375px), tablet (768px), and desktop (1280px) breakpoints',
|
|
77
|
+
priority: 'high',
|
|
78
|
+
automatable: true,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: `${title} — Visual: component renders without layout shift`,
|
|
82
|
+
type: 'unit',
|
|
83
|
+
file: `${testDir}/visual/`,
|
|
84
|
+
description: 'Component snapshot test — verify no unintended style or markup changes in isolated render',
|
|
85
|
+
priority: 'medium',
|
|
86
|
+
automatable: true,
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
// Dark mode def added unconditionally — caller controls knowledge, but we always include it
|
|
90
|
+
// since isVisualRegressionProject already filtered for UI projects that commonly support dark mode
|
|
91
|
+
defs.push({
|
|
92
|
+
name: `${title} — Visual: dark mode toggle does not break layout`,
|
|
93
|
+
type: 'e2e',
|
|
94
|
+
file: `${testDir}/visual/`,
|
|
95
|
+
description: 'Toggle dark/light mode and verify screenshot matches approved dark-mode snapshot without overflow or invisible text',
|
|
96
|
+
priority: 'medium',
|
|
97
|
+
automatable: true,
|
|
98
|
+
});
|
|
99
|
+
return defs;
|
|
100
|
+
}
|
|
101
|
+
// === Language-specific content generators ===
|
|
102
|
+
function generatePlaywrightTest(title, slug) {
|
|
103
|
+
return `// Visual regression tests for ${title}
|
|
104
|
+
// Requires: @playwright/test with --update-snapshots for initial baseline
|
|
105
|
+
import { test, expect } from '@playwright/test';
|
|
106
|
+
|
|
107
|
+
const BASE_URL = process.env.BASE_URL ?? 'http://localhost:3000';
|
|
108
|
+
|
|
109
|
+
test.describe('${title} — Visual Regression', () => {
|
|
110
|
+
test('full-page screenshot matches baseline', async ({ page }) => {
|
|
111
|
+
await page.goto(BASE_URL);
|
|
112
|
+
await page.waitForLoadState('networkidle');
|
|
113
|
+
await expect(page).toHaveScreenshot('${slug}-baseline.png', {
|
|
114
|
+
fullPage: true,
|
|
115
|
+
maxDiffPixelRatio: 0.02,
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('responsive — mobile 375px', async ({ page }) => {
|
|
120
|
+
await page.setViewportSize({ width: ${BP_MOBILE.width}, height: ${BP_MOBILE.height} });
|
|
121
|
+
await page.goto(BASE_URL);
|
|
122
|
+
await page.waitForLoadState('networkidle');
|
|
123
|
+
await expect(page).toHaveScreenshot('${slug}-mobile.png', {
|
|
124
|
+
maxDiffPixelRatio: 0.02,
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('responsive — tablet 768px', async ({ page }) => {
|
|
129
|
+
await page.setViewportSize({ width: ${BP_TABLET.width}, height: ${BP_TABLET.height} });
|
|
130
|
+
await page.goto(BASE_URL);
|
|
131
|
+
await page.waitForLoadState('networkidle');
|
|
132
|
+
await expect(page).toHaveScreenshot('${slug}-tablet.png', {
|
|
133
|
+
maxDiffPixelRatio: 0.02,
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('responsive — desktop 1280px', async ({ page }) => {
|
|
138
|
+
await page.setViewportSize({ width: ${BP_DESKTOP.width}, height: ${BP_DESKTOP.height} });
|
|
139
|
+
await page.goto(BASE_URL);
|
|
140
|
+
await page.waitForLoadState('networkidle');
|
|
141
|
+
await expect(page).toHaveScreenshot('${slug}-desktop.png', {
|
|
142
|
+
maxDiffPixelRatio: 0.02,
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('component renders without layout shift', async ({ page }) => {
|
|
147
|
+
await page.goto(\`\${BASE_URL}/components\`);
|
|
148
|
+
// TODO: navigate to your component preview/Storybook URL
|
|
149
|
+
await page.waitForLoadState('networkidle');
|
|
150
|
+
await expect(page).toHaveScreenshot('${slug}-component.png');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('dark mode toggle does not break layout', async ({ page }) => {
|
|
154
|
+
await page.goto(BASE_URL);
|
|
155
|
+
await page.waitForLoadState('networkidle');
|
|
156
|
+
// TODO: adjust selector to match your dark mode toggle
|
|
157
|
+
await page.click('[data-testid="theme-toggle"], button[aria-label*="dark"], button[aria-label*="theme"]');
|
|
158
|
+
await page.waitForTimeout(300); // allow CSS transition
|
|
159
|
+
await expect(page).toHaveScreenshot('${slug}-dark-mode.png', {
|
|
160
|
+
maxDiffPixelRatio: 0.02,
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
`;
|
|
165
|
+
}
|
|
166
|
+
function generatePytestPlaywrightTest(title, slug) {
|
|
167
|
+
const pySlug = slug.replace(/-/g, '_');
|
|
168
|
+
return `"""Visual regression tests for ${title} — pytest-playwright."""
|
|
169
|
+
# Requires: pytest-playwright + pytest-playwright-snapshot
|
|
170
|
+
# Install: pip install pytest-playwright playwright-pytest-snapshot
|
|
171
|
+
# Run: pytest --update-snapshots (first time to create baseline)
|
|
172
|
+
import pytest
|
|
173
|
+
from playwright.sync_api import Page, expect
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
BASE_URL = "http://localhost:3000"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@pytest.fixture(scope="session")
|
|
180
|
+
def base_url() -> str:
|
|
181
|
+
import os
|
|
182
|
+
return os.environ.get("BASE_URL", BASE_URL)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_${pySlug}_full_page_screenshot(page: Page, base_url: str, assert_snapshot: object) -> None:
|
|
186
|
+
"""Full-page screenshot matches approved baseline."""
|
|
187
|
+
page.goto(base_url)
|
|
188
|
+
page.wait_for_load_state("networkidle")
|
|
189
|
+
assert_snapshot(page.screenshot(full_page=True), name="${slug}-baseline.png")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_${pySlug}_responsive_mobile(page: Page, base_url: str, assert_snapshot: object) -> None:
|
|
193
|
+
"""Layout intact at 375px (mobile)."""
|
|
194
|
+
page.set_viewport_size({"width": ${BP_MOBILE.width}, "height": ${BP_MOBILE.height}})
|
|
195
|
+
page.goto(base_url)
|
|
196
|
+
page.wait_for_load_state("networkidle")
|
|
197
|
+
assert_snapshot(page.screenshot(), name="${slug}-mobile.png")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_${pySlug}_responsive_tablet(page: Page, base_url: str, assert_snapshot: object) -> None:
|
|
201
|
+
"""Layout intact at 768px (tablet)."""
|
|
202
|
+
page.set_viewport_size({"width": ${BP_TABLET.width}, "height": ${BP_TABLET.height}})
|
|
203
|
+
page.goto(base_url)
|
|
204
|
+
page.wait_for_load_state("networkidle")
|
|
205
|
+
assert_snapshot(page.screenshot(), name="${slug}-tablet.png")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def test_${pySlug}_responsive_desktop(page: Page, base_url: str, assert_snapshot: object) -> None:
|
|
209
|
+
"""Layout intact at 1280px (desktop)."""
|
|
210
|
+
page.set_viewport_size({"width": ${BP_DESKTOP.width}, "height": ${BP_DESKTOP.height}})
|
|
211
|
+
page.goto(base_url)
|
|
212
|
+
page.wait_for_load_state("networkidle")
|
|
213
|
+
assert_snapshot(page.screenshot(), name="${slug}-desktop.png")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def test_${pySlug}_dark_mode(page: Page, base_url: str, assert_snapshot: object) -> None:
|
|
217
|
+
"""Dark mode toggle does not break layout."""
|
|
218
|
+
page.goto(base_url)
|
|
219
|
+
page.wait_for_load_state("networkidle")
|
|
220
|
+
# TODO: adjust selector to match your dark mode toggle
|
|
221
|
+
page.click('[data-testid="theme-toggle"]')
|
|
222
|
+
page.wait_for_timeout(300)
|
|
223
|
+
assert_snapshot(page.screenshot(), name="${slug}-dark-mode.png")
|
|
224
|
+
`;
|
|
225
|
+
}
|
|
226
|
+
function generateCypressVisualTest(title, slug) {
|
|
227
|
+
return `// Visual regression tests for ${title} — Cypress + cypress-image-snapshot
|
|
228
|
+
// Install: npm install --save-dev cypress-image-snapshot
|
|
229
|
+
// Config: import 'cypress-image-snapshot/command' in cypress/support/commands.ts
|
|
230
|
+
describe('${title} — Visual Regression', () => {
|
|
231
|
+
const BASE_URL = Cypress.env('BASE_URL') ?? 'http://localhost:3000';
|
|
232
|
+
|
|
233
|
+
it('full-page screenshot matches baseline', () => {
|
|
234
|
+
cy.visit(BASE_URL);
|
|
235
|
+
cy.matchImageSnapshot('${slug}-baseline', { failureThreshold: 0.02 });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('responsive — mobile 375px', () => {
|
|
239
|
+
cy.viewport(${BP_MOBILE.width}, ${BP_MOBILE.height});
|
|
240
|
+
cy.visit(BASE_URL);
|
|
241
|
+
cy.matchImageSnapshot('${slug}-mobile', { failureThreshold: 0.02 });
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('responsive — tablet 768px', () => {
|
|
245
|
+
cy.viewport(${BP_TABLET.width}, ${BP_TABLET.height});
|
|
246
|
+
cy.visit(BASE_URL);
|
|
247
|
+
cy.matchImageSnapshot('${slug}-tablet', { failureThreshold: 0.02 });
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('responsive — desktop 1280px', () => {
|
|
251
|
+
cy.viewport(${BP_DESKTOP.width}, ${BP_DESKTOP.height});
|
|
252
|
+
cy.visit(BASE_URL);
|
|
253
|
+
cy.matchImageSnapshot('${slug}-desktop', { failureThreshold: 0.02 });
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('dark mode toggle does not break layout', () => {
|
|
257
|
+
cy.visit(BASE_URL);
|
|
258
|
+
// TODO: adjust selector to match your dark mode toggle
|
|
259
|
+
cy.get('[data-testid="theme-toggle"]').click();
|
|
260
|
+
cy.wait(300); // allow CSS transition
|
|
261
|
+
cy.matchImageSnapshot('${slug}-dark-mode', { failureThreshold: 0.02 });
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
`;
|
|
265
|
+
}
|
|
266
|
+
function generateVisualTestContent(title, slug, framework, language) {
|
|
267
|
+
if (language === 'python') {
|
|
268
|
+
return generatePytestPlaywrightTest(title, slug);
|
|
269
|
+
}
|
|
270
|
+
if (framework === 'cypress') {
|
|
271
|
+
return generateCypressVisualTest(title, slug);
|
|
272
|
+
}
|
|
273
|
+
return generatePlaywrightTest(title, slug);
|
|
274
|
+
}
|
|
275
|
+
function getFileExtension(language) {
|
|
276
|
+
const exts = {
|
|
277
|
+
python: 'py',
|
|
278
|
+
go: 'go',
|
|
279
|
+
ruby: 'rb',
|
|
280
|
+
java: 'java',
|
|
281
|
+
javascript: 'test.ts',
|
|
282
|
+
typescript: 'test.ts',
|
|
283
|
+
};
|
|
284
|
+
return exts[language] ?? 'test.ts';
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Generate visual regression test files for the given spec.
|
|
288
|
+
*/
|
|
289
|
+
export function generateVisualRegressionTestFiles(spec, testDir, framework, language, testExt, autoGenerate) {
|
|
290
|
+
const ext = language === 'python' ? 'py' : getFileExtension(language);
|
|
291
|
+
const resolvedFramework = framework === 'cypress' ? 'cypress' : 'playwright';
|
|
292
|
+
const content = generateVisualTestContent(spec.title, spec.slug, framework, language);
|
|
293
|
+
return [
|
|
294
|
+
{
|
|
295
|
+
path: `${testDir}/visual/${spec.slug}.visual.${ext === 'test.ts' ? testExt : ext}`,
|
|
296
|
+
framework: resolvedFramework,
|
|
297
|
+
content,
|
|
298
|
+
ready: autoGenerate,
|
|
299
|
+
},
|
|
300
|
+
];
|
|
301
|
+
}
|
|
302
|
+
// Re-export for dark mode detection utility (used in spec-dispatcher if needed)
|
|
303
|
+
export { hasDarkModeSupport };
|
|
304
|
+
//# sourceMappingURL=visual-regression-generator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visual-regression-generator.js","sourceRoot":"","sources":["../../../../src/tools/generate-tests/generators/visual-regression-generator.ts"],"names":[],"mappings":"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"}
|