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
@@ -15,6 +15,7 @@ export interface RunOptions {
15
15
  verbose?: boolean;
16
16
  dryRun?: boolean;
17
17
  strict?: boolean;
18
+ headed?: boolean;
18
19
  }
19
20
  /**
20
21
  * Unified pack type - can be v1 or v2, Phase3Runner handles both
@@ -1,8 +1,8 @@
1
1
  /**
2
- * QA360 Playwright UI Adapter (Socle OOTB)
3
- * UI smoke tests + accessibility via axe-core
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 (Socle OOTB)
3
- * UI smoke tests + accessibility via axe-core
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
- const summary = this.calculateSummary(results);
50
- const junit = this.generateJUnit(results);
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: [], inapplicable: [] });
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: [], inapplicable: [] });
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 total = results.length;
250
- const passed = results.filter(r => r.success).length;
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 = total > 0 ?
253
- Math.round(results.reduce((sum, r) => sum + r.loadTime, 0) / total) : 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 { total, passed, failed, avgLoadTime, avgA11yScore };
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
- <testsuite name="UI Smoke Tests" tests="${summary.total}" failures="${summary.failed}" time="${summary.avgLoadTime / 1000}" timestamp="${timestamp}">
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 += ` <testcase name="${this.escapeXml(testName)}" time="${time}">
578
+ junit += ` <testcase name="${this.escapeXml(testName)}" time="${time}">
274
579
  `;
275
580
  if (!result.success) {
276
- junit += ` <failure message="${this.escapeXml(result.error || 'Test failed')}">${this.escapeXml(JSON.stringify(result, null, 2))}</failure>
581
+ junit += ` <failure message="${this.escapeXml(result.error || 'Test failed')}">${this.escapeXml(JSON.stringify(result, null, 2))}</failure>
277
582
  `;
278
583
  }
279
- junit += ` </testcase>
584
+ junit += ` </testcase>
280
585
  `;
281
586
  }
282
- junit += `</testsuite>`;
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, '&quot;')
294
624
  .replace(/'/g, '&apos;');
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';