@testsmith/perfornium 0.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.
- package/README.md +360 -0
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.js +192 -0
- package/dist/cli/commands/distributed.d.ts +11 -0
- package/dist/cli/commands/distributed.js +179 -0
- package/dist/cli/commands/import.d.ts +23 -0
- package/dist/cli/commands/import.js +461 -0
- package/dist/cli/commands/init.d.ts +7 -0
- package/dist/cli/commands/init.js +923 -0
- package/dist/cli/commands/mock.d.ts +7 -0
- package/dist/cli/commands/mock.js +281 -0
- package/dist/cli/commands/report.d.ts +5 -0
- package/dist/cli/commands/report.js +70 -0
- package/dist/cli/commands/run.d.ts +12 -0
- package/dist/cli/commands/run.js +260 -0
- package/dist/cli/commands/validate.d.ts +3 -0
- package/dist/cli/commands/validate.js +35 -0
- package/dist/cli/commands/worker.d.ts +27 -0
- package/dist/cli/commands/worker.js +320 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.js +20 -0
- package/dist/config/parser.d.ts +19 -0
- package/dist/config/parser.js +330 -0
- package/dist/config/types/global-config.d.ts +74 -0
- package/dist/config/types/global-config.js +2 -0
- package/dist/config/types/hooks.d.ts +58 -0
- package/dist/config/types/hooks.js +3 -0
- package/dist/config/types/import-types.d.ts +33 -0
- package/dist/config/types/import-types.js +2 -0
- package/dist/config/types/index.d.ts +11 -0
- package/dist/config/types/index.js +27 -0
- package/dist/config/types/load-config.d.ts +32 -0
- package/dist/config/types/load-config.js +9 -0
- package/dist/config/types/output-config.d.ts +10 -0
- package/dist/config/types/output-config.js +2 -0
- package/dist/config/types/report-config.d.ts +10 -0
- package/dist/config/types/report-config.js +2 -0
- package/dist/config/types/runtime-types.d.ts +6 -0
- package/dist/config/types/runtime-types.js +2 -0
- package/dist/config/types/scenario-config.d.ts +30 -0
- package/dist/config/types/scenario-config.js +2 -0
- package/dist/config/types/step-types.d.ts +139 -0
- package/dist/config/types/step-types.js +2 -0
- package/dist/config/types/test-configuration.d.ts +18 -0
- package/dist/config/types/test-configuration.js +2 -0
- package/dist/config/types/worker-config.d.ts +12 -0
- package/dist/config/types/worker-config.js +2 -0
- package/dist/config/validator.d.ts +19 -0
- package/dist/config/validator.js +198 -0
- package/dist/core/csv-data-provider.d.ts +47 -0
- package/dist/core/csv-data-provider.js +265 -0
- package/dist/core/hooks-manager.d.ts +33 -0
- package/dist/core/hooks-manager.js +129 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +11 -0
- package/dist/core/script-executor.d.ts +14 -0
- package/dist/core/script-executor.js +290 -0
- package/dist/core/step-executor.d.ts +41 -0
- package/dist/core/step-executor.js +680 -0
- package/dist/core/test-runner.d.ts +34 -0
- package/dist/core/test-runner.js +465 -0
- package/dist/core/threshold-evaluator.d.ts +43 -0
- package/dist/core/threshold-evaluator.js +170 -0
- package/dist/core/virtual-user-pool.d.ts +42 -0
- package/dist/core/virtual-user-pool.js +136 -0
- package/dist/core/virtual-user.d.ts +51 -0
- package/dist/core/virtual-user.js +488 -0
- package/dist/distributed/coordinator.d.ts +34 -0
- package/dist/distributed/coordinator.js +158 -0
- package/dist/distributed/health-monitor.d.ts +18 -0
- package/dist/distributed/health-monitor.js +72 -0
- package/dist/distributed/load-distributor.d.ts +17 -0
- package/dist/distributed/load-distributor.js +106 -0
- package/dist/distributed/remote-worker.d.ts +37 -0
- package/dist/distributed/remote-worker.js +241 -0
- package/dist/distributed/result-aggregator.d.ts +43 -0
- package/dist/distributed/result-aggregator.js +146 -0
- package/dist/dsl/index.d.ts +3 -0
- package/dist/dsl/index.js +11 -0
- package/dist/dsl/test-builder.d.ts +111 -0
- package/dist/dsl/test-builder.js +514 -0
- package/dist/importers/har-importer.d.ts +17 -0
- package/dist/importers/har-importer.js +172 -0
- package/dist/importers/open-api-importer.d.ts +23 -0
- package/dist/importers/open-api-importer.js +181 -0
- package/dist/importers/wsdl-importer.d.ts +42 -0
- package/dist/importers/wsdl-importer.js +440 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +17 -0
- package/dist/load-patterns/arrivals.d.ts +7 -0
- package/dist/load-patterns/arrivals.js +118 -0
- package/dist/load-patterns/base.d.ts +9 -0
- package/dist/load-patterns/base.js +2 -0
- package/dist/load-patterns/basic.d.ts +7 -0
- package/dist/load-patterns/basic.js +117 -0
- package/dist/load-patterns/stepping.d.ts +6 -0
- package/dist/load-patterns/stepping.js +122 -0
- package/dist/metrics/collector.d.ts +72 -0
- package/dist/metrics/collector.js +662 -0
- package/dist/metrics/types.d.ts +135 -0
- package/dist/metrics/types.js +2 -0
- package/dist/outputs/base.d.ts +7 -0
- package/dist/outputs/base.js +2 -0
- package/dist/outputs/csv.d.ts +13 -0
- package/dist/outputs/csv.js +163 -0
- package/dist/outputs/graphite.d.ts +13 -0
- package/dist/outputs/graphite.js +126 -0
- package/dist/outputs/influxdb.d.ts +12 -0
- package/dist/outputs/influxdb.js +82 -0
- package/dist/outputs/json.d.ts +14 -0
- package/dist/outputs/json.js +107 -0
- package/dist/outputs/streaming-csv.d.ts +37 -0
- package/dist/outputs/streaming-csv.js +254 -0
- package/dist/outputs/streaming-json.d.ts +43 -0
- package/dist/outputs/streaming-json.js +353 -0
- package/dist/outputs/webhook.d.ts +16 -0
- package/dist/outputs/webhook.js +96 -0
- package/dist/protocols/base.d.ts +33 -0
- package/dist/protocols/base.js +2 -0
- package/dist/protocols/rest/handler.d.ts +67 -0
- package/dist/protocols/rest/handler.js +776 -0
- package/dist/protocols/soap/handler.d.ts +12 -0
- package/dist/protocols/soap/handler.js +165 -0
- package/dist/protocols/web/core-web-vitals.d.ts +121 -0
- package/dist/protocols/web/core-web-vitals.js +373 -0
- package/dist/protocols/web/handler.d.ts +50 -0
- package/dist/protocols/web/handler.js +706 -0
- package/dist/recorder/native-recorder.d.ts +14 -0
- package/dist/recorder/native-recorder.js +533 -0
- package/dist/recorder/scenario-recorder.d.ts +55 -0
- package/dist/recorder/scenario-recorder.js +296 -0
- package/dist/reporting/constants.d.ts +94 -0
- package/dist/reporting/constants.js +82 -0
- package/dist/reporting/enhanced-html-generator.d.ts +55 -0
- package/dist/reporting/enhanced-html-generator.js +965 -0
- package/dist/reporting/generator.d.ts +42 -0
- package/dist/reporting/generator.js +1217 -0
- package/dist/reporting/statistics.d.ts +144 -0
- package/dist/reporting/statistics.js +742 -0
- package/dist/reporting/templates/enhanced-report.hbs +2812 -0
- package/dist/reporting/templates/html.hbs +2453 -0
- package/dist/utils/faker-manager.d.ts +55 -0
- package/dist/utils/faker-manager.js +166 -0
- package/dist/utils/file-manager.d.ts +33 -0
- package/dist/utils/file-manager.js +154 -0
- package/dist/utils/handlebars-manager.d.ts +42 -0
- package/dist/utils/handlebars-manager.js +172 -0
- package/dist/utils/logger.d.ts +16 -0
- package/dist/utils/logger.js +46 -0
- package/dist/utils/template.d.ts +80 -0
- package/dist/utils/template.js +513 -0
- package/dist/utils/test-output-writer.d.ts +56 -0
- package/dist/utils/test-output-writer.js +643 -0
- package/dist/utils/time.d.ts +3 -0
- package/dist/utils/time.js +23 -0
- package/dist/utils/timestamp-helper.d.ts +17 -0
- package/dist/utils/timestamp-helper.js +53 -0
- package/dist/workers/manager.d.ts +18 -0
- package/dist/workers/manager.js +95 -0
- package/dist/workers/server.d.ts +21 -0
- package/dist/workers/server.js +205 -0
- package/dist/workers/worker.d.ts +19 -0
- package/dist/workers/worker.js +147 -0
- package/package.json +102 -0
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WebHandler = void 0;
|
|
4
|
+
const playwright_1 = require("playwright");
|
|
5
|
+
const logger_1 = require("../../utils/logger");
|
|
6
|
+
const core_web_vitals_1 = require("./core-web-vitals");
|
|
7
|
+
class WebHandler {
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.browsers = new Map();
|
|
10
|
+
this.contexts = new Map();
|
|
11
|
+
this.pages = new Map();
|
|
12
|
+
this.verificationMetrics = new Map();
|
|
13
|
+
this.config = {
|
|
14
|
+
...config,
|
|
15
|
+
type: config.type || 'chromium',
|
|
16
|
+
headless: config.headless !== undefined ? config.headless : true
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
async initialize() {
|
|
20
|
+
logger_1.logger.debug(`Enhanced WebHandler initialized - Core Web Vitals tracking enabled (type: ${this.config.type}, headless: ${this.config.headless})`);
|
|
21
|
+
}
|
|
22
|
+
async execute(action, context) {
|
|
23
|
+
try {
|
|
24
|
+
logger_1.logger.info(`🎬 WebHandler.execute: command="${action.command}", selector="${action.selector || 'N/A'}", url="${action.url || 'N/A'}"`);
|
|
25
|
+
const page = await this.getPage(context.vu_id);
|
|
26
|
+
let result;
|
|
27
|
+
let verificationMetrics;
|
|
28
|
+
let webVitals;
|
|
29
|
+
let performanceMetrics;
|
|
30
|
+
// Always inject Web Vitals collector for browser tests
|
|
31
|
+
await core_web_vitals_1.CoreWebVitalsCollector.injectVitalsCollector(page);
|
|
32
|
+
switch (action.command) {
|
|
33
|
+
case 'goto':
|
|
34
|
+
{
|
|
35
|
+
const actionStart = Date.now();
|
|
36
|
+
result = await this.handleGoto(page, action);
|
|
37
|
+
result.action_time = Date.now() - actionStart;
|
|
38
|
+
// Collect Web Vitals after navigation (optional, doesn't affect action_time)
|
|
39
|
+
if (action.collectWebVitals !== false) {
|
|
40
|
+
webVitals = await core_web_vitals_1.CoreWebVitalsCollector.collectVitals(page, action.webVitalsWaitTime || 1000);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
break;
|
|
44
|
+
case 'click':
|
|
45
|
+
{
|
|
46
|
+
const actionStart = Date.now();
|
|
47
|
+
if (action.measureVerification) {
|
|
48
|
+
const measured = await core_web_vitals_1.VerificationMetricsCollector.measureVerificationStep(action.verificationName || 'click_action', 'click', () => this.handleClick(page, action), { selector: action.selector });
|
|
49
|
+
result = measured.result;
|
|
50
|
+
verificationMetrics = measured.metrics;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
result = await this.handleClick(page, action);
|
|
54
|
+
}
|
|
55
|
+
result.action_time = Date.now() - actionStart;
|
|
56
|
+
// Only collect Web Vitals if explicitly requested
|
|
57
|
+
if (action.collectWebVitals) {
|
|
58
|
+
webVitals = await core_web_vitals_1.CoreWebVitalsCollector.collectVitals(page, action.webVitalsWaitTime || 1000);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
case 'fill':
|
|
63
|
+
{
|
|
64
|
+
const actionStart = Date.now();
|
|
65
|
+
if (action.measureVerification) {
|
|
66
|
+
const measured = await core_web_vitals_1.VerificationMetricsCollector.measureVerificationStep(action.verificationName || 'fill_action', 'fill', () => this.handleFill(page, action), { selector: action.selector, expected_text: action.value });
|
|
67
|
+
result = measured.result;
|
|
68
|
+
verificationMetrics = measured.metrics;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
result = await this.handleFill(page, action);
|
|
72
|
+
}
|
|
73
|
+
result.action_time = Date.now() - actionStart;
|
|
74
|
+
// Only collect Web Vitals if explicitly requested
|
|
75
|
+
if (action.collectWebVitals) {
|
|
76
|
+
webVitals = await core_web_vitals_1.CoreWebVitalsCollector.collectVitals(page, action.webVitalsWaitTime || 1000);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
case 'select':
|
|
81
|
+
{
|
|
82
|
+
const actionStart = Date.now();
|
|
83
|
+
result = await this.handleSelect(page, action);
|
|
84
|
+
result.action_time = Date.now() - actionStart;
|
|
85
|
+
}
|
|
86
|
+
break;
|
|
87
|
+
case 'press':
|
|
88
|
+
{
|
|
89
|
+
const actionStart = Date.now();
|
|
90
|
+
result = await this.handlePress(page, action);
|
|
91
|
+
result.action_time = Date.now() - actionStart;
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
case 'wait_for_selector':
|
|
95
|
+
{
|
|
96
|
+
const actionStart = Date.now();
|
|
97
|
+
if (action.measureVerification) {
|
|
98
|
+
const measured = await core_web_vitals_1.VerificationMetricsCollector.measureVerificationStep(action.verificationName || 'wait_for_selector', 'wait', () => this.handleWaitForSelector(page, action), { selector: action.selector });
|
|
99
|
+
result = measured.result;
|
|
100
|
+
verificationMetrics = measured.metrics;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
result = await this.handleWaitForSelector(page, action);
|
|
104
|
+
}
|
|
105
|
+
result.action_time = Date.now() - actionStart;
|
|
106
|
+
}
|
|
107
|
+
break;
|
|
108
|
+
case 'verify_exists':
|
|
109
|
+
const measured = await core_web_vitals_1.VerificationMetricsCollector.measureVerificationStep(action.verificationName || action.name || 'verify_exists', 'verification', () => this.handleVerifyExists(page, action), { selector: action.selector });
|
|
110
|
+
result = measured.result;
|
|
111
|
+
verificationMetrics = measured.metrics;
|
|
112
|
+
break;
|
|
113
|
+
case 'verify_visible':
|
|
114
|
+
const visibleMeasured = await core_web_vitals_1.VerificationMetricsCollector.measureVerificationStep(action.verificationName || action.name || 'verify_visible', 'verification', () => this.handleVerifyVisible(page, action), { selector: action.selector });
|
|
115
|
+
result = visibleMeasured.result;
|
|
116
|
+
verificationMetrics = visibleMeasured.metrics;
|
|
117
|
+
break;
|
|
118
|
+
case 'verify_text':
|
|
119
|
+
const textMeasured = await core_web_vitals_1.VerificationMetricsCollector.measureVerificationStep(action.verificationName || action.name || 'verify_text', 'verification', () => this.handleVerifyText(page, action), { selector: action.selector, expected_text: action.expected_text });
|
|
120
|
+
result = textMeasured.result;
|
|
121
|
+
verificationMetrics = textMeasured.metrics;
|
|
122
|
+
break;
|
|
123
|
+
case 'verify_contains':
|
|
124
|
+
const containsMeasured = await core_web_vitals_1.VerificationMetricsCollector.measureVerificationStep(action.verificationName || action.name || 'verify_contains', 'verification', () => this.handleVerifyContains(page, action), { selector: action.selector, expected_text: action.value });
|
|
125
|
+
result = containsMeasured.result;
|
|
126
|
+
verificationMetrics = containsMeasured.metrics;
|
|
127
|
+
break;
|
|
128
|
+
case 'verify_not_exists':
|
|
129
|
+
const notExistsMeasured = await core_web_vitals_1.VerificationMetricsCollector.measureVerificationStep(action.verificationName || action.name || 'verify_not_exists', 'verification', () => this.handleVerifyNotExists(page, action), { selector: action.selector });
|
|
130
|
+
result = notExistsMeasured.result;
|
|
131
|
+
verificationMetrics = notExistsMeasured.metrics;
|
|
132
|
+
break;
|
|
133
|
+
case 'measure_web_vitals':
|
|
134
|
+
webVitals = await core_web_vitals_1.CoreWebVitalsCollector.collectVitals(page, action.webVitalsWaitTime || 3000);
|
|
135
|
+
result = { web_vitals: webVitals };
|
|
136
|
+
break;
|
|
137
|
+
case 'performance_audit':
|
|
138
|
+
performanceMetrics = await core_web_vitals_1.WebPerformanceCollector.collectAllMetrics(page, verificationMetrics);
|
|
139
|
+
result = { performance_audit: performanceMetrics };
|
|
140
|
+
break;
|
|
141
|
+
case 'wait_for_load_state':
|
|
142
|
+
const waitUntil = action.waitUntil === 'commit' ? 'load' : (action.waitUntil || 'load');
|
|
143
|
+
await page.waitForLoadState(waitUntil, { timeout: action.timeout || 30000 });
|
|
144
|
+
result = { load_state: waitUntil };
|
|
145
|
+
break;
|
|
146
|
+
case 'network_idle':
|
|
147
|
+
await page.waitForLoadState('networkidle', { timeout: action.networkIdleTimeout || 30000 });
|
|
148
|
+
result = { network_idle: true };
|
|
149
|
+
break;
|
|
150
|
+
case 'dom_ready':
|
|
151
|
+
await page.waitForLoadState('domcontentloaded', { timeout: action.timeout || 30000 });
|
|
152
|
+
result = { dom_ready: true };
|
|
153
|
+
break;
|
|
154
|
+
case 'screenshot':
|
|
155
|
+
const screenshot = await page.screenshot({
|
|
156
|
+
type: 'png',
|
|
157
|
+
fullPage: action.options?.fullPage || false
|
|
158
|
+
});
|
|
159
|
+
result = {
|
|
160
|
+
screenshot: screenshot.length,
|
|
161
|
+
screenshot_data: action.options?.includeData ? screenshot.toString('base64') : undefined
|
|
162
|
+
};
|
|
163
|
+
if (verificationMetrics) {
|
|
164
|
+
verificationMetrics.screenshot_size = screenshot.length;
|
|
165
|
+
}
|
|
166
|
+
break;
|
|
167
|
+
default:
|
|
168
|
+
// Fall back to original handler methods for backward compatibility
|
|
169
|
+
result = await this.handleLegacyAction(page, action, context);
|
|
170
|
+
}
|
|
171
|
+
// Store verification metrics for later analysis
|
|
172
|
+
if (verificationMetrics) {
|
|
173
|
+
const vuMetrics = this.verificationMetrics.get(context.vu_id) || [];
|
|
174
|
+
vuMetrics.push(verificationMetrics);
|
|
175
|
+
this.verificationMetrics.set(context.vu_id, vuMetrics);
|
|
176
|
+
}
|
|
177
|
+
// Evaluate Web Vitals if collected
|
|
178
|
+
let vitalsScore;
|
|
179
|
+
let vitalsDetails;
|
|
180
|
+
if (webVitals) {
|
|
181
|
+
const evaluation = core_web_vitals_1.CoreWebVitalsCollector.evaluateVitals(webVitals, action.webVitalsThresholds);
|
|
182
|
+
vitalsScore = evaluation.score;
|
|
183
|
+
vitalsDetails = evaluation.details;
|
|
184
|
+
}
|
|
185
|
+
// Use action_time if available (actual action duration without web vitals collection)
|
|
186
|
+
// Or verification metrics duration for verify steps
|
|
187
|
+
const responseTime = result?.action_time || verificationMetrics?.duration;
|
|
188
|
+
// Only record metrics for meaningful performance measurements:
|
|
189
|
+
// - Verifications (verify_*) - time for elements/text to appear (measures app responsiveness)
|
|
190
|
+
// - Waits (wait_for_*) - time for conditions to be met
|
|
191
|
+
// - Performance measurements (measure_*, performance_audit)
|
|
192
|
+
// NOT recorded: goto, click, fill, press, select, hover, screenshot (navigation/interactions)
|
|
193
|
+
const measurableCommands = [
|
|
194
|
+
'verify_exists', 'verify_visible', 'verify_text', 'verify_contains', 'verify_not_exists',
|
|
195
|
+
'wait_for_selector', 'wait_for_text',
|
|
196
|
+
'measure_web_vitals', 'performance_audit'
|
|
197
|
+
];
|
|
198
|
+
const shouldRecord = measurableCommands.includes(action.command);
|
|
199
|
+
const enhancedResult = {
|
|
200
|
+
success: true,
|
|
201
|
+
data: result,
|
|
202
|
+
shouldRecord, // Only record verifications and navigations for meaningful metrics
|
|
203
|
+
response_time: responseTime,
|
|
204
|
+
custom_metrics: {
|
|
205
|
+
page_url: page.url(),
|
|
206
|
+
page_title: await page.title(),
|
|
207
|
+
vu_id: context.vu_id,
|
|
208
|
+
command: action.command,
|
|
209
|
+
action_time: result?.action_time,
|
|
210
|
+
web_vitals: webVitals,
|
|
211
|
+
vitals_score: vitalsScore,
|
|
212
|
+
vitals_details: vitalsDetails,
|
|
213
|
+
verification_metrics: verificationMetrics,
|
|
214
|
+
performance_metrics: performanceMetrics
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
return enhancedResult;
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
return {
|
|
221
|
+
success: false,
|
|
222
|
+
error: error.message,
|
|
223
|
+
shouldRecord: true, // Record errors too for analysis
|
|
224
|
+
custom_metrics: {
|
|
225
|
+
vu_id: context.vu_id,
|
|
226
|
+
error_type: error.constructor.name,
|
|
227
|
+
error_stack: error.stack?.split('\n').slice(0, 3).join('; ')
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async handleGoto(page, action) {
|
|
233
|
+
const fullUrl = action.url?.startsWith('http')
|
|
234
|
+
? action.url
|
|
235
|
+
: `${this.config.base_url}${action.url || ''}`;
|
|
236
|
+
const response = await page.goto(fullUrl, {
|
|
237
|
+
timeout: action.timeout || 30000,
|
|
238
|
+
waitUntil: action.waitUntil || 'domcontentloaded'
|
|
239
|
+
});
|
|
240
|
+
return {
|
|
241
|
+
url: page.url(),
|
|
242
|
+
status: response?.status(),
|
|
243
|
+
headers: await response?.allHeaders(),
|
|
244
|
+
loading_time: Date.now() - performance.now()
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
async handleClick(page, action) {
|
|
248
|
+
const timeout = action.timeout || 30000;
|
|
249
|
+
const selector = action.selector;
|
|
250
|
+
// Wait for the element to be visible and stable before clicking
|
|
251
|
+
await page.waitForSelector(selector, {
|
|
252
|
+
state: 'visible',
|
|
253
|
+
timeout
|
|
254
|
+
});
|
|
255
|
+
// Highlight element before clicking (if enabled)
|
|
256
|
+
await this.highlightElement(page, selector);
|
|
257
|
+
// Click using locator for reliability
|
|
258
|
+
await page.locator(selector).click({ timeout });
|
|
259
|
+
return { clicked: selector };
|
|
260
|
+
}
|
|
261
|
+
async handleFill(page, action) {
|
|
262
|
+
const timeout = action.timeout || 30000;
|
|
263
|
+
// Wait for the element to be visible before filling
|
|
264
|
+
await page.waitForSelector(action.selector, {
|
|
265
|
+
state: 'visible',
|
|
266
|
+
timeout
|
|
267
|
+
});
|
|
268
|
+
// Highlight element before filling (if enabled)
|
|
269
|
+
await this.highlightElement(page, action.selector);
|
|
270
|
+
await page.locator(action.selector).fill(action.value, { timeout });
|
|
271
|
+
return { filled: action.selector, value: action.value };
|
|
272
|
+
}
|
|
273
|
+
async handleSelect(page, action) {
|
|
274
|
+
const timeout = action.timeout || 30000;
|
|
275
|
+
await page.waitForSelector(action.selector, {
|
|
276
|
+
state: 'visible',
|
|
277
|
+
timeout
|
|
278
|
+
});
|
|
279
|
+
// Highlight element before selecting (if enabled)
|
|
280
|
+
await this.highlightElement(page, action.selector);
|
|
281
|
+
await page.locator(action.selector).selectOption(action.value, { timeout });
|
|
282
|
+
return { selected: action.selector, value: action.value };
|
|
283
|
+
}
|
|
284
|
+
async handlePress(page, action) {
|
|
285
|
+
const timeout = action.timeout || 30000;
|
|
286
|
+
const key = action.key;
|
|
287
|
+
if (action.selector) {
|
|
288
|
+
// Press key on specific element
|
|
289
|
+
await page.waitForSelector(action.selector, {
|
|
290
|
+
state: 'visible',
|
|
291
|
+
timeout
|
|
292
|
+
});
|
|
293
|
+
// Highlight element before pressing (if enabled)
|
|
294
|
+
await this.highlightElement(page, action.selector);
|
|
295
|
+
await page.locator(action.selector).press(key, { timeout });
|
|
296
|
+
return { pressed: key, selector: action.selector };
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
// Press key globally (on the page)
|
|
300
|
+
await page.keyboard.press(key);
|
|
301
|
+
return { pressed: key };
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async handleWaitForSelector(page, action) {
|
|
305
|
+
await page.waitForSelector(action.selector, {
|
|
306
|
+
timeout: action.timeout || 30000
|
|
307
|
+
});
|
|
308
|
+
return { waited_for: action.selector };
|
|
309
|
+
}
|
|
310
|
+
async handleVerifyExists(page, action) {
|
|
311
|
+
await page.waitForSelector(action.selector, {
|
|
312
|
+
state: 'attached',
|
|
313
|
+
timeout: action.timeout || 30000
|
|
314
|
+
});
|
|
315
|
+
const elementCount = await page.locator(action.selector).count();
|
|
316
|
+
return {
|
|
317
|
+
verified: 'exists',
|
|
318
|
+
selector: action.selector,
|
|
319
|
+
name: action.name,
|
|
320
|
+
found_elements: elementCount,
|
|
321
|
+
element_count: elementCount
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
async handleVerifyVisible(page, action) {
|
|
325
|
+
await page.waitForSelector(action.selector, {
|
|
326
|
+
state: 'visible',
|
|
327
|
+
timeout: action.timeout || 30000
|
|
328
|
+
});
|
|
329
|
+
return {
|
|
330
|
+
verified: 'visible',
|
|
331
|
+
selector: action.selector,
|
|
332
|
+
name: action.name,
|
|
333
|
+
is_visible: true
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
async handleVerifyText(page, action) {
|
|
337
|
+
await page.waitForSelector(action.selector, {
|
|
338
|
+
state: 'attached',
|
|
339
|
+
timeout: action.timeout || 30000
|
|
340
|
+
});
|
|
341
|
+
const textLocator = page.locator(action.selector);
|
|
342
|
+
const actualText = await textLocator.textContent();
|
|
343
|
+
const expectedText = action.expected_text;
|
|
344
|
+
if (!actualText || !actualText.includes(expectedText)) {
|
|
345
|
+
throw new Error(`Verification failed: Element "${action.selector}" text "${actualText}" does not contain expected text "${expectedText}"${action.name ? ` (${action.name})` : ''}`);
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
verified: 'text',
|
|
349
|
+
selector: action.selector,
|
|
350
|
+
name: action.name,
|
|
351
|
+
expected_text: expectedText,
|
|
352
|
+
actual_text: actualText,
|
|
353
|
+
text_match: true
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
async handleVerifyContains(page, action) {
|
|
357
|
+
await page.waitForSelector(action.selector, {
|
|
358
|
+
state: 'attached',
|
|
359
|
+
timeout: action.timeout || 30000
|
|
360
|
+
});
|
|
361
|
+
const textLocator = page.locator(action.selector);
|
|
362
|
+
const actualText = await textLocator.textContent();
|
|
363
|
+
const expectedText = action.value;
|
|
364
|
+
if (!actualText || !actualText.includes(expectedText)) {
|
|
365
|
+
throw new Error(`Verification failed: Element "${action.selector}" text "${actualText}" does not contain "${expectedText}"${action.name ? ` (${action.name})` : ''}`);
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
verified: 'contains',
|
|
369
|
+
selector: action.selector,
|
|
370
|
+
name: action.name,
|
|
371
|
+
expected_text: expectedText,
|
|
372
|
+
actual_text: actualText,
|
|
373
|
+
text_match: true
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
async handleVerifyNotExists(page, action) {
|
|
377
|
+
try {
|
|
378
|
+
await page.waitForSelector(action.selector, {
|
|
379
|
+
state: 'detached',
|
|
380
|
+
timeout: action.timeout || 5000
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
const count = await page.locator(action.selector).count();
|
|
385
|
+
if (count > 0) {
|
|
386
|
+
throw new Error(`Verification failed: Element "${action.selector}" exists but should not exist${action.name ? ` (${action.name})` : ''}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
verified: 'not_exists',
|
|
391
|
+
selector: action.selector,
|
|
392
|
+
name: action.name,
|
|
393
|
+
found_elements: 0
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
async handleLegacyAction(page, action, context) {
|
|
397
|
+
// Handle any legacy actions not covered by new implementation
|
|
398
|
+
switch (action.command) {
|
|
399
|
+
case 'evaluate':
|
|
400
|
+
if (action.script) {
|
|
401
|
+
const result = await page.evaluate(action.script);
|
|
402
|
+
return { evaluation_result: result };
|
|
403
|
+
}
|
|
404
|
+
break;
|
|
405
|
+
case 'hover':
|
|
406
|
+
await page.hover(action.selector);
|
|
407
|
+
return { hovered: action.selector };
|
|
408
|
+
default:
|
|
409
|
+
throw new Error(`Unsupported web action: ${action.command}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
async getPage(vuId) {
|
|
413
|
+
let page = this.pages.get(vuId);
|
|
414
|
+
if (!page) {
|
|
415
|
+
const browser = await this.createBrowserForVU(vuId);
|
|
416
|
+
this.browsers.set(vuId, browser);
|
|
417
|
+
const context = await browser.newContext({
|
|
418
|
+
viewport: this.config.viewport || { width: 1280, height: 720 },
|
|
419
|
+
ignoreHTTPSErrors: true,
|
|
420
|
+
storageState: undefined
|
|
421
|
+
});
|
|
422
|
+
page = await context.newPage();
|
|
423
|
+
page.setDefaultTimeout(30000);
|
|
424
|
+
page.setDefaultNavigationTimeout(30000);
|
|
425
|
+
// Clear storage if configured
|
|
426
|
+
await this.clearStorageIfConfigured(page, context);
|
|
427
|
+
this.contexts.set(vuId, context);
|
|
428
|
+
this.pages.set(vuId, page);
|
|
429
|
+
logger_1.logger.debug(`VU ${vuId}: Created enhanced browser with Web Vitals support`);
|
|
430
|
+
}
|
|
431
|
+
return page;
|
|
432
|
+
}
|
|
433
|
+
async createBrowserForVU(vuId) {
|
|
434
|
+
const browserType = this.config.type || 'chromium';
|
|
435
|
+
const launchOptions = {
|
|
436
|
+
headless: this.config.headless !== false,
|
|
437
|
+
slowMo: this.config.slow_mo || 0,
|
|
438
|
+
args: [
|
|
439
|
+
'--no-sandbox',
|
|
440
|
+
'--disable-dev-shm-usage',
|
|
441
|
+
'--disable-web-security',
|
|
442
|
+
'--disable-features=VizDisplayCompositor',
|
|
443
|
+
'--disable-http-cache',
|
|
444
|
+
'--disable-cache',
|
|
445
|
+
'--disable-application-cache',
|
|
446
|
+
'--disable-offline-load-stale-cache',
|
|
447
|
+
'--disk-cache-size=0',
|
|
448
|
+
'--media-cache-size=0',
|
|
449
|
+
'--no-first-run',
|
|
450
|
+
'--no-default-browser-check',
|
|
451
|
+
'--disable-background-networking',
|
|
452
|
+
'--disable-sync',
|
|
453
|
+
'--metrics-recording-only',
|
|
454
|
+
'--mute-audio',
|
|
455
|
+
'--disable-renderer-backgrounding',
|
|
456
|
+
// Enable performance monitoring
|
|
457
|
+
'--enable-precise-memory-info',
|
|
458
|
+
'--enable-performance-manager-web-contents-observer',
|
|
459
|
+
'--enable-experimental-web-platform-features'
|
|
460
|
+
]
|
|
461
|
+
};
|
|
462
|
+
let browser;
|
|
463
|
+
try {
|
|
464
|
+
switch (browserType) {
|
|
465
|
+
case 'chromium':
|
|
466
|
+
browser = await playwright_1.chromium.launch(launchOptions);
|
|
467
|
+
break;
|
|
468
|
+
case 'firefox':
|
|
469
|
+
browser = await playwright_1.firefox.launch(launchOptions);
|
|
470
|
+
break;
|
|
471
|
+
case 'webkit':
|
|
472
|
+
browser = await playwright_1.webkit.launch(launchOptions);
|
|
473
|
+
break;
|
|
474
|
+
default:
|
|
475
|
+
throw new Error(`Unsupported browser type: ${browserType}`);
|
|
476
|
+
}
|
|
477
|
+
logger_1.logger.debug(`VU ${vuId}: Launched enhanced ${browserType} browser with Web Vitals support`);
|
|
478
|
+
return browser;
|
|
479
|
+
}
|
|
480
|
+
catch (error) {
|
|
481
|
+
logger_1.logger.error(`VU ${vuId}: Failed to launch ${browserType} browser:`, error);
|
|
482
|
+
throw error;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// Get verification metrics for a specific VU
|
|
486
|
+
getVerificationMetrics(vuId) {
|
|
487
|
+
return this.verificationMetrics.get(vuId) || [];
|
|
488
|
+
}
|
|
489
|
+
// Get aggregated verification metrics across all VUs
|
|
490
|
+
getAggregatedVerificationMetrics() {
|
|
491
|
+
const allMetrics = [];
|
|
492
|
+
for (const metrics of this.verificationMetrics.values()) {
|
|
493
|
+
allMetrics.push(...metrics);
|
|
494
|
+
}
|
|
495
|
+
if (allMetrics.length === 0) {
|
|
496
|
+
return {
|
|
497
|
+
total_verifications: 0,
|
|
498
|
+
success_rate: 0,
|
|
499
|
+
average_duration: 0,
|
|
500
|
+
p95_duration: 0,
|
|
501
|
+
slowest_step: null,
|
|
502
|
+
fastest_step: null
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
const successful = allMetrics.filter(m => m.success);
|
|
506
|
+
const durations = allMetrics.map(m => m.duration).sort((a, b) => a - b);
|
|
507
|
+
return {
|
|
508
|
+
total_verifications: allMetrics.length,
|
|
509
|
+
success_rate: successful.length / allMetrics.length,
|
|
510
|
+
average_duration: durations.reduce((a, b) => a + b, 0) / durations.length,
|
|
511
|
+
p95_duration: durations[Math.floor(durations.length * 0.95)],
|
|
512
|
+
slowest_step: allMetrics.reduce((prev, current) => prev.duration > current.duration ? prev : current),
|
|
513
|
+
fastest_step: allMetrics.reduce((prev, current) => prev.duration < current.duration ? prev : current)
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
async cleanup() {
|
|
517
|
+
const browserCount = this.browsers.size;
|
|
518
|
+
logger_1.logger.debug(`Cleaning up ${browserCount} enhanced browsers with Web Vitals data...`);
|
|
519
|
+
// Log final verification metrics summary
|
|
520
|
+
const aggregatedMetrics = this.getAggregatedVerificationMetrics();
|
|
521
|
+
logger_1.logger.info('Final verification metrics summary:', aggregatedMetrics);
|
|
522
|
+
// Close all resources
|
|
523
|
+
for (const [vuId, page] of this.pages) {
|
|
524
|
+
try {
|
|
525
|
+
if (!page.isClosed()) {
|
|
526
|
+
await page.close();
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
logger_1.logger.warn(`VU ${vuId}: Error closing page:`, error);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
for (const [vuId, context] of this.contexts) {
|
|
534
|
+
try {
|
|
535
|
+
await context.close();
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
logger_1.logger.warn(`VU ${vuId}: Error closing context:`, error);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
for (const [vuId, browser] of this.browsers) {
|
|
542
|
+
try {
|
|
543
|
+
if (browser.isConnected()) {
|
|
544
|
+
await browser.close();
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
logger_1.logger.warn(`VU ${vuId}: Error closing browser:`, error);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
this.pages.clear();
|
|
552
|
+
this.contexts.clear();
|
|
553
|
+
this.browsers.clear();
|
|
554
|
+
this.verificationMetrics.clear();
|
|
555
|
+
logger_1.logger.debug(`Enhanced cleanup completed - ${browserCount} browsers closed`);
|
|
556
|
+
}
|
|
557
|
+
async cleanupVU(vuId) {
|
|
558
|
+
logger_1.logger.debug(`Cleaning up enhanced browser resources for VU ${vuId}...`);
|
|
559
|
+
const page = this.pages.get(vuId);
|
|
560
|
+
if (page) {
|
|
561
|
+
try {
|
|
562
|
+
if (!page.isClosed()) {
|
|
563
|
+
await page.close();
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
catch (error) {
|
|
567
|
+
logger_1.logger.warn(`VU ${vuId}: Error closing page:`, error);
|
|
568
|
+
}
|
|
569
|
+
this.pages.delete(vuId);
|
|
570
|
+
}
|
|
571
|
+
const context = this.contexts.get(vuId);
|
|
572
|
+
if (context) {
|
|
573
|
+
try {
|
|
574
|
+
await context.close();
|
|
575
|
+
}
|
|
576
|
+
catch (error) {
|
|
577
|
+
// Ignore "context already closed" errors - this is expected when browser closes first
|
|
578
|
+
if (!error?.message?.includes('Failed to find context') &&
|
|
579
|
+
!error?.message?.includes('Target closed') &&
|
|
580
|
+
!error?.message?.includes('has been closed')) {
|
|
581
|
+
logger_1.logger.warn(`VU ${vuId}: Error closing context:`, error);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
this.contexts.delete(vuId);
|
|
585
|
+
}
|
|
586
|
+
const browser = this.browsers.get(vuId);
|
|
587
|
+
if (browser) {
|
|
588
|
+
try {
|
|
589
|
+
if (browser.isConnected()) {
|
|
590
|
+
await browser.close();
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
catch (error) {
|
|
594
|
+
logger_1.logger.warn(`VU ${vuId}: Error closing browser:`, error);
|
|
595
|
+
}
|
|
596
|
+
this.browsers.delete(vuId);
|
|
597
|
+
}
|
|
598
|
+
// Clean up verification metrics
|
|
599
|
+
this.verificationMetrics.delete(vuId);
|
|
600
|
+
logger_1.logger.debug(`VU ${vuId}: Enhanced browser cleanup completed`);
|
|
601
|
+
}
|
|
602
|
+
getBrowserInfo(vuId) {
|
|
603
|
+
const browser = this.browsers.get(vuId);
|
|
604
|
+
if (!browser)
|
|
605
|
+
return null;
|
|
606
|
+
return {
|
|
607
|
+
connected: browser.isConnected()
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
getActiveVUCount() {
|
|
611
|
+
return this.browsers.size;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Clear browser storage (localStorage, sessionStorage, cookies) if configured
|
|
615
|
+
*/
|
|
616
|
+
async clearStorageIfConfigured(page, context) {
|
|
617
|
+
const clearConfig = this.config.clear_storage;
|
|
618
|
+
if (!clearConfig)
|
|
619
|
+
return;
|
|
620
|
+
const config = typeof clearConfig === 'boolean'
|
|
621
|
+
? { local_storage: true, session_storage: true, cookies: true, cache: false }
|
|
622
|
+
: clearConfig;
|
|
623
|
+
try {
|
|
624
|
+
// We need to navigate to a page first to access storage
|
|
625
|
+
// Use about:blank or a data URL
|
|
626
|
+
await page.goto('about:blank');
|
|
627
|
+
// Clear cookies
|
|
628
|
+
if (config.cookies !== false) {
|
|
629
|
+
await context.clearCookies();
|
|
630
|
+
logger_1.logger.debug('Cleared cookies');
|
|
631
|
+
}
|
|
632
|
+
// Clear localStorage and sessionStorage
|
|
633
|
+
if (config.local_storage !== false || config.session_storage !== false) {
|
|
634
|
+
await page.evaluate((opts) => {
|
|
635
|
+
if (opts.local_storage !== false) {
|
|
636
|
+
try {
|
|
637
|
+
localStorage.clear();
|
|
638
|
+
}
|
|
639
|
+
catch (e) { /* ignore */ }
|
|
640
|
+
}
|
|
641
|
+
if (opts.session_storage !== false) {
|
|
642
|
+
try {
|
|
643
|
+
sessionStorage.clear();
|
|
644
|
+
}
|
|
645
|
+
catch (e) { /* ignore */ }
|
|
646
|
+
}
|
|
647
|
+
}, { local_storage: config.local_storage, session_storage: config.session_storage });
|
|
648
|
+
if (config.local_storage !== false)
|
|
649
|
+
logger_1.logger.debug('Cleared localStorage');
|
|
650
|
+
if (config.session_storage !== false)
|
|
651
|
+
logger_1.logger.debug('Cleared sessionStorage');
|
|
652
|
+
}
|
|
653
|
+
logger_1.logger.debug('Browser storage cleared');
|
|
654
|
+
}
|
|
655
|
+
catch (error) {
|
|
656
|
+
logger_1.logger.warn('Failed to clear storage:', error);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Highlight an element before interacting with it (for debugging)
|
|
661
|
+
*/
|
|
662
|
+
async highlightElement(page, selector) {
|
|
663
|
+
const highlightConfig = this.config.highlight;
|
|
664
|
+
// Skip if highlight is not enabled
|
|
665
|
+
if (!highlightConfig)
|
|
666
|
+
return;
|
|
667
|
+
const config = typeof highlightConfig === 'boolean'
|
|
668
|
+
? { enabled: highlightConfig }
|
|
669
|
+
: highlightConfig;
|
|
670
|
+
if (!config.enabled)
|
|
671
|
+
return;
|
|
672
|
+
const duration = config.duration || 500;
|
|
673
|
+
const color = config.color || '#ff0000';
|
|
674
|
+
const style = config.style || 'border';
|
|
675
|
+
try {
|
|
676
|
+
const locator = page.locator(selector).first();
|
|
677
|
+
const count = await locator.count();
|
|
678
|
+
if (count === 0)
|
|
679
|
+
return;
|
|
680
|
+
// Apply highlight styles
|
|
681
|
+
await locator.evaluate((el, opts) => {
|
|
682
|
+
const { color, style, duration } = opts;
|
|
683
|
+
const originalStyle = el.getAttribute('style') || '';
|
|
684
|
+
let highlightStyle = '';
|
|
685
|
+
if (style === 'border' || style === 'both') {
|
|
686
|
+
highlightStyle += `outline: 3px solid ${color} !important; outline-offset: 2px !important;`;
|
|
687
|
+
}
|
|
688
|
+
if (style === 'background' || style === 'both') {
|
|
689
|
+
highlightStyle += `background-color: ${color}33 !important;`;
|
|
690
|
+
}
|
|
691
|
+
el.setAttribute('style', originalStyle + highlightStyle);
|
|
692
|
+
// Restore original style after duration
|
|
693
|
+
setTimeout(() => {
|
|
694
|
+
el.setAttribute('style', originalStyle);
|
|
695
|
+
}, duration);
|
|
696
|
+
}, { color, style, duration });
|
|
697
|
+
// Wait for highlight to be visible
|
|
698
|
+
await page.waitForTimeout(duration);
|
|
699
|
+
}
|
|
700
|
+
catch (error) {
|
|
701
|
+
// Don't fail the test if highlighting fails
|
|
702
|
+
logger_1.logger.debug(`Failed to highlight element ${selector}:`, error);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
exports.WebHandler = WebHandler;
|