@testsmith/perfornium 0.4.0 → 0.5.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/dist/cli/cli.js CHANGED
@@ -81,6 +81,7 @@ program
81
81
  .option('-o, --output <file>', 'Output file for recorded scenario')
82
82
  .option('--viewport <viewport>', 'Browser viewport size (e.g., 1920x1080)')
83
83
  .option('--base-url <url>', 'Base URL to relativize recorded URLs')
84
+ .option('-b, --browser <browser>', 'Browser to use: chromium, chrome, msedge, firefox, webkit', 'chromium')
84
85
  .option('-f, --format <format>', 'Output format: yaml, json, or typescript', 'yaml')
85
86
  .action(async (url, options) => {
86
87
  // Auto-determine file extension if output not specified
@@ -97,7 +98,8 @@ program
97
98
  output: options.output,
98
99
  format: options.format,
99
100
  viewport: options.viewport,
100
- baseUrl: options.baseUrl
101
+ baseUrl: options.baseUrl,
102
+ browser: options.browser
101
103
  });
102
104
  });
103
105
  // ============================================
@@ -40,7 +40,7 @@ export interface FakerConfig {
40
40
  seed?: number;
41
41
  }
42
42
  export interface BrowserConfig {
43
- type: 'chromium' | 'firefox' | 'webkit';
43
+ type: 'chromium' | 'firefox' | 'webkit' | 'msedge' | 'chrome';
44
44
  headless?: boolean;
45
45
  viewport?: {
46
46
  width: number;
@@ -50,6 +50,15 @@ export interface BrowserConfig {
50
50
  base_url?: string;
51
51
  highlight?: boolean | HighlightConfig;
52
52
  clear_storage?: boolean | ClearStorageConfig;
53
+ /** Capture screenshot on test failure */
54
+ screenshot_on_failure?: boolean | ScreenshotConfig;
55
+ }
56
+ export interface ScreenshotConfig {
57
+ enabled: boolean;
58
+ /** Directory to save screenshots (default: 'screenshots') */
59
+ output_dir?: string;
60
+ /** Capture full page screenshot (default: true) */
61
+ full_page?: boolean;
53
62
  }
54
63
  export interface ClearStorageConfig {
55
64
  local_storage?: boolean;
@@ -290,7 +290,8 @@ class TestRunner {
290
290
  viewport: browserConfig.viewport,
291
291
  slow_mo: browserConfig.slow_mo,
292
292
  highlight: browserConfig.highlight,
293
- clear_storage: browserConfig.clear_storage
293
+ clear_storage: browserConfig.clear_storage,
294
+ screenshot_on_failure: browserConfig.screenshot_on_failure
294
295
  };
295
296
  const handler = new handler_3.WebHandler(webConfig);
296
297
  await handler.initialize();
@@ -926,7 +926,10 @@ class DashboardServer {
926
926
  <svg viewBox="0 0 128 128"><defs><linearGradient id="lg1" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#00d4ff"/><stop offset="100%" stop-color="#9c40ff"/></linearGradient><linearGradient id="lg2" x1="0%" y1="100%" x2="100%" y2="0%"><stop offset="0%" stop-color="#00d4ff"/><stop offset="100%" stop-color="#9c40ff"/></linearGradient></defs><rect x="4" y="4" width="120" height="120" rx="24" fill="#0f0f23"/><rect x="32" y="28" width="12" height="72" rx="6" fill="url(#lg1)"/><path d="M 38 28 L 62 28 C 88 28 88 60 62 60 L 38 60" fill="none" stroke="url(#lg1)" stroke-width="12" stroke-linecap="round" stroke-linejoin="round"/><rect x="76" y="68" width="8" height="32" rx="4" fill="url(#lg2)" opacity="0.9"/><rect x="88" y="54" width="8" height="46" rx="4" fill="url(#lg1)" opacity="0.9"/></svg>
927
927
  Perfornium Dashboard
928
928
  </div>
929
- <div id="connectionStatus"></div>
929
+ <div style="display: flex; align-items: center; gap: 16px;">
930
+ <div id="workersStatus"></div>
931
+ <div id="connectionStatus"></div>
932
+ </div>
930
933
  </div>
931
934
 
932
935
  <div class="container">
@@ -1037,8 +1040,8 @@ class DashboardServer {
1037
1040
  function initWebSocket() {
1038
1041
  const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
1039
1042
  ws = new WebSocket(protocol + '//' + location.host);
1040
- ws.onopen = () => { document.getElementById('connectionStatus').innerHTML = '<span class="live-badge">Connected</span>'; };
1041
- ws.onclose = () => { document.getElementById('connectionStatus').innerHTML = ''; setTimeout(initWebSocket, 3000); };
1043
+ ws.onopen = () => { document.getElementById('connectionStatus').innerHTML = '<span class="live-badge">Dashboard</span>'; };
1044
+ ws.onclose = () => { document.getElementById('connectionStatus').innerHTML = '<span style="color: var(--text-secondary); font-size: 12px;">Reconnecting...</span>'; setTimeout(initWebSocket, 3000); };
1042
1045
  ws.onmessage = (e) => handleMessage(JSON.parse(e.data));
1043
1046
  }
1044
1047
 
@@ -1067,10 +1070,17 @@ class DashboardServer {
1067
1070
  workersData = await res.json();
1068
1071
  const section = document.getElementById('workersSection');
1069
1072
  const info = document.getElementById('workersInfo');
1073
+ const headerStatus = document.getElementById('workersStatus');
1070
1074
  if (workersData.available && workersData.workers.length > 0) {
1071
1075
  section.style.display = 'block';
1072
1076
  const totalCapacity = workersData.workers.reduce((sum, w) => sum + (w.capacity || 0), 0);
1073
- info.textContent = '(' + workersData.workers.length + ' workers, ' + totalCapacity + ' total capacity)';
1077
+ const workerCount = workersData.workers.length;
1078
+ info.textContent = '(' + workerCount + ' workers, ' + totalCapacity + ' total capacity)';
1079
+ // Show workers info in header
1080
+ const workerNames = workersData.workers.map(w => w.name || (w.host + ':' + w.port)).join(', ');
1081
+ headerStatus.innerHTML = '<span style="display: inline-flex; align-items: center; gap: 6px; padding: 4px 12px; background: linear-gradient(135deg, #9c40ff 0%, #00d4ff 100%); border-radius: 20px; font-size: 12px; color: white; font-weight: 500; cursor: help;" title="' + workerNames + '"><span style="width: 8px; height: 8px; background: white; border-radius: 50%; animation: pulse 1.5s infinite;"></span>' + workerCount + ' Worker' + (workerCount > 1 ? 's' : '') + '</span>';
1082
+ } else {
1083
+ headerStatus.innerHTML = '';
1074
1084
  }
1075
1085
  } catch (e) { console.error('Failed to load workers:', e); }
1076
1086
  }
@@ -47,4 +47,8 @@ export declare class WebHandler implements ProtocolHandler {
47
47
  * Highlight an element before interacting with it (for debugging)
48
48
  */
49
49
  private highlightElement;
50
+ /**
51
+ * Capture a screenshot when a test step fails
52
+ */
53
+ private captureFailureScreenshot;
50
54
  }
@@ -1,8 +1,43 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.WebHandler = void 0;
4
37
  const playwright_1 = require("playwright");
5
38
  const logger_1 = require("../../utils/logger");
39
+ const path = __importStar(require("path"));
40
+ const fs = __importStar(require("fs"));
6
41
  const core_web_vitals_1 = require("./core-web-vitals");
7
42
  class WebHandler {
8
43
  constructor(config) {
@@ -249,6 +284,23 @@ class WebHandler {
249
284
  const shouldRecordError = measurableCommands.includes(action.command);
250
285
  // Get verification metrics from error if available (attached by measureVerificationStep)
251
286
  const verificationMetrics = error.verificationMetrics;
287
+ // Capture screenshot on failure if enabled
288
+ let screenshotPath;
289
+ if (this.config.screenshot_on_failure) {
290
+ logger_1.logger.debug(`Screenshot on failure enabled, attempting capture for VU ${context.vu_id}`);
291
+ try {
292
+ const page = this.pages.get(context.vu_id);
293
+ if (page && !page.isClosed()) {
294
+ screenshotPath = await this.captureFailureScreenshot(page, context.vu_id, action.command);
295
+ }
296
+ else {
297
+ logger_1.logger.warn(`Cannot capture screenshot: page is ${page ? 'closed' : 'not found'} for VU ${context.vu_id}`);
298
+ }
299
+ }
300
+ catch (screenshotError) {
301
+ logger_1.logger.warn(`Failed to capture failure screenshot: ${screenshotError.message}`);
302
+ }
303
+ }
252
304
  return {
253
305
  success: false,
254
306
  error: error.message,
@@ -259,7 +311,8 @@ class WebHandler {
259
311
  command: action.command,
260
312
  error_type: error.constructor.name,
261
313
  error_stack: error.stack?.split('\n').slice(0, 3).join('; '),
262
- verification_metrics: verificationMetrics
314
+ verification_metrics: verificationMetrics,
315
+ screenshot: screenshotPath
263
316
  }
264
317
  };
265
318
  }
@@ -500,6 +553,12 @@ class WebHandler {
500
553
  case 'chromium':
501
554
  browser = await playwright_1.chromium.launch(launchOptions);
502
555
  break;
556
+ case 'chrome':
557
+ browser = await playwright_1.chromium.launch({ ...launchOptions, channel: 'chrome' });
558
+ break;
559
+ case 'msedge':
560
+ browser = await playwright_1.chromium.launch({ ...launchOptions, channel: 'msedge' });
561
+ break;
503
562
  case 'firefox':
504
563
  browser = await playwright_1.firefox.launch(launchOptions);
505
564
  break;
@@ -507,7 +566,7 @@ class WebHandler {
507
566
  browser = await playwright_1.webkit.launch(launchOptions);
508
567
  break;
509
568
  default:
510
- throw new Error(`Unsupported browser type: ${browserType}`);
569
+ throw new Error(`Unsupported browser type: ${browserType}. Supported: chromium, chrome, msedge, firefox, webkit`);
511
570
  }
512
571
  logger_1.logger.debug(`VU ${vuId}: Launched enhanced ${browserType} browser with Web Vitals support`);
513
572
  return browser;
@@ -737,5 +796,34 @@ class WebHandler {
737
796
  logger_1.logger.debug(`Failed to highlight element ${selector}:`, error);
738
797
  }
739
798
  }
799
+ /**
800
+ * Capture a screenshot when a test step fails
801
+ */
802
+ async captureFailureScreenshot(page, vuId, command) {
803
+ const screenshotConfig = this.config.screenshot_on_failure;
804
+ // Parse config - can be boolean or ScreenshotConfig object
805
+ let outputDir = 'screenshots';
806
+ let fullPage = true;
807
+ if (typeof screenshotConfig === 'object') {
808
+ outputDir = screenshotConfig.output_dir || 'screenshots';
809
+ fullPage = screenshotConfig.full_page !== false;
810
+ }
811
+ // Ensure directory exists
812
+ if (!fs.existsSync(outputDir)) {
813
+ fs.mkdirSync(outputDir, { recursive: true });
814
+ }
815
+ // Generate unique filename with timestamp
816
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
817
+ const sanitizedCommand = command.replace(/[^a-zA-Z0-9_-]/g, '_');
818
+ const filename = `failure_vu${vuId}_${sanitizedCommand}_${timestamp}.png`;
819
+ const screenshotPath = path.join(outputDir, filename);
820
+ // Capture the screenshot
821
+ await page.screenshot({
822
+ path: screenshotPath,
823
+ fullPage
824
+ });
825
+ logger_1.logger.info(`📸 Screenshot captured: ${screenshotPath}`);
826
+ return screenshotPath;
827
+ }
740
828
  }
741
829
  exports.WebHandler = WebHandler;
@@ -4,6 +4,7 @@ export interface NativeRecorderOptions {
4
4
  viewport?: string;
5
5
  baseUrl?: string;
6
6
  device?: string;
7
+ browser?: 'chromium' | 'chrome' | 'msedge' | 'firefox' | 'webkit';
7
8
  }
8
9
  /**
9
10
  * Native Playwright Recorder
@@ -78,6 +78,18 @@ async function startNativeRecording(url, options = {}) {
78
78
  logger_1.logger.info('');
79
79
  // Build playwright codegen arguments
80
80
  const args = ['codegen', '--output', tempFile];
81
+ const browserType = options.browser || 'chromium';
82
+ // Add browser option - playwright codegen uses --browser for channel selection
83
+ if (browserType === 'msedge') {
84
+ args.push('--browser', 'chromium', '--channel', 'msedge');
85
+ }
86
+ else if (browserType === 'chrome') {
87
+ args.push('--browser', 'chromium', '--channel', 'chrome');
88
+ }
89
+ else if (browserType === 'firefox' || browserType === 'webkit') {
90
+ args.push('--browser', browserType);
91
+ }
92
+ // chromium is the default, no need to specify
81
93
  if (options.viewport) {
82
94
  const [width, height] = options.viewport.split('x');
83
95
  args.push('--viewport-size', `${width},${height}`);
@@ -196,7 +208,7 @@ async function startNativeRecording(url, options = {}) {
196
208
  const outputFile = options.output || getDefaultOutputFile(format);
197
209
  const outputPath = path.resolve(outputFile);
198
210
  logger_1.logger.info(`\nConverting recording to ${format.toUpperCase()} format...`);
199
- const converter = new PlaywrightToPerfornium(playwrightCode, savedWaitPoints, url, options.baseUrl);
211
+ const converter = new PlaywrightToPerfornium(playwrightCode, savedWaitPoints, url, options.baseUrl, options.browser);
200
212
  switch (format) {
201
213
  case 'typescript':
202
214
  fs.writeFileSync(outputPath, converter.toTypeScript());
@@ -236,10 +248,11 @@ function getDefaultOutputFile(format) {
236
248
  * Converts Playwright codegen output to Perfornium formats
237
249
  */
238
250
  class PlaywrightToPerfornium {
239
- constructor(playwrightCode, waitPoints, startUrl, baseUrl) {
251
+ constructor(playwrightCode, waitPoints, startUrl, baseUrl, browserType) {
240
252
  this.actions = [];
241
253
  this.waitPoints = waitPoints;
242
254
  this.baseUrl = baseUrl || new URL(startUrl).origin;
255
+ this.browserType = browserType || 'chromium';
243
256
  this.parsePlaywrightCode(playwrightCode);
244
257
  }
245
258
  parsePlaywrightCode(code) {
@@ -416,7 +429,7 @@ class PlaywrightToPerfornium {
416
429
  description: `Recorded on ${new Date().toISOString()} using Playwright codegen`,
417
430
  global: {
418
431
  base_url: this.baseUrl,
419
- browser: { type: 'chromium', headless: false },
432
+ browser: { type: this.browserType, headless: false },
420
433
  think_time: '1-3'
421
434
  },
422
435
  load: {
@@ -466,7 +479,7 @@ class PlaywrightToPerfornium {
466
479
 
467
480
  const testConfig = test('Recorded Web Scenario')
468
481
  .baseUrl('${this.baseUrl}')
469
- .withBrowser('chromium', {
482
+ .withBrowser('${this.browserType}', {
470
483
  headless: process.env.HEADLESS !== 'false',
471
484
  viewport: { width: 1920, height: 1080 }
472
485
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testsmith/perfornium",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Flexible performance testing framework for REST, SOAP, and web applications",
5
5
  "author": "TestSmith",
6
6
  "license": "MIT",