@testsmith/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 (47) 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 +431 -0
  7. package/dist/cli/reporters.d.ts +62 -0
  8. package/dist/cli/reporters.js +451 -0
  9. package/dist/client/assets/index-Cq84-VIf.js +2137 -0
  10. package/dist/client/assets/index-Cq84-VIf.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 +226 -0
  45. package/dist/server/startServer.d.ts +7 -0
  46. package/dist/server/startServer.js +405 -0
  47. package/package.json +104 -0
@@ -0,0 +1,431 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const commander_1 = require("commander");
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const glob_1 = require("glob");
41
+ const executor_1 = require("./executor");
42
+ const reporters_1 = require("./reporters");
43
+ const startServer_1 = require("../server/startServer");
44
+ const program = new commander_1.Command();
45
+ program
46
+ .name('testblocks')
47
+ .description('CLI runner for TestBlocks visual test automation')
48
+ .version('1.0.0');
49
+ program
50
+ .command('run')
51
+ .description('Run test files')
52
+ .argument('<patterns...>', 'Test file patterns (glob supported)')
53
+ .option('-H, --headed', 'Run tests in headed mode (show browser)', false)
54
+ .option('-t, --timeout <ms>', 'Test timeout in milliseconds', '30000')
55
+ .option('-r, --reporter <type>', 'Reporter type: console, json, junit, html', 'console')
56
+ .option('-o, --output <dir>', 'Output directory for reports', './testblocks-results')
57
+ .option('-b, --base-url <url>', 'Base URL for relative URLs')
58
+ .option('-v, --var <vars...>', 'Variables in key=value format')
59
+ .option('--fail-fast', 'Stop on first test failure', false)
60
+ .option('-p, --parallel <count>', 'Number of parallel workers', '1')
61
+ .option('--filter <pattern>', 'Only run tests matching pattern')
62
+ .action(async (patterns, options) => {
63
+ try {
64
+ // Find test files
65
+ const files = [];
66
+ for (const pattern of patterns) {
67
+ const matches = await (0, glob_1.glob)(pattern, { absolute: true });
68
+ files.push(...matches);
69
+ }
70
+ if (files.length === 0) {
71
+ console.error('No test files found matching patterns:', patterns);
72
+ process.exit(1);
73
+ }
74
+ console.log(`Found ${files.length} test file(s)\n`);
75
+ // Parse variables
76
+ const variables = {};
77
+ if (options.var) {
78
+ for (const v of options.var) {
79
+ const [key, ...valueParts] = v.split('=');
80
+ const value = valueParts.join('=');
81
+ // Try to parse as JSON, otherwise use as string
82
+ try {
83
+ variables[key] = JSON.parse(value);
84
+ }
85
+ catch {
86
+ variables[key] = value;
87
+ }
88
+ }
89
+ }
90
+ // Create executor options
91
+ const executorOptions = {
92
+ headless: !options.headed,
93
+ timeout: parseInt(options.timeout, 10),
94
+ baseUrl: options.baseUrl,
95
+ variables,
96
+ };
97
+ // Create reporter
98
+ const reporter = createReporter(options.reporter, options.output);
99
+ // Run tests
100
+ const allResults = [];
101
+ let hasFailures = false;
102
+ for (const file of files) {
103
+ console.log(`Running: ${path.basename(file)}`);
104
+ const content = fs.readFileSync(file, 'utf-8');
105
+ const testFile = JSON.parse(content);
106
+ // Apply filter if specified
107
+ if (options.filter) {
108
+ const filterRegex = new RegExp(options.filter, 'i');
109
+ testFile.tests = testFile.tests.filter(t => filterRegex.test(t.name));
110
+ }
111
+ if (testFile.tests.length === 0) {
112
+ console.log(' (no tests match filter)\n');
113
+ continue;
114
+ }
115
+ const executor = new executor_1.TestExecutor(executorOptions);
116
+ const results = await executor.runTestFile(testFile);
117
+ allResults.push({ file, results });
118
+ // Report results
119
+ reporter.onTestFileComplete(file, testFile, results);
120
+ // Check for failures
121
+ const failed = results.some(r => r.status !== 'passed');
122
+ if (failed) {
123
+ hasFailures = true;
124
+ if (options.failFast) {
125
+ console.log('\nStopping due to --fail-fast\n');
126
+ break;
127
+ }
128
+ }
129
+ }
130
+ // Generate final report
131
+ reporter.onComplete(allResults);
132
+ // Exit with appropriate code
133
+ process.exit(hasFailures ? 1 : 0);
134
+ }
135
+ catch (error) {
136
+ console.error('Error:', error.message);
137
+ process.exit(1);
138
+ }
139
+ });
140
+ program
141
+ .command('validate')
142
+ .description('Validate test files without running them')
143
+ .argument('<patterns...>', 'Test file patterns (glob supported)')
144
+ .action(async (patterns) => {
145
+ try {
146
+ const files = [];
147
+ for (const pattern of patterns) {
148
+ const matches = await (0, glob_1.glob)(pattern, { absolute: true });
149
+ files.push(...matches);
150
+ }
151
+ if (files.length === 0) {
152
+ console.error('No test files found matching patterns:', patterns);
153
+ process.exit(1);
154
+ }
155
+ let hasErrors = false;
156
+ for (const file of files) {
157
+ console.log(`Validating: ${path.basename(file)}`);
158
+ try {
159
+ const content = fs.readFileSync(file, 'utf-8');
160
+ const testFile = JSON.parse(content);
161
+ const errors = validateTestFile(testFile);
162
+ if (errors.length > 0) {
163
+ hasErrors = true;
164
+ console.log(' ✗ Invalid');
165
+ errors.forEach(err => console.log(` - ${err}`));
166
+ }
167
+ else {
168
+ console.log(` ✓ Valid (${testFile.tests.length} tests)`);
169
+ }
170
+ }
171
+ catch (error) {
172
+ hasErrors = true;
173
+ console.log(` ✗ Parse error: ${error.message}`);
174
+ }
175
+ }
176
+ process.exit(hasErrors ? 1 : 0);
177
+ }
178
+ catch (error) {
179
+ console.error('Error:', error.message);
180
+ process.exit(1);
181
+ }
182
+ });
183
+ program
184
+ .command('init')
185
+ .description('Initialize a new TestBlocks project')
186
+ .argument('[directory]', 'Directory to initialize (default: current directory)', '.')
187
+ .option('--name <name>', 'Project name', 'my-testblocks-project')
188
+ .action((directory, options) => {
189
+ const projectDir = path.resolve(directory);
190
+ const projectName = options.name;
191
+ console.log(`\nInitializing TestBlocks project in ${projectDir}...\n`);
192
+ // Create directories
193
+ const dirs = ['tests', 'snippets', 'plugins', 'reports'];
194
+ dirs.forEach(dir => {
195
+ const dirPath = path.join(projectDir, dir);
196
+ if (!fs.existsSync(dirPath)) {
197
+ fs.mkdirSync(dirPath, { recursive: true });
198
+ console.log(` Created: ${dir}/`);
199
+ }
200
+ });
201
+ // Create globals.json
202
+ const globalsPath = path.join(projectDir, 'globals.json');
203
+ if (!fs.existsSync(globalsPath)) {
204
+ const globals = {
205
+ variables: {
206
+ baseUrl: 'https://example.com',
207
+ credentials: {
208
+ validUser: {
209
+ email: 'test@example.com',
210
+ password: 'password123',
211
+ },
212
+ },
213
+ },
214
+ testIdAttribute: 'data-testid',
215
+ };
216
+ fs.writeFileSync(globalsPath, JSON.stringify(globals, null, 2));
217
+ console.log(' Created: globals.json');
218
+ }
219
+ // Create package.json
220
+ const packagePath = path.join(projectDir, 'package.json');
221
+ if (!fs.existsSync(packagePath)) {
222
+ const packageJson = {
223
+ name: projectName,
224
+ version: '1.0.0',
225
+ description: 'TestBlocks test automation project',
226
+ scripts: {
227
+ test: 'testblocks run tests/**/*.testblocks.json',
228
+ 'test:headed': 'testblocks run tests/**/*.testblocks.json --headed',
229
+ 'test:html': 'testblocks run tests/**/*.testblocks.json -r html -o reports',
230
+ 'test:junit': 'testblocks run tests/**/*.testblocks.json -r junit -o reports',
231
+ },
232
+ devDependencies: {
233
+ '@testsmith/testblocks': '^0.1.0',
234
+ },
235
+ };
236
+ fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2));
237
+ console.log(' Created: package.json');
238
+ }
239
+ // Create example test file
240
+ const exampleTestPath = path.join(projectDir, 'tests', 'example.testblocks.json');
241
+ if (!fs.existsSync(exampleTestPath)) {
242
+ const exampleTest = {
243
+ version: '1.0.0',
244
+ name: 'Example Test Suite',
245
+ description: 'Sample tests demonstrating TestBlocks features',
246
+ variables: {},
247
+ tests: [
248
+ {
249
+ id: 'test-web-1',
250
+ name: 'Example Web Test',
251
+ description: 'Navigate to a page and verify content',
252
+ steps: [
253
+ {
254
+ id: 'step-1',
255
+ type: 'web_navigate',
256
+ params: { URL: '${baseUrl}' },
257
+ },
258
+ {
259
+ id: 'step-2',
260
+ type: 'web_expect_title',
261
+ params: { EXPECTED: 'Example Domain' },
262
+ },
263
+ ],
264
+ tags: ['web', 'smoke'],
265
+ },
266
+ ],
267
+ };
268
+ fs.writeFileSync(exampleTestPath, JSON.stringify(exampleTest, null, 2));
269
+ console.log(' Created: tests/example.testblocks.json');
270
+ }
271
+ // Create data-driven example
272
+ const dataDrivenPath = path.join(projectDir, 'tests', 'login-data-driven.testblocks.json');
273
+ if (!fs.existsSync(dataDrivenPath)) {
274
+ const dataDrivenTest = {
275
+ version: '1.0.0',
276
+ name: 'Login Tests (Data-Driven)',
277
+ description: 'Data-driven login tests with multiple user credentials',
278
+ variables: {},
279
+ tests: [
280
+ {
281
+ id: 'test-login-dd',
282
+ name: 'Login with credentials',
283
+ description: 'Tests login with different user types',
284
+ data: [
285
+ { name: 'Valid Admin', values: { username: 'admin@example.com', password: 'admin123', shouldSucceed: true } },
286
+ { name: 'Valid User', values: { username: 'user@example.com', password: 'user123', shouldSucceed: true } },
287
+ { name: 'Invalid Password', values: { username: 'user@example.com', password: 'wrong', shouldSucceed: false } },
288
+ { name: 'Invalid User', values: { username: 'nobody@example.com', password: 'test', shouldSucceed: false } },
289
+ ],
290
+ steps: [
291
+ {
292
+ id: 'step-1',
293
+ type: 'web_navigate',
294
+ params: { URL: '${baseUrl}/login' },
295
+ },
296
+ {
297
+ id: 'step-2',
298
+ type: 'web_fill',
299
+ params: { SELECTOR: '#email', VALUE: '${username}' },
300
+ },
301
+ {
302
+ id: 'step-3',
303
+ type: 'web_fill',
304
+ params: { SELECTOR: '#password', VALUE: '${password}' },
305
+ },
306
+ {
307
+ id: 'step-4',
308
+ type: 'web_click',
309
+ params: { SELECTOR: 'button[type="submit"]' },
310
+ },
311
+ ],
312
+ tags: ['web', 'login', 'data-driven'],
313
+ },
314
+ ],
315
+ };
316
+ fs.writeFileSync(dataDrivenPath, JSON.stringify(dataDrivenTest, null, 2));
317
+ console.log(' Created: tests/login-data-driven.testblocks.json');
318
+ }
319
+ // Create .gitignore
320
+ const gitignorePath = path.join(projectDir, '.gitignore');
321
+ if (!fs.existsSync(gitignorePath)) {
322
+ const gitignore = `# Dependencies
323
+ node_modules/
324
+
325
+ # Reports
326
+ reports/
327
+
328
+ # IDE
329
+ .idea/
330
+ .vscode/
331
+
332
+ # OS
333
+ .DS_Store
334
+ Thumbs.db
335
+ `;
336
+ fs.writeFileSync(gitignorePath, gitignore);
337
+ console.log(' Created: .gitignore');
338
+ }
339
+ console.log('\n✓ Project initialized successfully!\n');
340
+ console.log('Next steps:');
341
+ console.log(' 1. cd ' + (directory === '.' ? '' : directory));
342
+ console.log(' 2. npm install');
343
+ console.log(' 3. npm test\n');
344
+ console.log('To open the visual test editor:');
345
+ console.log(' testblocks serve\n');
346
+ });
347
+ program
348
+ .command('list')
349
+ .description('List tests in test files')
350
+ .argument('<patterns...>', 'Test file patterns (glob supported)')
351
+ .action(async (patterns) => {
352
+ try {
353
+ const files = [];
354
+ for (const pattern of patterns) {
355
+ const matches = await (0, glob_1.glob)(pattern, { absolute: true });
356
+ files.push(...matches);
357
+ }
358
+ if (files.length === 0) {
359
+ console.error('No test files found matching patterns:', patterns);
360
+ process.exit(1);
361
+ }
362
+ for (const file of files) {
363
+ console.log(`\n${path.basename(file)}:`);
364
+ const content = fs.readFileSync(file, 'utf-8');
365
+ const testFile = JSON.parse(content);
366
+ testFile.tests.forEach((test, index) => {
367
+ const tags = test.tags?.length ? ` [${test.tags.join(', ')}]` : '';
368
+ console.log(` ${index + 1}. ${test.name}${tags}`);
369
+ });
370
+ }
371
+ }
372
+ catch (error) {
373
+ console.error('Error:', error.message);
374
+ process.exit(1);
375
+ }
376
+ });
377
+ program
378
+ .command('serve')
379
+ .description('Start the TestBlocks web UI')
380
+ .option('-p, --port <port>', 'Port to run on', '3000')
381
+ .option('--plugins-dir <dir>', 'Plugins directory', './plugins')
382
+ .option('--globals-dir <dir>', 'Globals directory (where globals.json is located)', '.')
383
+ .option('-o, --open', 'Open browser automatically', false)
384
+ .action(async (options) => {
385
+ const port = parseInt(options.port, 10);
386
+ const pluginsDir = path.resolve(options.pluginsDir);
387
+ const globalsDir = path.resolve(options.globalsDir);
388
+ await (0, startServer_1.startServer)({
389
+ port,
390
+ pluginsDir,
391
+ globalsDir,
392
+ open: options.open,
393
+ });
394
+ });
395
+ function createReporter(type, outputDir) {
396
+ switch (type) {
397
+ case 'json':
398
+ return new reporters_1.JSONReporter(outputDir);
399
+ case 'junit':
400
+ return new reporters_1.JUnitReporter(outputDir);
401
+ case 'html':
402
+ return new reporters_1.HTMLReporter(outputDir);
403
+ case 'console':
404
+ default:
405
+ return new reporters_1.ConsoleReporter();
406
+ }
407
+ }
408
+ function validateTestFile(testFile) {
409
+ const errors = [];
410
+ if (!testFile.version) {
411
+ errors.push('Missing version field');
412
+ }
413
+ if (!testFile.name) {
414
+ errors.push('Missing name field');
415
+ }
416
+ if (!testFile.tests || !Array.isArray(testFile.tests)) {
417
+ errors.push('Missing or invalid tests array');
418
+ }
419
+ else {
420
+ testFile.tests.forEach((test, index) => {
421
+ if (!test.id) {
422
+ errors.push(`Test at index ${index} is missing an id`);
423
+ }
424
+ if (!test.name) {
425
+ errors.push(`Test at index ${index} is missing a name`);
426
+ }
427
+ });
428
+ }
429
+ return errors;
430
+ }
431
+ program.parse();
@@ -0,0 +1,62 @@
1
+ import { TestFile, TestResult } from '../core';
2
+ export interface Reporter {
3
+ onTestFileComplete(file: string, testFile: TestFile, results: TestResult[]): void;
4
+ onComplete(allResults: {
5
+ file: string;
6
+ results: TestResult[];
7
+ }[]): void;
8
+ }
9
+ export declare function getTimestamp(): string;
10
+ export declare class ConsoleReporter implements Reporter {
11
+ onTestFileComplete(file: string, testFile: TestFile, results: TestResult[]): void;
12
+ onComplete(allResults: {
13
+ file: string;
14
+ results: TestResult[];
15
+ }[]): void;
16
+ }
17
+ export declare class JSONReporter implements Reporter {
18
+ private outputDir;
19
+ private allResults;
20
+ constructor(outputDir: string);
21
+ onTestFileComplete(file: string, testFile: TestFile, results: TestResult[]): void;
22
+ onComplete(allResults: {
23
+ file: string;
24
+ results: TestResult[];
25
+ }[]): void;
26
+ }
27
+ export declare class JUnitReporter implements Reporter {
28
+ private outputDir;
29
+ private allResults;
30
+ constructor(outputDir: string);
31
+ onTestFileComplete(file: string, testFile: TestFile, results: TestResult[]): void;
32
+ onComplete(allResults: {
33
+ file: string;
34
+ results: TestResult[];
35
+ }[]): void;
36
+ }
37
+ export declare class HTMLReporter implements Reporter {
38
+ private outputDir;
39
+ private allResults;
40
+ constructor(outputDir: string);
41
+ onTestFileComplete(file: string, testFile: TestFile, results: TestResult[]): void;
42
+ onComplete(allResults: {
43
+ file: string;
44
+ results: TestResult[];
45
+ }[]): void;
46
+ }
47
+ export interface ReportData {
48
+ timestamp: string;
49
+ summary: {
50
+ totalTests: number;
51
+ passed: number;
52
+ failed: number;
53
+ duration: number;
54
+ };
55
+ testFiles: {
56
+ file: string;
57
+ testFile: TestFile;
58
+ results: TestResult[];
59
+ }[];
60
+ }
61
+ export declare function generateHTMLReport(data: ReportData): string;
62
+ export declare function generateJUnitXML(data: ReportData): string;