@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.
- package/dist/cli/executor.d.ts +2 -2
- package/dist/cli/executor.js +29 -5
- package/dist/cli/index.js +101 -6
- package/package.json +1 -1
package/dist/cli/executor.d.ts
CHANGED
|
@@ -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;
|
package/dist/cli/executor.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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}`);
|