@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,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,3 @@
1
+ export declare function parseTime(timeStr: string | number): number;
2
+ export declare function sleep(ms: number): Promise<void>;
3
+ export declare function randomBetween(min: number, max: number): number;
@@ -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
+ }