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,598 @@
1
+ "use strict";
2
+ /**
3
+ * Playwright Codegen Parser
4
+ *
5
+ * Parses Playwright-generated JavaScript code and converts it to TestBlocks TestStep format.
6
+ * Converts Playwright locators to CSS selectors where possible.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.parsePlaywrightCode = parsePlaywrightCode;
10
+ exports.isValidPlaywrightCode = isValidPlaywrightCode;
11
+ /**
12
+ * Generate a unique step ID
13
+ */
14
+ function generateStepId() {
15
+ return `step-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
16
+ }
17
+ /**
18
+ * Escape special characters for CSS attribute selectors
19
+ */
20
+ function escapeCssAttributeValue(value) {
21
+ return value.replace(/"/g, '\\"');
22
+ }
23
+ /**
24
+ * Extract and convert Playwright selector to CSS/XPath where possible
25
+ * For selectors that can't be converted to pure CSS, returns Playwright selector syntax
26
+ * @param selectorCode - The Playwright selector code
27
+ * @param testIdAttribute - The attribute used for test IDs (default: 'data-testid')
28
+ */
29
+ function extractSelector(selectorCode, testIdAttribute = 'data-testid') {
30
+ // Handle locator('selector') - already CSS/XPath
31
+ // Use backreference to match opening/closing quote (handles quotes inside selector)
32
+ const locatorMatchSingle = selectorCode.match(/\.locator\('([^']*(?:''[^']*)*)'\)/);
33
+ const locatorMatchDouble = selectorCode.match(/\.locator\("([^"]*(?:""[^"]*)*)"\)/);
34
+ const locatorMatchBacktick = selectorCode.match(/\.locator\(`([^`]*(?:``[^`]*)*)`\)/);
35
+ if (locatorMatchSingle) {
36
+ return { selector: locatorMatchSingle[1], selectorType: 'css' };
37
+ }
38
+ if (locatorMatchDouble) {
39
+ return { selector: locatorMatchDouble[1], selectorType: 'css' };
40
+ }
41
+ if (locatorMatchBacktick) {
42
+ return { selector: locatorMatchBacktick[1], selectorType: 'css' };
43
+ }
44
+ // Handle getByTestId('testid') → store with testid: prefix
45
+ const getByTestIdMatch = selectorCode.match(/\.getByTestId\(['"`]([^'"`]+)['"`]\)/);
46
+ if (getByTestIdMatch) {
47
+ // Use prefix convention so it survives Blockly serialization
48
+ return { selector: `testid:${getByTestIdMatch[1]}`, selectorType: 'testid' };
49
+ }
50
+ // Handle getByPlaceholder('placeholder') → [placeholder="..."]
51
+ const getByPlaceholderMatch = selectorCode.match(/\.getByPlaceholder\(['"`]([^'"`]+)['"`]\)/);
52
+ if (getByPlaceholderMatch) {
53
+ return { selector: `[placeholder="${escapeCssAttributeValue(getByPlaceholderMatch[1])}"]`, selectorType: 'css' };
54
+ }
55
+ // Handle getByAltText('alt') → [alt="..."]
56
+ const getByAltTextMatch = selectorCode.match(/\.getByAltText\(['"`]([^'"`]+)['"`]\)/);
57
+ if (getByAltTextMatch) {
58
+ return { selector: `[alt="${escapeCssAttributeValue(getByAltTextMatch[1])}"]`, selectorType: 'css' };
59
+ }
60
+ // Handle getByTitle('title') → [title="..."]
61
+ const getByTitleMatch = selectorCode.match(/\.getByTitle\(['"`]([^'"`]+)['"`]\)/);
62
+ if (getByTitleMatch) {
63
+ return { selector: `[title="${escapeCssAttributeValue(getByTitleMatch[1])}"]`, selectorType: 'css' };
64
+ }
65
+ // Handle getByRole('role', { name: 'text' }) - convert to CSS where possible
66
+ const getByRoleMatch = selectorCode.match(/\.getByRole\(['"`]([^'"`]+)['"`](?:,\s*\{\s*name:\s*['"`]([^'"`]+)['"`](?:,\s*exact:\s*(true|false))?\s*\})?\)/);
67
+ if (getByRoleMatch) {
68
+ const role = getByRoleMatch[1];
69
+ const name = getByRoleMatch[2];
70
+ // Map common roles to HTML elements/selectors
71
+ const roleToSelector = {
72
+ 'button': 'button, [role="button"], input[type="button"], input[type="submit"]',
73
+ 'link': 'a[href]',
74
+ 'textbox': 'input[type="text"], input:not([type]), textarea',
75
+ 'checkbox': 'input[type="checkbox"]',
76
+ 'radio': 'input[type="radio"]',
77
+ 'combobox': 'select',
78
+ 'listbox': 'select, [role="listbox"]',
79
+ 'heading': 'h1, h2, h3, h4, h5, h6',
80
+ 'img': 'img',
81
+ 'navigation': 'nav',
82
+ 'main': 'main',
83
+ 'banner': 'header',
84
+ 'contentinfo': 'footer',
85
+ };
86
+ if (roleToSelector[role] && name) {
87
+ // For buttons/links with name, try to match by text content
88
+ if (role === 'button') {
89
+ return { selector: `button:has-text("${escapeCssAttributeValue(name)}"), input[type="submit"][value="${escapeCssAttributeValue(name)}"]`, selectorType: 'css' };
90
+ }
91
+ else if (role === 'link') {
92
+ return { selector: `a:has-text("${escapeCssAttributeValue(name)}")`, selectorType: 'css' };
93
+ }
94
+ }
95
+ // Fallback to Playwright's role selector (works with Playwright)
96
+ if (name) {
97
+ return { selector: `role=${role}[name="${escapeCssAttributeValue(name)}"]`, selectorType: 'role' };
98
+ }
99
+ return { selector: `role=${role}`, selectorType: 'role' };
100
+ }
101
+ // Handle getByText('text') - use Playwright text selector
102
+ const getByTextMatch = selectorCode.match(/\.getByText\(['"`]([^'"`]+)['"`](?:,\s*\{\s*exact:\s*(true|false)\s*\})?\)/);
103
+ if (getByTextMatch) {
104
+ const text = getByTextMatch[1];
105
+ const exact = getByTextMatch[2] === 'true';
106
+ if (exact) {
107
+ return { selector: `text="${escapeCssAttributeValue(text)}"`, selectorType: 'text' };
108
+ }
109
+ return { selector: `text=${text}`, selectorType: 'text' };
110
+ }
111
+ // Handle getByLabel('label') - try to convert to CSS for common patterns
112
+ const getByLabelMatch = selectorCode.match(/\.getByLabel\(['"`]([^'"`]+)['"`]\)/);
113
+ if (getByLabelMatch) {
114
+ // Playwright label selector - works with Playwright
115
+ return { selector: `label=${getByLabelMatch[1]}`, selectorType: 'label' };
116
+ }
117
+ // Handle chained .first(), .last(), .nth(n) - only if they exist
118
+ const nthMatch = selectorCode.match(/\.nth\((\d+)\)/);
119
+ const hasFirst = selectorCode.includes('.first()');
120
+ const hasLast = selectorCode.includes('.last()');
121
+ if (nthMatch || hasFirst || hasLast) {
122
+ // Remove chaining methods to get base selector
123
+ const baseCode = selectorCode
124
+ .replace(/\.first\(\)/, '')
125
+ .replace(/\.last\(\)/, '')
126
+ .replace(/\.nth\(\d+\)/, '');
127
+ // Only recurse if we actually removed something
128
+ if (baseCode !== selectorCode) {
129
+ const baseResult = extractSelector(baseCode, testIdAttribute);
130
+ if (nthMatch) {
131
+ const n = parseInt(nthMatch[1], 10);
132
+ return { selector: `${baseResult.selector} >> nth=${n}`, selectorType: baseResult.selectorType };
133
+ }
134
+ else if (hasFirst) {
135
+ return { selector: `${baseResult.selector} >> nth=0`, selectorType: baseResult.selectorType };
136
+ }
137
+ else if (hasLast) {
138
+ return { selector: `${baseResult.selector} >> nth=-1`, selectorType: baseResult.selectorType };
139
+ }
140
+ }
141
+ }
142
+ // Fallback: try to extract selector from locator() if present
143
+ // This handles cases like: locator('[data-test="value"]')
144
+ const fallbackLocatorMatch = selectorCode.match(/locator\((['"`])([\s\S]*?)\1\)/);
145
+ if (fallbackLocatorMatch) {
146
+ return { selector: fallbackLocatorMatch[2], selectorType: 'css' };
147
+ }
148
+ // Last resort: return cleaned up code
149
+ return {
150
+ selector: selectorCode.replace(/^page\./, '').replace(/await\s+/, ''),
151
+ selectorType: 'css',
152
+ };
153
+ }
154
+ /**
155
+ * Extract selector from expect() statement
156
+ */
157
+ function extractExpectSelector(line, testIdAttribute = 'data-testid') {
158
+ // expect(page.locator(...))
159
+ const locatorInExpect = line.match(/expect\(page\.locator\(['"`]([^'"`]+)['"`]\)\)/);
160
+ if (locatorInExpect) {
161
+ return { selector: locatorInExpect[1], selectorType: 'css' };
162
+ }
163
+ // expect(page.getBy*(...))
164
+ const getByInExpect = line.match(/expect\((page\.[^)]+\))\)/);
165
+ if (getByInExpect) {
166
+ return extractSelector(getByInExpect[1], testIdAttribute);
167
+ }
168
+ return null;
169
+ }
170
+ /**
171
+ * Parse a single line of Playwright code and convert to TestStep
172
+ * @param line - The line of Playwright code
173
+ * @param testIdAttribute - The attribute used for test IDs (default: 'data-testid')
174
+ */
175
+ function parseLine(line, testIdAttribute = 'data-testid') {
176
+ const trimmed = line.trim();
177
+ // Skip empty lines, comments, imports, exports, test structure
178
+ if (!trimmed ||
179
+ trimmed.startsWith('//') ||
180
+ trimmed.startsWith('/*') ||
181
+ trimmed.startsWith('*') ||
182
+ trimmed.startsWith('import') ||
183
+ trimmed.startsWith('export') ||
184
+ trimmed.startsWith('const ') ||
185
+ trimmed.startsWith('let ') ||
186
+ trimmed.startsWith('var ') ||
187
+ trimmed.startsWith('test(') ||
188
+ trimmed.startsWith('test.describe') ||
189
+ trimmed.startsWith('test.beforeEach') ||
190
+ trimmed.startsWith('test.afterEach') ||
191
+ trimmed === '});' ||
192
+ trimmed === '});' ||
193
+ trimmed === '{' ||
194
+ trimmed === '}' ||
195
+ trimmed === '});') {
196
+ return null;
197
+ }
198
+ // ===== ASSERTIONS =====
199
+ // expect(page.locator(...)).toBeVisible()
200
+ if (trimmed.includes('.toBeVisible()')) {
201
+ const result = extractExpectSelector(trimmed, testIdAttribute);
202
+ if (result) {
203
+ return {
204
+ id: generateStepId(),
205
+ type: 'web_assert_visible',
206
+ params: {
207
+ SELECTOR: result.selector,
208
+ ...(result.selectorType && { SELECTOR_TYPE: result.selectorType }),
209
+ },
210
+ };
211
+ }
212
+ }
213
+ // expect(page.locator(...)).toBeHidden() or .not.toBeVisible()
214
+ if (trimmed.includes('.toBeHidden()') || trimmed.includes('.not.toBeVisible()')) {
215
+ const result = extractExpectSelector(trimmed, testIdAttribute);
216
+ if (result) {
217
+ return {
218
+ id: generateStepId(),
219
+ type: 'web_assert_not_visible',
220
+ params: {
221
+ SELECTOR: result.selector,
222
+ ...(result.selectorType && { SELECTOR_TYPE: result.selectorType }),
223
+ },
224
+ };
225
+ }
226
+ }
227
+ // expect(page.locator(...)).toContainText('text')
228
+ const containTextMatch = trimmed.match(/\.toContainText\(['"`]([^'"`]+)['"`]\)/);
229
+ if (containTextMatch) {
230
+ const result = extractExpectSelector(trimmed, testIdAttribute);
231
+ if (result) {
232
+ return {
233
+ id: generateStepId(),
234
+ type: 'web_assert_text_contains',
235
+ params: {
236
+ SELECTOR: result.selector,
237
+ ...(result.selectorType && { SELECTOR_TYPE: result.selectorType }),
238
+ TEXT: containTextMatch[1],
239
+ },
240
+ };
241
+ }
242
+ }
243
+ // expect(page.locator(...)).toHaveText('text')
244
+ const haveTextMatch = trimmed.match(/\.toHaveText\(['"`]([^'"`]+)['"`]\)/);
245
+ if (haveTextMatch) {
246
+ const result = extractExpectSelector(trimmed, testIdAttribute);
247
+ if (result) {
248
+ return {
249
+ id: generateStepId(),
250
+ type: 'web_assert_text_equals',
251
+ params: {
252
+ SELECTOR: result.selector,
253
+ ...(result.selectorType && { SELECTOR_TYPE: result.selectorType }),
254
+ TEXT: haveTextMatch[1],
255
+ },
256
+ };
257
+ }
258
+ }
259
+ // expect(page).toHaveURL('url') or toHaveURL(/regex/)
260
+ const haveURLMatch = trimmed.match(/expect\(page\)\.toHaveURL\(['"`]([^'"`]+)['"`]\)/);
261
+ if (haveURLMatch) {
262
+ return {
263
+ id: generateStepId(),
264
+ type: 'web_assert_url_contains',
265
+ params: {
266
+ TEXT: haveURLMatch[1],
267
+ },
268
+ };
269
+ }
270
+ // expect(page).toHaveTitle('title')
271
+ const haveTitleMatch = trimmed.match(/expect\(page\)\.toHaveTitle\(['"`]([^'"`]+)['"`]\)/);
272
+ if (haveTitleMatch) {
273
+ return {
274
+ id: generateStepId(),
275
+ type: 'web_assert_title_contains',
276
+ params: {
277
+ TEXT: haveTitleMatch[1],
278
+ },
279
+ };
280
+ }
281
+ // expect(page.locator(...)).toBeEnabled()
282
+ if (trimmed.includes('.toBeEnabled()')) {
283
+ const result = extractExpectSelector(trimmed, testIdAttribute);
284
+ if (result) {
285
+ return {
286
+ id: generateStepId(),
287
+ type: 'web_assert_enabled',
288
+ params: {
289
+ SELECTOR: result.selector,
290
+ ...(result.selectorType && { SELECTOR_TYPE: result.selectorType }),
291
+ },
292
+ };
293
+ }
294
+ }
295
+ // expect(page.locator(...)).toBeChecked()
296
+ if (trimmed.includes('.toBeChecked()')) {
297
+ const result = extractExpectSelector(trimmed, testIdAttribute);
298
+ if (result) {
299
+ return {
300
+ id: generateStepId(),
301
+ type: 'web_assert_checked',
302
+ params: {
303
+ SELECTOR: result.selector,
304
+ ...(result.selectorType && { SELECTOR_TYPE: result.selectorType }),
305
+ CHECKED: true,
306
+ },
307
+ };
308
+ }
309
+ }
310
+ // expect(page.locator(...)).not.toBeChecked()
311
+ if (trimmed.includes('.not.toBeChecked()')) {
312
+ const result = extractExpectSelector(trimmed, testIdAttribute);
313
+ if (result) {
314
+ return {
315
+ id: generateStepId(),
316
+ type: 'web_assert_checked',
317
+ params: {
318
+ SELECTOR: result.selector,
319
+ ...(result.selectorType && { SELECTOR_TYPE: result.selectorType }),
320
+ CHECKED: false,
321
+ },
322
+ };
323
+ }
324
+ }
325
+ // ===== ACTIONS =====
326
+ // page.goto('url')
327
+ const gotoMatch = trimmed.match(/await\s+page\.goto\(['"`]([^'"`]+)['"`]/);
328
+ if (gotoMatch) {
329
+ return {
330
+ id: generateStepId(),
331
+ type: 'web_navigate',
332
+ params: {
333
+ URL: gotoMatch[1],
334
+ WAIT_UNTIL: 'load',
335
+ },
336
+ };
337
+ }
338
+ // page.locator(...).click() or page.getBy*(...).click()
339
+ if (trimmed.includes('.click(') && !trimmed.includes('expect(')) {
340
+ const selectorPart = trimmed.replace(/await\s+/, '').replace(/\.click\([^)]*\).*$/, '');
341
+ const result = extractSelector(selectorPart, testIdAttribute);
342
+ return {
343
+ id: generateStepId(),
344
+ type: 'web_click',
345
+ params: {
346
+ SELECTOR: result.selector,
347
+ ...(result.selectorType && { SELECTOR_TYPE: result.selectorType }),
348
+ TIMEOUT: 30000,
349
+ },
350
+ };
351
+ }
352
+ // page.locator(...).dblclick()
353
+ if (trimmed.includes('.dblclick(')) {
354
+ const selectorPart = trimmed.replace(/await\s+/, '').replace(/\.dblclick\([^)]*\).*$/, '');
355
+ const result = extractSelector(selectorPart, testIdAttribute);
356
+ return {
357
+ id: generateStepId(),
358
+ type: 'web_click',
359
+ params: {
360
+ SELECTOR: result.selector,
361
+ ...(result.selectorType && { SELECTOR_TYPE: result.selectorType }),
362
+ TIMEOUT: 30000,
363
+ // Note: TestBlocks may not support double-click, this is a single click fallback
364
+ },
365
+ };
366
+ }
367
+ // page.locator(...).fill('value') or page.getBy*(...).fill('value')
368
+ const fillMatch = trimmed.match(/\.fill\(['"`]([^'"`]*)['"`]\)/);
369
+ if (fillMatch) {
370
+ const selectorPart = trimmed.replace(/await\s+/, '').replace(/\.fill\([^)]*\).*$/, '');
371
+ const result = extractSelector(selectorPart, testIdAttribute);
372
+ return {
373
+ id: generateStepId(),
374
+ type: 'web_fill',
375
+ params: {
376
+ SELECTOR: result.selector,
377
+ ...(result.selectorType && { SELECTOR_TYPE: result.selectorType }),
378
+ VALUE: fillMatch[1],
379
+ },
380
+ };
381
+ }
382
+ // page.locator(...).type('text') or .pressSequentially('text')
383
+ const typeMatch = trimmed.match(/\.(type|pressSequentially)\(['"`]([^'"`]*)['"`]/);
384
+ if (typeMatch) {
385
+ const selectorPart = trimmed.replace(/await\s+/, '').replace(/\.(type|pressSequentially)\([^)]*\).*$/, '');
386
+ const result = extractSelector(selectorPart, testIdAttribute);
387
+ return {
388
+ id: generateStepId(),
389
+ type: 'web_type',
390
+ params: {
391
+ SELECTOR: result.selector,
392
+ ...(result.selectorType && { SELECTOR_TYPE: result.selectorType }),
393
+ TEXT: typeMatch[2],
394
+ DELAY: 50,
395
+ },
396
+ };
397
+ }
398
+ // page.locator(...).clear()
399
+ if (trimmed.includes('.clear()')) {
400
+ const selectorPart = trimmed.replace(/await\s+/, '').replace(/\.clear\(\).*$/, '');
401
+ const result = extractSelector(selectorPart, testIdAttribute);
402
+ return {
403
+ id: generateStepId(),
404
+ type: 'web_fill',
405
+ params: {
406
+ SELECTOR: result.selector,
407
+ ...(result.selectorType && { SELECTOR_TYPE: result.selectorType }),
408
+ VALUE: '',
409
+ },
410
+ };
411
+ }
412
+ // page.locator(...).check()
413
+ if (trimmed.includes('.check()') && !trimmed.includes('toBeChecked')) {
414
+ const selectorPart = trimmed.replace(/await\s+/, '').replace(/\.check\(\).*$/, '');
415
+ const result = extractSelector(selectorPart, testIdAttribute);
416
+ return {
417
+ id: generateStepId(),
418
+ type: 'web_checkbox',
419
+ params: {
420
+ SELECTOR: result.selector,
421
+ ...(result.selectorType && { SELECTOR_TYPE: result.selectorType }),
422
+ ACTION: 'check',
423
+ },
424
+ };
425
+ }
426
+ // page.locator(...).uncheck()
427
+ if (trimmed.includes('.uncheck()')) {
428
+ const selectorPart = trimmed.replace(/await\s+/, '').replace(/\.uncheck\(\).*$/, '');
429
+ const result = extractSelector(selectorPart, testIdAttribute);
430
+ return {
431
+ id: generateStepId(),
432
+ type: 'web_checkbox',
433
+ params: {
434
+ SELECTOR: result.selector,
435
+ ...(result.selectorType && { SELECTOR_TYPE: result.selectorType }),
436
+ ACTION: 'uncheck',
437
+ },
438
+ };
439
+ }
440
+ // page.locator(...).hover()
441
+ if (trimmed.includes('.hover()')) {
442
+ const selectorPart = trimmed.replace(/await\s+/, '').replace(/\.hover\(\).*$/, '');
443
+ const result = extractSelector(selectorPart, testIdAttribute);
444
+ return {
445
+ id: generateStepId(),
446
+ type: 'web_hover',
447
+ params: {
448
+ SELECTOR: result.selector,
449
+ ...(result.selectorType && { SELECTOR_TYPE: result.selectorType }),
450
+ },
451
+ };
452
+ }
453
+ // page.locator(...).selectOption('value') or selectOption(['value1', 'value2'])
454
+ const selectMatch = trimmed.match(/\.selectOption\(['"`]([^'"`]*)['"`]\)/);
455
+ if (selectMatch) {
456
+ const selectorPart = trimmed.replace(/await\s+/, '').replace(/\.selectOption\([^)]*\).*$/, '');
457
+ const result = extractSelector(selectorPart, testIdAttribute);
458
+ return {
459
+ id: generateStepId(),
460
+ type: 'web_select',
461
+ params: {
462
+ SELECTOR: result.selector,
463
+ ...(result.selectorType && { SELECTOR_TYPE: result.selectorType }),
464
+ VALUE: selectMatch[1],
465
+ },
466
+ };
467
+ }
468
+ // page.locator(...).press('key') or page.keyboard.press('key')
469
+ const pressMatch = trimmed.match(/\.press\(['"`]([^'"`]+)['"`]\)/);
470
+ if (pressMatch) {
471
+ if (trimmed.includes('.keyboard.')) {
472
+ return {
473
+ id: generateStepId(),
474
+ type: 'web_press_key',
475
+ params: {
476
+ KEY: pressMatch[1],
477
+ },
478
+ };
479
+ }
480
+ else {
481
+ const selectorPart = trimmed.replace(/await\s+/, '').replace(/\.press\([^)]*\).*$/, '');
482
+ const result = extractSelector(selectorPart, testIdAttribute);
483
+ return {
484
+ id: generateStepId(),
485
+ type: 'web_press_key',
486
+ params: {
487
+ SELECTOR: result.selector,
488
+ ...(result.selectorType && { SELECTOR_TYPE: result.selectorType }),
489
+ KEY: pressMatch[1],
490
+ },
491
+ };
492
+ }
493
+ }
494
+ // page.locator(...).focus()
495
+ if (trimmed.includes('.focus()')) {
496
+ const selectorPart = trimmed.replace(/await\s+/, '').replace(/\.focus\(\).*$/, '');
497
+ const result = extractSelector(selectorPart, testIdAttribute);
498
+ // Focus isn't a direct TestBlocks action, use click instead
499
+ return {
500
+ id: generateStepId(),
501
+ type: 'web_click',
502
+ params: {
503
+ SELECTOR: result.selector,
504
+ ...(result.selectorType && { SELECTOR_TYPE: result.selectorType }),
505
+ TIMEOUT: 30000,
506
+ },
507
+ };
508
+ }
509
+ // page.waitForSelector('selector') or page.locator(...).waitFor()
510
+ if (trimmed.includes('.waitFor(') || trimmed.includes('.waitForSelector(')) {
511
+ const waitForSelectorMatch = trimmed.match(/\.waitForSelector\(['"`]([^'"`]+)['"`]/);
512
+ if (waitForSelectorMatch) {
513
+ return {
514
+ id: generateStepId(),
515
+ type: 'web_wait_for_element',
516
+ params: {
517
+ SELECTOR: waitForSelectorMatch[1],
518
+ SELECTOR_TYPE: 'css',
519
+ STATE: 'visible',
520
+ TIMEOUT: 30000,
521
+ },
522
+ };
523
+ }
524
+ // locator().waitFor()
525
+ const selectorPart = trimmed.replace(/await\s+/, '').replace(/\.waitFor\([^)]*\).*$/, '');
526
+ const result = extractSelector(selectorPart, testIdAttribute);
527
+ if (result.selector !== selectorPart) {
528
+ return {
529
+ id: generateStepId(),
530
+ type: 'web_wait_for_element',
531
+ params: {
532
+ SELECTOR: result.selector,
533
+ ...(result.selectorType && { SELECTOR_TYPE: result.selectorType }),
534
+ STATE: 'visible',
535
+ TIMEOUT: 30000,
536
+ },
537
+ };
538
+ }
539
+ }
540
+ // page.waitForTimeout(ms)
541
+ const waitTimeoutMatch = trimmed.match(/\.waitForTimeout\((\d+)\)/);
542
+ if (waitTimeoutMatch) {
543
+ return {
544
+ id: generateStepId(),
545
+ type: 'web_wait',
546
+ params: {
547
+ DURATION: parseInt(waitTimeoutMatch[1], 10),
548
+ },
549
+ };
550
+ }
551
+ // page.waitForURL('url')
552
+ const waitForURLMatch = trimmed.match(/\.waitForURL\(['"`]([^'"`]+)['"`]/);
553
+ if (waitForURLMatch) {
554
+ return {
555
+ id: generateStepId(),
556
+ type: 'web_wait_for_url',
557
+ params: {
558
+ URL: waitForURLMatch[1],
559
+ TIMEOUT: 30000,
560
+ },
561
+ };
562
+ }
563
+ // page.screenshot()
564
+ if (trimmed.includes('.screenshot(')) {
565
+ const pathMatch = trimmed.match(/path:\s*['"`]([^'"`]+)['"`]/);
566
+ return {
567
+ id: generateStepId(),
568
+ type: 'web_screenshot',
569
+ params: {
570
+ PATH: pathMatch ? pathMatch[1] : 'screenshot.png',
571
+ },
572
+ };
573
+ }
574
+ // Unrecognized line - skip it
575
+ return null;
576
+ }
577
+ /**
578
+ * Parse Playwright-generated code and convert to TestBlocks TestStep array
579
+ * @param code - The Playwright-generated code
580
+ * @param testIdAttribute - The attribute used for test IDs (default: 'data-testid')
581
+ */
582
+ function parsePlaywrightCode(code, testIdAttribute = 'data-testid') {
583
+ const lines = code.split('\n');
584
+ const steps = [];
585
+ for (const line of lines) {
586
+ const step = parseLine(line, testIdAttribute);
587
+ if (step) {
588
+ steps.push(step);
589
+ }
590
+ }
591
+ return steps;
592
+ }
593
+ /**
594
+ * Check if the code looks like valid Playwright test code
595
+ */
596
+ function isValidPlaywrightCode(code) {
597
+ return code.includes('page.') || code.includes('await ') || code.includes('expect(');
598
+ }
@@ -0,0 +1,37 @@
1
+ import { TestFile, TestCase, TestResult, ExecutionContext, Plugin, ProcedureDefinition, TestDataSet } from '../core';
2
+ export interface ExecutorOptions {
3
+ headless?: boolean;
4
+ timeout?: number;
5
+ baseUrl?: string;
6
+ variables?: Record<string, unknown>;
7
+ plugins?: Plugin[];
8
+ testIdAttribute?: string;
9
+ baseDir?: string;
10
+ }
11
+ export declare class TestExecutor {
12
+ private options;
13
+ private browser;
14
+ private context;
15
+ private page;
16
+ private plugins;
17
+ constructor(options?: ExecutorOptions);
18
+ initialize(): Promise<void>;
19
+ cleanup(): Promise<void>;
20
+ runTestFile(testFile: TestFile): Promise<TestResult[]>;
21
+ private runLifecycleSteps;
22
+ /**
23
+ * Public method to register custom blocks from procedures
24
+ */
25
+ registerProcedures(procedures: Record<string, ProcedureDefinition>): void;
26
+ private registerCustomBlocksFromProcedures;
27
+ runTest(test: TestCase, fileVariables?: Record<string, unknown>, sharedContext?: ExecutionContext): Promise<TestResult>;
28
+ runTestWithData(test: TestCase, fileVariables: Record<string, unknown> | undefined, dataSet: TestDataSet, dataIndex: number, sharedContext?: ExecutionContext): Promise<TestResult>;
29
+ private extractStepsFromBlocklyState;
30
+ private blocksToSteps;
31
+ private blockToStep;
32
+ private runStep;
33
+ private resolveParams;
34
+ private loadDataFromFile;
35
+ private resolveVariableDefaults;
36
+ private createLogger;
37
+ }