viberadar 0.3.46 β 0.3.47
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/server/index.d.ts.map +1 -1
- package/dist/server/index.js +233 -305
- package/dist/server/index.js.map +1 -1
- package/dist/ui/dashboard.html +898 -395
- package/package.json +43 -43
package/dist/server/index.js
CHANGED
|
@@ -123,90 +123,68 @@ function fmtTool(name, input = {}) {
|
|
|
123
123
|
default: return `π§ ${name}${fp ? ': ' + fp : ''}`;
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
|
-
function e2ePlanDir(projectRoot) {
|
|
127
|
-
return path.join(projectRoot, '.viberadar', 'e2e-plans');
|
|
128
|
-
}
|
|
129
|
-
function e2ePlanPath(projectRoot, featureKey) {
|
|
130
|
-
return path.join(e2ePlanDir(projectRoot), `${featureKey}.json`);
|
|
131
|
-
}
|
|
132
|
-
function loadE2ePlan(projectRoot, featureKey) {
|
|
133
|
-
const fp = e2ePlanPath(projectRoot, featureKey);
|
|
134
|
-
try {
|
|
135
|
-
return JSON.parse(fs.readFileSync(fp, 'utf-8'));
|
|
136
|
-
}
|
|
137
|
-
catch {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
function saveE2ePlan(projectRoot, plan) {
|
|
142
|
-
const dir = e2ePlanDir(projectRoot);
|
|
143
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
144
|
-
plan.updatedAt = new Date().toISOString();
|
|
145
|
-
fs.writeFileSync(e2ePlanPath(projectRoot, plan.featureKey), JSON.stringify(plan, null, 2), 'utf-8');
|
|
146
|
-
}
|
|
147
|
-
function e2eScreenshotDir(projectRoot, featureKey) {
|
|
148
|
-
return path.join(projectRoot, '.viberadar', 'e2e-screenshots', featureKey);
|
|
149
|
-
}
|
|
150
|
-
function collectScreenshots(projectRoot, featureKey) {
|
|
151
|
-
const baseDir = e2eScreenshotDir(projectRoot, featureKey);
|
|
152
|
-
const result = new Map();
|
|
153
|
-
if (!fs.existsSync(baseDir))
|
|
154
|
-
return result;
|
|
155
|
-
try {
|
|
156
|
-
for (const entry of fs.readdirSync(baseDir, { withFileTypes: true })) {
|
|
157
|
-
if (!entry.isDirectory())
|
|
158
|
-
continue;
|
|
159
|
-
const tcPath = path.join(baseDir, entry.name);
|
|
160
|
-
const screenshots = fs.readdirSync(tcPath)
|
|
161
|
-
.filter(f => /\.(png|jpg|jpeg|webp)$/i.test(f))
|
|
162
|
-
.map(f => `${featureKey}/${entry.name}/${f}`);
|
|
163
|
-
result.set(entry.name, screenshots);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
catch { }
|
|
167
|
-
return result;
|
|
168
|
-
}
|
|
169
|
-
function hasPlaywright(projectRoot) {
|
|
170
|
-
try {
|
|
171
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf-8'));
|
|
172
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
173
|
-
return !!deps['@playwright/test'] || !!deps['playwright'];
|
|
174
|
-
}
|
|
175
|
-
catch {
|
|
176
|
-
return false;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
126
|
// βββ Test runner after agent ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
180
127
|
const TEST_FILE_RE = /\.(test|spec)\.(ts|tsx|js|jsx)$/;
|
|
181
128
|
function runTestFiles(files, projectRoot) {
|
|
182
129
|
return new Promise((resolve) => {
|
|
183
|
-
|
|
130
|
+
// Write JSON to a temp file β avoids stdout encoding/regex issues on Windows
|
|
131
|
+
const tmpDir = path.join(projectRoot, '.viberadar');
|
|
132
|
+
const tmpFile = path.join(tmpDir, '_test-results.json');
|
|
133
|
+
try {
|
|
134
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
135
|
+
}
|
|
136
|
+
catch { }
|
|
137
|
+
try {
|
|
138
|
+
fs.unlinkSync(tmpFile);
|
|
139
|
+
}
|
|
140
|
+
catch { }
|
|
141
|
+
const proc = (0, child_process_1.spawn)('npx', ['vitest', 'run', '--reporter=json', `--outputFile=${tmpFile}`, ...files], { cwd: projectRoot, shell: true, stdio: 'pipe' });
|
|
184
142
|
let stdout = '';
|
|
185
|
-
proc.stdout?.on('data', (d) => stdout += d.toString());
|
|
143
|
+
proc.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
186
144
|
proc.on('close', () => {
|
|
187
145
|
try {
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
146
|
+
// Prefer the output file (reliable); fall back to stdout
|
|
147
|
+
let jsonStr;
|
|
148
|
+
if (fs.existsSync(tmpFile)) {
|
|
149
|
+
jsonStr = fs.readFileSync(tmpFile, 'utf-8').trim();
|
|
150
|
+
process.stdout.write(` β
vitest outputFile: ${tmpFile} (${jsonStr.length} bytes)\n`);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
process.stdout.write(` β οΈ vitest outputFile not found, falling back to stdout (${stdout.length} bytes)\n`);
|
|
154
|
+
const match = stdout.match(/\{[\s\S]*"testResults"[\s\S]*\}/);
|
|
155
|
+
jsonStr = match ? match[0] : stdout.trim();
|
|
156
|
+
}
|
|
157
|
+
const json = JSON.parse(jsonStr);
|
|
158
|
+
process.stdout.write(` π vitest JSON: passed=${json.numPassedTests} failed=${json.numFailedTests} testResults=${(json.testResults ?? []).length}\n`);
|
|
191
159
|
// Extract per-file failure details
|
|
192
160
|
const fileDetails = {};
|
|
193
161
|
for (const tr of (json.testResults ?? [])) {
|
|
194
162
|
const fp = tr.testFilePath ?? '';
|
|
163
|
+
if (!fp)
|
|
164
|
+
continue;
|
|
165
|
+
const assertions = tr.assertionResults ?? tr.testResults ?? [];
|
|
195
166
|
const errors = [];
|
|
196
|
-
for (const ar of
|
|
167
|
+
for (const ar of assertions) {
|
|
197
168
|
if (ar.status === 'failed') {
|
|
198
169
|
errors.push({
|
|
199
170
|
testName: ar.fullName ?? ar.title ?? 'unknown',
|
|
200
|
-
message: (ar.failureMessages?.[0] ?? '').split('\n')[0].slice(0, 300),
|
|
171
|
+
message: (ar.failureMessages?.[0] ?? ar.errors?.[0]?.message ?? '').split('\n')[0].slice(0, 300),
|
|
201
172
|
});
|
|
202
173
|
}
|
|
203
174
|
}
|
|
204
175
|
fileDetails[fp] = {
|
|
205
|
-
passed:
|
|
176
|
+
passed: assertions.filter((a) => a.status === 'passed').length,
|
|
206
177
|
failed: errors.length,
|
|
207
178
|
errors,
|
|
208
179
|
};
|
|
209
180
|
}
|
|
181
|
+
const failedFiles = Object.entries(fileDetails).filter(([, d]) => d.failed > 0);
|
|
182
|
+
process.stdout.write(` π fileDetails: ${Object.keys(fileDetails).length} files, ${failedFiles.length} with failures\n`);
|
|
183
|
+
if (failedFiles.length > 0) {
|
|
184
|
+
for (const [fp, d] of failedFiles) {
|
|
185
|
+
process.stdout.write(` β ${fp} β ${d.failed} failed\n`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
210
188
|
resolve({
|
|
211
189
|
passed: json.numPassedTests ?? 0,
|
|
212
190
|
failed: json.numFailedTests ?? 0,
|
|
@@ -214,58 +192,106 @@ function runTestFiles(files, projectRoot) {
|
|
|
214
192
|
fileDetails,
|
|
215
193
|
});
|
|
216
194
|
}
|
|
217
|
-
catch {
|
|
195
|
+
catch (err) {
|
|
196
|
+
process.stdout.write(` β runTestFiles parse error: ${err.message}\n`);
|
|
218
197
|
resolve({ passed: 0, failed: 0, files, fileDetails: {} });
|
|
219
198
|
}
|
|
220
199
|
});
|
|
221
200
|
proc.on('error', () => resolve({ passed: 0, failed: 0, files, fileDetails: {} }));
|
|
222
201
|
});
|
|
223
202
|
}
|
|
203
|
+
// βββ E2E plan storage βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
204
|
+
function e2ePlanDir(projectRoot) {
|
|
205
|
+
return path.join(projectRoot, '.viberadar', 'e2e-plans');
|
|
206
|
+
}
|
|
207
|
+
function e2ePlanPath(projectRoot, featureKey) {
|
|
208
|
+
return path.join(e2ePlanDir(projectRoot), `${featureKey}.json`);
|
|
209
|
+
}
|
|
210
|
+
function loadE2ePlan(projectRoot, featureKey) {
|
|
211
|
+
try {
|
|
212
|
+
const p = e2ePlanPath(projectRoot, featureKey);
|
|
213
|
+
if (!fs.existsSync(p))
|
|
214
|
+
return null;
|
|
215
|
+
return JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function saveE2ePlan(projectRoot, plan) {
|
|
222
|
+
const dir = e2ePlanDir(projectRoot);
|
|
223
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
224
|
+
plan.updatedAt = new Date().toISOString();
|
|
225
|
+
fs.writeFileSync(e2ePlanPath(projectRoot, plan.featureKey), JSON.stringify(plan, null, 2), 'utf-8');
|
|
226
|
+
}
|
|
227
|
+
function e2eScreenshotDir(projectRoot, featureKey) {
|
|
228
|
+
return path.join(projectRoot, '.viberadar', 'e2e-screenshots', featureKey);
|
|
229
|
+
}
|
|
230
|
+
function collectScreenshots(projectRoot, featureKey, testCaseId) {
|
|
231
|
+
const dir = path.join(e2eScreenshotDir(projectRoot, featureKey), testCaseId);
|
|
232
|
+
try {
|
|
233
|
+
if (!fs.existsSync(dir))
|
|
234
|
+
return [];
|
|
235
|
+
return fs.readdirSync(dir)
|
|
236
|
+
.filter(f => /\.(png|jpg|jpeg|webp)$/i.test(f))
|
|
237
|
+
.sort()
|
|
238
|
+
.map(f => `${featureKey}/${testCaseId}/${f}`);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return [];
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function hasPlaywright(projectRoot) {
|
|
245
|
+
try {
|
|
246
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf-8'));
|
|
247
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
248
|
+
return !!deps['@playwright/test'] || !!deps['playwright'];
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// βββ Playwright runner ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
224
255
|
function runPlaywrightTests(files, projectRoot) {
|
|
225
256
|
return new Promise((resolve) => {
|
|
226
|
-
const proc = (0, child_process_1.spawn)('npx', ['playwright', 'test',
|
|
257
|
+
const proc = (0, child_process_1.spawn)('npx', ['playwright', 'test', '--reporter=json', ...files], { cwd: projectRoot, shell: true, stdio: 'pipe' });
|
|
227
258
|
let stdout = '';
|
|
228
|
-
proc.stdout?.on('data', (d) => stdout += d.toString());
|
|
259
|
+
proc.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
229
260
|
proc.on('close', () => {
|
|
230
261
|
try {
|
|
231
262
|
const match = stdout.match(/\{[\s\S]*"suites"[\s\S]*\}/);
|
|
232
263
|
const json = JSON.parse(match ? match[0] : stdout);
|
|
233
|
-
const
|
|
264
|
+
const results = {};
|
|
265
|
+
const errors = {};
|
|
234
266
|
let passed = 0, failed = 0;
|
|
235
267
|
const walkSuites = (suites) => {
|
|
236
|
-
for (const suite of suites) {
|
|
237
|
-
for (const spec of
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
title: spec.title || '',
|
|
250
|
-
status,
|
|
251
|
-
error: result.error?.message?.slice(0, 500),
|
|
252
|
-
duration: result.duration || 0,
|
|
253
|
-
});
|
|
254
|
-
}
|
|
268
|
+
for (const suite of suites ?? []) {
|
|
269
|
+
for (const spec of suite.specs ?? []) {
|
|
270
|
+
const ok = spec.tests?.every((t) => t.results?.every((r) => r.status === 'passed'));
|
|
271
|
+
const title = spec.title ?? suite.title ?? 'unknown';
|
|
272
|
+
if (ok) {
|
|
273
|
+
results[title] = 'passed';
|
|
274
|
+
passed++;
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
results[title] = 'failed';
|
|
278
|
+
failed++;
|
|
279
|
+
const errMsg = spec.tests?.[0]?.results?.[0]?.error?.message ?? 'Test failed';
|
|
280
|
+
errors[title] = errMsg.split('\n')[0].slice(0, 300);
|
|
255
281
|
}
|
|
256
282
|
}
|
|
257
283
|
if (suite.suites)
|
|
258
284
|
walkSuites(suite.suites);
|
|
259
285
|
}
|
|
260
286
|
};
|
|
261
|
-
walkSuites(json.suites
|
|
262
|
-
resolve({ passed, failed,
|
|
287
|
+
walkSuites(json.suites ?? []);
|
|
288
|
+
resolve({ passed, failed, results, errors });
|
|
263
289
|
}
|
|
264
290
|
catch {
|
|
265
|
-
resolve({ passed: 0, failed: 0,
|
|
291
|
+
resolve({ passed: 0, failed: 0, results: {}, errors: {} });
|
|
266
292
|
}
|
|
267
293
|
});
|
|
268
|
-
proc.on('error', () => resolve({ passed: 0, failed: 0,
|
|
294
|
+
proc.on('error', () => resolve({ passed: 0, failed: 0, results: {}, errors: {} }));
|
|
269
295
|
});
|
|
270
296
|
}
|
|
271
297
|
// βββ Prompt builders ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -392,92 +418,64 @@ function buildMapUnmappedPrompt(modules, features) {
|
|
|
392
418
|
...unmapped,
|
|
393
419
|
].join('\n');
|
|
394
420
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
const srcFiles = modules
|
|
421
|
+
function buildE2ePlanPrompt(feat, modules) {
|
|
422
|
+
const files = modules
|
|
398
423
|
.filter(m => m.featureKeys.includes(feat.key) && m.type !== 'test' && !m.isInfra)
|
|
399
424
|
.map(m => '- ' + m.relativePath.replace(/\\/g, '/'));
|
|
400
|
-
const pageFiles = modules
|
|
401
|
-
.filter(m => m.featureKeys.includes(feat.key) &&
|
|
402
|
-
(/page\./i.test(m.name) || /route/i.test(m.name) || /\/(pages|app)\//i.test(m.relativePath)))
|
|
403
|
-
.map(m => '- ' + m.relativePath.replace(/\\/g, '/'));
|
|
404
|
-
const pwConfigExists = fs.existsSync(path.join(projectRoot, 'playwright.config.ts')) ||
|
|
405
|
-
fs.existsSync(path.join(projectRoot, 'playwright.config.js'));
|
|
406
425
|
return [
|
|
407
|
-
|
|
408
|
-
``,
|
|
409
|
-
`ΠΡΠΎΡΠΈΡΠ°ΠΉ ΠΈΡΡ
ΠΎΠ΄Π½ΡΠΉ ΠΊΠΎΠ΄ ΡΠΈΡΠΈ ΡΡΠΎΠ±Ρ ΠΏΠΎΠ½ΡΡΡ UI: ΡΠΎΡΠΌΡ, ΠΊΠ½ΠΎΠΏΠΊΠΈ, Π½Π°Π²ΠΈΠ³Π°ΡΠΈΡ, ΡΠΎΡΡΠΎΡΠ½ΠΈΡ, API-Π²ΡΠ·ΠΎΠ²Ρ.`,
|
|
426
|
+
`Π’Ρ β QA-ΠΈΠ½ΠΆΠ΅Π½Π΅Ρ. ΠΡΠΎΠ°Π½Π°Π»ΠΈΠ·ΠΈΡΡΠΉ ΠΈΡΡ
ΠΎΠ΄Π½ΡΠΉ ΠΊΠΎΠ΄ ΡΠΈΡΠΈ "${feat.label}" ΠΈ ΡΠΎΡΡΠ°Π²Ρ ΠΏΠΎΠ΄ΡΠΎΠ±Π½ΡΠΉ ΠΏΠ»Π°Π½ E2E-ΡΠ΅ΡΡΠΈΡΠΎΠ²Π°Π½ΠΈΡ.`,
|
|
410
427
|
``,
|
|
411
|
-
|
|
412
|
-
...
|
|
428
|
+
`Π€Π°ΠΉΠ»Ρ ΡΠΈΡΠΈ:`,
|
|
429
|
+
...files,
|
|
413
430
|
``,
|
|
414
|
-
|
|
431
|
+
`ΠΡΠΎΡΠΈΡΠ°ΠΉ ΡΡΠΈ ΡΠ°ΠΉΠ»Ρ, ΠΈΠ·ΡΡΠΈ UI: ΡΠΎΡΠΌΡ, ΠΊΠ½ΠΎΠΏΠΊΠΈ, Π½Π°Π²ΠΈΠ³Π°ΡΠΈΡ, Π±ΠΈΠ·Π½Π΅Ρ-Π»ΠΎΠ³ΠΈΠΊΡ.`,
|
|
415
432
|
``,
|
|
416
|
-
|
|
417
|
-
`- ΠΠ°ΠΆΠ΄ΡΠΉ ΡΠ΅ΡΡ-ΠΊΠ΅ΠΉΡ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΏΡΠΎΠ²Π΅ΡΡΡΡ ΠΎΠ΄ΠΈΠ½ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»ΡΡΠΊΠΈΠΉ ΡΡΠ΅Π½Π°ΡΠΈΠΉ`,
|
|
418
|
-
`- ΠΠΊΠ»ΡΡΠΈ happy path, edge cases, ΠΎΡΠΈΠ±ΠΊΠΈ Π²Π°Π»ΠΈΠ΄Π°ΡΠΈΠΈ`,
|
|
419
|
-
`- Steps Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΊΠΎΠ½ΠΊΡΠ΅ΡΠ½ΡΠΌΠΈ: ΡΠΊΠ°ΠΆΠΈ ΡΠ΅Π»Π΅ΠΊΡΠΎΡΡ, URL, ΡΠ΅ΠΊΡΡ ΠΊΠ½ΠΎΠΏΠΎΠΊ ΠΈΠ· ΡΠ΅Π°Π»ΡΠ½ΠΎΠ³ΠΎ ΠΊΠΎΠ΄Π°`,
|
|
420
|
-
`- id ΡΠ΅ΡΡ-ΠΊΠ΅ΠΉΡΠ°: ${feat.key}-<scenario>-NN (Π½Π°ΠΏΡΠΈΠΌΠ΅Ρ: ${feat.key}-login-01)`,
|
|
421
|
-
``,
|
|
422
|
-
`ΠΡΠ²Π΅Ρ Π²Π΅ΡΠ½ΠΈ Π‘Π’Π ΠΠΠ Π² ΡΠΎΡΠΌΠ°ΡΠ΅ JSON (Π±Π΅Π· markdown-Π±Π»ΠΎΠΊΠΎΠ², Π±Π΅Π· ΠΊΠΎΠΌΠΌΠ΅Π½ΡΠ°ΡΠΈΠ΅Π², ΡΠΈΡΡΡΠΉ JSON):`,
|
|
433
|
+
`ΠΠ΅ΡΠ½ΠΈ Π’ΠΠΠ¬ΠΠ Π²Π°Π»ΠΈΠ΄Π½ΡΠΉ JSON Π±Π΅Π· markdown-Π±Π»ΠΎΠΊΠΎΠ² Π² ΡΠΎΡΠΌΠ°ΡΠ΅:`,
|
|
423
434
|
`{`,
|
|
424
435
|
` "baseUrl": "http://localhost:3000",`,
|
|
425
436
|
` "testCases": [`,
|
|
426
437
|
` {`,
|
|
427
|
-
` "id": "
|
|
428
|
-
` "name": "
|
|
429
|
-
` "description": "Π§ΡΠΎ ΠΏΡΠΎΠ²Π΅ΡΡΠ΅Ρ
|
|
430
|
-
` "steps": [
|
|
431
|
-
`
|
|
432
|
-
` "Fill [data-testid=email] with test@example.com",`,
|
|
433
|
-
` "Click button:has-text('Submit')"`,
|
|
434
|
-
` ],`,
|
|
435
|
-
` "expectedResults": [`,
|
|
436
|
-
` "URL changes to /dashboard",`,
|
|
437
|
-
` "Text 'Welcome' is visible"`,
|
|
438
|
-
` ]`,
|
|
438
|
+
` "id": "feature-key-01",`,
|
|
439
|
+
` "name": "ΠΡΠ°ΡΠΊΠΎΠ΅ Π½Π°Π·Π²Π°Π½ΠΈΠ΅ ΡΠ΅ΡΡΠ°",`,
|
|
440
|
+
` "description": "Π§ΡΠΎ ΠΏΡΠΎΠ²Π΅ΡΡΠ΅Ρ ΡΠ΅ΡΡ",`,
|
|
441
|
+
` "steps": ["Π¨Π°Π³ 1", "Π¨Π°Π³ 2", "Π¨Π°Π³ 3"],`,
|
|
442
|
+
` "expectedResults": ["ΠΠΆΠΈΠ΄Π°Π΅ΠΌΡΠΉ ΡΠ΅Π·ΡΠ»ΡΡΠ°Ρ 1", "ΠΠΆΠΈΠ΄Π°Π΅ΠΌΡΠΉ ΡΠ΅Π·ΡΠ»ΡΡΠ°Ρ 2"]`,
|
|
439
443
|
` }`,
|
|
440
444
|
` ]`,
|
|
441
445
|
`}`,
|
|
442
446
|
``,
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
447
|
+
`Π’ΡΠ΅Π±ΠΎΠ²Π°Π½ΠΈΡ:`,
|
|
448
|
+
`- 5-15 ΡΠ΅ΡΡ-ΠΊΠ΅ΠΉΡΠΎΠ², ΠΏΠΎΠΊΡΡΠ²Π°ΡΡΠΈΡ
ΠΎΡΠ½ΠΎΠ²Π½ΡΠ΅ ΡΡΠ΅Π½Π°ΡΠΈΠΈ`,
|
|
449
|
+
`- ΠΠ°ΠΆΠ΄ΡΠΉ ΠΊΠ΅ΠΉΡ: happy path, edge cases, ΠΎΠ±ΡΠ°Π±ΠΎΡΠΊΠ° ΠΎΡΠΈΠ±ΠΎΠΊ`,
|
|
450
|
+
`- id: ΡΡΡΠΎΡΠ½ΡΠ΅ Π±ΡΠΊΠ²Ρ ΠΈ ΡΠΈΡΡΡ ΡΠ΅ΡΠ΅Π· Π΄Π΅ΡΠΈΡ, ΡΠ½ΠΈΠΊΠ°Π»ΡΠ½ΡΠΉ`,
|
|
451
|
+
`- Π’ΠΎΠ»ΡΠΊΠΎ JSON, Π±Π΅Π· ΠΎΠ±ΡΡΡΠ½Π΅Π½ΠΈΠΉ`,
|
|
452
|
+
].join('\n');
|
|
446
453
|
}
|
|
447
454
|
function buildWriteE2eTestPrompt(feat, plan, modules) {
|
|
448
455
|
const approvedCases = plan.testCases.filter(tc => tc.status === 'approved');
|
|
449
456
|
const existingE2e = modules
|
|
450
|
-
.filter(m => m.
|
|
457
|
+
.filter(m => m.featureKeys.includes(feat.key) && m.type === 'e2e')
|
|
451
458
|
.map(m => '- ' + m.relativePath.replace(/\\/g, '/'));
|
|
452
459
|
return [
|
|
453
|
-
`ΠΠ°ΠΏΠΈΡΠΈ Playwright E2E
|
|
454
|
-
`Base URL: ${plan.baseUrl || 'http://localhost:3000'}`,
|
|
455
|
-
``,
|
|
456
|
-
`Π£ΡΠ²Π΅ΡΠΆΠ΄ΡΠ½Π½ΡΠ΅ ΡΠ΅ΡΡ-ΠΊΠ΅ΠΉΡΡ (${approvedCases.length}):`,
|
|
460
|
+
`ΠΠ°ΠΏΠΈΡΠΈ Playwright E2E ΡΠ΅ΡΡΡ Π΄Π»Ρ ΡΠΈΡΠΈ "${feat.label}".`,
|
|
457
461
|
``,
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
...tc.expectedResults.map(r => ` - ${r}`),
|
|
465
|
-
``,
|
|
462
|
+
`Approved ΡΠ΅ΡΡ-ΠΊΠ΅ΠΉΡΡ (${approvedCases.length}):`,
|
|
463
|
+
...approvedCases.map(tc => [
|
|
464
|
+
`### ${tc.name} (id: ${tc.id})`,
|
|
465
|
+
`${tc.description}`,
|
|
466
|
+
`Π¨Π°Π³ΠΈ: ${tc.steps.join(' β ')}`,
|
|
467
|
+
`ΠΠΆΠΈΠ΄Π°Π΅ΡΡΡ: ${tc.expectedResults.join('; ')}`,
|
|
466
468
|
].join('\n')),
|
|
467
469
|
``,
|
|
468
|
-
existingE2e.length > 0
|
|
469
|
-
? `Π‘ΡΡΠ΅ΡΡΠ²ΡΡΡΠΈΠ΅ E2E ΡΠ΅ΡΡΡ (ΡΠ»Π΅Π΄ΡΠΉ ΡΡΠΈΠΌ ΠΏΠ°ΡΡΠ΅ΡΠ½Π°ΠΌ):\n${existingE2e.join('\n')}`
|
|
470
|
-
: 'Π‘ΡΡΠ΅ΡΡΠ²ΡΡΡΠΈΡ
E2E ΡΠ΅ΡΡΠΎΠ² ΠΏΠΎΠΊΠ° Π½Π΅Ρ.',
|
|
470
|
+
existingE2e.length > 0 ? `Π‘ΡΡΠ΅ΡΡΠ²ΡΡΡΠΈΠ΅ E2E ΡΠ΅ΡΡΡ (ΡΠ»Π΅Π΄ΡΠΉ ΠΏΠ°ΡΡΠ΅ΡΠ½Π°ΠΌ):\n${existingE2e.join('\n')}` : '',
|
|
471
471
|
``,
|
|
472
472
|
`Π’ΡΠ΅Π±ΠΎΠ²Π°Π½ΠΈΡ:`,
|
|
473
|
-
`-
|
|
474
|
-
`-
|
|
475
|
-
`-
|
|
476
|
-
`-
|
|
477
|
-
|
|
478
|
-
`-
|
|
479
|
-
`- ΠΠΎΠ±Π°Π²Ρ page.waitForLoadState() ΠΈ Π΄ΡΡΠ³ΠΈΠ΅ ΠΎΠΆΠΈΠ΄Π°Π½ΠΈΡ`,
|
|
480
|
-
`- ΠΠ΅ ΠΈΠ·ΠΌΠ΅Π½ΡΠΉ ΡΡΡΠ΅ΡΡΠ²ΡΡΡΠΈΠ΅ ΡΠ΅ΡΡΡ`,
|
|
473
|
+
`- Π‘ΠΎΠ·Π΄Π°ΠΉ ΡΠ°ΠΉΠ»Ρ Π² Π΄ΠΈΡΠ΅ΠΊΡΠΎΡΠΈΠΈ e2e/${feat.key}/`,
|
|
474
|
+
`- ΠΡΠΏΠΎΠ»ΡΠ·ΡΠΉ @playwright/test`,
|
|
475
|
+
`- baseUrl: ${plan.baseUrl || 'http://localhost:3000'}`,
|
|
476
|
+
`- ΠΠΎΡΠ»Π΅ ΠΊΠ»ΡΡΠ΅Π²ΡΡ
ΡΠ°Π³ΠΎΠ² Π΄ΠΎΠ±Π°Π²Ρ ΡΠΊΡΠΈΠ½ΡΠΎΡΡ: await page.screenshot({ path: '.viberadar/e2e-screenshots/${feat.key}/<testCaseId>/step-N.png' })`,
|
|
477
|
+
`- ΠΡΡΠΏΠΏΠΈΡΡΠΉ ΠΏΠΎ test case id (ΠΎΠ΄ΠΈΠ½ ΡΠ°ΠΉΠ» Π½Π° ΠΊΠ΅ΠΉΡ ΠΈΠ»ΠΈ Π½Π΅ΡΠΊΠΎΠ»ΡΠΊΠΎ ΠΊΠ΅ΠΉΡΠΎΠ² Π² ΠΎΠ΄Π½ΠΎΠΌ ΡΠ°ΠΉΠ»Π΅)`,
|
|
478
|
+
`- Π‘Π»Π΅Π΄ΡΠΉ ΡΡΡΠ΅ΡΡΠ²ΡΡΡΠΈΠΌ ΠΏΠ°ΡΡΠ΅ΡΠ½Π°ΠΌ ΠΏΡΠΎΠ΅ΠΊΡΠ°`,
|
|
481
479
|
].filter(Boolean).join('\n');
|
|
482
480
|
}
|
|
483
481
|
// βββ Coverage provider auto-install ββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -705,27 +703,18 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
705
703
|
broadcast('agent-error', { message: `Π€ΠΈΡΠ° Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Π°: ${featureKey}` });
|
|
706
704
|
return;
|
|
707
705
|
}
|
|
708
|
-
prompt = buildE2ePlanPrompt(feat, currentData.modules
|
|
706
|
+
prompt = buildE2ePlanPrompt(feat, currentData.modules);
|
|
709
707
|
title = `${agentLabel} β E2E ΠΏΠ»Π°Π½ Π΄Π»Ρ "${feat.label}"`;
|
|
710
708
|
}
|
|
711
709
|
else if (task === 'write-e2e-tests') {
|
|
712
710
|
const feat = currentData.features?.find(f => f.key === featureKey);
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
}
|
|
717
|
-
const plan = loadE2ePlan(projectRoot, feat.key);
|
|
718
|
-
if (!plan) {
|
|
719
|
-
broadcast('agent-error', { message: `E2E ΠΏΠ»Π°Π½ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½ Π΄Π»Ρ "${feat.label}". Π‘Π½Π°ΡΠ°Π»Π° ΡΠ³Π΅Π½Π΅ΡΠΈΡΡΠΉ ΠΏΠ»Π°Π½.` });
|
|
720
|
-
return;
|
|
721
|
-
}
|
|
722
|
-
const approved = plan.testCases.filter(tc => tc.status === 'approved');
|
|
723
|
-
if (approved.length === 0) {
|
|
724
|
-
broadcast('agent-error', { message: `ΠΠ΅Ρ ΠΎΠ΄ΠΎΠ±ΡΠ΅Π½Π½ΡΡ
ΡΠ΅ΡΡ-ΠΊΠ΅ΠΉΡΠΎΠ² Π² ΠΏΠ»Π°Π½Π΅ "${feat.label}"` });
|
|
711
|
+
const plan = featureKey ? loadE2ePlan(projectRoot, featureKey) : null;
|
|
712
|
+
if (!feat || !plan) {
|
|
713
|
+
broadcast('agent-error', { message: `Π€ΠΈΡΠ° ΠΈΠ»ΠΈ ΠΏΠ»Π°Π½ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Ρ: ${featureKey}` });
|
|
725
714
|
return;
|
|
726
715
|
}
|
|
727
716
|
prompt = buildWriteE2eTestPrompt(feat, plan, currentData.modules);
|
|
728
|
-
title = `${agentLabel} β E2E ΡΠ΅ΡΡΡ Π΄Π»Ρ "${feat.label}"
|
|
717
|
+
title = `${agentLabel} β ΠΏΠΈΡΡ E2E ΡΠ΅ΡΡΡ Π΄Π»Ρ "${feat.label}"`;
|
|
729
718
|
}
|
|
730
719
|
else {
|
|
731
720
|
prompt = buildMapUnmappedPrompt(currentData.modules, currentData.features || []);
|
|
@@ -754,7 +743,7 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
754
743
|
broadcast('agent-output', { line: `π ΠΠ°Π΄Π°ΡΠ° Π·Π°ΠΏΠΈΡΠ°Π½Π° Π² .viberadar/task.md` });
|
|
755
744
|
// Track test files written/edited by agent (for auto-run after)
|
|
756
745
|
const createdTestFiles = [];
|
|
757
|
-
// Accumulate full result text
|
|
746
|
+
// Accumulate full result text for E2E plan parsing
|
|
758
747
|
let agentResultText = '';
|
|
759
748
|
function trackWrittenFiles(raw) {
|
|
760
749
|
try {
|
|
@@ -786,11 +775,10 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
786
775
|
return;
|
|
787
776
|
}
|
|
788
777
|
if (parsed.startsWith('Β§RESULTΒ§')) {
|
|
789
|
-
const resultBody = parsed.slice('Β§RESULTΒ§'.length);
|
|
790
|
-
agentResultText += resultBody + '\n';
|
|
791
778
|
// Full result summary β split into lines and prefix with indent
|
|
779
|
+
agentResultText = parsed.slice('Β§RESULTΒ§'.length).trim();
|
|
792
780
|
broadcast('agent-output', { line: 'βββββββββββββββββββββββββββββ' });
|
|
793
|
-
for (const l of
|
|
781
|
+
for (const l of agentResultText.split('\n')) {
|
|
794
782
|
if (l.trim())
|
|
795
783
|
broadcast('agent-output', { line: ' ' + l });
|
|
796
784
|
}
|
|
@@ -843,61 +831,36 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
843
831
|
}
|
|
844
832
|
broadcast('agent-summary', result);
|
|
845
833
|
}
|
|
846
|
-
//
|
|
834
|
+
// E2E plan post-processing
|
|
847
835
|
if (task === 'generate-e2e-plan' && featureKey) {
|
|
848
|
-
const feat = currentData.features?.find(f => f.key === featureKey);
|
|
849
836
|
try {
|
|
850
|
-
// Extract JSON from agent result text
|
|
851
837
|
const jsonMatch = agentResultText.match(/\{[\s\S]*"testCases"[\s\S]*\}/);
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
steps: tc.steps || [],
|
|
865
|
-
expectedResults: tc.expectedResults || [],
|
|
866
|
-
status: 'pending',
|
|
867
|
-
})),
|
|
868
|
-
};
|
|
869
|
-
saveE2ePlan(projectRoot, plan);
|
|
870
|
-
broadcast('e2e-plan-ready', { featureKey, plan: plan });
|
|
871
|
-
broadcast('agent-output', { line: `β
E2E ΠΏΠ»Π°Π½ ΡΠΎΡ
ΡΠ°Π½ΡΠ½: ${plan.testCases.length} ΡΠ΅ΡΡ-ΠΊΠ΅ΠΉΡΠΎΠ²` });
|
|
872
|
-
}
|
|
873
|
-
else {
|
|
874
|
-
broadcast('e2e-plan-error', { featureKey, message: 'ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ ΠΈΠ·Π²Π»Π΅ΡΡ JSON ΠΈΠ· ΠΎΡΠ²Π΅ΡΠ° Π°Π³Π΅Π½ΡΠ°' });
|
|
875
|
-
broadcast('agent-output', { line: 'β ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ ΡΠ°ΡΠΏΠ°ΡΡΠΈΡΡ JSON ΠΏΠ»Π°Π½ ΠΈΠ· ΠΎΡΠ²Π΅ΡΠ°', isError: true });
|
|
876
|
-
}
|
|
838
|
+
const parsed = JSON.parse(jsonMatch ? jsonMatch[0] : agentResultText);
|
|
839
|
+
const feat = currentData.features?.find(f => f.key === featureKey);
|
|
840
|
+
const plan = {
|
|
841
|
+
featureKey,
|
|
842
|
+
featureLabel: feat?.label || featureKey,
|
|
843
|
+
generatedAt: new Date().toISOString(),
|
|
844
|
+
updatedAt: new Date().toISOString(),
|
|
845
|
+
baseUrl: parsed.baseUrl,
|
|
846
|
+
testCases: (parsed.testCases || []).map((tc) => ({ ...tc, status: 'pending' })),
|
|
847
|
+
};
|
|
848
|
+
saveE2ePlan(projectRoot, plan);
|
|
849
|
+
broadcast('e2e-plan-ready', { featureKey, plan });
|
|
877
850
|
}
|
|
878
851
|
catch (err) {
|
|
879
|
-
broadcast('e2e-plan-error', { featureKey, message: err.message });
|
|
880
|
-
broadcast('agent-output', { line: `β ΠΡΠΈΠ±ΠΊΠ° ΠΏΠ°ΡΡΠΈΠ½Π³Π° ΠΏΠ»Π°Π½Π°: ${err.message}`, isError: true });
|
|
852
|
+
broadcast('e2e-plan-error', { featureKey, message: `ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ ΡΠ°ΡΠΏΠ°ΡΡΠΈΡΡ ΠΏΠ»Π°Π½: ${err.message}` });
|
|
881
853
|
}
|
|
882
854
|
}
|
|
883
855
|
if (task === 'write-e2e-tests' && featureKey) {
|
|
884
|
-
// Update plan: mark approved test cases as 'written' and set file paths
|
|
885
856
|
const plan = loadE2ePlan(projectRoot, featureKey);
|
|
886
857
|
if (plan) {
|
|
887
858
|
for (const tc of plan.testCases) {
|
|
888
|
-
if (tc.status === 'approved')
|
|
859
|
+
if (tc.status === 'approved')
|
|
889
860
|
tc.status = 'written';
|
|
890
|
-
// Try to match created test files to test cases
|
|
891
|
-
const matchingFile = createdTestFiles.find(fp => fp.replace(/\\/g, '/').includes(tc.id) ||
|
|
892
|
-
fp.replace(/\\/g, '/').includes(featureKey));
|
|
893
|
-
if (matchingFile) {
|
|
894
|
-
tc.testFilePath = path.relative(projectRoot, matchingFile).replace(/\\/g, '/');
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
861
|
}
|
|
898
862
|
saveE2ePlan(projectRoot, plan);
|
|
899
|
-
broadcast('e2e-
|
|
900
|
-
broadcast('agent-output', { line: `β
E2E ΠΏΠ»Π°Π½ ΠΎΠ±Π½ΠΎΠ²Π»ΡΠ½: ΡΠ΅ΡΡΡ ΠΎΡΠΌΠ΅ΡΠ΅Π½Ρ ΠΊΠ°ΠΊ Π½Π°ΠΏΠΈΡΠ°Π½Π½ΡΠ΅` });
|
|
863
|
+
broadcast('e2e-tests-written', { featureKey, files: createdTestFiles });
|
|
901
864
|
}
|
|
902
865
|
}
|
|
903
866
|
process.stdout.write(' β
Agent done, rescanning...\n');
|
|
@@ -951,13 +914,13 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
951
914
|
testErrors[rel] = detail;
|
|
952
915
|
}
|
|
953
916
|
// Check which features have E2E plans
|
|
954
|
-
const e2ePlansExist =
|
|
955
|
-
const plansDir = e2ePlanDir(projectRoot);
|
|
917
|
+
const e2ePlansExist = {};
|
|
956
918
|
try {
|
|
957
|
-
|
|
958
|
-
|
|
919
|
+
const planDir = e2ePlanDir(projectRoot);
|
|
920
|
+
if (fs.existsSync(planDir)) {
|
|
921
|
+
for (const f of fs.readdirSync(planDir)) {
|
|
959
922
|
if (f.endsWith('.json'))
|
|
960
|
-
e2ePlansExist
|
|
923
|
+
e2ePlansExist[f.replace('.json', '')] = true;
|
|
961
924
|
}
|
|
962
925
|
}
|
|
963
926
|
}
|
|
@@ -1025,7 +988,7 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1025
988
|
}
|
|
1026
989
|
}
|
|
1027
990
|
}
|
|
1028
|
-
broadcast('agent-output', { line: ' β ΠΠ°ΠΆΠΌΠΈ
|
|
991
|
+
broadcast('agent-output', { line: ' β ΠΠ°ΠΆΠΌΠΈ ΠΠΎΡΠΈΠ½ΠΈΡΡ ΡΡΠ΄ΠΎΠΌ Ρ ΡΠ°ΠΉΠ»ΠΎΠΌ ΡΡΠΎΠ±Ρ Π°Π³Π΅Π½Ρ ΠΈΡΠΏΡΠ°Π²ΠΈΠ»' });
|
|
1029
992
|
}
|
|
1030
993
|
// Send testErrors directly in event β avoids path/timing issues with /api/data fetch
|
|
1031
994
|
const testErrorsForClient = {};
|
|
@@ -1033,6 +996,11 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1033
996
|
const rel = path.relative(projectRoot, fp).replace(/\\/g, '/');
|
|
1034
997
|
testErrorsForClient[rel] = detail;
|
|
1035
998
|
}
|
|
999
|
+
const errKeys = Object.keys(testErrorsForClient);
|
|
1000
|
+
process.stdout.write(` π testErrors to client: ${errKeys.length} keys: ${errKeys.slice(0, 3).join(', ')}\n`);
|
|
1001
|
+
if (result.failed > 0 && errKeys.length === 0) {
|
|
1002
|
+
broadcast('agent-output', { line: `β οΈ ΠΡΡΡ ${result.failed} ΡΠΏΠ°Π²ΡΠΈΡ
ΡΠ΅ΡΡΠ°, Π½ΠΎ Π΄Π΅ΡΠ°Π»ΠΈ ΠΏΠΎ ΡΠ°ΠΉΠ»Π°ΠΌ Π½Π΅ ΠΏΠΎΠ»ΡΡΠ΅Π½Ρ β ΠΏΡΠΎΠ²Π΅ΡΡ viberadar Π»ΠΎΠ³`, isDim: true });
|
|
1003
|
+
}
|
|
1036
1004
|
broadcast('tests-done', { passed: result.passed, failed: result.failed, testErrors: testErrorsForClient });
|
|
1037
1005
|
}
|
|
1038
1006
|
catch (err) {
|
|
@@ -1091,7 +1059,19 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1091
1059
|
});
|
|
1092
1060
|
return;
|
|
1093
1061
|
}
|
|
1094
|
-
//
|
|
1062
|
+
// Server-Sent Events endpoint
|
|
1063
|
+
if (url === '/api/events') {
|
|
1064
|
+
res.writeHead(200, {
|
|
1065
|
+
'Content-Type': 'text/event-stream',
|
|
1066
|
+
'Cache-Control': 'no-cache',
|
|
1067
|
+
'Connection': 'keep-alive',
|
|
1068
|
+
});
|
|
1069
|
+
res.write('data: connected\n\n');
|
|
1070
|
+
sseClients.add(res);
|
|
1071
|
+
req.on('close', () => sseClients.delete(res));
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
// ββ E2E routes βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1095
1075
|
if (url === '/api/e2e/generate-plan' && req.method === 'POST') {
|
|
1096
1076
|
let body = '';
|
|
1097
1077
|
req.on('data', d => body += d);
|
|
@@ -1110,18 +1090,17 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1110
1090
|
});
|
|
1111
1091
|
return;
|
|
1112
1092
|
}
|
|
1113
|
-
|
|
1114
|
-
if (
|
|
1115
|
-
const featureKey = decodeURIComponent(
|
|
1093
|
+
const planMatch = url.match(/^\/api\/e2e\/plan\/(.+)$/);
|
|
1094
|
+
if (planMatch && req.method === 'GET') {
|
|
1095
|
+
const featureKey = decodeURIComponent(planMatch[1]);
|
|
1116
1096
|
const plan = loadE2ePlan(projectRoot, featureKey);
|
|
1117
|
-
if (plan) {
|
|
1118
|
-
res.writeHead(
|
|
1119
|
-
res.end(JSON.stringify(
|
|
1120
|
-
|
|
1121
|
-
else {
|
|
1122
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1123
|
-
res.end(JSON.stringify({ error: 'not found' }));
|
|
1097
|
+
if (!plan) {
|
|
1098
|
+
res.writeHead(404);
|
|
1099
|
+
res.end(JSON.stringify({ error: 'Plan not found' }));
|
|
1100
|
+
return;
|
|
1124
1101
|
}
|
|
1102
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1103
|
+
res.end(JSON.stringify(plan));
|
|
1125
1104
|
return;
|
|
1126
1105
|
}
|
|
1127
1106
|
if (url === '/api/e2e/review' && req.method === 'POST') {
|
|
@@ -1137,12 +1116,8 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1137
1116
|
return;
|
|
1138
1117
|
}
|
|
1139
1118
|
const tc = plan.testCases.find(t => t.id === testCaseId);
|
|
1140
|
-
if (
|
|
1141
|
-
|
|
1142
|
-
res.end(JSON.stringify({ error: 'Test case not found' }));
|
|
1143
|
-
return;
|
|
1144
|
-
}
|
|
1145
|
-
tc.status = status;
|
|
1119
|
+
if (tc)
|
|
1120
|
+
tc.status = status;
|
|
1146
1121
|
saveE2ePlan(projectRoot, plan);
|
|
1147
1122
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1148
1123
|
res.end(JSON.stringify({ ok: true, plan }));
|
|
@@ -1166,10 +1141,8 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1166
1141
|
res.end(JSON.stringify({ error: 'Plan not found' }));
|
|
1167
1142
|
return;
|
|
1168
1143
|
}
|
|
1169
|
-
for (const tc of plan.testCases)
|
|
1170
|
-
|
|
1171
|
-
tc.status = status;
|
|
1172
|
-
}
|
|
1144
|
+
for (const tc of plan.testCases)
|
|
1145
|
+
tc.status = status;
|
|
1173
1146
|
saveE2ePlan(projectRoot, plan);
|
|
1174
1147
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1175
1148
|
res.end(JSON.stringify({ ok: true, plan }));
|
|
@@ -1211,58 +1184,26 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1211
1184
|
res.end(JSON.stringify({ error: 'Plan not found' }));
|
|
1212
1185
|
return;
|
|
1213
1186
|
}
|
|
1214
|
-
|
|
1215
|
-
const testFiles =
|
|
1216
|
-
.
|
|
1217
|
-
.
|
|
1218
|
-
|
|
1219
|
-
const e2eDir = path.join(projectRoot, 'e2e', featureKey);
|
|
1220
|
-
if (fs.existsSync(e2eDir)) {
|
|
1221
|
-
for (const f of fs.readdirSync(e2eDir)) {
|
|
1222
|
-
if (/\.(spec|test)\.(ts|js)$/.test(f)) {
|
|
1223
|
-
const relPath = `e2e/${featureKey}/${f}`;
|
|
1224
|
-
if (!testFiles.includes(relPath))
|
|
1225
|
-
testFiles.push(relPath);
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
if (testFiles.length === 0) {
|
|
1230
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1231
|
-
res.end(JSON.stringify({ ok: true, count: 0 }));
|
|
1232
|
-
broadcast('agent-output', { line: 'ΠΠ΅Ρ E2E ΡΠ΅ΡΡ-ΡΠ°ΠΉΠ»ΠΎΠ² Π΄Π»Ρ Π·Π°ΠΏΡΡΠΊΠ°' });
|
|
1233
|
-
return;
|
|
1234
|
-
}
|
|
1187
|
+
const writtenCases = plan.testCases.filter(tc => ['written', 'passed', 'failed'].includes(tc.status));
|
|
1188
|
+
const testFiles = writtenCases
|
|
1189
|
+
.map(tc => tc.testFilePath)
|
|
1190
|
+
.filter((f) => !!f)
|
|
1191
|
+
.map(f => path.isAbsolute(f) ? f : path.join(projectRoot, f));
|
|
1235
1192
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1236
1193
|
res.end(JSON.stringify({ ok: true, count: testFiles.length }));
|
|
1237
1194
|
broadcast('e2e-tests-running', { featureKey, count: testFiles.length });
|
|
1238
|
-
|
|
1239
|
-
const result = await runPlaywrightTests(testFiles, projectRoot);
|
|
1240
|
-
// Collect screenshots
|
|
1241
|
-
const screenshots = collectScreenshots(projectRoot, featureKey);
|
|
1195
|
+
const result = await runPlaywrightTests(testFiles.length > 0 ? testFiles : [`e2e/${featureKey}`], projectRoot);
|
|
1242
1196
|
// Update plan with results
|
|
1243
1197
|
for (const tc of plan.testCases) {
|
|
1244
|
-
if (
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
r.title.toLowerCase().includes(tc.name.toLowerCase()));
|
|
1249
|
-
if (matching) {
|
|
1250
|
-
tc.status = matching.status === 'passed' ? 'passed' : 'failed';
|
|
1251
|
-
tc.lastError = matching.error;
|
|
1198
|
+
if (result.results[tc.name]) {
|
|
1199
|
+
tc.status = result.results[tc.name];
|
|
1200
|
+
if (result.errors[tc.name])
|
|
1201
|
+
tc.lastError = result.errors[tc.name];
|
|
1252
1202
|
}
|
|
1253
|
-
tc.screenshotPaths =
|
|
1203
|
+
tc.screenshotPaths = collectScreenshots(projectRoot, featureKey, tc.id);
|
|
1254
1204
|
}
|
|
1255
1205
|
saveE2ePlan(projectRoot, plan);
|
|
1256
|
-
|
|
1257
|
-
? `β
E2E: Π²ΡΠ΅ ${result.passed} ΡΠ΅ΡΡΠΎΠ² ΠΏΡΠΎΡΠ»ΠΈ`
|
|
1258
|
-
: `β οΈ E2E: ${result.passed} passed, ${result.failed} failed`;
|
|
1259
|
-
broadcast('agent-output', { line: summary });
|
|
1260
|
-
broadcast('e2e-tests-done', {
|
|
1261
|
-
featureKey,
|
|
1262
|
-
passed: result.passed,
|
|
1263
|
-
failed: result.failed,
|
|
1264
|
-
plan: plan,
|
|
1265
|
-
});
|
|
1206
|
+
broadcast('e2e-tests-done', { featureKey, passed: result.passed, failed: result.failed, plan });
|
|
1266
1207
|
}
|
|
1267
1208
|
catch (err) {
|
|
1268
1209
|
res.writeHead(400);
|
|
@@ -1271,42 +1212,29 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1271
1212
|
});
|
|
1272
1213
|
return;
|
|
1273
1214
|
}
|
|
1274
|
-
|
|
1275
|
-
if (url.startsWith('/api/e2e/screenshot/')) {
|
|
1215
|
+
if (url.startsWith('/api/e2e/screenshot/') && req.method === 'GET') {
|
|
1276
1216
|
const relPath = decodeURIComponent(url.slice('/api/e2e/screenshot/'.length));
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
const
|
|
1280
|
-
|
|
1281
|
-
if (!resolved.startsWith(screenshotsRoot)) {
|
|
1217
|
+
// Path traversal protection
|
|
1218
|
+
const screenshotBase = path.join(projectRoot, '.viberadar', 'e2e-screenshots');
|
|
1219
|
+
const safePath = path.resolve(screenshotBase, relPath);
|
|
1220
|
+
if (!safePath.startsWith(screenshotBase)) {
|
|
1282
1221
|
res.writeHead(403);
|
|
1283
1222
|
res.end('Forbidden');
|
|
1284
1223
|
return;
|
|
1285
1224
|
}
|
|
1286
|
-
|
|
1287
|
-
const
|
|
1288
|
-
const
|
|
1289
|
-
|
|
1290
|
-
|
|
1225
|
+
try {
|
|
1226
|
+
const img = fs.readFileSync(safePath);
|
|
1227
|
+
const ext = path.extname(safePath).toLowerCase();
|
|
1228
|
+
const mime = ext === '.png' ? 'image/png' : ext === '.jpg' || ext === '.jpeg' ? 'image/jpeg' : 'image/webp';
|
|
1229
|
+
res.writeHead(200, { 'Content-Type': mime });
|
|
1230
|
+
res.end(img);
|
|
1291
1231
|
}
|
|
1292
|
-
|
|
1232
|
+
catch {
|
|
1293
1233
|
res.writeHead(404);
|
|
1294
|
-
res.end('
|
|
1234
|
+
res.end('Not found');
|
|
1295
1235
|
}
|
|
1296
1236
|
return;
|
|
1297
1237
|
}
|
|
1298
|
-
// Server-Sent Events endpoint
|
|
1299
|
-
if (url === '/api/events') {
|
|
1300
|
-
res.writeHead(200, {
|
|
1301
|
-
'Content-Type': 'text/event-stream',
|
|
1302
|
-
'Cache-Control': 'no-cache',
|
|
1303
|
-
'Connection': 'keep-alive',
|
|
1304
|
-
});
|
|
1305
|
-
res.write('data: connected\n\n');
|
|
1306
|
-
sseClients.add(res);
|
|
1307
|
-
req.on('close', () => sseClients.delete(res));
|
|
1308
|
-
return;
|
|
1309
|
-
}
|
|
1310
1238
|
res.writeHead(404);
|
|
1311
1239
|
res.end('Not found');
|
|
1312
1240
|
});
|