@browserflow-ai/generator 0.0.6
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/dist/config-emit.d.ts +41 -0
- package/dist/config-emit.d.ts.map +1 -0
- package/dist/config-emit.js +191 -0
- package/dist/config-emit.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/locator-emit.d.ts +76 -0
- package/dist/locator-emit.d.ts.map +1 -0
- package/dist/locator-emit.js +239 -0
- package/dist/locator-emit.js.map +1 -0
- package/dist/locator-emit.test.d.ts +6 -0
- package/dist/locator-emit.test.d.ts.map +1 -0
- package/dist/locator-emit.test.js +425 -0
- package/dist/locator-emit.test.js.map +1 -0
- package/dist/playwright-ts.d.ts +97 -0
- package/dist/playwright-ts.d.ts.map +1 -0
- package/dist/playwright-ts.js +373 -0
- package/dist/playwright-ts.js.map +1 -0
- package/dist/playwright-ts.test.d.ts +6 -0
- package/dist/playwright-ts.test.d.ts.map +1 -0
- package/dist/playwright-ts.test.js +548 -0
- package/dist/playwright-ts.test.js.map +1 -0
- package/dist/visual-checks.d.ts +76 -0
- package/dist/visual-checks.d.ts.map +1 -0
- package/dist/visual-checks.js +195 -0
- package/dist/visual-checks.js.map +1 -0
- package/dist/visual-checks.test.d.ts +6 -0
- package/dist/visual-checks.test.d.ts.map +1 -0
- package/dist/visual-checks.test.js +188 -0
- package/dist/visual-checks.test.js.map +1 -0
- package/package.json +34 -0
- package/src/config-emit.ts +253 -0
- package/src/index.ts +57 -0
- package/src/locator-emit.test.ts +533 -0
- package/src/locator-emit.ts +310 -0
- package/src/playwright-ts.test.ts +704 -0
- package/src/playwright-ts.ts +519 -0
- package/src/visual-checks.test.ts +232 -0
- package/src/visual-checks.ts +294 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* locator-emit.ts
|
|
3
|
+
* Converts LocatorObject instances to Playwright locator code strings.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { LegacyLocatorObject, LocatorMethod } from '@browserflow-ai/core';
|
|
7
|
+
|
|
8
|
+
// Use the legacy interface for backwards compatibility
|
|
9
|
+
type LocatorObject = LegacyLocatorObject;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Options for code generation.
|
|
13
|
+
*/
|
|
14
|
+
export interface LocatorEmitOptions {
|
|
15
|
+
/** Variable name for the page object (default: "page") */
|
|
16
|
+
pageVar?: string;
|
|
17
|
+
/** Whether to chain .first() for potentially multiple matches */
|
|
18
|
+
chainFirst?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Index-based selection for multiple matches:
|
|
21
|
+
* - 0: .first()
|
|
22
|
+
* - -1: .last()
|
|
23
|
+
* - other: .nth(n)
|
|
24
|
+
* Takes precedence over chainFirst when specified.
|
|
25
|
+
*/
|
|
26
|
+
nth?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Parent locator for scoping. The generated code will be
|
|
29
|
+
* chained from this parent locator.
|
|
30
|
+
* Example: within: { method: 'getByTestId', args: { testId: 'form' } }
|
|
31
|
+
* Results in: page.getByTestId('form').getByRole('button')
|
|
32
|
+
*/
|
|
33
|
+
within?: LocatorObject;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Escapes a string for use in generated TypeScript code.
|
|
38
|
+
*/
|
|
39
|
+
export function escapeString(str: string): string {
|
|
40
|
+
return str
|
|
41
|
+
.replace(/\\/g, '\\\\')
|
|
42
|
+
.replace(/'/g, "\\'")
|
|
43
|
+
.replace(/\n/g, '\\n')
|
|
44
|
+
.replace(/\r/g, '\\r')
|
|
45
|
+
.replace(/\t/g, '\\t');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Formats a value as a TypeScript literal.
|
|
50
|
+
*/
|
|
51
|
+
function formatValue(value: unknown): string {
|
|
52
|
+
if (typeof value === 'string') {
|
|
53
|
+
return `'${escapeString(value)}'`;
|
|
54
|
+
}
|
|
55
|
+
if (value instanceof RegExp) {
|
|
56
|
+
return value.toString();
|
|
57
|
+
}
|
|
58
|
+
if (typeof value === 'boolean' || typeof value === 'number') {
|
|
59
|
+
return String(value);
|
|
60
|
+
}
|
|
61
|
+
if (value === null || value === undefined) {
|
|
62
|
+
return 'undefined';
|
|
63
|
+
}
|
|
64
|
+
return JSON.stringify(value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Generates Playwright locator code from a LocatorObject.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* // getByRole example
|
|
72
|
+
* generateLocatorCode({ method: 'getByRole', args: { role: 'button', name: 'Submit' } })
|
|
73
|
+
* // Returns: "page.getByRole('button', { name: 'Submit' })"
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* // CSS selector example
|
|
77
|
+
* generateLocatorCode({ selector: 'button.primary' })
|
|
78
|
+
* // Returns: "page.locator('button.primary')"
|
|
79
|
+
*/
|
|
80
|
+
export function generateLocatorCode(
|
|
81
|
+
locator: LocatorObject,
|
|
82
|
+
options: LocatorEmitOptions = {}
|
|
83
|
+
): string {
|
|
84
|
+
const { pageVar = 'page', chainFirst = false, nth, within } = options;
|
|
85
|
+
|
|
86
|
+
// Start with the base variable or parent locator chain
|
|
87
|
+
let baseVar = pageVar;
|
|
88
|
+
if (within) {
|
|
89
|
+
// Generate code for the parent locator (without page var suffix methods)
|
|
90
|
+
baseVar = generateLocatorCode(within, { pageVar });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let code: string;
|
|
94
|
+
|
|
95
|
+
if (locator.method && locator.args) {
|
|
96
|
+
code = generateMethodLocator(locator.method, locator.args, baseVar);
|
|
97
|
+
} else if (locator.selector) {
|
|
98
|
+
code = `${baseVar}.locator('${escapeString(locator.selector)}')`;
|
|
99
|
+
} else if (locator.ref) {
|
|
100
|
+
// Element refs need to be resolved - use a placeholder selector
|
|
101
|
+
// In practice, the exploration should have resolved this to a selector
|
|
102
|
+
code = `${baseVar}.locator('[data-ref="${escapeString(locator.ref)}"]')`;
|
|
103
|
+
} else {
|
|
104
|
+
throw new Error('LocatorObject must have either method+args, selector, or ref');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Apply nth handling - takes precedence over chainFirst
|
|
108
|
+
if (nth !== undefined) {
|
|
109
|
+
if (nth === 0) {
|
|
110
|
+
code += '.first()';
|
|
111
|
+
} else if (nth === -1) {
|
|
112
|
+
code += '.last()';
|
|
113
|
+
} else {
|
|
114
|
+
code += `.nth(${nth})`;
|
|
115
|
+
}
|
|
116
|
+
} else if (chainFirst) {
|
|
117
|
+
code += '.first()';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return code;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Generates code for Playwright locator methods (getByRole, getByText, etc.)
|
|
125
|
+
*/
|
|
126
|
+
function generateMethodLocator(
|
|
127
|
+
method: LocatorMethod,
|
|
128
|
+
args: Record<string, unknown>,
|
|
129
|
+
pageVar: string
|
|
130
|
+
): string {
|
|
131
|
+
switch (method) {
|
|
132
|
+
case 'getByRole': {
|
|
133
|
+
const { role, ...options } = args;
|
|
134
|
+
if (!role) {
|
|
135
|
+
throw new Error('getByRole requires a role argument');
|
|
136
|
+
}
|
|
137
|
+
const escapedRole = escapeString(String(role));
|
|
138
|
+
const optionsStr = formatOptions(options);
|
|
139
|
+
if (optionsStr) {
|
|
140
|
+
return `${pageVar}.getByRole('${escapedRole}', ${optionsStr})`;
|
|
141
|
+
}
|
|
142
|
+
return `${pageVar}.getByRole('${escapedRole}')`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
case 'getByText': {
|
|
146
|
+
const { text, exact } = args;
|
|
147
|
+
if (text === undefined) {
|
|
148
|
+
throw new Error('getByText requires a text argument');
|
|
149
|
+
}
|
|
150
|
+
const textValue = formatValue(text);
|
|
151
|
+
if (exact !== undefined) {
|
|
152
|
+
return `${pageVar}.getByText(${textValue}, { exact: ${exact} })`;
|
|
153
|
+
}
|
|
154
|
+
return `${pageVar}.getByText(${textValue})`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
case 'getByLabel': {
|
|
158
|
+
const { text, exact } = args;
|
|
159
|
+
if (text === undefined) {
|
|
160
|
+
throw new Error('getByLabel requires a text argument');
|
|
161
|
+
}
|
|
162
|
+
const textValue = formatValue(text);
|
|
163
|
+
if (exact !== undefined) {
|
|
164
|
+
return `${pageVar}.getByLabel(${textValue}, { exact: ${exact} })`;
|
|
165
|
+
}
|
|
166
|
+
return `${pageVar}.getByLabel(${textValue})`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
case 'getByPlaceholder': {
|
|
170
|
+
const { text, exact } = args;
|
|
171
|
+
if (text === undefined) {
|
|
172
|
+
throw new Error('getByPlaceholder requires a text argument');
|
|
173
|
+
}
|
|
174
|
+
const textValue = formatValue(text);
|
|
175
|
+
if (exact !== undefined) {
|
|
176
|
+
return `${pageVar}.getByPlaceholder(${textValue}, { exact: ${exact} })`;
|
|
177
|
+
}
|
|
178
|
+
return `${pageVar}.getByPlaceholder(${textValue})`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
case 'getByTestId': {
|
|
182
|
+
const { testId } = args;
|
|
183
|
+
if (testId === undefined) {
|
|
184
|
+
throw new Error('getByTestId requires a testId argument');
|
|
185
|
+
}
|
|
186
|
+
return `${pageVar}.getByTestId('${escapeString(String(testId))}')`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
case 'getByAltText': {
|
|
190
|
+
const { text, exact } = args;
|
|
191
|
+
if (text === undefined) {
|
|
192
|
+
throw new Error('getByAltText requires a text argument');
|
|
193
|
+
}
|
|
194
|
+
const textValue = formatValue(text);
|
|
195
|
+
if (exact !== undefined) {
|
|
196
|
+
return `${pageVar}.getByAltText(${textValue}, { exact: ${exact} })`;
|
|
197
|
+
}
|
|
198
|
+
return `${pageVar}.getByAltText(${textValue})`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
case 'getByTitle': {
|
|
202
|
+
const { text, exact } = args;
|
|
203
|
+
if (text === undefined) {
|
|
204
|
+
throw new Error('getByTitle requires a text argument');
|
|
205
|
+
}
|
|
206
|
+
const textValue = formatValue(text);
|
|
207
|
+
if (exact !== undefined) {
|
|
208
|
+
return `${pageVar}.getByTitle(${textValue}, { exact: ${exact} })`;
|
|
209
|
+
}
|
|
210
|
+
return `${pageVar}.getByTitle(${textValue})`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
case 'locator': {
|
|
214
|
+
const { selector } = args;
|
|
215
|
+
if (selector === undefined) {
|
|
216
|
+
throw new Error('locator requires a selector argument');
|
|
217
|
+
}
|
|
218
|
+
return `${pageVar}.locator('${escapeString(String(selector))}')`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
default:
|
|
222
|
+
throw new Error(`Unknown locator method: ${method}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Formats an options object for code generation.
|
|
228
|
+
*/
|
|
229
|
+
function formatOptions(options: Record<string, unknown>): string {
|
|
230
|
+
const entries = Object.entries(options).filter(
|
|
231
|
+
([_, v]) => v !== undefined && v !== null
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
if (entries.length === 0) {
|
|
235
|
+
return '';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const parts = entries.map(([key, value]) => {
|
|
239
|
+
return `${key}: ${formatValue(value)}`;
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
return `{ ${parts.join(', ')} }`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Creates a LocatorObject from a CSS selector string.
|
|
247
|
+
*/
|
|
248
|
+
export function selectorToLocator(selector: string): LocatorObject {
|
|
249
|
+
return { selector };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Creates a LocatorObject for getByRole.
|
|
254
|
+
*/
|
|
255
|
+
export function roleLocator(
|
|
256
|
+
role: string,
|
|
257
|
+
options?: { name?: string; exact?: boolean }
|
|
258
|
+
): LocatorObject {
|
|
259
|
+
return {
|
|
260
|
+
method: 'getByRole',
|
|
261
|
+
args: { role, ...options },
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Creates a LocatorObject for getByText.
|
|
267
|
+
*/
|
|
268
|
+
export function textLocator(
|
|
269
|
+
text: string,
|
|
270
|
+
options?: { exact?: boolean }
|
|
271
|
+
): LocatorObject {
|
|
272
|
+
return {
|
|
273
|
+
method: 'getByText',
|
|
274
|
+
args: { text, ...options },
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Creates a LocatorObject for getByTestId.
|
|
280
|
+
*/
|
|
281
|
+
export function testIdLocator(testId: string): LocatorObject {
|
|
282
|
+
return {
|
|
283
|
+
method: 'getByTestId',
|
|
284
|
+
args: { testId },
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Resolves the best locator code from an exploration step.
|
|
290
|
+
* Prefers method-based locators over CSS selectors.
|
|
291
|
+
*/
|
|
292
|
+
export function resolveLocatorCode(
|
|
293
|
+
locator: LocatorObject | undefined,
|
|
294
|
+
fallbackSelector: string | undefined,
|
|
295
|
+
options: LocatorEmitOptions = {}
|
|
296
|
+
): string {
|
|
297
|
+
if (locator?.method && locator.args) {
|
|
298
|
+
return generateLocatorCode(locator, options);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (locator?.selector) {
|
|
302
|
+
return generateLocatorCode(locator, options);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (fallbackSelector) {
|
|
306
|
+
return generateLocatorCode({ selector: fallbackSelector }, options);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
throw new Error('No locator or selector available');
|
|
310
|
+
}
|