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.
@@ -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
- const analysis = await this.analyzeScreenshot(screenshot, url, context, onEvent);
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} elements found`,
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 identified across ${analysis.elements.length} interactive surfaces`,
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 (e.g. "${s.testScenarios[0]}")`,
225
+ message: ` · ${s.interactionType}: ${s.testScenarios.length} scenarios`,
222
226
  });
223
227
  }
224
228
  }
225
- // Debug: log every elementType:priority Claude returned, so we can see
226
- // why the highPriority filter may be excluding everything.
227
- const elementTypes = analysis.elements
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.clickElement(element, pageMap, onEvent);
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 from ${url} by clicking "${element.description}"`,
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
- // click failed continue
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 analyzeScreenshot(screenshot, url, context, onEvent) {
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 with 15 years experience testing enterprise web applications.
336
+ text: `You are a senior QA engineer analysing a web application page for testable interactions.
329
337
 
330
338
  Current URL: ${url}
331
- Context: ${context}
332
- Already visited (do not prioritise these):
333
- ${Array.from(this.visitedUrls).join("\n")}
339
+ Page context: ${context}
340
+
341
+ Analyse this screenshot and identify ALL interactive elements that need QA testing.
334
342
 
335
- Analyse this screenshot carefully. For EVERY interactive element visible on screen, identify:
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 clickElement(element, pageMap, onEvent) {
503
+ async clickNavItem(navItem, pageMap, onEvent) {
494
504
  onEvent({
495
505
  type: "action",
496
- message: `Clicking "${element.description}"`,
506
+ message: `Clicking "${navItem.description}"`,
497
507
  });
498
- const coords = await this.getElementCoordinates(element.description, element.visualLocation);
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qazen-cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "QAZen CLI — capture authenticated browser sessions for enterprise SSO testing",
5
5
  "license": "MIT",
6
6
  "author": "QAZen",