@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.
Files changed (41) hide show
  1. package/dist/config-emit.d.ts +41 -0
  2. package/dist/config-emit.d.ts.map +1 -0
  3. package/dist/config-emit.js +191 -0
  4. package/dist/config-emit.js.map +1 -0
  5. package/dist/index.d.ts +16 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +15 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/locator-emit.d.ts +76 -0
  10. package/dist/locator-emit.d.ts.map +1 -0
  11. package/dist/locator-emit.js +239 -0
  12. package/dist/locator-emit.js.map +1 -0
  13. package/dist/locator-emit.test.d.ts +6 -0
  14. package/dist/locator-emit.test.d.ts.map +1 -0
  15. package/dist/locator-emit.test.js +425 -0
  16. package/dist/locator-emit.test.js.map +1 -0
  17. package/dist/playwright-ts.d.ts +97 -0
  18. package/dist/playwright-ts.d.ts.map +1 -0
  19. package/dist/playwright-ts.js +373 -0
  20. package/dist/playwright-ts.js.map +1 -0
  21. package/dist/playwright-ts.test.d.ts +6 -0
  22. package/dist/playwright-ts.test.d.ts.map +1 -0
  23. package/dist/playwright-ts.test.js +548 -0
  24. package/dist/playwright-ts.test.js.map +1 -0
  25. package/dist/visual-checks.d.ts +76 -0
  26. package/dist/visual-checks.d.ts.map +1 -0
  27. package/dist/visual-checks.js +195 -0
  28. package/dist/visual-checks.js.map +1 -0
  29. package/dist/visual-checks.test.d.ts +6 -0
  30. package/dist/visual-checks.test.d.ts.map +1 -0
  31. package/dist/visual-checks.test.js +188 -0
  32. package/dist/visual-checks.test.js.map +1 -0
  33. package/package.json +34 -0
  34. package/src/config-emit.ts +253 -0
  35. package/src/index.ts +57 -0
  36. package/src/locator-emit.test.ts +533 -0
  37. package/src/locator-emit.ts +310 -0
  38. package/src/playwright-ts.test.ts +704 -0
  39. package/src/playwright-ts.ts +519 -0
  40. package/src/visual-checks.test.ts +232 -0
  41. 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
+ }