@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,519 @@
1
+ /**
2
+ * playwright-ts.ts
3
+ * Main TypeScript test generator for Playwright.
4
+ */
5
+
6
+ import type {
7
+ ExplorationLockfile,
8
+ ExplorationStep,
9
+ GeneratedTest,
10
+ ReviewData,
11
+ LegacySpecStep,
12
+ } from '@browserflow-ai/core';
13
+ import Handlebars from 'handlebars';
14
+ import { resolveLocatorCode, escapeString } from './locator-emit.js';
15
+ import {
16
+ generateScreenshotAssertion,
17
+ generateWaitForAnimations,
18
+ generateMaskSetupCode,
19
+ } from './visual-checks.js';
20
+
21
+ /**
22
+ * Options for test generation.
23
+ */
24
+ export interface TestGeneratorOptions {
25
+ /** Include baseline screenshot comparisons */
26
+ includeVisualChecks?: boolean;
27
+ /** Directory for baseline screenshots */
28
+ baselinesDir?: string;
29
+ /** Include step comments */
30
+ includeComments?: boolean;
31
+ /** Custom test template */
32
+ template?: string;
33
+ /** Test timeout in ms */
34
+ timeout?: number;
35
+ }
36
+
37
+ /**
38
+ * Default test file template.
39
+ */
40
+ const DEFAULT_TEST_TEMPLATE = `import { test, expect } from '@playwright/test';
41
+
42
+ /**
43
+ * BrowserFlow Generated Test: {{specName}}
44
+ * ═══════════════════════════════════════════════════════════════════════════
45
+ * Spec: {{specPath}}
46
+ * Exploration: {{explorationId}}
47
+ * {{#if reviewer}}Approved by: {{reviewer}} @ {{reviewedAt}}{{/if}}
48
+ * Generated: {{generatedAt}}
49
+ *
50
+ * This test was generated from an approved exploration. Do not edit manually.
51
+ * To update, re-run exploration and get new approval.
52
+ * ═══════════════════════════════════════════════════════════════════════════
53
+ */
54
+
55
+ test.describe('{{specName}}', () => {
56
+ test('{{testDescription}}', async ({ page }) => {
57
+ {{#each steps}}
58
+ {{{code}}}
59
+
60
+ {{/each}}
61
+ {{#if outcomeChecks}}
62
+ // Final outcome verifications
63
+ {{#each outcomeChecks}}
64
+ {{{code}}}
65
+ {{/each}}
66
+ {{/if}}
67
+ });
68
+ });
69
+ `;
70
+
71
+ /**
72
+ * PlaywrightGenerator - Converts exploration lockfiles to Playwright tests.
73
+ */
74
+ export class PlaywrightGenerator {
75
+ private readonly options: TestGeneratorOptions;
76
+ private readonly template: HandlebarsTemplateDelegate;
77
+
78
+ constructor(options: TestGeneratorOptions = {}) {
79
+ this.options = {
80
+ includeVisualChecks: true,
81
+ includeComments: true,
82
+ timeout: 30000,
83
+ ...options,
84
+ };
85
+
86
+ const templateSource = options.template ?? DEFAULT_TEST_TEMPLATE;
87
+ this.template = Handlebars.compile(templateSource);
88
+ }
89
+
90
+ /**
91
+ * Generates a Playwright test file from an exploration lockfile.
92
+ */
93
+ generate(
94
+ lockfile: ExplorationLockfile,
95
+ review?: ReviewData
96
+ ): GeneratedTest {
97
+ const steps = this.generateSteps(lockfile);
98
+ const outcomeChecks = this.generateOutcomeChecks(lockfile);
99
+
100
+ const content = this.template({
101
+ specName: lockfile.spec,
102
+ specPath: lockfile.spec_path,
103
+ explorationId: lockfile.exploration_id,
104
+ reviewer: review?.reviewer,
105
+ reviewedAt: review?.submitted_at,
106
+ generatedAt: new Date().toISOString(),
107
+ testDescription: this.generateTestDescription(lockfile),
108
+ steps,
109
+ outcomeChecks: outcomeChecks.length > 0 ? outcomeChecks : undefined,
110
+ });
111
+
112
+ return {
113
+ path: `e2e/tests/${lockfile.spec}.spec.ts`,
114
+ content,
115
+ specName: lockfile.spec,
116
+ explorationId: lockfile.exploration_id,
117
+ generatedAt: new Date().toISOString(),
118
+ };
119
+ }
120
+
121
+ /**
122
+ * Generates a test description from the spec name.
123
+ */
124
+ private generateTestDescription(lockfile: ExplorationLockfile): string {
125
+ // Convert kebab-case to readable description
126
+ return lockfile.spec
127
+ .split('-')
128
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
129
+ .join(' ');
130
+ }
131
+
132
+ /**
133
+ * Generates code for all steps.
134
+ */
135
+ private generateSteps(
136
+ lockfile: ExplorationLockfile
137
+ ): Array<{ code: string }> {
138
+ return lockfile.steps.map((step, index) => {
139
+ const code = this.generateStepCode(step, index);
140
+ return { code };
141
+ });
142
+ }
143
+
144
+ /**
145
+ * Generates code for a single step wrapped in test.step().
146
+ */
147
+ private generateStepCode(step: ExplorationStep, index: number): string {
148
+ const lines: string[] = [];
149
+ const indent = ' '; // 4 spaces for inside test block
150
+ const innerIndent = ' '; // 6 spaces for inside test.step block
151
+
152
+ // Determine step name: use description if available, otherwise step_{index}
153
+ const stepName = step.spec_action.description ?? `step_${index}`;
154
+
155
+ // Generate action-specific code
156
+ const actionCode = this.generateActionCode(step);
157
+
158
+ // Start test.step() wrapper
159
+ lines.push(`${indent}await test.step('${escapeString(stepName)}', async () => {`);
160
+
161
+ // Add step comment inside the test.step block
162
+ if (this.options.includeComments) {
163
+ lines.push(
164
+ `${innerIndent}// ─────────────────────────────────────────────────────────────────────────`
165
+ );
166
+ lines.push(
167
+ `${innerIndent}// Step ${index}: ${this.describeAction(step.spec_action)}`
168
+ );
169
+ if (step.execution.method) {
170
+ lines.push(`${innerIndent}// Found via: ${step.execution.method}`);
171
+ }
172
+ lines.push(
173
+ `${innerIndent}// ─────────────────────────────────────────────────────────────────────────`
174
+ );
175
+ }
176
+
177
+ // Add action code inside the test.step block
178
+ lines.push(...actionCode.map((line) => `${innerIndent}${line}`));
179
+
180
+ // Close test.step() wrapper
181
+ lines.push(`${indent}});`);
182
+
183
+ return lines.join('\n');
184
+ }
185
+
186
+ /**
187
+ * Describes an action for comments.
188
+ */
189
+ private describeAction(action: LegacySpecStep): string {
190
+ switch (action.action) {
191
+ case 'click':
192
+ return `Click: ${action.query ?? action.selector ?? 'element'}`;
193
+ case 'navigate':
194
+ return `Navigate to: ${action.to}`;
195
+ case 'fill':
196
+ return `Fill: ${action.query ?? action.selector ?? 'input'}`;
197
+ case 'type':
198
+ return `Type: ${action.query ?? action.selector ?? 'input'}`;
199
+ case 'wait':
200
+ return `Wait for: ${action.for ?? 'condition'}`;
201
+ case 'screenshot':
202
+ return `Screenshot: ${action.name ?? 'unnamed'}`;
203
+ case 'verify_state':
204
+ return 'Verify state';
205
+ case 'select':
206
+ return `Select: ${action.option ?? 'option'}`;
207
+ case 'check':
208
+ return `Checkbox: ${action.checked ? 'check' : 'uncheck'}`;
209
+ default:
210
+ return action.action;
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Generates code for a specific action.
216
+ */
217
+ private generateActionCode(step: ExplorationStep): string[] {
218
+ const { spec_action: action, execution } = step;
219
+ const lines: string[] = [];
220
+
221
+ switch (action.action) {
222
+ case 'click':
223
+ lines.push(...this.generateClickCode(step));
224
+ break;
225
+
226
+ case 'navigate':
227
+ lines.push(...this.generateNavigateCode(action));
228
+ break;
229
+
230
+ case 'fill':
231
+ lines.push(...this.generateFillCode(step));
232
+ break;
233
+
234
+ case 'type':
235
+ lines.push(...this.generateTypeCode(step));
236
+ break;
237
+
238
+ case 'wait':
239
+ lines.push(...this.generateWaitCode(action, execution.duration_ms));
240
+ break;
241
+
242
+ case 'screenshot':
243
+ lines.push(...this.generateScreenshotCode(action));
244
+ break;
245
+
246
+ case 'verify_state':
247
+ lines.push(...this.generateVerifyCode(action));
248
+ break;
249
+
250
+ case 'select':
251
+ lines.push(...this.generateSelectCode(step));
252
+ break;
253
+
254
+ case 'check':
255
+ lines.push(...this.generateCheckCode(step));
256
+ break;
257
+
258
+ case 'back':
259
+ lines.push('await page.goBack();');
260
+ break;
261
+
262
+ case 'forward':
263
+ lines.push('await page.goForward();');
264
+ break;
265
+
266
+ case 'refresh':
267
+ lines.push('await page.reload();');
268
+ break;
269
+
270
+ default:
271
+ lines.push(`// TODO: Unsupported action: ${action.action}`);
272
+ }
273
+
274
+ return lines;
275
+ }
276
+
277
+ /**
278
+ * Generates click action code.
279
+ */
280
+ private generateClickCode(step: ExplorationStep): string[] {
281
+ const locatorCode = resolveLocatorCode(
282
+ step.execution.locator,
283
+ step.execution.selector_used
284
+ );
285
+ return [`await ${locatorCode}.click();`];
286
+ }
287
+
288
+ /**
289
+ * Generates navigate action code.
290
+ */
291
+ private generateNavigateCode(action: LegacySpecStep): string[] {
292
+ // Support both v2 (url) and legacy (to) field names
293
+ const url = (action as { url?: string }).url ?? action.to ?? '/';
294
+ return [`await page.goto('${escapeString(url)}');`];
295
+ }
296
+
297
+ /**
298
+ * Generates fill action code.
299
+ */
300
+ private generateFillCode(step: ExplorationStep): string[] {
301
+ const locatorCode = resolveLocatorCode(
302
+ step.execution.locator,
303
+ step.execution.selector_used
304
+ );
305
+ const value = step.execution.value_used ?? step.spec_action.value ?? '';
306
+ return [`await ${locatorCode}.fill('${escapeString(value)}');`];
307
+ }
308
+
309
+ /**
310
+ * Generates type action code.
311
+ */
312
+ private generateTypeCode(step: ExplorationStep): string[] {
313
+ const lines: string[] = [];
314
+ const locatorCode = resolveLocatorCode(
315
+ step.execution.locator,
316
+ step.execution.selector_used
317
+ );
318
+ const value = step.execution.value_used ?? step.spec_action.value ?? '';
319
+
320
+ lines.push(`await ${locatorCode}.pressSequentially('${escapeString(value)}');`);
321
+
322
+ if (step.spec_action.pressEnter) {
323
+ lines.push(`await ${locatorCode}.press('Enter');`);
324
+ }
325
+
326
+ return lines;
327
+ }
328
+
329
+ /**
330
+ * Generates wait action code.
331
+ */
332
+ private generateWaitCode(action: LegacySpecStep, actualDuration?: number): string[] {
333
+ const lines: string[] = [];
334
+ const timeout = action.timeout ?? this.options.timeout ?? 30000;
335
+
336
+ switch (action.for) {
337
+ case 'element':
338
+ if (action.selector) {
339
+ lines.push(
340
+ `await page.locator('${escapeString(action.selector)}').waitFor({ timeout: ${timeout} });`
341
+ );
342
+ }
343
+ break;
344
+
345
+ case 'text':
346
+ if (action.text) {
347
+ lines.push(
348
+ `await page.getByText('${escapeString(action.text)}').waitFor({ timeout: ${timeout} });`
349
+ );
350
+ }
351
+ break;
352
+
353
+ case 'url':
354
+ if (action.contains) {
355
+ lines.push(
356
+ `await page.waitForURL(url => url.href.includes('${escapeString(action.contains)}'), { timeout: ${timeout} });`
357
+ );
358
+ }
359
+ break;
360
+
361
+ case 'time':
362
+ const duration = action.duration ?? actualDuration ?? 1000;
363
+ lines.push(`await page.waitForTimeout(${duration});`);
364
+ break;
365
+
366
+ default:
367
+ lines.push(`await page.waitForLoadState('networkidle');`);
368
+ }
369
+
370
+ return lines;
371
+ }
372
+
373
+ /**
374
+ * Generates screenshot action code.
375
+ */
376
+ private generateScreenshotCode(action: LegacySpecStep): string[] {
377
+ const lines: string[] = [];
378
+ const name = action.name ?? 'screenshot';
379
+
380
+ if (this.options.includeVisualChecks) {
381
+ // Add wait for animations before screenshot
382
+ lines.push(generateWaitForAnimations());
383
+ lines.push('');
384
+
385
+ // Inject mask overlay elements if there are region-based masks
386
+ if (action.mask && action.mask.length > 0) {
387
+ const maskSetupCode = generateMaskSetupCode(action.mask);
388
+ if (maskSetupCode) {
389
+ lines.push(maskSetupCode);
390
+ lines.push('');
391
+ }
392
+ }
393
+
394
+ // Generate screenshot assertion
395
+ const assertion = generateScreenshotAssertion(
396
+ {
397
+ name,
398
+ mask: action.mask,
399
+ animations: 'disabled',
400
+ },
401
+ { includeComments: this.options.includeComments }
402
+ );
403
+ lines.push(assertion);
404
+ } else {
405
+ // Just capture screenshot without assertion
406
+ lines.push(`await page.screenshot({ path: 'screenshots/${escapeString(name)}.png' });`);
407
+ }
408
+
409
+ return lines;
410
+ }
411
+
412
+ /**
413
+ * Generates verify_state action code.
414
+ */
415
+ private generateVerifyCode(action: LegacySpecStep): string[] {
416
+ const lines: string[] = [];
417
+
418
+ for (const check of action.checks ?? []) {
419
+ if (check.element_visible) {
420
+ lines.push(
421
+ `await expect(page.locator('${escapeString(check.element_visible)}')).toBeVisible();`
422
+ );
423
+ }
424
+
425
+ if (check.element_not_visible) {
426
+ lines.push(
427
+ `await expect(page.locator('${escapeString(check.element_not_visible)}')).not.toBeVisible();`
428
+ );
429
+ }
430
+
431
+ if (check.text_contains) {
432
+ lines.push(
433
+ `await expect(page.getByText('${escapeString(check.text_contains)}')).toBeVisible();`
434
+ );
435
+ }
436
+
437
+ if (check.text_not_contains) {
438
+ lines.push(
439
+ `await expect(page.getByText('${escapeString(check.text_not_contains)}')).not.toBeVisible();`
440
+ );
441
+ }
442
+
443
+ if (check.url_contains) {
444
+ lines.push(
445
+ `await expect(page).toHaveURL(new RegExp('${escapeString(check.url_contains)}'));`
446
+ );
447
+ }
448
+
449
+ if (check.element_count) {
450
+ lines.push(
451
+ `await expect(page.locator('${escapeString(check.element_count.selector)}')).toHaveCount(${check.element_count.expected});`
452
+ );
453
+ }
454
+
455
+ if (check.attribute) {
456
+ lines.push(
457
+ `await expect(page.locator('${escapeString(check.attribute.selector)}')).toHaveAttribute('${escapeString(check.attribute.attribute)}', '${escapeString(check.attribute.equals)}');`
458
+ );
459
+ }
460
+ }
461
+
462
+ return lines;
463
+ }
464
+
465
+ /**
466
+ * Generates select action code.
467
+ */
468
+ private generateSelectCode(step: ExplorationStep): string[] {
469
+ const locatorCode = resolveLocatorCode(
470
+ step.execution.locator,
471
+ step.execution.selector_used
472
+ );
473
+ const option = step.spec_action.option ?? '';
474
+ return [`await ${locatorCode}.selectOption('${escapeString(option)}');`];
475
+ }
476
+
477
+ /**
478
+ * Generates check/uncheck action code.
479
+ */
480
+ private generateCheckCode(step: ExplorationStep): string[] {
481
+ const locatorCode = resolveLocatorCode(
482
+ step.execution.locator,
483
+ step.execution.selector_used
484
+ );
485
+ const checked = step.spec_action.checked ?? true;
486
+
487
+ if (checked) {
488
+ return [`await ${locatorCode}.check();`];
489
+ } else {
490
+ return [`await ${locatorCode}.uncheck();`];
491
+ }
492
+ }
493
+
494
+ /**
495
+ * Generates code for outcome checks.
496
+ */
497
+ private generateOutcomeChecks(
498
+ lockfile: ExplorationLockfile
499
+ ): Array<{ code: string }> {
500
+ return lockfile.outcome_checks
501
+ .filter((check) => check.passed)
502
+ .map((check) => {
503
+ const code = ` // Outcome: ${check.check} = ${String(check.expected)}`;
504
+ return { code };
505
+ });
506
+ }
507
+ }
508
+
509
+ /**
510
+ * Convenience function to generate a test from a lockfile.
511
+ */
512
+ export function generateTest(
513
+ lockfile: ExplorationLockfile,
514
+ options?: TestGeneratorOptions,
515
+ review?: ReviewData
516
+ ): GeneratedTest {
517
+ const generator = new PlaywrightGenerator(options);
518
+ return generator.generate(lockfile, review);
519
+ }