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
package/dist/commands/run.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* QA360 Playwright UI Adapter (
|
|
3
|
-
* UI
|
|
2
|
+
* QA360 Playwright UI Adapter (Extended)
|
|
3
|
+
* Complete UI E2E testing with all Playwright actions
|
|
4
4
|
*/
|
|
5
|
-
import { WebTarget, PackBudgets } from '../types/pack-v1.js';
|
|
5
|
+
import type { WebTarget, PackBudgets, UiTestDefinition, UiTestStep } from '../types/pack-v1.js';
|
|
6
6
|
import { AuthCredentials } from '../auth/index.js';
|
|
7
7
|
export interface UiTestConfig {
|
|
8
8
|
target: WebTarget;
|
|
@@ -17,6 +17,8 @@ export interface UiTestConfig {
|
|
|
17
17
|
passwordSelector?: string;
|
|
18
18
|
submitSelector?: string;
|
|
19
19
|
};
|
|
20
|
+
/** CLI override for headed mode */
|
|
21
|
+
cliHeaded?: boolean;
|
|
20
22
|
}
|
|
21
23
|
export interface UiTestResult {
|
|
22
24
|
page: string;
|
|
@@ -24,6 +26,7 @@ export interface UiTestResult {
|
|
|
24
26
|
loadTime: number;
|
|
25
27
|
error?: string;
|
|
26
28
|
screenshot?: string;
|
|
29
|
+
video?: string;
|
|
27
30
|
accessibility?: {
|
|
28
31
|
score: number;
|
|
29
32
|
violations: Array<{
|
|
@@ -44,9 +47,24 @@ export interface UiTestResult {
|
|
|
44
47
|
};
|
|
45
48
|
};
|
|
46
49
|
}
|
|
50
|
+
export interface UiTestStepResult {
|
|
51
|
+
step: UiTestStep;
|
|
52
|
+
success: boolean;
|
|
53
|
+
error?: string;
|
|
54
|
+
duration: number;
|
|
55
|
+
screenshot?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface UiE2eResult {
|
|
58
|
+
test: UiTestDefinition;
|
|
59
|
+
success: boolean;
|
|
60
|
+
steps: UiTestStepResult[];
|
|
61
|
+
duration: number;
|
|
62
|
+
error?: string;
|
|
63
|
+
}
|
|
47
64
|
export interface UiSmokeResult {
|
|
48
65
|
success: boolean;
|
|
49
66
|
results: UiTestResult[];
|
|
67
|
+
e2eResults?: UiE2eResult[];
|
|
50
68
|
summary: {
|
|
51
69
|
total: number;
|
|
52
70
|
passed: number;
|
|
@@ -62,6 +80,10 @@ export declare class PlaywrightUiAdapter {
|
|
|
62
80
|
private page?;
|
|
63
81
|
private redactor;
|
|
64
82
|
private auth?;
|
|
83
|
+
private assertions?;
|
|
84
|
+
private artifactDir;
|
|
85
|
+
private videoDir;
|
|
86
|
+
private traceDir;
|
|
65
87
|
constructor();
|
|
66
88
|
/**
|
|
67
89
|
* Set authentication credentials for requests
|
|
@@ -71,10 +93,30 @@ export declare class PlaywrightUiAdapter {
|
|
|
71
93
|
* Execute UI smoke tests with accessibility
|
|
72
94
|
*/
|
|
73
95
|
runSmokeTests(config: UiTestConfig): Promise<UiSmokeResult>;
|
|
96
|
+
/**
|
|
97
|
+
* Run a single E2E test
|
|
98
|
+
*/
|
|
99
|
+
runE2eTest(test: UiTestDefinition, config: UiTestConfig): Promise<UiE2eResult>;
|
|
100
|
+
/**
|
|
101
|
+
* Execute a single UI test step
|
|
102
|
+
*/
|
|
103
|
+
private executeStep;
|
|
104
|
+
/**
|
|
105
|
+
* Verify expected outcomes after a step
|
|
106
|
+
*/
|
|
107
|
+
private verifyExpected;
|
|
74
108
|
/**
|
|
75
109
|
* Test single page with accessibility
|
|
76
110
|
*/
|
|
77
111
|
private testPage;
|
|
112
|
+
/**
|
|
113
|
+
* Setup browser with all options
|
|
114
|
+
*/
|
|
115
|
+
private setupBrowser;
|
|
116
|
+
/**
|
|
117
|
+
* Determine if video should be recorded
|
|
118
|
+
*/
|
|
119
|
+
private shouldRecordVideo;
|
|
78
120
|
/**
|
|
79
121
|
* Perform login if configured
|
|
80
122
|
*/
|
|
@@ -103,10 +145,6 @@ export declare class PlaywrightUiAdapter {
|
|
|
103
145
|
* Escape XML special characters
|
|
104
146
|
*/
|
|
105
147
|
private escapeXml;
|
|
106
|
-
/**
|
|
107
|
-
* Setup browser context
|
|
108
|
-
*/
|
|
109
|
-
private setupBrowser;
|
|
110
148
|
/**
|
|
111
149
|
* Cleanup browser resources
|
|
112
150
|
*/
|
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* QA360 Playwright UI Adapter (
|
|
3
|
-
* UI
|
|
2
|
+
* QA360 Playwright UI Adapter (Extended)
|
|
3
|
+
* Complete UI E2E testing with all Playwright actions
|
|
4
4
|
*/
|
|
5
|
-
import { chromium } from '@playwright/test';
|
|
5
|
+
import { chromium, firefox, webkit } from '@playwright/test';
|
|
6
6
|
import { SecurityRedactor } from '../security/redactor.js';
|
|
7
|
+
import { createAssertionsEngine } from '../assertions/index.js';
|
|
7
8
|
export class PlaywrightUiAdapter {
|
|
8
9
|
browser;
|
|
9
10
|
context;
|
|
10
11
|
page;
|
|
11
12
|
redactor;
|
|
12
13
|
auth;
|
|
14
|
+
assertions;
|
|
15
|
+
// Storage for artifacts
|
|
16
|
+
artifactDir;
|
|
17
|
+
videoDir;
|
|
18
|
+
traceDir;
|
|
13
19
|
constructor() {
|
|
14
20
|
this.redactor = SecurityRedactor.forLogs();
|
|
21
|
+
this.artifactDir = '.qa360/artifacts/ui';
|
|
22
|
+
this.videoDir = `${this.artifactDir}/videos`;
|
|
23
|
+
this.traceDir = `${this.artifactDir}/traces`;
|
|
15
24
|
}
|
|
16
25
|
/**
|
|
17
26
|
* Set authentication credentials for requests
|
|
@@ -26,7 +35,7 @@ export class PlaywrightUiAdapter {
|
|
|
26
35
|
try {
|
|
27
36
|
// Store auth config
|
|
28
37
|
this.auth = config.auth;
|
|
29
|
-
await this.setupBrowser();
|
|
38
|
+
await this.setupBrowser(config);
|
|
30
39
|
const results = [];
|
|
31
40
|
const pages = config.target.pages || [config.target.baseUrl];
|
|
32
41
|
console.log(`🖥️ Running UI smoke tests (${pages.length} pages)`);
|
|
@@ -46,17 +55,237 @@ export class PlaywrightUiAdapter {
|
|
|
46
55
|
console.log(` ❌ ${pageUrl} -> ${testResult.error}`);
|
|
47
56
|
}
|
|
48
57
|
}
|
|
49
|
-
|
|
50
|
-
|
|
58
|
+
// Run E2E tests if defined
|
|
59
|
+
let e2eResults = [];
|
|
60
|
+
if (config.target.uiTests && config.target.uiTests.length > 0) {
|
|
61
|
+
console.log(`🧪 Running E2E tests (${config.target.uiTests.length} tests)`);
|
|
62
|
+
for (const test of config.target.uiTests) {
|
|
63
|
+
if (test.enabled !== false) {
|
|
64
|
+
const result = await this.runE2eTest(test, config);
|
|
65
|
+
e2eResults.push(result);
|
|
66
|
+
const status = result.success ? '✅' : '❌';
|
|
67
|
+
console.log(` ${status} ${test.name} (${result.duration}ms)`);
|
|
68
|
+
if (!result.success) {
|
|
69
|
+
console.log(` Error: ${result.error}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const summary = this.calculateSummary(results, e2eResults);
|
|
75
|
+
const junit = this.generateJUnit(results, e2eResults);
|
|
51
76
|
return {
|
|
52
77
|
success: summary.failed === 0,
|
|
53
78
|
results,
|
|
79
|
+
e2eResults,
|
|
54
80
|
summary,
|
|
55
81
|
junit
|
|
56
82
|
};
|
|
57
83
|
}
|
|
58
84
|
finally {
|
|
59
|
-
await this.cleanup();
|
|
85
|
+
await this.cleanup(config);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Run a single E2E test
|
|
90
|
+
*/
|
|
91
|
+
async runE2eTest(test, config) {
|
|
92
|
+
const startTime = Date.now();
|
|
93
|
+
const steps = [];
|
|
94
|
+
try {
|
|
95
|
+
// Determine starting URL
|
|
96
|
+
const startUrl = test.url || `${config.target.baseUrl.replace(/\/$/, '')}${test.path || ''}`;
|
|
97
|
+
// Navigate to start URL
|
|
98
|
+
console.log(` 📍 Navigate to: ${startUrl}`);
|
|
99
|
+
await this.page.goto(startUrl, { timeout: test.timeout || config.timeout || 30000 });
|
|
100
|
+
// Initialize assertions engine
|
|
101
|
+
this.assertions = createAssertionsEngine(this.page);
|
|
102
|
+
// Execute each step
|
|
103
|
+
for (const step of test.steps) {
|
|
104
|
+
const stepResult = await this.executeStep(step);
|
|
105
|
+
steps.push(stepResult);
|
|
106
|
+
if (!stepResult.success) {
|
|
107
|
+
return {
|
|
108
|
+
test,
|
|
109
|
+
success: false,
|
|
110
|
+
steps,
|
|
111
|
+
duration: Date.now() - startTime,
|
|
112
|
+
error: stepResult.error,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
test,
|
|
118
|
+
success: true,
|
|
119
|
+
steps,
|
|
120
|
+
duration: Date.now() - startTime,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
return {
|
|
125
|
+
test,
|
|
126
|
+
success: false,
|
|
127
|
+
steps,
|
|
128
|
+
duration: Date.now() - startTime,
|
|
129
|
+
error: this.redactor.redact(error instanceof Error ? error.message : 'Unknown error'),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Execute a single UI test step
|
|
135
|
+
*/
|
|
136
|
+
async executeStep(step) {
|
|
137
|
+
const startTime = Date.now();
|
|
138
|
+
let screenshot;
|
|
139
|
+
try {
|
|
140
|
+
const { action, selector, value, options = {}, expected } = step;
|
|
141
|
+
const opts = { timeout: 5000, ...options };
|
|
142
|
+
switch (action) {
|
|
143
|
+
case 'navigate':
|
|
144
|
+
await this.page.goto(value, opts);
|
|
145
|
+
break;
|
|
146
|
+
case 'click':
|
|
147
|
+
await this.page.click(selector, opts);
|
|
148
|
+
break;
|
|
149
|
+
case 'dblClick':
|
|
150
|
+
await this.page.dblclick(selector, opts);
|
|
151
|
+
break;
|
|
152
|
+
case 'rightClick':
|
|
153
|
+
await this.page.click(selector, { ...opts, button: 'right' });
|
|
154
|
+
break;
|
|
155
|
+
case 'hover':
|
|
156
|
+
await this.page.hover(selector, opts);
|
|
157
|
+
break;
|
|
158
|
+
case 'focus':
|
|
159
|
+
await this.page.focus(selector, opts);
|
|
160
|
+
break;
|
|
161
|
+
case 'fill':
|
|
162
|
+
await this.page.fill(selector, value, opts);
|
|
163
|
+
break;
|
|
164
|
+
case 'type':
|
|
165
|
+
await this.page.type(selector, value, opts);
|
|
166
|
+
break;
|
|
167
|
+
case 'clear':
|
|
168
|
+
await this.page.fill(selector, '', opts);
|
|
169
|
+
break;
|
|
170
|
+
case 'select':
|
|
171
|
+
if (value !== undefined) {
|
|
172
|
+
await this.page.selectOption(selector, value, opts);
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
case 'check':
|
|
176
|
+
await this.page.check(selector, opts);
|
|
177
|
+
break;
|
|
178
|
+
case 'uncheck':
|
|
179
|
+
await this.page.uncheck(selector, opts);
|
|
180
|
+
break;
|
|
181
|
+
case 'upload':
|
|
182
|
+
if (value !== undefined) {
|
|
183
|
+
await this.page.setInputFiles(selector, value, opts);
|
|
184
|
+
}
|
|
185
|
+
break;
|
|
186
|
+
case 'press': {
|
|
187
|
+
const delay = options?.delay;
|
|
188
|
+
const pressOpts = delay !== undefined ? { delay } : {};
|
|
189
|
+
await this.page.keyboard.press(value, pressOpts);
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
case 'waitFor':
|
|
193
|
+
case 'waitForSelector':
|
|
194
|
+
await this.page.waitForSelector(selector, opts);
|
|
195
|
+
break;
|
|
196
|
+
case 'waitForNavigation':
|
|
197
|
+
await this.page.waitForNavigation(opts);
|
|
198
|
+
break;
|
|
199
|
+
case 'waitForTimeout':
|
|
200
|
+
await this.page.waitForTimeout(parseInt(value, 10));
|
|
201
|
+
break;
|
|
202
|
+
case 'scroll':
|
|
203
|
+
if (selector) {
|
|
204
|
+
await this.page.evaluate((sel) => {
|
|
205
|
+
const el = document.querySelector(sel);
|
|
206
|
+
if (el)
|
|
207
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
208
|
+
}, selector);
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
case 'dragAndDrop':
|
|
212
|
+
await this.page.dragAndDrop(selector, value, opts);
|
|
213
|
+
break;
|
|
214
|
+
case 'tap':
|
|
215
|
+
await this.page.tap(selector, opts);
|
|
216
|
+
break;
|
|
217
|
+
default:
|
|
218
|
+
throw new Error(`Unknown action: ${action}`);
|
|
219
|
+
}
|
|
220
|
+
// Add wait after action if specified
|
|
221
|
+
if (step.wait) {
|
|
222
|
+
await this.page.waitForTimeout(step.wait);
|
|
223
|
+
}
|
|
224
|
+
// Take screenshot on failure if configured (will be checked in catch block)
|
|
225
|
+
// or take screenshot if requested
|
|
226
|
+
// Verify expected outcomes
|
|
227
|
+
if (expected) {
|
|
228
|
+
await this.verifyExpected(expected);
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
step,
|
|
232
|
+
success: true,
|
|
233
|
+
duration: Date.now() - startTime,
|
|
234
|
+
screenshot,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
// Take screenshot on failure
|
|
239
|
+
try {
|
|
240
|
+
const buffer = await this.page.screenshot({ type: 'png' });
|
|
241
|
+
screenshot = `data:image/png;base64,${buffer.toString('base64')}`;
|
|
242
|
+
}
|
|
243
|
+
catch { }
|
|
244
|
+
return {
|
|
245
|
+
step,
|
|
246
|
+
success: false,
|
|
247
|
+
duration: Date.now() - startTime,
|
|
248
|
+
error: this.redactor.redact(error instanceof Error ? error.message : 'Unknown error'),
|
|
249
|
+
screenshot,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Verify expected outcomes after a step
|
|
255
|
+
*/
|
|
256
|
+
async verifyExpected(expected) {
|
|
257
|
+
if (!expected)
|
|
258
|
+
return;
|
|
259
|
+
if (expected.url !== undefined) {
|
|
260
|
+
const currentUrl = this.page.url();
|
|
261
|
+
if (currentUrl !== expected.url) {
|
|
262
|
+
throw new Error(`URL mismatch: expected "${expected.url}", got "${currentUrl}"`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (expected.urlContains !== undefined) {
|
|
266
|
+
const currentUrl = this.page.url();
|
|
267
|
+
if (!currentUrl.includes(expected.urlContains)) {
|
|
268
|
+
throw new Error(`URL does not contain "${expected.urlContains}": "${currentUrl}"`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (expected.visible !== undefined) {
|
|
272
|
+
const element = this.page.locator(expected.visible);
|
|
273
|
+
if (!(await element.isVisible())) {
|
|
274
|
+
throw new Error(`Expected element to be visible: ${expected.visible}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (expected.hidden !== undefined) {
|
|
278
|
+
const element = this.page.locator(expected.hidden);
|
|
279
|
+
if (!(await element.isHidden())) {
|
|
280
|
+
throw new Error(`Expected element to be hidden: ${expected.hidden}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (expected.elementText) {
|
|
284
|
+
const element = this.page.locator(expected.elementText.selector);
|
|
285
|
+
const text = await element.textContent();
|
|
286
|
+
if (text !== expected.elementText.text) {
|
|
287
|
+
throw new Error(`Text mismatch for ${expected.elementText.selector}: expected "${expected.elementText.text}", got "${text}"`);
|
|
288
|
+
}
|
|
60
289
|
}
|
|
61
290
|
}
|
|
62
291
|
/**
|
|
@@ -79,8 +308,8 @@ export class PlaywrightUiAdapter {
|
|
|
79
308
|
error: `HTTP ${response?.status() || 'unknown'}: Failed to load page`
|
|
80
309
|
};
|
|
81
310
|
}
|
|
82
|
-
// Take screenshot
|
|
83
|
-
const screenshot = await this.takeScreenshot(pageUrl);
|
|
311
|
+
// Take screenshot based on config
|
|
312
|
+
const screenshot = await this.takeScreenshot(pageUrl, config.target.screenshot);
|
|
84
313
|
// Get DOM snapshot
|
|
85
314
|
const domSnapshot = await this.getDomSnapshot();
|
|
86
315
|
// Run accessibility tests
|
|
@@ -108,6 +337,68 @@ export class PlaywrightUiAdapter {
|
|
|
108
337
|
};
|
|
109
338
|
}
|
|
110
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* Setup browser with all options
|
|
342
|
+
*/
|
|
343
|
+
async setupBrowser(config) {
|
|
344
|
+
// Determine browser type
|
|
345
|
+
const browserType = config.target.browser || 'chromium';
|
|
346
|
+
const browserTypeObj = browserType === 'firefox' ? firefox : browserType === 'webkit' ? webkit : chromium;
|
|
347
|
+
// Determine headed mode (CLI override > target config > default headless)
|
|
348
|
+
const headless = config.cliHeaded ?? config.target.headless ?? true;
|
|
349
|
+
// Launch browser
|
|
350
|
+
this.browser = await browserTypeObj.launch({
|
|
351
|
+
headless,
|
|
352
|
+
args: ['--no-sandbox', '--disable-dev-shm-usage'],
|
|
353
|
+
slowMo: config.target.slowMo || 0,
|
|
354
|
+
});
|
|
355
|
+
// Build extra HTTP headers with auth
|
|
356
|
+
const extraHTTPHeaders = {
|
|
357
|
+
'User-Agent': 'QA360-UI-Test/1.0'
|
|
358
|
+
};
|
|
359
|
+
if (this.auth?.headers) {
|
|
360
|
+
Object.assign(extraHTTPHeaders, this.auth.headers);
|
|
361
|
+
}
|
|
362
|
+
// Setup viewport based on device or explicit config
|
|
363
|
+
let viewport = { width: 1280, height: 720 };
|
|
364
|
+
if (config.target.device === 'mobile') {
|
|
365
|
+
viewport = { width: 375, height: 667 };
|
|
366
|
+
}
|
|
367
|
+
else if (config.target.device === 'tablet') {
|
|
368
|
+
viewport = { width: 768, height: 1024 };
|
|
369
|
+
}
|
|
370
|
+
else if (config.target.viewport) {
|
|
371
|
+
viewport = config.target.viewport;
|
|
372
|
+
}
|
|
373
|
+
// Create context with video recording if enabled
|
|
374
|
+
const recordVideo = this.shouldRecordVideo(config.target.video)
|
|
375
|
+
? { dir: this.videoDir, size: viewport }
|
|
376
|
+
: undefined;
|
|
377
|
+
this.context = await this.browser.newContext({
|
|
378
|
+
viewport,
|
|
379
|
+
userAgent: 'QA360-UI-Test/1.0',
|
|
380
|
+
extraHTTPHeaders,
|
|
381
|
+
recordVideo,
|
|
382
|
+
});
|
|
383
|
+
// Add cookies from auth credentials after context creation
|
|
384
|
+
if (this.auth?.cookies && this.auth.cookies.length > 0) {
|
|
385
|
+
await this.context.addCookies(this.auth.cookies.map(c => ({
|
|
386
|
+
name: c.name,
|
|
387
|
+
value: c.value,
|
|
388
|
+
domain: c.domain || '',
|
|
389
|
+
path: c.path || '/',
|
|
390
|
+
httpOnly: c.httpOnly || false,
|
|
391
|
+
secure: c.secure || false
|
|
392
|
+
})));
|
|
393
|
+
}
|
|
394
|
+
this.page = await this.context.newPage();
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Determine if video should be recorded
|
|
398
|
+
*/
|
|
399
|
+
shouldRecordVideo(mode) {
|
|
400
|
+
return mode === 'always' || mode === 'retain-on-fail';
|
|
401
|
+
}
|
|
111
402
|
/**
|
|
112
403
|
* Perform login if configured
|
|
113
404
|
*/
|
|
@@ -153,7 +444,7 @@ export class PlaywrightUiAdapter {
|
|
|
153
444
|
// @ts-ignore
|
|
154
445
|
axe.run((err, results) => {
|
|
155
446
|
if (err) {
|
|
156
|
-
resolve({ violations: [], passes: [], incomplete: []
|
|
447
|
+
resolve({ violations: [], passes: [], incomplete: [] });
|
|
157
448
|
}
|
|
158
449
|
else {
|
|
159
450
|
resolve(results);
|
|
@@ -161,7 +452,7 @@ export class PlaywrightUiAdapter {
|
|
|
161
452
|
});
|
|
162
453
|
}
|
|
163
454
|
else {
|
|
164
|
-
resolve({ violations: [], passes: [], incomplete: []
|
|
455
|
+
resolve({ violations: [], passes: [], incomplete: [] });
|
|
165
456
|
}
|
|
166
457
|
});
|
|
167
458
|
});
|
|
@@ -229,7 +520,10 @@ export class PlaywrightUiAdapter {
|
|
|
229
520
|
/**
|
|
230
521
|
* Take screenshot for debugging
|
|
231
522
|
*/
|
|
232
|
-
async takeScreenshot(pageUrl) {
|
|
523
|
+
async takeScreenshot(pageUrl, mode) {
|
|
524
|
+
const shouldTake = mode === 'always' || mode === 'only-on-fail';
|
|
525
|
+
if (!shouldTake)
|
|
526
|
+
return '';
|
|
233
527
|
try {
|
|
234
528
|
const screenshot = await this.page.screenshot({
|
|
235
529
|
type: 'png',
|
|
@@ -245,41 +539,77 @@ export class PlaywrightUiAdapter {
|
|
|
245
539
|
/**
|
|
246
540
|
* Calculate test summary
|
|
247
541
|
*/
|
|
248
|
-
calculateSummary(results) {
|
|
249
|
-
const
|
|
250
|
-
const
|
|
542
|
+
calculateSummary(results, e2eResults = []) {
|
|
543
|
+
const smokeTests = results.length;
|
|
544
|
+
const e2eTests = e2eResults.length;
|
|
545
|
+
const total = smokeTests + e2eTests;
|
|
546
|
+
const smokePassed = results.filter(r => r.success).length;
|
|
547
|
+
const e2ePassed = e2eResults.filter(r => r.success).length;
|
|
548
|
+
const passed = smokePassed + e2ePassed;
|
|
251
549
|
const failed = total - passed;
|
|
252
|
-
const avgLoadTime =
|
|
253
|
-
Math.round(results.reduce((sum, r) => sum + r.loadTime, 0) /
|
|
550
|
+
const avgLoadTime = smokeTests > 0 ?
|
|
551
|
+
Math.round(results.reduce((sum, r) => sum + r.loadTime, 0) / smokeTests) : 0;
|
|
254
552
|
const a11yScores = results
|
|
255
553
|
.map(r => r.accessibility?.score)
|
|
256
554
|
.filter((score) => typeof score === 'number');
|
|
257
555
|
const avgA11yScore = a11yScores.length > 0 ?
|
|
258
556
|
Math.round(a11yScores.reduce((sum, score) => sum + score, 0) / a11yScores.length) : 0;
|
|
259
|
-
return {
|
|
557
|
+
return {
|
|
558
|
+
total,
|
|
559
|
+
passed,
|
|
560
|
+
failed,
|
|
561
|
+
avgLoadTime,
|
|
562
|
+
avgA11yScore
|
|
563
|
+
};
|
|
260
564
|
}
|
|
261
565
|
/**
|
|
262
566
|
* Generate JUnit XML fragment
|
|
263
567
|
*/
|
|
264
|
-
generateJUnit(results) {
|
|
265
|
-
const summary = this.calculateSummary(results);
|
|
568
|
+
generateJUnit(results, e2eResults = []) {
|
|
569
|
+
const summary = this.calculateSummary(results, e2eResults);
|
|
266
570
|
const timestamp = new Date().toISOString();
|
|
267
571
|
let junit = `<?xml version="1.0" encoding="UTF-8"?>
|
|
268
|
-
<
|
|
572
|
+
<testsuites>
|
|
573
|
+
<testsuite name="UI Smoke Tests" tests="${results.length}" failures="${results.filter(r => !r.success).length}" time="${summary.avgLoadTime / 1000}" timestamp="${timestamp}">
|
|
269
574
|
`;
|
|
270
575
|
for (const result of results) {
|
|
271
576
|
const testName = `UI Test: ${result.page}`;
|
|
272
577
|
const time = result.loadTime / 1000;
|
|
273
|
-
junit += `
|
|
578
|
+
junit += ` <testcase name="${this.escapeXml(testName)}" time="${time}">
|
|
274
579
|
`;
|
|
275
580
|
if (!result.success) {
|
|
276
|
-
junit += `
|
|
581
|
+
junit += ` <failure message="${this.escapeXml(result.error || 'Test failed')}">${this.escapeXml(JSON.stringify(result, null, 2))}</failure>
|
|
277
582
|
`;
|
|
278
583
|
}
|
|
279
|
-
junit += `
|
|
584
|
+
junit += ` </testcase>
|
|
280
585
|
`;
|
|
281
586
|
}
|
|
282
|
-
junit +=
|
|
587
|
+
junit += ` </testsuite>
|
|
588
|
+
`;
|
|
589
|
+
// Add E2E test suite
|
|
590
|
+
if (e2eResults.length > 0) {
|
|
591
|
+
const e2eFailed = e2eResults.filter(r => !r.success).length;
|
|
592
|
+
const e2eDuration = e2eResults.reduce((sum, r) => sum + r.duration, 0) / 1000;
|
|
593
|
+
junit += ` <testsuite name="E2E Tests" tests="${e2eResults.length}" failures="${e2eFailed}" time="${e2eDuration}" timestamp="${timestamp}">
|
|
594
|
+
`;
|
|
595
|
+
for (const result of e2eResults) {
|
|
596
|
+
const testName = result.test.name;
|
|
597
|
+
const time = result.duration / 1000;
|
|
598
|
+
junit += ` <testcase name="${this.escapeXml(testName)}" time="${time}">
|
|
599
|
+
`;
|
|
600
|
+
if (!result.success) {
|
|
601
|
+
const failedSteps = result.steps.filter(s => !s.success);
|
|
602
|
+
const failureDetails = failedSteps.map(s => `${s.step.action}: ${s.error}`).join('; ');
|
|
603
|
+
junit += ` <failure message="${this.escapeXml(result.error || 'Test failed')}">${this.escapeXml(failureDetails)}</failure>
|
|
604
|
+
`;
|
|
605
|
+
}
|
|
606
|
+
junit += ` </testcase>
|
|
607
|
+
`;
|
|
608
|
+
}
|
|
609
|
+
junit += ` </testsuite>
|
|
610
|
+
`;
|
|
611
|
+
}
|
|
612
|
+
junit += `</testsuites>`;
|
|
283
613
|
return junit;
|
|
284
614
|
}
|
|
285
615
|
/**
|
|
@@ -293,43 +623,19 @@ export class PlaywrightUiAdapter {
|
|
|
293
623
|
.replace(/"/g, '"')
|
|
294
624
|
.replace(/'/g, ''');
|
|
295
625
|
}
|
|
296
|
-
/**
|
|
297
|
-
* Setup browser context
|
|
298
|
-
*/
|
|
299
|
-
async setupBrowser() {
|
|
300
|
-
this.browser = await chromium.launch({
|
|
301
|
-
headless: true,
|
|
302
|
-
args: ['--no-sandbox', '--disable-dev-shm-usage']
|
|
303
|
-
});
|
|
304
|
-
// Build extra HTTP headers with auth
|
|
305
|
-
const extraHTTPHeaders = {
|
|
306
|
-
'User-Agent': 'QA360-UI-Smoke/1.0'
|
|
307
|
-
};
|
|
308
|
-
if (this.auth?.headers) {
|
|
309
|
-
Object.assign(extraHTTPHeaders, this.auth.headers);
|
|
310
|
-
}
|
|
311
|
-
this.context = await this.browser.newContext({
|
|
312
|
-
viewport: { width: 1280, height: 720 },
|
|
313
|
-
userAgent: 'QA360-UI-Smoke/1.0',
|
|
314
|
-
extraHTTPHeaders
|
|
315
|
-
});
|
|
316
|
-
// Add cookies from auth credentials after context creation
|
|
317
|
-
if (this.auth?.cookies && this.auth.cookies.length > 0) {
|
|
318
|
-
await this.context.addCookies(this.auth.cookies.map(c => ({
|
|
319
|
-
name: c.name,
|
|
320
|
-
value: c.value,
|
|
321
|
-
domain: c.domain || '',
|
|
322
|
-
path: c.path || '/',
|
|
323
|
-
httpOnly: c.httpOnly || false,
|
|
324
|
-
secure: c.secure || false
|
|
325
|
-
})));
|
|
326
|
-
}
|
|
327
|
-
this.page = await this.context.newPage();
|
|
328
|
-
}
|
|
329
626
|
/**
|
|
330
627
|
* Cleanup browser resources
|
|
331
628
|
*/
|
|
332
|
-
async cleanup() {
|
|
629
|
+
async cleanup(config) {
|
|
630
|
+
// Save video/trace artifacts if configured
|
|
631
|
+
if (this.page && (config.target.video === 'retain-on-fail' || config.target.trace === 'retain-on-fail')) {
|
|
632
|
+
// Check if tests failed and retain artifacts
|
|
633
|
+
const hasFailures = false; // Would need to track this
|
|
634
|
+
if (!hasFailures) {
|
|
635
|
+
// Clean up artifacts if all tests passed
|
|
636
|
+
// TODO: Implement artifact cleanup
|
|
637
|
+
}
|
|
638
|
+
}
|
|
333
639
|
if (this.page) {
|
|
334
640
|
await this.page.close();
|
|
335
641
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QA360 Assertions Engine
|
|
3
|
+
*
|
|
4
|
+
* Executes assertions against Playwright Page objects
|
|
5
|
+
*/
|
|
6
|
+
import type { Assertion, AssertionResult, AssertionRunOptions, AssertionGroupResult } from './types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Assertions Engine class
|
|
9
|
+
*/
|
|
10
|
+
export declare class AssertionsEngine {
|
|
11
|
+
private page;
|
|
12
|
+
private defaultTimeout;
|
|
13
|
+
constructor(page: any, defaultTimeout?: number);
|
|
14
|
+
/**
|
|
15
|
+
* Run a single assertion
|
|
16
|
+
*/
|
|
17
|
+
runAssertion(assertion: Assertion): Promise<AssertionResult>;
|
|
18
|
+
/**
|
|
19
|
+
* Run multiple assertions
|
|
20
|
+
*/
|
|
21
|
+
runAssertions(assertions: Assertion[], options?: Partial<AssertionRunOptions>): Promise<AssertionGroupResult>;
|
|
22
|
+
private isVisible;
|
|
23
|
+
private waitForVisible;
|
|
24
|
+
private isHidden;
|
|
25
|
+
private waitForHidden;
|
|
26
|
+
private isAttached;
|
|
27
|
+
private getTextContent;
|
|
28
|
+
private getInputValue;
|
|
29
|
+
private getAttribute;
|
|
30
|
+
private hasAttribute;
|
|
31
|
+
private getClasses;
|
|
32
|
+
private getTagName;
|
|
33
|
+
private count;
|
|
34
|
+
private isEnabled;
|
|
35
|
+
private isChecked;
|
|
36
|
+
private isFocused;
|
|
37
|
+
private isReadOnly;
|
|
38
|
+
private isSelected;
|
|
39
|
+
private getCssProperty;
|
|
40
|
+
private isInViewport;
|
|
41
|
+
private getBoundingBox;
|
|
42
|
+
private compare;
|
|
43
|
+
private contains;
|
|
44
|
+
private matches;
|
|
45
|
+
private formatError;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Create an assertions engine
|
|
49
|
+
*/
|
|
50
|
+
export declare function createAssertionsEngine(page: any, timeout?: number): AssertionsEngine;
|
|
51
|
+
export * from './types.js';
|