testblocks 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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +333 -0
  3. package/dist/cli/executor.d.ts +32 -0
  4. package/dist/cli/executor.js +517 -0
  5. package/dist/cli/index.d.ts +2 -0
  6. package/dist/cli/index.js +411 -0
  7. package/dist/cli/reporters.d.ts +62 -0
  8. package/dist/cli/reporters.js +451 -0
  9. package/dist/client/assets/index-4hbFPUhP.js +2087 -0
  10. package/dist/client/assets/index-4hbFPUhP.js.map +1 -0
  11. package/dist/client/assets/index-Dnk1ti7l.css +1 -0
  12. package/dist/client/index.html +25 -0
  13. package/dist/core/blocks/api.d.ts +2 -0
  14. package/dist/core/blocks/api.js +610 -0
  15. package/dist/core/blocks/data-driven.d.ts +2 -0
  16. package/dist/core/blocks/data-driven.js +245 -0
  17. package/dist/core/blocks/index.d.ts +15 -0
  18. package/dist/core/blocks/index.js +71 -0
  19. package/dist/core/blocks/lifecycle.d.ts +2 -0
  20. package/dist/core/blocks/lifecycle.js +199 -0
  21. package/dist/core/blocks/logic.d.ts +2 -0
  22. package/dist/core/blocks/logic.js +357 -0
  23. package/dist/core/blocks/playwright.d.ts +2 -0
  24. package/dist/core/blocks/playwright.js +764 -0
  25. package/dist/core/blocks/procedures.d.ts +5 -0
  26. package/dist/core/blocks/procedures.js +321 -0
  27. package/dist/core/index.d.ts +5 -0
  28. package/dist/core/index.js +44 -0
  29. package/dist/core/plugins.d.ts +66 -0
  30. package/dist/core/plugins.js +118 -0
  31. package/dist/core/types.d.ts +153 -0
  32. package/dist/core/types.js +2 -0
  33. package/dist/server/codegenManager.d.ts +54 -0
  34. package/dist/server/codegenManager.js +259 -0
  35. package/dist/server/codegenParser.d.ts +17 -0
  36. package/dist/server/codegenParser.js +598 -0
  37. package/dist/server/executor.d.ts +37 -0
  38. package/dist/server/executor.js +672 -0
  39. package/dist/server/globals.d.ts +85 -0
  40. package/dist/server/globals.js +273 -0
  41. package/dist/server/index.d.ts +2 -0
  42. package/dist/server/index.js +361 -0
  43. package/dist/server/plugins.d.ts +55 -0
  44. package/dist/server/plugins.js +206 -0
  45. package/package.json +103 -0
@@ -0,0 +1,672 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.TestExecutor = void 0;
37
+ const playwright_1 = require("playwright");
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const core_1 = require("../core");
41
+ // Parse CSV content into TestDataSet array
42
+ function parseCSV(content) {
43
+ const lines = content.trim().split('\n');
44
+ if (lines.length < 2)
45
+ return []; // Need header + at least one data row
46
+ const headers = lines[0].split(',').map(h => h.trim());
47
+ const dataSets = [];
48
+ for (let i = 1; i < lines.length; i++) {
49
+ const line = lines[i].trim();
50
+ if (!line)
51
+ continue;
52
+ // Simple CSV parsing (handles basic quoted values)
53
+ const values = [];
54
+ let current = '';
55
+ let inQuotes = false;
56
+ for (const char of line) {
57
+ if (char === '"') {
58
+ inQuotes = !inQuotes;
59
+ }
60
+ else if (char === ',' && !inQuotes) {
61
+ values.push(current.trim());
62
+ current = '';
63
+ }
64
+ else {
65
+ current += char;
66
+ }
67
+ }
68
+ values.push(current.trim());
69
+ const row = {};
70
+ headers.forEach((header, idx) => {
71
+ const val = values[idx] ?? '';
72
+ // Try to parse as number or boolean
73
+ if (val === 'true') {
74
+ row[header] = true;
75
+ }
76
+ else if (val === 'false') {
77
+ row[header] = false;
78
+ }
79
+ else if (val !== '' && !isNaN(Number(val))) {
80
+ row[header] = Number(val);
81
+ }
82
+ else {
83
+ row[header] = val;
84
+ }
85
+ });
86
+ dataSets.push({ name: `Row ${i}`, values: row });
87
+ }
88
+ return dataSets;
89
+ }
90
+ class TestExecutor {
91
+ constructor(options = {}) {
92
+ this.browser = null;
93
+ this.context = null;
94
+ this.page = null;
95
+ this.plugins = new Map();
96
+ this.options = {
97
+ headless: true,
98
+ timeout: 30000,
99
+ ...options,
100
+ };
101
+ // Register plugins
102
+ if (options.plugins) {
103
+ options.plugins.forEach(plugin => {
104
+ this.plugins.set(plugin.name, plugin);
105
+ });
106
+ }
107
+ }
108
+ async initialize() {
109
+ // Set the test ID attribute globally for Playwright selectors
110
+ if (this.options.testIdAttribute) {
111
+ playwright_1.selectors.setTestIdAttribute(this.options.testIdAttribute);
112
+ }
113
+ this.browser = await playwright_1.chromium.launch({
114
+ headless: this.options.headless,
115
+ });
116
+ this.context = await this.browser.newContext();
117
+ this.page = await this.context.newPage();
118
+ if (this.options.timeout) {
119
+ this.page.setDefaultTimeout(this.options.timeout);
120
+ }
121
+ }
122
+ async cleanup() {
123
+ if (this.page)
124
+ await this.page.close();
125
+ if (this.context)
126
+ await this.context.close();
127
+ if (this.browser)
128
+ await this.browser.close();
129
+ this.page = null;
130
+ this.context = null;
131
+ this.browser = null;
132
+ }
133
+ async runTestFile(testFile) {
134
+ const results = [];
135
+ // Register custom blocks from procedures
136
+ if (testFile.procedures) {
137
+ this.registerCustomBlocksFromProcedures(testFile.procedures);
138
+ }
139
+ await this.initialize();
140
+ // Create shared execution context for lifecycle hooks
141
+ const sharedContext = {
142
+ variables: new Map(Object.entries({
143
+ ...this.resolveVariableDefaults(testFile.variables),
144
+ ...this.options.variables,
145
+ })),
146
+ results: [],
147
+ browser: this.browser,
148
+ page: this.page,
149
+ logger: this.createLogger(),
150
+ plugins: this.plugins,
151
+ testIdAttribute: this.options.testIdAttribute,
152
+ };
153
+ try {
154
+ // Run beforeAll hooks
155
+ if (testFile.beforeAll && testFile.beforeAll.length > 0) {
156
+ sharedContext.logger.info('Running beforeAll hooks...');
157
+ const beforeAllResult = await this.runLifecycleSteps(testFile.beforeAll, 'beforeAll', sharedContext);
158
+ results.push(beforeAllResult);
159
+ if (beforeAllResult.status === 'failed' || beforeAllResult.status === 'error') {
160
+ // Don't run tests if beforeAll failed
161
+ return results;
162
+ }
163
+ }
164
+ // Run each test with beforeEach/afterEach
165
+ for (const test of testFile.tests) {
166
+ // Load data from file if specified
167
+ let testData = test.data;
168
+ if (test.dataFile && !testData) {
169
+ testData = this.loadDataFromFile(test.dataFile);
170
+ }
171
+ // Check if test has data-driven sets
172
+ if (testData && testData.length > 0) {
173
+ // Run test for each data set
174
+ for (let i = 0; i < testData.length; i++) {
175
+ const dataSet = testData[i];
176
+ // Run suite-level beforeEach
177
+ if (testFile.beforeEach && testFile.beforeEach.length > 0) {
178
+ for (const step of testFile.beforeEach) {
179
+ await this.runStep(step, sharedContext);
180
+ }
181
+ }
182
+ const result = await this.runTestWithData(test, testFile.variables, dataSet, i, sharedContext);
183
+ results.push(result);
184
+ // Run suite-level afterEach
185
+ if (testFile.afterEach && testFile.afterEach.length > 0) {
186
+ for (const step of testFile.afterEach) {
187
+ await this.runStep(step, sharedContext);
188
+ }
189
+ }
190
+ }
191
+ }
192
+ else {
193
+ // Run test once without data
194
+ // Run suite-level beforeEach
195
+ if (testFile.beforeEach && testFile.beforeEach.length > 0) {
196
+ for (const step of testFile.beforeEach) {
197
+ await this.runStep(step, sharedContext);
198
+ }
199
+ }
200
+ const result = await this.runTest(test, testFile.variables, sharedContext);
201
+ results.push(result);
202
+ // Run suite-level afterEach
203
+ if (testFile.afterEach && testFile.afterEach.length > 0) {
204
+ for (const step of testFile.afterEach) {
205
+ await this.runStep(step, sharedContext);
206
+ }
207
+ }
208
+ }
209
+ }
210
+ // Run afterAll hooks
211
+ if (testFile.afterAll && testFile.afterAll.length > 0) {
212
+ sharedContext.logger.info('Running afterAll hooks...');
213
+ const afterAllResult = await this.runLifecycleSteps(testFile.afterAll, 'afterAll', sharedContext);
214
+ results.push(afterAllResult);
215
+ }
216
+ }
217
+ finally {
218
+ await this.cleanup();
219
+ }
220
+ return results;
221
+ }
222
+ async runLifecycleSteps(steps, lifecycleType, context) {
223
+ const startedAt = new Date().toISOString();
224
+ const startTime = Date.now();
225
+ const stepResults = [];
226
+ let status = 'passed';
227
+ let error;
228
+ for (const step of steps) {
229
+ const stepResult = await this.runStep(step, context);
230
+ stepResults.push(stepResult);
231
+ if (stepResult.status === 'failed' || stepResult.status === 'error') {
232
+ status = stepResult.status;
233
+ error = stepResult.error;
234
+ break;
235
+ }
236
+ }
237
+ return {
238
+ testId: `lifecycle-${lifecycleType}`,
239
+ testName: lifecycleType,
240
+ status,
241
+ duration: Date.now() - startTime,
242
+ steps: stepResults,
243
+ error,
244
+ startedAt,
245
+ finishedAt: new Date().toISOString(),
246
+ isLifecycle: true,
247
+ lifecycleType,
248
+ };
249
+ }
250
+ /**
251
+ * Public method to register custom blocks from procedures
252
+ */
253
+ registerProcedures(procedures) {
254
+ this.registerCustomBlocksFromProcedures(procedures);
255
+ }
256
+ registerCustomBlocksFromProcedures(procedures) {
257
+ Object.values(procedures).forEach(proc => {
258
+ if (!proc.steps || proc.steps.length === 0)
259
+ return;
260
+ const blockType = `custom_${proc.name.toLowerCase().replace(/\s+/g, '_')}`;
261
+ // Check if already registered
262
+ if ((0, core_1.getBlock)(blockType))
263
+ return;
264
+ const blockDef = {
265
+ type: blockType,
266
+ category: 'Custom',
267
+ color: '#607D8B',
268
+ tooltip: proc.description || `Custom block: ${proc.name}`,
269
+ inputs: (proc.params || []).map(param => ({
270
+ name: param.name.toUpperCase(),
271
+ type: 'field',
272
+ fieldType: param.type === 'number' ? 'number' : 'text',
273
+ default: param.default,
274
+ })),
275
+ previousStatement: true,
276
+ nextStatement: true,
277
+ execute: async (params, context) => {
278
+ context.logger.info(`Executing custom block: ${proc.name}`);
279
+ // Set procedure parameters in context.variables so ${paramName} references work
280
+ (proc.params || []).forEach(p => {
281
+ const value = params[p.name.toUpperCase()];
282
+ if (value !== undefined) {
283
+ context.variables.set(p.name, value);
284
+ }
285
+ });
286
+ return {
287
+ customBlock: true,
288
+ name: proc.name,
289
+ steps: proc.steps,
290
+ };
291
+ },
292
+ };
293
+ (0, core_1.registerBlock)(blockDef);
294
+ });
295
+ }
296
+ async runTest(test, fileVariables, sharedContext) {
297
+ const startedAt = new Date().toISOString();
298
+ const startTime = Date.now();
299
+ const stepResults = [];
300
+ // Create execution context, inheriting variables from shared context (e.g., from beforeAll)
301
+ const baseVariables = sharedContext
302
+ ? Object.fromEntries(sharedContext.variables)
303
+ : {
304
+ ...this.resolveVariableDefaults(fileVariables),
305
+ ...this.options.variables,
306
+ };
307
+ const context = {
308
+ variables: new Map(Object.entries(baseVariables)),
309
+ results: [],
310
+ browser: this.browser,
311
+ page: this.page,
312
+ logger: this.createLogger(),
313
+ plugins: this.plugins,
314
+ testIdAttribute: this.options.testIdAttribute,
315
+ };
316
+ // Run beforeTest hooks
317
+ for (const plugin of this.plugins.values()) {
318
+ if (plugin.hooks?.beforeTest) {
319
+ await plugin.hooks.beforeTest(context, test);
320
+ }
321
+ }
322
+ let testStatus = 'passed';
323
+ let testError;
324
+ try {
325
+ // Execute steps from Blockly serialization format
326
+ const steps = this.extractStepsFromBlocklyState(test.steps);
327
+ for (const step of steps) {
328
+ const stepResult = await this.runStep(step, context);
329
+ stepResults.push(stepResult);
330
+ if (stepResult.status === 'failed' || stepResult.status === 'error') {
331
+ testStatus = stepResult.status;
332
+ testError = stepResult.error;
333
+ break;
334
+ }
335
+ }
336
+ }
337
+ catch (error) {
338
+ testStatus = 'error';
339
+ testError = {
340
+ message: error.message,
341
+ stack: error.stack,
342
+ };
343
+ }
344
+ const result = {
345
+ testId: test.id,
346
+ testName: test.name,
347
+ status: testStatus,
348
+ duration: Date.now() - startTime,
349
+ steps: stepResults,
350
+ error: testError,
351
+ startedAt,
352
+ finishedAt: new Date().toISOString(),
353
+ };
354
+ // Run afterTest hooks
355
+ for (const plugin of this.plugins.values()) {
356
+ if (plugin.hooks?.afterTest) {
357
+ await plugin.hooks.afterTest(context, test, result);
358
+ }
359
+ }
360
+ return result;
361
+ }
362
+ async runTestWithData(test, fileVariables, dataSet, dataIndex, sharedContext) {
363
+ const testName = dataSet.name
364
+ ? `${test.name} [${dataSet.name}]`
365
+ : `${test.name} [${dataIndex + 1}]`;
366
+ console.log(` Running: ${testName}`);
367
+ const startedAt = new Date().toISOString();
368
+ const startTime = Date.now();
369
+ const stepResults = [];
370
+ // Create execution context with data values, inheriting from shared context
371
+ const baseVariables = sharedContext
372
+ ? Object.fromEntries(sharedContext.variables)
373
+ : {
374
+ ...this.resolveVariableDefaults(fileVariables),
375
+ ...this.options.variables,
376
+ };
377
+ const context = {
378
+ variables: new Map(Object.entries(baseVariables)),
379
+ results: [],
380
+ browser: this.browser,
381
+ page: this.page,
382
+ logger: this.createLogger(),
383
+ plugins: this.plugins,
384
+ testIdAttribute: this.options.testIdAttribute,
385
+ currentData: dataSet,
386
+ dataIndex,
387
+ };
388
+ // Inject data values into variables
389
+ for (const [key, value] of Object.entries(dataSet.values)) {
390
+ context.variables.set(key, value);
391
+ }
392
+ // Run beforeTest hooks
393
+ for (const plugin of this.plugins.values()) {
394
+ if (plugin.hooks?.beforeTest) {
395
+ await plugin.hooks.beforeTest(context, test);
396
+ }
397
+ }
398
+ let testStatus = 'passed';
399
+ let testError;
400
+ try {
401
+ // Execute steps from Blockly serialization format
402
+ const steps = this.extractStepsFromBlocklyState(test.steps);
403
+ for (const step of steps) {
404
+ const stepResult = await this.runStep(step, context);
405
+ stepResults.push(stepResult);
406
+ if (stepResult.status === 'failed' || stepResult.status === 'error') {
407
+ testStatus = stepResult.status;
408
+ testError = stepResult.error;
409
+ break;
410
+ }
411
+ }
412
+ }
413
+ catch (error) {
414
+ testStatus = 'error';
415
+ testError = {
416
+ message: error.message,
417
+ stack: error.stack,
418
+ };
419
+ }
420
+ const result = {
421
+ testId: `${test.id}-${dataIndex}`,
422
+ testName,
423
+ status: testStatus,
424
+ duration: Date.now() - startTime,
425
+ steps: stepResults,
426
+ error: testError,
427
+ startedAt,
428
+ finishedAt: new Date().toISOString(),
429
+ };
430
+ // Run afterTest hooks
431
+ for (const plugin of this.plugins.values()) {
432
+ if (plugin.hooks?.afterTest) {
433
+ await plugin.hooks.afterTest(context, test, result);
434
+ }
435
+ }
436
+ return result;
437
+ }
438
+ extractStepsFromBlocklyState(state) {
439
+ if (!state || typeof state !== 'object')
440
+ return [];
441
+ const stateObj = state;
442
+ // Handle Blockly serialization format
443
+ if ('blocks' in stateObj && typeof stateObj.blocks === 'object') {
444
+ const blocks = stateObj.blocks;
445
+ if ('blocks' in blocks && Array.isArray(blocks.blocks)) {
446
+ return this.blocksToSteps(blocks.blocks);
447
+ }
448
+ }
449
+ // Handle direct array of steps
450
+ if (Array.isArray(state)) {
451
+ return state;
452
+ }
453
+ return [];
454
+ }
455
+ blocksToSteps(blocks) {
456
+ const steps = [];
457
+ for (const block of blocks) {
458
+ const step = this.blockToStep(block);
459
+ if (step) {
460
+ steps.push(step);
461
+ // Handle next block in chain
462
+ let currentBlock = block;
463
+ while (currentBlock.next) {
464
+ const nextBlock = currentBlock.next.block;
465
+ const nextStep = this.blockToStep(nextBlock);
466
+ if (nextStep) {
467
+ steps.push(nextStep);
468
+ }
469
+ currentBlock = nextBlock;
470
+ }
471
+ }
472
+ }
473
+ return steps;
474
+ }
475
+ blockToStep(block) {
476
+ if (!block || !block.type)
477
+ return null;
478
+ const step = {
479
+ id: block.id || `step-${Date.now()}`,
480
+ type: block.type,
481
+ params: {},
482
+ };
483
+ // Extract field values
484
+ if (block.fields && typeof block.fields === 'object') {
485
+ for (const [name, value] of Object.entries(block.fields)) {
486
+ step.params[name] = value;
487
+ }
488
+ }
489
+ // Extract inputs (connected blocks)
490
+ if (block.inputs && typeof block.inputs === 'object') {
491
+ for (const [name, input] of Object.entries(block.inputs)) {
492
+ const inputObj = input;
493
+ if (inputObj.block) {
494
+ // Recursively convert connected block
495
+ const connectedStep = this.blockToStep(inputObj.block);
496
+ if (connectedStep) {
497
+ step.params[name] = connectedStep;
498
+ }
499
+ }
500
+ }
501
+ }
502
+ return step;
503
+ }
504
+ async runStep(step, context) {
505
+ const startTime = Date.now();
506
+ // Run beforeStep hooks
507
+ for (const plugin of this.plugins.values()) {
508
+ if (plugin.hooks?.beforeStep) {
509
+ await plugin.hooks.beforeStep(context, step);
510
+ }
511
+ }
512
+ let status = 'passed';
513
+ let output;
514
+ let error;
515
+ try {
516
+ // Get block definition
517
+ const blockDef = (0, core_1.getBlock)(step.type);
518
+ if (!blockDef) {
519
+ throw new Error(`Unknown block type: ${step.type}`);
520
+ }
521
+ // Resolve any nested step params (connected blocks) to their values
522
+ const resolvedParams = await this.resolveParams(step.params, context);
523
+ // Execute the block
524
+ output = await blockDef.execute(resolvedParams, context);
525
+ // Handle custom blocks that return steps to execute
526
+ if (output && typeof output === 'object' && 'customBlock' in output) {
527
+ const customOutput = output;
528
+ for (const childStep of customOutput.steps) {
529
+ const childResult = await this.runStep(childStep, context);
530
+ if (childResult.status === 'failed' || childResult.status === 'error') {
531
+ status = childResult.status;
532
+ error = childResult.error;
533
+ break;
534
+ }
535
+ }
536
+ }
537
+ // Handle compound actions (like procedure_login)
538
+ if (output && typeof output === 'object' && 'compoundAction' in output) {
539
+ const compoundOutput = output;
540
+ for (const childStep of compoundOutput.steps) {
541
+ const childResult = await this.runStep(childStep, context);
542
+ if (childResult.status === 'failed' || childResult.status === 'error') {
543
+ status = childResult.status;
544
+ error = childResult.error;
545
+ break;
546
+ }
547
+ }
548
+ }
549
+ }
550
+ catch (err) {
551
+ status = 'failed';
552
+ error = {
553
+ message: err.message,
554
+ stack: err.stack,
555
+ };
556
+ }
557
+ // Capture screenshot on failure for web tests only
558
+ let screenshot;
559
+ const isWebStep = step.type.startsWith('web_');
560
+ if (status === 'failed' && isWebStep && context.page) {
561
+ try {
562
+ const page = context.page;
563
+ const screenshotBuffer = await page.screenshot({ fullPage: false });
564
+ screenshot = `data:image/png;base64,${screenshotBuffer.toString('base64')}`;
565
+ }
566
+ catch (screenshotErr) {
567
+ console.warn('Failed to capture screenshot:', screenshotErr.message);
568
+ }
569
+ }
570
+ const result = {
571
+ stepId: step.id,
572
+ stepType: step.type,
573
+ status,
574
+ duration: Date.now() - startTime,
575
+ output,
576
+ error,
577
+ screenshot,
578
+ };
579
+ // Run afterStep hooks
580
+ for (const plugin of this.plugins.values()) {
581
+ if (plugin.hooks?.afterStep) {
582
+ await plugin.hooks.afterStep(context, step, result);
583
+ }
584
+ }
585
+ return result;
586
+ }
587
+ async resolveParams(params, context) {
588
+ const resolved = {};
589
+ for (const [key, value] of Object.entries(params)) {
590
+ if (value && typeof value === 'object' && 'type' in value) {
591
+ // This is a connected block - execute it to get the value
592
+ const nestedStep = value;
593
+ const blockDef = (0, core_1.getBlock)(nestedStep.type);
594
+ if (blockDef) {
595
+ const nestedParams = await this.resolveParams(nestedStep.params || {}, context);
596
+ resolved[key] = await blockDef.execute(nestedParams, context);
597
+ }
598
+ }
599
+ else {
600
+ resolved[key] = value;
601
+ }
602
+ }
603
+ return resolved;
604
+ }
605
+ loadDataFromFile(filePath) {
606
+ try {
607
+ // Resolve path relative to baseDir if not absolute
608
+ const resolvedPath = path.isAbsolute(filePath)
609
+ ? filePath
610
+ : path.resolve(this.options.baseDir || process.cwd(), filePath);
611
+ if (!fs.existsSync(resolvedPath)) {
612
+ console.error(`Data file not found: ${resolvedPath}`);
613
+ return [];
614
+ }
615
+ const content = fs.readFileSync(resolvedPath, 'utf-8');
616
+ const ext = path.extname(resolvedPath).toLowerCase();
617
+ if (ext === '.csv') {
618
+ return parseCSV(content);
619
+ }
620
+ else if (ext === '.json') {
621
+ const data = JSON.parse(content);
622
+ // Expect JSON to be an array of objects with 'values' or direct objects
623
+ if (Array.isArray(data)) {
624
+ return data.map((item, i) => ({
625
+ name: item.name || `Row ${i + 1}`,
626
+ values: item.values || item,
627
+ }));
628
+ }
629
+ return [];
630
+ }
631
+ else {
632
+ console.error(`Unsupported data file format: ${ext}`);
633
+ return [];
634
+ }
635
+ }
636
+ catch (error) {
637
+ console.error(`Failed to load data file: ${filePath}`, error);
638
+ return [];
639
+ }
640
+ }
641
+ resolveVariableDefaults(vars) {
642
+ if (!vars)
643
+ return {};
644
+ const resolved = {};
645
+ for (const [key, value] of Object.entries(vars)) {
646
+ if (value && typeof value === 'object' && 'default' in value) {
647
+ resolved[key] = value.default;
648
+ }
649
+ else {
650
+ resolved[key] = value;
651
+ }
652
+ }
653
+ return resolved;
654
+ }
655
+ createLogger() {
656
+ return {
657
+ info: (message, data) => {
658
+ console.log(`[INFO] ${message}`, data !== undefined ? data : '');
659
+ },
660
+ warn: (message, data) => {
661
+ console.warn(`[WARN] ${message}`, data !== undefined ? data : '');
662
+ },
663
+ error: (message, data) => {
664
+ console.error(`[ERROR] ${message}`, data !== undefined ? data : '');
665
+ },
666
+ debug: (message, data) => {
667
+ console.debug(`[DEBUG] ${message}`, data !== undefined ? data : '');
668
+ },
669
+ };
670
+ }
671
+ }
672
+ exports.TestExecutor = TestExecutor;