@testsmith/testblocks 0.1.0 → 0.3.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.
@@ -16,8 +16,8 @@
16
16
  overflow: hidden;
17
17
  }
18
18
  </style>
19
- <script type="module" crossorigin src="/assets/index-Cq84-VIf.js"></script>
20
- <link rel="stylesheet" crossorigin href="/assets/index-Dnk1ti7l.css">
19
+ <script type="module" crossorigin src="/assets/index-DOJk7uTQ.js"></script>
20
+ <link rel="stylesheet" crossorigin href="/assets/index-CIvp_myM.css">
21
21
  </head>
22
22
  <body>
23
23
  <div id="root"></div>
@@ -1,3 +1,10 @@
1
+ export interface FolderHooks {
2
+ version: string;
3
+ beforeAll?: TestStep[];
4
+ afterAll?: TestStep[];
5
+ beforeEach?: TestStep[];
6
+ afterEach?: TestStep[];
7
+ }
1
8
  export interface TestFile {
2
9
  version: string;
3
10
  name: string;
@@ -17,6 +17,7 @@ export declare class TestExecutor {
17
17
  constructor(options?: ExecutorOptions);
18
18
  initialize(): Promise<void>;
19
19
  cleanup(): Promise<void>;
20
+ private requiresBrowser;
20
21
  runTestFile(testFile: TestFile): Promise<TestResult[]>;
21
22
  private runLifecycleSteps;
22
23
  /**
@@ -130,13 +130,44 @@ class TestExecutor {
130
130
  this.context = null;
131
131
  this.browser = null;
132
132
  }
133
+ requiresBrowser(testFile) {
134
+ const hasWebStep = (steps) => {
135
+ return steps.some(step => step.type.startsWith('web_'));
136
+ };
137
+ const hasWebStepInState = (state) => {
138
+ const steps = this.extractStepsFromBlocklyState(state);
139
+ return hasWebStep(steps);
140
+ };
141
+ // Check beforeAll/afterAll hooks
142
+ if (testFile.beforeAll && hasWebStepInState(testFile.beforeAll))
143
+ return true;
144
+ if (testFile.afterAll && hasWebStepInState(testFile.afterAll))
145
+ return true;
146
+ if (testFile.beforeEach && hasWebStepInState(testFile.beforeEach))
147
+ return true;
148
+ if (testFile.afterEach && hasWebStepInState(testFile.afterEach))
149
+ return true;
150
+ // Check all tests
151
+ for (const test of testFile.tests) {
152
+ if (hasWebStepInState(test.steps))
153
+ return true;
154
+ if (test.beforeEach && hasWebStepInState(test.beforeEach))
155
+ return true;
156
+ if (test.afterEach && hasWebStepInState(test.afterEach))
157
+ return true;
158
+ }
159
+ return false;
160
+ }
133
161
  async runTestFile(testFile) {
134
162
  const results = [];
135
163
  // Register custom blocks from procedures
136
164
  if (testFile.procedures) {
137
165
  this.registerCustomBlocksFromProcedures(testFile.procedures);
138
166
  }
139
- await this.initialize();
167
+ // Only initialize browser if test file contains web steps
168
+ if (this.requiresBrowser(testFile)) {
169
+ await this.initialize();
170
+ }
140
171
  // Create shared execution context for lifecycle hooks
141
172
  const sharedContext = {
142
173
  variables: new Map(Object.entries({
@@ -7,6 +7,11 @@ exports.TestExecutor = void 0;
7
7
  const express_1 = __importDefault(require("express"));
8
8
  const cors_1 = __importDefault(require("cors"));
9
9
  const path_1 = __importDefault(require("path"));
10
+ const fs_1 = __importDefault(require("fs"));
11
+ // Read version from package.json
12
+ const packageJsonPath = path_1.default.join(__dirname, '../../package.json');
13
+ const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf-8'));
14
+ const VERSION = packageJson.version || '0.0.0';
10
15
  const executor_1 = require("./executor");
11
16
  Object.defineProperty(exports, "TestExecutor", { enumerable: true, get: function () { return executor_1.TestExecutor; } });
12
17
  const reporters_1 = require("../cli/reporters");
@@ -27,13 +32,70 @@ const globalsDir = process.env.GLOBALS_DIR || path_1.default.join(process.cwd(),
27
32
  }).catch(err => {
28
33
  console.error('Failed to load plugins:', err);
29
34
  });
35
+ /**
36
+ * Merge folder hooks into a test file.
37
+ * Folder hooks are ordered from outermost to innermost folder.
38
+ * - beforeAll: run parent hooks first, then child hooks, then test file hooks
39
+ * - afterAll: run test file hooks first, then child hooks, then parent hooks
40
+ * - beforeEach/afterEach: same pattern
41
+ */
42
+ function mergeFolderHooksIntoTestFile(testFile, folderHooks) {
43
+ if (!folderHooks || folderHooks.length === 0) {
44
+ return testFile;
45
+ }
46
+ // Collect all steps from folder hooks (parent to child order is already provided)
47
+ const beforeAllSteps = [];
48
+ const afterAllSteps = [];
49
+ const beforeEachSteps = [];
50
+ const afterEachSteps = [];
51
+ // Parent to child order for beforeAll/beforeEach
52
+ for (const hooks of folderHooks) {
53
+ if (hooks.beforeAll)
54
+ beforeAllSteps.push(...hooks.beforeAll);
55
+ if (hooks.beforeEach)
56
+ beforeEachSteps.push(...hooks.beforeEach);
57
+ }
58
+ // Child to parent order for afterAll/afterEach
59
+ for (let i = folderHooks.length - 1; i >= 0; i--) {
60
+ const hooks = folderHooks[i];
61
+ if (hooks.afterAll)
62
+ afterAllSteps.unshift(...hooks.afterAll);
63
+ if (hooks.afterEach)
64
+ afterEachSteps.unshift(...hooks.afterEach);
65
+ }
66
+ // Merge with test file hooks
67
+ const merged = {
68
+ ...testFile,
69
+ beforeAll: [
70
+ ...beforeAllSteps,
71
+ ...(testFile.beforeAll || []),
72
+ ],
73
+ afterAll: [
74
+ ...(testFile.afterAll || []),
75
+ ...afterAllSteps,
76
+ ],
77
+ beforeEach: [
78
+ ...beforeEachSteps,
79
+ ...(testFile.beforeEach || []),
80
+ ],
81
+ afterEach: [
82
+ ...(testFile.afterEach || []),
83
+ ...afterEachSteps,
84
+ ],
85
+ };
86
+ return merged;
87
+ }
30
88
  const app = (0, express_1.default)();
31
89
  const PORT = process.env.PORT || 3001;
32
90
  app.use((0, cors_1.default)());
33
91
  app.use(express_1.default.json({ limit: '10mb' }));
34
92
  // Health check
35
93
  app.get('/api/health', (req, res) => {
36
- res.json({ status: 'ok', version: '1.0.0' });
94
+ res.json({ status: 'ok', version: VERSION });
95
+ });
96
+ // Version endpoint
97
+ app.get('/api/version', (req, res) => {
98
+ res.json({ version: VERSION });
37
99
  });
38
100
  // List available plugins (with full block definitions for client registration)
39
101
  app.get('/api/plugins', (req, res) => {
@@ -90,11 +152,13 @@ app.put('/api/globals/test-id-attribute', (req, res) => {
90
152
  // Run tests
91
153
  app.post('/api/run', async (req, res) => {
92
154
  try {
93
- const testFile = req.body;
155
+ const { testFile, folderHooks } = req.body;
94
156
  if (!testFile || !testFile.tests) {
95
157
  return res.status(400).json({ error: 'Invalid test file format' });
96
158
  }
97
159
  console.log(`Running ${testFile.tests.length} tests from "${testFile.name}"...`);
160
+ // Merge folder hooks into test file (parent to child order for beforeAll, child to parent for afterAll)
161
+ const mergedTestFile = mergeFolderHooksIntoTestFile(testFile, folderHooks || []);
98
162
  // Merge global variables with test file variables
99
163
  const globalVars = (0, globals_1.getGlobalVariables)();
100
164
  const testIdAttr = (0, globals_1.getTestIdAttribute)();
@@ -105,7 +169,7 @@ app.post('/api/run', async (req, res) => {
105
169
  testIdAttribute: testIdAttr, // Pass test ID attribute
106
170
  baseDir: globalsDir, // Base directory for resolving relative file paths
107
171
  });
108
- const results = await executor.runTestFile(testFile);
172
+ const results = await executor.runTestFile(mergedTestFile);
109
173
  const passed = results.filter(r => r.status === 'passed').length;
110
174
  const failed = results.filter(r => r.status === 'failed').length;
111
175
  console.log(`Results: ${passed} passed, ${failed} failed`);
@@ -124,12 +188,15 @@ app.post('/api/run', async (req, res) => {
124
188
  // Run a single test
125
189
  app.post('/api/run/:testId', async (req, res) => {
126
190
  try {
127
- const testFile = req.body;
191
+ const { testFile, folderHooks } = req.body;
128
192
  const { testId } = req.params;
129
193
  const test = testFile.tests.find(t => t.id === testId);
130
194
  if (!test) {
131
195
  return res.status(404).json({ error: `Test not found: ${testId}` });
132
196
  }
197
+ // For single test runs, merge folder beforeEach/afterEach hooks
198
+ // (beforeAll/afterAll are handled at suite level)
199
+ const mergedTestFile = mergeFolderHooksIntoTestFile(testFile, folderHooks || []);
133
200
  // Merge global variables
134
201
  const globalVars = (0, globals_1.getGlobalVariables)();
135
202
  const testIdAttr = (0, globals_1.getTestIdAttribute)();
@@ -141,11 +208,11 @@ app.post('/api/run/:testId', async (req, res) => {
141
208
  baseDir: globalsDir, // Base directory for resolving relative file paths
142
209
  });
143
210
  // Register custom blocks from procedures before running the test
144
- if (testFile.procedures) {
145
- executor.registerProcedures(testFile.procedures);
211
+ if (mergedTestFile.procedures) {
212
+ executor.registerProcedures(mergedTestFile.procedures);
146
213
  }
147
214
  await executor.initialize();
148
- const result = await executor.runTest(test, testFile.variables);
215
+ const result = await executor.runTest(test, mergedTestFile.variables);
149
216
  await executor.cleanup();
150
217
  res.json(result);
151
218
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testsmith/testblocks",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Visual test automation tool with Blockly - API and Playwright testing",
5
5
  "author": "Roy de Kleijn",
6
6
  "license": "MIT",