@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,643 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/utils/test-output-writer.ts
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.TestOutputWriter = void 0;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const yaml = __importStar(require("yaml"));
|
|
41
|
+
const logger_1 = require("./logger");
|
|
42
|
+
class TestOutputWriter {
|
|
43
|
+
constructor(config, dslOptions) {
|
|
44
|
+
this.config = config;
|
|
45
|
+
this.dslOptions = {
|
|
46
|
+
includeDataGeneration: true,
|
|
47
|
+
includeCSVSupport: true,
|
|
48
|
+
includeHooks: true,
|
|
49
|
+
includeCustomLogic: true,
|
|
50
|
+
includeMultipleLoadPatterns: true,
|
|
51
|
+
...dslOptions
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async write() {
|
|
55
|
+
const outputPath = path.resolve(this.config.outputPath);
|
|
56
|
+
// Ensure directory exists
|
|
57
|
+
const dir = path.dirname(outputPath);
|
|
58
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
59
|
+
let content;
|
|
60
|
+
switch (this.config.format) {
|
|
61
|
+
case 'typescript':
|
|
62
|
+
content = this.generateTypeScriptDSL();
|
|
63
|
+
break;
|
|
64
|
+
case 'json':
|
|
65
|
+
content = this.generateJSON();
|
|
66
|
+
break;
|
|
67
|
+
case 'yaml':
|
|
68
|
+
default:
|
|
69
|
+
content = this.generateYAML();
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
await fs.promises.writeFile(outputPath, content, 'utf8');
|
|
73
|
+
const stats = await fs.promises.stat(outputPath);
|
|
74
|
+
logger_1.logger.info(`✅ Test file saved: ${outputPath} (${stats.size} bytes)`);
|
|
75
|
+
return outputPath;
|
|
76
|
+
}
|
|
77
|
+
generateYAML() {
|
|
78
|
+
const testConfig = this.buildTestConfiguration();
|
|
79
|
+
return yaml.stringify(testConfig, { indent: 2, lineWidth: 120 });
|
|
80
|
+
}
|
|
81
|
+
generateJSON() {
|
|
82
|
+
const testConfig = this.buildTestConfiguration();
|
|
83
|
+
return JSON.stringify(testConfig, null, 2);
|
|
84
|
+
}
|
|
85
|
+
// Fixed methods for TestOutputWriter class
|
|
86
|
+
generateTypeScriptDSL() {
|
|
87
|
+
const sourceInfo = this.getSourceInfo();
|
|
88
|
+
return `import { test, faker, testData } from '@perfornium/dsl';
|
|
89
|
+
${this.dslOptions.includeCSVSupport ? "import { CSV } from '@perfornium/data';" : ''}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* ${this.config.name}
|
|
93
|
+
* ${this.config.description || 'Auto-generated test scenario'}
|
|
94
|
+
*
|
|
95
|
+
* ${sourceInfo}
|
|
96
|
+
* Generated on: ${new Date().toISOString()}
|
|
97
|
+
*/
|
|
98
|
+
|
|
99
|
+
${this.generateDataSection()}
|
|
100
|
+
|
|
101
|
+
${this.generateTestConfiguration()}
|
|
102
|
+
|
|
103
|
+
${this.generateExportSection()}
|
|
104
|
+
`;
|
|
105
|
+
}
|
|
106
|
+
generateTestConfiguration() {
|
|
107
|
+
const hasWebScenarios = this.hasWebScenarios();
|
|
108
|
+
let config = `// ============================================
|
|
109
|
+
// Test Configuration
|
|
110
|
+
// ============================================
|
|
111
|
+
|
|
112
|
+
const testConfig = test('${this.config.name}')
|
|
113
|
+
.baseUrl('${this.config.baseUrl || 'http://localhost:3000'}')`;
|
|
114
|
+
// Add browser config if web scenarios exist
|
|
115
|
+
if (hasWebScenarios) {
|
|
116
|
+
config += `
|
|
117
|
+
.withBrowser('chromium', {
|
|
118
|
+
headless: process.env.HEADLESS === 'true',
|
|
119
|
+
viewport: { width: 1920, height: 1080 }
|
|
120
|
+
})`;
|
|
121
|
+
}
|
|
122
|
+
config += `
|
|
123
|
+
.timeout(30000)`;
|
|
124
|
+
// Add global variables if any
|
|
125
|
+
if (this.config.metadata?.variables) {
|
|
126
|
+
config += `
|
|
127
|
+
.variables(${this.formatJSONForDSL(this.config.metadata.variables)})`;
|
|
128
|
+
}
|
|
129
|
+
// Generate scenarios
|
|
130
|
+
for (const scenario of this.config.scenarios) {
|
|
131
|
+
config += this.generateScenarioDSL(scenario);
|
|
132
|
+
}
|
|
133
|
+
// Add load configuration
|
|
134
|
+
config += `
|
|
135
|
+
.withLoad({
|
|
136
|
+
pattern: 'basic',
|
|
137
|
+
virtual_users: ${this.getVirtualUsers()},
|
|
138
|
+
ramp_up: '${this.getRampUp()}',
|
|
139
|
+
duration: '${this.getDuration()}'
|
|
140
|
+
})`;
|
|
141
|
+
// Add output configurations
|
|
142
|
+
config += `
|
|
143
|
+
.withJSONOutput('results/test-results.json')`;
|
|
144
|
+
// Add report configuration
|
|
145
|
+
config += `
|
|
146
|
+
.withReport('reports/test-report.html')`;
|
|
147
|
+
// Build the configuration
|
|
148
|
+
config += `
|
|
149
|
+
.build();`;
|
|
150
|
+
return config;
|
|
151
|
+
}
|
|
152
|
+
generateScenarioDSL(scenario) {
|
|
153
|
+
const scenarioName = scenario.name || 'test_scenario';
|
|
154
|
+
const weight = scenario.weight || 100;
|
|
155
|
+
let scenarioDSL = `
|
|
156
|
+
.scenario('${scenarioName}', ${weight})`;
|
|
157
|
+
// Add CSV data if present
|
|
158
|
+
if (scenario.csv_data && this.dslOptions.includeCSVSupport) {
|
|
159
|
+
scenarioDSL += `
|
|
160
|
+
.withCSV('${scenario.csv_data.file}', {
|
|
161
|
+
mode: '${scenario.csv_data.mode || 'unique'}',
|
|
162
|
+
cycleOnExhaustion: ${scenario.csv_data.cycleOnExhaustion !== false}
|
|
163
|
+
})`;
|
|
164
|
+
}
|
|
165
|
+
// Add think time if present
|
|
166
|
+
if (scenario.think_time) {
|
|
167
|
+
scenarioDSL += `
|
|
168
|
+
.thinkTime('${scenario.think_time}')`;
|
|
169
|
+
}
|
|
170
|
+
// Add variables if present
|
|
171
|
+
if (scenario.variables) {
|
|
172
|
+
scenarioDSL += `
|
|
173
|
+
.variables(${this.formatJSONForDSL(scenario.variables)})`;
|
|
174
|
+
}
|
|
175
|
+
// Add loop if present
|
|
176
|
+
if (scenario.loop) {
|
|
177
|
+
scenarioDSL += `
|
|
178
|
+
.loop(${scenario.loop})`;
|
|
179
|
+
}
|
|
180
|
+
// Add before hook if enabled - WITH CONTEXT PARAMETER
|
|
181
|
+
if (this.dslOptions.includeHooks) {
|
|
182
|
+
scenarioDSL += `
|
|
183
|
+
.beforeScenario(async (context) => {
|
|
184
|
+
// Setup: Authentication, test data preparation, etc.
|
|
185
|
+
console.log(\`Starting test for VU: \${context.vu_id}\`);
|
|
186
|
+
|
|
187
|
+
// Example: Get authentication token
|
|
188
|
+
// const token = await authenticate(testData.username, testData.password);
|
|
189
|
+
// context.variables.authToken = token;
|
|
190
|
+
|
|
191
|
+
// Example: Create test data via API
|
|
192
|
+
// const user = await createTestUser(testData);
|
|
193
|
+
// context.variables.userId = user.id;
|
|
194
|
+
})`;
|
|
195
|
+
}
|
|
196
|
+
// Generate steps
|
|
197
|
+
if (scenario.steps && scenario.steps.length > 0) {
|
|
198
|
+
scenarioDSL += this.generateStepsDSL(scenario.steps);
|
|
199
|
+
}
|
|
200
|
+
// Add after hook if enabled - WITH CONTEXT PARAMETER
|
|
201
|
+
if (this.dslOptions.includeHooks) {
|
|
202
|
+
scenarioDSL += `
|
|
203
|
+
.afterScenario(async (context) => {
|
|
204
|
+
// Cleanup: Logout, delete test data, etc.
|
|
205
|
+
console.log(\`Test completed for VU: \${context.vu_id}\`);
|
|
206
|
+
|
|
207
|
+
// Example: Cleanup test data
|
|
208
|
+
// if (context.variables.userId) {
|
|
209
|
+
// await deleteTestUser(context.variables.userId);
|
|
210
|
+
// }
|
|
211
|
+
})`;
|
|
212
|
+
}
|
|
213
|
+
// End the scenario - IMPORTANT: Must call done() to return to test builder
|
|
214
|
+
scenarioDSL += `
|
|
215
|
+
.done()`;
|
|
216
|
+
return scenarioDSL;
|
|
217
|
+
}
|
|
218
|
+
// Update generateWebStep to use context in custom steps
|
|
219
|
+
generateWebStep(step) {
|
|
220
|
+
const action = step.action || {};
|
|
221
|
+
switch (action.command) {
|
|
222
|
+
case 'goto':
|
|
223
|
+
return ` .goto('${action.url || '/'}')`;
|
|
224
|
+
case 'click':
|
|
225
|
+
return ` .click('${this.escape(action.selector || '')}')`;
|
|
226
|
+
case 'fill':
|
|
227
|
+
return this.generateFillStep(action);
|
|
228
|
+
case 'select':
|
|
229
|
+
return ` .select('${this.escape(action.selector || '')}', '${this.escape(action.value || '')}')`;
|
|
230
|
+
case 'verify_visible':
|
|
231
|
+
case 'verify_exists':
|
|
232
|
+
return action.name
|
|
233
|
+
? ` .expectVisible('${this.escape(action.selector || '')}', '${this.escape(action.name)}')`
|
|
234
|
+
: ` .expectVisible('${this.escape(action.selector || '')}')`;
|
|
235
|
+
case 'verify_text':
|
|
236
|
+
return action.name
|
|
237
|
+
? ` .expectText('${this.escape(action.selector || '')}', '${this.escape(action.expected_text || '')}', '${this.escape(action.name)}')`
|
|
238
|
+
: ` .expectText('${this.escape(action.selector || '')}', '${this.escape(action.expected_text || '')}')`;
|
|
239
|
+
case 'verify_not_exists':
|
|
240
|
+
return action.name
|
|
241
|
+
? ` .expectNotVisible('${this.escape(action.selector || '')}', '${this.escape(action.name)}')`
|
|
242
|
+
: ` .expectNotVisible('${this.escape(action.selector || '')}')`;
|
|
243
|
+
default:
|
|
244
|
+
// Custom step WITH CONTEXT PARAMETER
|
|
245
|
+
return ` .step('${step.name || 'custom_step'}', async (context) => {
|
|
246
|
+
// Custom step: ${action.command}
|
|
247
|
+
// Access page: context.page
|
|
248
|
+
// Access variables: context.variables
|
|
249
|
+
// Access VU ID: context.vu_id
|
|
250
|
+
})`;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Update custom logic example to use context
|
|
254
|
+
generateCustomLogicExample() {
|
|
255
|
+
return `
|
|
256
|
+
.step('Custom Logic', async (context) => {
|
|
257
|
+
// Add your custom logic here
|
|
258
|
+
|
|
259
|
+
// Example: Conditional navigation
|
|
260
|
+
// const isLoggedIn = await context.page.locator('.user-menu').isVisible();
|
|
261
|
+
// if (!isLoggedIn) {
|
|
262
|
+
// await context.page.click('.login-button');
|
|
263
|
+
// }
|
|
264
|
+
|
|
265
|
+
// Example: API call
|
|
266
|
+
// const response = await fetch(\`\${context.variables.base_url}/api/status\`);
|
|
267
|
+
// const data = await response.json();
|
|
268
|
+
// context.variables.apiStatus = data.status;
|
|
269
|
+
|
|
270
|
+
// Example: Dynamic wait
|
|
271
|
+
// await context.page.waitForSelector('.dynamic-content', { timeout: 10000 });
|
|
272
|
+
|
|
273
|
+
// Example: Complex interaction
|
|
274
|
+
// const items = await context.page.locator('.item').count();
|
|
275
|
+
// for (let i = 0; i < Math.min(items, 5); i++) {
|
|
276
|
+
// await context.page.locator('.item').nth(i).click();
|
|
277
|
+
// await context.page.waitForTimeout(500);
|
|
278
|
+
// }
|
|
279
|
+
})`;
|
|
280
|
+
}
|
|
281
|
+
generateStepsDSL(steps) {
|
|
282
|
+
const dslSteps = [];
|
|
283
|
+
for (const step of steps) {
|
|
284
|
+
// Add think time if present at step level
|
|
285
|
+
if (step.think_time) {
|
|
286
|
+
dslSteps.push(` .wait('${step.think_time}')`);
|
|
287
|
+
}
|
|
288
|
+
// Generate step based on type
|
|
289
|
+
if (step.type === 'web' || step.action) {
|
|
290
|
+
dslSteps.push(this.generateWebStep(step));
|
|
291
|
+
}
|
|
292
|
+
else if (step.type === 'soap') {
|
|
293
|
+
dslSteps.push(this.generateSOAPStep(step));
|
|
294
|
+
}
|
|
295
|
+
else if (step.type === 'rest') {
|
|
296
|
+
dslSteps.push(this.generateRESTStep(step));
|
|
297
|
+
}
|
|
298
|
+
else if (step.type === 'wait') {
|
|
299
|
+
dslSteps.push(` .wait('${step.duration}')`);
|
|
300
|
+
}
|
|
301
|
+
else if (step.type === 'custom') {
|
|
302
|
+
dslSteps.push(` .step('${step.name}', ${step.script})`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return dslSteps.length > 0 ? '\n' + dslSteps.join('\n') : '';
|
|
306
|
+
}
|
|
307
|
+
generateFillStep(action) {
|
|
308
|
+
const selector = action.selector || '';
|
|
309
|
+
const value = action.value || '';
|
|
310
|
+
// Detect field type and suggest dynamic data
|
|
311
|
+
if (this.dslOptions.includeDataGeneration) {
|
|
312
|
+
if (selector.match(/email|username|user/i)) {
|
|
313
|
+
return ` .fill('${this.escape(selector)}', testData.username)`;
|
|
314
|
+
}
|
|
315
|
+
else if (selector.match(/password|pwd/i)) {
|
|
316
|
+
return ` .fill('${this.escape(selector)}', testData.password)`;
|
|
317
|
+
}
|
|
318
|
+
else if (selector.match(/first.*name/i)) {
|
|
319
|
+
return ` .fill('${this.escape(selector)}', testData.firstName)`;
|
|
320
|
+
}
|
|
321
|
+
else if (selector.match(/last.*name/i)) {
|
|
322
|
+
return ` .fill('${this.escape(selector)}', testData.lastName)`;
|
|
323
|
+
}
|
|
324
|
+
else if (selector.match(/phone|mobile|tel/i)) {
|
|
325
|
+
return ` .fill('${this.escape(selector)}', testData.phoneNumber)`;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return ` .fill('${this.escape(selector)}', '${this.escape(value)}')`;
|
|
329
|
+
}
|
|
330
|
+
generateRESTStep(step) {
|
|
331
|
+
const method = (step.method || 'GET').toLowerCase();
|
|
332
|
+
const path = step.path || step.url || '/';
|
|
333
|
+
let stepDSL = ` .${method}('${path}'`;
|
|
334
|
+
// Add body/json if present
|
|
335
|
+
if (step.json || step.body) {
|
|
336
|
+
const bodyParam = step.json || step.body;
|
|
337
|
+
stepDSL += `, ${this.formatJSONForDSL(bodyParam)}`;
|
|
338
|
+
}
|
|
339
|
+
// Add options if present
|
|
340
|
+
const options = {};
|
|
341
|
+
if (step.headers)
|
|
342
|
+
options.headers = step.headers;
|
|
343
|
+
if (step.extract)
|
|
344
|
+
options.extract = step.extract;
|
|
345
|
+
if (step.checks)
|
|
346
|
+
options.checks = step.checks;
|
|
347
|
+
if (Object.keys(options).length > 0) {
|
|
348
|
+
if (!step.json && !step.body && method !== 'get' && method !== 'delete') {
|
|
349
|
+
stepDSL += `, undefined`; // Add undefined for body parameter
|
|
350
|
+
}
|
|
351
|
+
stepDSL += `, ${this.formatJSONForDSL(options)}`;
|
|
352
|
+
}
|
|
353
|
+
stepDSL += ')';
|
|
354
|
+
// Chain extract methods if they exist
|
|
355
|
+
if (step.extract && Array.isArray(step.extract)) {
|
|
356
|
+
for (const extraction of step.extract) {
|
|
357
|
+
stepDSL += `\n .extract('${extraction.name}', '${extraction.expression || extraction.jsonPath || '$.' + extraction.name}')`;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// Chain check methods if they exist
|
|
361
|
+
if (step.checks && Array.isArray(step.checks)) {
|
|
362
|
+
for (const check of step.checks) {
|
|
363
|
+
stepDSL += `\n .check('${check.type}', ${this.formatJSONForDSL(check.value)}${check.description ? `, '${this.escape(check.description)}'` : ''})`;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return stepDSL;
|
|
367
|
+
}
|
|
368
|
+
generateSOAPStep(step) {
|
|
369
|
+
return ` .soap('${step.operation || 'operation'}', ${this.formatJSONForDSL(step.args || {})}${step.wsdl ? `, '${step.wsdl}'` : ''})`;
|
|
370
|
+
}
|
|
371
|
+
generateExportSection() {
|
|
372
|
+
return `
|
|
373
|
+
// ============================================
|
|
374
|
+
// Export Configuration
|
|
375
|
+
// ============================================
|
|
376
|
+
|
|
377
|
+
export default testConfig;
|
|
378
|
+
|
|
379
|
+
// Alternative: Run the test directly without building
|
|
380
|
+
// await test('${this.config.name}')
|
|
381
|
+
// .baseUrl('${this.config.baseUrl || 'http://localhost:3000'}')
|
|
382
|
+
// .scenario('Quick Test')
|
|
383
|
+
// .goto('/')
|
|
384
|
+
// .done()
|
|
385
|
+
// .run();
|
|
386
|
+
|
|
387
|
+
${this.dslOptions.includeMultipleLoadPatterns ? this.generateAlternativeLoadPatterns() : ''}`;
|
|
388
|
+
}
|
|
389
|
+
generateAlternativeLoadPatterns() {
|
|
390
|
+
return `// ============================================
|
|
391
|
+
// Alternative Load Patterns
|
|
392
|
+
// ============================================
|
|
393
|
+
|
|
394
|
+
// Example: Stepping Load Pattern
|
|
395
|
+
// const steppingTest = test('${this.config.name} - Stepping')
|
|
396
|
+
// .baseUrl('${this.config.baseUrl || 'http://localhost:3000'}')
|
|
397
|
+
// .scenario('User Journey', 100)
|
|
398
|
+
// // ... add your steps here ...
|
|
399
|
+
// .done()
|
|
400
|
+
// .withLoad({
|
|
401
|
+
// pattern: 'stepping',
|
|
402
|
+
// steps: [
|
|
403
|
+
// { users: 10, duration: '2m', ramp_up: '30s' },
|
|
404
|
+
// { users: 50, duration: '5m', ramp_up: '1m' },
|
|
405
|
+
// { users: 100, duration: '10m', ramp_up: '2m' }
|
|
406
|
+
// ]
|
|
407
|
+
// })
|
|
408
|
+
// .build();
|
|
409
|
+
|
|
410
|
+
// Example: Arrivals Pattern (Constant Request Rate)
|
|
411
|
+
// const arrivalsTest = test('${this.config.name} - Arrivals')
|
|
412
|
+
// .baseUrl('${this.config.baseUrl || 'http://localhost:3000'}')
|
|
413
|
+
// .scenario('User Journey', 100)
|
|
414
|
+
// // ... add your steps here ...
|
|
415
|
+
// .done()
|
|
416
|
+
// .withLoad({
|
|
417
|
+
// pattern: 'arrivals',
|
|
418
|
+
// rate: 10, // 10 requests per second
|
|
419
|
+
// duration: '5m'
|
|
420
|
+
// })
|
|
421
|
+
// .build();
|
|
422
|
+
|
|
423
|
+
// Example: Using LoadBuilder
|
|
424
|
+
// import { load } from '@perfornium/dsl';
|
|
425
|
+
//
|
|
426
|
+
// const customLoad = load()
|
|
427
|
+
// .pattern('stepping')
|
|
428
|
+
// .virtualUsers(100)
|
|
429
|
+
// .rampUp('2m')
|
|
430
|
+
// .duration('10m')
|
|
431
|
+
// .build();
|
|
432
|
+
//
|
|
433
|
+
// const testWithCustomLoad = test('${this.config.name}')
|
|
434
|
+
// .baseUrl('${this.config.baseUrl || 'http://localhost:3000'}')
|
|
435
|
+
// .scenario('User Journey', 100)
|
|
436
|
+
// // ... add your steps here ...
|
|
437
|
+
// .done()
|
|
438
|
+
// .withLoad(customLoad)
|
|
439
|
+
// .build();`;
|
|
440
|
+
}
|
|
441
|
+
getSourceInfo() {
|
|
442
|
+
const metadata = this.config.metadata || {};
|
|
443
|
+
switch (this.config.sourceType) {
|
|
444
|
+
case 'recorder':
|
|
445
|
+
return `Recorded from: ${metadata.sourceUrl || this.config.baseUrl || 'unknown'}`;
|
|
446
|
+
case 'openapi':
|
|
447
|
+
return `Imported from OpenAPI: ${metadata.importedFrom || 'API specification'}`;
|
|
448
|
+
case 'wsdl':
|
|
449
|
+
return `Imported from WSDL: ${metadata.importedFrom || 'SOAP service'}`;
|
|
450
|
+
case 'har':
|
|
451
|
+
return `Imported from HAR: ${metadata.importedFrom || 'HTTP archive'}`;
|
|
452
|
+
case 'postman':
|
|
453
|
+
return `Imported from Postman: ${metadata.importedFrom || 'collection'}`;
|
|
454
|
+
default:
|
|
455
|
+
return 'Source: Generated test configuration';
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
generateDataSection() {
|
|
459
|
+
if (!this.dslOptions.includeDataGeneration) {
|
|
460
|
+
return '// Test data generation disabled';
|
|
461
|
+
}
|
|
462
|
+
return `// ============================================
|
|
463
|
+
// Test Data Generation
|
|
464
|
+
// ============================================
|
|
465
|
+
|
|
466
|
+
const testData = {
|
|
467
|
+
// User data
|
|
468
|
+
username: faker.internet.email(),
|
|
469
|
+
password: faker.internet.password({ length: 12, memorable: true }),
|
|
470
|
+
firstName: faker.person.firstName(),
|
|
471
|
+
lastName: faker.person.lastName(),
|
|
472
|
+
phoneNumber: faker.phone.number(),
|
|
473
|
+
|
|
474
|
+
// Address data
|
|
475
|
+
street: faker.location.streetAddress(),
|
|
476
|
+
city: faker.location.city(),
|
|
477
|
+
zipCode: faker.location.zipCode(),
|
|
478
|
+
country: faker.location.country(),
|
|
479
|
+
|
|
480
|
+
// Product/Order data
|
|
481
|
+
productName: faker.commerce.productName(),
|
|
482
|
+
productPrice: faker.commerce.price(),
|
|
483
|
+
quantity: faker.number.int({ min: 1, max: 10 }),
|
|
484
|
+
orderId: faker.string.uuid(),
|
|
485
|
+
|
|
486
|
+
// Custom data
|
|
487
|
+
timestamp: Date.now(),
|
|
488
|
+
randomId: faker.string.alphanumeric(10),
|
|
489
|
+
|
|
490
|
+
// Add your custom test data here
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
// Optional: Load data from external sources
|
|
494
|
+
${this.dslOptions.includeCSVSupport ? `// const csvData = await CSV.load('./test-data.csv');` : ''}
|
|
495
|
+
|
|
496
|
+
// Optional: Environment-specific configuration
|
|
497
|
+
const config = {
|
|
498
|
+
baseUrl: process.env.BASE_URL || '${this.config.baseUrl || 'http://localhost:3000'}',
|
|
499
|
+
apiKey: process.env.API_KEY || 'test-api-key',
|
|
500
|
+
timeout: parseInt(process.env.TIMEOUT || '30000'),
|
|
501
|
+
};`;
|
|
502
|
+
}
|
|
503
|
+
generateTestScenarios() {
|
|
504
|
+
const scenarios = [];
|
|
505
|
+
for (const scenario of this.config.scenarios) {
|
|
506
|
+
scenarios.push(this.generateScenarioDSL(scenario));
|
|
507
|
+
}
|
|
508
|
+
return scenarios.join('\n\n');
|
|
509
|
+
}
|
|
510
|
+
generateHooks() {
|
|
511
|
+
return `
|
|
512
|
+
.beforeScenario(async (context) => {
|
|
513
|
+
// Setup: Authentication, test data preparation, etc.
|
|
514
|
+
console.log(\`Starting test for VU: \${context.vu_id}\`);
|
|
515
|
+
|
|
516
|
+
// Example: Get authentication token
|
|
517
|
+
// const token = await authenticate(testData.username, testData.password);
|
|
518
|
+
// context.variables.authToken = token;
|
|
519
|
+
|
|
520
|
+
// Example: Create test data via API
|
|
521
|
+
// const user = await createTestUser(testData);
|
|
522
|
+
// context.variables.userId = user.id;
|
|
523
|
+
})
|
|
524
|
+
.afterScenario(async (context) => {
|
|
525
|
+
// Cleanup: Logout, delete test data, etc.
|
|
526
|
+
console.log(\`Test completed for VU: \${context.vu_id}\`);
|
|
527
|
+
|
|
528
|
+
// Example: Cleanup test data
|
|
529
|
+
// if (context.variables.userId) {
|
|
530
|
+
// await deleteTestUser(context.variables.userId);
|
|
531
|
+
// }
|
|
532
|
+
})`;
|
|
533
|
+
}
|
|
534
|
+
generateVerifyStep(action, assertion, expectedValue) {
|
|
535
|
+
if (action.name) {
|
|
536
|
+
let verifyContent = ` .verify('${this.escape(action.name)}', async (page) => {\n`;
|
|
537
|
+
verifyContent += ` await expect(page.locator('${this.escape(action.selector || '')}')).${assertion}`;
|
|
538
|
+
if (expectedValue) {
|
|
539
|
+
verifyContent += `('${this.escape(expectedValue)}')`;
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
verifyContent += `()`;
|
|
543
|
+
}
|
|
544
|
+
verifyContent += `;\n })`;
|
|
545
|
+
return verifyContent;
|
|
546
|
+
}
|
|
547
|
+
if (expectedValue) {
|
|
548
|
+
return ` .expectText('${this.escape(action.selector || '')}', '${this.escape(expectedValue)}')`;
|
|
549
|
+
}
|
|
550
|
+
return ` .expectVisible('${this.escape(action.selector || '')}')`;
|
|
551
|
+
}
|
|
552
|
+
buildTestConfiguration() {
|
|
553
|
+
return {
|
|
554
|
+
name: this.config.name,
|
|
555
|
+
description: this.config.description || `Generated from ${this.config.sourceType || 'unknown source'}`,
|
|
556
|
+
global: {
|
|
557
|
+
base_url: this.config.baseUrl || '{{env.BASE_URL}}',
|
|
558
|
+
timeout: 30000,
|
|
559
|
+
think_time: '1-3',
|
|
560
|
+
...(this.hasWebScenarios() && {
|
|
561
|
+
browser: {
|
|
562
|
+
type: 'chromium',
|
|
563
|
+
headless: false
|
|
564
|
+
}
|
|
565
|
+
})
|
|
566
|
+
},
|
|
567
|
+
load: {
|
|
568
|
+
pattern: 'basic',
|
|
569
|
+
virtual_users: this.getVirtualUsers(),
|
|
570
|
+
ramp_up: this.getRampUp(),
|
|
571
|
+
duration: this.getDuration()
|
|
572
|
+
},
|
|
573
|
+
scenarios: this.config.scenarios,
|
|
574
|
+
outputs: [
|
|
575
|
+
{ type: 'json', file: 'results/test-results.json' }
|
|
576
|
+
],
|
|
577
|
+
report: {
|
|
578
|
+
generate: true,
|
|
579
|
+
output: 'reports/test-report.html'
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
hasWebScenarios() {
|
|
584
|
+
return this.config.scenarios.some((s) => s.steps?.some((step) => step.type === 'web' || step.action));
|
|
585
|
+
}
|
|
586
|
+
getVirtualUsers() {
|
|
587
|
+
return this.config.sourceType === 'recorder' ? 1 : 5;
|
|
588
|
+
}
|
|
589
|
+
getRampUp() {
|
|
590
|
+
return this.config.sourceType === 'recorder' ? '10s' : '30s';
|
|
591
|
+
}
|
|
592
|
+
getDuration() {
|
|
593
|
+
return this.config.sourceType === 'recorder' ? '2m' : '5m';
|
|
594
|
+
}
|
|
595
|
+
formatJSONForDSL(obj) {
|
|
596
|
+
if (typeof obj === 'string') {
|
|
597
|
+
return `'${this.escape(obj)}'`;
|
|
598
|
+
}
|
|
599
|
+
const json = JSON.stringify(obj, null, 4);
|
|
600
|
+
return json.split('\n').map((line, i) => i === 0 ? line : ' ' + line).join('\n');
|
|
601
|
+
}
|
|
602
|
+
toCamelCase(str) {
|
|
603
|
+
return str
|
|
604
|
+
.toLowerCase()
|
|
605
|
+
.replace(/[^a-zA-Z0-9]+(.)/g, (_, chr) => chr.toUpperCase())
|
|
606
|
+
.replace(/^[^a-zA-Z]+/, '');
|
|
607
|
+
}
|
|
608
|
+
escape(str) {
|
|
609
|
+
return str
|
|
610
|
+
.replace(/\\/g, '\\\\')
|
|
611
|
+
.replace(/'/g, "\\'")
|
|
612
|
+
.replace(/"/g, '\\"')
|
|
613
|
+
.replace(/\n/g, '\\n')
|
|
614
|
+
.replace(/\r/g, '\\r')
|
|
615
|
+
.replace(/\t/g, '\\t');
|
|
616
|
+
}
|
|
617
|
+
// Static helper method for getting safe filename
|
|
618
|
+
static async getSafeFilename(filepath) {
|
|
619
|
+
try {
|
|
620
|
+
await fs.promises.access(filepath);
|
|
621
|
+
// File exists, create numbered version
|
|
622
|
+
const ext = path.extname(filepath);
|
|
623
|
+
const base = filepath.slice(0, -ext.length);
|
|
624
|
+
let counter = 1;
|
|
625
|
+
let newPath;
|
|
626
|
+
do {
|
|
627
|
+
newPath = `${base}_${counter}${ext}`;
|
|
628
|
+
counter++;
|
|
629
|
+
try {
|
|
630
|
+
await fs.promises.access(newPath);
|
|
631
|
+
}
|
|
632
|
+
catch {
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
} while (counter < 100);
|
|
636
|
+
return newPath;
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
return filepath; // File doesn't exist, use original
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
exports.TestOutputWriter = TestOutputWriter;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseTime = parseTime;
|
|
4
|
+
exports.sleep = sleep;
|
|
5
|
+
exports.randomBetween = randomBetween;
|
|
6
|
+
function parseTime(timeStr) {
|
|
7
|
+
if (typeof timeStr === 'number')
|
|
8
|
+
return timeStr;
|
|
9
|
+
const units = {
|
|
10
|
+
'ms': 1, 's': 1000, 'm': 60 * 1000, 'h': 60 * 60 * 1000
|
|
11
|
+
};
|
|
12
|
+
const match = timeStr.match(/^(\d+(?:\.\d+)?)(ms|s|m|h)$/);
|
|
13
|
+
if (!match)
|
|
14
|
+
throw new Error(`Invalid time format: ${timeStr}`);
|
|
15
|
+
const [, value, unit] = match;
|
|
16
|
+
return parseFloat(value) * units[unit];
|
|
17
|
+
}
|
|
18
|
+
function sleep(ms) {
|
|
19
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
20
|
+
}
|
|
21
|
+
function randomBetween(min, max) {
|
|
22
|
+
return Math.random() * (max - min) + min;
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced timestamp utility for file naming and templating
|
|
3
|
+
*/
|
|
4
|
+
export declare class TimestampHelper {
|
|
5
|
+
/**
|
|
6
|
+
* Generate various timestamp formats
|
|
7
|
+
*/
|
|
8
|
+
static getTimestamp(format?: 'unix' | 'iso' | 'readable' | 'file'): string;
|
|
9
|
+
/**
|
|
10
|
+
* Create filename with timestamp ensuring directory exists
|
|
11
|
+
*/
|
|
12
|
+
static createTimestampedPath(template: string, format?: 'unix' | 'iso' | 'readable' | 'file'): string;
|
|
13
|
+
/**
|
|
14
|
+
* Generate filename ensuring uniqueness
|
|
15
|
+
*/
|
|
16
|
+
static generateUniqueFilename(baseTemplate: string): string;
|
|
17
|
+
}
|