qa360 2.0.13 → 2.1.1
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/scan.d.ts +5 -0
- package/dist/commands/scan.js +155 -0
- package/dist/core/adapters/playwright-native-adapter.d.ts +121 -0
- package/dist/core/adapters/playwright-native-adapter.js +339 -0
- package/dist/core/adapters/playwright-ui.d.ts +38 -0
- package/dist/core/adapters/playwright-ui.js +164 -4
- package/dist/core/artifacts/index.d.ts +6 -0
- package/dist/core/artifacts/index.js +6 -0
- package/dist/core/artifacts/ui-artifacts.d.ts +133 -0
- package/dist/core/artifacts/ui-artifacts.js +304 -0
- package/dist/core/core/coverage/analyzer.d.ts +101 -0
- package/dist/core/core/coverage/analyzer.js +415 -0
- package/dist/core/core/coverage/collector.d.ts +74 -0
- package/dist/core/core/coverage/collector.js +459 -0
- package/dist/core/core/coverage/config.d.ts +37 -0
- package/dist/core/core/coverage/config.js +156 -0
- package/dist/core/core/coverage/index.d.ts +11 -0
- package/dist/core/core/coverage/index.js +15 -0
- package/dist/core/core/coverage/types.d.ts +267 -0
- package/dist/core/core/coverage/types.js +6 -0
- package/dist/core/core/coverage/vault.d.ts +95 -0
- package/dist/core/core/coverage/vault.js +405 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.js +9 -0
- package/dist/core/parallel/index.d.ts +6 -0
- package/dist/core/parallel/index.js +6 -0
- package/dist/core/parallel/parallel-runner.d.ts +107 -0
- package/dist/core/parallel/parallel-runner.js +192 -0
- package/dist/core/reporting/html-reporter.d.ts +119 -0
- package/dist/core/reporting/html-reporter.js +737 -0
- package/dist/core/reporting/index.d.ts +6 -0
- package/dist/core/reporting/index.js +6 -0
- package/dist/core/runner/phase3-runner.js +29 -4
- package/dist/core/vault/cas.d.ts +5 -1
- package/dist/core/vault/cas.js +6 -0
- package/dist/core/visual/index.d.ts +6 -0
- package/dist/core/visual/index.js +6 -0
- package/dist/core/visual/visual-regression.d.ts +113 -0
- package/dist/core/visual/visual-regression.js +236 -0
- package/dist/generators/index.d.ts +5 -0
- package/dist/generators/index.js +5 -0
- package/dist/generators/json-reporter.d.ts +10 -0
- package/dist/generators/json-reporter.js +12 -0
- package/dist/generators/test-generator.d.ts +18 -0
- package/dist/generators/test-generator.js +78 -0
- package/dist/index.js +3 -0
- package/dist/scanners/dom-scanner.d.ts +52 -0
- package/dist/scanners/dom-scanner.js +296 -0
- package/dist/scanners/index.d.ts +4 -0
- package/dist/scanners/index.js +4 -0
- package/dist/types/scan.d.ts +68 -0
- package/dist/types/scan.js +4 -0
- package/examples/README.md +38 -0
- package/examples/crawler.yml +38 -0
- package/examples/ui-advanced.yml +49 -0
- package/package.json +2 -2
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scan Command - Discover UI elements from a web page
|
|
3
|
+
*/
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { chromium } from '@playwright/test';
|
|
6
|
+
import { DOMScanner } from '../scanners/dom-scanner.js';
|
|
7
|
+
import { JSONReporter } from '../generators/json-reporter.js';
|
|
8
|
+
import { TestGenerator } from '../generators/test-generator.js';
|
|
9
|
+
import { resolve } from 'path';
|
|
10
|
+
import { mkdirSync } from 'fs';
|
|
11
|
+
export const scanCommand = new Command('scan');
|
|
12
|
+
scanCommand
|
|
13
|
+
.description('Scan a web page for UI elements and generate test configurations')
|
|
14
|
+
.argument('<url>', 'URL of the page to scan')
|
|
15
|
+
.option('-o, --output <file>', 'Output file path (default: ".qa360/discovery/elements.json")')
|
|
16
|
+
.option('-i, --include <types>', 'Element types to include: buttons,links,forms,inputs,images,headings,all (default: "all")')
|
|
17
|
+
.option('-e, --exclude <selectors>', 'CSS selectors to exclude (comma-separated)')
|
|
18
|
+
.option('-s, --screenshot', 'Capture screenshots of elements (not yet implemented)')
|
|
19
|
+
.option('-a, --auto-generate-test', 'Auto-generate QA360 test file')
|
|
20
|
+
.option('-t, --timeout <ms>', 'Navigation timeout in milliseconds', '30000')
|
|
21
|
+
.option('--headed', 'Run in headed mode (show browser)')
|
|
22
|
+
.action(async (url, options) => {
|
|
23
|
+
const startTime = Date.now();
|
|
24
|
+
// Validate URL
|
|
25
|
+
try {
|
|
26
|
+
new URL(url);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
console.error(`❌ Invalid URL: ${url}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
// Parse include types
|
|
33
|
+
const includeTypes = options.include || 'all';
|
|
34
|
+
const include = includeTypes
|
|
35
|
+
.split(',')
|
|
36
|
+
.map((t) => t.trim().toLowerCase());
|
|
37
|
+
// Validate element types
|
|
38
|
+
const validTypes = ['buttons', 'links', 'forms', 'inputs', 'images', 'headings', 'all'];
|
|
39
|
+
for (const type of include) {
|
|
40
|
+
if (!validTypes.includes(type)) {
|
|
41
|
+
console.error(`❌ Invalid element type: ${type}`);
|
|
42
|
+
console.error(` Valid types: ${validTypes.join(', ')}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Parse exclude selectors
|
|
47
|
+
const exclude = options.exclude
|
|
48
|
+
? options.exclude.split(',').map((s) => s.trim())
|
|
49
|
+
: [];
|
|
50
|
+
const scanOptions = {
|
|
51
|
+
url,
|
|
52
|
+
output: options.output || '.qa360/discovery/elements.json',
|
|
53
|
+
include,
|
|
54
|
+
exclude,
|
|
55
|
+
screenshot: options.screenshot || false,
|
|
56
|
+
autoGenerateTest: options.autoGenerateTest || false,
|
|
57
|
+
timeout: parseInt(options.timeout, 10) || 30000,
|
|
58
|
+
headless: !options.headed
|
|
59
|
+
};
|
|
60
|
+
console.log(`🔍 Scanning page: ${url}`);
|
|
61
|
+
console.log('━'.repeat(50));
|
|
62
|
+
let browser = null;
|
|
63
|
+
let context = null;
|
|
64
|
+
let page = null;
|
|
65
|
+
try {
|
|
66
|
+
// Launch browser
|
|
67
|
+
browser = await chromium.launch({
|
|
68
|
+
headless: scanOptions.headless
|
|
69
|
+
});
|
|
70
|
+
context = await browser.newContext({
|
|
71
|
+
viewport: { width: 1280, height: 720 }
|
|
72
|
+
});
|
|
73
|
+
page = await context.newPage();
|
|
74
|
+
// Navigate to URL
|
|
75
|
+
await page.goto(url, {
|
|
76
|
+
timeout: scanOptions.timeout,
|
|
77
|
+
waitUntil: 'networkidle'
|
|
78
|
+
});
|
|
79
|
+
// Run scan
|
|
80
|
+
const scanner = new DOMScanner(page, url);
|
|
81
|
+
const elements = await scanner.scan(scanOptions);
|
|
82
|
+
// Generate summary
|
|
83
|
+
const summary = {
|
|
84
|
+
totalElements: elements.length,
|
|
85
|
+
buttons: elements.filter(e => e.type === 'button').length,
|
|
86
|
+
links: elements.filter(e => e.type === 'link').length,
|
|
87
|
+
forms: elements.filter(e => e.type === 'form').length,
|
|
88
|
+
inputs: elements.filter(e => e.type === 'input').length,
|
|
89
|
+
images: elements.filter(e => e.type === 'image').length,
|
|
90
|
+
headings: elements.filter(e => e.type === 'heading').length
|
|
91
|
+
};
|
|
92
|
+
// Print summary
|
|
93
|
+
console.log(`✅ Buttons found: ${summary.buttons}`);
|
|
94
|
+
console.log(`✅ Links found: ${summary.links}`);
|
|
95
|
+
console.log(`✅ Forms found: ${summary.forms}`);
|
|
96
|
+
console.log(`✅ Inputs found: ${summary.inputs}`);
|
|
97
|
+
console.log(`✅ Images found: ${summary.images}`);
|
|
98
|
+
console.log(`✅ Headings found: ${summary.headings}`);
|
|
99
|
+
console.log('━'.repeat(50));
|
|
100
|
+
console.log(`📦 Total elements: ${summary.totalElements}`);
|
|
101
|
+
console.log();
|
|
102
|
+
// Create output directory
|
|
103
|
+
const outputPath = resolve(scanOptions.output);
|
|
104
|
+
const outputDir = outputPath.substring(0, outputPath.lastIndexOf('/'));
|
|
105
|
+
mkdirSync(outputDir, { recursive: true });
|
|
106
|
+
// Generate scan result
|
|
107
|
+
const result = {
|
|
108
|
+
scanDate: new Date().toISOString(),
|
|
109
|
+
url,
|
|
110
|
+
duration: Date.now() - startTime,
|
|
111
|
+
summary,
|
|
112
|
+
elements
|
|
113
|
+
};
|
|
114
|
+
// Generate JSON report
|
|
115
|
+
const reporter = new JSONReporter();
|
|
116
|
+
await reporter.generate(result, outputPath);
|
|
117
|
+
console.log(`💾 Saved to: ${outputPath}`);
|
|
118
|
+
// Auto-generate test if requested
|
|
119
|
+
if (options.autoGenerateTest) {
|
|
120
|
+
const testGenerator = new TestGenerator();
|
|
121
|
+
const testPath = outputPath.replace('.json', '.yml');
|
|
122
|
+
await testGenerator.generate(result, testPath);
|
|
123
|
+
console.log(`📄 Test generated: ${testPath}`);
|
|
124
|
+
}
|
|
125
|
+
// Screenshots (placeholder for future implementation)
|
|
126
|
+
if (options.screenshot) {
|
|
127
|
+
console.log(`📸 Screenshots: ${outputDir}/screenshots/ (not yet implemented)`);
|
|
128
|
+
}
|
|
129
|
+
// Cleanup
|
|
130
|
+
await page.close();
|
|
131
|
+
await context.close();
|
|
132
|
+
await browser.close();
|
|
133
|
+
console.log();
|
|
134
|
+
console.log(`✨ Scan completed in ${((Date.now() - startTime) / 1000).toFixed(1)}s`);
|
|
135
|
+
console.log();
|
|
136
|
+
console.log('💡 Next steps:');
|
|
137
|
+
console.log(` • Review the discovered elements`);
|
|
138
|
+
if (options.autoGenerateTest) {
|
|
139
|
+
console.log(` • Run the auto-generated test: qa360 run ${outputPath.replace('.json', '.yml')}`);
|
|
140
|
+
}
|
|
141
|
+
console.log(` • Edit the test to remove unwanted elements`);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
// Cleanup on error
|
|
145
|
+
if (page)
|
|
146
|
+
await page.close().catch(() => { });
|
|
147
|
+
if (context)
|
|
148
|
+
await context.close().catch(() => { });
|
|
149
|
+
if (browser)
|
|
150
|
+
await browser.close().catch(() => { });
|
|
151
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
152
|
+
console.error(`❌ Scan failed: ${errorMessage}`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QA360 Playwright Native Adapter
|
|
3
|
+
*
|
|
4
|
+
* Zero-overhead adapter that uses Playwright's native API directly.
|
|
5
|
+
* This bypasses any wrapper logic and gives full access to Playwright's capabilities.
|
|
6
|
+
*
|
|
7
|
+
* Benefits:
|
|
8
|
+
* - Zero performance overhead
|
|
9
|
+
* - Full Playwright API access
|
|
10
|
+
* - Native video/trace/screenshot support
|
|
11
|
+
* - Direct access to browser context, page, locators
|
|
12
|
+
*/
|
|
13
|
+
import { Browser, BrowserContext, Page, Locator } from '@playwright/test';
|
|
14
|
+
import type { WebTarget, PackBudgets, UiTestDefinition, UiTestStep } from '../types/pack-v1.js';
|
|
15
|
+
export interface PlaywrightNativeConfig {
|
|
16
|
+
target: WebTarget;
|
|
17
|
+
budgets?: PackBudgets;
|
|
18
|
+
timeout?: number;
|
|
19
|
+
browser?: 'chromium' | 'firefox' | 'webkit';
|
|
20
|
+
headless?: boolean;
|
|
21
|
+
slowMo?: number;
|
|
22
|
+
devtools?: boolean;
|
|
23
|
+
device?: 'desktop' | 'tablet' | 'mobile' | 'custom';
|
|
24
|
+
viewport?: {
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
};
|
|
28
|
+
userAgent?: string;
|
|
29
|
+
locale?: string;
|
|
30
|
+
timezone?: string;
|
|
31
|
+
screenshots?: 'always' | 'only-on-failure' | 'never';
|
|
32
|
+
video?: 'always' | 'retain-on-failure' | 'never';
|
|
33
|
+
trace?: 'always' | 'retain-on-failure' | 'never' | 'on-first-failure';
|
|
34
|
+
bail?: number;
|
|
35
|
+
workers?: number;
|
|
36
|
+
ignoreHTTPSErrors?: boolean;
|
|
37
|
+
acceptDownloads?: boolean;
|
|
38
|
+
bypassCSP?: boolean;
|
|
39
|
+
}
|
|
40
|
+
export interface PlaywrightNativeResult {
|
|
41
|
+
success: boolean;
|
|
42
|
+
duration: number;
|
|
43
|
+
artifacts?: {
|
|
44
|
+
screenshots: string[];
|
|
45
|
+
videos: string[];
|
|
46
|
+
traces: string[];
|
|
47
|
+
};
|
|
48
|
+
error?: string;
|
|
49
|
+
coverage?: {
|
|
50
|
+
lines: number;
|
|
51
|
+
statements: number;
|
|
52
|
+
branches: number;
|
|
53
|
+
functions: number;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export interface PlaywrightNativeStepResult {
|
|
57
|
+
step: UiTestStep;
|
|
58
|
+
success: boolean;
|
|
59
|
+
duration: number;
|
|
60
|
+
error?: string;
|
|
61
|
+
screenshot?: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Playwright Native Adapter
|
|
65
|
+
*
|
|
66
|
+
* Provides zero-overhead access to Playwright with automatic
|
|
67
|
+
* artifact collection (screenshots, videos, traces).
|
|
68
|
+
*/
|
|
69
|
+
export declare class PlaywrightNativeAdapter {
|
|
70
|
+
private config;
|
|
71
|
+
private browser?;
|
|
72
|
+
private context?;
|
|
73
|
+
private page?;
|
|
74
|
+
private artifacts?;
|
|
75
|
+
private assertions?;
|
|
76
|
+
private testResults;
|
|
77
|
+
private failureCount;
|
|
78
|
+
constructor(config: PlaywrightNativeConfig);
|
|
79
|
+
/**
|
|
80
|
+
* Run a single E2E test with full Playwright native access
|
|
81
|
+
*/
|
|
82
|
+
runTest(test: UiTestDefinition): Promise<PlaywrightNativeResult>;
|
|
83
|
+
/**
|
|
84
|
+
* Execute a single step using Playwright native API
|
|
85
|
+
*/
|
|
86
|
+
private executeStep;
|
|
87
|
+
/**
|
|
88
|
+
* Execute action using Playwright native API
|
|
89
|
+
*/
|
|
90
|
+
private executeAction;
|
|
91
|
+
/**
|
|
92
|
+
* Setup browser with trace context
|
|
93
|
+
*/
|
|
94
|
+
private setup;
|
|
95
|
+
/**
|
|
96
|
+
* Teardown and save artifacts
|
|
97
|
+
*/
|
|
98
|
+
private teardown;
|
|
99
|
+
/**
|
|
100
|
+
* Get artifact paths for result
|
|
101
|
+
*/
|
|
102
|
+
private getArtifactPaths;
|
|
103
|
+
/**
|
|
104
|
+
* Get direct access to Playwright objects
|
|
105
|
+
*/
|
|
106
|
+
getBrowser(): Browser | undefined;
|
|
107
|
+
getContext(): BrowserContext | undefined;
|
|
108
|
+
getPage(): Page | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* Get locator for direct manipulation
|
|
111
|
+
*/
|
|
112
|
+
locator(selector: string): Locator;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Create a Playwright Native Adapter
|
|
116
|
+
*/
|
|
117
|
+
export declare function createPlaywrightNativeAdapter(config: PlaywrightNativeConfig): PlaywrightNativeAdapter;
|
|
118
|
+
/**
|
|
119
|
+
* Create adapter from WebTarget (convenience function)
|
|
120
|
+
*/
|
|
121
|
+
export declare function createFromTarget(target: WebTarget, options?: Partial<PlaywrightNativeConfig>): PlaywrightNativeAdapter;
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QA360 Playwright Native Adapter
|
|
3
|
+
*
|
|
4
|
+
* Zero-overhead adapter that uses Playwright's native API directly.
|
|
5
|
+
* This bypasses any wrapper logic and gives full access to Playwright's capabilities.
|
|
6
|
+
*
|
|
7
|
+
* Benefits:
|
|
8
|
+
* - Zero performance overhead
|
|
9
|
+
* - Full Playwright API access
|
|
10
|
+
* - Native video/trace/screenshot support
|
|
11
|
+
* - Direct access to browser context, page, locators
|
|
12
|
+
*/
|
|
13
|
+
import { chromium, firefox, webkit } from '@playwright/test';
|
|
14
|
+
import { UIArtifactsManager } from '../artifacts/ui-artifacts.js';
|
|
15
|
+
import { createAssertionsEngine } from '../assertions/index.js';
|
|
16
|
+
/**
|
|
17
|
+
* Playwright Native Adapter
|
|
18
|
+
*
|
|
19
|
+
* Provides zero-overhead access to Playwright with automatic
|
|
20
|
+
* artifact collection (screenshots, videos, traces).
|
|
21
|
+
*/
|
|
22
|
+
export class PlaywrightNativeAdapter {
|
|
23
|
+
config;
|
|
24
|
+
browser;
|
|
25
|
+
context;
|
|
26
|
+
page;
|
|
27
|
+
artifacts;
|
|
28
|
+
assertions;
|
|
29
|
+
// Test state tracking
|
|
30
|
+
testResults = new Map();
|
|
31
|
+
failureCount = 0;
|
|
32
|
+
constructor(config) {
|
|
33
|
+
this.config = config;
|
|
34
|
+
// Initialize artifacts manager
|
|
35
|
+
this.artifacts = new UIArtifactsManager('.qa360/artifacts/playwright-native', '.qa360/runs/cas');
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Run a single E2E test with full Playwright native access
|
|
39
|
+
*/
|
|
40
|
+
async runTest(test) {
|
|
41
|
+
const startTime = Date.now();
|
|
42
|
+
const testId = test.name || 'unknown';
|
|
43
|
+
this.artifacts?.startTest(testId);
|
|
44
|
+
this.testResults.set(testId, []);
|
|
45
|
+
try {
|
|
46
|
+
// Setup browser with trace context if enabled
|
|
47
|
+
await this.setup();
|
|
48
|
+
// Navigate to start URL
|
|
49
|
+
const startUrl = test.url || `${this.config.target.baseUrl.replace(/\/$/, '')}${test.path || ''}`;
|
|
50
|
+
await this.page.goto(startUrl, { timeout: this.config.timeout || 30000 });
|
|
51
|
+
// Initialize assertions engine
|
|
52
|
+
this.assertions = createAssertionsEngine(this.page);
|
|
53
|
+
// Take initial screenshot
|
|
54
|
+
if (this.config.screenshots !== 'never') {
|
|
55
|
+
await this.artifacts?.takeScreenshot(this.page, {}, {
|
|
56
|
+
testId,
|
|
57
|
+
type: 'screenshot',
|
|
58
|
+
tags: ['initial'],
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
// Execute each step
|
|
62
|
+
for (let i = 0; i < test.steps.length; i++) {
|
|
63
|
+
const step = test.steps[i];
|
|
64
|
+
const stepResult = await this.executeStep(step, i);
|
|
65
|
+
this.testResults.get(testId).push(stepResult);
|
|
66
|
+
if (!stepResult.success) {
|
|
67
|
+
this.failureCount++;
|
|
68
|
+
// Check if we should bail
|
|
69
|
+
if (this.config.bail && this.failureCount >= this.config.bail) {
|
|
70
|
+
throw new Error(`Bail: ${this.failureCount} failures`);
|
|
71
|
+
}
|
|
72
|
+
// Check if we should continue on failure
|
|
73
|
+
const continueOnFailure = step.continueOnError ?? false;
|
|
74
|
+
if (!continueOnFailure) {
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Screenshot after each step if configured
|
|
79
|
+
if (this.config.screenshots === 'always' || (this.config.screenshots === 'only-on-failure' && !stepResult.success)) {
|
|
80
|
+
await this.artifacts?.takeAfterScreenshot(this.page, step.action || 'step', i, stepResult.success);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// All steps passed
|
|
84
|
+
const duration = Date.now() - startTime;
|
|
85
|
+
return {
|
|
86
|
+
success: true,
|
|
87
|
+
duration,
|
|
88
|
+
artifacts: this.getArtifactPaths(),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
// Take error screenshot
|
|
93
|
+
if (this.page) {
|
|
94
|
+
await this.artifacts?.takeErrorScreenshot(this.page, error);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
success: false,
|
|
98
|
+
duration: Date.now() - startTime,
|
|
99
|
+
error: error instanceof Error ? error.message : String(error),
|
|
100
|
+
artifacts: this.getArtifactPaths(),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
this.artifacts?.endTest();
|
|
105
|
+
await this.teardown();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Execute a single step using Playwright native API
|
|
110
|
+
*/
|
|
111
|
+
async executeStep(step, index) {
|
|
112
|
+
const startTime = Date.now();
|
|
113
|
+
try {
|
|
114
|
+
// Take before screenshot if enabled
|
|
115
|
+
if (this.config.screenshots === 'always') {
|
|
116
|
+
await this.artifacts?.takeBeforeScreenshot(this.page, step.action || 'step', index);
|
|
117
|
+
}
|
|
118
|
+
// Execute action based on type
|
|
119
|
+
const result = await this.executeAction(step);
|
|
120
|
+
// Execute assertions if present
|
|
121
|
+
if (step.assertions && step.assertions.length > 0 && this.assertions) {
|
|
122
|
+
for (const assertion of step.assertions) {
|
|
123
|
+
const assertionResult = await this.assertions.runAssertion(assertion);
|
|
124
|
+
if (!assertionResult.passed && !assertion.soft) {
|
|
125
|
+
throw new Error(assertionResult.error || `Assertion failed: ${assertion.type}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
step,
|
|
131
|
+
success: true,
|
|
132
|
+
duration: Date.now() - startTime,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
return {
|
|
137
|
+
step,
|
|
138
|
+
success: false,
|
|
139
|
+
duration: Date.now() - startTime,
|
|
140
|
+
error: error instanceof Error ? error.message : String(error),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Execute action using Playwright native API
|
|
146
|
+
*/
|
|
147
|
+
async executeAction(step) {
|
|
148
|
+
const selector = step.selector;
|
|
149
|
+
const page = this.page;
|
|
150
|
+
const timeout = step.timeout || this.config.timeout || 30000;
|
|
151
|
+
// Ensure selector is defined for actions that need it
|
|
152
|
+
const needsSelector = ['click', 'dblClick', 'rightClick', 'hover', 'focus', 'fill', 'type',
|
|
153
|
+
'clear', 'select', 'check', 'uncheck', 'press', 'upload', 'tap', 'scroll'].includes(step.action);
|
|
154
|
+
if (needsSelector && !selector) {
|
|
155
|
+
throw new Error(`Selector is required for action: ${step.action}`);
|
|
156
|
+
}
|
|
157
|
+
switch (step.action) {
|
|
158
|
+
case 'navigate':
|
|
159
|
+
await page.goto(step.value || '', { timeout });
|
|
160
|
+
break;
|
|
161
|
+
case 'click':
|
|
162
|
+
await page.click(selector, { timeout });
|
|
163
|
+
break;
|
|
164
|
+
case 'dblClick':
|
|
165
|
+
await page.dblclick(selector, { timeout });
|
|
166
|
+
break;
|
|
167
|
+
case 'rightClick':
|
|
168
|
+
await page.click(selector, { button: 'right', timeout });
|
|
169
|
+
break;
|
|
170
|
+
case 'fill':
|
|
171
|
+
case 'type':
|
|
172
|
+
if (step.value) {
|
|
173
|
+
await page.fill(selector, step.value, { timeout });
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
case 'clear':
|
|
177
|
+
await page.fill(selector, '', { timeout });
|
|
178
|
+
break;
|
|
179
|
+
case 'select':
|
|
180
|
+
if (step.value !== undefined) {
|
|
181
|
+
await page.selectOption(selector, step.value, { timeout });
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
case 'check':
|
|
185
|
+
await page.check(selector, { timeout });
|
|
186
|
+
break;
|
|
187
|
+
case 'uncheck':
|
|
188
|
+
await page.uncheck(selector, { timeout });
|
|
189
|
+
break;
|
|
190
|
+
case 'hover':
|
|
191
|
+
await page.hover(selector, { timeout });
|
|
192
|
+
break;
|
|
193
|
+
case 'focus':
|
|
194
|
+
await page.focus(selector, { timeout });
|
|
195
|
+
break;
|
|
196
|
+
case 'press':
|
|
197
|
+
if (step.value) {
|
|
198
|
+
await page.press(selector, step.value, { timeout });
|
|
199
|
+
}
|
|
200
|
+
break;
|
|
201
|
+
case 'upload':
|
|
202
|
+
if (step.value) {
|
|
203
|
+
await page.setInputFiles(selector, step.value, { timeout });
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
case 'dragAndDrop':
|
|
207
|
+
if (step.value) {
|
|
208
|
+
const source = page.locator(selector);
|
|
209
|
+
const target = page.locator(step.value);
|
|
210
|
+
await source.dragTo(target, { timeout });
|
|
211
|
+
}
|
|
212
|
+
break;
|
|
213
|
+
case 'scroll':
|
|
214
|
+
if (selector) {
|
|
215
|
+
await page.locator(selector).scrollIntoViewIfNeeded({ timeout });
|
|
216
|
+
}
|
|
217
|
+
break;
|
|
218
|
+
case 'waitFor':
|
|
219
|
+
case 'waitForSelector':
|
|
220
|
+
if (selector) {
|
|
221
|
+
await page.waitForSelector(selector, {
|
|
222
|
+
state: step.value || undefined,
|
|
223
|
+
timeout
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
case 'waitForTimeout':
|
|
228
|
+
await page.waitForTimeout(parseInt(step.value || '0', 10) || 0);
|
|
229
|
+
break;
|
|
230
|
+
case 'waitForNavigation':
|
|
231
|
+
await page.waitForNavigation({ timeout });
|
|
232
|
+
break;
|
|
233
|
+
case 'tap':
|
|
234
|
+
await page.tap(selector, { timeout });
|
|
235
|
+
break;
|
|
236
|
+
default:
|
|
237
|
+
throw new Error(`Unknown action: ${step.action}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Setup browser with trace context
|
|
242
|
+
*/
|
|
243
|
+
async setup() {
|
|
244
|
+
const browserType = this.config.browser || 'chromium';
|
|
245
|
+
const browserTypeObj = browserType === 'firefox' ? firefox : browserType === 'webkit' ? webkit : chromium;
|
|
246
|
+
// Launch browser
|
|
247
|
+
this.browser = await browserTypeObj.launch({
|
|
248
|
+
headless: this.config.headless ?? true,
|
|
249
|
+
slowMo: this.config.slowMo || 0,
|
|
250
|
+
devtools: this.config.devtools || false,
|
|
251
|
+
});
|
|
252
|
+
// Create context with trace recording if enabled
|
|
253
|
+
const contextOptions = {
|
|
254
|
+
viewport: this.config.viewport || { width: 1280, height: 720 },
|
|
255
|
+
userAgent: this.config.userAgent,
|
|
256
|
+
locale: this.config.locale || 'en-US',
|
|
257
|
+
timezoneId: this.config.timezone,
|
|
258
|
+
acceptDownloads: this.config.acceptDownloads ?? true,
|
|
259
|
+
ignoreHTTPSErrors: this.config.ignoreHTTPSErrors,
|
|
260
|
+
bypassCSP: this.config.bypassCSP,
|
|
261
|
+
};
|
|
262
|
+
// Add video recording
|
|
263
|
+
if (this.config.video === 'always' || this.config.video === 'retain-on-failure') {
|
|
264
|
+
contextOptions.recordVideo = {
|
|
265
|
+
dir: '.qa360/artifacts/playwright-native/videos',
|
|
266
|
+
size: this.config.viewport || { width: 1280, height: 720 },
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
this.context = await this.browser.newContext(contextOptions);
|
|
270
|
+
// Add tracing if enabled
|
|
271
|
+
if (this.config.trace === 'always' || this.config.trace === 'on-first-failure' || this.config.trace === 'retain-on-failure') {
|
|
272
|
+
// Start tracing (Playwright's trace feature)
|
|
273
|
+
// Note: tracing needs to be started per test
|
|
274
|
+
}
|
|
275
|
+
this.page = await this.context.newPage();
|
|
276
|
+
// Set default timeout
|
|
277
|
+
this.page.setDefaultTimeout(this.config.timeout || 30000);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Teardown and save artifacts
|
|
281
|
+
*/
|
|
282
|
+
async teardown() {
|
|
283
|
+
if (this.page) {
|
|
284
|
+
// Stop tracing if enabled
|
|
285
|
+
// Save trace file
|
|
286
|
+
await this.page.close();
|
|
287
|
+
}
|
|
288
|
+
if (this.context) {
|
|
289
|
+
// Save video if enabled and tests failed
|
|
290
|
+
if (this.config.video === 'retain-on-failure' && this.failureCount > 0) {
|
|
291
|
+
// Videos are automatically saved by Playwright
|
|
292
|
+
}
|
|
293
|
+
await this.context.close();
|
|
294
|
+
}
|
|
295
|
+
if (this.browser) {
|
|
296
|
+
await this.browser.close();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Get artifact paths for result
|
|
301
|
+
*/
|
|
302
|
+
getArtifactPaths() {
|
|
303
|
+
return {
|
|
304
|
+
screenshots: [],
|
|
305
|
+
videos: [],
|
|
306
|
+
traces: [],
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Get direct access to Playwright objects
|
|
311
|
+
*/
|
|
312
|
+
getBrowser() { return this.browser; }
|
|
313
|
+
getContext() { return this.context; }
|
|
314
|
+
getPage() { return this.page; }
|
|
315
|
+
/**
|
|
316
|
+
* Get locator for direct manipulation
|
|
317
|
+
*/
|
|
318
|
+
locator(selector) {
|
|
319
|
+
if (!this.page) {
|
|
320
|
+
throw new Error('Page not initialized. Call setup() first.');
|
|
321
|
+
}
|
|
322
|
+
return this.page.locator(selector);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Create a Playwright Native Adapter
|
|
327
|
+
*/
|
|
328
|
+
export function createPlaywrightNativeAdapter(config) {
|
|
329
|
+
return new PlaywrightNativeAdapter(config);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Create adapter from WebTarget (convenience function)
|
|
333
|
+
*/
|
|
334
|
+
export function createFromTarget(target, options = {}) {
|
|
335
|
+
return new PlaywrightNativeAdapter({
|
|
336
|
+
target,
|
|
337
|
+
...options,
|
|
338
|
+
});
|
|
339
|
+
}
|