testblocks 0.2.0 → 0.4.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,7 +16,7 @@
16
16
  overflow: hidden;
17
17
  }
18
18
  </style>
19
- <script type="module" crossorigin src="/assets/index-RpGJFDVQ.js"></script>
19
+ <script type="module" crossorigin src="/assets/index-Cq84-VIf.js"></script>
20
20
  <link rel="stylesheet" crossorigin href="/assets/index-Dnk1ti7l.css">
21
21
  </head>
22
22
  <body>
@@ -35,14 +35,14 @@ app.use(express_1.default.json({ limit: '10mb' }));
35
35
  app.get('/api/health', (req, res) => {
36
36
  res.json({ status: 'ok', version: '1.0.0' });
37
37
  });
38
- // List available plugins
38
+ // List available plugins (with full block definitions for client registration)
39
39
  app.get('/api/plugins', (req, res) => {
40
40
  const available = (0, plugins_1.discoverPlugins)();
41
41
  const loaded = (0, plugins_1.getServerPlugins)().map(p => ({
42
42
  name: p.name,
43
43
  version: p.version,
44
44
  description: p.description,
45
- blockCount: p.blocks.length,
45
+ blocks: p.blocks, // Include full block definitions
46
46
  }));
47
47
  res.json({
48
48
  directory: (0, plugins_1.getPluginsDirectory)(),
@@ -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 (with full block definitions for client registration)
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
+ blocks: p.blocks, // Include full block definitions
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.4.0",
4
4
  "description": "Visual test automation tool with Blockly - API and Playwright testing",
5
5
  "author": "Roy de Kleijn",
6
6
  "license": "MIT",