testblocks 0.2.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.
package/dist/cli/index.js CHANGED
@@ -40,6 +40,7 @@ const path = __importStar(require("path"));
40
40
  const glob_1 = require("glob");
41
41
  const executor_1 = require("./executor");
42
42
  const reporters_1 = require("./reporters");
43
+ const startServer_1 = require("../server/startServer");
43
44
  const program = new commander_1.Command();
44
45
  program
45
46
  .name('testblocks')
@@ -229,7 +230,7 @@ program
229
230
  'test:junit': 'testblocks run tests/**/*.testblocks.json -r junit -o reports',
230
231
  },
231
232
  devDependencies: {
232
- testblocks: '^1.0.0',
233
+ testblocks: '^0.3.0',
233
234
  },
234
235
  };
235
236
  fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2));
@@ -340,7 +341,8 @@ Thumbs.db
340
341
  console.log(' 1. cd ' + (directory === '.' ? '' : directory));
341
342
  console.log(' 2. npm install');
342
343
  console.log(' 3. npm test\n');
343
- console.log('Or open the folder in TestBlocks web editor to create tests visually.\n');
344
+ console.log('To open the visual test editor:');
345
+ console.log(' testblocks serve\n');
344
346
  });
345
347
  program
346
348
  .command('list')
@@ -372,6 +374,24 @@ program
372
374
  process.exit(1);
373
375
  }
374
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
+ });
375
395
  function createReporter(type, outputDir) {
376
396
  switch (type) {
377
397
  case 'json':
@@ -0,0 +1,7 @@
1
+ export interface ServerOptions {
2
+ port?: number;
3
+ pluginsDir?: string;
4
+ globalsDir?: string;
5
+ open?: boolean;
6
+ }
7
+ export declare function startServer(options?: ServerOptions): Promise<void>;
@@ -0,0 +1,405 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.startServer = startServer;
40
+ const express_1 = __importDefault(require("express"));
41
+ const cors_1 = __importDefault(require("cors"));
42
+ const path_1 = __importDefault(require("path"));
43
+ const fs_1 = __importDefault(require("fs"));
44
+ const executor_1 = require("./executor");
45
+ const reporters_1 = require("../cli/reporters");
46
+ const plugins_1 = require("./plugins");
47
+ const globals_1 = require("./globals");
48
+ const codegenManager_1 = require("./codegenManager");
49
+ async function startServer(options = {}) {
50
+ const port = options.port || 3000;
51
+ const workingDir = process.cwd();
52
+ // Set directories
53
+ const pluginsDir = options.pluginsDir || path_1.default.join(workingDir, 'plugins');
54
+ const globalsDir = options.globalsDir || workingDir;
55
+ (0, plugins_1.setPluginsDirectory)(pluginsDir);
56
+ (0, globals_1.setGlobalsDirectory)(globalsDir);
57
+ // Load plugins and globals
58
+ try {
59
+ await (0, plugins_1.loadAllPlugins)();
60
+ (0, plugins_1.initializeServerPlugins)();
61
+ (0, globals_1.initializeGlobalsAndSnippets)();
62
+ }
63
+ catch (err) {
64
+ console.error('Failed to load plugins:', err);
65
+ }
66
+ const app = (0, express_1.default)();
67
+ app.use((0, cors_1.default)());
68
+ app.use(express_1.default.json({ limit: '10mb' }));
69
+ // Serve static client files
70
+ const clientDir = path_1.default.join(__dirname, '..', 'client');
71
+ if (fs_1.default.existsSync(clientDir)) {
72
+ app.use(express_1.default.static(clientDir));
73
+ }
74
+ // Health check
75
+ app.get('/api/health', (_req, res) => {
76
+ res.json({ status: 'ok', version: '1.0.0' });
77
+ });
78
+ // List available plugins
79
+ app.get('/api/plugins', (_req, res) => {
80
+ const available = (0, plugins_1.discoverPlugins)();
81
+ const loaded = (0, plugins_1.getServerPlugins)().map(p => ({
82
+ name: p.name,
83
+ version: p.version,
84
+ description: p.description,
85
+ blockCount: p.blocks.length,
86
+ }));
87
+ res.json({
88
+ directory: (0, plugins_1.getPluginsDirectory)(),
89
+ available,
90
+ loaded,
91
+ });
92
+ });
93
+ // Load specific plugins
94
+ app.post('/api/plugins/load', async (req, res) => {
95
+ try {
96
+ const { plugins } = req.body;
97
+ await (0, plugins_1.loadTestFilePlugins)(plugins);
98
+ res.json({ loaded: plugins });
99
+ }
100
+ catch (error) {
101
+ res.status(500).json({ error: error.message });
102
+ }
103
+ });
104
+ // Get globals and snippets
105
+ app.get('/api/globals', (_req, res) => {
106
+ const globals = (0, globals_1.getGlobals)();
107
+ const snippets = (0, globals_1.getAllSnippets)().map(s => ({
108
+ name: s.name,
109
+ description: s.description,
110
+ category: s.category,
111
+ params: s.params,
112
+ stepCount: s.steps.length,
113
+ }));
114
+ res.json({
115
+ directory: (0, globals_1.getGlobalsDirectory)(),
116
+ globals,
117
+ snippets,
118
+ testIdAttribute: (0, globals_1.getTestIdAttribute)(),
119
+ });
120
+ });
121
+ // Update test ID attribute
122
+ app.put('/api/globals/test-id-attribute', (req, res) => {
123
+ const { testIdAttribute } = req.body;
124
+ if (!testIdAttribute || typeof testIdAttribute !== 'string') {
125
+ return res.status(400).json({ error: 'testIdAttribute is required and must be a string' });
126
+ }
127
+ (0, globals_1.setTestIdAttribute)(testIdAttribute);
128
+ res.json({ testIdAttribute: (0, globals_1.getTestIdAttribute)() });
129
+ });
130
+ // Run tests
131
+ app.post('/api/run', async (req, res) => {
132
+ try {
133
+ const testFile = req.body;
134
+ if (!testFile || !testFile.tests) {
135
+ return res.status(400).json({ error: 'Invalid test file format' });
136
+ }
137
+ console.log(`Running ${testFile.tests.length} tests from "${testFile.name}"...`);
138
+ const globalVars = (0, globals_1.getGlobalVariables)();
139
+ const testIdAttr = (0, globals_1.getTestIdAttribute)();
140
+ const executor = new executor_1.TestExecutor({
141
+ headless: req.query.headless !== 'false',
142
+ timeout: Number(req.query.timeout) || 30000,
143
+ variables: globalVars,
144
+ testIdAttribute: testIdAttr,
145
+ baseDir: globalsDir,
146
+ });
147
+ const results = await executor.runTestFile(testFile);
148
+ const passed = results.filter(r => r.status === 'passed').length;
149
+ const failed = results.filter(r => r.status === 'failed').length;
150
+ console.log(`Results: ${passed} passed, ${failed} failed`);
151
+ res.json(results);
152
+ }
153
+ catch (error) {
154
+ const err = error;
155
+ console.error('Test execution failed:', err.message);
156
+ res.status(500).json({
157
+ error: 'Test execution failed',
158
+ message: err.message,
159
+ });
160
+ }
161
+ });
162
+ // Run a single test
163
+ app.post('/api/run/:testId', async (req, res) => {
164
+ try {
165
+ const testFile = req.body;
166
+ const { testId } = req.params;
167
+ const test = testFile.tests.find(t => t.id === testId);
168
+ if (!test) {
169
+ return res.status(404).json({ error: `Test not found: ${testId}` });
170
+ }
171
+ const globalVars = (0, globals_1.getGlobalVariables)();
172
+ const testIdAttr = (0, globals_1.getTestIdAttribute)();
173
+ const executor = new executor_1.TestExecutor({
174
+ headless: req.query.headless !== 'false',
175
+ timeout: Number(req.query.timeout) || 30000,
176
+ variables: globalVars,
177
+ testIdAttribute: testIdAttr,
178
+ baseDir: globalsDir,
179
+ });
180
+ if (testFile.procedures) {
181
+ executor.registerProcedures(testFile.procedures);
182
+ }
183
+ await executor.initialize();
184
+ const result = await executor.runTest(test, testFile.variables);
185
+ await executor.cleanup();
186
+ res.json(result);
187
+ }
188
+ catch (error) {
189
+ console.error('Test execution failed:', error);
190
+ res.status(500).json({
191
+ error: 'Test execution failed',
192
+ message: error.message,
193
+ });
194
+ }
195
+ });
196
+ // Validate test file
197
+ app.post('/api/validate', (req, res) => {
198
+ try {
199
+ const testFile = req.body;
200
+ const errors = [];
201
+ if (!testFile.version) {
202
+ errors.push('Missing version field');
203
+ }
204
+ if (!testFile.name) {
205
+ errors.push('Missing name field');
206
+ }
207
+ if (!testFile.tests || !Array.isArray(testFile.tests)) {
208
+ errors.push('Missing or invalid tests array');
209
+ }
210
+ else {
211
+ testFile.tests.forEach((test, index) => {
212
+ if (!test.id) {
213
+ errors.push(`Test at index ${index} is missing an id`);
214
+ }
215
+ if (!test.name) {
216
+ errors.push(`Test at index ${index} is missing a name`);
217
+ }
218
+ });
219
+ }
220
+ if (errors.length > 0) {
221
+ res.status(400).json({ valid: false, errors });
222
+ }
223
+ else {
224
+ res.json({ valid: true });
225
+ }
226
+ }
227
+ catch (error) {
228
+ res.status(400).json({
229
+ valid: false,
230
+ errors: ['Invalid JSON: ' + error.message],
231
+ });
232
+ }
233
+ });
234
+ // Recording endpoints
235
+ app.post('/api/record/start', async (req, res) => {
236
+ try {
237
+ const { url, testIdAttribute } = req.body;
238
+ if (!url) {
239
+ return res.status(400).json({ error: 'URL is required' });
240
+ }
241
+ console.log(`Starting recording session for URL: ${url}`);
242
+ const sessionId = await codegenManager_1.codegenManager.startRecording(url, {
243
+ testIdAttribute: testIdAttribute || undefined,
244
+ });
245
+ res.json({
246
+ sessionId,
247
+ status: 'running',
248
+ });
249
+ }
250
+ catch (error) {
251
+ console.error('Failed to start recording:', error);
252
+ res.status(500).json({
253
+ error: 'Failed to start recording',
254
+ message: error.message,
255
+ });
256
+ }
257
+ });
258
+ app.post('/api/record/stop', async (req, res) => {
259
+ try {
260
+ const { sessionId } = req.body;
261
+ if (!sessionId) {
262
+ return res.status(400).json({ error: 'Session ID is required' });
263
+ }
264
+ console.log(`Stopping recording session: ${sessionId}`);
265
+ const steps = await codegenManager_1.codegenManager.stopRecording(sessionId);
266
+ res.json({
267
+ status: 'completed',
268
+ steps,
269
+ });
270
+ setTimeout(() => {
271
+ codegenManager_1.codegenManager.cleanup(sessionId);
272
+ }, 5000);
273
+ }
274
+ catch (error) {
275
+ console.error('Failed to stop recording:', error);
276
+ res.status(500).json({
277
+ error: 'Failed to stop recording',
278
+ message: error.message,
279
+ });
280
+ }
281
+ });
282
+ app.get('/api/record/status/:sessionId', (req, res) => {
283
+ const { sessionId } = req.params;
284
+ const session = codegenManager_1.codegenManager.getStatus(sessionId);
285
+ if (!session) {
286
+ return res.status(404).json({ error: 'Session not found' });
287
+ }
288
+ res.json({
289
+ sessionId: session.id,
290
+ status: session.status,
291
+ url: session.url,
292
+ error: session.error,
293
+ });
294
+ });
295
+ // Report generation
296
+ app.post('/api/reports/html', (req, res) => {
297
+ try {
298
+ const { testFile, results } = req.body;
299
+ if (!testFile || !results) {
300
+ return res.status(400).json({ error: 'testFile and results are required' });
301
+ }
302
+ const timestamp = new Date().toISOString();
303
+ const reportData = {
304
+ timestamp,
305
+ summary: {
306
+ totalTests: results.length,
307
+ passed: results.filter(r => r.status === 'passed').length,
308
+ failed: results.filter(r => r.status !== 'passed').length,
309
+ duration: results.reduce((sum, r) => sum + r.duration, 0),
310
+ },
311
+ testFiles: [{
312
+ file: testFile.name || 'TestBlocks Test',
313
+ testFile,
314
+ results,
315
+ }],
316
+ };
317
+ const html = (0, reporters_1.generateHTMLReport)(reportData);
318
+ const filename = `report-${(0, reporters_1.getTimestamp)()}.html`;
319
+ res.setHeader('Content-Type', 'text/html');
320
+ res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
321
+ res.send(html);
322
+ }
323
+ catch (error) {
324
+ console.error('Failed to generate HTML report:', error);
325
+ res.status(500).json({
326
+ error: 'Failed to generate report',
327
+ message: error.message,
328
+ });
329
+ }
330
+ });
331
+ app.post('/api/reports/junit', (req, res) => {
332
+ try {
333
+ const { testFile, results } = req.body;
334
+ if (!testFile || !results) {
335
+ return res.status(400).json({ error: 'testFile and results are required' });
336
+ }
337
+ const timestamp = new Date().toISOString();
338
+ const reportData = {
339
+ timestamp,
340
+ summary: {
341
+ totalTests: results.length,
342
+ passed: results.filter(r => r.status === 'passed').length,
343
+ failed: results.filter(r => r.status !== 'passed').length,
344
+ duration: results.reduce((sum, r) => sum + r.duration, 0),
345
+ },
346
+ testFiles: [{
347
+ file: testFile.name || 'TestBlocks Test',
348
+ testFile,
349
+ results,
350
+ }],
351
+ };
352
+ const xml = (0, reporters_1.generateJUnitXML)(reportData);
353
+ const filename = `junit-${(0, reporters_1.getTimestamp)()}.xml`;
354
+ res.setHeader('Content-Type', 'application/xml');
355
+ res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
356
+ res.send(xml);
357
+ }
358
+ catch (error) {
359
+ console.error('Failed to generate JUnit report:', error);
360
+ res.status(500).json({
361
+ error: 'Failed to generate report',
362
+ message: error.message,
363
+ });
364
+ }
365
+ });
366
+ // Serve index.html for all non-API routes (SPA support)
367
+ app.get('*', (_req, res) => {
368
+ const indexPath = path_1.default.join(clientDir, 'index.html');
369
+ if (fs_1.default.existsSync(indexPath)) {
370
+ res.sendFile(indexPath);
371
+ }
372
+ else {
373
+ res.status(404).json({ error: 'Web UI not found. Make sure testblocks is properly installed.' });
374
+ }
375
+ });
376
+ // Cleanup handlers
377
+ process.on('SIGTERM', () => {
378
+ console.log('Received SIGTERM, cleaning up...');
379
+ codegenManager_1.codegenManager.cleanupAll();
380
+ process.exit(0);
381
+ });
382
+ process.on('SIGINT', () => {
383
+ console.log('Received SIGINT, cleaning up...');
384
+ codegenManager_1.codegenManager.cleanupAll();
385
+ process.exit(0);
386
+ });
387
+ // Start server
388
+ app.listen(port, () => {
389
+ console.log(`\nTestBlocks Web UI running at http://localhost:${port}\n`);
390
+ console.log('Directories:');
391
+ console.log(` Working directory: ${workingDir}`);
392
+ console.log(` Plugins: ${pluginsDir}`);
393
+ console.log(` Globals: ${globalsDir}`);
394
+ console.log('\nPress Ctrl+C to stop\n');
395
+ // Open browser if requested
396
+ if (options.open) {
397
+ const url = `http://localhost:${port}`;
398
+ const command = process.platform === 'darwin' ? 'open' :
399
+ process.platform === 'win32' ? 'start' : 'xdg-open';
400
+ Promise.resolve().then(() => __importStar(require('child_process'))).then(cp => {
401
+ cp.exec(`${command} ${url}`);
402
+ });
403
+ }
404
+ });
405
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testblocks",
3
- "version": "0.2.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",