qazen-cli 0.1.9 → 0.2.1
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/commands/visionScout.js +2 -0
- package/dist/lib/api.js +4 -0
- package/dist/lib/visionNavigator.js +194 -27
- package/package.json +1 -1
|
@@ -114,8 +114,10 @@ export async function visionScoutCommand(options) {
|
|
|
114
114
|
await uploadVisionDiscovery(config.apiUrl, config.cliToken, project.id, result);
|
|
115
115
|
uploadSpinner.succeed(chalk.green("Discovery uploaded to QAZen"));
|
|
116
116
|
console.log("\n" + chalk.hex("#6366F1").bold(" Vision Scout complete\n"));
|
|
117
|
+
const totalScenarios = result.pages.reduce((sum, p) => sum + (p.testScenariosFound?.length ?? 0), 0);
|
|
117
118
|
console.log(chalk.gray(` Pages explored: ${chalk.white(result.pages.length)}`));
|
|
118
119
|
console.log(chalk.gray(` Elements found: ${chalk.white(result.totalElements)}`));
|
|
120
|
+
console.log(chalk.gray(` Test scenarios: ${chalk.white(totalScenarios)}`));
|
|
119
121
|
console.log(chalk.gray(` Actions taken: ${chalk.white(result.totalActions)}`));
|
|
120
122
|
console.log(chalk.gray(` Screenshots: ${chalk.white(result.screenshots.length)}`));
|
|
121
123
|
console.log(chalk.gray(`\n View in QAZen: ${config.apiUrl}/discovery\n`));
|
package/dist/lib/api.js
CHANGED
|
@@ -49,6 +49,7 @@ export async function fetchProjectAuthSession(apiUrl, cliToken, projectId) {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
export async function uploadVisionDiscovery(apiUrl, cliToken, projectId, result) {
|
|
52
|
+
const totalTestScenarios = result.pages.reduce((sum, p) => sum + (p.testScenariosFound?.length ?? 0), 0);
|
|
52
53
|
const appMap = {
|
|
53
54
|
baseUrl: result.pages[0]?.url ?? "",
|
|
54
55
|
pages: result.pages.map((p) => ({
|
|
@@ -66,11 +67,14 @@ export async function uploadVisionDiscovery(apiUrl, cliToken, projectId, result)
|
|
|
66
67
|
.map((e) => e.action),
|
|
67
68
|
visualDescription: p.visualDescription,
|
|
68
69
|
workflow: p.workflow,
|
|
70
|
+
pageType: p.pageType,
|
|
69
71
|
actionsTaken: p.actions_taken,
|
|
72
|
+
testScenariosFound: p.testScenariosFound ?? [],
|
|
70
73
|
})),
|
|
71
74
|
api_endpoints: [],
|
|
72
75
|
total_pages: result.pages.length,
|
|
73
76
|
total_elements: result.totalElements,
|
|
77
|
+
totalTestScenarios,
|
|
74
78
|
discoveryMethod: "vision",
|
|
75
79
|
primaryWorkflows: result.primaryWorkflows,
|
|
76
80
|
screenshots: result.screenshots.slice(0, 5),
|
|
@@ -10,6 +10,76 @@ function firstTextBlock(content) {
|
|
|
10
10
|
const block = content[0];
|
|
11
11
|
return block && block.type === "text" ? block.text : "";
|
|
12
12
|
}
|
|
13
|
+
function salvageJson(text) {
|
|
14
|
+
const cleaned = stripCodeFences(text);
|
|
15
|
+
// First try clean parse
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(cleaned);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Fall through to salvage
|
|
21
|
+
}
|
|
22
|
+
// Salvage truncated JSON: extract metadata + every complete {...} element block.
|
|
23
|
+
try {
|
|
24
|
+
const descMatch = cleaned.match(/"pageDescription"\s*:\s*"([^"]+)"/);
|
|
25
|
+
const workflowMatch = cleaned.match(/"workflow"\s*:\s*"([^"]+)"/);
|
|
26
|
+
const pageTypeMatch = cleaned.match(/"pageType"\s*:\s*"([^"]+)"/);
|
|
27
|
+
const elements = [];
|
|
28
|
+
let depth = 0;
|
|
29
|
+
let start = -1;
|
|
30
|
+
let inString = false;
|
|
31
|
+
let escape = false;
|
|
32
|
+
for (let i = 0; i < cleaned.length; i++) {
|
|
33
|
+
const ch = cleaned[i];
|
|
34
|
+
if (escape) {
|
|
35
|
+
escape = false;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (ch === "\\") {
|
|
39
|
+
escape = true;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (ch === '"') {
|
|
43
|
+
inString = !inString;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (inString)
|
|
47
|
+
continue;
|
|
48
|
+
if (ch === "{") {
|
|
49
|
+
if (depth === 0)
|
|
50
|
+
start = i;
|
|
51
|
+
depth++;
|
|
52
|
+
}
|
|
53
|
+
else if (ch === "}") {
|
|
54
|
+
depth--;
|
|
55
|
+
if (depth === 0 && start !== -1) {
|
|
56
|
+
try {
|
|
57
|
+
const obj = JSON.parse(cleaned.substring(start, i + 1));
|
|
58
|
+
if (typeof obj.description === "string" &&
|
|
59
|
+
typeof obj.elementType === "string") {
|
|
60
|
+
elements.push(obj);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// skip incomplete object
|
|
65
|
+
}
|
|
66
|
+
start = -1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (elements.length === 0)
|
|
71
|
+
return null;
|
|
72
|
+
return {
|
|
73
|
+
pageDescription: descMatch?.[1] ?? "Page analysis (partial)",
|
|
74
|
+
workflow: workflowMatch?.[1] ?? "Unknown",
|
|
75
|
+
pageType: pageTypeMatch?.[1] ?? "other",
|
|
76
|
+
elements,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
13
83
|
export class VisionNavigator {
|
|
14
84
|
anthropic;
|
|
15
85
|
baseUrl;
|
|
@@ -120,6 +190,7 @@ export class VisionNavigator {
|
|
|
120
190
|
const analysis = await this.analyzeScreenshot(screenshot, url, context, onEvent);
|
|
121
191
|
if (!analysis)
|
|
122
192
|
return;
|
|
193
|
+
const testScenariosFound = analysis.elements.flatMap((el) => el.testScenarios || []);
|
|
123
194
|
const pageMap = {
|
|
124
195
|
url: currentUrl,
|
|
125
196
|
title: await this.page.title(),
|
|
@@ -127,12 +198,30 @@ export class VisionNavigator {
|
|
|
127
198
|
elements: analysis.elements,
|
|
128
199
|
screenshot,
|
|
129
200
|
...(analysis.workflow ? { workflow: analysis.workflow } : {}),
|
|
201
|
+
...(analysis.pageType ? { pageType: analysis.pageType } : {}),
|
|
130
202
|
actions_taken: [],
|
|
203
|
+
testScenariosFound,
|
|
131
204
|
};
|
|
132
205
|
onEvent({
|
|
133
206
|
type: "page_mapped",
|
|
134
207
|
message: `${analysis.pageDescription} — ${analysis.elements.length} elements found`,
|
|
135
208
|
});
|
|
209
|
+
const scenarioCount = pageMap.testScenariosFound.length;
|
|
210
|
+
if (scenarioCount > 0) {
|
|
211
|
+
onEvent({
|
|
212
|
+
type: "vision_analysis",
|
|
213
|
+
message: `${scenarioCount} test scenarios identified across ${analysis.elements.length} interactive surfaces`,
|
|
214
|
+
});
|
|
215
|
+
const topSurfaces = analysis.elements
|
|
216
|
+
.filter((e) => (e.testScenarios?.length ?? 0) > 2)
|
|
217
|
+
.slice(0, 3);
|
|
218
|
+
for (const s of topSurfaces) {
|
|
219
|
+
onEvent({
|
|
220
|
+
type: "vision_analysis",
|
|
221
|
+
message: ` · ${s.interactionType}: ${s.testScenarios.length} scenarios (e.g. "${s.testScenarios[0]}")`,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
136
225
|
const highPriority = analysis.elements
|
|
137
226
|
.filter((e) => {
|
|
138
227
|
if (e.priority !== "high")
|
|
@@ -202,7 +291,7 @@ export class VisionNavigator {
|
|
|
202
291
|
try {
|
|
203
292
|
const response = await this.anthropic.messages.create({
|
|
204
293
|
model: MODEL,
|
|
205
|
-
max_tokens:
|
|
294
|
+
max_tokens: 4000,
|
|
206
295
|
messages: [
|
|
207
296
|
{
|
|
208
297
|
role: "user",
|
|
@@ -213,46 +302,124 @@ export class VisionNavigator {
|
|
|
213
302
|
},
|
|
214
303
|
{
|
|
215
304
|
type: "text",
|
|
216
|
-
text: `You are a QA engineer
|
|
217
|
-
|
|
305
|
+
text: `You are a senior QA engineer with 15 years experience testing enterprise web applications.
|
|
306
|
+
|
|
218
307
|
Current URL: ${url}
|
|
308
|
+
Context: ${context}
|
|
309
|
+
Already visited (do not prioritise these):
|
|
310
|
+
${Array.from(this.visitedUrls).join("\n")}
|
|
311
|
+
|
|
312
|
+
Analyse this screenshot carefully. For EVERY interactive element visible on screen, identify:
|
|
313
|
+
1. What TYPE of interaction it represents
|
|
314
|
+
2. What TEST SCENARIOS it generates for QA
|
|
315
|
+
3. What INPUT DATA would properly exercise it
|
|
316
|
+
|
|
317
|
+
Interactive surface types to identify:
|
|
318
|
+
|
|
319
|
+
SEARCH — What can be searched? Generate scenarios:
|
|
320
|
+
empty search, partial match, exact match,
|
|
321
|
+
no results found, special characters,
|
|
322
|
+
maximum length input, leading/trailing spaces
|
|
323
|
+
|
|
324
|
+
DATE PICKER / DATE RANGE — Generate scenarios:
|
|
325
|
+
single day, multi-day range, maximum range,
|
|
326
|
+
minimum range (same day), future dates,
|
|
327
|
+
past dates, fiscal year boundaries,
|
|
328
|
+
invalid date format, date ordering (end before start)
|
|
329
|
+
|
|
330
|
+
DROPDOWN / SELECT — Generate scenarios:
|
|
331
|
+
each available option, default value,
|
|
332
|
+
does selecting one change another dropdown,
|
|
333
|
+
what happens with no selection
|
|
334
|
+
|
|
335
|
+
FILTER PANEL — Generate scenarios:
|
|
336
|
+
single filter applied, multiple filters combined,
|
|
337
|
+
clear all filters, filter with no results,
|
|
338
|
+
filter persists across page navigation
|
|
339
|
+
|
|
340
|
+
DATA TABLE — Generate scenarios:
|
|
341
|
+
sort each column ascending, sort descending,
|
|
342
|
+
default sort order, pagination if present,
|
|
343
|
+
rows per page selector, empty state,
|
|
344
|
+
export functionality
|
|
345
|
+
|
|
346
|
+
EXPORT BUTTON — Generate scenarios:
|
|
347
|
+
each available format (CSV/Excel/PDF),
|
|
348
|
+
export with filters applied,
|
|
349
|
+
export with date range applied,
|
|
350
|
+
large dataset export
|
|
351
|
+
|
|
352
|
+
CHART / GRAPH — Generate scenarios:
|
|
353
|
+
hover to see tooltip values,
|
|
354
|
+
click to drill down if interactive,
|
|
355
|
+
date range changes update chart,
|
|
356
|
+
empty data state, maximum data points
|
|
357
|
+
|
|
358
|
+
TOGGLE / SWITCH — Generate scenarios:
|
|
359
|
+
enable the toggle, disable the toggle,
|
|
360
|
+
does state persist on page refresh,
|
|
361
|
+
does it affect other elements
|
|
362
|
+
|
|
363
|
+
TAB BAR — Generate scenarios:
|
|
364
|
+
each tab loads correct content,
|
|
365
|
+
default active tab on page load,
|
|
366
|
+
tab state preserved on browser back navigation
|
|
367
|
+
|
|
368
|
+
FORM — Generate scenarios:
|
|
369
|
+
all required fields missing,
|
|
370
|
+
individual required fields missing,
|
|
371
|
+
invalid format (email, phone, date),
|
|
372
|
+
maximum field length exceeded,
|
|
373
|
+
successful submission,
|
|
374
|
+
error message display
|
|
219
375
|
|
|
220
|
-
|
|
376
|
+
MODAL / DIALOG — Generate scenarios:
|
|
377
|
+
open the modal, close with X button,
|
|
378
|
+
close with Escape key,
|
|
379
|
+
close by clicking outside (backdrop),
|
|
380
|
+
action confirmation inside modal
|
|
381
|
+
|
|
382
|
+
PAGINATION — Generate scenarios:
|
|
383
|
+
navigate to next page, previous page,
|
|
384
|
+
jump to last page, jump to first page,
|
|
385
|
+
rows per page selector
|
|
386
|
+
|
|
387
|
+
Return ONLY valid JSON — no markdown, no explanation:
|
|
221
388
|
{
|
|
222
|
-
"pageDescription": "
|
|
223
|
-
"workflow": "
|
|
389
|
+
"pageDescription": "one sentence description",
|
|
390
|
+
"workflow": "business workflow name",
|
|
391
|
+
"pageType": "report|form|dashboard|list|detail|other",
|
|
224
392
|
"elements": [
|
|
225
393
|
{
|
|
226
|
-
"description": "
|
|
227
|
-
"elementType": "
|
|
228
|
-
"
|
|
394
|
+
"description": "specific description of this element",
|
|
395
|
+
"elementType": "one of the types listed above",
|
|
396
|
+
"interactionType": "e.g. date range selector, market dropdown",
|
|
397
|
+
"testScenarios": [
|
|
398
|
+
"specific test scenario 1",
|
|
399
|
+
"specific test scenario 2"
|
|
400
|
+
],
|
|
401
|
+
"action": "what clicking/interacting does",
|
|
229
402
|
"priority": "high|medium|low",
|
|
230
|
-
"visualLocation": "
|
|
403
|
+
"visualLocation": "where on screen",
|
|
404
|
+
"requiresInput": true or false,
|
|
405
|
+
"inputExamples": ["example1", "example2"]
|
|
231
406
|
}
|
|
232
407
|
]
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
Already visited URLs — do NOT suggest clicking these as high priority:
|
|
236
|
-
${Array.from(this.visitedUrls).join("\n")}
|
|
237
|
-
|
|
238
|
-
If an element links to an already-visited URL, set its priority to 'low'.
|
|
239
|
-
|
|
240
|
-
Focus on:
|
|
241
|
-
- Navigation items and menu links (high priority)
|
|
242
|
-
- Primary action buttons (high priority)
|
|
243
|
-
- Tab bars and section switchers (high priority)
|
|
244
|
-
- Form inputs (medium priority)
|
|
245
|
-
- Secondary buttons (medium priority)
|
|
246
|
-
|
|
247
|
-
Ignore: decorative elements, footer links, external links.
|
|
248
|
-
Return ONLY the JSON object. No markdown, no explanation.`,
|
|
408
|
+
}`,
|
|
249
409
|
},
|
|
250
410
|
],
|
|
251
411
|
},
|
|
252
412
|
],
|
|
253
413
|
});
|
|
254
414
|
const text = firstTextBlock(response.content);
|
|
255
|
-
const parsed =
|
|
415
|
+
const parsed = salvageJson(text);
|
|
416
|
+
if (!parsed) {
|
|
417
|
+
onEvent({
|
|
418
|
+
type: "error",
|
|
419
|
+
message: "Vision analysis failed: could not parse or salvage JSON response",
|
|
420
|
+
});
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
256
423
|
onEvent({
|
|
257
424
|
type: "vision_analysis",
|
|
258
425
|
message: parsed.pageDescription + (parsed.workflow ? ` (${parsed.workflow})` : ""),
|