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