@testsmith/testblocks 0.8.0 → 0.8.2

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.
@@ -20,8 +20,8 @@ export declare class TestExecutor {
20
20
  private requiresBrowser;
21
21
  runTestFile(testFile: TestFile): Promise<TestResult[]>;
22
22
  private createBaseContext;
23
- runTestWithData(test: TestCase, testFile: TestFile, dataSet: TestDataSet, dataIndex: number): Promise<TestResult>;
24
- runTest(test: TestCase, testFile: TestFile): Promise<TestResult>;
23
+ runTestWithData(test: TestCase, testFile: TestFile, dataSet: TestDataSet, dataIndex: number, sharedVariables?: Map<string, unknown>): Promise<TestResult>;
24
+ runTest(test: TestCase, testFile: TestFile, sharedVariables?: Map<string, unknown>): Promise<TestResult>;
25
25
  private runSteps;
26
26
  private resolveVariableDefaults;
27
27
  private registerCustomBlocksFromProcedures;
@@ -99,20 +99,20 @@ class TestExecutor {
99
99
  const steps = this.extractStepsFromBlocklyState(testFile.beforeAll);
100
100
  await this.runSteps(steps, baseContext, 'beforeAll');
101
101
  }
102
- // Run each test
102
+ // Run each test - pass baseContext variables so beforeAll state persists
103
103
  for (const test of testFile.tests) {
104
104
  // Check if test has data-driven sets
105
105
  if (test.data && test.data.length > 0) {
106
106
  // Run test for each data set
107
107
  for (let i = 0; i < test.data.length; i++) {
108
108
  const dataSet = test.data[i];
109
- const result = await this.runTestWithData(test, testFile, dataSet, i);
109
+ const result = await this.runTestWithData(test, testFile, dataSet, i, baseContext.variables);
110
110
  results.push(result);
111
111
  }
112
112
  }
113
113
  else {
114
114
  // Run test once without data
115
- const result = await this.runTest(test, testFile);
115
+ const result = await this.runTest(test, testFile, baseContext.variables);
116
116
  results.push(result);
117
117
  }
118
118
  }
@@ -141,7 +141,7 @@ class TestExecutor {
141
141
  procedures: this.procedures,
142
142
  };
143
143
  }
144
- async runTestWithData(test, testFile, dataSet, dataIndex) {
144
+ async runTestWithData(test, testFile, dataSet, dataIndex, sharedVariables) {
145
145
  const testName = dataSet.name
146
146
  ? `${test.name} [${dataSet.name}]`
147
147
  : `${test.name} [${dataIndex + 1}]`;
@@ -155,6 +155,17 @@ class TestExecutor {
155
155
  currentData: dataSet,
156
156
  dataIndex,
157
157
  };
158
+ // Merge shared variables from beforeAll (if any)
159
+ if (sharedVariables) {
160
+ for (const [key, value] of sharedVariables) {
161
+ if (!context.variables.has(key) || context.variables.get(key) === '' || context.variables.get(key) === undefined) {
162
+ context.variables.set(key, value);
163
+ }
164
+ else if (key.startsWith('__')) {
165
+ context.variables.set(key, value);
166
+ }
167
+ }
168
+ }
158
169
  // Inject data values into variables
159
170
  for (const [key, value] of Object.entries(dataSet.values)) {
160
171
  context.variables.set(key, value);
@@ -239,12 +250,25 @@ class TestExecutor {
239
250
  },
240
251
  };
241
252
  }
242
- async runTest(test, testFile) {
253
+ async runTest(test, testFile, sharedVariables) {
243
254
  console.log(` Running: ${test.name}`);
244
255
  const startedAt = new Date().toISOString();
245
256
  const startTime = Date.now();
246
257
  const stepResults = [];
247
258
  const context = this.createBaseContext(testFile.variables);
259
+ // Merge shared variables from beforeAll (if any)
260
+ if (sharedVariables) {
261
+ for (const [key, value] of sharedVariables) {
262
+ // Only copy if not already set (don't override file-level defaults)
263
+ if (!context.variables.has(key) || context.variables.get(key) === '' || context.variables.get(key) === undefined) {
264
+ context.variables.set(key, value);
265
+ }
266
+ else if (key.startsWith('__')) {
267
+ // Always copy internal state variables like __requestHeaders
268
+ context.variables.set(key, value);
269
+ }
270
+ }
271
+ }
248
272
  for (const plugin of this.plugins.values()) {
249
273
  if (plugin.hooks?.beforeTest) {
250
274
  await plugin.hooks.beforeTest(context, test);
package/dist/cli/index.js CHANGED
@@ -97,6 +97,75 @@ function findPluginsDir(startDir) {
97
97
  }
98
98
  return null;
99
99
  }
100
+ /**
101
+ * Load folder hooks from a test file's directory up to the root globals directory
102
+ * Returns hooks in order from outermost folder to innermost
103
+ */
104
+ function loadFolderHooks(testFilePath, globalsDir) {
105
+ let currentDir = path.dirname(path.resolve(testFilePath));
106
+ const stopDir = globalsDir ? path.resolve(globalsDir) : null;
107
+ // Collect hooks from innermost to outermost
108
+ const collectedHooks = [];
109
+ while (currentDir) {
110
+ const hooksPath = path.join(currentDir, '_hooks.testblocks.json');
111
+ if (fs.existsSync(hooksPath)) {
112
+ try {
113
+ const content = fs.readFileSync(hooksPath, 'utf-8');
114
+ const hooksFile = JSON.parse(content);
115
+ collectedHooks.push(hooksFile);
116
+ }
117
+ catch (e) {
118
+ console.warn(`Warning: Could not load hooks from ${hooksPath}: ${e.message}`);
119
+ }
120
+ }
121
+ // Stop at globals directory or root
122
+ if (stopDir && currentDir === stopDir)
123
+ break;
124
+ const parentDir = path.dirname(currentDir);
125
+ if (parentDir === currentDir)
126
+ break; // Reached root
127
+ currentDir = parentDir;
128
+ }
129
+ // Reverse to get outermost-to-innermost order
130
+ return collectedHooks.reverse();
131
+ }
132
+ /**
133
+ * Merge folder hooks into a test file
134
+ * - beforeAll/beforeEach: parent hooks run first
135
+ * - afterAll/afterEach: child hooks run first (reverse order)
136
+ */
137
+ function mergeFolderHooksIntoTestFile(testFile, folderHooks) {
138
+ if (!folderHooks || folderHooks.length === 0) {
139
+ return testFile;
140
+ }
141
+ const beforeAllSteps = [];
142
+ const afterAllSteps = [];
143
+ const beforeEachSteps = [];
144
+ const afterEachSteps = [];
145
+ // Parent to child order for beforeAll/beforeEach
146
+ for (const hooks of folderHooks) {
147
+ if (hooks.beforeAll)
148
+ beforeAllSteps.push(...hooks.beforeAll);
149
+ if (hooks.beforeEach)
150
+ beforeEachSteps.push(...hooks.beforeEach);
151
+ }
152
+ // Child to parent order for afterAll/afterEach
153
+ for (let i = folderHooks.length - 1; i >= 0; i--) {
154
+ const hooks = folderHooks[i];
155
+ if (hooks.afterAll)
156
+ afterAllSteps.unshift(...hooks.afterAll);
157
+ if (hooks.afterEach)
158
+ afterEachSteps.unshift(...hooks.afterEach);
159
+ }
160
+ // Merge with test file hooks
161
+ return {
162
+ ...testFile,
163
+ beforeAll: [...beforeAllSteps, ...(testFile.beforeAll || [])],
164
+ afterAll: [...(testFile.afterAll || []), ...afterAllSteps],
165
+ beforeEach: [...beforeEachSteps, ...(testFile.beforeEach || [])],
166
+ afterEach: [...(testFile.afterEach || []), ...afterEachSteps],
167
+ };
168
+ }
100
169
  const program = new commander_1.Command();
101
170
  program
102
171
  .name('testblocks')
@@ -212,9 +281,25 @@ program
212
281
  const allResults = [];
213
282
  let hasFailures = false;
214
283
  for (const file of files) {
215
- console.log(`Running: ${path.basename(file)}`);
284
+ // Skip _hooks.testblocks.json files - these are folder hooks, not test files
285
+ const basename = path.basename(file);
286
+ if (basename === '_hooks.testblocks.json') {
287
+ continue;
288
+ }
289
+ console.log(`Running: ${basename}`);
216
290
  const content = fs.readFileSync(file, 'utf-8');
217
- const testFile = JSON.parse(content);
291
+ let testFile = JSON.parse(content);
292
+ // Skip files that have no tests array (e.g., hooks-only files)
293
+ if (!testFile.tests || !Array.isArray(testFile.tests)) {
294
+ console.log(' (no tests in file)\n');
295
+ continue;
296
+ }
297
+ // Load and merge folder hooks
298
+ const globalsDir = fs.existsSync(globalsPath) ? path.dirname(globalsPath) : null;
299
+ const folderHooks = loadFolderHooks(file, globalsDir);
300
+ if (folderHooks.length > 0) {
301
+ testFile = mergeFolderHooksIntoTestFile(testFile, folderHooks);
302
+ }
218
303
  // Apply filter if specified
219
304
  if (options.filter) {
220
305
  const filterRegex = new RegExp(options.filter, 'i');
@@ -266,7 +351,13 @@ program
266
351
  }
267
352
  let hasErrors = false;
268
353
  for (const file of files) {
269
- console.log(`Validating: ${path.basename(file)}`);
354
+ const basename = path.basename(file);
355
+ // Skip _hooks.testblocks.json files from validation (they're hooks, not test files)
356
+ if (basename === '_hooks.testblocks.json') {
357
+ console.log(`Skipping: ${basename} (folder hooks file)`);
358
+ continue;
359
+ }
360
+ console.log(`Validating: ${basename}`);
270
361
  try {
271
362
  const content = fs.readFileSync(file, 'utf-8');
272
363
  const testFile = JSON.parse(content);
@@ -277,7 +368,8 @@ program
277
368
  errors.forEach(err => console.log(` - ${err}`));
278
369
  }
279
370
  else {
280
- console.log(` ✓ Valid (${testFile.tests.length} tests)`);
371
+ const testCount = testFile.tests?.length || 0;
372
+ console.log(` ✓ Valid (${testCount} tests)`);
281
373
  }
282
374
  }
283
375
  catch (error) {
@@ -342,7 +434,7 @@ program
342
434
  'test:junit': 'testblocks run tests/**/*.testblocks.json -r junit -o reports',
343
435
  },
344
436
  devDependencies: {
345
- '@testsmith/testblocks': '^0.8.0',
437
+ '@testsmith/testblocks': '^0.8.2',
346
438
  },
347
439
  };
348
440
  fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2));
@@ -522,9 +614,18 @@ program
522
614
  process.exit(1);
523
615
  }
524
616
  for (const file of files) {
525
- console.log(`\n${path.basename(file)}:`);
617
+ // Skip _hooks.testblocks.json files
618
+ const basename = path.basename(file);
619
+ if (basename === '_hooks.testblocks.json') {
620
+ continue;
621
+ }
622
+ console.log(`\n${basename}:`);
526
623
  const content = fs.readFileSync(file, 'utf-8');
527
624
  const testFile = JSON.parse(content);
625
+ if (!testFile.tests || !Array.isArray(testFile.tests)) {
626
+ console.log(' (no tests in file)');
627
+ continue;
628
+ }
528
629
  testFile.tests.forEach((test, index) => {
529
630
  const tags = test.tags?.length ? ` [${test.tags.join(', ')}]` : '';
530
631
  console.log(` ${index + 1}. ${test.name}${tags}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testsmith/testblocks",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Visual test automation tool with Blockly - API and Playwright testing",
5
5
  "author": "Roy de Kleijn",
6
6
  "license": "MIT",