@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,14 @@
1
+ export interface NativeRecorderOptions {
2
+ output?: string;
3
+ format?: 'yaml' | 'typescript' | 'json';
4
+ viewport?: string;
5
+ baseUrl?: string;
6
+ device?: string;
7
+ }
8
+ /**
9
+ * Native Playwright Recorder
10
+ *
11
+ * Wraps the actual `playwright codegen` command to get the best selector
12
+ * generation and recording experience, then converts output to Perfornium format.
13
+ */
14
+ export declare function startNativeRecording(url: string, options?: NativeRecorderOptions): Promise<void>;
@@ -0,0 +1,533 @@
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
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.startNativeRecording = startNativeRecording;
37
+ const child_process_1 = require("child_process");
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const yaml = __importStar(require("yaml"));
41
+ const readline = __importStar(require("readline"));
42
+ const logger_1 = require("../utils/logger");
43
+ /**
44
+ * Native Playwright Recorder
45
+ *
46
+ * Wraps the actual `playwright codegen` command to get the best selector
47
+ * generation and recording experience, then converts output to Perfornium format.
48
+ */
49
+ async function startNativeRecording(url, options = {}) {
50
+ const format = options.format || 'yaml';
51
+ const tempFile = path.join(process.cwd(), '.perfornium-recording.ts');
52
+ const waitPointsFile = path.join(process.cwd(), '.perfornium-waitpoints.json');
53
+ // Clean up temp files
54
+ if (fs.existsSync(tempFile))
55
+ fs.unlinkSync(tempFile);
56
+ if (fs.existsSync(waitPointsFile))
57
+ fs.unlinkSync(waitPointsFile);
58
+ // Initialize wait points file
59
+ fs.writeFileSync(waitPointsFile, JSON.stringify({ waitPoints: [] }));
60
+ logger_1.logger.info('');
61
+ logger_1.logger.info('╔══════════════════════════════════════════════════════════════╗');
62
+ logger_1.logger.info('║ Native Playwright Recorder with Wait Points ║');
63
+ logger_1.logger.info('╠══════════════════════════════════════════════════════════════╣');
64
+ logger_1.logger.info('║ The Playwright Inspector will open with the browser. ║');
65
+ logger_1.logger.info('║ ║');
66
+ logger_1.logger.info('║ RECORDING: ║');
67
+ logger_1.logger.info('║ • Interact with the page - actions are recorded ║');
68
+ logger_1.logger.info('║ • Use the Inspector to pick elements and add assertions ║');
69
+ logger_1.logger.info('║ • Click "Record" button to pause/resume recording ║');
70
+ logger_1.logger.info('║ ║');
71
+ logger_1.logger.info('║ WAIT POINTS (in this terminal): ║');
72
+ logger_1.logger.info('║ • Press W + Enter to add a wait point at current position ║');
73
+ logger_1.logger.info('║ • You\'ll be prompted for duration (e.g., 2s, 500ms) ║');
74
+ logger_1.logger.info('║ ║');
75
+ logger_1.logger.info('║ FINISH: ║');
76
+ logger_1.logger.info('║ • Close the browser window when done ║');
77
+ logger_1.logger.info('╚══════════════════════════════════════════════════════════════╝');
78
+ logger_1.logger.info('');
79
+ // Build playwright codegen arguments
80
+ const args = ['codegen', '--output', tempFile];
81
+ if (options.viewport) {
82
+ const [width, height] = options.viewport.split('x');
83
+ args.push('--viewport-size', `${width},${height}`);
84
+ }
85
+ if (options.device) {
86
+ args.push('--device', options.device);
87
+ }
88
+ args.push(url);
89
+ // Track wait points via terminal input
90
+ const waitPoints = [];
91
+ let lineCount = 0;
92
+ // Set up readline for wait point input
93
+ const rl = readline.createInterface({
94
+ input: process.stdin,
95
+ output: process.stdout
96
+ });
97
+ // Watch the temp file for changes to track line count
98
+ let fileWatcher = null;
99
+ const updateLineCount = () => {
100
+ try {
101
+ if (fs.existsSync(tempFile)) {
102
+ const content = fs.readFileSync(tempFile, 'utf-8');
103
+ lineCount = content.split('\n').length;
104
+ }
105
+ }
106
+ catch (e) {
107
+ // File might be being written
108
+ }
109
+ };
110
+ // Normalize duration to ensure it has a unit (default to seconds)
111
+ const normalizeDuration = (input) => {
112
+ const trimmed = input.trim();
113
+ // If it's just a number, add 's' for seconds
114
+ if (/^\d+$/.test(trimmed)) {
115
+ return `${trimmed}s`;
116
+ }
117
+ // If it already has a unit (s, ms, m), return as-is
118
+ if (/^\d+\s*(s|ms|m)$/i.test(trimmed)) {
119
+ return trimmed.replace(/\s+/g, '');
120
+ }
121
+ return trimmed;
122
+ };
123
+ // Prompt for wait points
124
+ const promptForWaitPoint = () => {
125
+ rl.question('\n⏱️ Enter wait duration (e.g., 2s, 500ms, or blank to cancel): ', (duration) => {
126
+ if (duration && duration.trim()) {
127
+ updateLineCount();
128
+ const normalizedDuration = normalizeDuration(duration);
129
+ waitPoints.push({ afterLine: lineCount, duration: normalizedDuration });
130
+ fs.writeFileSync(waitPointsFile, JSON.stringify({ waitPoints }));
131
+ logger_1.logger.info(`✓ Wait point added: ${normalizedDuration} (after line ${lineCount})`);
132
+ }
133
+ logger_1.logger.info('\nPress W + Enter to add another wait point...');
134
+ });
135
+ };
136
+ // Listen for 'w' key
137
+ rl.on('line', (input) => {
138
+ if (input.toLowerCase() === 'w') {
139
+ promptForWaitPoint();
140
+ }
141
+ });
142
+ logger_1.logger.info('Starting Playwright codegen...');
143
+ logger_1.logger.info('Press W + Enter in this terminal to add wait points.\n');
144
+ // Start playwright codegen
145
+ const codegen = (0, child_process_1.spawn)('npx', ['playwright', ...args], {
146
+ stdio: ['inherit', 'inherit', 'inherit'],
147
+ shell: true
148
+ });
149
+ // Start watching the file after a short delay
150
+ setTimeout(() => {
151
+ if (fs.existsSync(tempFile)) {
152
+ fileWatcher = fs.watch(tempFile, () => updateLineCount());
153
+ }
154
+ // Also poll periodically in case watch doesn't work
155
+ const pollInterval = setInterval(updateLineCount, 1000);
156
+ codegen.on('exit', () => clearInterval(pollInterval));
157
+ }, 2000);
158
+ // Wait for codegen to finish
159
+ await new Promise((resolve, reject) => {
160
+ codegen.on('exit', (code) => {
161
+ rl.close();
162
+ if (fileWatcher)
163
+ fileWatcher.close();
164
+ if (code === 0 || code === null) {
165
+ resolve();
166
+ }
167
+ else {
168
+ reject(new Error(`Playwright codegen exited with code ${code}`));
169
+ }
170
+ });
171
+ codegen.on('error', (err) => {
172
+ rl.close();
173
+ if (fileWatcher)
174
+ fileWatcher.close();
175
+ reject(err);
176
+ });
177
+ });
178
+ // Check if recording was created
179
+ if (!fs.existsSync(tempFile)) {
180
+ logger_1.logger.warn('No recording was saved. Browser may have been closed without recording.');
181
+ return;
182
+ }
183
+ // Read the generated Playwright code
184
+ const playwrightCode = fs.readFileSync(tempFile, 'utf-8');
185
+ // Load wait points
186
+ let savedWaitPoints = [];
187
+ if (fs.existsSync(waitPointsFile)) {
188
+ try {
189
+ savedWaitPoints = JSON.parse(fs.readFileSync(waitPointsFile, 'utf-8')).waitPoints;
190
+ }
191
+ catch (e) {
192
+ // Ignore
193
+ }
194
+ }
195
+ // Convert to Perfornium format
196
+ const outputFile = options.output || getDefaultOutputFile(format);
197
+ const outputPath = path.resolve(outputFile);
198
+ logger_1.logger.info(`\nConverting recording to ${format.toUpperCase()} format...`);
199
+ const converter = new PlaywrightToPerfornium(playwrightCode, savedWaitPoints, url, options.baseUrl);
200
+ switch (format) {
201
+ case 'typescript':
202
+ fs.writeFileSync(outputPath, converter.toTypeScript());
203
+ break;
204
+ case 'json':
205
+ fs.writeFileSync(outputPath, converter.toJSON());
206
+ break;
207
+ default:
208
+ fs.writeFileSync(outputPath, converter.toYAML());
209
+ }
210
+ // Clean up temp files
211
+ fs.unlinkSync(tempFile);
212
+ if (fs.existsSync(waitPointsFile))
213
+ fs.unlinkSync(waitPointsFile);
214
+ logger_1.logger.info(`✓ Recording saved to: ${outputPath}`);
215
+ logger_1.logger.info(` Actions recorded: ${converter.getActionCount()}`);
216
+ logger_1.logger.info(` Wait points: ${savedWaitPoints.length}`);
217
+ }
218
+ function getDefaultOutputFile(format) {
219
+ const timestamp = new Date().toISOString()
220
+ .replace(/[:.]/g, '-')
221
+ .replace('T', '_')
222
+ .slice(0, 19);
223
+ const baseName = `recording_${timestamp}`;
224
+ // Ensure tests/web directory exists
225
+ const outputDir = path.join(process.cwd(), 'tests', 'web');
226
+ if (!fs.existsSync(outputDir)) {
227
+ fs.mkdirSync(outputDir, { recursive: true });
228
+ }
229
+ switch (format) {
230
+ case 'typescript': return path.join(outputDir, `${baseName}.spec.ts`);
231
+ case 'json': return path.join(outputDir, `${baseName}.json`);
232
+ default: return path.join(outputDir, `${baseName}.yml`);
233
+ }
234
+ }
235
+ /**
236
+ * Converts Playwright codegen output to Perfornium formats
237
+ */
238
+ class PlaywrightToPerfornium {
239
+ constructor(playwrightCode, waitPoints, startUrl, baseUrl) {
240
+ this.actions = [];
241
+ this.waitPoints = waitPoints;
242
+ this.baseUrl = baseUrl || new URL(startUrl).origin;
243
+ this.parsePlaywrightCode(playwrightCode);
244
+ }
245
+ parsePlaywrightCode(code) {
246
+ const lines = code.split('\n');
247
+ let lineNum = 0;
248
+ for (const line of lines) {
249
+ lineNum++;
250
+ const trimmed = line.trim();
251
+ // Skip non-action lines
252
+ if (!trimmed.startsWith('await page.') && !trimmed.startsWith('await expect(')) {
253
+ continue;
254
+ }
255
+ const action = this.parseLine(trimmed, lineNum);
256
+ if (action) {
257
+ // Check if there's a wait point after this line
258
+ const waitPoint = this.waitPoints.find(wp => wp.afterLine === lineNum || wp.afterLine === lineNum + 1);
259
+ if (waitPoint) {
260
+ action.waitAfter = waitPoint.duration;
261
+ }
262
+ this.actions.push(action);
263
+ }
264
+ }
265
+ }
266
+ parseLine(line, lineNum) {
267
+ // page.goto('url')
268
+ let match = line.match(/await page\.goto\(['"](.+?)['"]\)/);
269
+ if (match) {
270
+ return { type: 'goto', url: this.relativizeUrl(match[1]), line: lineNum };
271
+ }
272
+ // page.getByRole('button', { name: 'Submit' }).click()
273
+ match = line.match(/await page\.(getBy\w+)\((.+?)\)\.(\w+)\((.*?)\)/);
274
+ if (match) {
275
+ const [, locatorMethod, locatorArgs, action, actionArgs] = match;
276
+ const selector = this.buildSelector(locatorMethod, locatorArgs);
277
+ return this.buildAction(action, selector, actionArgs, lineNum);
278
+ }
279
+ // page.locator('selector').click()
280
+ match = line.match(/await page\.locator\(['"](.+?)['"]\)\.(\w+)\((.*?)\)/);
281
+ if (match) {
282
+ const [, selector, action, actionArgs] = match;
283
+ return this.buildAction(action, selector, actionArgs, lineNum);
284
+ }
285
+ // expect(page.getBy...).toBeVisible() etc
286
+ match = line.match(/await expect\(page\.(getBy\w+)\((.+?)\)\)\.(to\w+)\((.*?)\)/);
287
+ if (match) {
288
+ const [, locatorMethod, locatorArgs, assertion, assertionArgs] = match;
289
+ const selector = this.buildSelector(locatorMethod, locatorArgs);
290
+ return this.buildAssertion(assertion, selector, assertionArgs, lineNum);
291
+ }
292
+ // expect(page.locator('selector')).toBeVisible()
293
+ match = line.match(/await expect\(page\.locator\(['"](.+?)['"]\)\)\.(to\w+)\((.*?)\)/);
294
+ if (match) {
295
+ const [, selector, assertion, assertionArgs] = match;
296
+ return this.buildAssertion(assertion, selector, assertionArgs, lineNum);
297
+ }
298
+ return null;
299
+ }
300
+ buildSelector(method, args) {
301
+ switch (method) {
302
+ case 'getByRole':
303
+ return this.parseRoleSelector(args);
304
+ case 'getByText':
305
+ return `text=${this.extractFirstArg(args)}`;
306
+ case 'getByLabel':
307
+ return `label=${this.extractFirstArg(args)}`;
308
+ case 'getByPlaceholder':
309
+ return `placeholder=${this.extractFirstArg(args)}`;
310
+ case 'getByTestId':
311
+ return `[data-testid="${this.extractFirstArg(args)}"]`;
312
+ case 'getByAltText':
313
+ return `[alt="${this.extractFirstArg(args)}"]`;
314
+ case 'getByTitle':
315
+ return `[title="${this.extractFirstArg(args)}"]`;
316
+ default:
317
+ return args.replace(/['"]/g, '').trim();
318
+ }
319
+ }
320
+ extractFirstArg(args) {
321
+ // Extract first string argument: 'value' or "value"
322
+ const match = args.match(/['"]([^'"]+)['"]/);
323
+ return match ? match[1] : args.replace(/['"]/g, '').trim();
324
+ }
325
+ parseRoleSelector(args) {
326
+ // Parse: 'button', { name: 'Submit' } -> role=button[name="Submit"]
327
+ // Or just: 'button' -> role=button
328
+ const parts = args.split(',').map(p => p.trim());
329
+ const role = parts[0].replace(/['"]/g, '');
330
+ if (parts.length === 1) {
331
+ return `role=${role}`;
332
+ }
333
+ // Parse the options object: { name: 'Submit', exact: true }
334
+ const optionsStr = parts.slice(1).join(',');
335
+ const attributes = [];
336
+ // Extract name attribute
337
+ const nameMatch = optionsStr.match(/name:\s*['"]([^'"]+)['"]/);
338
+ if (nameMatch) {
339
+ attributes.push(`name="${nameMatch[1]}"`);
340
+ }
341
+ // Extract other common attributes
342
+ const exactMatch = optionsStr.match(/exact:\s*(true|false)/);
343
+ if (exactMatch && exactMatch[1] === 'true') {
344
+ attributes.push('exact=true');
345
+ }
346
+ if (attributes.length > 0) {
347
+ return `role=${role}[${attributes.join('][')}]`;
348
+ }
349
+ return `role=${role}`;
350
+ }
351
+ buildAction(action, selector, args, lineNum) {
352
+ const cleanArgs = args.replace(/^['"]|['"]$/g, '').trim();
353
+ switch (action) {
354
+ case 'click':
355
+ return { type: 'click', selector, line: lineNum };
356
+ case 'fill':
357
+ return { type: 'fill', selector, value: cleanArgs, line: lineNum };
358
+ case 'press':
359
+ return { type: 'press', selector, key: cleanArgs, line: lineNum };
360
+ case 'check':
361
+ return { type: 'check', selector, line: lineNum };
362
+ case 'uncheck':
363
+ return { type: 'uncheck', selector, line: lineNum };
364
+ case 'selectOption':
365
+ return { type: 'select', selector, value: cleanArgs, line: lineNum };
366
+ case 'hover':
367
+ return { type: 'hover', selector, line: lineNum };
368
+ case 'dblclick':
369
+ return { type: 'dblclick', selector, line: lineNum };
370
+ default:
371
+ return null;
372
+ }
373
+ }
374
+ buildAssertion(assertion, selector, args, lineNum) {
375
+ const cleanArgs = args.replace(/^['"]|['"]$/g, '').trim();
376
+ switch (assertion) {
377
+ case 'toBeVisible':
378
+ return { type: 'verify_visible', selector, line: lineNum };
379
+ case 'toBeHidden':
380
+ return { type: 'verify_hidden', selector, line: lineNum };
381
+ case 'toHaveText':
382
+ return { type: 'verify_text', selector, value: cleanArgs, line: lineNum };
383
+ case 'toContainText':
384
+ return { type: 'verify_contains', selector, value: cleanArgs, line: lineNum };
385
+ case 'toBeEnabled':
386
+ return { type: 'verify_enabled', selector, line: lineNum };
387
+ case 'toBeDisabled':
388
+ return { type: 'verify_disabled', selector, line: lineNum };
389
+ default:
390
+ return null;
391
+ }
392
+ }
393
+ relativizeUrl(url) {
394
+ if (url.startsWith(this.baseUrl)) {
395
+ return url.substring(this.baseUrl.length) || '/';
396
+ }
397
+ return url;
398
+ }
399
+ getActionCount() {
400
+ return this.actions.length;
401
+ }
402
+ toYAML() {
403
+ const steps = this.actions.map((action, idx) => {
404
+ const step = {
405
+ name: `${action.type}_${idx + 1}`,
406
+ type: 'web',
407
+ action: this.actionToYamlAction(action)
408
+ };
409
+ if (action.waitAfter) {
410
+ step.wait = action.waitAfter;
411
+ }
412
+ return step;
413
+ });
414
+ const scenario = {
415
+ name: 'Recorded Web Scenario',
416
+ description: `Recorded on ${new Date().toISOString()} using Playwright codegen`,
417
+ global: {
418
+ base_url: this.baseUrl,
419
+ browser: { type: 'chromium', headless: false },
420
+ think_time: '1-3'
421
+ },
422
+ load: {
423
+ pattern: 'basic',
424
+ virtual_users: 1,
425
+ ramp_up: '30s'
426
+ },
427
+ scenarios: [{
428
+ name: 'recorded_user_journey',
429
+ weight: 100,
430
+ loop: 1,
431
+ steps
432
+ }],
433
+ outputs: [{ type: 'json', file: 'results/recorded-results.json' }],
434
+ report: { generate: true, output: 'reports/recorded-report.html' }
435
+ };
436
+ return yaml.stringify(scenario, { indent: 2, lineWidth: 120 });
437
+ }
438
+ actionToYamlAction(action) {
439
+ const result = { command: action.type };
440
+ if (action.selector)
441
+ result.selector = action.selector;
442
+ if (action.url)
443
+ result.url = action.url;
444
+ if (action.value)
445
+ result.value = action.value;
446
+ if (action.key)
447
+ result.key = action.key;
448
+ return result;
449
+ }
450
+ toTypeScript() {
451
+ const steps = this.actions.map(action => {
452
+ let step = this.actionToTypeScriptStep(action);
453
+ if (action.waitAfter) {
454
+ step += `\n .wait('${action.waitAfter}')`;
455
+ }
456
+ return step;
457
+ }).join('\n');
458
+ return `import { test, faker } from 'perfornium2';
459
+
460
+ /**
461
+ * Recorded Web Scenario
462
+ * Generated: ${new Date().toISOString()}
463
+ * Recorder: Playwright codegen
464
+ * Base URL: ${this.baseUrl}
465
+ */
466
+
467
+ const testConfig = test('Recorded Web Scenario')
468
+ .baseUrl('${this.baseUrl}')
469
+ .withBrowser('chromium', {
470
+ headless: process.env.HEADLESS !== 'false',
471
+ viewport: { width: 1920, height: 1080 }
472
+ })
473
+ .timeout(30000)
474
+ .scenario('Recorded User Journey', 100)
475
+ ${steps}
476
+ .done()
477
+ .withLoad({
478
+ pattern: 'basic',
479
+ virtual_users: 1,
480
+ ramp_up: '30s',
481
+ duration: '5m'
482
+ })
483
+ .withJSONOutput('results/test-results.json')
484
+ .withReport('reports/test-report.html')
485
+ .build();
486
+
487
+ export default testConfig;
488
+ `;
489
+ }
490
+ actionToTypeScriptStep(action) {
491
+ const sel = (s) => s.replace(/'/g, "\\'");
492
+ switch (action.type) {
493
+ case 'goto':
494
+ return ` .goto('${sel(action.url || '/')}')`;
495
+ case 'click':
496
+ return ` .click('${sel(action.selector || '')}')`;
497
+ case 'fill':
498
+ return ` .fill('${sel(action.selector || '')}', '${sel(action.value || '')}')`;
499
+ case 'press':
500
+ return ` .press('${sel(action.selector || '')}', '${action.key || ''}')`;
501
+ case 'check':
502
+ return ` .check('${sel(action.selector || '')}')`;
503
+ case 'uncheck':
504
+ return ` .uncheck('${sel(action.selector || '')}')`;
505
+ case 'select':
506
+ return ` .select('${sel(action.selector || '')}', '${sel(action.value || '')}')`;
507
+ case 'hover':
508
+ return ` .hover('${sel(action.selector || '')}')`;
509
+ case 'dblclick':
510
+ return ` .dblclick('${sel(action.selector || '')}')`;
511
+ case 'verify_visible':
512
+ return ` .expectVisible('${sel(action.selector || '')}')`;
513
+ case 'verify_hidden':
514
+ return ` .expectHidden('${sel(action.selector || '')}')`;
515
+ case 'verify_text':
516
+ return ` .expectText('${sel(action.selector || '')}', '${sel(action.value || '')}')`;
517
+ case 'verify_contains':
518
+ return ` .expectContains('${sel(action.selector || '')}', '${sel(action.value || '')}')`;
519
+ default:
520
+ return ` // Unknown action: ${action.type}`;
521
+ }
522
+ }
523
+ toJSON() {
524
+ return JSON.stringify({
525
+ metadata: {
526
+ recorded: new Date().toISOString(),
527
+ baseUrl: this.baseUrl,
528
+ recorder: 'playwright-codegen'
529
+ },
530
+ actions: this.actions
531
+ }, null, 2);
532
+ }
533
+ }
@@ -0,0 +1,55 @@
1
+ import { EventEmitter } from 'events';
2
+ export interface RecordedRequest {
3
+ timestamp: number;
4
+ method: string;
5
+ url: string;
6
+ path: string;
7
+ headers?: Record<string, string>;
8
+ params?: Record<string, any>;
9
+ body?: any;
10
+ response?: {
11
+ status: number;
12
+ statusText: string;
13
+ headers?: Record<string, string>;
14
+ data?: any;
15
+ duration: number;
16
+ };
17
+ }
18
+ export interface RecordedScenario {
19
+ name: string;
20
+ description?: string;
21
+ baseURL?: string;
22
+ steps: RecordedRequest[];
23
+ variables?: Record<string, any>;
24
+ extractions?: Array<{
25
+ from: string;
26
+ name: string;
27
+ expression: string;
28
+ type: 'json_path' | 'regex' | 'header' | 'cookie';
29
+ }>;
30
+ }
31
+ export type OutputFormat = 'yaml' | 'typescript' | 'json';
32
+ export declare class ScenarioRecorder extends EventEmitter {
33
+ private recording;
34
+ private currentScenario;
35
+ private recordedRequests;
36
+ private startTime;
37
+ private extractionRules;
38
+ startRecording(scenarioName: string, description?: string): void;
39
+ recordRequest(request: RecordedRequest): void;
40
+ private detectVariables;
41
+ private detectExtractions;
42
+ private hasNestedField;
43
+ private findJsonPath;
44
+ stopRecording(): RecordedScenario | null;
45
+ private optimizeSteps;
46
+ private isImportantRequest;
47
+ private cleanRequest;
48
+ exportScenario(scenario: RecordedScenario, format: OutputFormat): string;
49
+ private exportAsYAML;
50
+ private exportAsTypeScript;
51
+ private convertToTestConfig;
52
+ saveToFile(scenario: RecordedScenario, filename: string, format: OutputFormat): void;
53
+ isRecording(): boolean;
54
+ getRecordedRequestsCount(): number;
55
+ }