@apiquest/fracture 1.0.4 → 1.0.5

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 (59) hide show
  1. package/README.md +90 -2
  2. package/dist/CollectionRunner.d.ts +2 -0
  3. package/dist/CollectionRunner.d.ts.map +1 -1
  4. package/dist/CollectionRunner.js +20 -2
  5. package/dist/CollectionRunner.js.map +1 -1
  6. package/dist/LibraryLoader.d.ts +49 -0
  7. package/dist/LibraryLoader.d.ts.map +1 -0
  8. package/dist/LibraryLoader.js +198 -0
  9. package/dist/LibraryLoader.js.map +1 -0
  10. package/dist/PluginLoader.d.ts.map +1 -1
  11. package/dist/PluginLoader.js +9 -6
  12. package/dist/PluginLoader.js.map +1 -1
  13. package/dist/PluginResolver.d.ts +1 -1
  14. package/dist/PluginResolver.d.ts.map +1 -1
  15. package/dist/PluginResolver.js +1 -1
  16. package/dist/PluginResolver.js.map +1 -1
  17. package/dist/ScriptEngine.d.ts +2 -1
  18. package/dist/ScriptEngine.d.ts.map +1 -1
  19. package/dist/ScriptEngine.js +15 -8
  20. package/dist/ScriptEngine.js.map +1 -1
  21. package/dist/cli/index.js +35 -3
  22. package/dist/cli/index.js.map +1 -1
  23. package/dist/cli/plugin-commands.d.ts.map +1 -1
  24. package/dist/cli/plugin-commands.js +47 -81
  25. package/dist/cli/plugin-commands.js.map +1 -1
  26. package/dist/cli/plugin-installer.d.ts +48 -0
  27. package/dist/cli/plugin-installer.d.ts.map +1 -0
  28. package/dist/cli/plugin-installer.js +136 -0
  29. package/dist/cli/plugin-installer.js.map +1 -0
  30. package/dist/cli/plugin-registry.d.ts +17 -0
  31. package/dist/cli/plugin-registry.d.ts.map +1 -0
  32. package/dist/cli/plugin-registry.js +77 -0
  33. package/dist/cli/plugin-registry.js.map +1 -0
  34. package/package.json +1 -1
  35. package/src/CollectionAnalyzer.ts +0 -102
  36. package/src/CollectionRunner.ts +0 -1423
  37. package/src/CollectionRunner.types.ts +0 -9
  38. package/src/CollectionValidator.ts +0 -289
  39. package/src/ConsoleReporter.ts +0 -143
  40. package/src/CookieJar.ts +0 -258
  41. package/src/DagScheduler.ts +0 -439
  42. package/src/Logger.ts +0 -85
  43. package/src/PluginLoader.ts +0 -126
  44. package/src/PluginManager.ts +0 -208
  45. package/src/PluginResolver.ts +0 -154
  46. package/src/QuestAPI.ts +0 -764
  47. package/src/QuestAPI.types.ts +0 -33
  48. package/src/QuestTestAPI.ts +0 -164
  49. package/src/RequestFilter.ts +0 -224
  50. package/src/ScriptEngine.ts +0 -219
  51. package/src/ScriptValidator.ts +0 -428
  52. package/src/TaskGraph.ts +0 -598
  53. package/src/TestCounter.ts +0 -109
  54. package/src/VariableResolver.ts +0 -114
  55. package/src/cli/index.ts +0 -480
  56. package/src/cli/plugin-commands.ts +0 -342
  57. package/src/cli/plugin-discovery.ts +0 -44
  58. package/src/index.ts +0 -24
  59. package/src/utils.ts +0 -52
package/src/QuestAPI.ts DELETED
@@ -1,764 +0,0 @@
1
- import type { ExecutionContext, TestResult, Cookie, CookieSetOptions } from '@apiquest/types';
2
- import { ScriptType } from '@apiquest/types';
3
- import { createQuestTestAPI } from './QuestTestAPI.js';
4
- import type { CookieJar } from './CookieJar.js';
5
- import type { RequestConfig, ResponseObject, HistoryFilterCriteria } from './QuestAPI.types.js';
6
- import { isNullOrWhitespace } from './utils.js';
7
-
8
- /**
9
- * Helper: Execute HTTP request and return response object
10
- * Used by quest.sendRequest() to make requests from scripts
11
- */
12
- async function executeHttpRequest(config: RequestConfig, signal: AbortSignal): Promise<ResponseObject> {
13
- // Use native fetch
14
- const url = config.url;
15
- const method = config.method ?? 'GET';
16
- const headers = config.header ?? config.headers ?? {};
17
-
18
- if (isNullOrWhitespace(url)) {
19
- throw new Error('sendRequest requires a "url" property');
20
- }
21
-
22
- const fetchOptions: RequestInit = {
23
- method,
24
- headers,
25
- signal
26
- };
27
-
28
- // Handle body
29
- if (config.body !== null && config.body !== undefined) {
30
- if (typeof config.body === 'object' && 'mode' in config.body && config.body.mode !== null && config.body.mode !== undefined) {
31
- // Handle different body modes
32
- if (config.body.mode === 'raw') {
33
- fetchOptions.body = config.body.raw;
34
- } else if (config.body.mode === 'urlencoded' && config.body.urlencoded !== null && config.body.urlencoded !== undefined) {
35
- // Convert to URLSearchParams
36
- const params = new URLSearchParams();
37
- for (const item of config.body.urlencoded) {
38
- params.append(item.key, item.value);
39
- }
40
- fetchOptions.body = params.toString();
41
- (fetchOptions.headers as Record<string, string>)['Content-Type'] = 'application/x-www-form-urlencoded';
42
- } else if (config.body.mode === 'formdata' && config.body.formdata !== null && config.body.formdata !== undefined) {
43
- // FormData
44
- const formData = new FormData();
45
- for (const item of config.body.formdata) {
46
- formData.append(item.key, item.value);
47
- }
48
- fetchOptions.body = formData;
49
- }
50
- } else if (typeof config.body === 'string') {
51
- fetchOptions.body = config.body;
52
- } else {
53
- // Assume JSON
54
- fetchOptions.body = JSON.stringify(config.body);
55
- const headersRecord = fetchOptions.headers as Record<string, string>;
56
- headersRecord['Content-Type'] ??= 'application/json';
57
- }
58
- }
59
-
60
- try {
61
- const startTime = Date.now();
62
- const response = await fetch(url, fetchOptions);
63
- const duration = Date.now() - startTime;
64
- const body = await response.text();
65
-
66
- // Convert headers, preserving multiple values (e.g., set-cookie)
67
- const headers: Record<string, string | string[]> = {};
68
- const headerCounts: Record<string, number> = {};
69
-
70
- response.headers.forEach((value, key) => {
71
- const lowerKey = key.toLowerCase();
72
- if (headerCounts[lowerKey] === 0 || headerCounts[lowerKey] === undefined) {
73
- headerCounts[lowerKey] = 0;
74
- headers[lowerKey] = value;
75
- } else {
76
- // Multiple values for this header - convert to array
77
- const existing = headers[lowerKey];
78
- if (!Array.isArray(existing)) {
79
- headers[lowerKey] = [existing];
80
- }
81
- (headers[lowerKey] as string[]).push(value);
82
- }
83
- headerCounts[lowerKey]++;
84
- });
85
-
86
- // Return response object compatible with quest API
87
- const responseObj: ResponseObject = {
88
- status: response.status,
89
- statusText: response.statusText,
90
- body: body,
91
- headers: headers,
92
- time: duration,
93
- // Helper methods
94
- json() {
95
- try {
96
- return JSON.parse(body) as unknown;
97
- } catch {
98
- return null;
99
- }
100
- },
101
- text() {
102
- return body;
103
- }
104
- };
105
-
106
- return responseObj;
107
- } catch (error: unknown) {
108
- const errorName = (error as { name?: string }).name;
109
- const errorMsg = (error as { message?: string }).message ?? 'Unknown error';
110
-
111
- // Handle abort error
112
- if (errorName === 'AbortError') {
113
- throw new Error('Request aborted');
114
- }
115
-
116
- throw new Error(`Request failed: ${errorMsg}`);
117
- }
118
- }
119
-
120
- /**
121
- * Creates the complete quest API object
122
- * Returns all quest.* methods and properties for script execution
123
- */
124
- export function createQuestAPI(
125
- context: ExecutionContext,
126
- scriptType: ScriptType,
127
- tests: TestResult[], // Array to collect test results
128
- emitAssertion: (test: TestResult) => void
129
- ): Record<string, unknown> {
130
- // Create test API (test, skip, fail) with abort signal
131
- const testAPI = createQuestTestAPI(tests, scriptType, emitAssertion, context.abortSignal);
132
-
133
- return {
134
- // Test API
135
- test: testAPI.test,
136
- skip: testAPI.skip,
137
- fail: testAPI.fail,
138
-
139
- // Send HTTP request - supports BOTH async/await and callback patterns
140
- sendRequest(config: RequestConfig, callback?: (err: Error | null, res: ResponseObject | null) => void) {
141
- const requestPromise = executeHttpRequest(config, context.abortSignal);
142
-
143
- // If callback provided, use callback pattern
144
- if (callback !== null && callback !== undefined && typeof callback === 'function') {
145
- requestPromise
146
- .then((res) => {
147
- callback(null, res);
148
- })
149
- .catch((err) => {
150
- callback(err as Error, null);
151
- });
152
- return undefined; // Don't return promise in callback mode
153
- } else {
154
- // No callback, return Promise for async/await
155
- return requestPromise;
156
- }
157
- },
158
-
159
- // Wait/delay execution
160
- wait(ms: number) {
161
- if (typeof ms !== 'number' || isNaN(ms)) {
162
- throw new Error('quest.wait() requires a valid number of milliseconds');
163
- }
164
- if (ms < 0) {
165
- throw new Error('quest.wait() milliseconds must be non-negative');
166
- }
167
- return new Promise(resolve => setTimeout(resolve, ms));
168
- },
169
-
170
- // Variables API
171
- variables: (() => {
172
- const variablesAPI = {
173
- get(key: string) {
174
- // Priority: iteration > scope stack > collection > env => global
175
- const currentIterationData = context.iterationData?.[context.iterationCurrent - 1];
176
- if (currentIterationData !== null && currentIterationData !== undefined && key in currentIterationData) {
177
- return String(currentIterationData[key]);
178
- }
179
-
180
- // Search scope stack (top to bottom)
181
- for (let i = context.scopeStack.length - 1; i >= 0; i--) {
182
- if (key in context.scopeStack[i].vars) {
183
- return context.scopeStack[i].vars[key];
184
- }
185
- }
186
-
187
- if (key in context.collectionVariables) {
188
- return context.collectionVariables[key];
189
- }
190
- if (context.environment !== null && context.environment !== undefined && key in context.environment.variables) {
191
- return context.environment.variables[key];
192
- }
193
- if (key in context.globalVariables) {
194
- return context.globalVariables[key];
195
- }
196
- return null;
197
- },
198
-
199
- set(key: string, value: string) {
200
- // Search scope stack for existing key, or set in top scope
201
- for (let i = context.scopeStack.length - 1; i >= 0; i--) {
202
- if (key in context.scopeStack[i].vars) {
203
- context.scopeStack[i].vars[key] = value;
204
- return;
205
- }
206
- }
207
-
208
- // Not found: set in current (top) scope
209
- if (context.scopeStack.length > 0) {
210
- context.scopeStack[context.scopeStack.length - 1].vars[key] = value;
211
- }
212
- },
213
-
214
- replaceIn(template: string): string {
215
- if (isNullOrWhitespace(template)) return template;
216
-
217
- // Replace all {{variable}} patterns
218
- return template.replace(/\{\{([^}]+)\}\}/g, (match, varName: string) => {
219
- const value = variablesAPI.get(varName.trim());
220
- // If variable not found, leave the placeholder
221
- return value !== null ? String(value) : match;
222
- });
223
- },
224
-
225
- has(key: string) {
226
- return variablesAPI.get(key) !== null;
227
- }
228
- };
229
- return variablesAPI;
230
- })(),
231
-
232
- // Global variables
233
- global: {
234
- variables: {
235
- get(key: string) {
236
- return context.globalVariables[key] ?? null;
237
- },
238
- set(key: string, value: string) {
239
- context.globalVariables[key] = value;
240
- },
241
- has(key: string) {
242
- return key in context.globalVariables;
243
- },
244
- remove(key: string) {
245
- if (key in context.globalVariables) {
246
- delete context.globalVariables[key];
247
- return true;
248
- }
249
- return false;
250
- },
251
- clear() {
252
- context.globalVariables = {};
253
- },
254
- toObject() {
255
- return { ...context.globalVariables };
256
- }
257
- }
258
- },
259
-
260
- // Collection API
261
- collection: {
262
- // Collection info
263
- info:
264
- {
265
- ...context.collectionInfo,
266
- version:
267
- context.collectionInfo.version !== null &&
268
- context.collectionInfo.version !== undefined &&
269
- context.collectionInfo.version !== ''
270
- ? context.collectionInfo.version
271
- : null,
272
- description:
273
- context.collectionInfo.description !== null &&
274
- context.collectionInfo.description !== undefined &&
275
- context.collectionInfo.description !== ''
276
- ? context.collectionInfo.description
277
- : null,
278
- },
279
-
280
- // Collection variables
281
- variables: {
282
- get(key: string) {
283
- return context.collectionVariables[key] ?? null;
284
- },
285
- set(key: string, value: string) {
286
- context.collectionVariables[key] = value;
287
- },
288
- has(key: string) {
289
- return key in context.collectionVariables;
290
- },
291
- remove(key: string) {
292
- if (key in context.collectionVariables) {
293
- delete context.collectionVariables[key];
294
- return true;
295
- }
296
- return false;
297
- },
298
- clear() {
299
- context.collectionVariables = {};
300
- },
301
- toObject() {
302
- return { ...context.collectionVariables };
303
- }
304
- }
305
- },
306
-
307
- // Environment variables
308
- environment: {
309
- name: context.environment?.name ?? null,
310
- variables: {
311
- get(key: string) {
312
- return context.environment?.variables[key] ?? null;
313
- },
314
- set(key: string, value: string) {
315
- context.environment ??= { name: 'Runtime Environment', variables: {} };
316
- context.environment.variables[key] = value;
317
- },
318
- has(key: string) {
319
- return context.environment !== null && context.environment !== undefined ? key in context.environment.variables : false;
320
- },
321
- remove(key: string) {
322
- if (context.environment !== null && context.environment !== undefined && key in context.environment.variables) {
323
- delete context.environment.variables[key];
324
- return true;
325
- }
326
- return false;
327
- },
328
- clear() {
329
- if (context.environment !== null && context.environment !== undefined) {
330
- context.environment.variables = {};
331
- }
332
- },
333
- toObject() {
334
- return context.environment !== null && context.environment !== undefined ? { ...context.environment.variables } : {};
335
- }
336
- }
337
- },
338
-
339
- // Scope variables (hierarchical)
340
- scope: {
341
- variables: {
342
- get(key: string) {
343
- // Search scope stack top to bottom
344
- for (let i = context.scopeStack.length - 1; i >= 0; i--) {
345
- if (key in context.scopeStack[i].vars) {
346
- return context.scopeStack[i].vars[key];
347
- }
348
- }
349
- return null;
350
- },
351
- set(key: string, value: string) {
352
- // Search stack for existing key, or set in top scope
353
- for (let i = context.scopeStack.length - 1; i >= 0; i--) {
354
- if (key in context.scopeStack[i].vars) {
355
- context.scopeStack[i].vars[key] = value;
356
- return;
357
- }
358
- }
359
-
360
- // Not found: set in current (top) scope
361
- if (context.scopeStack.length > 0) {
362
- context.scopeStack[context.scopeStack.length - 1].vars[key] = value;
363
- }
364
- },
365
- has(key: string) {
366
- return this.get(key) !== null;
367
- },
368
- remove(key: string) {
369
- // Remove from the scope where it exists
370
- for (let i = context.scopeStack.length - 1; i >= 0; i--) {
371
- if (key in context.scopeStack[i].vars) {
372
- delete context.scopeStack[i].vars[key];
373
- return true;
374
- }
375
- }
376
- return false;
377
- },
378
- clear() {
379
- // Clear current (top) scope only
380
- if (context.scopeStack.length > 0) {
381
- context.scopeStack[context.scopeStack.length - 1].vars = {};
382
- }
383
- },
384
- toObject() {
385
- // Merge all scopes (bottom to top, so top overrides)
386
- const result: Record<string, string> = {};
387
- for (let i = 0; i < context.scopeStack.length; i++) {
388
- Object.assign(result, context.scopeStack[i].vars);
389
- }
390
- return result;
391
- }
392
- }
393
- },
394
-
395
- // Response API
396
- response: context.currentResponse !== null && context.currentResponse !== undefined ? {
397
- status: context.currentResponse.status,
398
- statusText: context.currentResponse.statusText,
399
- headers: {
400
- // Method API
401
- get(name: string) {
402
- if (context.currentResponse?.headers === null || context.currentResponse?.headers === undefined) return null;
403
- // Case-insensitive lookup
404
- const lowerName = name.toLowerCase();
405
- for (const [key, value] of Object.entries(context.currentResponse.headers)) {
406
- if (key.toLowerCase() === lowerName) {
407
- return value;
408
- }
409
- }
410
- return null;
411
- },
412
- has(name: string) {
413
- if (context.currentResponse?.headers === null || context.currentResponse?.headers === undefined) return false;
414
- // Case-insensitive lookup
415
- const lowerName = name.toLowerCase();
416
- for (const key of Object.keys(context.currentResponse.headers)) {
417
- if (key.toLowerCase() === lowerName) {
418
- return true;
419
- }
420
- }
421
- return false;
422
- },
423
- toObject() {
424
- return context.currentResponse?.headers ?? {};
425
- }
426
- },
427
- body: context.currentResponse.body,
428
- text() {
429
- return context.currentResponse?.body ?? '';
430
- },
431
- json() {
432
- try {
433
- return JSON.parse(context.currentResponse?.body ?? '{}') as unknown;
434
- } catch {
435
- return {};
436
- }
437
- },
438
- time: context.currentResponse.duration,
439
- size: context.currentResponse.body?.length ?? 0,
440
- // Assertion helpers
441
- to: {
442
- be: {
443
- ok: context.currentResponse.status === 200,
444
- success: context.currentResponse.status >= 200 && context.currentResponse.status < 300,
445
- clientError: context.currentResponse.status >= 400 && context.currentResponse.status < 500,
446
- serverError: context.currentResponse.status >= 500 && context.currentResponse.status < 600
447
- },
448
- have: {
449
- status(code: number) {
450
- return context.currentResponse?.status === code;
451
- },
452
- header(name: string) {
453
- if (context.currentResponse?.headers === null || context.currentResponse?.headers === undefined) return false;
454
- const lowerName = name.toLowerCase();
455
- for (const key of Object.keys(context.currentResponse.headers)) {
456
- if (key.toLowerCase() === lowerName) {
457
- return true;
458
- }
459
- }
460
- return false;
461
- },
462
- jsonBody(field: string) {
463
- try {
464
- const data = JSON.parse(context.currentResponse?.body ?? '{}') as Record<string, unknown>;
465
- return field in data;
466
- } catch {
467
- return false;
468
- }
469
- }
470
- }
471
- }
472
- } : null,
473
-
474
- // Request info and modification API
475
- request: {
476
- info: {
477
- name: context.currentRequest?.name ?? '',
478
- id: context.currentRequest?.id ?? '',
479
- protocol: context.protocol,
480
- description: context.currentRequest?.description ?? ''
481
- },
482
- dependsOn: context.currentRequest?.dependsOn ?? null,
483
- condition: context.currentRequest?.condition ?? null,
484
- url: (context.currentRequest?.data.url ?? '') as string,
485
- method: (context.currentRequest?.data.method ?? '') as string,
486
- body: {
487
- get() {
488
- if (context.currentRequest?.data.body === null || context.currentRequest?.data.body === undefined) return null;
489
- const body = context.currentRequest.data.body as string | Record<string, unknown>;
490
-
491
- // Handle different body modes
492
- if (typeof body === 'string') return body;
493
- if (typeof body === 'object' && 'mode' in body && (body as { mode?: string }).mode === 'raw') return (body as { raw?: string }).raw ?? null;
494
- if (typeof body === 'object' && 'mode' in body && (body as { mode?: string }).mode === 'urlencoded') return null; // Return null for non-raw modes
495
- if (typeof body === 'object' && 'mode' in body && (body as { mode?: string }).mode === 'formdata') return null;
496
-
497
- return null;
498
- },
499
- set(content: string) {
500
- if (context.currentRequest === null || context.currentRequest === undefined) return;
501
- if (context.currentRequest.data.body === null || context.currentRequest.data.body === undefined) {
502
- context.currentRequest.data.body = { mode: 'raw', raw: content };
503
- } else if (typeof context.currentRequest.data.body === 'string') {
504
- context.currentRequest.data.body = content;
505
- } else if (typeof context.currentRequest.data.body === 'object') {
506
- (context.currentRequest.data.body as { raw?: string }).raw = content;
507
- }
508
- },
509
- get mode() {
510
- if (context.currentRequest?.data.body === null || context.currentRequest?.data.body === undefined) return null;
511
- const body = context.currentRequest.data.body as string | Record<string, unknown>;
512
-
513
- if (typeof body === 'string') return 'raw';
514
- return (typeof body === 'object' && 'mode' in body ? (body as { mode?: string }).mode : 'raw') as string;
515
- }
516
- },
517
- headers: {
518
- add(header: { key: string; value: string; }) {
519
- if (context.currentRequest === null || context.currentRequest === undefined) return;
520
- const headers = context.currentRequest.data.headers as Record<string, string> | undefined;
521
- if (headers === null || headers === undefined) {
522
- context.currentRequest.data.headers = {};
523
- }
524
- (context.currentRequest.data.headers as Record<string, string>)[header.key] = header.value;
525
- },
526
- remove(key: string) {
527
- if (context.currentRequest?.data.headers === null || context.currentRequest?.data.headers === undefined) return;
528
- delete (context.currentRequest.data.headers as Record<string, string>)[key];
529
- },
530
- get(key: string) {
531
- if (context.currentRequest?.data.headers === null || context.currentRequest?.data.headers === undefined) return null;
532
- // Case-insensitive lookup
533
- const lowerKey = key.toLowerCase();
534
- for (const [headerKey, value] of Object.entries(context.currentRequest.data.headers as Record<string, string>)) {
535
- if (headerKey.toLowerCase() === lowerKey) {
536
- return value;
537
- }
538
- }
539
- return null;
540
- },
541
- upsert(header: { key: string; value: string; }) {
542
- if (context.currentRequest === null || context.currentRequest === undefined) return;
543
- const headers = context.currentRequest.data.headers as Record<string, string> | undefined;
544
- if (headers === null || headers === undefined) {
545
- context.currentRequest.data.headers = {};
546
- }
547
- (context.currentRequest.data.headers as Record<string, string>)[header.key] = header.value;
548
- },
549
- toObject() {
550
- return (context.currentRequest?.data.headers ?? {}) as Record<string, string>;
551
- }
552
- },
553
- timeout: {
554
- set(ms: number) {
555
- // Only allowed in preRequestScript
556
- if (scriptType !== ScriptType.PreRequest) {
557
- throw new Error('quest.request.timeout.set() can only be called in preRequestScript');
558
- }
559
-
560
- if (context.currentRequest === null || context.currentRequest === undefined) {
561
- throw new Error('quest.request.timeout.set() requires an active request');
562
- }
563
-
564
- // Validate timeout is a positive number
565
- if (typeof ms !== 'number' || ms <= 0 || !Number.isFinite(ms)) {
566
- throw new Error('quest.request.timeout.set() requires a positive finite number in milliseconds');
567
- }
568
-
569
- // Initialize options object using nullish coalescing operator
570
- context.currentRequest.options ??= {};
571
-
572
- // Initialize timeout object using nullish coalescing operator
573
- context.currentRequest.options.timeout ??= {};
574
-
575
- // Set the per-request timeout override
576
- context.currentRequest.options.timeout.request = ms;
577
- },
578
- get() {
579
- // Can be called from any script
580
- if (context.currentRequest === null || context.currentRequest === undefined) {
581
- return null;
582
- }
583
-
584
- // Check for per-request timeout override first
585
- const requestTimeout = context.currentRequest.options?.timeout?.request;
586
- if (requestTimeout !== null && requestTimeout !== undefined) {
587
- return requestTimeout;
588
- }
589
-
590
- // Fall back to context/CLI timeout
591
- const contextTimeout = context.options?.timeout?.request;
592
- return contextTimeout ?? null;
593
- }
594
- }
595
- },
596
-
597
- // Iteration API
598
- iteration: {
599
- current: context.iterationCurrent,
600
- count: context.iterationCount,
601
- data: {
602
- get(key: string) {
603
- const currentData = context.iterationData?.[context.iterationCurrent - 1];
604
- return currentData?.[key] ?? null;
605
- },
606
- has(key: string) {
607
- const currentData = context.iterationData?.[context.iterationCurrent - 1];
608
- return currentData !== null && currentData !== undefined ? key in currentData : false;
609
- },
610
- toObject() {
611
- return context.iterationData?.[context.iterationCurrent - 1] ?? {};
612
- },
613
- keys() {
614
- const currentData = context.iterationData?.[context.iterationCurrent - 1];
615
- return currentData !== null && currentData !== undefined ? Object.keys(currentData) : [];
616
- },
617
- all() {
618
- return context.iterationData ?? [];
619
- }
620
- }
621
- },
622
-
623
- // Execution history API
624
- history: {
625
- requests: {
626
- count() {
627
- return context.executionHistory.length;
628
- },
629
- get(idOrName: string) {
630
- return context.executionHistory.find(
631
- entry => entry.id === idOrName || entry.name === idOrName
632
- ) ?? null;
633
- },
634
- all() {
635
- return context.executionHistory;
636
- },
637
- last() {
638
- return context.executionHistory.length > 0
639
- ? context.executionHistory[context.executionHistory.length - 1]
640
- : null;
641
- },
642
- filter(criteria: HistoryFilterCriteria) {
643
- return context.executionHistory.filter(entry => {
644
- // Filter by path (with wildcard support)
645
- if (criteria.path !== null && criteria.path !== undefined) {
646
- const pathPattern = criteria.path.replace(/\*/g, '.*');
647
- const pathRegex = new RegExp(`^${pathPattern}$`);
648
- if (!pathRegex.test(entry.path)) {
649
- return false;
650
- }
651
- }
652
-
653
- // Filter by name
654
- if (criteria.name !== null && criteria.name !== undefined && entry.name !== criteria.name) {
655
- return false;
656
- }
657
-
658
- // Filter by iteration
659
- if (criteria.iteration !== null && criteria.iteration !== undefined && entry.iteration !== criteria.iteration) {
660
- return false;
661
- }
662
-
663
- // Filter by id
664
- if (criteria.id !== null && criteria.id !== undefined && entry.id !== criteria.id) {
665
- return false;
666
- }
667
-
668
- return true;
669
- });
670
- }
671
- }
672
- },
673
-
674
- // Cookies API - Uses cookie jar for persistence across requests
675
- cookies: {
676
- get(name: string) {
677
- // Use cookie jar if available
678
- if (context.cookieJar !== null && context.cookieJar !== undefined) {
679
- return context.cookieJar.get(name);
680
- }
681
- return null;
682
- },
683
- set(name: string, value: string, options: CookieSetOptions) {
684
- if (context.cookieJar !== null && context.cookieJar !== undefined) {
685
- context.cookieJar.set(name, value, options);
686
- }
687
- },
688
- has(name: string) {
689
- // Use cookie jar if available
690
- if (context.cookieJar !== null && context.cookieJar !== undefined) {
691
- return context.cookieJar.has(name);
692
- }
693
- return false;
694
- },
695
- remove(name: string) {
696
- // Use cookie jar if available
697
- if (context.cookieJar !== null && context.cookieJar !== undefined) {
698
- context.cookieJar.remove(name);
699
- }
700
- },
701
- clear() {
702
- // Use cookie jar if available
703
- if (context.cookieJar !== null && context.cookieJar !== undefined) {
704
- context.cookieJar.clear();
705
- }
706
- },
707
- toObject() {
708
- // Use cookie jar if available
709
- if (context.cookieJar !== null && context.cookieJar !== undefined) {
710
- return context.cookieJar.toObject();
711
- }
712
- return {};
713
- }
714
- },
715
-
716
- // Plugin event API (for PluginEvent script type)
717
- event: context.currentEvent !== null && context.currentEvent !== undefined ? {
718
- name: context.currentEvent.eventName,
719
- timestamp: context.currentEvent.timestamp,
720
- data: (() => {
721
- const rawData = context.currentEvent.data as string | Record<string, unknown>;
722
- // Add json() helper method
723
- const dataWithHelper: Record<string, unknown> = typeof rawData === 'string' ? { value: rawData } : rawData;
724
- dataWithHelper.json = function () {
725
- try {
726
- return (typeof rawData === 'string' ? JSON.parse(rawData) : rawData) as unknown;
727
- } catch {
728
- return null;
729
- }
730
- };
731
- return dataWithHelper;
732
- })(),
733
- index: context.currentEvent.index
734
- } : null,
735
-
736
- // Hint expected message count (for streaming protocols)
737
- expectMessages(count: number, timeout?: number) {
738
- // Only allowed in preRequestScript
739
- if (scriptType !== ScriptType.PreRequest) {
740
- throw new Error('quest.expectMessages() can only be called in preRequestScript');
741
- }
742
-
743
- // Validate count is positive integer
744
- if (!Number.isInteger(count) || count <= 0) {
745
- throw new Error('quest.expectMessages() requires a positive integer count');
746
- }
747
-
748
- // Validate protocol has plugin events with canHaveTests
749
- const protocolPlugin = context.protocolPlugin;
750
- const hasTestableEvents = protocolPlugin.events?.some((e: { canHaveTests?: boolean }) => e.canHaveTests === true) === true;
751
-
752
- if (hasTestableEvents === false) {
753
- throw new Error(
754
- `quest.expectMessages() is not supported for protocol '${context.protocol}' ` +
755
- `(no plugin events with canHaveTests)`
756
- );
757
- }
758
-
759
- // Store count in context for plugin optimization and test counting
760
- context.expectedMessages = count;
761
-
762
- }
763
- };
764
- }