qa360 2.0.13 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,21 @@
1
1
  /**
2
2
  * QA360 Playwright UI Adapter (Extended)
3
3
  * Complete UI E2E testing with all Playwright actions
4
+ *
5
+ * Playwright++ Features:
6
+ * - Video recording (always/retain-on-fail/never)
7
+ * - Automatic screenshots (before/after steps, on error)
8
+ * - Trace capture for debugging
9
+ * - Artifacts management with CAS
10
+ * - HTML report generation
4
11
  */
5
12
  import { chromium, firefox, webkit } from '@playwright/test';
6
13
  import { SecurityRedactor } from '../security/redactor.js';
7
14
  import { createAssertionsEngine } from '../assertions/index.js';
15
+ import { UIArtifactsManager } from '../artifacts/index.js';
16
+ import { HTMLReporter } from '../reporting/index.js';
17
+ import { mkdirSync, existsSync } from 'fs';
18
+ import { join } from 'path';
8
19
  export class PlaywrightUiAdapter {
9
20
  browser;
10
21
  context;
@@ -16,6 +27,13 @@ export class PlaywrightUiAdapter {
16
27
  artifactDir;
17
28
  videoDir;
18
29
  traceDir;
30
+ // Playwright++: Artifacts manager
31
+ artifactsManager;
32
+ failureCount = 0;
33
+ currentTestId;
34
+ allScreenshots = [];
35
+ allVideos = [];
36
+ allTraces = [];
19
37
  constructor() {
20
38
  this.redactor = SecurityRedactor.forLogs();
21
39
  this.artifactDir = '.qa360/artifacts/ui';
@@ -30,11 +48,31 @@ export class PlaywrightUiAdapter {
30
48
  }
31
49
  /**
32
50
  * Execute UI smoke tests with accessibility
51
+ * Playwright++: Supports artifacts, screenshots, video, trace, HTML reporting
33
52
  */
34
53
  async runSmokeTests(config) {
54
+ const startTime = Date.now();
55
+ const outputDir = config.artifacts?.outputDir || this.artifactDir;
56
+ // Ensure output directories exist
57
+ for (const dir of [outputDir, this.videoDir, this.traceDir]) {
58
+ if (!existsSync(dir)) {
59
+ mkdirSync(dir, { recursive: true });
60
+ }
61
+ }
62
+ // Initialize artifacts manager if Playwright++ features enabled
63
+ const artifactsEnabled = config.artifacts?.screenshots !== 'never' ||
64
+ config.artifacts?.video !== 'never' ||
65
+ config.artifacts?.trace !== 'never';
66
+ if (artifactsEnabled) {
67
+ this.artifactsManager = new UIArtifactsManager(outputDir, '.qa360/runs/cas');
68
+ }
35
69
  try {
36
70
  // Store auth config
37
71
  this.auth = config.auth;
72
+ this.failureCount = 0;
73
+ this.allScreenshots = [];
74
+ this.allVideos = [];
75
+ this.allTraces = [];
38
76
  await this.setupBrowser(config);
39
77
  const results = [];
40
78
  const pages = config.target.pages || [config.target.baseUrl];
@@ -44,6 +82,9 @@ export class PlaywrightUiAdapter {
44
82
  await this.performLogin(config.login);
45
83
  }
46
84
  for (const pageUrl of pages) {
85
+ // Start artifacts for this test
86
+ this.currentTestId = `smoke-${this.failureCount}`;
87
+ this.artifactsManager?.startTest(this.currentTestId);
47
88
  const testResult = await this.testPage(pageUrl, config);
48
89
  results.push(testResult);
49
90
  if (testResult.success) {
@@ -52,8 +93,15 @@ export class PlaywrightUiAdapter {
52
93
  console.log(` ✅ ${pageUrl} -> ${testResult.loadTime}ms${a11yInfo}`);
53
94
  }
54
95
  else {
96
+ this.failureCount++;
55
97
  console.log(` ❌ ${pageUrl} -> ${testResult.error}`);
98
+ // Check bail condition
99
+ if (config.bail && this.failureCount >= config.bail) {
100
+ console.log(` 🛑 Bailing after ${this.failureCount} failures`);
101
+ break;
102
+ }
56
103
  }
104
+ this.artifactsManager?.endTest();
57
105
  }
58
106
  // Run E2E tests if defined
59
107
  let e2eResults = [];
@@ -61,18 +109,33 @@ export class PlaywrightUiAdapter {
61
109
  console.log(`🧪 Running E2E tests (${config.target.uiTests.length} tests)`);
62
110
  for (const test of config.target.uiTests) {
63
111
  if (test.enabled !== false) {
112
+ // Start artifacts for this test
113
+ this.currentTestId = `e2e-${test.name}`;
114
+ this.artifactsManager?.startTest(this.currentTestId);
64
115
  const result = await this.runE2eTest(test, config);
65
116
  e2eResults.push(result);
66
117
  const status = result.success ? '✅' : '❌';
67
118
  console.log(` ${status} ${test.name} (${result.duration}ms)`);
68
119
  if (!result.success) {
69
120
  console.log(` Error: ${result.error}`);
121
+ this.failureCount++;
122
+ // Check bail condition
123
+ if (config.bail && this.failureCount >= config.bail) {
124
+ console.log(` 🛑 Bailing after ${this.failureCount} failures`);
125
+ this.artifactsManager?.endTest();
126
+ break;
127
+ }
70
128
  }
129
+ this.artifactsManager?.endTest();
71
130
  }
72
131
  }
73
132
  }
74
133
  const summary = this.calculateSummary(results, e2eResults);
75
134
  const junit = this.generateJUnit(results, e2eResults);
135
+ // Playwright++: Generate HTML report if requested
136
+ if (config.htmlReport) {
137
+ await this.generateHtmlReport(config, results, e2eResults, summary);
138
+ }
76
139
  return {
77
140
  success: summary.failed === 0,
78
141
  results,
@@ -87,23 +150,44 @@ export class PlaywrightUiAdapter {
87
150
  }
88
151
  /**
89
152
  * Run a single E2E test
153
+ * Playwright++: Takes before/after screenshots, captures artifacts on failure
90
154
  */
91
155
  async runE2eTest(test, config) {
92
156
  const startTime = Date.now();
93
157
  const steps = [];
158
+ const screenshotMode = config.artifacts?.screenshots || 'never';
94
159
  try {
95
160
  // Determine starting URL
96
161
  const startUrl = test.url || `${config.target.baseUrl.replace(/\/$/, '')}${test.path || ''}`;
97
162
  // Navigate to start URL
98
163
  console.log(` 📍 Navigate to: ${startUrl}`);
99
164
  await this.page.goto(startUrl, { timeout: test.timeout || config.timeout || 30000 });
165
+ // Take initial screenshot
166
+ if (screenshotMode === 'always') {
167
+ await this.artifactsManager?.takeScreenshot(this.page, {}, {
168
+ testId: this.currentTestId || 'unknown',
169
+ type: 'screenshot',
170
+ tags: ['initial'],
171
+ });
172
+ }
100
173
  // Initialize assertions engine
101
174
  this.assertions = createAssertionsEngine(this.page);
102
175
  // Execute each step
103
- for (const step of test.steps) {
104
- const stepResult = await this.executeStep(step);
176
+ for (let i = 0; i < test.steps.length; i++) {
177
+ const step = test.steps[i];
178
+ // Take before screenshot if configured
179
+ if (screenshotMode === 'always') {
180
+ await this.artifactsManager?.takeBeforeScreenshot(this.page, step.action || 'step', i);
181
+ }
182
+ const stepResult = await this.executeStep(step, config);
105
183
  steps.push(stepResult);
184
+ // Take after screenshot (always on failure, or always if configured)
185
+ if (screenshotMode === 'always' || (screenshotMode === 'only-on-failure' && !stepResult.success)) {
186
+ await this.artifactsManager?.takeAfterScreenshot(this.page, step.action || 'step', i, stepResult.success);
187
+ }
106
188
  if (!stepResult.success) {
189
+ // Take error screenshot
190
+ await this.artifactsManager?.takeErrorScreenshot(this.page, new Error(stepResult.error || 'Step failed'), step.action);
107
191
  return {
108
192
  test,
109
193
  success: false,
@@ -121,6 +205,8 @@ export class PlaywrightUiAdapter {
121
205
  };
122
206
  }
123
207
  catch (error) {
208
+ // Take error screenshot
209
+ await this.artifactsManager?.takeErrorScreenshot(this.page, error);
124
210
  return {
125
211
  test,
126
212
  success: false,
@@ -132,8 +218,9 @@ export class PlaywrightUiAdapter {
132
218
  }
133
219
  /**
134
220
  * Execute a single UI test step
221
+ * Playwright++: Enhanced error handling with artifacts
135
222
  */
136
- async executeStep(step) {
223
+ async executeStep(step, config) {
137
224
  const startTime = Date.now();
138
225
  let screenshot;
139
226
  try {
@@ -339,6 +426,7 @@ export class PlaywrightUiAdapter {
339
426
  }
340
427
  /**
341
428
  * Setup browser with all options
429
+ * Playwright++: Enhanced video/trace recording support
342
430
  */
343
431
  async setupBrowser(config) {
344
432
  // Determine browser type
@@ -370,8 +458,11 @@ export class PlaywrightUiAdapter {
370
458
  else if (config.target.viewport) {
371
459
  viewport = config.target.viewport;
372
460
  }
461
+ // Playwright++: Determine video recording mode
462
+ const videoMode = config.artifacts?.video ?? config.target.video;
463
+ const shouldRecordVideo = videoMode === 'always' || videoMode === 'retain-on-failure';
373
464
  // Create context with video recording if enabled
374
- const recordVideo = this.shouldRecordVideo(config.target.video)
465
+ const recordVideo = shouldRecordVideo
375
466
  ? { dir: this.videoDir, size: viewport }
376
467
  : undefined;
377
468
  this.context = await this.browser.newContext({
@@ -380,6 +471,13 @@ export class PlaywrightUiAdapter {
380
471
  extraHTTPHeaders,
381
472
  recordVideo,
382
473
  });
474
+ // Playwright++: Start tracing if enabled
475
+ const traceMode = config.artifacts?.trace ?? config.target.trace;
476
+ if (traceMode === 'always' || traceMode === 'on-first-failure' || traceMode === 'retain-on-failure') {
477
+ // Start tracing - will be saved in cleanup
478
+ // Note: Playwright's trace API is context.startTracing()
479
+ // Implementation depends on Playwright version
480
+ }
383
481
  // Add cookies from auth credentials after context creation
384
482
  if (this.auth?.cookies && this.auth.cookies.length > 0) {
385
483
  await this.context.addCookies(this.auth.cookies.map(c => ({
@@ -399,6 +497,68 @@ export class PlaywrightUiAdapter {
399
497
  shouldRecordVideo(mode) {
400
498
  return mode === 'always' || mode === 'retain-on-fail';
401
499
  }
500
+ /**
501
+ * Playwright++: Generate HTML report
502
+ */
503
+ async generateHtmlReport(config, results, e2eResults, summary) {
504
+ const reportPath = config.htmlReport || join(config.artifacts?.outputDir || this.artifactDir, 'report.html');
505
+ const timestamp = new Date().toISOString();
506
+ const reportData = {
507
+ title: `QA360 UI Test Report - ${timestamp}`,
508
+ summary: {
509
+ total: summary.total,
510
+ passed: summary.passed,
511
+ failed: summary.failed,
512
+ skipped: 0,
513
+ duration: summary.avgLoadTime * summary.total,
514
+ timestamp,
515
+ },
516
+ tests: [
517
+ ...results.map((r, i) => ({
518
+ id: `smoke-${i}`,
519
+ name: `UI Smoke: ${r.page}`,
520
+ status: (r.success ? 'passed' : 'failed'),
521
+ duration: r.loadTime,
522
+ error: r.error,
523
+ steps: [],
524
+ artifacts: r.artifacts,
525
+ })),
526
+ ...e2eResults.map((r, i) => ({
527
+ id: `e2e-${i}`,
528
+ name: r.test.name || 'E2E Test',
529
+ status: (r.success ? 'passed' : 'failed'),
530
+ duration: r.duration,
531
+ error: r.error,
532
+ steps: r.steps.map((s, j) => ({
533
+ name: s.step.action || `Step ${j + 1}`,
534
+ action: s.step.action || 'step',
535
+ selector: s.step.selector,
536
+ value: s.step.value,
537
+ status: (s.success ? 'passed' : 'failed'),
538
+ duration: s.duration,
539
+ error: s.error,
540
+ })),
541
+ artifacts: undefined,
542
+ })),
543
+ ],
544
+ artifacts: {
545
+ screenshots: this.allScreenshots.map(s => ({
546
+ path: s.localPath,
547
+ timestamp: s.metadata.timestamp,
548
+ type: 'after',
549
+ })),
550
+ videos: this.allVideos.map(v => ({ path: v, duration: 0 })),
551
+ traces: this.allTraces.map(t => ({ path: t, format: 'zip' })),
552
+ },
553
+ environment: {
554
+ browser: config.target.browser || 'chromium',
555
+ platform: process.platform,
556
+ nodeVersion: process.version,
557
+ },
558
+ };
559
+ HTMLReporter.generate(reportData, reportPath);
560
+ console.log(`\n📊 HTML report generated: ${reportPath}`);
561
+ }
402
562
  /**
403
563
  * Perform login if configured
404
564
  */
@@ -0,0 +1,6 @@
1
+ /**
2
+ * QA360 Artifacts Module
3
+ *
4
+ * Manages screenshots, videos, traces, and other test artifacts
5
+ */
6
+ export { UIArtifactsManager, createUIArtifactsManager, type ScreenshotOptions, type ArtifactMetadata, type StoredArtifact, type VideoArtifact, type ScreenshotArtifact, type TraceArtifact, } from './ui-artifacts.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * QA360 Artifacts Module
3
+ *
4
+ * Manages screenshots, videos, traces, and other test artifacts
5
+ */
6
+ export { UIArtifactsManager, createUIArtifactsManager, } from './ui-artifacts.js';
@@ -0,0 +1,133 @@
1
+ /**
2
+ * QA360 UI Artifacts Manager
3
+ *
4
+ * Manages screenshots, videos, traces, and other test artifacts
5
+ * with content-addressable storage (CAS) integration
6
+ */
7
+ export type ScreenshotFormat = 'png' | 'jpeg' | 'webp';
8
+ export interface ScreenshotOptions {
9
+ fullPage?: boolean;
10
+ quality?: number;
11
+ type?: ScreenshotFormat;
12
+ animations?: 'disabled' | 'allow';
13
+ }
14
+ export interface ArtifactMetadata {
15
+ testId: string;
16
+ stepIndex?: number;
17
+ stepName?: string;
18
+ type: 'screenshot' | 'video' | 'trace' | 'network' | 'console' | 'coverage';
19
+ timestamp: string;
20
+ status?: 'passed' | 'failed' | 'flaky';
21
+ tags?: string[];
22
+ }
23
+ export interface StoredArtifact {
24
+ casPath: string;
25
+ localPath: string;
26
+ hash: string;
27
+ metadata: ArtifactMetadata;
28
+ size: number;
29
+ type: 'screenshot' | 'video' | 'trace' | 'network' | 'console' | 'coverage';
30
+ }
31
+ export interface VideoArtifact extends StoredArtifact {
32
+ type: 'video';
33
+ format: 'webm';
34
+ duration: number;
35
+ }
36
+ export interface ScreenshotArtifact extends StoredArtifact {
37
+ type: 'screenshot';
38
+ format: ScreenshotFormat;
39
+ width: number;
40
+ height: number;
41
+ }
42
+ export interface TraceArtifact extends StoredArtifact {
43
+ type: 'trace';
44
+ format: 'zip';
45
+ }
46
+ /**
47
+ * UI Artifacts Manager
48
+ *
49
+ * Handles:
50
+ * - Automatic screenshots (before/after steps, on error)
51
+ * - Video recording
52
+ * - Trace files
53
+ * - Network captures
54
+ * - Console logs
55
+ * - Storage in CAS for deduplication
56
+ */
57
+ export declare class UIArtifactsManager {
58
+ private artifactDir;
59
+ private casDir;
60
+ private currentRunId;
61
+ private currentTestId?;
62
+ private artifacts;
63
+ private cas;
64
+ constructor(artifactDir?: string, casDir?: string);
65
+ private ensureDirectories;
66
+ /**
67
+ * Start a new test session
68
+ */
69
+ startTest(testId: string): void;
70
+ /**
71
+ * End current test session
72
+ */
73
+ endTest(): void;
74
+ /**
75
+ * Take a screenshot and store it
76
+ */
77
+ takeScreenshot(page: any, // Playwright Page
78
+ options?: ScreenshotOptions, metadata?: Partial<ArtifactMetadata>): Promise<ScreenshotArtifact>;
79
+ /**
80
+ * Take screenshot before a step
81
+ */
82
+ takeBeforeScreenshot(page: any, stepName: string, stepIndex: number): Promise<ScreenshotArtifact>;
83
+ /**
84
+ * Take screenshot after a step
85
+ */
86
+ takeAfterScreenshot(page: any, stepName: string, stepIndex: number, success: boolean): Promise<ScreenshotArtifact>;
87
+ /**
88
+ * Take screenshot on error
89
+ */
90
+ takeErrorScreenshot(page: any, error: Error, stepName?: string): Promise<ScreenshotArtifact>;
91
+ /**
92
+ * Save video artifact
93
+ */
94
+ saveVideo(videoPath: string, metadata?: Partial<ArtifactMetadata>): Promise<VideoArtifact>;
95
+ /**
96
+ * Save trace artifact
97
+ */
98
+ saveTrace(tracePath: string, metadata?: Partial<ArtifactMetadata>): Promise<TraceArtifact>;
99
+ /**
100
+ * Get artifact by ID
101
+ */
102
+ getArtifact(id: string): StoredArtifact | undefined;
103
+ /**
104
+ * Get all artifacts for current test
105
+ */
106
+ getTestArtifacts(): StoredArtifact[];
107
+ /**
108
+ * Get all artifacts by type
109
+ */
110
+ getArtifactsByType(type: StoredArtifact['type']): StoredArtifact[];
111
+ /**
112
+ * Clean up old artifacts
113
+ */
114
+ cleanup(maxAge?: number): void;
115
+ /**
116
+ * Generate CAS path from hash
117
+ */
118
+ private getCasPath;
119
+ /**
120
+ * Generate artifacts summary for reporting
121
+ */
122
+ generateSummary(): {
123
+ screenshots: number;
124
+ videos: number;
125
+ traces: number;
126
+ totalSize: number;
127
+ casDedupSavings: number;
128
+ };
129
+ }
130
+ /**
131
+ * Create a UI artifacts manager
132
+ */
133
+ export declare function createUIArtifactsManager(artifactDir?: string, casDir?: string): UIArtifactsManager;