@testsmith/testblocks 0.7.0 → 0.8.1

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
@@ -41,6 +41,32 @@ const glob_1 = require("glob");
41
41
  const executor_1 = require("./executor");
42
42
  const reporters_1 = require("./reporters");
43
43
  const startServer_1 = require("../server/startServer");
44
+ const plugins_1 = require("../server/plugins");
45
+ const globals_1 = require("../server/globals");
46
+ /**
47
+ * Get the package version from package.json
48
+ */
49
+ function getVersion() {
50
+ try {
51
+ // Try to find package.json relative to the compiled CLI
52
+ const possiblePaths = [
53
+ path.join(__dirname, '../../package.json'), // dist/cli -> package.json
54
+ path.join(__dirname, '../../../package.json'), // nested node_modules
55
+ ];
56
+ for (const pkgPath of possiblePaths) {
57
+ if (fs.existsSync(pkgPath)) {
58
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
59
+ if (pkg.name === '@testsmith/testblocks' && pkg.version) {
60
+ return pkg.version;
61
+ }
62
+ }
63
+ }
64
+ }
65
+ catch {
66
+ // Ignore errors
67
+ }
68
+ return '0.0.0';
69
+ }
44
70
  /**
45
71
  * Search up the directory tree for globals.json starting from the given directory
46
72
  */
@@ -56,11 +82,26 @@ function findGlobalsFile(startDir) {
56
82
  }
57
83
  return null;
58
84
  }
85
+ /**
86
+ * Search up the directory tree for a plugins directory starting from the given directory
87
+ */
88
+ function findPluginsDir(startDir) {
89
+ let currentDir = path.resolve(startDir);
90
+ const root = path.parse(currentDir).root;
91
+ while (currentDir !== root) {
92
+ const pluginsPath = path.join(currentDir, 'plugins');
93
+ if (fs.existsSync(pluginsPath) && fs.statSync(pluginsPath).isDirectory()) {
94
+ return pluginsPath;
95
+ }
96
+ currentDir = path.dirname(currentDir);
97
+ }
98
+ return null;
99
+ }
59
100
  const program = new commander_1.Command();
60
101
  program
61
102
  .name('testblocks')
62
103
  .description('CLI runner for TestBlocks visual test automation')
63
- .version('1.0.0');
104
+ .version(getVersion());
64
105
  program
65
106
  .command('run')
66
107
  .description('Run test files')
@@ -72,6 +113,7 @@ program
72
113
  .option('-b, --base-url <url>', 'Base URL for relative URLs')
73
114
  .option('-v, --var <vars...>', 'Variables in key=value format')
74
115
  .option('-g, --globals <path>', 'Path to globals.json file', './globals.json')
116
+ .option('--plugins-dir <dir>', 'Plugins directory (auto-discovered if not specified)')
75
117
  .option('--fail-fast', 'Stop on first test failure', false)
76
118
  .option('-p, --parallel <count>', 'Number of parallel workers', '1')
77
119
  .option('--filter <pattern>', 'Only run tests matching pattern')
@@ -112,6 +154,33 @@ program
112
154
  console.warn(`Warning: Could not load globals from ${globalsPath}: ${e.message}`);
113
155
  }
114
156
  }
157
+ // Load plugins - search up directory tree from first test file if not explicitly specified
158
+ let pluginsDir = options.pluginsDir ? path.resolve(options.pluginsDir) : null;
159
+ // If plugins dir not specified, auto-discover from test file location or globals location
160
+ if (!pluginsDir && files.length > 0) {
161
+ const testDir = path.dirname(files[0]);
162
+ pluginsDir = findPluginsDir(testDir);
163
+ }
164
+ // Also check next to globals.json if still not found
165
+ if (!pluginsDir && fs.existsSync(globalsPath)) {
166
+ const globalsDir = path.dirname(globalsPath);
167
+ const pluginsDirNextToGlobals = path.join(globalsDir, 'plugins');
168
+ if (fs.existsSync(pluginsDirNextToGlobals) && fs.statSync(pluginsDirNextToGlobals).isDirectory()) {
169
+ pluginsDir = pluginsDirNextToGlobals;
170
+ }
171
+ }
172
+ // Load plugins if directory found
173
+ if (pluginsDir && fs.existsSync(pluginsDir)) {
174
+ (0, plugins_1.setPluginsDirectory)(pluginsDir);
175
+ await (0, plugins_1.loadAllPlugins)();
176
+ (0, plugins_1.initializeServerPlugins)();
177
+ }
178
+ // Load snippets from the globals directory (snippets/ folder next to globals.json)
179
+ if (fs.existsSync(globalsPath)) {
180
+ const globalsDir = path.dirname(globalsPath);
181
+ (0, globals_1.setGlobalsDirectory)(globalsDir);
182
+ (0, globals_1.loadAllSnippets)();
183
+ }
115
184
  // Parse CLI variables (these override globals)
116
185
  const cliVariables = {};
117
186
  if (options.var) {
@@ -143,9 +212,19 @@ program
143
212
  const allResults = [];
144
213
  let hasFailures = false;
145
214
  for (const file of files) {
146
- console.log(`Running: ${path.basename(file)}`);
215
+ // Skip _hooks.testblocks.json files - these are folder hooks, not test files
216
+ const basename = path.basename(file);
217
+ if (basename === '_hooks.testblocks.json') {
218
+ continue;
219
+ }
220
+ console.log(`Running: ${basename}`);
147
221
  const content = fs.readFileSync(file, 'utf-8');
148
222
  const testFile = JSON.parse(content);
223
+ // Skip files that have no tests array (e.g., hooks-only files)
224
+ if (!testFile.tests || !Array.isArray(testFile.tests)) {
225
+ console.log(' (no tests in file)\n');
226
+ continue;
227
+ }
149
228
  // Apply filter if specified
150
229
  if (options.filter) {
151
230
  const filterRegex = new RegExp(options.filter, 'i');
@@ -197,7 +276,13 @@ program
197
276
  }
198
277
  let hasErrors = false;
199
278
  for (const file of files) {
200
- console.log(`Validating: ${path.basename(file)}`);
279
+ const basename = path.basename(file);
280
+ // Skip _hooks.testblocks.json files from validation (they're hooks, not test files)
281
+ if (basename === '_hooks.testblocks.json') {
282
+ console.log(`Skipping: ${basename} (folder hooks file)`);
283
+ continue;
284
+ }
285
+ console.log(`Validating: ${basename}`);
201
286
  try {
202
287
  const content = fs.readFileSync(file, 'utf-8');
203
288
  const testFile = JSON.parse(content);
@@ -208,7 +293,8 @@ program
208
293
  errors.forEach(err => console.log(` - ${err}`));
209
294
  }
210
295
  else {
211
- console.log(` ✓ Valid (${testFile.tests.length} tests)`);
296
+ const testCount = testFile.tests?.length || 0;
297
+ console.log(` ✓ Valid (${testCount} tests)`);
212
298
  }
213
299
  }
214
300
  catch (error) {
@@ -273,7 +359,7 @@ program
273
359
  'test:junit': 'testblocks run tests/**/*.testblocks.json -r junit -o reports',
274
360
  },
275
361
  devDependencies: {
276
- '@testsmith/testblocks': '^0.7.0',
362
+ '@testsmith/testblocks': '^0.8.1',
277
363
  },
278
364
  };
279
365
  fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2));
@@ -453,9 +539,18 @@ program
453
539
  process.exit(1);
454
540
  }
455
541
  for (const file of files) {
456
- console.log(`\n${path.basename(file)}:`);
542
+ // Skip _hooks.testblocks.json files
543
+ const basename = path.basename(file);
544
+ if (basename === '_hooks.testblocks.json') {
545
+ continue;
546
+ }
547
+ console.log(`\n${basename}:`);
457
548
  const content = fs.readFileSync(file, 'utf-8');
458
549
  const testFile = JSON.parse(content);
550
+ if (!testFile.tests || !Array.isArray(testFile.tests)) {
551
+ console.log(' (no tests in file)');
552
+ continue;
553
+ }
459
554
  testFile.tests.forEach((test, index) => {
460
555
  const tags = test.tags?.length ? ` [${test.tags.join(', ')}]` : '';
461
556
  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.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "Visual test automation tool with Blockly - API and Playwright testing",
5
5
  "author": "Roy de Kleijn",
6
6
  "license": "MIT",