@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,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
|
+
}
|