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.
@@ -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,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 (e.g. "${s.testScenarios[0]}")`,
225
+ message: ` · ${s.interactionType}: ${s.testScenarios.length} scenarios`,
222
226
  });
223
227
  }
224
228
  }
225
- const highPriority = analysis.elements
226
- .filter((e) => {
227
- if (e.priority !== "high")
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.clickElement(element, pageMap, onEvent);
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 from ${url} by clicking "${element.description}"`,
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
- // click failed continue
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 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) {
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 with 15 years experience testing enterprise web applications.
336
+ text: `You are a senior QA engineer analysing a web application page for testable interactions.
306
337
 
307
338
  Current URL: ${url}
308
- Context: ${context}
309
- Already visited (do not prioritise these):
310
- ${Array.from(this.visitedUrls).join("\n")}
339
+ Page context: ${context}
311
340
 
312
- Analyse this screenshot carefully. For EVERY interactive element visible on screen, identify:
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 clickElement(element, pageMap, onEvent) {
503
+ async clickNavItem(navItem, pageMap, onEvent) {
471
504
  onEvent({
472
505
  type: "action",
473
- message: `Clicking "${element.description}"`,
506
+ message: `Clicking "${navItem.description}"`,
474
507
  });
475
- const coords = await this.getElementCoordinates(element.description, element.visualLocation);
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qazen-cli",
3
- "version": "0.2.1",
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",