@testsmith/perfornium 0.1.0 → 0.2.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.
@@ -2,6 +2,15 @@ import { TestConfiguration } from './types';
2
2
  export declare class ConfigParser {
3
3
  private templateProcessor;
4
4
  parse(configPath: string, environment?: string): Promise<TestConfiguration>;
5
+ /**
6
+ * Process includes - load scenarios from external files
7
+ *
8
+ * Supports:
9
+ * - includes: ["scenarios/login.yml", "scenarios/checkout.yml"]
10
+ * - Included files can contain single scenario or array of scenarios
11
+ * - Scenarios are merged with inline scenarios
12
+ */
13
+ private processIncludes;
5
14
  private parseTypeScript;
6
15
  private evaluatePlainTypeScript;
7
16
  private setupBaseDirectories;
@@ -62,6 +62,8 @@ class ConfigParser {
62
62
  const configContent = fs.readFileSync(configPath, 'utf8');
63
63
  config = this.parseContent(configContent);
64
64
  }
65
+ // Process includes (scenarios from external files)
66
+ config = await this.processIncludes(config, path.dirname(configPath));
65
67
  logger_1.logger.debug(`Parsed config.global: ${JSON.stringify(config.global, null, 2)}`);
66
68
  // Setup faker configuration if specified
67
69
  logger_1.logger.debug('About to setup faker...');
@@ -77,6 +79,55 @@ class ConfigParser {
77
79
  }
78
80
  return config;
79
81
  }
82
+ /**
83
+ * Process includes - load scenarios from external files
84
+ *
85
+ * Supports:
86
+ * - includes: ["scenarios/login.yml", "scenarios/checkout.yml"]
87
+ * - Included files can contain single scenario or array of scenarios
88
+ * - Scenarios are merged with inline scenarios
89
+ */
90
+ async processIncludes(config, baseDir) {
91
+ if (!config.includes || !Array.isArray(config.includes)) {
92
+ return config;
93
+ }
94
+ logger_1.logger.debug(`Processing ${config.includes.length} include(s)`);
95
+ const includedScenarios = [];
96
+ for (const includePath of config.includes) {
97
+ const fullPath = path.isAbsolute(includePath)
98
+ ? includePath
99
+ : path.join(baseDir, includePath);
100
+ if (!fs.existsSync(fullPath)) {
101
+ throw new Error(`Included file not found: ${fullPath}`);
102
+ }
103
+ logger_1.logger.debug(`Loading included file: ${fullPath}`);
104
+ const content = fs.readFileSync(fullPath, 'utf8');
105
+ const parsed = this.parseContent(content);
106
+ // Support both single scenario object and array of scenarios
107
+ if (Array.isArray(parsed)) {
108
+ // File contains array of scenarios
109
+ includedScenarios.push(...parsed);
110
+ }
111
+ else if (parsed.scenarios && Array.isArray(parsed.scenarios)) {
112
+ // File contains scenarios property
113
+ includedScenarios.push(...parsed.scenarios);
114
+ }
115
+ else if (parsed.name && parsed.steps) {
116
+ // File contains single scenario
117
+ includedScenarios.push(parsed);
118
+ }
119
+ else {
120
+ logger_1.logger.warn(`Included file ${includePath} has no valid scenarios`);
121
+ }
122
+ }
123
+ // Merge included scenarios with inline scenarios
124
+ const inlineScenarios = config.scenarios || [];
125
+ config.scenarios = [...includedScenarios, ...inlineScenarios];
126
+ // Remove includes from config (no longer needed)
127
+ delete config.includes;
128
+ logger_1.logger.debug(`Total scenarios after includes: ${config.scenarios.length}`);
129
+ return config;
130
+ }
80
131
  async parseTypeScript(configPath) {
81
132
  try {
82
133
  // Resolve to absolute path
@@ -1,8 +1,8 @@
1
1
  import { StepHooks } from "./hooks";
2
- export type Step = RESTStep | SOAPStep | WebStep | CustomStep | WaitStep | ScriptStep;
2
+ export type Step = RESTStep | SOAPStep | WebStep | CustomStep | WaitStep | ScriptStep | RendezvousStep;
3
3
  export interface BaseStep {
4
4
  name?: string;
5
- type?: 'rest' | 'soap' | 'web' | 'custom' | 'wait' | 'script';
5
+ type?: 'rest' | 'soap' | 'web' | 'custom' | 'wait' | 'script' | 'rendezvous';
6
6
  condition?: string;
7
7
  continueOnError?: boolean;
8
8
  hooks?: StepHooks;
@@ -79,6 +79,21 @@ export interface ScriptStep extends BaseStep {
79
79
  returns?: string;
80
80
  timeout?: number;
81
81
  }
82
+ /**
83
+ * Rendezvous step for synchronized VU coordination
84
+ *
85
+ * Creates a synchronization point where VUs wait for each other before proceeding.
86
+ * Use this to create coordinated load spikes at specific points in the test.
87
+ *
88
+ * Example: 10 VUs simultaneously checking account balances
89
+ */
90
+ export interface RendezvousStep extends BaseStep {
91
+ type: 'rendezvous';
92
+ rendezvous: string;
93
+ count: number;
94
+ timeout?: number | string;
95
+ policy?: 'all' | 'count';
96
+ }
82
97
  export interface WebAction {
83
98
  name?: string;
84
99
  expected_text?: string;
@@ -3,3 +3,5 @@ export { VirtualUser } from './virtual-user';
3
3
  export { StepExecutor } from './step-executor';
4
4
  export { CSVDataProvider } from './csv-data-provider';
5
5
  export type { CSVDataRow } from './csv-data-provider';
6
+ export { RendezvousManager } from './rendezvous';
7
+ export type { RendezvousConfig, RendezvousResult, RendezvousStats } from './rendezvous';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CSVDataProvider = exports.StepExecutor = exports.VirtualUser = exports.TestRunner = void 0;
3
+ exports.RendezvousManager = exports.CSVDataProvider = exports.StepExecutor = exports.VirtualUser = exports.TestRunner = void 0;
4
4
  var test_runner_1 = require("./test-runner");
5
5
  Object.defineProperty(exports, "TestRunner", { enumerable: true, get: function () { return test_runner_1.TestRunner; } });
6
6
  var virtual_user_1 = require("./virtual-user");
@@ -9,3 +9,5 @@ var step_executor_1 = require("./step-executor");
9
9
  Object.defineProperty(exports, "StepExecutor", { enumerable: true, get: function () { return step_executor_1.StepExecutor; } });
10
10
  var csv_data_provider_1 = require("./csv-data-provider");
11
11
  Object.defineProperty(exports, "CSVDataProvider", { enumerable: true, get: function () { return csv_data_provider_1.CSVDataProvider; } });
12
+ var rendezvous_1 = require("./rendezvous");
13
+ Object.defineProperty(exports, "RendezvousManager", { enumerable: true, get: function () { return rendezvous_1.RendezvousManager; } });
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Rendezvous Manager for synchronized VU coordination
3
+ *
4
+ * Rendezvous points allow Virtual Users to wait for each other at specific
5
+ * points in the test execution, creating coordinated load spikes.
6
+ *
7
+ * Like a fairground ride queue:
8
+ * - VUs enter the queue and wait
9
+ * - When enough VUs are waiting (release count), they all proceed together
10
+ * - If timeout expires, waiting VUs are released regardless of count
11
+ */
12
+ import { EventEmitter } from 'events';
13
+ export interface RendezvousConfig {
14
+ name: string;
15
+ count: number;
16
+ timeout?: number;
17
+ releasePolicy?: 'all' | 'count';
18
+ }
19
+ export declare class RendezvousManager extends EventEmitter {
20
+ private static instance;
21
+ private rendezvousPoints;
22
+ private isActive;
23
+ private constructor();
24
+ static getInstance(): RendezvousManager;
25
+ /**
26
+ * Reset the manager for a new test run
27
+ */
28
+ reset(): void;
29
+ /**
30
+ * Stop the manager and release all waiting VUs
31
+ */
32
+ stop(): void;
33
+ /**
34
+ * Wait at a rendezvous point
35
+ * Returns when either:
36
+ * - The required number of VUs are waiting (synchronized release)
37
+ * - The timeout expires
38
+ * - The test is stopped
39
+ */
40
+ wait(config: RendezvousConfig, vuId: number): Promise<RendezvousResult>;
41
+ /**
42
+ * Release VUs from a rendezvous point
43
+ */
44
+ private releaseVUs;
45
+ /**
46
+ * Force release all waiting VUs (used during shutdown)
47
+ */
48
+ private releaseWaitingVUs;
49
+ /**
50
+ * Get statistics for a rendezvous point
51
+ */
52
+ getStats(name: string): RendezvousStats | null;
53
+ /**
54
+ * Get all rendezvous statistics
55
+ */
56
+ getAllStats(): RendezvousStats[];
57
+ }
58
+ export interface RendezvousResult {
59
+ released: boolean;
60
+ reason: 'count_reached' | 'timeout' | 'manager_inactive' | 'error';
61
+ waitTime: number;
62
+ vuCount: number;
63
+ }
64
+ export interface RendezvousStats {
65
+ name: string;
66
+ requiredCount: number;
67
+ currentlyWaiting: number;
68
+ totalReleased: number;
69
+ timeout: number;
70
+ }
@@ -0,0 +1,216 @@
1
+ "use strict";
2
+ /**
3
+ * Rendezvous Manager for synchronized VU coordination
4
+ *
5
+ * Rendezvous points allow Virtual Users to wait for each other at specific
6
+ * points in the test execution, creating coordinated load spikes.
7
+ *
8
+ * Like a fairground ride queue:
9
+ * - VUs enter the queue and wait
10
+ * - When enough VUs are waiting (release count), they all proceed together
11
+ * - If timeout expires, waiting VUs are released regardless of count
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.RendezvousManager = void 0;
15
+ const logger_1 = require("../utils/logger");
16
+ const events_1 = require("events");
17
+ class RendezvousManager extends events_1.EventEmitter {
18
+ constructor() {
19
+ super();
20
+ this.rendezvousPoints = new Map();
21
+ this.isActive = true;
22
+ }
23
+ static getInstance() {
24
+ if (!RendezvousManager.instance) {
25
+ RendezvousManager.instance = new RendezvousManager();
26
+ }
27
+ return RendezvousManager.instance;
28
+ }
29
+ /**
30
+ * Reset the manager for a new test run
31
+ */
32
+ reset() {
33
+ // Clear all timeout handles
34
+ for (const point of this.rendezvousPoints.values()) {
35
+ if (point.timeoutHandle) {
36
+ clearTimeout(point.timeoutHandle);
37
+ }
38
+ // Release any waiting VUs
39
+ for (const vu of point.waiting) {
40
+ vu.reject(new Error('Rendezvous manager reset'));
41
+ }
42
+ }
43
+ this.rendezvousPoints.clear();
44
+ this.isActive = true;
45
+ logger_1.logger.debug('RendezvousManager reset');
46
+ }
47
+ /**
48
+ * Stop the manager and release all waiting VUs
49
+ */
50
+ stop() {
51
+ this.isActive = false;
52
+ for (const point of this.rendezvousPoints.values()) {
53
+ if (point.timeoutHandle) {
54
+ clearTimeout(point.timeoutHandle);
55
+ }
56
+ // Release all waiting VUs
57
+ this.releaseWaitingVUs(point, 'Test stopped');
58
+ }
59
+ logger_1.logger.debug('RendezvousManager stopped');
60
+ }
61
+ /**
62
+ * Wait at a rendezvous point
63
+ * Returns when either:
64
+ * - The required number of VUs are waiting (synchronized release)
65
+ * - The timeout expires
66
+ * - The test is stopped
67
+ */
68
+ async wait(config, vuId) {
69
+ if (!this.isActive) {
70
+ return {
71
+ released: true,
72
+ reason: 'manager_inactive',
73
+ waitTime: 0,
74
+ vuCount: 0
75
+ };
76
+ }
77
+ const startTime = Date.now();
78
+ const pointName = config.name;
79
+ // Get or create rendezvous point
80
+ let point = this.rendezvousPoints.get(pointName);
81
+ if (!point) {
82
+ point = {
83
+ config: {
84
+ ...config,
85
+ timeout: config.timeout ?? 30000,
86
+ releasePolicy: config.releasePolicy ?? 'all'
87
+ },
88
+ waiting: [],
89
+ releasedCount: 0
90
+ };
91
+ this.rendezvousPoints.set(pointName, point);
92
+ logger_1.logger.debug(`Rendezvous point '${pointName}' created (count: ${config.count}, timeout: ${point.config.timeout}ms)`);
93
+ }
94
+ logger_1.logger.debug(`VU${vuId} arriving at rendezvous '${pointName}' (${point.waiting.length + 1}/${config.count})`);
95
+ return new Promise((resolve, reject) => {
96
+ const waitingVU = {
97
+ vuId,
98
+ resolve: () => {
99
+ const waitTime = Date.now() - startTime;
100
+ resolve({
101
+ released: true,
102
+ reason: 'count_reached',
103
+ waitTime,
104
+ vuCount: point.waiting.length + 1
105
+ });
106
+ },
107
+ reject,
108
+ arrivalTime: startTime
109
+ };
110
+ point.waiting.push(waitingVU);
111
+ // Emit event for monitoring
112
+ this.emit('vu_arrived', {
113
+ rendezvousName: pointName,
114
+ vuId,
115
+ waitingCount: point.waiting.length,
116
+ requiredCount: config.count
117
+ });
118
+ // Check if we've reached the required count
119
+ if (point.waiting.length >= config.count) {
120
+ logger_1.logger.debug(`Rendezvous '${pointName}' reached count (${point.waiting.length}/${config.count}) - releasing VUs`);
121
+ this.releaseVUs(point);
122
+ return;
123
+ }
124
+ // Set up timeout if not already set and timeout > 0
125
+ if (!point.timeoutHandle && point.config.timeout && point.config.timeout > 0) {
126
+ point.timeoutHandle = setTimeout(() => {
127
+ if (point.waiting.length > 0) {
128
+ logger_1.logger.debug(`Rendezvous '${pointName}' timeout - releasing ${point.waiting.length} VUs`);
129
+ this.releaseVUs(point, 'timeout');
130
+ }
131
+ }, point.config.timeout);
132
+ }
133
+ });
134
+ }
135
+ /**
136
+ * Release VUs from a rendezvous point
137
+ */
138
+ releaseVUs(point, reason = 'count_reached') {
139
+ // Clear timeout
140
+ if (point.timeoutHandle) {
141
+ clearTimeout(point.timeoutHandle);
142
+ point.timeoutHandle = undefined;
143
+ }
144
+ const toRelease = point.config.releasePolicy === 'count'
145
+ ? point.waiting.splice(0, point.config.count)
146
+ : point.waiting.splice(0);
147
+ const releaseTime = Date.now();
148
+ for (const vu of toRelease) {
149
+ const waitTime = releaseTime - vu.arrivalTime;
150
+ point.releasedCount++;
151
+ // Resolve the promise
152
+ vu.resolve();
153
+ logger_1.logger.debug(`VU${vu.vuId} released from rendezvous '${point.config.name}' (waited ${waitTime}ms, reason: ${reason})`);
154
+ }
155
+ // Emit release event
156
+ this.emit('vus_released', {
157
+ rendezvousName: point.config.name,
158
+ releasedCount: toRelease.length,
159
+ reason
160
+ });
161
+ // Reset timeout for next batch if there are remaining VUs
162
+ if (point.waiting.length > 0 && point.config.timeout && point.config.timeout > 0) {
163
+ point.timeoutHandle = setTimeout(() => {
164
+ if (point.waiting.length > 0) {
165
+ this.releaseVUs(point, 'timeout');
166
+ }
167
+ }, point.config.timeout);
168
+ }
169
+ }
170
+ /**
171
+ * Force release all waiting VUs (used during shutdown)
172
+ */
173
+ releaseWaitingVUs(point, reason) {
174
+ const toRelease = point.waiting.splice(0);
175
+ for (const vu of toRelease) {
176
+ vu.resolve();
177
+ }
178
+ this.emit('vus_released', {
179
+ rendezvousName: point.config.name,
180
+ releasedCount: toRelease.length,
181
+ reason
182
+ });
183
+ }
184
+ /**
185
+ * Get statistics for a rendezvous point
186
+ */
187
+ getStats(name) {
188
+ const point = this.rendezvousPoints.get(name);
189
+ if (!point)
190
+ return null;
191
+ return {
192
+ name: point.config.name,
193
+ requiredCount: point.config.count,
194
+ currentlyWaiting: point.waiting.length,
195
+ totalReleased: point.releasedCount,
196
+ timeout: point.config.timeout ?? 0
197
+ };
198
+ }
199
+ /**
200
+ * Get all rendezvous statistics
201
+ */
202
+ getAllStats() {
203
+ const stats = [];
204
+ for (const point of this.rendezvousPoints.values()) {
205
+ stats.push({
206
+ name: point.config.name,
207
+ requiredCount: point.config.count,
208
+ currentlyWaiting: point.waiting.length,
209
+ totalReleased: point.releasedCount,
210
+ timeout: point.config.timeout ?? 0
211
+ });
212
+ }
213
+ return stats;
214
+ }
215
+ }
216
+ exports.RendezvousManager = RendezvousManager;
@@ -29,6 +29,7 @@ export declare class StepExecutor {
29
29
  private executeWebStep;
30
30
  private executeCustomStep;
31
31
  private executeWaitStep;
32
+ private executeRendezvousStep;
32
33
  private executeScriptStep;
33
34
  private executeScript;
34
35
  private evaluateCondition;
@@ -37,6 +37,7 @@ exports.StepExecutor = void 0;
37
37
  const hooks_manager_1 = require("./hooks-manager");
38
38
  const script_executor_1 = require("./script-executor");
39
39
  const threshold_evaluator_1 = require("./threshold-evaluator");
40
+ const rendezvous_1 = require("./rendezvous");
40
41
  const time_1 = require("../utils/time");
41
42
  const template_1 = require("../utils/template");
42
43
  const logger_1 = require("../utils/logger");
@@ -205,6 +206,9 @@ class StepExecutor {
205
206
  case 'script':
206
207
  result = await this.executeScriptStep(processedStep, context);
207
208
  break;
209
+ case 'rendezvous':
210
+ result = await this.executeRendezvousStep(processedStep, context);
211
+ break;
208
212
  default:
209
213
  throw new Error(`Unsupported step type: ${step.type}`);
210
214
  }
@@ -443,6 +447,54 @@ class StepExecutor {
443
447
  custom_metrics: { wait_duration: duration }
444
448
  };
445
449
  }
450
+ async executeRendezvousStep(step, context) {
451
+ const rendezvousManager = rendezvous_1.RendezvousManager.getInstance();
452
+ // Parse timeout - can be number (ms) or string ("30s", "1m")
453
+ let timeoutMs = 30000; // Default 30 seconds
454
+ if (step.timeout !== undefined) {
455
+ if (typeof step.timeout === 'number') {
456
+ timeoutMs = step.timeout;
457
+ }
458
+ else if (typeof step.timeout === 'string') {
459
+ timeoutMs = (0, time_1.parseTime)(step.timeout);
460
+ }
461
+ }
462
+ try {
463
+ const result = await rendezvousManager.wait({
464
+ name: step.rendezvous,
465
+ count: step.count,
466
+ timeout: timeoutMs,
467
+ releasePolicy: step.policy || 'all'
468
+ }, context.vu_id);
469
+ return {
470
+ success: true,
471
+ data: {
472
+ rendezvous: step.rendezvous,
473
+ released: result.released,
474
+ reason: result.reason,
475
+ vuCount: result.vuCount
476
+ },
477
+ response_time: result.waitTime,
478
+ custom_metrics: {
479
+ rendezvous_name: step.rendezvous,
480
+ rendezvous_wait_time: result.waitTime,
481
+ rendezvous_reason: result.reason,
482
+ rendezvous_vu_count: result.vuCount
483
+ },
484
+ shouldRecord: true // Always record rendezvous timing
485
+ };
486
+ }
487
+ catch (error) {
488
+ return {
489
+ success: false,
490
+ error: error.message,
491
+ custom_metrics: {
492
+ rendezvous_name: step.rendezvous,
493
+ rendezvous_error: true
494
+ }
495
+ };
496
+ }
497
+ }
446
498
  async executeScriptStep(step, context) {
447
499
  const { file, function: funcName, params, returns, timeout = 30000 } = step;
448
500
  const path = require('path');
@@ -50,6 +50,7 @@ const webhook_1 = require("../outputs/webhook");
50
50
  const logger_1 = require("../utils/logger");
51
51
  const time_1 = require("../utils/time");
52
52
  const csv_data_provider_1 = require("./csv-data-provider");
53
+ const rendezvous_1 = require("./rendezvous");
53
54
  const file_manager_1 = require("../utils/file-manager");
54
55
  class TestRunner {
55
56
  constructor(config) {
@@ -83,6 +84,8 @@ class TestRunner {
83
84
  logger_1.logger.info(`🚀 Starting test: ${this.config.name}`);
84
85
  this.isRunning = true;
85
86
  this.startTime = Date.now();
87
+ // Reset rendezvous manager for this test run
88
+ rendezvous_1.RendezvousManager.getInstance().reset();
86
89
  try {
87
90
  await this.initialize();
88
91
  // NO CSV termination callback setup needed anymore
@@ -116,6 +119,8 @@ class TestRunner {
116
119
  async stop() {
117
120
  logger_1.logger.info('âšī¸ Stopping test...');
118
121
  this.isRunning = false;
122
+ // Stop rendezvous manager - releases any waiting VUs
123
+ rendezvous_1.RendezvousManager.getInstance().stop();
119
124
  // Stop all active VUs
120
125
  this.activeVUs.forEach(vu => vu.stop());
121
126
  // Wait for VUs to finish current operations
@@ -35,6 +35,15 @@ export declare class VirtualUser {
35
35
  executeScenario(scenario: Scenario): Promise<void>;
36
36
  private loadCSVDataIfNeeded;
37
37
  private loadCSVDataForScenario;
38
+ /**
39
+ * Select scenarios based on weights using proportional distribution.
40
+ *
41
+ * Weights determine the probability of a scenario being selected:
42
+ * - scenario1 (weight: 50) + scenario2 (weight: 25) + scenario3 (weight: 25) = 100
43
+ * - 50% of VUs will run scenario1, 25% scenario2, 25% scenario3
44
+ *
45
+ * If weights don't sum to 100, they are normalized proportionally.
46
+ */
38
47
  private selectScenarios;
39
48
  private executeSetup;
40
49
  private executeTeardown;
@@ -366,20 +366,36 @@ class VirtualUser {
366
366
  return await provider.getNextRow(this.id);
367
367
  }
368
368
  }
369
+ /**
370
+ * Select scenarios based on weights using proportional distribution.
371
+ *
372
+ * Weights determine the probability of a scenario being selected:
373
+ * - scenario1 (weight: 50) + scenario2 (weight: 25) + scenario3 (weight: 25) = 100
374
+ * - 50% of VUs will run scenario1, 25% scenario2, 25% scenario3
375
+ *
376
+ * If weights don't sum to 100, they are normalized proportionally.
377
+ */
369
378
  selectScenarios(scenarios) {
370
- const selected = [];
379
+ if (scenarios.length === 0) {
380
+ return [];
381
+ }
382
+ if (scenarios.length === 1) {
383
+ return scenarios;
384
+ }
385
+ // Calculate total weight
386
+ const totalWeight = scenarios.reduce((sum, s) => sum + (s.weight ?? 100), 0);
387
+ // Generate random value between 0 and totalWeight
388
+ const random = Math.random() * totalWeight;
389
+ // Select scenario based on cumulative weight
390
+ let cumulative = 0;
371
391
  for (const scenario of scenarios) {
372
- const weight = scenario.weight || 100;
373
- const random = Math.random() * 100;
374
- if (random < weight) {
375
- selected.push(scenario);
392
+ cumulative += (scenario.weight ?? 100);
393
+ if (random < cumulative) {
394
+ return [scenario];
376
395
  }
377
396
  }
378
- // Ensure at least one scenario is selected
379
- if (selected.length === 0 && scenarios.length > 0) {
380
- selected.push(scenarios[0]);
381
- }
382
- return selected;
397
+ // Fallback to last scenario (should not reach here)
398
+ return [scenarios[scenarios.length - 1]];
383
399
  }
384
400
  async executeSetup(setupScript) {
385
401
  try {
@@ -27,6 +27,24 @@ export declare class ScenarioBuilder {
27
27
  expectText(selector: string, text: string, name?: string): this;
28
28
  expectNotVisible(selector: string, name?: string): this;
29
29
  wait(duration: string | number): this;
30
+ /**
31
+ * Add a rendezvous point to synchronize Virtual Users
32
+ *
33
+ * @param name - Unique name for this rendezvous point
34
+ * @param count - Number of VUs to wait for before releasing
35
+ * @param options - Optional configuration (timeout, policy)
36
+ *
37
+ * @example
38
+ * scenario('Flash Sale')
39
+ * .post('/cart/add', { productId: 'LIMITED-001' })
40
+ * .rendezvous('checkout_sync', 10, { timeout: '30s' })
41
+ * .post('/checkout', { paymentMethod: 'card' })
42
+ * .done()
43
+ */
44
+ rendezvous(name: string, count: number, options?: {
45
+ timeout?: string | number;
46
+ policy?: 'all' | 'count';
47
+ }): this;
30
48
  get(path: string, options?: any): this;
31
49
  post(path: string, body?: any, options?: any): this;
32
50
  put(path: string, body?: any, options?: any): this;
@@ -163,6 +163,30 @@ class ScenarioBuilder {
163
163
  duration: durationStr
164
164
  });
165
165
  }
166
+ /**
167
+ * Add a rendezvous point to synchronize Virtual Users
168
+ *
169
+ * @param name - Unique name for this rendezvous point
170
+ * @param count - Number of VUs to wait for before releasing
171
+ * @param options - Optional configuration (timeout, policy)
172
+ *
173
+ * @example
174
+ * scenario('Flash Sale')
175
+ * .post('/cart/add', { productId: 'LIMITED-001' })
176
+ * .rendezvous('checkout_sync', 10, { timeout: '30s' })
177
+ * .post('/checkout', { paymentMethod: 'card' })
178
+ * .done()
179
+ */
180
+ rendezvous(name, count, options) {
181
+ return this.addStep({
182
+ name: `Rendezvous: ${name}`,
183
+ type: 'rendezvous',
184
+ rendezvous: name,
185
+ count,
186
+ ...(options?.timeout && { timeout: options.timeout }),
187
+ ...(options?.policy && { policy: options.policy })
188
+ });
189
+ }
166
190
  // REST API step methods
167
191
  get(path, options) {
168
192
  return this.request('GET', path, options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testsmith/perfornium",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Flexible performance testing framework for REST, SOAP, and web applications",
5
5
  "author": "TestSmith",
6
6
  "license": "MIT",