qazen-cli 0.1.8 → 0.2.0

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.
@@ -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),
@@ -114,10 +114,13 @@ export class VisionNavigator {
114
114
  return;
115
115
  }
116
116
  this.visitedUrls.add(normalizedFinal);
117
+ // Mark original URL as visited too — prevents re-queuing via redirect.
118
+ this.visitedUrls.add(normalizedUrl);
117
119
  const screenshot = await this.takeScreenshot(url);
118
120
  const analysis = await this.analyzeScreenshot(screenshot, url, context, onEvent);
119
121
  if (!analysis)
120
122
  return;
123
+ const testScenariosFound = analysis.elements.flatMap((el) => el.testScenarios || []);
121
124
  const pageMap = {
122
125
  url: currentUrl,
123
126
  title: await this.page.title(),
@@ -125,12 +128,30 @@ export class VisionNavigator {
125
128
  elements: analysis.elements,
126
129
  screenshot,
127
130
  ...(analysis.workflow ? { workflow: analysis.workflow } : {}),
131
+ ...(analysis.pageType ? { pageType: analysis.pageType } : {}),
128
132
  actions_taken: [],
133
+ testScenariosFound,
129
134
  };
130
135
  onEvent({
131
136
  type: "page_mapped",
132
137
  message: `${analysis.pageDescription} — ${analysis.elements.length} elements found`,
133
138
  });
139
+ const scenarioCount = pageMap.testScenariosFound.length;
140
+ if (scenarioCount > 0) {
141
+ onEvent({
142
+ type: "vision_analysis",
143
+ message: `${scenarioCount} test scenarios identified across ${analysis.elements.length} interactive surfaces`,
144
+ });
145
+ const topSurfaces = analysis.elements
146
+ .filter((e) => (e.testScenarios?.length ?? 0) > 2)
147
+ .slice(0, 3);
148
+ for (const s of topSurfaces) {
149
+ onEvent({
150
+ type: "vision_analysis",
151
+ message: ` · ${s.interactionType}: ${s.testScenarios.length} scenarios (e.g. "${s.testScenarios[0]}")`,
152
+ });
153
+ }
154
+ }
134
155
  const highPriority = analysis.elements
135
156
  .filter((e) => {
136
157
  if (e.priority !== "high")
@@ -211,39 +232,110 @@ export class VisionNavigator {
211
232
  },
212
233
  {
213
234
  type: "text",
214
- text: `You are a QA engineer exploring a web application.
215
- Context: ${context}
235
+ text: `You are a senior QA engineer with 15 years experience testing enterprise web applications.
236
+
216
237
  Current URL: ${url}
238
+ Context: ${context}
239
+ Already visited (do not prioritise these):
240
+ ${Array.from(this.visitedUrls).join("\n")}
241
+
242
+ Analyse this screenshot carefully. For EVERY interactive element visible on screen, identify:
243
+ 1. What TYPE of interaction it represents
244
+ 2. What TEST SCENARIOS it generates for QA
245
+ 3. What INPUT DATA would properly exercise it
246
+
247
+ Interactive surface types to identify:
248
+
249
+ SEARCH — What can be searched? Generate scenarios:
250
+ empty search, partial match, exact match,
251
+ no results found, special characters,
252
+ maximum length input, leading/trailing spaces
253
+
254
+ DATE PICKER / DATE RANGE — Generate scenarios:
255
+ single day, multi-day range, maximum range,
256
+ minimum range (same day), future dates,
257
+ past dates, fiscal year boundaries,
258
+ invalid date format, date ordering (end before start)
259
+
260
+ DROPDOWN / SELECT — Generate scenarios:
261
+ each available option, default value,
262
+ does selecting one change another dropdown,
263
+ what happens with no selection
264
+
265
+ FILTER PANEL — Generate scenarios:
266
+ single filter applied, multiple filters combined,
267
+ clear all filters, filter with no results,
268
+ filter persists across page navigation
269
+
270
+ DATA TABLE — Generate scenarios:
271
+ sort each column ascending, sort descending,
272
+ default sort order, pagination if present,
273
+ rows per page selector, empty state,
274
+ export functionality
275
+
276
+ EXPORT BUTTON — Generate scenarios:
277
+ each available format (CSV/Excel/PDF),
278
+ export with filters applied,
279
+ export with date range applied,
280
+ large dataset export
281
+
282
+ CHART / GRAPH — Generate scenarios:
283
+ hover to see tooltip values,
284
+ click to drill down if interactive,
285
+ date range changes update chart,
286
+ empty data state, maximum data points
287
+
288
+ TOGGLE / SWITCH — Generate scenarios:
289
+ enable the toggle, disable the toggle,
290
+ does state persist on page refresh,
291
+ does it affect other elements
292
+
293
+ TAB BAR — Generate scenarios:
294
+ each tab loads correct content,
295
+ default active tab on page load,
296
+ tab state preserved on browser back navigation
297
+
298
+ FORM — Generate scenarios:
299
+ all required fields missing,
300
+ individual required fields missing,
301
+ invalid format (email, phone, date),
302
+ maximum field length exceeded,
303
+ successful submission,
304
+ error message display
305
+
306
+ MODAL / DIALOG — Generate scenarios:
307
+ open the modal, close with X button,
308
+ close with Escape key,
309
+ close by clicking outside (backdrop),
310
+ action confirmation inside modal
217
311
 
218
- Analyse this screenshot and return ONLY valid JSON:
312
+ PAGINATION Generate scenarios:
313
+ navigate to next page, previous page,
314
+ jump to last page, jump to first page,
315
+ rows per page selector
316
+
317
+ Return ONLY valid JSON — no markdown, no explanation:
219
318
  {
220
- "pageDescription": "One sentence describing what this page/section does",
221
- "workflow": "The business workflow this page is part of (e.g. 'User Authentication', 'Product Catalog', 'Order Management')",
319
+ "pageDescription": "one sentence description",
320
+ "workflow": "business workflow name",
321
+ "pageType": "report|form|dashboard|list|detail|other",
222
322
  "elements": [
223
323
  {
224
- "description": "Clear description of the element",
225
- "elementType": "button|link|input|tab|menu|dropdown|other",
226
- "action": "What clicking/interacting would do OR the href URL if it's a link",
324
+ "description": "specific description of this element",
325
+ "elementType": "one of the types listed above",
326
+ "interactionType": "e.g. date range selector, market dropdown",
327
+ "testScenarios": [
328
+ "specific test scenario 1",
329
+ "specific test scenario 2"
330
+ ],
331
+ "action": "what clicking/interacting does",
227
332
  "priority": "high|medium|low",
228
- "visualLocation": "Brief description of where it is on screen"
333
+ "visualLocation": "where on screen",
334
+ "requiresInput": true or false,
335
+ "inputExamples": ["example1", "example2"]
229
336
  }
230
337
  ]
231
- }
232
-
233
- Already visited URLs — do NOT suggest clicking these as high priority:
234
- ${Array.from(this.visitedUrls).join("\n")}
235
-
236
- If an element links to an already-visited URL, set its priority to 'low'.
237
-
238
- Focus on:
239
- - Navigation items and menu links (high priority)
240
- - Primary action buttons (high priority)
241
- - Tab bars and section switchers (high priority)
242
- - Form inputs (medium priority)
243
- - Secondary buttons (medium priority)
244
-
245
- Ignore: decorative elements, footer links, external links.
246
- Return ONLY the JSON object. No markdown, no explanation.`,
338
+ }`,
247
339
  },
248
340
  ],
249
341
  },
@@ -265,13 +357,9 @@ Return ONLY the JSON object. No markdown, no explanation.`,
265
357
  return null;
266
358
  }
267
359
  }
268
- async clickElement(element, pageMap, onEvent) {
269
- onEvent({
270
- type: "action",
271
- message: `Clicking "${element.description}" (${element.visualLocation || element.elementType})`,
272
- });
273
- const urlBefore = this.page.url();
360
+ async getElementCoordinates(description, visualLocation) {
274
361
  const screenshot = await this.takeScreenshot("click-target");
362
+ const hint = visualLocation ? ` (${visualLocation})` : "";
275
363
  const coordResponse = await this.anthropic.messages.create({
276
364
  model: MODEL,
277
365
  max_tokens: 200,
@@ -285,7 +373,7 @@ Return ONLY the JSON object. No markdown, no explanation.`,
285
373
  },
286
374
  {
287
375
  type: "text",
288
- text: `Find "${element.description}" on screen.
376
+ text: `Find "${description}"${hint} on screen.
289
377
  Return ONLY JSON: {"x": number, "y": number, "found": boolean}
290
378
  x and y are pixel coordinates (image is 1280x720).
291
379
  If not found, return {"x": 0, "y": 0, "found": false}`,
@@ -295,24 +383,50 @@ If not found, return {"x": 0, "y": 0, "found": false}`,
295
383
  ],
296
384
  });
297
385
  const coordText = firstTextBlock(coordResponse.content);
298
- let coords;
299
386
  try {
300
- coords = JSON.parse(stripCodeFences(coordText));
387
+ return JSON.parse(stripCodeFences(coordText));
301
388
  }
302
389
  catch {
303
390
  return null;
304
391
  }
305
- if (!coords.found)
392
+ }
393
+ async clickElement(element, pageMap, onEvent) {
394
+ onEvent({
395
+ type: "action",
396
+ message: `Clicking "${element.description}"`,
397
+ });
398
+ const coords = await this.getElementCoordinates(element.description, element.visualLocation);
399
+ if (!coords?.found)
306
400
  return null;
401
+ // Set up navigation listener BEFORE clicking so we catch the redirect.
402
+ let navigationUrl = null;
403
+ const navigationPromise = this.page.waitForNavigation({
404
+ timeout: 3000,
405
+ waitUntil: "domcontentloaded",
406
+ })
407
+ .then(() => {
408
+ navigationUrl = this.page.url();
409
+ })
410
+ .catch(() => {
411
+ /* no navigation — fine */
412
+ });
307
413
  await this.page.mouse.click(coords.x, coords.y);
308
414
  this.totalActions++;
309
- pageMap.actions_taken.push(`Clicked "${element.description}" at (${coords.x}, ${coords.y})`);
310
- await this.page.waitForTimeout(2000);
311
- const urlAfter = this.page.url();
312
- if (urlAfter !== urlBefore) {
313
- const newScreenshot = await this.takeScreenshot(urlAfter);
314
- this.screenshots.push(newScreenshot);
315
- return urlAfter;
415
+ await Promise.race([navigationPromise, this.page.waitForTimeout(2000)]);
416
+ if (navigationUrl) {
417
+ const normalizedNew = this.normalizeUrl(navigationUrl);
418
+ // If we landed on an already-mapped page, go back and skip.
419
+ if (this.visitedUrls.has(normalizedNew)) {
420
+ onEvent({
421
+ type: "vision_analysis",
422
+ message: `Skipping ${normalizedNew} — already mapped`,
423
+ });
424
+ await this.page.goBack({ waitUntil: "domcontentloaded", timeout: 10000 }).catch(() => { });
425
+ await this.page.waitForTimeout(1000);
426
+ return null;
427
+ }
428
+ pageMap.actions_taken.push(`Navigated to ${navigationUrl}`);
429
+ return navigationUrl;
316
430
  }
317
431
  return null;
318
432
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qazen-cli",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "description": "QAZen CLI — capture authenticated browser sessions for enterprise SSO testing",
5
5
  "license": "MIT",
6
6
  "author": "QAZen",