qa360 2.0.11 → 2.0.13

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.
Files changed (42) hide show
  1. package/dist/commands/ai.js +26 -14
  2. package/dist/commands/ask.d.ts +75 -23
  3. package/dist/commands/ask.js +413 -265
  4. package/dist/commands/crawl.d.ts +24 -0
  5. package/dist/commands/crawl.js +121 -0
  6. package/dist/commands/history.js +38 -3
  7. package/dist/commands/init.d.ts +89 -95
  8. package/dist/commands/init.js +282 -200
  9. package/dist/commands/run.d.ts +1 -0
  10. package/dist/core/adapters/playwright-ui.d.ts +45 -7
  11. package/dist/core/adapters/playwright-ui.js +365 -59
  12. package/dist/core/assertions/engine.d.ts +51 -0
  13. package/dist/core/assertions/engine.js +530 -0
  14. package/dist/core/assertions/index.d.ts +11 -0
  15. package/dist/core/assertions/index.js +11 -0
  16. package/dist/core/assertions/types.d.ts +121 -0
  17. package/dist/core/assertions/types.js +37 -0
  18. package/dist/core/crawler/index.d.ts +57 -0
  19. package/dist/core/crawler/index.js +281 -0
  20. package/dist/core/crawler/journey-generator.d.ts +49 -0
  21. package/dist/core/crawler/journey-generator.js +412 -0
  22. package/dist/core/crawler/page-analyzer.d.ts +88 -0
  23. package/dist/core/crawler/page-analyzer.js +709 -0
  24. package/dist/core/crawler/selector-generator.d.ts +34 -0
  25. package/dist/core/crawler/selector-generator.js +240 -0
  26. package/dist/core/crawler/types.d.ts +353 -0
  27. package/dist/core/crawler/types.js +6 -0
  28. package/dist/core/generation/crawler-pack-generator.d.ts +44 -0
  29. package/dist/core/generation/crawler-pack-generator.js +231 -0
  30. package/dist/core/generation/index.d.ts +2 -0
  31. package/dist/core/generation/index.js +2 -0
  32. package/dist/core/index.d.ts +3 -0
  33. package/dist/core/index.js +4 -0
  34. package/dist/core/types/pack-v1.d.ts +90 -0
  35. package/dist/index.js +6 -2
  36. package/examples/accessibility.yml +39 -16
  37. package/examples/api-basic.yml +19 -14
  38. package/examples/complete.yml +134 -42
  39. package/examples/fullstack.yml +66 -31
  40. package/examples/security.yml +47 -15
  41. package/examples/ui-basic.yml +16 -12
  42. package/package.json +3 -2
@@ -0,0 +1,412 @@
1
+ /**
2
+ * QA360 Journey Generator
3
+ *
4
+ * Automatically generates user journeys from crawled pages
5
+ */
6
+ /**
7
+ * Journey Generator class
8
+ */
9
+ export class JourneyGenerator {
10
+ siteMap;
11
+ constructor(siteMap) {
12
+ this.siteMap = siteMap;
13
+ }
14
+ /**
15
+ * Generate all user journeys from the site map
16
+ */
17
+ generateJourneys() {
18
+ const journeys = [];
19
+ // Generate authentication journeys
20
+ journeys.push(...this.generateAuthJourneys());
21
+ // Generate navigation journeys
22
+ journeys.push(...this.generateNavigationJourneys());
23
+ // Generate form submission journeys
24
+ journeys.push(...this.generateFormJourneys());
25
+ // Generate search journeys
26
+ journeys.push(...this.generateSearchJourneys());
27
+ // Generate transaction journeys (checkout, etc.)
28
+ journeys.push(...this.generateTransactionJourneys());
29
+ return journeys.filter(j => j.steps.length > 1);
30
+ }
31
+ /**
32
+ * Generate authentication journeys (login, signup)
33
+ */
34
+ generateAuthJourneys() {
35
+ const journeys = [];
36
+ // Find login page
37
+ const loginPage = this.siteMap.pages.find(p => p.pageType === 'login');
38
+ if (loginPage) {
39
+ const loginForm = loginPage.elements.forms.find(f => f.purpose === 'login');
40
+ if (loginForm) {
41
+ journeys.push({
42
+ name: 'User Login',
43
+ description: 'Standard user login flow with email and password',
44
+ type: 'authentication',
45
+ entryPoint: loginPage.url,
46
+ finalUrl: this.siteMap.pages.find(p => p.pageType === 'dashboard')?.url,
47
+ estimatedDuration: 5,
48
+ steps: this.generateLoginSteps(loginPage, loginForm),
49
+ });
50
+ }
51
+ }
52
+ // Find signup page
53
+ const signupPage = this.siteMap.pages.find(p => p.pageType === 'signup');
54
+ if (signupPage) {
55
+ const signupForm = signupPage.elements.forms.find(f => f.purpose === 'signup');
56
+ if (signupForm) {
57
+ journeys.push({
58
+ name: 'User Registration',
59
+ description: 'New user registration flow',
60
+ type: 'authentication',
61
+ entryPoint: signupPage.url,
62
+ estimatedDuration: 8,
63
+ steps: this.generateSignupSteps(signupPage, signupForm),
64
+ });
65
+ }
66
+ }
67
+ return journeys;
68
+ }
69
+ /**
70
+ * Generate login flow steps
71
+ */
72
+ generateLoginSteps(page, form) {
73
+ const steps = [];
74
+ // Navigate to login page
75
+ steps.push({
76
+ order: steps.length + 1,
77
+ description: 'Navigate to login page',
78
+ action: 'navigate',
79
+ value: page.url,
80
+ expected: { url: page.url },
81
+ });
82
+ // Fill email field
83
+ const emailField = form.fields.find(f => f.inputType === 'email' || f.name?.includes('email') || f.name?.includes('username'));
84
+ if (emailField) {
85
+ steps.push({
86
+ order: steps.length + 1,
87
+ description: 'Enter email/username',
88
+ action: 'fill',
89
+ selector: emailField.selector,
90
+ value: '${TEST_USER_EMAIL}',
91
+ });
92
+ }
93
+ // Fill password field
94
+ const passwordField = form.fields.find(f => f.inputType === 'password');
95
+ if (passwordField) {
96
+ steps.push({
97
+ order: steps.length + 1,
98
+ description: 'Enter password',
99
+ action: 'fill',
100
+ selector: passwordField.selector,
101
+ value: '${TEST_USER_PASSWORD}',
102
+ });
103
+ }
104
+ // Click submit
105
+ if (form.submitButton) {
106
+ steps.push({
107
+ order: steps.length + 1,
108
+ description: 'Click login button',
109
+ action: 'click',
110
+ selector: form.submitButton.selector,
111
+ expected: { visible: '.user-menu, .dashboard, .welcome' },
112
+ });
113
+ }
114
+ return steps;
115
+ }
116
+ /**
117
+ * Generate signup flow steps
118
+ */
119
+ generateSignupSteps(page, form) {
120
+ const steps = [];
121
+ // Navigate to signup page
122
+ steps.push({
123
+ order: steps.length + 1,
124
+ description: 'Navigate to registration page',
125
+ action: 'navigate',
126
+ value: page.url,
127
+ expected: { url: page.url },
128
+ });
129
+ // Fill all required fields
130
+ for (const field of form.fields) {
131
+ if (!field.required)
132
+ continue;
133
+ const step = {
134
+ order: steps.length + 1,
135
+ description: `Enter ${field.name || field.inputType}`,
136
+ action: field.inputType === 'select-one' ? 'select' : 'fill',
137
+ selector: field.selector,
138
+ };
139
+ // Set test value based on field type
140
+ if (field.inputType === 'email') {
141
+ step.value = '${TEST_USER_EMAIL}';
142
+ }
143
+ else if (field.inputType === 'password') {
144
+ step.value = '${TEST_USER_PASSWORD}';
145
+ }
146
+ else if (field.inputType === 'select-one' && field.options && field.options.length > 0) {
147
+ step.value = field.options[0];
148
+ }
149
+ else {
150
+ step.value = 'test-value';
151
+ }
152
+ steps.push(step);
153
+ }
154
+ // Click submit
155
+ if (form.submitButton) {
156
+ steps.push({
157
+ order: steps.length + 1,
158
+ description: 'Submit registration form',
159
+ action: 'click',
160
+ selector: form.submitButton.selector,
161
+ });
162
+ }
163
+ return steps;
164
+ }
165
+ /**
166
+ * Generate navigation journeys
167
+ */
168
+ generateNavigationJourneys() {
169
+ const journeys = [];
170
+ // Homepage navigation journey
171
+ const homepage = this.siteMap.pages.find(p => p.pageType === 'homepage');
172
+ if (homepage) {
173
+ const steps = [];
174
+ // Start at homepage
175
+ steps.push({
176
+ order: steps.length + 1,
177
+ description: 'Navigate to homepage',
178
+ action: 'navigate',
179
+ value: homepage.url,
180
+ expected: { url: homepage.url },
181
+ });
182
+ // Add navigation menu clicks
183
+ if (homepage.navigation.main) {
184
+ for (const link of homepage.navigation.main.items.slice(0, 5)) {
185
+ steps.push({
186
+ order: steps.length + 1,
187
+ description: `Navigate to "${link.text || link.selector}"`,
188
+ action: 'click',
189
+ selector: link.selector,
190
+ expected: { url: link.url },
191
+ });
192
+ steps.push({
193
+ order: steps.length + 1,
194
+ description: 'Wait for page load',
195
+ action: 'waitForNavigation',
196
+ });
197
+ }
198
+ }
199
+ journeys.push({
200
+ name: 'Main Navigation Flow',
201
+ description: 'Navigate through main menu items',
202
+ type: 'navigation',
203
+ entryPoint: homepage.url,
204
+ estimatedDuration: steps.length * 3,
205
+ steps,
206
+ });
207
+ }
208
+ return journeys;
209
+ }
210
+ /**
211
+ * Generate form submission journeys
212
+ */
213
+ generateFormJourneys() {
214
+ const journeys = [];
215
+ for (const page of this.siteMap.pages) {
216
+ for (const form of page.elements.forms) {
217
+ if (form.purpose === 'login' || form.purpose === 'signup')
218
+ continue;
219
+ const steps = [];
220
+ // Navigate to page
221
+ steps.push({
222
+ order: steps.length + 1,
223
+ description: `Navigate to ${form.purpose} form`,
224
+ action: 'navigate',
225
+ value: page.url,
226
+ });
227
+ // Fill form fields
228
+ for (const field of form.fields) {
229
+ if (!field.required && Math.random() > 0.5)
230
+ continue;
231
+ const step = {
232
+ order: steps.length + 1,
233
+ description: `Fill ${field.name || field.inputType} field`,
234
+ action: field.inputType === 'select-one' ? 'select' : 'fill',
235
+ selector: field.selector,
236
+ };
237
+ // Set appropriate test value
238
+ if (field.inputType === 'email') {
239
+ step.value = 'test@example.com';
240
+ }
241
+ else if (field.inputType === 'tel') {
242
+ step.value = '+1234567890';
243
+ }
244
+ else if (field.inputType === 'number') {
245
+ const min = field.validation?.min;
246
+ const max = field.validation?.max;
247
+ const num = typeof min === 'number' ? min + 1 : 1;
248
+ step.value = String(typeof max === 'number' ? Math.min(num, max - 1) : num);
249
+ }
250
+ else if (field.inputType === 'select-one' && field.options && field.options.length > 0) {
251
+ step.value = field.options[0];
252
+ }
253
+ else if (field.inputType === 'checkbox') {
254
+ step.action = 'check';
255
+ }
256
+ else {
257
+ step.value = 'Test value';
258
+ }
259
+ steps.push(step);
260
+ }
261
+ // Submit form
262
+ if (form.submitButton) {
263
+ steps.push({
264
+ order: steps.length + 1,
265
+ description: `Submit ${form.purpose} form`,
266
+ action: 'click',
267
+ selector: form.submitButton.selector,
268
+ });
269
+ }
270
+ journeys.push({
271
+ name: `${form.purpose.charAt(0).toUpperCase() + form.purpose.slice(1)} Form Submission`,
272
+ description: `Fill and submit the ${form.purpose} form`,
273
+ type: 'form-submission',
274
+ entryPoint: page.url,
275
+ estimatedDuration: steps.length * 2,
276
+ steps,
277
+ });
278
+ }
279
+ }
280
+ return journeys;
281
+ }
282
+ /**
283
+ * Generate search journeys
284
+ */
285
+ generateSearchJourneys() {
286
+ const journeys = [];
287
+ for (const page of this.siteMap.pages) {
288
+ const searchForm = page.elements.forms.find(f => f.purpose === 'search');
289
+ if (!searchForm)
290
+ continue;
291
+ const steps = [];
292
+ // Navigate to page
293
+ steps.push({
294
+ order: steps.length + 1,
295
+ description: 'Navigate to search page',
296
+ action: 'navigate',
297
+ value: page.url,
298
+ });
299
+ // Fill search field
300
+ const searchField = searchForm.fields.find(f => f.name?.includes('q') || f.name?.includes('search'));
301
+ if (searchField) {
302
+ steps.push({
303
+ order: steps.length + 1,
304
+ description: 'Enter search query',
305
+ action: 'fill',
306
+ selector: searchField.selector,
307
+ value: '${SEARCH_QUERY}',
308
+ });
309
+ }
310
+ // Submit search
311
+ if (searchForm.submitButton) {
312
+ steps.push({
313
+ order: steps.length + 1,
314
+ description: 'Click search button',
315
+ action: 'click',
316
+ selector: searchForm.submitButton.selector,
317
+ expected: { visible: '.search-results, .results, [role="main"]' },
318
+ });
319
+ }
320
+ else {
321
+ // Press Enter
322
+ steps.push({
323
+ order: steps.length + 1,
324
+ description: 'Submit search (Enter)',
325
+ action: 'press',
326
+ value: 'Enter',
327
+ });
328
+ }
329
+ journeys.push({
330
+ name: 'Search Query',
331
+ description: 'Perform a search query and verify results',
332
+ type: 'search',
333
+ entryPoint: page.url,
334
+ estimatedDuration: 3,
335
+ steps,
336
+ });
337
+ }
338
+ return journeys;
339
+ }
340
+ /**
341
+ * Generate transaction journeys (checkout, booking, etc.)
342
+ */
343
+ generateTransactionJourneys() {
344
+ const journeys = [];
345
+ // Find checkout flow
346
+ const checkoutPage = this.siteMap.pages.find(p => p.elements.forms.some(f => f.purpose === 'checkout'));
347
+ if (checkoutPage) {
348
+ const checkoutForm = checkoutPage.elements.forms.find(f => f.purpose === 'checkout');
349
+ if (checkoutForm) {
350
+ const steps = [];
351
+ steps.push({
352
+ order: steps.length + 1,
353
+ description: 'Navigate to checkout',
354
+ action: 'navigate',
355
+ value: checkoutPage.url,
356
+ });
357
+ // Fill checkout fields
358
+ for (const field of checkoutForm.fields) {
359
+ const step = {
360
+ order: steps.length + 1,
361
+ description: `Fill ${field.name || field.inputType}`,
362
+ action: field.inputType === 'select-one' ? 'select' : 'fill',
363
+ selector: field.selector,
364
+ };
365
+ // Set test payment data
366
+ if (field.name?.includes('card')) {
367
+ step.value = '4111111111111111'; // Test card number
368
+ }
369
+ else if (field.name?.includes('cvv') || field.name?.includes('cvc')) {
370
+ step.value = '123';
371
+ }
372
+ else if (field.name?.includes('expiry') || field.name?.includes('exp')) {
373
+ step.value = '12/25';
374
+ }
375
+ else if (field.inputType === 'select-one' && field.options && field.options.length > 0) {
376
+ step.value = field.options[0];
377
+ }
378
+ else {
379
+ step.value = 'Test value';
380
+ }
381
+ steps.push(step);
382
+ }
383
+ // Submit checkout
384
+ if (checkoutForm.submitButton) {
385
+ steps.push({
386
+ order: steps.length + 1,
387
+ description: 'Complete purchase',
388
+ action: 'click',
389
+ selector: checkoutForm.submitButton.selector,
390
+ expected: { visible: '.order-confirmation, .thank-you, [role="status"]' },
391
+ });
392
+ }
393
+ journeys.push({
394
+ name: 'Checkout Flow',
395
+ description: 'Complete a purchase flow',
396
+ type: 'transaction',
397
+ entryPoint: checkoutPage.url,
398
+ estimatedDuration: 10,
399
+ steps,
400
+ });
401
+ }
402
+ }
403
+ return journeys;
404
+ }
405
+ }
406
+ /**
407
+ * Generate journeys from a site map
408
+ */
409
+ export function generateJourneys(siteMap) {
410
+ const generator = new JourneyGenerator(siteMap);
411
+ return generator.generateJourneys();
412
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * QA360 Page Analyzer
3
+ *
4
+ * Analyzes web pages to discover elements, forms, and patterns
5
+ */
6
+ import type { CrawlOptions, PageDefinition } from './types.js';
7
+ /**
8
+ * Page Analyzer class
9
+ */
10
+ export declare class PageAnalyzer {
11
+ private browser?;
12
+ private context?;
13
+ private page?;
14
+ private options;
15
+ constructor(options: CrawlOptions);
16
+ /**
17
+ * Initialize browser
18
+ */
19
+ private initBrowser;
20
+ /**
21
+ * Perform authentication if configured
22
+ */
23
+ private performAuth;
24
+ /**
25
+ * Analyze a single page
26
+ */
27
+ analyze(url: string, depth: number): Promise<PageDefinition>;
28
+ /**
29
+ * Discover all interactive elements on the page
30
+ */
31
+ private discoverElements;
32
+ /**
33
+ * Discover buttons
34
+ */
35
+ private discoverButtons;
36
+ /**
37
+ * Discover links
38
+ */
39
+ private discoverLinks;
40
+ /**
41
+ * Discover forms
42
+ */
43
+ private discoverForms;
44
+ /**
45
+ * Discover fields within a form
46
+ */
47
+ private discoverFormFields;
48
+ /**
49
+ * Detect form purpose based on fields
50
+ */
51
+ private detectFormPurpose;
52
+ /**
53
+ * Calculate confidence for form purpose detection
54
+ */
55
+ private calculateFormPurposeConfidence;
56
+ /**
57
+ * Discover standalone input fields (not in forms)
58
+ */
59
+ private discoverInputs;
60
+ /**
61
+ * Discover select elements
62
+ */
63
+ private discoverSelects;
64
+ /**
65
+ * Discover checkboxes
66
+ */
67
+ private discoverCheckboxes;
68
+ /**
69
+ * Discover radio buttons
70
+ */
71
+ private discoverRadios;
72
+ /**
73
+ * Analyze page navigation structure
74
+ */
75
+ private analyzeNavigation;
76
+ /**
77
+ * Run accessibility scan using axe-core
78
+ */
79
+ private runAccessibilityScan;
80
+ /**
81
+ * Detect page type
82
+ */
83
+ private detectPageType;
84
+ /**
85
+ * Clean up resources
86
+ */
87
+ cleanup(): Promise<void>;
88
+ }