qazen-cli 0.2.1 → 0.2.3
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/lib/visionNavigator.js +90 -57
- package/package.json +1 -1
|
@@ -187,7 +187,10 @@ export class VisionNavigator {
|
|
|
187
187
|
// Mark original URL as visited too — prevents re-queuing via redirect.
|
|
188
188
|
this.visitedUrls.add(normalizedUrl);
|
|
189
189
|
const screenshot = await this.takeScreenshot(url);
|
|
190
|
-
|
|
190
|
+
// Call 1 — Navigation mapping (fast + cheap, Haiku)
|
|
191
|
+
const navItems = await this.analyzeNavigation(screenshot, url, onEvent);
|
|
192
|
+
// Call 2 — Interaction analysis (thorough, Opus)
|
|
193
|
+
const analysis = await this.analyzeInteractions(screenshot, url, context, onEvent);
|
|
191
194
|
if (!analysis)
|
|
192
195
|
return;
|
|
193
196
|
const testScenariosFound = analysis.elements.flatMap((el) => el.testScenarios || []);
|
|
@@ -196,6 +199,7 @@ export class VisionNavigator {
|
|
|
196
199
|
title: await this.page.title(),
|
|
197
200
|
visualDescription: analysis.pageDescription,
|
|
198
201
|
elements: analysis.elements,
|
|
202
|
+
interactiveSurfaces: analysis.elements,
|
|
199
203
|
screenshot,
|
|
200
204
|
...(analysis.workflow ? { workflow: analysis.workflow } : {}),
|
|
201
205
|
...(analysis.pageType ? { pageType: analysis.pageType } : {}),
|
|
@@ -204,13 +208,13 @@ export class VisionNavigator {
|
|
|
204
208
|
};
|
|
205
209
|
onEvent({
|
|
206
210
|
type: "page_mapped",
|
|
207
|
-
message: `${analysis.pageDescription} — ${analysis.elements.length}
|
|
211
|
+
message: `${analysis.pageDescription} — ${analysis.elements.length} interactive surfaces`,
|
|
208
212
|
});
|
|
209
213
|
const scenarioCount = pageMap.testScenariosFound.length;
|
|
210
214
|
if (scenarioCount > 0) {
|
|
211
215
|
onEvent({
|
|
212
216
|
type: "vision_analysis",
|
|
213
|
-
message: `${scenarioCount} test scenarios
|
|
217
|
+
message: `${scenarioCount} test scenarios across ${analysis.elements.length} surfaces`,
|
|
214
218
|
});
|
|
215
219
|
const topSurfaces = analysis.elements
|
|
216
220
|
.filter((e) => (e.testScenarios?.length ?? 0) > 2)
|
|
@@ -218,64 +222,27 @@ export class VisionNavigator {
|
|
|
218
222
|
for (const s of topSurfaces) {
|
|
219
223
|
onEvent({
|
|
220
224
|
type: "vision_analysis",
|
|
221
|
-
message: ` · ${s.interactionType}: ${s.testScenarios.length} scenarios
|
|
225
|
+
message: ` · ${s.interactionType}: ${s.testScenarios.length} scenarios`,
|
|
222
226
|
});
|
|
223
227
|
}
|
|
224
228
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
return false;
|
|
229
|
-
if (!["link", "tab", "menu"].includes(e.elementType))
|
|
230
|
-
return false;
|
|
231
|
-
// Skip if this action URL is already visited
|
|
232
|
-
if (e.action && e.action.startsWith("http")) {
|
|
233
|
-
const normalized = this.normalizeUrl(e.action);
|
|
234
|
-
if (this.visitedUrls.has(normalized))
|
|
235
|
-
return false;
|
|
236
|
-
}
|
|
237
|
-
// Skip if element description indicates it's the current page
|
|
238
|
-
const desc = e.description.toLowerCase();
|
|
239
|
-
if (desc.includes("currently active") ||
|
|
240
|
-
desc.includes("current page") ||
|
|
241
|
-
desc.includes("(active)")) {
|
|
242
|
-
return false;
|
|
243
|
-
}
|
|
244
|
-
return true;
|
|
245
|
-
})
|
|
246
|
-
.slice(0, 4);
|
|
247
|
-
for (const element of highPriority) {
|
|
229
|
+
this.pages.push(pageMap);
|
|
230
|
+
// Navigate using nav items from Call 1
|
|
231
|
+
for (const navItem of navItems.slice(0, 4)) {
|
|
248
232
|
try {
|
|
249
|
-
const newUrl = await this.
|
|
233
|
+
const newUrl = await this.clickNavItem(navItem, pageMap, onEvent);
|
|
250
234
|
if (newUrl && !this.visitedUrls.has(this.normalizeUrl(newUrl))) {
|
|
251
235
|
this.explorationQueue.push({
|
|
252
236
|
url: newUrl,
|
|
253
|
-
context: `Navigated
|
|
237
|
+
context: `Navigated via "${navItem.description}"`,
|
|
254
238
|
});
|
|
255
239
|
}
|
|
240
|
+
// Return to original page so the next nav-item click starts fresh.
|
|
256
241
|
await this.page.goto(url, { waitUntil: "domcontentloaded", timeout: 20000 });
|
|
257
242
|
await this.page.waitForTimeout(1500);
|
|
258
243
|
}
|
|
259
244
|
catch {
|
|
260
|
-
//
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
this.pages.push(pageMap);
|
|
264
|
-
const links = analysis.elements
|
|
265
|
-
.filter((e) => e.elementType === "link" && e.action.startsWith("http"))
|
|
266
|
-
.map((e) => e.action)
|
|
267
|
-
.filter((href) => {
|
|
268
|
-
try {
|
|
269
|
-
const hostname = new URL(href).hostname;
|
|
270
|
-
return hostname.includes(baseRegistrable);
|
|
271
|
-
}
|
|
272
|
-
catch {
|
|
273
|
-
return false;
|
|
274
|
-
}
|
|
275
|
-
});
|
|
276
|
-
for (const link of links) {
|
|
277
|
-
if (!this.visitedUrls.has(this.normalizeUrl(link))) {
|
|
278
|
-
this.explorationQueue.push({ url: link, context: `Link found on ${url}` });
|
|
245
|
+
// Continue to next nav item
|
|
279
246
|
}
|
|
280
247
|
}
|
|
281
248
|
}
|
|
@@ -286,7 +253,71 @@ export class VisionNavigator {
|
|
|
286
253
|
});
|
|
287
254
|
}
|
|
288
255
|
}
|
|
289
|
-
async
|
|
256
|
+
async analyzeNavigation(screenshot, _url, onEvent) {
|
|
257
|
+
try {
|
|
258
|
+
const response = await this.anthropic.messages.create({
|
|
259
|
+
model: "claude-haiku-4-5",
|
|
260
|
+
max_tokens: 800,
|
|
261
|
+
messages: [
|
|
262
|
+
{
|
|
263
|
+
role: "user",
|
|
264
|
+
content: [
|
|
265
|
+
{
|
|
266
|
+
type: "image",
|
|
267
|
+
source: { type: "base64", media_type: "image/jpeg", data: screenshot },
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
type: "text",
|
|
271
|
+
text: `Look at this web page screenshot.
|
|
272
|
+
|
|
273
|
+
Find ONLY elements that will navigate the user to a DIFFERENT page or section of the application.
|
|
274
|
+
|
|
275
|
+
This includes:
|
|
276
|
+
- Sidebar menu items
|
|
277
|
+
- Top navigation links
|
|
278
|
+
- Tab bars that switch between pages
|
|
279
|
+
- Breadcrumb links
|
|
280
|
+
- "Back" or "Next" navigation buttons
|
|
281
|
+
|
|
282
|
+
Do NOT include:
|
|
283
|
+
- Dropdowns that filter data on the current page
|
|
284
|
+
- Buttons that submit forms
|
|
285
|
+
- Elements that open modals
|
|
286
|
+
- Date pickers or search boxes
|
|
287
|
+
|
|
288
|
+
Already visited pages — skip links to these:
|
|
289
|
+
${Array.from(this.visitedUrls).join("\n")}
|
|
290
|
+
|
|
291
|
+
Return ONLY valid JSON array:
|
|
292
|
+
[
|
|
293
|
+
{
|
|
294
|
+
"description": "exact label of the nav item",
|
|
295
|
+
"visualLocation": "where it appears on screen",
|
|
296
|
+
"priority": "high or medium"
|
|
297
|
+
}
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
Return empty array [] if no navigation found.
|
|
301
|
+
Return ONLY the JSON array, nothing else.`,
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
});
|
|
307
|
+
const text = firstTextBlock(response.content) || "[]";
|
|
308
|
+
const parsed = JSON.parse(stripCodeFences(text));
|
|
309
|
+
const navItems = Array.isArray(parsed) ? parsed : [];
|
|
310
|
+
onEvent({
|
|
311
|
+
type: "vision_analysis",
|
|
312
|
+
message: `Navigation: ${navItems.length} items found`,
|
|
313
|
+
});
|
|
314
|
+
return navItems;
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async analyzeInteractions(screenshot, url, context, onEvent) {
|
|
290
321
|
onEvent({ type: "vision_analysis", message: "Sending screenshot to Claude Vision..." });
|
|
291
322
|
try {
|
|
292
323
|
const response = await this.anthropic.messages.create({
|
|
@@ -302,18 +333,20 @@ export class VisionNavigator {
|
|
|
302
333
|
},
|
|
303
334
|
{
|
|
304
335
|
type: "text",
|
|
305
|
-
text: `You are a senior QA engineer
|
|
336
|
+
text: `You are a senior QA engineer analysing a web application page for testable interactions.
|
|
306
337
|
|
|
307
338
|
Current URL: ${url}
|
|
308
|
-
|
|
309
|
-
Already visited (do not prioritise these):
|
|
310
|
-
${Array.from(this.visitedUrls).join("\n")}
|
|
339
|
+
Page context: ${context}
|
|
311
340
|
|
|
312
|
-
Analyse this screenshot
|
|
341
|
+
Analyse this screenshot and identify ALL interactive elements that need QA testing.
|
|
342
|
+
|
|
343
|
+
For EACH interactive element visible on screen, identify:
|
|
313
344
|
1. What TYPE of interaction it represents
|
|
314
345
|
2. What TEST SCENARIOS it generates for QA
|
|
315
346
|
3. What INPUT DATA would properly exercise it
|
|
316
347
|
|
|
348
|
+
Do NOT include navigation links, sidebar menu items, top-nav links, breadcrumbs, or tab bars that switch pages — those are handled separately. Focus only on on-page test surfaces.
|
|
349
|
+
|
|
317
350
|
Interactive surface types to identify:
|
|
318
351
|
|
|
319
352
|
SEARCH — What can be searched? Generate scenarios:
|
|
@@ -467,12 +500,12 @@ If not found, return {"x": 0, "y": 0, "found": false}`,
|
|
|
467
500
|
return null;
|
|
468
501
|
}
|
|
469
502
|
}
|
|
470
|
-
async
|
|
503
|
+
async clickNavItem(navItem, pageMap, onEvent) {
|
|
471
504
|
onEvent({
|
|
472
505
|
type: "action",
|
|
473
|
-
message: `Clicking "${
|
|
506
|
+
message: `Clicking "${navItem.description}"`,
|
|
474
507
|
});
|
|
475
|
-
const coords = await this.getElementCoordinates(
|
|
508
|
+
const coords = await this.getElementCoordinates(navItem.description, navItem.visualLocation);
|
|
476
509
|
if (!coords?.found)
|
|
477
510
|
return null;
|
|
478
511
|
// Set up navigation listener BEFORE clicking so we catch the redirect.
|