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.
- package/dist/cli/index.js +22 -2
- package/dist/client/assets/index-RpGJFDVQ.js +2137 -0
- package/dist/client/assets/index-RpGJFDVQ.js.map +1 -0
- package/dist/client/index.html +1 -1
- package/dist/server/startServer.d.ts +7 -0
- package/dist/server/startServer.js +405 -0
- package/package.json +2 -2
- package/dist/client/assets/index-4hbFPUhP.js +0 -2087
- package/dist/client/assets/index-4hbFPUhP.js.map +0 -1
package/dist/client/index.html
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
overflow: hidden;
|
|
17
17
|
}
|
|
18
18
|
</style>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-RpGJFDVQ.js"></script>
|
|
20
20
|
<link rel="stylesheet" crossorigin href="/assets/index-Dnk1ti7l.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
@@ -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.
|
|
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",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"@blockly/field-multilineinput": "^6.0.5",
|
|
66
66
|
"@faker-js/faker": "^10.1.0",
|
|
67
67
|
"@playwright/test": "^1.57.0",
|
|
68
|
-
"blockly": "^
|
|
68
|
+
"blockly": "^12.0.0",
|
|
69
69
|
"commander": "^12.1.0",
|
|
70
70
|
"cors": "^2.8.5",
|
|
71
71
|
"express": "^4.21.2",
|