qazen-cli 0.2.2 → 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 -80
- 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,87 +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
|
-
const
|
|
228
|
-
.map((e) => `${e.elementType}:${e.priority}`)
|
|
229
|
-
.join(", ");
|
|
230
|
-
onEvent({
|
|
231
|
-
type: "vision_analysis",
|
|
232
|
-
message: `Element types found: ${elementTypes}`,
|
|
233
|
-
});
|
|
234
|
-
const NAV_TYPES = ["link", "tab", "menu"];
|
|
235
|
-
const NAV_TYPES_EXPANDED = [...NAV_TYPES, "button", "other"];
|
|
236
|
-
const highPriority = analysis.elements
|
|
237
|
-
.filter((e) => {
|
|
238
|
-
if (e.priority !== "high")
|
|
239
|
-
return false;
|
|
240
|
-
const desc = e.description.toLowerCase();
|
|
241
|
-
const isNavElement = desc.includes("navigation") ||
|
|
242
|
-
desc.includes("menu") ||
|
|
243
|
-
desc.includes("nav link") ||
|
|
244
|
-
desc.includes("sidebar");
|
|
245
|
-
// Allow strict nav types, OR an expanded type set when the
|
|
246
|
-
// description itself signals navigation intent.
|
|
247
|
-
if (!isNavElement && !NAV_TYPES.includes(e.elementType))
|
|
248
|
-
return false;
|
|
249
|
-
if (!NAV_TYPES_EXPANDED.includes(e.elementType))
|
|
250
|
-
return false;
|
|
251
|
-
// Skip if this action URL is already visited
|
|
252
|
-
if (e.action && e.action.startsWith("http")) {
|
|
253
|
-
const normalized = this.normalizeUrl(e.action);
|
|
254
|
-
if (this.visitedUrls.has(normalized))
|
|
255
|
-
return false;
|
|
256
|
-
}
|
|
257
|
-
// Skip if element description indicates it's the current page
|
|
258
|
-
if (desc.includes("currently active") ||
|
|
259
|
-
desc.includes("current page") ||
|
|
260
|
-
desc.includes("(active)")) {
|
|
261
|
-
return false;
|
|
262
|
-
}
|
|
263
|
-
return true;
|
|
264
|
-
})
|
|
265
|
-
.slice(0, 4);
|
|
266
|
-
onEvent({
|
|
267
|
-
type: "vision_analysis",
|
|
268
|
-
message: `Navigation candidates: ${highPriority.length} (of ${analysis.elements.length})`,
|
|
269
|
-
});
|
|
270
|
-
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)) {
|
|
271
232
|
try {
|
|
272
|
-
const newUrl = await this.
|
|
233
|
+
const newUrl = await this.clickNavItem(navItem, pageMap, onEvent);
|
|
273
234
|
if (newUrl && !this.visitedUrls.has(this.normalizeUrl(newUrl))) {
|
|
274
235
|
this.explorationQueue.push({
|
|
275
236
|
url: newUrl,
|
|
276
|
-
context: `Navigated
|
|
237
|
+
context: `Navigated via "${navItem.description}"`,
|
|
277
238
|
});
|
|
278
239
|
}
|
|
240
|
+
// Return to original page so the next nav-item click starts fresh.
|
|
279
241
|
await this.page.goto(url, { waitUntil: "domcontentloaded", timeout: 20000 });
|
|
280
242
|
await this.page.waitForTimeout(1500);
|
|
281
243
|
}
|
|
282
244
|
catch {
|
|
283
|
-
//
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
this.pages.push(pageMap);
|
|
287
|
-
const links = analysis.elements
|
|
288
|
-
.filter((e) => e.elementType === "link" && e.action.startsWith("http"))
|
|
289
|
-
.map((e) => e.action)
|
|
290
|
-
.filter((href) => {
|
|
291
|
-
try {
|
|
292
|
-
const hostname = new URL(href).hostname;
|
|
293
|
-
return hostname.includes(baseRegistrable);
|
|
294
|
-
}
|
|
295
|
-
catch {
|
|
296
|
-
return false;
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
|
-
for (const link of links) {
|
|
300
|
-
if (!this.visitedUrls.has(this.normalizeUrl(link))) {
|
|
301
|
-
this.explorationQueue.push({ url: link, context: `Link found on ${url}` });
|
|
245
|
+
// Continue to next nav item
|
|
302
246
|
}
|
|
303
247
|
}
|
|
304
248
|
}
|
|
@@ -309,7 +253,71 @@ export class VisionNavigator {
|
|
|
309
253
|
});
|
|
310
254
|
}
|
|
311
255
|
}
|
|
312
|
-
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) {
|
|
313
321
|
onEvent({ type: "vision_analysis", message: "Sending screenshot to Claude Vision..." });
|
|
314
322
|
try {
|
|
315
323
|
const response = await this.anthropic.messages.create({
|
|
@@ -325,18 +333,20 @@ export class VisionNavigator {
|
|
|
325
333
|
},
|
|
326
334
|
{
|
|
327
335
|
type: "text",
|
|
328
|
-
text: `You are a senior QA engineer
|
|
336
|
+
text: `You are a senior QA engineer analysing a web application page for testable interactions.
|
|
329
337
|
|
|
330
338
|
Current URL: ${url}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
339
|
+
Page context: ${context}
|
|
340
|
+
|
|
341
|
+
Analyse this screenshot and identify ALL interactive elements that need QA testing.
|
|
334
342
|
|
|
335
|
-
|
|
343
|
+
For EACH interactive element visible on screen, identify:
|
|
336
344
|
1. What TYPE of interaction it represents
|
|
337
345
|
2. What TEST SCENARIOS it generates for QA
|
|
338
346
|
3. What INPUT DATA would properly exercise it
|
|
339
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
|
+
|
|
340
350
|
Interactive surface types to identify:
|
|
341
351
|
|
|
342
352
|
SEARCH — What can be searched? Generate scenarios:
|
|
@@ -490,12 +500,12 @@ If not found, return {"x": 0, "y": 0, "found": false}`,
|
|
|
490
500
|
return null;
|
|
491
501
|
}
|
|
492
502
|
}
|
|
493
|
-
async
|
|
503
|
+
async clickNavItem(navItem, pageMap, onEvent) {
|
|
494
504
|
onEvent({
|
|
495
505
|
type: "action",
|
|
496
|
-
message: `Clicking "${
|
|
506
|
+
message: `Clicking "${navItem.description}"`,
|
|
497
507
|
});
|
|
498
|
-
const coords = await this.getElementCoordinates(
|
|
508
|
+
const coords = await this.getElementCoordinates(navItem.description, navItem.visualLocation);
|
|
499
509
|
if (!coords?.found)
|
|
500
510
|
return null;
|
|
501
511
|
// Set up navigation listener BEFORE clicking so we catch the redirect.
|