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.
- package/dist/commands/ai.js +26 -14
- package/dist/commands/ask.d.ts +75 -23
- package/dist/commands/ask.js +413 -265
- package/dist/commands/crawl.d.ts +24 -0
- package/dist/commands/crawl.js +121 -0
- package/dist/commands/history.js +38 -3
- package/dist/commands/init.d.ts +89 -95
- package/dist/commands/init.js +282 -200
- package/dist/commands/run.d.ts +1 -0
- package/dist/core/adapters/playwright-ui.d.ts +45 -7
- package/dist/core/adapters/playwright-ui.js +365 -59
- package/dist/core/assertions/engine.d.ts +51 -0
- package/dist/core/assertions/engine.js +530 -0
- package/dist/core/assertions/index.d.ts +11 -0
- package/dist/core/assertions/index.js +11 -0
- package/dist/core/assertions/types.d.ts +121 -0
- package/dist/core/assertions/types.js +37 -0
- package/dist/core/crawler/index.d.ts +57 -0
- package/dist/core/crawler/index.js +281 -0
- package/dist/core/crawler/journey-generator.d.ts +49 -0
- package/dist/core/crawler/journey-generator.js +412 -0
- package/dist/core/crawler/page-analyzer.d.ts +88 -0
- package/dist/core/crawler/page-analyzer.js +709 -0
- package/dist/core/crawler/selector-generator.d.ts +34 -0
- package/dist/core/crawler/selector-generator.js +240 -0
- package/dist/core/crawler/types.d.ts +353 -0
- package/dist/core/crawler/types.js +6 -0
- package/dist/core/generation/crawler-pack-generator.d.ts +44 -0
- package/dist/core/generation/crawler-pack-generator.js +231 -0
- package/dist/core/generation/index.d.ts +2 -0
- package/dist/core/generation/index.js +2 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +4 -0
- package/dist/core/types/pack-v1.d.ts +90 -0
- package/dist/index.js +6 -2
- package/examples/accessibility.yml +39 -16
- package/examples/api-basic.yml +19 -14
- package/examples/complete.yml +134 -42
- package/examples/fullstack.yml +66 -31
- package/examples/security.yml +47 -15
- package/examples/ui-basic.yml +16 -12
- package/package.json +3 -2
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QA360 Selector Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates stable, resilient CSS selectors for web elements
|
|
5
|
+
*/
|
|
6
|
+
import type { ElementInfo } from './types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Generate optimal selector for an element
|
|
9
|
+
*/
|
|
10
|
+
export declare function generateSelector(element: {
|
|
11
|
+
tagName?: string;
|
|
12
|
+
id?: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
attributes?: Record<string, string>;
|
|
15
|
+
textContent?: string;
|
|
16
|
+
role?: string;
|
|
17
|
+
name?: string;
|
|
18
|
+
}): string;
|
|
19
|
+
/**
|
|
20
|
+
* Generate selector from Playwright element handle
|
|
21
|
+
*/
|
|
22
|
+
export declare function generateSelectorFromElement(element: any, page: any): Promise<ElementInfo>;
|
|
23
|
+
/**
|
|
24
|
+
* Optimize selector for resiliency
|
|
25
|
+
*/
|
|
26
|
+
export declare function optimizeSelector(selector: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Generate fallback selectors for resilience
|
|
29
|
+
*/
|
|
30
|
+
export declare function generateFallbackSelectors(primarySelector: string): string[];
|
|
31
|
+
/**
|
|
32
|
+
* Score selector quality (0-100)
|
|
33
|
+
*/
|
|
34
|
+
export declare function scoreSelector(selector: string): number;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QA360 Selector Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates stable, resilient CSS selectors for web elements
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Selector priority (most stable first)
|
|
8
|
+
*/
|
|
9
|
+
const SELECTOR_PRIORITY = [
|
|
10
|
+
'data-testid', // Best: explicit test attribute
|
|
11
|
+
'data-cy', // Cypress convention
|
|
12
|
+
'data-test', // Common convention
|
|
13
|
+
'data-test-id', // Common convention
|
|
14
|
+
'id', // Good if stable
|
|
15
|
+
'aria-label', // Accessible, usually stable
|
|
16
|
+
'name', // Form field names
|
|
17
|
+
'role + aria-label', // ARIA combo
|
|
18
|
+
'class', // Less stable, use as fallback
|
|
19
|
+
'tag + text', // Content-based
|
|
20
|
+
'nth-child', // Last resort
|
|
21
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* Generate optimal selector for an element
|
|
24
|
+
*/
|
|
25
|
+
export function generateSelector(element) {
|
|
26
|
+
const { tagName, id, className, attributes = {}, textContent, role, name } = element;
|
|
27
|
+
// 1. Best: data-testid or similar test attributes
|
|
28
|
+
for (const attr of ['data-testid', 'data-cy', 'data-test', 'data-test-id']) {
|
|
29
|
+
if (attributes[attr]) {
|
|
30
|
+
return `[${attr}="${attributes[attr]}"]`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// 2. ID (if not auto-generated)
|
|
34
|
+
if (id && !isGeneratedId(id)) {
|
|
35
|
+
return `#${escapeCss(id)}`;
|
|
36
|
+
}
|
|
37
|
+
// 3. aria-label (accessible and stable)
|
|
38
|
+
if (attributes['aria-label']) {
|
|
39
|
+
return `[aria-label="${escapeCss(attributes['aria-label'])}"]`;
|
|
40
|
+
}
|
|
41
|
+
// 4. Form field name
|
|
42
|
+
if (name) {
|
|
43
|
+
return `[name="${escapeCss(name)}"]`;
|
|
44
|
+
}
|
|
45
|
+
// 5. Role + label combination
|
|
46
|
+
if (role && attributes['aria-label']) {
|
|
47
|
+
return `[role="${role}"][aria-label="${escapeCss(attributes['aria-label'])}"]`;
|
|
48
|
+
}
|
|
49
|
+
// 6. Class (avoid if too generic)
|
|
50
|
+
if (className && !isGenericClass(className)) {
|
|
51
|
+
const classes = className.split(' ').filter(c => !isGenericClass(c));
|
|
52
|
+
if (classes.length > 0) {
|
|
53
|
+
const classSelector = classes.map(c => `.${escapeCss(c)}`).join('');
|
|
54
|
+
// Qualify with tag if available
|
|
55
|
+
return tagName ? `${tagName}${classSelector}` : classSelector;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// 7. Tag + text content (for buttons, links)
|
|
59
|
+
if (tagName && textContent && textContent.trim()) {
|
|
60
|
+
const text = textContent.trim().slice(0, 50);
|
|
61
|
+
return `${tagName}:has-text("${escapeCss(text)}")`;
|
|
62
|
+
}
|
|
63
|
+
// 8. Tag only (last resort)
|
|
64
|
+
return tagName || '*';
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Generate selector from Playwright element handle
|
|
68
|
+
*/
|
|
69
|
+
export async function generateSelectorFromElement(element, page) {
|
|
70
|
+
try {
|
|
71
|
+
// Get all attributes
|
|
72
|
+
const attributes = await element.evaluate((el) => {
|
|
73
|
+
const attrs = {};
|
|
74
|
+
for (const attr of el.attributes || []) {
|
|
75
|
+
attrs[attr.name] = attr.value;
|
|
76
|
+
}
|
|
77
|
+
return attrs;
|
|
78
|
+
});
|
|
79
|
+
// Get basic info
|
|
80
|
+
const tagName = await element.evaluate((el) => el.tagName?.toLowerCase());
|
|
81
|
+
const textContent = await element.evaluate((el) => el.textContent?.trim());
|
|
82
|
+
const className = attributes.class || attributes.className;
|
|
83
|
+
const id = attributes.id;
|
|
84
|
+
const role = attributes.role || attributes['role'];
|
|
85
|
+
const name = attributes.name;
|
|
86
|
+
// Generate selector
|
|
87
|
+
const selector = generateSelector({
|
|
88
|
+
tagName,
|
|
89
|
+
id,
|
|
90
|
+
className,
|
|
91
|
+
attributes,
|
|
92
|
+
textContent,
|
|
93
|
+
role,
|
|
94
|
+
name,
|
|
95
|
+
});
|
|
96
|
+
// Determine stability
|
|
97
|
+
const stable = isSelectorStable(selector);
|
|
98
|
+
return {
|
|
99
|
+
selector,
|
|
100
|
+
text: textContent || undefined,
|
|
101
|
+
ariaLabel: attributes['aria-label'],
|
|
102
|
+
testId: attributes['data-testid'] || attributes['data-cy'] || attributes['data-test'],
|
|
103
|
+
role: role || undefined,
|
|
104
|
+
type: tagName || 'unknown',
|
|
105
|
+
stable,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
// Fallback
|
|
110
|
+
return {
|
|
111
|
+
selector: 'unknown',
|
|
112
|
+
type: 'unknown',
|
|
113
|
+
stable: false,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Check if ID is auto-generated (not stable)
|
|
119
|
+
*/
|
|
120
|
+
function isGeneratedId(id) {
|
|
121
|
+
// Patterns indicating auto-generated IDs
|
|
122
|
+
const generatedPatterns = [
|
|
123
|
+
/^\w+_\d+$/, // react_123, vue_456
|
|
124
|
+
/^(ember|ember)\d+$/, // ember1234
|
|
125
|
+
/^yui_[^_]+_\d+$/, // yui_gen_123
|
|
126
|
+
/^\w+-\w+-\w+$/, // random-hash patterns
|
|
127
|
+
/^[a-f0-9]{8,}$/, // hex hashes
|
|
128
|
+
];
|
|
129
|
+
return generatedPatterns.some(pattern => pattern.test(id));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Check if class is too generic
|
|
133
|
+
*/
|
|
134
|
+
function isGenericClass(className) {
|
|
135
|
+
const genericClasses = [
|
|
136
|
+
'active', 'inactive', 'selected', 'disabled', 'enabled',
|
|
137
|
+
'hidden', 'visible', 'show', 'hide', 'open', 'closed',
|
|
138
|
+
'first', 'last', 'odd', 'even',
|
|
139
|
+
'container', 'wrapper', 'content', 'section',
|
|
140
|
+
'btn', 'button', 'input', 'form', 'field',
|
|
141
|
+
'row', 'col', 'column', 'grid',
|
|
142
|
+
];
|
|
143
|
+
const classes = className.split(' ');
|
|
144
|
+
return classes.every(c => genericClasses.includes(c));
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Check if selector is considered stable
|
|
148
|
+
*/
|
|
149
|
+
function isSelectorStable(selector) {
|
|
150
|
+
// Stable selectors: data-testid, aria-label, stable IDs
|
|
151
|
+
const stablePatterns = [
|
|
152
|
+
/\[data-(testid|test|test-id|cy)/,
|
|
153
|
+
/\[aria-label=/,
|
|
154
|
+
/\[name=/,
|
|
155
|
+
/#\w[\w-]*\w/, // ID with word chars
|
|
156
|
+
];
|
|
157
|
+
// Unstable selectors: nth-child, generated classes
|
|
158
|
+
const unstablePatterns = [
|
|
159
|
+
/:nth-child/,
|
|
160
|
+
/:\w+-\w+-\w+/, // hash-like classes
|
|
161
|
+
];
|
|
162
|
+
const hasStable = stablePatterns.some(p => p.test(selector));
|
|
163
|
+
const hasUnstable = unstablePatterns.some(p => p.test(selector));
|
|
164
|
+
return hasStable || !hasUnstable;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Escape special CSS characters
|
|
168
|
+
*/
|
|
169
|
+
function escapeCss(str) {
|
|
170
|
+
return str.replace(/(["\\])/g, '\\$1').replace(/"/g, '\\"');
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Optimize selector for resiliency
|
|
174
|
+
*/
|
|
175
|
+
export function optimizeSelector(selector) {
|
|
176
|
+
// Remove unnecessary descendants
|
|
177
|
+
let optimized = selector.replace(/\s*>\s*/g, ' > ');
|
|
178
|
+
// Use :has() for more specific targeting
|
|
179
|
+
if (optimized.includes(' ')) {
|
|
180
|
+
const parts = optimized.split(' ');
|
|
181
|
+
if (parts.length > 3) {
|
|
182
|
+
// Keep only the last 3 parts for specificity
|
|
183
|
+
optimized = parts.slice(-3).join(' ');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return optimized;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Generate fallback selectors for resilience
|
|
190
|
+
*/
|
|
191
|
+
export function generateFallbackSelectors(primarySelector) {
|
|
192
|
+
const fallbacks = [];
|
|
193
|
+
// If primary is data-testid, add aria-label fallback
|
|
194
|
+
if (primarySelector.includes('[data-testid')) {
|
|
195
|
+
const testId = primarySelector.match(/\[data-testid="([^"]+)"\]/)?.[1];
|
|
196
|
+
if (testId) {
|
|
197
|
+
// Try aria-label with similar text
|
|
198
|
+
fallbacks.push(`[aria-label="${testId}"]`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// If primary is ID, add name fallback
|
|
202
|
+
if (primarySelector.startsWith('#')) {
|
|
203
|
+
const id = primarySelector.slice(1);
|
|
204
|
+
fallbacks.push(`[name="${id}"]`);
|
|
205
|
+
}
|
|
206
|
+
// Add generic fallback based on tag
|
|
207
|
+
const tagMatch = primarySelector.match(/^(\w+)/);
|
|
208
|
+
if (tagMatch) {
|
|
209
|
+
fallbacks.push(tagMatch[1]);
|
|
210
|
+
}
|
|
211
|
+
return fallbacks;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Score selector quality (0-100)
|
|
215
|
+
*/
|
|
216
|
+
export function scoreSelector(selector) {
|
|
217
|
+
let score = 50; // Base score
|
|
218
|
+
// Bonus for test attributes
|
|
219
|
+
if (selector.includes('[data-testid'))
|
|
220
|
+
score += 40;
|
|
221
|
+
else if (selector.includes('[data-test'))
|
|
222
|
+
score += 35;
|
|
223
|
+
else if (selector.includes('[data-cy'))
|
|
224
|
+
score += 30;
|
|
225
|
+
else if (selector.includes('[aria-label'))
|
|
226
|
+
score += 25;
|
|
227
|
+
else if (selector.startsWith('#'))
|
|
228
|
+
score += 20;
|
|
229
|
+
else if (selector.includes('[name='))
|
|
230
|
+
score += 15;
|
|
231
|
+
// Penalty for unstable patterns
|
|
232
|
+
if (selector.includes(':nth-child'))
|
|
233
|
+
score -= 30;
|
|
234
|
+
if (selector.includes(':has-text'))
|
|
235
|
+
score -= 10;
|
|
236
|
+
// Bonus for specificity balance
|
|
237
|
+
if (!selector.includes(' '))
|
|
238
|
+
score += 5; // Simple selector
|
|
239
|
+
return Math.max(0, Math.min(100, score));
|
|
240
|
+
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QA360 Web Crawler - Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* Automatically discovers and analyzes web applications for E2E test generation
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Crawler configuration options
|
|
8
|
+
*/
|
|
9
|
+
export interface CrawlOptions {
|
|
10
|
+
/** Base URL to start crawling */
|
|
11
|
+
baseUrl: string;
|
|
12
|
+
/** Maximum depth to follow links (default: 3) */
|
|
13
|
+
maxDepth?: number;
|
|
14
|
+
/** Maximum number of pages to crawl (default: 50) */
|
|
15
|
+
maxPages?: number;
|
|
16
|
+
/** Follow internal links (default: true) */
|
|
17
|
+
followLinks?: boolean;
|
|
18
|
+
/** Discover and analyze forms (default: true) */
|
|
19
|
+
discoverForms?: boolean;
|
|
20
|
+
/** Discover buttons and actions (default: true) */
|
|
21
|
+
discoverButtons?: boolean;
|
|
22
|
+
/** Regex patterns to exclude (e.g., ['/admin', '/logout', '/api']) */
|
|
23
|
+
excludePatterns?: string[];
|
|
24
|
+
/** Authentication for protected areas */
|
|
25
|
+
auth?: CrawlAuth;
|
|
26
|
+
/** Timeout per page in milliseconds (default: 30000) */
|
|
27
|
+
timeout?: number;
|
|
28
|
+
/** Whether to take screenshots (default: false) */
|
|
29
|
+
screenshots?: boolean;
|
|
30
|
+
/** Headless mode (default: true) */
|
|
31
|
+
headless?: boolean;
|
|
32
|
+
/** Wait for network idle before analysis (default: true) */
|
|
33
|
+
waitForNetworkIdle?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Authentication for crawling
|
|
37
|
+
*/
|
|
38
|
+
export interface CrawlAuth {
|
|
39
|
+
/** Type of authentication */
|
|
40
|
+
type: 'basic' | 'form' | 'bearer' | 'cookie';
|
|
41
|
+
/** URL for login form (for type: 'form') */
|
|
42
|
+
loginUrl?: string;
|
|
43
|
+
/** Username */
|
|
44
|
+
username?: string;
|
|
45
|
+
/** Password */
|
|
46
|
+
password?: string;
|
|
47
|
+
/** Username selector (for type: 'form') */
|
|
48
|
+
usernameSelector?: string;
|
|
49
|
+
/** Password selector (for type: 'form') */
|
|
50
|
+
passwordSelector?: string;
|
|
51
|
+
/** Submit button selector (for type: 'form') */
|
|
52
|
+
submitSelector?: string;
|
|
53
|
+
/** Bearer token (for type: 'bearer') */
|
|
54
|
+
token?: string;
|
|
55
|
+
/** Cookies (for type: 'cookie') */
|
|
56
|
+
cookies?: Array<{
|
|
57
|
+
name: string;
|
|
58
|
+
value: string;
|
|
59
|
+
domain?: string;
|
|
60
|
+
}>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Element information discovered on a page
|
|
64
|
+
*/
|
|
65
|
+
export interface ElementInfo {
|
|
66
|
+
/** CSS selector (optimized for stability) */
|
|
67
|
+
selector: string;
|
|
68
|
+
/** Text content */
|
|
69
|
+
text?: string;
|
|
70
|
+
/** aria-label if present */
|
|
71
|
+
ariaLabel?: string;
|
|
72
|
+
/** data-testid if present */
|
|
73
|
+
testId?: string;
|
|
74
|
+
/** Element role */
|
|
75
|
+
role?: string;
|
|
76
|
+
/** Element type */
|
|
77
|
+
type: string;
|
|
78
|
+
/** Whether selector is considered stable */
|
|
79
|
+
stable: boolean;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Form field information
|
|
83
|
+
*/
|
|
84
|
+
export interface FieldInfo extends ElementInfo {
|
|
85
|
+
/** Field input type */
|
|
86
|
+
inputType: string;
|
|
87
|
+
/** Field name */
|
|
88
|
+
name?: string;
|
|
89
|
+
/** Whether field is required */
|
|
90
|
+
required: boolean;
|
|
91
|
+
/** Placeholder text */
|
|
92
|
+
placeholder?: string;
|
|
93
|
+
/** Options for select elements */
|
|
94
|
+
options?: string[];
|
|
95
|
+
/** Validation attributes */
|
|
96
|
+
validation?: {
|
|
97
|
+
min?: number | string;
|
|
98
|
+
max?: number | string;
|
|
99
|
+
pattern?: string;
|
|
100
|
+
minLength?: number;
|
|
101
|
+
maxLength?: number;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Form definition discovered on a page
|
|
106
|
+
*/
|
|
107
|
+
export interface FormInfo {
|
|
108
|
+
/** Form action/endpoint */
|
|
109
|
+
action?: string;
|
|
110
|
+
/** Form method */
|
|
111
|
+
method?: string;
|
|
112
|
+
/** Stable selector for the form */
|
|
113
|
+
selector: string;
|
|
114
|
+
/** Form purpose (detected) */
|
|
115
|
+
purpose: 'login' | 'signup' | 'search' | 'contact' | 'checkout' | 'filter' | 'other';
|
|
116
|
+
/** All fields in the form */
|
|
117
|
+
fields: FieldInfo[];
|
|
118
|
+
/** Submit button */
|
|
119
|
+
submitButton?: ElementInfo;
|
|
120
|
+
/** Confidence score for purpose detection (0-1) */
|
|
121
|
+
confidence: number;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Link information
|
|
125
|
+
*/
|
|
126
|
+
export interface LinkInfo extends ElementInfo {
|
|
127
|
+
/** href URL */
|
|
128
|
+
url: string;
|
|
129
|
+
/** Whether link is internal to the domain */
|
|
130
|
+
internal: boolean;
|
|
131
|
+
/** Whether link was visited */
|
|
132
|
+
visited: boolean;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Page definition after crawling
|
|
136
|
+
*/
|
|
137
|
+
export interface PageDefinition {
|
|
138
|
+
/** Full URL */
|
|
139
|
+
url: string;
|
|
140
|
+
/** Path relative to base */
|
|
141
|
+
path: string;
|
|
142
|
+
/** Page title */
|
|
143
|
+
title: string;
|
|
144
|
+
/** Depth from base URL */
|
|
145
|
+
depth: number;
|
|
146
|
+
/** HTTP status code */
|
|
147
|
+
status: number;
|
|
148
|
+
/** Load time in milliseconds */
|
|
149
|
+
loadTime: number;
|
|
150
|
+
/** Content type */
|
|
151
|
+
contentType?: string;
|
|
152
|
+
/** Meta description */
|
|
153
|
+
description?: string;
|
|
154
|
+
/** All discovered elements */
|
|
155
|
+
elements: {
|
|
156
|
+
/** Buttons on the page */
|
|
157
|
+
buttons: ElementInfo[];
|
|
158
|
+
/** Links on the page */
|
|
159
|
+
links: LinkInfo[];
|
|
160
|
+
/** Forms on the page */
|
|
161
|
+
forms: FormInfo[];
|
|
162
|
+
/** Input fields (standalone) */
|
|
163
|
+
inputs: FieldInfo[];
|
|
164
|
+
/** Select dropdowns */
|
|
165
|
+
selects: FieldInfo[];
|
|
166
|
+
/** Checkboxes */
|
|
167
|
+
checkboxes: ElementInfo[];
|
|
168
|
+
/** Radio buttons */
|
|
169
|
+
radios: ElementInfo[];
|
|
170
|
+
};
|
|
171
|
+
/** Navigation elements detected */
|
|
172
|
+
navigation: {
|
|
173
|
+
/** Main navigation menu */
|
|
174
|
+
main?: {
|
|
175
|
+
selector: string;
|
|
176
|
+
items: LinkInfo[];
|
|
177
|
+
};
|
|
178
|
+
/** Breadcrumb */
|
|
179
|
+
breadcrumb?: {
|
|
180
|
+
selector: string;
|
|
181
|
+
items: Array<{
|
|
182
|
+
text: string;
|
|
183
|
+
url: string;
|
|
184
|
+
}>;
|
|
185
|
+
};
|
|
186
|
+
/** Pagination */
|
|
187
|
+
pagination?: {
|
|
188
|
+
selector: string;
|
|
189
|
+
nextPage?: LinkInfo;
|
|
190
|
+
prevPage?: LinkInfo;
|
|
191
|
+
};
|
|
192
|
+
/** Footer links */
|
|
193
|
+
footer?: {
|
|
194
|
+
selector: string;
|
|
195
|
+
items: LinkInfo[];
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
/** Screenshot (base64) */
|
|
199
|
+
screenshot?: string;
|
|
200
|
+
/** Accessibility snapshot */
|
|
201
|
+
accessibility?: A11ySnapshot;
|
|
202
|
+
/** Page type detected */
|
|
203
|
+
pageType: 'homepage' | 'listing' | 'detail' | 'form' | 'dashboard' | 'login' | 'signup' | 'other';
|
|
204
|
+
/** Confidence for page type (0-1) */
|
|
205
|
+
pageTypeConfidence: number;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Accessibility snapshot from axe-core
|
|
209
|
+
*/
|
|
210
|
+
export interface A11ySnapshot {
|
|
211
|
+
/** Accessibility score (0-100) */
|
|
212
|
+
score: number;
|
|
213
|
+
/** Violations found */
|
|
214
|
+
violations: Array<{
|
|
215
|
+
id: string;
|
|
216
|
+
impact: 'critical' | 'serious' | 'moderate' | 'minor';
|
|
217
|
+
description: string;
|
|
218
|
+
nodes: number;
|
|
219
|
+
selectors: string[];
|
|
220
|
+
}>;
|
|
221
|
+
/** Pass count */
|
|
222
|
+
passes: number;
|
|
223
|
+
/** Incomplete checks */
|
|
224
|
+
incomplete: number;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* User journey discovered
|
|
228
|
+
*/
|
|
229
|
+
export interface UserJourney {
|
|
230
|
+
/** Journey name */
|
|
231
|
+
name: string;
|
|
232
|
+
/** Journey description */
|
|
233
|
+
description: string;
|
|
234
|
+
/** Journey type */
|
|
235
|
+
type: 'authentication' | 'navigation' | 'transaction' | 'search' | 'form-submission' | 'other';
|
|
236
|
+
/** Steps in the journey */
|
|
237
|
+
steps: JourneyStep[];
|
|
238
|
+
/** Entry point URL */
|
|
239
|
+
entryPoint: string;
|
|
240
|
+
/** Expected final URL */
|
|
241
|
+
finalUrl?: string;
|
|
242
|
+
/** Estimated duration in seconds */
|
|
243
|
+
estimatedDuration: number;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Single step in a user journey
|
|
247
|
+
*/
|
|
248
|
+
export interface JourneyStep {
|
|
249
|
+
/** Step order */
|
|
250
|
+
order: number;
|
|
251
|
+
/** Step description */
|
|
252
|
+
description: string;
|
|
253
|
+
/** Action to perform */
|
|
254
|
+
action: JourneyAction;
|
|
255
|
+
/** Target selector */
|
|
256
|
+
selector?: string;
|
|
257
|
+
/** Value to input (for fill actions) */
|
|
258
|
+
value?: string;
|
|
259
|
+
/** Expected outcome */
|
|
260
|
+
expected?: {
|
|
261
|
+
/** Expected URL after action */
|
|
262
|
+
url?: string;
|
|
263
|
+
/** Expected visible element */
|
|
264
|
+
visible?: string;
|
|
265
|
+
/** Expected text content */
|
|
266
|
+
text?: string;
|
|
267
|
+
};
|
|
268
|
+
/** Wait time in ms */
|
|
269
|
+
wait?: number;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Journey action types
|
|
273
|
+
*/
|
|
274
|
+
export type JourneyAction = 'navigate' | 'click' | 'fill' | 'select' | 'check' | 'uncheck' | 'upload' | 'hover' | 'press' | 'waitFor' | 'waitForNavigation' | 'scroll';
|
|
275
|
+
/**
|
|
276
|
+
* Site map structure
|
|
277
|
+
*/
|
|
278
|
+
export interface SiteMap {
|
|
279
|
+
/** Base URL */
|
|
280
|
+
baseUrl: string;
|
|
281
|
+
/** All discovered pages */
|
|
282
|
+
pages: PageDefinition[];
|
|
283
|
+
/** Pages by depth */
|
|
284
|
+
pagesByDepth: Record<number, PageDefinition[]>;
|
|
285
|
+
/** Orphan pages (no links to them) */
|
|
286
|
+
orphans: PageDefinition[];
|
|
287
|
+
/** Page type distribution */
|
|
288
|
+
pageTypeDistribution: Record<string, number>;
|
|
289
|
+
/** Total unique links */
|
|
290
|
+
totalLinks: number;
|
|
291
|
+
/** Total forms */
|
|
292
|
+
totalForms: number;
|
|
293
|
+
/** Crawl metadata */
|
|
294
|
+
metadata: {
|
|
295
|
+
/** Pages crawled */
|
|
296
|
+
pagesCrawled: number;
|
|
297
|
+
/** Pages skipped (excluded) */
|
|
298
|
+
pagesSkipped: number;
|
|
299
|
+
/** Pages failed */
|
|
300
|
+
pagesFailed: number;
|
|
301
|
+
/** Total duration in ms */
|
|
302
|
+
duration: number;
|
|
303
|
+
/** Average page load time */
|
|
304
|
+
avgLoadTime: number;
|
|
305
|
+
/** Max depth reached */
|
|
306
|
+
maxDepth: number;
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Complete crawl result
|
|
311
|
+
*/
|
|
312
|
+
export interface CrawlResult {
|
|
313
|
+
/** Success status */
|
|
314
|
+
success: boolean;
|
|
315
|
+
/** Site map */
|
|
316
|
+
siteMap: SiteMap;
|
|
317
|
+
/** Discovered user journeys */
|
|
318
|
+
userJourneys: UserJourney[];
|
|
319
|
+
/** Forms requiring testing */
|
|
320
|
+
forms: FormInfo[];
|
|
321
|
+
/** Pages requiring accessibility testing */
|
|
322
|
+
accessibilityIssues: Array<{
|
|
323
|
+
page: string;
|
|
324
|
+
score: number;
|
|
325
|
+
violations: A11ySnapshot['violations'];
|
|
326
|
+
}>;
|
|
327
|
+
/** Warnings */
|
|
328
|
+
warnings: string[];
|
|
329
|
+
/** Errors */
|
|
330
|
+
errors: Array<{
|
|
331
|
+
page: string;
|
|
332
|
+
error: string;
|
|
333
|
+
}>;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Crawler progress callback
|
|
337
|
+
*/
|
|
338
|
+
export interface CrawlProgress {
|
|
339
|
+
/** Current page being crawled */
|
|
340
|
+
currentPage: string;
|
|
341
|
+
/** Pages completed */
|
|
342
|
+
completed: number;
|
|
343
|
+
/** Total pages to crawl */
|
|
344
|
+
total: number;
|
|
345
|
+
/** Percentage complete */
|
|
346
|
+
progress: number;
|
|
347
|
+
/** Current depth */
|
|
348
|
+
depth: number;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Crawler events
|
|
352
|
+
*/
|
|
353
|
+
export type CrawlEventHandler = (event: 'page-start' | 'page-complete' | 'page-error' | 'journey-discovered' | 'complete', data: CrawlProgress | PageDefinition | UserJourney | CrawlResult) => void;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QA360 Crawler Pack Generator
|
|
3
|
+
*
|
|
4
|
+
* Crawls a website and generates a complete pack.yml with E2E tests
|
|
5
|
+
*/
|
|
6
|
+
import type { CrawlOptions, CrawlResult } from '../crawler/index.js';
|
|
7
|
+
/**
|
|
8
|
+
* Crawler Pack Generator Options
|
|
9
|
+
*/
|
|
10
|
+
export interface CrawlerPackGeneratorOptions {
|
|
11
|
+
/** Base URL to crawl */
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
/** Output pack file path */
|
|
14
|
+
output?: string;
|
|
15
|
+
/** Crawl options */
|
|
16
|
+
crawl?: Partial<CrawlOptions>;
|
|
17
|
+
/** Pack name */
|
|
18
|
+
packName?: string;
|
|
19
|
+
/** Include accessibility tests */
|
|
20
|
+
includeA11y?: boolean;
|
|
21
|
+
/** Include performance tests */
|
|
22
|
+
includePerf?: boolean;
|
|
23
|
+
/** Include security tests */
|
|
24
|
+
includeSecurity?: boolean;
|
|
25
|
+
/** Custom journey names */
|
|
26
|
+
journeyNames?: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Generate pack.yml from crawled website
|
|
30
|
+
*/
|
|
31
|
+
export declare function generatePackFromCrawl(options: CrawlerPackGeneratorOptions): Promise<{
|
|
32
|
+
success: boolean;
|
|
33
|
+
packPath?: string;
|
|
34
|
+
result?: CrawlResult;
|
|
35
|
+
error?: string;
|
|
36
|
+
}>;
|
|
37
|
+
/**
|
|
38
|
+
* Quick crawl - generates pack without detailed analysis
|
|
39
|
+
*/
|
|
40
|
+
export declare function quickCrawl(baseUrl: string, outputPath?: string): Promise<{
|
|
41
|
+
success: boolean;
|
|
42
|
+
packPath?: string;
|
|
43
|
+
error?: string;
|
|
44
|
+
}>;
|