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.
- package/LICENSE +21 -0
- package/README.md +333 -0
- package/dist/cli/executor.d.ts +32 -0
- package/dist/cli/executor.js +517 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +411 -0
- package/dist/cli/reporters.d.ts +62 -0
- package/dist/cli/reporters.js +451 -0
- package/dist/client/assets/index-4hbFPUhP.js +2087 -0
- package/dist/client/assets/index-4hbFPUhP.js.map +1 -0
- package/dist/client/assets/index-Dnk1ti7l.css +1 -0
- package/dist/client/index.html +25 -0
- package/dist/core/blocks/api.d.ts +2 -0
- package/dist/core/blocks/api.js +610 -0
- package/dist/core/blocks/data-driven.d.ts +2 -0
- package/dist/core/blocks/data-driven.js +245 -0
- package/dist/core/blocks/index.d.ts +15 -0
- package/dist/core/blocks/index.js +71 -0
- package/dist/core/blocks/lifecycle.d.ts +2 -0
- package/dist/core/blocks/lifecycle.js +199 -0
- package/dist/core/blocks/logic.d.ts +2 -0
- package/dist/core/blocks/logic.js +357 -0
- package/dist/core/blocks/playwright.d.ts +2 -0
- package/dist/core/blocks/playwright.js +764 -0
- package/dist/core/blocks/procedures.d.ts +5 -0
- package/dist/core/blocks/procedures.js +321 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +44 -0
- package/dist/core/plugins.d.ts +66 -0
- package/dist/core/plugins.js +118 -0
- package/dist/core/types.d.ts +153 -0
- package/dist/core/types.js +2 -0
- package/dist/server/codegenManager.d.ts +54 -0
- package/dist/server/codegenManager.js +259 -0
- package/dist/server/codegenParser.d.ts +17 -0
- package/dist/server/codegenParser.js +598 -0
- package/dist/server/executor.d.ts +37 -0
- package/dist/server/executor.js +672 -0
- package/dist/server/globals.d.ts +85 -0
- package/dist/server/globals.js +273 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +361 -0
- package/dist/server/plugins.d.ts +55 -0
- package/dist/server/plugins.js +206 -0
- package/package.json +103 -0
|
@@ -0,0 +1,672 @@
|
|
|
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.TestExecutor = void 0;
|
|
37
|
+
const playwright_1 = require("playwright");
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const core_1 = require("../core");
|
|
41
|
+
// Parse CSV content into TestDataSet array
|
|
42
|
+
function parseCSV(content) {
|
|
43
|
+
const lines = content.trim().split('\n');
|
|
44
|
+
if (lines.length < 2)
|
|
45
|
+
return []; // Need header + at least one data row
|
|
46
|
+
const headers = lines[0].split(',').map(h => h.trim());
|
|
47
|
+
const dataSets = [];
|
|
48
|
+
for (let i = 1; i < lines.length; i++) {
|
|
49
|
+
const line = lines[i].trim();
|
|
50
|
+
if (!line)
|
|
51
|
+
continue;
|
|
52
|
+
// Simple CSV parsing (handles basic quoted values)
|
|
53
|
+
const values = [];
|
|
54
|
+
let current = '';
|
|
55
|
+
let inQuotes = false;
|
|
56
|
+
for (const char of line) {
|
|
57
|
+
if (char === '"') {
|
|
58
|
+
inQuotes = !inQuotes;
|
|
59
|
+
}
|
|
60
|
+
else if (char === ',' && !inQuotes) {
|
|
61
|
+
values.push(current.trim());
|
|
62
|
+
current = '';
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
current += char;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
values.push(current.trim());
|
|
69
|
+
const row = {};
|
|
70
|
+
headers.forEach((header, idx) => {
|
|
71
|
+
const val = values[idx] ?? '';
|
|
72
|
+
// Try to parse as number or boolean
|
|
73
|
+
if (val === 'true') {
|
|
74
|
+
row[header] = true;
|
|
75
|
+
}
|
|
76
|
+
else if (val === 'false') {
|
|
77
|
+
row[header] = false;
|
|
78
|
+
}
|
|
79
|
+
else if (val !== '' && !isNaN(Number(val))) {
|
|
80
|
+
row[header] = Number(val);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
row[header] = val;
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
dataSets.push({ name: `Row ${i}`, values: row });
|
|
87
|
+
}
|
|
88
|
+
return dataSets;
|
|
89
|
+
}
|
|
90
|
+
class TestExecutor {
|
|
91
|
+
constructor(options = {}) {
|
|
92
|
+
this.browser = null;
|
|
93
|
+
this.context = null;
|
|
94
|
+
this.page = null;
|
|
95
|
+
this.plugins = new Map();
|
|
96
|
+
this.options = {
|
|
97
|
+
headless: true,
|
|
98
|
+
timeout: 30000,
|
|
99
|
+
...options,
|
|
100
|
+
};
|
|
101
|
+
// Register plugins
|
|
102
|
+
if (options.plugins) {
|
|
103
|
+
options.plugins.forEach(plugin => {
|
|
104
|
+
this.plugins.set(plugin.name, plugin);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async initialize() {
|
|
109
|
+
// Set the test ID attribute globally for Playwright selectors
|
|
110
|
+
if (this.options.testIdAttribute) {
|
|
111
|
+
playwright_1.selectors.setTestIdAttribute(this.options.testIdAttribute);
|
|
112
|
+
}
|
|
113
|
+
this.browser = await playwright_1.chromium.launch({
|
|
114
|
+
headless: this.options.headless,
|
|
115
|
+
});
|
|
116
|
+
this.context = await this.browser.newContext();
|
|
117
|
+
this.page = await this.context.newPage();
|
|
118
|
+
if (this.options.timeout) {
|
|
119
|
+
this.page.setDefaultTimeout(this.options.timeout);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async cleanup() {
|
|
123
|
+
if (this.page)
|
|
124
|
+
await this.page.close();
|
|
125
|
+
if (this.context)
|
|
126
|
+
await this.context.close();
|
|
127
|
+
if (this.browser)
|
|
128
|
+
await this.browser.close();
|
|
129
|
+
this.page = null;
|
|
130
|
+
this.context = null;
|
|
131
|
+
this.browser = null;
|
|
132
|
+
}
|
|
133
|
+
async runTestFile(testFile) {
|
|
134
|
+
const results = [];
|
|
135
|
+
// Register custom blocks from procedures
|
|
136
|
+
if (testFile.procedures) {
|
|
137
|
+
this.registerCustomBlocksFromProcedures(testFile.procedures);
|
|
138
|
+
}
|
|
139
|
+
await this.initialize();
|
|
140
|
+
// Create shared execution context for lifecycle hooks
|
|
141
|
+
const sharedContext = {
|
|
142
|
+
variables: new Map(Object.entries({
|
|
143
|
+
...this.resolveVariableDefaults(testFile.variables),
|
|
144
|
+
...this.options.variables,
|
|
145
|
+
})),
|
|
146
|
+
results: [],
|
|
147
|
+
browser: this.browser,
|
|
148
|
+
page: this.page,
|
|
149
|
+
logger: this.createLogger(),
|
|
150
|
+
plugins: this.plugins,
|
|
151
|
+
testIdAttribute: this.options.testIdAttribute,
|
|
152
|
+
};
|
|
153
|
+
try {
|
|
154
|
+
// Run beforeAll hooks
|
|
155
|
+
if (testFile.beforeAll && testFile.beforeAll.length > 0) {
|
|
156
|
+
sharedContext.logger.info('Running beforeAll hooks...');
|
|
157
|
+
const beforeAllResult = await this.runLifecycleSteps(testFile.beforeAll, 'beforeAll', sharedContext);
|
|
158
|
+
results.push(beforeAllResult);
|
|
159
|
+
if (beforeAllResult.status === 'failed' || beforeAllResult.status === 'error') {
|
|
160
|
+
// Don't run tests if beforeAll failed
|
|
161
|
+
return results;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Run each test with beforeEach/afterEach
|
|
165
|
+
for (const test of testFile.tests) {
|
|
166
|
+
// Load data from file if specified
|
|
167
|
+
let testData = test.data;
|
|
168
|
+
if (test.dataFile && !testData) {
|
|
169
|
+
testData = this.loadDataFromFile(test.dataFile);
|
|
170
|
+
}
|
|
171
|
+
// Check if test has data-driven sets
|
|
172
|
+
if (testData && testData.length > 0) {
|
|
173
|
+
// Run test for each data set
|
|
174
|
+
for (let i = 0; i < testData.length; i++) {
|
|
175
|
+
const dataSet = testData[i];
|
|
176
|
+
// Run suite-level beforeEach
|
|
177
|
+
if (testFile.beforeEach && testFile.beforeEach.length > 0) {
|
|
178
|
+
for (const step of testFile.beforeEach) {
|
|
179
|
+
await this.runStep(step, sharedContext);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const result = await this.runTestWithData(test, testFile.variables, dataSet, i, sharedContext);
|
|
183
|
+
results.push(result);
|
|
184
|
+
// Run suite-level afterEach
|
|
185
|
+
if (testFile.afterEach && testFile.afterEach.length > 0) {
|
|
186
|
+
for (const step of testFile.afterEach) {
|
|
187
|
+
await this.runStep(step, sharedContext);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
// Run test once without data
|
|
194
|
+
// Run suite-level beforeEach
|
|
195
|
+
if (testFile.beforeEach && testFile.beforeEach.length > 0) {
|
|
196
|
+
for (const step of testFile.beforeEach) {
|
|
197
|
+
await this.runStep(step, sharedContext);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const result = await this.runTest(test, testFile.variables, sharedContext);
|
|
201
|
+
results.push(result);
|
|
202
|
+
// Run suite-level afterEach
|
|
203
|
+
if (testFile.afterEach && testFile.afterEach.length > 0) {
|
|
204
|
+
for (const step of testFile.afterEach) {
|
|
205
|
+
await this.runStep(step, sharedContext);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// Run afterAll hooks
|
|
211
|
+
if (testFile.afterAll && testFile.afterAll.length > 0) {
|
|
212
|
+
sharedContext.logger.info('Running afterAll hooks...');
|
|
213
|
+
const afterAllResult = await this.runLifecycleSteps(testFile.afterAll, 'afterAll', sharedContext);
|
|
214
|
+
results.push(afterAllResult);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
await this.cleanup();
|
|
219
|
+
}
|
|
220
|
+
return results;
|
|
221
|
+
}
|
|
222
|
+
async runLifecycleSteps(steps, lifecycleType, context) {
|
|
223
|
+
const startedAt = new Date().toISOString();
|
|
224
|
+
const startTime = Date.now();
|
|
225
|
+
const stepResults = [];
|
|
226
|
+
let status = 'passed';
|
|
227
|
+
let error;
|
|
228
|
+
for (const step of steps) {
|
|
229
|
+
const stepResult = await this.runStep(step, context);
|
|
230
|
+
stepResults.push(stepResult);
|
|
231
|
+
if (stepResult.status === 'failed' || stepResult.status === 'error') {
|
|
232
|
+
status = stepResult.status;
|
|
233
|
+
error = stepResult.error;
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
testId: `lifecycle-${lifecycleType}`,
|
|
239
|
+
testName: lifecycleType,
|
|
240
|
+
status,
|
|
241
|
+
duration: Date.now() - startTime,
|
|
242
|
+
steps: stepResults,
|
|
243
|
+
error,
|
|
244
|
+
startedAt,
|
|
245
|
+
finishedAt: new Date().toISOString(),
|
|
246
|
+
isLifecycle: true,
|
|
247
|
+
lifecycleType,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Public method to register custom blocks from procedures
|
|
252
|
+
*/
|
|
253
|
+
registerProcedures(procedures) {
|
|
254
|
+
this.registerCustomBlocksFromProcedures(procedures);
|
|
255
|
+
}
|
|
256
|
+
registerCustomBlocksFromProcedures(procedures) {
|
|
257
|
+
Object.values(procedures).forEach(proc => {
|
|
258
|
+
if (!proc.steps || proc.steps.length === 0)
|
|
259
|
+
return;
|
|
260
|
+
const blockType = `custom_${proc.name.toLowerCase().replace(/\s+/g, '_')}`;
|
|
261
|
+
// Check if already registered
|
|
262
|
+
if ((0, core_1.getBlock)(blockType))
|
|
263
|
+
return;
|
|
264
|
+
const blockDef = {
|
|
265
|
+
type: blockType,
|
|
266
|
+
category: 'Custom',
|
|
267
|
+
color: '#607D8B',
|
|
268
|
+
tooltip: proc.description || `Custom block: ${proc.name}`,
|
|
269
|
+
inputs: (proc.params || []).map(param => ({
|
|
270
|
+
name: param.name.toUpperCase(),
|
|
271
|
+
type: 'field',
|
|
272
|
+
fieldType: param.type === 'number' ? 'number' : 'text',
|
|
273
|
+
default: param.default,
|
|
274
|
+
})),
|
|
275
|
+
previousStatement: true,
|
|
276
|
+
nextStatement: true,
|
|
277
|
+
execute: async (params, context) => {
|
|
278
|
+
context.logger.info(`Executing custom block: ${proc.name}`);
|
|
279
|
+
// Set procedure parameters in context.variables so ${paramName} references work
|
|
280
|
+
(proc.params || []).forEach(p => {
|
|
281
|
+
const value = params[p.name.toUpperCase()];
|
|
282
|
+
if (value !== undefined) {
|
|
283
|
+
context.variables.set(p.name, value);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
return {
|
|
287
|
+
customBlock: true,
|
|
288
|
+
name: proc.name,
|
|
289
|
+
steps: proc.steps,
|
|
290
|
+
};
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
(0, core_1.registerBlock)(blockDef);
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
async runTest(test, fileVariables, sharedContext) {
|
|
297
|
+
const startedAt = new Date().toISOString();
|
|
298
|
+
const startTime = Date.now();
|
|
299
|
+
const stepResults = [];
|
|
300
|
+
// Create execution context, inheriting variables from shared context (e.g., from beforeAll)
|
|
301
|
+
const baseVariables = sharedContext
|
|
302
|
+
? Object.fromEntries(sharedContext.variables)
|
|
303
|
+
: {
|
|
304
|
+
...this.resolveVariableDefaults(fileVariables),
|
|
305
|
+
...this.options.variables,
|
|
306
|
+
};
|
|
307
|
+
const context = {
|
|
308
|
+
variables: new Map(Object.entries(baseVariables)),
|
|
309
|
+
results: [],
|
|
310
|
+
browser: this.browser,
|
|
311
|
+
page: this.page,
|
|
312
|
+
logger: this.createLogger(),
|
|
313
|
+
plugins: this.plugins,
|
|
314
|
+
testIdAttribute: this.options.testIdAttribute,
|
|
315
|
+
};
|
|
316
|
+
// Run beforeTest hooks
|
|
317
|
+
for (const plugin of this.plugins.values()) {
|
|
318
|
+
if (plugin.hooks?.beforeTest) {
|
|
319
|
+
await plugin.hooks.beforeTest(context, test);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
let testStatus = 'passed';
|
|
323
|
+
let testError;
|
|
324
|
+
try {
|
|
325
|
+
// Execute steps from Blockly serialization format
|
|
326
|
+
const steps = this.extractStepsFromBlocklyState(test.steps);
|
|
327
|
+
for (const step of steps) {
|
|
328
|
+
const stepResult = await this.runStep(step, context);
|
|
329
|
+
stepResults.push(stepResult);
|
|
330
|
+
if (stepResult.status === 'failed' || stepResult.status === 'error') {
|
|
331
|
+
testStatus = stepResult.status;
|
|
332
|
+
testError = stepResult.error;
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
testStatus = 'error';
|
|
339
|
+
testError = {
|
|
340
|
+
message: error.message,
|
|
341
|
+
stack: error.stack,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
const result = {
|
|
345
|
+
testId: test.id,
|
|
346
|
+
testName: test.name,
|
|
347
|
+
status: testStatus,
|
|
348
|
+
duration: Date.now() - startTime,
|
|
349
|
+
steps: stepResults,
|
|
350
|
+
error: testError,
|
|
351
|
+
startedAt,
|
|
352
|
+
finishedAt: new Date().toISOString(),
|
|
353
|
+
};
|
|
354
|
+
// Run afterTest hooks
|
|
355
|
+
for (const plugin of this.plugins.values()) {
|
|
356
|
+
if (plugin.hooks?.afterTest) {
|
|
357
|
+
await plugin.hooks.afterTest(context, test, result);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return result;
|
|
361
|
+
}
|
|
362
|
+
async runTestWithData(test, fileVariables, dataSet, dataIndex, sharedContext) {
|
|
363
|
+
const testName = dataSet.name
|
|
364
|
+
? `${test.name} [${dataSet.name}]`
|
|
365
|
+
: `${test.name} [${dataIndex + 1}]`;
|
|
366
|
+
console.log(` Running: ${testName}`);
|
|
367
|
+
const startedAt = new Date().toISOString();
|
|
368
|
+
const startTime = Date.now();
|
|
369
|
+
const stepResults = [];
|
|
370
|
+
// Create execution context with data values, inheriting from shared context
|
|
371
|
+
const baseVariables = sharedContext
|
|
372
|
+
? Object.fromEntries(sharedContext.variables)
|
|
373
|
+
: {
|
|
374
|
+
...this.resolveVariableDefaults(fileVariables),
|
|
375
|
+
...this.options.variables,
|
|
376
|
+
};
|
|
377
|
+
const context = {
|
|
378
|
+
variables: new Map(Object.entries(baseVariables)),
|
|
379
|
+
results: [],
|
|
380
|
+
browser: this.browser,
|
|
381
|
+
page: this.page,
|
|
382
|
+
logger: this.createLogger(),
|
|
383
|
+
plugins: this.plugins,
|
|
384
|
+
testIdAttribute: this.options.testIdAttribute,
|
|
385
|
+
currentData: dataSet,
|
|
386
|
+
dataIndex,
|
|
387
|
+
};
|
|
388
|
+
// Inject data values into variables
|
|
389
|
+
for (const [key, value] of Object.entries(dataSet.values)) {
|
|
390
|
+
context.variables.set(key, value);
|
|
391
|
+
}
|
|
392
|
+
// Run beforeTest hooks
|
|
393
|
+
for (const plugin of this.plugins.values()) {
|
|
394
|
+
if (plugin.hooks?.beforeTest) {
|
|
395
|
+
await plugin.hooks.beforeTest(context, test);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
let testStatus = 'passed';
|
|
399
|
+
let testError;
|
|
400
|
+
try {
|
|
401
|
+
// Execute steps from Blockly serialization format
|
|
402
|
+
const steps = this.extractStepsFromBlocklyState(test.steps);
|
|
403
|
+
for (const step of steps) {
|
|
404
|
+
const stepResult = await this.runStep(step, context);
|
|
405
|
+
stepResults.push(stepResult);
|
|
406
|
+
if (stepResult.status === 'failed' || stepResult.status === 'error') {
|
|
407
|
+
testStatus = stepResult.status;
|
|
408
|
+
testError = stepResult.error;
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
testStatus = 'error';
|
|
415
|
+
testError = {
|
|
416
|
+
message: error.message,
|
|
417
|
+
stack: error.stack,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
const result = {
|
|
421
|
+
testId: `${test.id}-${dataIndex}`,
|
|
422
|
+
testName,
|
|
423
|
+
status: testStatus,
|
|
424
|
+
duration: Date.now() - startTime,
|
|
425
|
+
steps: stepResults,
|
|
426
|
+
error: testError,
|
|
427
|
+
startedAt,
|
|
428
|
+
finishedAt: new Date().toISOString(),
|
|
429
|
+
};
|
|
430
|
+
// Run afterTest hooks
|
|
431
|
+
for (const plugin of this.plugins.values()) {
|
|
432
|
+
if (plugin.hooks?.afterTest) {
|
|
433
|
+
await plugin.hooks.afterTest(context, test, result);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return result;
|
|
437
|
+
}
|
|
438
|
+
extractStepsFromBlocklyState(state) {
|
|
439
|
+
if (!state || typeof state !== 'object')
|
|
440
|
+
return [];
|
|
441
|
+
const stateObj = state;
|
|
442
|
+
// Handle Blockly serialization format
|
|
443
|
+
if ('blocks' in stateObj && typeof stateObj.blocks === 'object') {
|
|
444
|
+
const blocks = stateObj.blocks;
|
|
445
|
+
if ('blocks' in blocks && Array.isArray(blocks.blocks)) {
|
|
446
|
+
return this.blocksToSteps(blocks.blocks);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
// Handle direct array of steps
|
|
450
|
+
if (Array.isArray(state)) {
|
|
451
|
+
return state;
|
|
452
|
+
}
|
|
453
|
+
return [];
|
|
454
|
+
}
|
|
455
|
+
blocksToSteps(blocks) {
|
|
456
|
+
const steps = [];
|
|
457
|
+
for (const block of blocks) {
|
|
458
|
+
const step = this.blockToStep(block);
|
|
459
|
+
if (step) {
|
|
460
|
+
steps.push(step);
|
|
461
|
+
// Handle next block in chain
|
|
462
|
+
let currentBlock = block;
|
|
463
|
+
while (currentBlock.next) {
|
|
464
|
+
const nextBlock = currentBlock.next.block;
|
|
465
|
+
const nextStep = this.blockToStep(nextBlock);
|
|
466
|
+
if (nextStep) {
|
|
467
|
+
steps.push(nextStep);
|
|
468
|
+
}
|
|
469
|
+
currentBlock = nextBlock;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return steps;
|
|
474
|
+
}
|
|
475
|
+
blockToStep(block) {
|
|
476
|
+
if (!block || !block.type)
|
|
477
|
+
return null;
|
|
478
|
+
const step = {
|
|
479
|
+
id: block.id || `step-${Date.now()}`,
|
|
480
|
+
type: block.type,
|
|
481
|
+
params: {},
|
|
482
|
+
};
|
|
483
|
+
// Extract field values
|
|
484
|
+
if (block.fields && typeof block.fields === 'object') {
|
|
485
|
+
for (const [name, value] of Object.entries(block.fields)) {
|
|
486
|
+
step.params[name] = value;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// Extract inputs (connected blocks)
|
|
490
|
+
if (block.inputs && typeof block.inputs === 'object') {
|
|
491
|
+
for (const [name, input] of Object.entries(block.inputs)) {
|
|
492
|
+
const inputObj = input;
|
|
493
|
+
if (inputObj.block) {
|
|
494
|
+
// Recursively convert connected block
|
|
495
|
+
const connectedStep = this.blockToStep(inputObj.block);
|
|
496
|
+
if (connectedStep) {
|
|
497
|
+
step.params[name] = connectedStep;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return step;
|
|
503
|
+
}
|
|
504
|
+
async runStep(step, context) {
|
|
505
|
+
const startTime = Date.now();
|
|
506
|
+
// Run beforeStep hooks
|
|
507
|
+
for (const plugin of this.plugins.values()) {
|
|
508
|
+
if (plugin.hooks?.beforeStep) {
|
|
509
|
+
await plugin.hooks.beforeStep(context, step);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
let status = 'passed';
|
|
513
|
+
let output;
|
|
514
|
+
let error;
|
|
515
|
+
try {
|
|
516
|
+
// Get block definition
|
|
517
|
+
const blockDef = (0, core_1.getBlock)(step.type);
|
|
518
|
+
if (!blockDef) {
|
|
519
|
+
throw new Error(`Unknown block type: ${step.type}`);
|
|
520
|
+
}
|
|
521
|
+
// Resolve any nested step params (connected blocks) to their values
|
|
522
|
+
const resolvedParams = await this.resolveParams(step.params, context);
|
|
523
|
+
// Execute the block
|
|
524
|
+
output = await blockDef.execute(resolvedParams, context);
|
|
525
|
+
// Handle custom blocks that return steps to execute
|
|
526
|
+
if (output && typeof output === 'object' && 'customBlock' in output) {
|
|
527
|
+
const customOutput = output;
|
|
528
|
+
for (const childStep of customOutput.steps) {
|
|
529
|
+
const childResult = await this.runStep(childStep, context);
|
|
530
|
+
if (childResult.status === 'failed' || childResult.status === 'error') {
|
|
531
|
+
status = childResult.status;
|
|
532
|
+
error = childResult.error;
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
// Handle compound actions (like procedure_login)
|
|
538
|
+
if (output && typeof output === 'object' && 'compoundAction' in output) {
|
|
539
|
+
const compoundOutput = output;
|
|
540
|
+
for (const childStep of compoundOutput.steps) {
|
|
541
|
+
const childResult = await this.runStep(childStep, context);
|
|
542
|
+
if (childResult.status === 'failed' || childResult.status === 'error') {
|
|
543
|
+
status = childResult.status;
|
|
544
|
+
error = childResult.error;
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
catch (err) {
|
|
551
|
+
status = 'failed';
|
|
552
|
+
error = {
|
|
553
|
+
message: err.message,
|
|
554
|
+
stack: err.stack,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
// Capture screenshot on failure for web tests only
|
|
558
|
+
let screenshot;
|
|
559
|
+
const isWebStep = step.type.startsWith('web_');
|
|
560
|
+
if (status === 'failed' && isWebStep && context.page) {
|
|
561
|
+
try {
|
|
562
|
+
const page = context.page;
|
|
563
|
+
const screenshotBuffer = await page.screenshot({ fullPage: false });
|
|
564
|
+
screenshot = `data:image/png;base64,${screenshotBuffer.toString('base64')}`;
|
|
565
|
+
}
|
|
566
|
+
catch (screenshotErr) {
|
|
567
|
+
console.warn('Failed to capture screenshot:', screenshotErr.message);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
const result = {
|
|
571
|
+
stepId: step.id,
|
|
572
|
+
stepType: step.type,
|
|
573
|
+
status,
|
|
574
|
+
duration: Date.now() - startTime,
|
|
575
|
+
output,
|
|
576
|
+
error,
|
|
577
|
+
screenshot,
|
|
578
|
+
};
|
|
579
|
+
// Run afterStep hooks
|
|
580
|
+
for (const plugin of this.plugins.values()) {
|
|
581
|
+
if (plugin.hooks?.afterStep) {
|
|
582
|
+
await plugin.hooks.afterStep(context, step, result);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return result;
|
|
586
|
+
}
|
|
587
|
+
async resolveParams(params, context) {
|
|
588
|
+
const resolved = {};
|
|
589
|
+
for (const [key, value] of Object.entries(params)) {
|
|
590
|
+
if (value && typeof value === 'object' && 'type' in value) {
|
|
591
|
+
// This is a connected block - execute it to get the value
|
|
592
|
+
const nestedStep = value;
|
|
593
|
+
const blockDef = (0, core_1.getBlock)(nestedStep.type);
|
|
594
|
+
if (blockDef) {
|
|
595
|
+
const nestedParams = await this.resolveParams(nestedStep.params || {}, context);
|
|
596
|
+
resolved[key] = await blockDef.execute(nestedParams, context);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
resolved[key] = value;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return resolved;
|
|
604
|
+
}
|
|
605
|
+
loadDataFromFile(filePath) {
|
|
606
|
+
try {
|
|
607
|
+
// Resolve path relative to baseDir if not absolute
|
|
608
|
+
const resolvedPath = path.isAbsolute(filePath)
|
|
609
|
+
? filePath
|
|
610
|
+
: path.resolve(this.options.baseDir || process.cwd(), filePath);
|
|
611
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
612
|
+
console.error(`Data file not found: ${resolvedPath}`);
|
|
613
|
+
return [];
|
|
614
|
+
}
|
|
615
|
+
const content = fs.readFileSync(resolvedPath, 'utf-8');
|
|
616
|
+
const ext = path.extname(resolvedPath).toLowerCase();
|
|
617
|
+
if (ext === '.csv') {
|
|
618
|
+
return parseCSV(content);
|
|
619
|
+
}
|
|
620
|
+
else if (ext === '.json') {
|
|
621
|
+
const data = JSON.parse(content);
|
|
622
|
+
// Expect JSON to be an array of objects with 'values' or direct objects
|
|
623
|
+
if (Array.isArray(data)) {
|
|
624
|
+
return data.map((item, i) => ({
|
|
625
|
+
name: item.name || `Row ${i + 1}`,
|
|
626
|
+
values: item.values || item,
|
|
627
|
+
}));
|
|
628
|
+
}
|
|
629
|
+
return [];
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
console.error(`Unsupported data file format: ${ext}`);
|
|
633
|
+
return [];
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
catch (error) {
|
|
637
|
+
console.error(`Failed to load data file: ${filePath}`, error);
|
|
638
|
+
return [];
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
resolveVariableDefaults(vars) {
|
|
642
|
+
if (!vars)
|
|
643
|
+
return {};
|
|
644
|
+
const resolved = {};
|
|
645
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
646
|
+
if (value && typeof value === 'object' && 'default' in value) {
|
|
647
|
+
resolved[key] = value.default;
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
resolved[key] = value;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return resolved;
|
|
654
|
+
}
|
|
655
|
+
createLogger() {
|
|
656
|
+
return {
|
|
657
|
+
info: (message, data) => {
|
|
658
|
+
console.log(`[INFO] ${message}`, data !== undefined ? data : '');
|
|
659
|
+
},
|
|
660
|
+
warn: (message, data) => {
|
|
661
|
+
console.warn(`[WARN] ${message}`, data !== undefined ? data : '');
|
|
662
|
+
},
|
|
663
|
+
error: (message, data) => {
|
|
664
|
+
console.error(`[ERROR] ${message}`, data !== undefined ? data : '');
|
|
665
|
+
},
|
|
666
|
+
debug: (message, data) => {
|
|
667
|
+
console.debug(`[DEBUG] ${message}`, data !== undefined ? data : '');
|
|
668
|
+
},
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
exports.TestExecutor = TestExecutor;
|