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,517 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TestExecutor = void 0;
4
+ const playwright_1 = require("playwright");
5
+ const core_1 = require("../core");
6
+ class TestExecutor {
7
+ constructor(options = {}) {
8
+ this.browser = null;
9
+ this.browserContext = null;
10
+ this.page = null;
11
+ this.plugins = new Map();
12
+ this.procedures = new Map();
13
+ this.options = {
14
+ headless: true,
15
+ timeout: 30000,
16
+ ...options,
17
+ };
18
+ if (options.plugins) {
19
+ options.plugins.forEach(plugin => {
20
+ this.plugins.set(plugin.name, plugin);
21
+ });
22
+ }
23
+ }
24
+ async initialize() {
25
+ this.browser = await playwright_1.chromium.launch({
26
+ headless: this.options.headless,
27
+ });
28
+ this.browserContext = await this.browser.newContext();
29
+ this.page = await this.browserContext.newPage();
30
+ if (this.options.timeout) {
31
+ this.page.setDefaultTimeout(this.options.timeout);
32
+ }
33
+ }
34
+ async cleanup() {
35
+ if (this.page)
36
+ await this.page.close();
37
+ if (this.browserContext)
38
+ await this.browserContext.close();
39
+ if (this.browser)
40
+ await this.browser.close();
41
+ this.page = null;
42
+ this.browserContext = null;
43
+ this.browser = null;
44
+ }
45
+ async runTestFile(testFile) {
46
+ const results = [];
47
+ await this.initialize();
48
+ // Register procedures from the test file
49
+ if (testFile.procedures) {
50
+ for (const [name, procedure] of Object.entries(testFile.procedures)) {
51
+ this.procedures.set(name, procedure);
52
+ }
53
+ }
54
+ // Create base context for hooks
55
+ const baseContext = this.createBaseContext(testFile.variables);
56
+ try {
57
+ // Run beforeAll hooks
58
+ if (testFile.beforeAll) {
59
+ const steps = this.extractStepsFromBlocklyState(testFile.beforeAll);
60
+ await this.runSteps(steps, baseContext, 'beforeAll');
61
+ }
62
+ // Run each test
63
+ for (const test of testFile.tests) {
64
+ // Check if test has data-driven sets
65
+ if (test.data && test.data.length > 0) {
66
+ // Run test for each data set
67
+ for (let i = 0; i < test.data.length; i++) {
68
+ const dataSet = test.data[i];
69
+ const result = await this.runTestWithData(test, testFile, dataSet, i);
70
+ results.push(result);
71
+ }
72
+ }
73
+ else {
74
+ // Run test once without data
75
+ const result = await this.runTest(test, testFile);
76
+ results.push(result);
77
+ }
78
+ }
79
+ // Run afterAll hooks
80
+ if (testFile.afterAll) {
81
+ const steps = this.extractStepsFromBlocklyState(testFile.afterAll);
82
+ await this.runSteps(steps, baseContext, 'afterAll');
83
+ }
84
+ }
85
+ finally {
86
+ await this.cleanup();
87
+ }
88
+ return results;
89
+ }
90
+ createBaseContext(fileVariables) {
91
+ return {
92
+ variables: new Map(Object.entries({
93
+ ...this.resolveVariableDefaults(fileVariables),
94
+ ...this.options.variables,
95
+ })),
96
+ results: [],
97
+ browser: this.browser,
98
+ page: this.page,
99
+ logger: this.createLogger(),
100
+ plugins: this.plugins,
101
+ procedures: this.procedures,
102
+ };
103
+ }
104
+ async runTestWithData(test, testFile, dataSet, dataIndex) {
105
+ const testName = dataSet.name
106
+ ? `${test.name} [${dataSet.name}]`
107
+ : `${test.name} [${dataIndex + 1}]`;
108
+ console.log(` Running: ${testName}`);
109
+ const startedAt = new Date().toISOString();
110
+ const startTime = Date.now();
111
+ const stepResults = [];
112
+ // Create context with data
113
+ const context = {
114
+ ...this.createBaseContext(testFile.variables),
115
+ currentData: dataSet,
116
+ dataIndex,
117
+ };
118
+ // Inject data values into variables
119
+ for (const [key, value] of Object.entries(dataSet.values)) {
120
+ context.variables.set(key, value);
121
+ }
122
+ let testStatus = 'passed';
123
+ let testError;
124
+ try {
125
+ // Run suite-level beforeEach
126
+ if (testFile.beforeEach) {
127
+ const steps = this.extractStepsFromBlocklyState(testFile.beforeEach);
128
+ const hookResults = await this.runSteps(steps, context, 'beforeEach');
129
+ stepResults.push(...hookResults);
130
+ }
131
+ // Run test-level beforeEach
132
+ if (test.beforeEach) {
133
+ const steps = this.extractStepsFromBlocklyState(test.beforeEach);
134
+ const hookResults = await this.runSteps(steps, context, 'test.beforeEach');
135
+ stepResults.push(...hookResults);
136
+ }
137
+ // Run test steps
138
+ const steps = this.extractStepsFromBlocklyState(test.steps);
139
+ for (const step of steps) {
140
+ const stepResult = await this.runStep(step, context);
141
+ stepResults.push(stepResult);
142
+ if (stepResult.status === 'failed' || stepResult.status === 'error') {
143
+ testStatus = stepResult.status;
144
+ testError = stepResult.error;
145
+ break;
146
+ }
147
+ }
148
+ }
149
+ catch (error) {
150
+ if (typeof error === 'object' && error !== null && 'skip' in error) {
151
+ testStatus = 'skipped';
152
+ testError = { message: error.reason || 'Skipped' };
153
+ }
154
+ else {
155
+ testStatus = 'error';
156
+ testError = {
157
+ message: error.message,
158
+ stack: error.stack,
159
+ };
160
+ }
161
+ }
162
+ finally {
163
+ // Run test-level afterEach
164
+ if (test.afterEach) {
165
+ try {
166
+ const steps = this.extractStepsFromBlocklyState(test.afterEach);
167
+ const hookResults = await this.runSteps(steps, context, 'test.afterEach');
168
+ stepResults.push(...hookResults);
169
+ }
170
+ catch (e) {
171
+ console.error('Error in test.afterEach:', e);
172
+ }
173
+ }
174
+ // Run suite-level afterEach
175
+ if (testFile.afterEach) {
176
+ try {
177
+ const steps = this.extractStepsFromBlocklyState(testFile.afterEach);
178
+ const hookResults = await this.runSteps(steps, context, 'afterEach');
179
+ stepResults.push(...hookResults);
180
+ }
181
+ catch (e) {
182
+ console.error('Error in afterEach:', e);
183
+ }
184
+ }
185
+ }
186
+ return {
187
+ testId: test.id,
188
+ testName,
189
+ status: testStatus,
190
+ duration: Date.now() - startTime,
191
+ steps: stepResults,
192
+ error: testError,
193
+ startedAt,
194
+ finishedAt: new Date().toISOString(),
195
+ dataIteration: {
196
+ index: dataIndex,
197
+ name: dataSet.name,
198
+ data: dataSet.values,
199
+ },
200
+ };
201
+ }
202
+ async runTest(test, testFile) {
203
+ console.log(` Running: ${test.name}`);
204
+ const startedAt = new Date().toISOString();
205
+ const startTime = Date.now();
206
+ const stepResults = [];
207
+ const context = this.createBaseContext(testFile.variables);
208
+ for (const plugin of this.plugins.values()) {
209
+ if (plugin.hooks?.beforeTest) {
210
+ await plugin.hooks.beforeTest(context, test);
211
+ }
212
+ }
213
+ let testStatus = 'passed';
214
+ let testError;
215
+ try {
216
+ // Run suite-level beforeEach
217
+ if (testFile.beforeEach) {
218
+ const steps = this.extractStepsFromBlocklyState(testFile.beforeEach);
219
+ const hookResults = await this.runSteps(steps, context, 'beforeEach');
220
+ stepResults.push(...hookResults);
221
+ }
222
+ // Run test-level beforeEach
223
+ if (test.beforeEach) {
224
+ const steps = this.extractStepsFromBlocklyState(test.beforeEach);
225
+ const hookResults = await this.runSteps(steps, context, 'test.beforeEach');
226
+ stepResults.push(...hookResults);
227
+ }
228
+ // Run test steps
229
+ const steps = this.extractStepsFromBlocklyState(test.steps);
230
+ for (const step of steps) {
231
+ const stepResult = await this.runStep(step, context);
232
+ stepResults.push(stepResult);
233
+ if (stepResult.status === 'failed' || stepResult.status === 'error') {
234
+ testStatus = stepResult.status;
235
+ testError = stepResult.error;
236
+ break;
237
+ }
238
+ }
239
+ }
240
+ catch (error) {
241
+ if (typeof error === 'object' && error !== null && 'skip' in error) {
242
+ testStatus = 'skipped';
243
+ testError = { message: error.reason || 'Skipped' };
244
+ }
245
+ else {
246
+ testStatus = 'error';
247
+ testError = {
248
+ message: error.message,
249
+ stack: error.stack,
250
+ };
251
+ }
252
+ }
253
+ finally {
254
+ // Run test-level afterEach
255
+ if (test.afterEach) {
256
+ try {
257
+ const steps = this.extractStepsFromBlocklyState(test.afterEach);
258
+ const hookResults = await this.runSteps(steps, context, 'test.afterEach');
259
+ stepResults.push(...hookResults);
260
+ }
261
+ catch (e) {
262
+ console.error('Error in test.afterEach:', e);
263
+ }
264
+ }
265
+ // Run suite-level afterEach
266
+ if (testFile.afterEach) {
267
+ try {
268
+ const steps = this.extractStepsFromBlocklyState(testFile.afterEach);
269
+ const hookResults = await this.runSteps(steps, context, 'afterEach');
270
+ stepResults.push(...hookResults);
271
+ }
272
+ catch (e) {
273
+ console.error('Error in afterEach:', e);
274
+ }
275
+ }
276
+ }
277
+ const result = {
278
+ testId: test.id,
279
+ testName: test.name,
280
+ status: testStatus,
281
+ duration: Date.now() - startTime,
282
+ steps: stepResults,
283
+ error: testError,
284
+ startedAt,
285
+ finishedAt: new Date().toISOString(),
286
+ };
287
+ for (const plugin of this.plugins.values()) {
288
+ if (plugin.hooks?.afterTest) {
289
+ await plugin.hooks.afterTest(context, test, result);
290
+ }
291
+ }
292
+ return result;
293
+ }
294
+ async runSteps(steps, context, phase) {
295
+ const results = [];
296
+ for (const step of steps) {
297
+ const result = await this.runStep(step, context);
298
+ results.push(result);
299
+ if (result.status === 'failed' || result.status === 'error') {
300
+ throw new Error(`${phase} failed: ${result.error?.message}`);
301
+ }
302
+ }
303
+ return results;
304
+ }
305
+ resolveVariableDefaults(vars) {
306
+ if (!vars)
307
+ return {};
308
+ const resolved = {};
309
+ for (const [key, value] of Object.entries(vars)) {
310
+ if (value && typeof value === 'object' && 'default' in value) {
311
+ resolved[key] = value.default;
312
+ }
313
+ else {
314
+ resolved[key] = value;
315
+ }
316
+ }
317
+ return resolved;
318
+ }
319
+ extractStepsFromBlocklyState(state) {
320
+ if (!state || typeof state !== 'object')
321
+ return [];
322
+ const stateObj = state;
323
+ if ('blocks' in stateObj && typeof stateObj.blocks === 'object') {
324
+ const blocks = stateObj.blocks;
325
+ if ('blocks' in blocks && Array.isArray(blocks.blocks)) {
326
+ return this.blocksToSteps(blocks.blocks);
327
+ }
328
+ }
329
+ if (Array.isArray(state)) {
330
+ return state;
331
+ }
332
+ return [];
333
+ }
334
+ blocksToSteps(blocks) {
335
+ const steps = [];
336
+ for (const block of blocks) {
337
+ const step = this.blockToStep(block);
338
+ if (step) {
339
+ steps.push(step);
340
+ let currentBlock = block;
341
+ while (currentBlock.next) {
342
+ const nextBlock = currentBlock.next.block;
343
+ const nextStep = this.blockToStep(nextBlock);
344
+ if (nextStep) {
345
+ steps.push(nextStep);
346
+ }
347
+ currentBlock = nextBlock;
348
+ }
349
+ }
350
+ }
351
+ return steps;
352
+ }
353
+ blockToStep(block) {
354
+ if (!block || !block.type)
355
+ return null;
356
+ const step = {
357
+ id: block.id || `step-${Date.now()}`,
358
+ type: block.type,
359
+ params: {},
360
+ };
361
+ if (block.fields && typeof block.fields === 'object') {
362
+ for (const [name, value] of Object.entries(block.fields)) {
363
+ step.params[name] = value;
364
+ }
365
+ }
366
+ if (block.inputs && typeof block.inputs === 'object') {
367
+ for (const [name, input] of Object.entries(block.inputs)) {
368
+ const inputObj = input;
369
+ if (inputObj.block) {
370
+ const connectedStep = this.blockToStep(inputObj.block);
371
+ if (connectedStep) {
372
+ step.params[name] = connectedStep;
373
+ }
374
+ }
375
+ }
376
+ }
377
+ return step;
378
+ }
379
+ async runStep(step, context) {
380
+ const startTime = Date.now();
381
+ for (const plugin of this.plugins.values()) {
382
+ if (plugin.hooks?.beforeStep) {
383
+ await plugin.hooks.beforeStep(context, step);
384
+ }
385
+ }
386
+ let status = 'passed';
387
+ let output;
388
+ let error;
389
+ try {
390
+ const blockDef = (0, core_1.getBlock)(step.type);
391
+ if (!blockDef) {
392
+ throw new Error(`Unknown block type: ${step.type}`);
393
+ }
394
+ const resolvedParams = await this.resolveParams(step.params, context);
395
+ output = await blockDef.execute(resolvedParams, context);
396
+ // Handle special return values for control flow
397
+ if (output && typeof output === 'object') {
398
+ const outputObj = output;
399
+ // Handle procedure calls
400
+ if (outputObj.procedureCall) {
401
+ output = await this.executeProcedure(outputObj.name, outputObj.args, context);
402
+ }
403
+ // Handle compound actions
404
+ if (outputObj.compoundAction && outputObj.steps) {
405
+ for (const subStep of outputObj.steps) {
406
+ const subResult = await this.runStep(subStep, context);
407
+ if (subResult.status !== 'passed') {
408
+ throw new Error(subResult.error?.message || 'Compound action step failed');
409
+ }
410
+ }
411
+ }
412
+ // Handle retry logic
413
+ // TODO: Implement proper retry with nested statement execution
414
+ if (outputObj.retry && outputObj.statement) {
415
+ const _times = outputObj.times;
416
+ const _delay = outputObj.delay;
417
+ // Retry logic placeholder - full implementation would execute nested statements
418
+ console.log(`Retry block: ${_times} attempts with ${_delay}ms delay`);
419
+ }
420
+ }
421
+ }
422
+ catch (err) {
423
+ // Handle skip
424
+ if (typeof err === 'object' && err !== null && 'skip' in err) {
425
+ throw err; // Re-throw skip to be handled by runTest
426
+ }
427
+ status = 'failed';
428
+ error = {
429
+ message: err.message,
430
+ stack: err.stack,
431
+ };
432
+ }
433
+ const result = {
434
+ stepId: step.id,
435
+ stepType: step.type,
436
+ status,
437
+ duration: Date.now() - startTime,
438
+ output,
439
+ error,
440
+ };
441
+ for (const plugin of this.plugins.values()) {
442
+ if (plugin.hooks?.afterStep) {
443
+ await plugin.hooks.afterStep(context, step, result);
444
+ }
445
+ }
446
+ return result;
447
+ }
448
+ async executeProcedure(name, args, context) {
449
+ const procedure = this.procedures.get(name);
450
+ if (!procedure) {
451
+ throw new Error(`Procedure not found: ${name}`);
452
+ }
453
+ context.logger.info(` Executing procedure: ${name}`);
454
+ // Set up parameter variables with prefix
455
+ for (const [key, value] of Object.entries(args)) {
456
+ context.variables.set(`__param_${key}`, value);
457
+ }
458
+ // Execute procedure steps
459
+ const steps = this.extractStepsFromBlocklyState(procedure.steps);
460
+ let returnValue;
461
+ for (const step of steps) {
462
+ const result = await this.runStep(step, context);
463
+ if (result.status !== 'passed') {
464
+ throw new Error(`Procedure ${name} failed: ${result.error?.message}`);
465
+ }
466
+ // Check for return value
467
+ if (result.output && typeof result.output === 'object') {
468
+ const outputObj = result.output;
469
+ if (outputObj.procedureReturn) {
470
+ returnValue = outputObj.value;
471
+ break;
472
+ }
473
+ }
474
+ }
475
+ // Clean up parameter variables
476
+ for (const key of Object.keys(args)) {
477
+ context.variables.delete(`__param_${key}`);
478
+ }
479
+ return returnValue;
480
+ }
481
+ async resolveParams(params, context) {
482
+ const resolved = {};
483
+ for (const [key, value] of Object.entries(params)) {
484
+ if (value && typeof value === 'object' && 'type' in value) {
485
+ const nestedStep = value;
486
+ const blockDef = (0, core_1.getBlock)(nestedStep.type);
487
+ if (blockDef) {
488
+ const nestedParams = await this.resolveParams(nestedStep.params || {}, context);
489
+ resolved[key] = await blockDef.execute(nestedParams, context);
490
+ }
491
+ }
492
+ else {
493
+ resolved[key] = value;
494
+ }
495
+ }
496
+ return resolved;
497
+ }
498
+ createLogger() {
499
+ return {
500
+ info: (message, data) => {
501
+ console.log(` ${message}`, data !== undefined ? data : '');
502
+ },
503
+ warn: (message, data) => {
504
+ console.warn(` ⚠ ${message}`, data !== undefined ? data : '');
505
+ },
506
+ error: (message, data) => {
507
+ console.error(` ✗ ${message}`, data !== undefined ? data : '');
508
+ },
509
+ debug: (message, data) => {
510
+ if (process.env.DEBUG) {
511
+ console.debug(` [debug] ${message}`, data !== undefined ? data : '');
512
+ }
513
+ },
514
+ };
515
+ }
516
+ }
517
+ exports.TestExecutor = TestExecutor;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};