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,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
|
+
}
|