testblocks 0.1.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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +333 -0
  3. package/dist/cli/executor.d.ts +32 -0
  4. package/dist/cli/executor.js +517 -0
  5. package/dist/cli/index.d.ts +2 -0
  6. package/dist/cli/index.js +411 -0
  7. package/dist/cli/reporters.d.ts +62 -0
  8. package/dist/cli/reporters.js +451 -0
  9. package/dist/client/assets/index-4hbFPUhP.js +2087 -0
  10. package/dist/client/assets/index-4hbFPUhP.js.map +1 -0
  11. package/dist/client/assets/index-Dnk1ti7l.css +1 -0
  12. package/dist/client/index.html +25 -0
  13. package/dist/core/blocks/api.d.ts +2 -0
  14. package/dist/core/blocks/api.js +610 -0
  15. package/dist/core/blocks/data-driven.d.ts +2 -0
  16. package/dist/core/blocks/data-driven.js +245 -0
  17. package/dist/core/blocks/index.d.ts +15 -0
  18. package/dist/core/blocks/index.js +71 -0
  19. package/dist/core/blocks/lifecycle.d.ts +2 -0
  20. package/dist/core/blocks/lifecycle.js +199 -0
  21. package/dist/core/blocks/logic.d.ts +2 -0
  22. package/dist/core/blocks/logic.js +357 -0
  23. package/dist/core/blocks/playwright.d.ts +2 -0
  24. package/dist/core/blocks/playwright.js +764 -0
  25. package/dist/core/blocks/procedures.d.ts +5 -0
  26. package/dist/core/blocks/procedures.js +321 -0
  27. package/dist/core/index.d.ts +5 -0
  28. package/dist/core/index.js +44 -0
  29. package/dist/core/plugins.d.ts +66 -0
  30. package/dist/core/plugins.js +118 -0
  31. package/dist/core/types.d.ts +153 -0
  32. package/dist/core/types.js +2 -0
  33. package/dist/server/codegenManager.d.ts +54 -0
  34. package/dist/server/codegenManager.js +259 -0
  35. package/dist/server/codegenParser.d.ts +17 -0
  36. package/dist/server/codegenParser.js +598 -0
  37. package/dist/server/executor.d.ts +37 -0
  38. package/dist/server/executor.js +672 -0
  39. package/dist/server/globals.d.ts +85 -0
  40. package/dist/server/globals.js +273 -0
  41. package/dist/server/index.d.ts +2 -0
  42. package/dist/server/index.js +361 -0
  43. package/dist/server/plugins.d.ts +55 -0
  44. package/dist/server/plugins.js +206 -0
  45. package/package.json +103 -0
@@ -0,0 +1,451 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.HTMLReporter = exports.JUnitReporter = exports.JSONReporter = exports.ConsoleReporter = void 0;
37
+ exports.getTimestamp = getTimestamp;
38
+ exports.generateHTMLReport = generateHTMLReport;
39
+ exports.generateJUnitXML = generateJUnitXML;
40
+ const fs = __importStar(require("fs"));
41
+ const path = __importStar(require("path"));
42
+ // Generate timestamp string for filenames (e.g., 2024-01-15T14-30-45)
43
+ function getTimestamp() {
44
+ return new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
45
+ }
46
+ class ConsoleReporter {
47
+ onTestFileComplete(file, testFile, results) {
48
+ console.log('');
49
+ for (const result of results) {
50
+ const icon = result.status === 'passed' ? '✓' : '✗';
51
+ const color = result.status === 'passed' ? '\x1b[32m' : '\x1b[31m';
52
+ const reset = '\x1b[0m';
53
+ console.log(`${color} ${icon} ${result.testName}${reset} (${result.duration}ms)`);
54
+ if (result.error) {
55
+ console.log(` ${result.error.message}`);
56
+ }
57
+ }
58
+ console.log('');
59
+ }
60
+ onComplete(allResults) {
61
+ const totalTests = allResults.reduce((sum, r) => sum + r.results.length, 0);
62
+ const passed = allResults.reduce((sum, r) => sum + r.results.filter(t => t.status === 'passed').length, 0);
63
+ const failed = allResults.reduce((sum, r) => sum + r.results.filter(t => t.status !== 'passed').length, 0);
64
+ const totalDuration = allResults.reduce((sum, r) => sum + r.results.reduce((s, t) => s + t.duration, 0), 0);
65
+ console.log('─'.repeat(50));
66
+ console.log(`Tests: ${passed} passed, ${failed} failed, ${totalTests} total`);
67
+ console.log(`Duration: ${(totalDuration / 1000).toFixed(2)}s`);
68
+ console.log(`Test Files: ${allResults.length}`);
69
+ console.log('─'.repeat(50));
70
+ if (failed > 0) {
71
+ console.log('\n\x1b[31mTest run failed\x1b[0m\n');
72
+ }
73
+ else {
74
+ console.log('\n\x1b[32mAll tests passed!\x1b[0m\n');
75
+ }
76
+ }
77
+ }
78
+ exports.ConsoleReporter = ConsoleReporter;
79
+ class JSONReporter {
80
+ constructor(outputDir) {
81
+ this.allResults = [];
82
+ this.outputDir = outputDir;
83
+ }
84
+ onTestFileComplete(file, testFile, results) {
85
+ this.allResults.push({ file, testFile, results });
86
+ const passed = results.filter(r => r.status === 'passed').length;
87
+ const failed = results.filter(r => r.status !== 'passed').length;
88
+ console.log(` ${passed} passed, ${failed} failed\n`);
89
+ }
90
+ onComplete(allResults) {
91
+ // Ensure output directory exists
92
+ if (!fs.existsSync(this.outputDir)) {
93
+ fs.mkdirSync(this.outputDir, { recursive: true });
94
+ }
95
+ const timestamp = getTimestamp();
96
+ const outputPath = path.join(this.outputDir, `results-${timestamp}.json`);
97
+ const report = {
98
+ timestamp: new Date().toISOString(),
99
+ summary: {
100
+ totalFiles: allResults.length,
101
+ totalTests: allResults.reduce((sum, r) => sum + r.results.length, 0),
102
+ passed: allResults.reduce((sum, r) => sum + r.results.filter(t => t.status === 'passed').length, 0),
103
+ failed: allResults.reduce((sum, r) => sum + r.results.filter(t => t.status !== 'passed').length, 0),
104
+ duration: allResults.reduce((sum, r) => sum + r.results.reduce((s, t) => s + t.duration, 0), 0),
105
+ },
106
+ testFiles: this.allResults.map(({ file, testFile, results }) => ({
107
+ file,
108
+ name: testFile.name,
109
+ results,
110
+ })),
111
+ };
112
+ fs.writeFileSync(outputPath, JSON.stringify(report, null, 2));
113
+ console.log(`JSON report saved to: ${outputPath}`);
114
+ }
115
+ }
116
+ exports.JSONReporter = JSONReporter;
117
+ class JUnitReporter {
118
+ constructor(outputDir) {
119
+ this.allResults = [];
120
+ this.outputDir = outputDir;
121
+ }
122
+ onTestFileComplete(file, testFile, results) {
123
+ this.allResults.push({ file, testFile, results });
124
+ const passed = results.filter(r => r.status === 'passed').length;
125
+ const failed = results.filter(r => r.status !== 'passed').length;
126
+ console.log(` ${passed} passed, ${failed} failed\n`);
127
+ }
128
+ onComplete(allResults) {
129
+ if (!fs.existsSync(this.outputDir)) {
130
+ fs.mkdirSync(this.outputDir, { recursive: true });
131
+ }
132
+ const timestamp = getTimestamp();
133
+ const outputPath = path.join(this.outputDir, `junit-${timestamp}.xml`);
134
+ const totalTests = allResults.reduce((sum, r) => sum + r.results.length, 0);
135
+ const failures = allResults.reduce((sum, r) => sum + r.results.filter(t => t.status !== 'passed').length, 0);
136
+ const totalTime = allResults.reduce((sum, r) => sum + r.results.reduce((s, t) => s + t.duration, 0), 0) / 1000;
137
+ let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
138
+ xml += `<testsuites tests="${totalTests}" failures="${failures}" time="${totalTime.toFixed(3)}">\n`;
139
+ for (const { file, testFile, results } of this.allResults) {
140
+ const suiteTests = results.length;
141
+ const suiteFailures = results.filter(r => r.status !== 'passed').length;
142
+ const suiteTime = results.reduce((s, t) => s + t.duration, 0) / 1000;
143
+ xml += ` <testsuite name="${escapeXml(testFile.name)}" tests="${suiteTests}" failures="${suiteFailures}" time="${suiteTime.toFixed(3)}" file="${escapeXml(file)}">\n`;
144
+ for (const result of results) {
145
+ const testTime = result.duration / 1000;
146
+ xml += ` <testcase name="${escapeXml(result.testName)}" classname="${escapeXml(testFile.name)}" time="${testTime.toFixed(3)}">\n`;
147
+ if (result.status !== 'passed' && result.error) {
148
+ xml += ` <failure message="${escapeXml(result.error.message)}">\n`;
149
+ xml += `${escapeXml(result.error.stack || result.error.message)}\n`;
150
+ xml += ` </failure>\n`;
151
+ }
152
+ xml += ` </testcase>\n`;
153
+ }
154
+ xml += ` </testsuite>\n`;
155
+ }
156
+ xml += '</testsuites>\n';
157
+ fs.writeFileSync(outputPath, xml);
158
+ console.log(`JUnit report saved to: ${outputPath}`);
159
+ }
160
+ }
161
+ exports.JUnitReporter = JUnitReporter;
162
+ function escapeXml(str) {
163
+ return str
164
+ .replace(/&/g, '&amp;')
165
+ .replace(/</g, '&lt;')
166
+ .replace(/>/g, '&gt;')
167
+ .replace(/"/g, '&quot;')
168
+ .replace(/'/g, '&apos;');
169
+ }
170
+ function escapeHtml(str) {
171
+ return str
172
+ .replace(/&/g, '&amp;')
173
+ .replace(/</g, '&lt;')
174
+ .replace(/>/g, '&gt;')
175
+ .replace(/"/g, '&quot;');
176
+ }
177
+ class HTMLReporter {
178
+ constructor(outputDir) {
179
+ this.allResults = [];
180
+ this.outputDir = outputDir;
181
+ }
182
+ onTestFileComplete(file, testFile, results) {
183
+ this.allResults.push({ file, testFile, results });
184
+ const passed = results.filter(r => r.status === 'passed').length;
185
+ const failed = results.filter(r => r.status !== 'passed').length;
186
+ console.log(` ${passed} passed, ${failed} failed\n`);
187
+ }
188
+ onComplete(allResults) {
189
+ if (!fs.existsSync(this.outputDir)) {
190
+ fs.mkdirSync(this.outputDir, { recursive: true });
191
+ }
192
+ const timestamp = getTimestamp();
193
+ const outputPath = path.join(this.outputDir, `report-${timestamp}.html`);
194
+ const totalTests = allResults.reduce((sum, r) => sum + r.results.length, 0);
195
+ const passed = allResults.reduce((sum, r) => sum + r.results.filter(t => t.status === 'passed').length, 0);
196
+ const failed = allResults.reduce((sum, r) => sum + r.results.filter(t => t.status !== 'passed').length, 0);
197
+ const totalDuration = allResults.reduce((sum, r) => sum + r.results.reduce((s, t) => s + t.duration, 0), 0);
198
+ const html = generateHTMLReport({
199
+ timestamp: new Date().toISOString(),
200
+ summary: { totalTests, passed, failed, duration: totalDuration },
201
+ testFiles: this.allResults,
202
+ });
203
+ fs.writeFileSync(outputPath, html);
204
+ console.log(`HTML report saved to: ${outputPath}`);
205
+ }
206
+ }
207
+ exports.HTMLReporter = HTMLReporter;
208
+ function generateHTMLReport(data) {
209
+ const { timestamp, summary, testFiles } = data;
210
+ const passRate = summary.totalTests > 0
211
+ ? ((summary.passed / summary.totalTests) * 100).toFixed(1)
212
+ : '0';
213
+ let html = `<!DOCTYPE html>
214
+ <html lang="en">
215
+ <head>
216
+ <meta charset="UTF-8">
217
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
218
+ <title>TestBlocks Report - ${timestamp}</title>
219
+ <style>
220
+ :root {
221
+ --color-passed: #22c55e;
222
+ --color-failed: #ef4444;
223
+ --color-skipped: #f59e0b;
224
+ --color-bg: #f8fafc;
225
+ --color-surface: #ffffff;
226
+ --color-border: #e2e8f0;
227
+ --color-text: #334155;
228
+ --color-text-secondary: #64748b;
229
+ }
230
+ * { box-sizing: border-box; margin: 0; padding: 0; }
231
+ body {
232
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
233
+ background: var(--color-bg);
234
+ color: var(--color-text);
235
+ line-height: 1.5;
236
+ padding: 24px;
237
+ }
238
+ .container { max-width: 1200px; margin: 0 auto; }
239
+ h1 { font-size: 24px; font-weight: 600; margin-bottom: 8px; }
240
+ .timestamp { color: var(--color-text-secondary); font-size: 14px; margin-bottom: 24px; }
241
+ .summary {
242
+ display: grid;
243
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
244
+ gap: 16px;
245
+ margin-bottom: 32px;
246
+ }
247
+ .summary-card {
248
+ background: var(--color-surface);
249
+ border: 1px solid var(--color-border);
250
+ border-radius: 8px;
251
+ padding: 16px;
252
+ text-align: center;
253
+ }
254
+ .summary-card .value { font-size: 32px; font-weight: 700; }
255
+ .summary-card .label { font-size: 14px; color: var(--color-text-secondary); }
256
+ .summary-card.passed .value { color: var(--color-passed); }
257
+ .summary-card.failed .value { color: var(--color-failed); }
258
+ .test-file {
259
+ background: var(--color-surface);
260
+ border: 1px solid var(--color-border);
261
+ border-radius: 8px;
262
+ margin-bottom: 16px;
263
+ overflow: hidden;
264
+ }
265
+ .test-file-header {
266
+ padding: 16px;
267
+ border-bottom: 1px solid var(--color-border);
268
+ font-weight: 600;
269
+ display: flex;
270
+ justify-content: space-between;
271
+ align-items: center;
272
+ }
273
+ .test-file-path { font-size: 12px; color: var(--color-text-secondary); font-weight: normal; }
274
+ .test-case {
275
+ padding: 12px 16px;
276
+ border-bottom: 1px solid var(--color-border);
277
+ display: flex;
278
+ align-items: center;
279
+ gap: 12px;
280
+ }
281
+ .test-case:last-child { border-bottom: none; }
282
+ .test-case.passed { border-left: 3px solid var(--color-passed); }
283
+ .test-case.failed { border-left: 3px solid var(--color-failed); }
284
+ .test-case.skipped { border-left: 3px solid var(--color-skipped); }
285
+ .status-icon {
286
+ width: 20px;
287
+ height: 20px;
288
+ border-radius: 50%;
289
+ display: flex;
290
+ align-items: center;
291
+ justify-content: center;
292
+ font-size: 12px;
293
+ color: white;
294
+ flex-shrink: 0;
295
+ }
296
+ .status-icon.passed { background: var(--color-passed); }
297
+ .status-icon.failed { background: var(--color-failed); }
298
+ .status-icon.skipped { background: var(--color-skipped); }
299
+ .test-name { flex: 1; }
300
+ .test-duration { color: var(--color-text-secondary); font-size: 14px; }
301
+ .error-details {
302
+ background: #fef2f2;
303
+ border: 1px solid #fecaca;
304
+ border-radius: 4px;
305
+ padding: 12px;
306
+ margin: 8px 16px 12px 44px;
307
+ font-family: monospace;
308
+ font-size: 13px;
309
+ white-space: pre-wrap;
310
+ word-break: break-word;
311
+ color: #991b1b;
312
+ }
313
+ .steps-toggle {
314
+ background: none;
315
+ border: none;
316
+ color: var(--color-text-secondary);
317
+ cursor: pointer;
318
+ font-size: 12px;
319
+ padding: 4px 8px;
320
+ }
321
+ .steps-toggle:hover { text-decoration: underline; }
322
+ .steps-list {
323
+ display: none;
324
+ padding: 8px 16px 12px 44px;
325
+ }
326
+ .steps-list.open { display: block; }
327
+ .step {
328
+ display: flex;
329
+ align-items: center;
330
+ gap: 8px;
331
+ padding: 4px 0;
332
+ font-size: 13px;
333
+ }
334
+ .step-icon { font-size: 10px; }
335
+ .step-icon.passed { color: var(--color-passed); }
336
+ .step-icon.failed { color: var(--color-failed); }
337
+ .step-type { color: var(--color-text-secondary); }
338
+ .step-duration { color: var(--color-text-secondary); font-size: 12px; }
339
+ .screenshot {
340
+ max-width: 100%;
341
+ max-height: 300px;
342
+ border: 1px solid var(--color-border);
343
+ border-radius: 4px;
344
+ margin-top: 8px;
345
+ }
346
+ </style>
347
+ </head>
348
+ <body>
349
+ <div class="container">
350
+ <h1>TestBlocks Test Report</h1>
351
+ <div class="timestamp">Generated: ${new Date(timestamp).toLocaleString()}</div>
352
+
353
+ <div class="summary">
354
+ <div class="summary-card">
355
+ <div class="value">${summary.totalTests}</div>
356
+ <div class="label">Total Tests</div>
357
+ </div>
358
+ <div class="summary-card passed">
359
+ <div class="value">${summary.passed}</div>
360
+ <div class="label">Passed</div>
361
+ </div>
362
+ <div class="summary-card failed">
363
+ <div class="value">${summary.failed}</div>
364
+ <div class="label">Failed</div>
365
+ </div>
366
+ <div class="summary-card">
367
+ <div class="value">${passRate}%</div>
368
+ <div class="label">Pass Rate</div>
369
+ </div>
370
+ <div class="summary-card">
371
+ <div class="value">${(summary.duration / 1000).toFixed(1)}s</div>
372
+ <div class="label">Duration</div>
373
+ </div>
374
+ </div>
375
+ `;
376
+ for (const { file, testFile, results } of testFiles) {
377
+ const filePassed = results.filter(r => r.status === 'passed').length;
378
+ const fileFailed = results.filter(r => r.status !== 'passed').length;
379
+ html += `
380
+ <div class="test-file">
381
+ <div class="test-file-header">
382
+ <span>${escapeHtml(testFile.name)}</span>
383
+ <span class="test-file-path">${escapeHtml(file)} • ${filePassed} passed, ${fileFailed} failed</span>
384
+ </div>
385
+ `;
386
+ for (const result of results) {
387
+ const statusIcon = result.status === 'passed' ? '✓' : result.status === 'failed' ? '✗' : '○';
388
+ html += `
389
+ <div class="test-case ${result.status}">
390
+ <div class="status-icon ${result.status}">${statusIcon}</div>
391
+ <div class="test-name">${escapeHtml(result.testName)}</div>
392
+ <div class="test-duration">${result.duration}ms</div>
393
+ <button class="steps-toggle" onclick="this.nextElementSibling.classList.toggle('open')">
394
+ ${result.steps.length} steps
395
+ </button>
396
+ </div>
397
+ <div class="steps-list">
398
+ `;
399
+ for (const step of result.steps) {
400
+ const stepIcon = step.status === 'passed' ? '✓' : '✗';
401
+ html += `
402
+ <div class="step">
403
+ <span class="step-icon ${step.status}">${stepIcon}</span>
404
+ <span class="step-type">${escapeHtml(step.stepType)}</span>
405
+ <span class="step-duration">${step.duration}ms</span>
406
+ </div>
407
+ `;
408
+ if (step.screenshot) {
409
+ html += ` <img class="screenshot" src="${step.screenshot}" alt="Screenshot at failure">\n`;
410
+ }
411
+ }
412
+ html += ` </div>\n`;
413
+ if (result.error) {
414
+ html += ` <div class="error-details">${escapeHtml(result.error.message)}${result.error.stack ? '\n\n' + escapeHtml(result.error.stack) : ''}</div>\n`;
415
+ }
416
+ }
417
+ html += ` </div>\n`;
418
+ }
419
+ html += `
420
+ </div>
421
+ </body>
422
+ </html>`;
423
+ return html;
424
+ }
425
+ function generateJUnitXML(data) {
426
+ const { testFiles } = data;
427
+ const totalTests = testFiles.reduce((sum, f) => sum + f.results.length, 0);
428
+ const failures = testFiles.reduce((sum, f) => sum + f.results.filter(t => t.status !== 'passed').length, 0);
429
+ const totalTime = testFiles.reduce((sum, f) => sum + f.results.reduce((s, t) => s + t.duration, 0), 0) / 1000;
430
+ let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
431
+ xml += `<testsuites tests="${totalTests}" failures="${failures}" time="${totalTime.toFixed(3)}">\n`;
432
+ for (const { file, testFile, results } of testFiles) {
433
+ const suiteTests = results.length;
434
+ const suiteFailures = results.filter(r => r.status !== 'passed').length;
435
+ const suiteTime = results.reduce((s, t) => s + t.duration, 0) / 1000;
436
+ xml += ` <testsuite name="${escapeXml(testFile.name)}" tests="${suiteTests}" failures="${suiteFailures}" time="${suiteTime.toFixed(3)}" file="${escapeXml(file)}">\n`;
437
+ for (const result of results) {
438
+ const testTime = result.duration / 1000;
439
+ xml += ` <testcase name="${escapeXml(result.testName)}" classname="${escapeXml(testFile.name)}" time="${testTime.toFixed(3)}">\n`;
440
+ if (result.status !== 'passed' && result.error) {
441
+ xml += ` <failure message="${escapeXml(result.error.message)}">\n`;
442
+ xml += `${escapeXml(result.error.stack || result.error.message)}\n`;
443
+ xml += ` </failure>\n`;
444
+ }
445
+ xml += ` </testcase>\n`;
446
+ }
447
+ xml += ` </testsuite>\n`;
448
+ }
449
+ xml += '</testsuites>\n';
450
+ return xml;
451
+ }