@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.
Files changed (164) hide show
  1. package/README.md +360 -0
  2. package/dist/cli/cli.d.ts +2 -0
  3. package/dist/cli/cli.js +192 -0
  4. package/dist/cli/commands/distributed.d.ts +11 -0
  5. package/dist/cli/commands/distributed.js +179 -0
  6. package/dist/cli/commands/import.d.ts +23 -0
  7. package/dist/cli/commands/import.js +461 -0
  8. package/dist/cli/commands/init.d.ts +7 -0
  9. package/dist/cli/commands/init.js +923 -0
  10. package/dist/cli/commands/mock.d.ts +7 -0
  11. package/dist/cli/commands/mock.js +281 -0
  12. package/dist/cli/commands/report.d.ts +5 -0
  13. package/dist/cli/commands/report.js +70 -0
  14. package/dist/cli/commands/run.d.ts +12 -0
  15. package/dist/cli/commands/run.js +260 -0
  16. package/dist/cli/commands/validate.d.ts +3 -0
  17. package/dist/cli/commands/validate.js +35 -0
  18. package/dist/cli/commands/worker.d.ts +27 -0
  19. package/dist/cli/commands/worker.js +320 -0
  20. package/dist/config/index.d.ts +2 -0
  21. package/dist/config/index.js +20 -0
  22. package/dist/config/parser.d.ts +19 -0
  23. package/dist/config/parser.js +330 -0
  24. package/dist/config/types/global-config.d.ts +74 -0
  25. package/dist/config/types/global-config.js +2 -0
  26. package/dist/config/types/hooks.d.ts +58 -0
  27. package/dist/config/types/hooks.js +3 -0
  28. package/dist/config/types/import-types.d.ts +33 -0
  29. package/dist/config/types/import-types.js +2 -0
  30. package/dist/config/types/index.d.ts +11 -0
  31. package/dist/config/types/index.js +27 -0
  32. package/dist/config/types/load-config.d.ts +32 -0
  33. package/dist/config/types/load-config.js +9 -0
  34. package/dist/config/types/output-config.d.ts +10 -0
  35. package/dist/config/types/output-config.js +2 -0
  36. package/dist/config/types/report-config.d.ts +10 -0
  37. package/dist/config/types/report-config.js +2 -0
  38. package/dist/config/types/runtime-types.d.ts +6 -0
  39. package/dist/config/types/runtime-types.js +2 -0
  40. package/dist/config/types/scenario-config.d.ts +30 -0
  41. package/dist/config/types/scenario-config.js +2 -0
  42. package/dist/config/types/step-types.d.ts +139 -0
  43. package/dist/config/types/step-types.js +2 -0
  44. package/dist/config/types/test-configuration.d.ts +18 -0
  45. package/dist/config/types/test-configuration.js +2 -0
  46. package/dist/config/types/worker-config.d.ts +12 -0
  47. package/dist/config/types/worker-config.js +2 -0
  48. package/dist/config/validator.d.ts +19 -0
  49. package/dist/config/validator.js +198 -0
  50. package/dist/core/csv-data-provider.d.ts +47 -0
  51. package/dist/core/csv-data-provider.js +265 -0
  52. package/dist/core/hooks-manager.d.ts +33 -0
  53. package/dist/core/hooks-manager.js +129 -0
  54. package/dist/core/index.d.ts +5 -0
  55. package/dist/core/index.js +11 -0
  56. package/dist/core/script-executor.d.ts +14 -0
  57. package/dist/core/script-executor.js +290 -0
  58. package/dist/core/step-executor.d.ts +41 -0
  59. package/dist/core/step-executor.js +680 -0
  60. package/dist/core/test-runner.d.ts +34 -0
  61. package/dist/core/test-runner.js +465 -0
  62. package/dist/core/threshold-evaluator.d.ts +43 -0
  63. package/dist/core/threshold-evaluator.js +170 -0
  64. package/dist/core/virtual-user-pool.d.ts +42 -0
  65. package/dist/core/virtual-user-pool.js +136 -0
  66. package/dist/core/virtual-user.d.ts +51 -0
  67. package/dist/core/virtual-user.js +488 -0
  68. package/dist/distributed/coordinator.d.ts +34 -0
  69. package/dist/distributed/coordinator.js +158 -0
  70. package/dist/distributed/health-monitor.d.ts +18 -0
  71. package/dist/distributed/health-monitor.js +72 -0
  72. package/dist/distributed/load-distributor.d.ts +17 -0
  73. package/dist/distributed/load-distributor.js +106 -0
  74. package/dist/distributed/remote-worker.d.ts +37 -0
  75. package/dist/distributed/remote-worker.js +241 -0
  76. package/dist/distributed/result-aggregator.d.ts +43 -0
  77. package/dist/distributed/result-aggregator.js +146 -0
  78. package/dist/dsl/index.d.ts +3 -0
  79. package/dist/dsl/index.js +11 -0
  80. package/dist/dsl/test-builder.d.ts +111 -0
  81. package/dist/dsl/test-builder.js +514 -0
  82. package/dist/importers/har-importer.d.ts +17 -0
  83. package/dist/importers/har-importer.js +172 -0
  84. package/dist/importers/open-api-importer.d.ts +23 -0
  85. package/dist/importers/open-api-importer.js +181 -0
  86. package/dist/importers/wsdl-importer.d.ts +42 -0
  87. package/dist/importers/wsdl-importer.js +440 -0
  88. package/dist/index.d.ts +5 -0
  89. package/dist/index.js +17 -0
  90. package/dist/load-patterns/arrivals.d.ts +7 -0
  91. package/dist/load-patterns/arrivals.js +118 -0
  92. package/dist/load-patterns/base.d.ts +9 -0
  93. package/dist/load-patterns/base.js +2 -0
  94. package/dist/load-patterns/basic.d.ts +7 -0
  95. package/dist/load-patterns/basic.js +117 -0
  96. package/dist/load-patterns/stepping.d.ts +6 -0
  97. package/dist/load-patterns/stepping.js +122 -0
  98. package/dist/metrics/collector.d.ts +72 -0
  99. package/dist/metrics/collector.js +662 -0
  100. package/dist/metrics/types.d.ts +135 -0
  101. package/dist/metrics/types.js +2 -0
  102. package/dist/outputs/base.d.ts +7 -0
  103. package/dist/outputs/base.js +2 -0
  104. package/dist/outputs/csv.d.ts +13 -0
  105. package/dist/outputs/csv.js +163 -0
  106. package/dist/outputs/graphite.d.ts +13 -0
  107. package/dist/outputs/graphite.js +126 -0
  108. package/dist/outputs/influxdb.d.ts +12 -0
  109. package/dist/outputs/influxdb.js +82 -0
  110. package/dist/outputs/json.d.ts +14 -0
  111. package/dist/outputs/json.js +107 -0
  112. package/dist/outputs/streaming-csv.d.ts +37 -0
  113. package/dist/outputs/streaming-csv.js +254 -0
  114. package/dist/outputs/streaming-json.d.ts +43 -0
  115. package/dist/outputs/streaming-json.js +353 -0
  116. package/dist/outputs/webhook.d.ts +16 -0
  117. package/dist/outputs/webhook.js +96 -0
  118. package/dist/protocols/base.d.ts +33 -0
  119. package/dist/protocols/base.js +2 -0
  120. package/dist/protocols/rest/handler.d.ts +67 -0
  121. package/dist/protocols/rest/handler.js +776 -0
  122. package/dist/protocols/soap/handler.d.ts +12 -0
  123. package/dist/protocols/soap/handler.js +165 -0
  124. package/dist/protocols/web/core-web-vitals.d.ts +121 -0
  125. package/dist/protocols/web/core-web-vitals.js +373 -0
  126. package/dist/protocols/web/handler.d.ts +50 -0
  127. package/dist/protocols/web/handler.js +706 -0
  128. package/dist/recorder/native-recorder.d.ts +14 -0
  129. package/dist/recorder/native-recorder.js +533 -0
  130. package/dist/recorder/scenario-recorder.d.ts +55 -0
  131. package/dist/recorder/scenario-recorder.js +296 -0
  132. package/dist/reporting/constants.d.ts +94 -0
  133. package/dist/reporting/constants.js +82 -0
  134. package/dist/reporting/enhanced-html-generator.d.ts +55 -0
  135. package/dist/reporting/enhanced-html-generator.js +965 -0
  136. package/dist/reporting/generator.d.ts +42 -0
  137. package/dist/reporting/generator.js +1217 -0
  138. package/dist/reporting/statistics.d.ts +144 -0
  139. package/dist/reporting/statistics.js +742 -0
  140. package/dist/reporting/templates/enhanced-report.hbs +2812 -0
  141. package/dist/reporting/templates/html.hbs +2453 -0
  142. package/dist/utils/faker-manager.d.ts +55 -0
  143. package/dist/utils/faker-manager.js +166 -0
  144. package/dist/utils/file-manager.d.ts +33 -0
  145. package/dist/utils/file-manager.js +154 -0
  146. package/dist/utils/handlebars-manager.d.ts +42 -0
  147. package/dist/utils/handlebars-manager.js +172 -0
  148. package/dist/utils/logger.d.ts +16 -0
  149. package/dist/utils/logger.js +46 -0
  150. package/dist/utils/template.d.ts +80 -0
  151. package/dist/utils/template.js +513 -0
  152. package/dist/utils/test-output-writer.d.ts +56 -0
  153. package/dist/utils/test-output-writer.js +643 -0
  154. package/dist/utils/time.d.ts +3 -0
  155. package/dist/utils/time.js +23 -0
  156. package/dist/utils/timestamp-helper.d.ts +17 -0
  157. package/dist/utils/timestamp-helper.js +53 -0
  158. package/dist/workers/manager.d.ts +18 -0
  159. package/dist/workers/manager.js +95 -0
  160. package/dist/workers/server.d.ts +21 -0
  161. package/dist/workers/server.js +205 -0
  162. package/dist/workers/worker.d.ts +19 -0
  163. package/dist/workers/worker.js +147 -0
  164. 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;