@testsmith/testblocks 0.6.0 → 0.7.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 (71) hide show
  1. package/dist/cli/executor.d.ts +4 -1
  2. package/dist/cli/executor.js +101 -5
  3. package/dist/cli/index.js +78 -3
  4. package/dist/cli/reporters/ConsoleReporter.d.ts +12 -0
  5. package/dist/cli/reporters/ConsoleReporter.js +39 -0
  6. package/dist/cli/reporters/HTMLReporter.d.ts +19 -0
  7. package/dist/cli/reporters/HTMLReporter.js +506 -0
  8. package/dist/cli/reporters/JSONReporter.d.ts +15 -0
  9. package/dist/cli/reporters/JSONReporter.js +80 -0
  10. package/dist/cli/reporters/JUnitReporter.d.ts +19 -0
  11. package/dist/cli/reporters/JUnitReporter.js +105 -0
  12. package/dist/cli/reporters/index.d.ts +17 -0
  13. package/dist/cli/reporters/index.js +31 -0
  14. package/dist/cli/reporters/types.d.ts +28 -0
  15. package/dist/cli/reporters/types.js +2 -0
  16. package/dist/cli/reporters/utils.d.ts +31 -0
  17. package/dist/cli/reporters/utils.js +136 -0
  18. package/dist/cli/reporters.d.ts +13 -62
  19. package/dist/cli/reporters.js +16 -719
  20. package/dist/client/assets/index-Boo8ZrY_.js +2195 -0
  21. package/dist/client/assets/{index-dXniUrbi.js.map → index-Boo8ZrY_.js.map} +1 -1
  22. package/dist/client/assets/index-OxNH9dW-.css +1 -0
  23. package/dist/client/index.html +2 -2
  24. package/dist/core/blocks/api.js +3 -6
  25. package/dist/core/blocks/assertions.d.ts +31 -0
  26. package/dist/core/blocks/assertions.js +72 -0
  27. package/dist/core/blocks/index.d.ts +1 -0
  28. package/dist/core/blocks/index.js +6 -1
  29. package/dist/core/blocks/lifecycle.js +5 -3
  30. package/dist/core/blocks/logic.js +2 -3
  31. package/dist/core/blocks/playwright/assertions.d.ts +5 -0
  32. package/dist/core/blocks/playwright/assertions.js +321 -0
  33. package/dist/core/blocks/playwright/index.d.ts +17 -0
  34. package/dist/core/blocks/playwright/index.js +49 -0
  35. package/dist/core/blocks/playwright/interactions.d.ts +5 -0
  36. package/dist/core/blocks/playwright/interactions.js +191 -0
  37. package/dist/core/blocks/playwright/navigation.d.ts +5 -0
  38. package/dist/core/blocks/playwright/navigation.js +133 -0
  39. package/dist/core/blocks/playwright/retrieval.d.ts +5 -0
  40. package/dist/core/blocks/playwright/retrieval.js +144 -0
  41. package/dist/core/blocks/playwright/types.d.ts +65 -0
  42. package/dist/core/blocks/playwright/types.js +5 -0
  43. package/dist/core/blocks/playwright/utils.d.ts +26 -0
  44. package/dist/core/blocks/playwright/utils.js +137 -0
  45. package/dist/core/blocks/playwright.d.ts +13 -2
  46. package/dist/core/blocks/playwright.js +14 -761
  47. package/dist/core/executor/BaseTestExecutor.d.ts +60 -0
  48. package/dist/core/executor/BaseTestExecutor.js +297 -0
  49. package/dist/core/executor/index.d.ts +1 -0
  50. package/dist/core/executor/index.js +5 -0
  51. package/dist/core/index.d.ts +1 -0
  52. package/dist/core/index.js +4 -0
  53. package/dist/core/types.d.ts +12 -0
  54. package/dist/core/utils/blocklyParser.d.ts +18 -0
  55. package/dist/core/utils/blocklyParser.js +84 -0
  56. package/dist/core/utils/dataLoader.d.ts +9 -0
  57. package/dist/core/utils/dataLoader.js +117 -0
  58. package/dist/core/utils/index.d.ts +2 -0
  59. package/dist/core/utils/index.js +12 -0
  60. package/dist/core/utils/logger.d.ts +14 -0
  61. package/dist/core/utils/logger.js +48 -0
  62. package/dist/core/utils/variableResolver.d.ts +24 -0
  63. package/dist/core/utils/variableResolver.js +92 -0
  64. package/dist/server/executor.d.ts +6 -0
  65. package/dist/server/executor.js +207 -47
  66. package/dist/server/globals.d.ts +6 -1
  67. package/dist/server/globals.js +7 -0
  68. package/dist/server/startServer.js +15 -0
  69. package/package.json +1 -1
  70. package/dist/client/assets/index-dXniUrbi.js +0 -2193
  71. package/dist/client/assets/index-oTTttNKd.css +0 -1
@@ -1,10 +1,11 @@
1
- import { TestFile, TestCase, TestResult, Plugin, TestDataSet } from '../core';
1
+ import { TestFile, TestCase, TestResult, Plugin, ProcedureDefinition, TestDataSet } from '../core';
2
2
  export interface ExecutorOptions {
3
3
  headless?: boolean;
4
4
  timeout?: number;
5
5
  baseUrl?: string;
6
6
  variables?: Record<string, unknown>;
7
7
  plugins?: Plugin[];
8
+ procedures?: Record<string, ProcedureDefinition>;
8
9
  }
9
10
  export declare class TestExecutor {
10
11
  private options;
@@ -23,11 +24,13 @@ export declare class TestExecutor {
23
24
  runTest(test: TestCase, testFile: TestFile): Promise<TestResult>;
24
25
  private runSteps;
25
26
  private resolveVariableDefaults;
27
+ private registerCustomBlocksFromProcedures;
26
28
  private extractStepsFromBlocklyState;
27
29
  private blocksToSteps;
28
30
  private blockToStep;
29
31
  private runStep;
30
32
  private executeProcedure;
33
+ private resolveVariablePlaceholders;
31
34
  private resolveParams;
32
35
  private createLogger;
33
36
  }
@@ -20,6 +20,13 @@ class TestExecutor {
20
20
  this.plugins.set(plugin.name, plugin);
21
21
  });
22
22
  }
23
+ // Register project-level procedures from options
24
+ if (options.procedures) {
25
+ for (const [name, procedure] of Object.entries(options.procedures)) {
26
+ this.procedures.set(name, procedure);
27
+ }
28
+ this.registerCustomBlocksFromProcedures(options.procedures);
29
+ }
23
30
  }
24
31
  async initialize() {
25
32
  this.browser = await playwright_1.chromium.launch({
@@ -81,6 +88,8 @@ class TestExecutor {
81
88
  for (const [name, procedure] of Object.entries(testFile.procedures)) {
82
89
  this.procedures.set(name, procedure);
83
90
  }
91
+ // Also register them as custom blocks so custom_xxx blocks work
92
+ this.registerCustomBlocksFromProcedures(testFile.procedures);
84
93
  }
85
94
  // Create base context for hooks
86
95
  const baseContext = this.createBaseContext(testFile.variables);
@@ -122,7 +131,7 @@ class TestExecutor {
122
131
  return {
123
132
  variables: new Map(Object.entries({
124
133
  ...this.resolveVariableDefaults(fileVariables),
125
- ...this.options.variables,
134
+ ...this.resolveVariableDefaults(this.options.variables),
126
135
  })),
127
136
  results: [],
128
137
  browser: this.browser,
@@ -347,6 +356,56 @@ class TestExecutor {
347
356
  }
348
357
  return resolved;
349
358
  }
359
+ registerCustomBlocksFromProcedures(procedures) {
360
+ Object.entries(procedures).forEach(([_name, proc]) => {
361
+ if (!proc.steps || proc.steps.length === 0)
362
+ return;
363
+ const blockType = `custom_${proc.name.toLowerCase().replace(/\s+/g, '_')}`;
364
+ // Check if already registered
365
+ if ((0, core_1.getBlock)(blockType))
366
+ return;
367
+ const blockDef = {
368
+ type: blockType,
369
+ category: 'Custom',
370
+ color: '#607D8B',
371
+ tooltip: proc.description || `Custom block: ${proc.name}`,
372
+ inputs: (proc.params || []).map(param => ({
373
+ name: param.name.toUpperCase(),
374
+ type: 'field',
375
+ fieldType: param.type === 'number' ? 'number' : 'text',
376
+ default: param.default,
377
+ })),
378
+ previousStatement: true,
379
+ nextStatement: true,
380
+ execute: async (params, context) => {
381
+ context.logger.info(`Executing custom block: ${proc.name}`);
382
+ // Set procedure parameters in context.variables so ${paramName} references work
383
+ // Resolve any ${variable} placeholders in the parameter values first
384
+ (proc.params || []).forEach(p => {
385
+ const paramKey = p.name.toUpperCase();
386
+ let value = params[paramKey];
387
+ if (value !== undefined) {
388
+ // Resolve variable placeholders like ${email} from context
389
+ if (typeof value === 'string') {
390
+ value = this.resolveVariablePlaceholders(value, context);
391
+ }
392
+ context.variables.set(p.name, value);
393
+ }
394
+ });
395
+ // Execute the procedure's steps directly
396
+ const steps = this.extractStepsFromBlocklyState(proc.steps);
397
+ for (const step of steps) {
398
+ const result = await this.runStep(step, context);
399
+ if (result.status !== 'passed') {
400
+ throw new Error(`Procedure ${proc.name} failed: ${result.error?.message}`);
401
+ }
402
+ }
403
+ return { customBlock: true, name: proc.name };
404
+ },
405
+ };
406
+ (0, core_1.registerBlock)(blockDef);
407
+ });
408
+ }
350
409
  extractStepsFromBlocklyState(state) {
351
410
  if (!state || typeof state !== 'object')
352
411
  return [];
@@ -482,9 +541,20 @@ class TestExecutor {
482
541
  throw new Error(`Procedure not found: ${name}`);
483
542
  }
484
543
  context.logger.info(` Executing procedure: ${name}`);
485
- // Set up parameter variables with prefix
544
+ // Save original variable values to restore after procedure execution
545
+ const savedValues = new Map();
546
+ // Set up parameter variables (procedure steps reference them directly as ${paramName})
486
547
  for (const [key, value] of Object.entries(args)) {
487
- context.variables.set(`__param_${key}`, value);
548
+ // Resolve any ${variable} placeholders in the argument value
549
+ let resolvedValue = value;
550
+ if (typeof value === 'string' && value.includes('${')) {
551
+ resolvedValue = this.resolveVariablePlaceholders(value, context);
552
+ }
553
+ // Save original value if it exists
554
+ if (context.variables.has(key)) {
555
+ savedValues.set(key, context.variables.get(key));
556
+ }
557
+ context.variables.set(key, resolvedValue);
488
558
  }
489
559
  // Execute procedure steps
490
560
  const steps = this.extractStepsFromBlocklyState(procedure.steps);
@@ -503,12 +573,38 @@ class TestExecutor {
503
573
  }
504
574
  }
505
575
  }
506
- // Clean up parameter variables
576
+ // Restore original variable values
577
+ for (const [key, originalValue] of savedValues) {
578
+ context.variables.set(key, originalValue);
579
+ }
580
+ // Remove variables that didn't exist before
507
581
  for (const key of Object.keys(args)) {
508
- context.variables.delete(`__param_${key}`);
582
+ if (!savedValues.has(key)) {
583
+ context.variables.delete(key);
584
+ }
509
585
  }
510
586
  return returnValue;
511
587
  }
588
+ resolveVariablePlaceholders(text, context) {
589
+ return text.replace(/\$\{([\w.]+)\}/g, (match, path) => {
590
+ const parts = path.split('.');
591
+ const varName = parts[0];
592
+ let value = context.variables.get(varName);
593
+ // Handle dot notation for nested object access
594
+ if (parts.length > 1 && value !== undefined && value !== null) {
595
+ for (let i = 1; i < parts.length; i++) {
596
+ if (value === undefined || value === null)
597
+ break;
598
+ value = value[parts[i]];
599
+ }
600
+ }
601
+ if (value === undefined || value === null)
602
+ return match;
603
+ if (typeof value === 'object')
604
+ return JSON.stringify(value);
605
+ return String(value);
606
+ });
607
+ }
512
608
  async resolveParams(params, context) {
513
609
  const resolved = {};
514
610
  for (const [key, value] of Object.entries(params)) {
package/dist/cli/index.js CHANGED
@@ -41,6 +41,21 @@ 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
+ /**
45
+ * Search up the directory tree for globals.json starting from the given directory
46
+ */
47
+ function findGlobalsFile(startDir) {
48
+ let currentDir = path.resolve(startDir);
49
+ const root = path.parse(currentDir).root;
50
+ while (currentDir !== root) {
51
+ const globalsPath = path.join(currentDir, 'globals.json');
52
+ if (fs.existsSync(globalsPath)) {
53
+ return globalsPath;
54
+ }
55
+ currentDir = path.dirname(currentDir);
56
+ }
57
+ return null;
58
+ }
44
59
  const program = new commander_1.Command();
45
60
  program
46
61
  .name('testblocks')
@@ -73,9 +88,15 @@ program
73
88
  process.exit(1);
74
89
  }
75
90
  console.log(`Found ${files.length} test file(s)\n`);
76
- // Load globals.json if it exists
91
+ // Load globals.json - search up directory tree from first test file if not explicitly specified
77
92
  let globalVariables = {};
78
- const globalsPath = path.resolve(options.globals);
93
+ let globalProcedures = {};
94
+ let globalsPath = path.resolve(options.globals);
95
+ // If default globals path doesn't exist, search up from first test file
96
+ if (!fs.existsSync(globalsPath) && options.globals === './globals.json' && files.length > 0) {
97
+ const testDir = path.dirname(files[0]);
98
+ globalsPath = findGlobalsFile(testDir) || globalsPath;
99
+ }
79
100
  if (fs.existsSync(globalsPath)) {
80
101
  try {
81
102
  const globalsContent = fs.readFileSync(globalsPath, 'utf-8');
@@ -83,6 +104,9 @@ program
83
104
  if (globals.variables && typeof globals.variables === 'object') {
84
105
  globalVariables = globals.variables;
85
106
  }
107
+ if (globals.procedures && typeof globals.procedures === 'object') {
108
+ globalProcedures = globals.procedures;
109
+ }
86
110
  }
87
111
  catch (e) {
88
112
  console.warn(`Warning: Could not load globals from ${globalsPath}: ${e.message}`);
@@ -111,6 +135,7 @@ program
111
135
  timeout: parseInt(options.timeout, 10),
112
136
  baseUrl: options.baseUrl,
113
137
  variables,
138
+ procedures: globalProcedures,
114
139
  };
115
140
  // Create reporter
116
141
  const reporter = createReporter(options.reporter, options.output);
@@ -248,7 +273,7 @@ program
248
273
  'test:junit': 'testblocks run tests/**/*.testblocks.json -r junit -o reports',
249
274
  },
250
275
  devDependencies: {
251
- '@testsmith/testblocks': '^0.6.0',
276
+ '@testsmith/testblocks': '^0.7.0',
252
277
  },
253
278
  };
254
279
  fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2));
@@ -354,6 +379,56 @@ Thumbs.db
354
379
  fs.writeFileSync(gitignorePath, gitignore);
355
380
  console.log(' Created: .gitignore');
356
381
  }
382
+ // Create GitHub Actions workflow
383
+ const workflowDir = path.join(projectDir, '.github', 'workflows');
384
+ if (!fs.existsSync(workflowDir)) {
385
+ fs.mkdirSync(workflowDir, { recursive: true });
386
+ console.log(' Created: .github/workflows/');
387
+ }
388
+ const workflowPath = path.join(workflowDir, 'testblocks.yml');
389
+ if (!fs.existsSync(workflowPath)) {
390
+ const workflow = `name: TestBlocks Tests
391
+
392
+ on:
393
+ push:
394
+ branches: [main, master]
395
+ pull_request:
396
+ branches: [main, master]
397
+
398
+ jobs:
399
+ test:
400
+ runs-on: ubuntu-latest
401
+
402
+ steps:
403
+ - name: Checkout repository
404
+ uses: actions/checkout@v4
405
+
406
+ - name: Setup Node.js
407
+ uses: actions/setup-node@v4
408
+ with:
409
+ node-version: '20'
410
+ cache: 'npm'
411
+
412
+ - name: Install dependencies
413
+ run: npm ci
414
+
415
+ - name: Install Playwright browsers
416
+ run: npx playwright install --with-deps chromium
417
+
418
+ - name: Run tests
419
+ run: npm test
420
+
421
+ - name: Upload test reports
422
+ uses: actions/upload-artifact@v4
423
+ if: always()
424
+ with:
425
+ name: test-reports
426
+ path: reports/
427
+ retention-days: 30
428
+ `;
429
+ fs.writeFileSync(workflowPath, workflow);
430
+ console.log(' Created: .github/workflows/testblocks.yml');
431
+ }
357
432
  console.log('\n✓ Project initialized successfully!\n');
358
433
  console.log('Next steps:');
359
434
  console.log(' 1. cd ' + (directory === '.' ? '' : directory));
@@ -0,0 +1,12 @@
1
+ import { TestFile, TestResult } from '../../core';
2
+ import { Reporter } from './types';
3
+ /**
4
+ * Console reporter - outputs results to the terminal
5
+ */
6
+ export declare class ConsoleReporter implements Reporter {
7
+ onTestFileComplete(file: string, testFile: TestFile, results: TestResult[]): void;
8
+ onComplete(allResults: {
9
+ file: string;
10
+ results: TestResult[];
11
+ }[]): void;
12
+ }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConsoleReporter = void 0;
4
+ /**
5
+ * Console reporter - outputs results to the terminal
6
+ */
7
+ class ConsoleReporter {
8
+ onTestFileComplete(file, testFile, results) {
9
+ console.log('');
10
+ for (const result of results) {
11
+ const icon = result.status === 'passed' ? '✓' : '✗';
12
+ const color = result.status === 'passed' ? '\x1b[32m' : '\x1b[31m';
13
+ const reset = '\x1b[0m';
14
+ console.log(`${color} ${icon} ${result.testName}${reset} (${result.duration}ms)`);
15
+ if (result.error) {
16
+ console.log(` ${result.error.message}`);
17
+ }
18
+ }
19
+ console.log('');
20
+ }
21
+ onComplete(allResults) {
22
+ const totalTests = allResults.reduce((sum, r) => sum + r.results.length, 0);
23
+ const passed = allResults.reduce((sum, r) => sum + r.results.filter(t => t.status === 'passed').length, 0);
24
+ const failed = allResults.reduce((sum, r) => sum + r.results.filter(t => t.status !== 'passed').length, 0);
25
+ const totalDuration = allResults.reduce((sum, r) => sum + r.results.reduce((s, t) => s + t.duration, 0), 0);
26
+ console.log('─'.repeat(50));
27
+ console.log(`Tests: ${passed} passed, ${failed} failed, ${totalTests} total`);
28
+ console.log(`Duration: ${(totalDuration / 1000).toFixed(2)}s`);
29
+ console.log(`Test Files: ${allResults.length}`);
30
+ console.log('─'.repeat(50));
31
+ if (failed > 0) {
32
+ console.log('\n\x1b[31mTest run failed\x1b[0m\n');
33
+ }
34
+ else {
35
+ console.log('\n\x1b[32mAll tests passed!\x1b[0m\n');
36
+ }
37
+ }
38
+ }
39
+ exports.ConsoleReporter = ConsoleReporter;
@@ -0,0 +1,19 @@
1
+ import { TestFile, TestResult } from '../../core';
2
+ import { Reporter, ReportData } from './types';
3
+ /**
4
+ * HTML reporter - generates a styled HTML report
5
+ */
6
+ export declare class HTMLReporter implements Reporter {
7
+ private outputDir;
8
+ private allResults;
9
+ constructor(outputDir: string);
10
+ onTestFileComplete(file: string, testFile: TestFile, results: TestResult[]): void;
11
+ onComplete(allResults: {
12
+ file: string;
13
+ results: TestResult[];
14
+ }[]): void;
15
+ }
16
+ /**
17
+ * Generate HTML report from report data
18
+ */
19
+ export declare function generateHTMLReport(data: ReportData): string;