|
| 1 | +// src/recording/code-generator.ts |
| 2 | +import type { RecordedStep, SessionHistory } from '../types/recording'; |
| 3 | + |
| 4 | +/** Escape single quotes so generated JS string literals are valid. */ |
| 5 | +function escapeStr(value: unknown): string { |
| 6 | + return String(value).replace(/\\/g, '\\\\').replace(/'/g, "\\'"); |
| 7 | +} |
| 8 | + |
| 9 | +function formatParams(params: Record<string, unknown>): string { |
| 10 | + return Object.entries(params) |
| 11 | + .map(([k, v]) => `${k}="${v}"`) |
| 12 | + .join(' '); |
| 13 | +} |
| 14 | + |
| 15 | +function indentJson(value: unknown): string { |
| 16 | + return JSON.stringify(value, null, 2) |
| 17 | + .split('\n') |
| 18 | + .map((line, i) => (i > 0 ? ` ${line}` : line)) |
| 19 | + .join('\n'); |
| 20 | +} |
| 21 | + |
| 22 | +function generateStep(step: RecordedStep, history: SessionHistory): string { |
| 23 | + if (step.tool === '__session_transition__') { |
| 24 | + const newId = (step.params.newSessionId as string) ?? 'unknown'; |
| 25 | + return `// --- new session: ${newId} started at ${step.timestamp} ---`; |
| 26 | + } |
| 27 | + |
| 28 | + if (step.status === 'error') { |
| 29 | + return `// [error] ${step.tool}: ${formatParams(step.params)} — ${step.error ?? 'unknown error'}`; |
| 30 | + } |
| 31 | + |
| 32 | + const p = step.params; |
| 33 | + switch (step.tool) { |
| 34 | + case 'start_browser': { |
| 35 | + const nav = p.navigationUrl ? `\nawait browser.url('${escapeStr(p.navigationUrl)}');` : ''; |
| 36 | + return `const browser = await remote({\n capabilities: ${indentJson(history.capabilities)}\n});${nav}`; |
| 37 | + } |
| 38 | + case 'start_app_session': { |
| 39 | + const config: Record<string, unknown> = { |
| 40 | + protocol: 'http', |
| 41 | + hostname: history.appiumConfig?.hostname ?? 'localhost', |
| 42 | + port: history.appiumConfig?.port ?? 4723, |
| 43 | + path: history.appiumConfig?.path ?? '/', |
| 44 | + capabilities: history.capabilities, |
| 45 | + }; |
| 46 | + return `const browser = await remote(${indentJson(config)});`; |
| 47 | + } |
| 48 | + case 'attach_browser': { |
| 49 | + const nav = p.navigationUrl ? `\nawait browser.url('${escapeStr(p.navigationUrl)}');` : ''; |
| 50 | + return `const browser = await remote({\n capabilities: ${indentJson(history.capabilities)}\n});${nav}`; |
| 51 | + } |
| 52 | + case 'navigate': |
| 53 | + return `await browser.url('${escapeStr(p.url)}');`; |
| 54 | + case 'click_element': |
| 55 | + return `await browser.$('${escapeStr(p.selector)}').click();`; |
| 56 | + case 'set_value': |
| 57 | + return `await browser.$('${escapeStr(p.selector)}').setValue('${escapeStr(p.value)}');`; |
| 58 | + case 'scroll': { |
| 59 | + const scrollAmount = (p.direction as string) === 'down' ? (p.pixels as number) : -(p.pixels as number); |
| 60 | + return `await browser.execute(() => window.scrollBy(0, ${scrollAmount}));`; |
| 61 | + } |
| 62 | + case 'tap_element': |
| 63 | + if (p.selector !== undefined) { |
| 64 | + return `await browser.$('${escapeStr(p.selector)}').click();`; |
| 65 | + } |
| 66 | + return `await browser.tap({ x: ${p.x}, y: ${p.y} });`; |
| 67 | + case 'swipe': |
| 68 | + return `await browser.execute('mobile: swipe', { direction: '${escapeStr(p.direction)}' });`; |
| 69 | + case 'drag_and_drop': |
| 70 | + if (p.targetSelector !== undefined) { |
| 71 | + return `await browser.$('${escapeStr(p.sourceSelector)}').dragAndDrop(browser.$('${escapeStr(p.targetSelector)}'));`; |
| 72 | + } |
| 73 | + return `await browser.$('${escapeStr(p.sourceSelector)}').dragAndDrop({ x: ${p.x}, y: ${p.y} });`; |
| 74 | + default: |
| 75 | + return `// [unknown tool] ${step.tool}`; |
| 76 | + } |
| 77 | +} |
| 78 | + |
| 79 | +export function generateCode(history: SessionHistory): string { |
| 80 | + const steps = history.steps.map(step => generateStep(step, history)).join('\n'); |
| 81 | + return `import { remote } from 'webdriverio';\n\n${steps}\n\nawait browser.deleteSession();`; |
| 82 | +} |
0 commit comments