UNPKG

playwright-ai-codegen-lib

Version:

A utility to auto-generate Playwright PageObjects and test scripts using OpenAI and DOM extraction.

147 lines (141 loc) • 5.21 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.generateSelectors = generateSelectors; exports.generateFunctions = generateFunctions; const openai_1 = __importDefault(require("openai")); const dotenv_1 = __importDefault(require("dotenv")); dotenv_1.default.config(); if (!process.env.OPENAI_API_KEY) { console.error('āŒ Missing OPENAI_API_KEY in .env'); process.exit(1); } const openai = new openai_1.default({ apiKey: process.env.OPENAI_API_KEY, }); // Split the DOM into 10k character chunks function splitDom(dom, chunkSize = 10000) { const chunks = []; for (let i = 0; i < dom.length; i += chunkSize) { chunks.push(dom.slice(i, i + chunkSize)); } return chunks; } // Clean the OpenAI response function sanitizeOutput(content) { return content .replace(/```typescript/g, '') .replace(/```/g, '') .replace(/\\"/g, '"') // double-escaped quotes to real quotes .replace(/\\'/g, "'") // escaped single quotes .replace(/\\n/g, '\n') // newline .trim(); } // Step 1: Generate Selectors async function generateSelectors(dom) { console.log('🧠 Starting OpenAI Selectors extraction...'); const chunks = splitDom(dom); let selectors = []; for (let i = 0; i < chunks.length; i++) { const chunkPrompt = ` You are building a Playwright Page Object Model in TypeScript. For chunk ${i + 1}, generate ONLY: - New private readonly selectors (e.g., buttons, inputs, tabs, links) āŒ DO NOT include: - methods - class declaration - constructor - extra comments or markdown - opening or closing braces DOM CHUNK: ${chunks[i]} `; console.log(`🧩 Sending DOM chunk ${i + 1} of ${chunks.length} to OpenAI...`); const response = await openai.chat.completions.create({ model: 'gpt-3.5-turbo', messages: [{ role: 'user', content: chunkPrompt }], temperature: 0.2, }); const raw = response.choices[0]?.message?.content || ''; const clean = sanitizeOutput(raw); selectors.push(clean); } console.log('āœ… Selectors extraction complete.'); return selectors; } // Step 2: Generate Methods from Selectors async function generateFunctions(selectorsRaw, title) { console.log('🧠 Structuring selector map and individual methods...'); const selectorLines = selectorsRaw .flatMap(raw => raw.split('\n')) .map(line => line.trim()) .filter(line => line.includes('=')); const seenVars = new Set(); const seenMethods = new Set(); const selectorMap = []; const methodBodies = []; for (const line of selectorLines) { const match = line.match(/private readonly (\w+)\s*=\s*(['"])(.*?)\2;?/); if (!match) continue; const varName = match[1]; const selector = match[3]; if (seenVars.has(varName)) continue; seenVars.add(varName); // Add to selector map selectorMap.push(` ${varName}: { selector: '${selector}', action: 'click' },`); // Generate method name let methodName = varName; if (methodName.startsWith('btn') || methodName.startsWith('button')) { methodName = methodName.replace(/^btn|button/, 'click'); } else if (methodName.startsWith('link')) { methodName = methodName.replace(/^link/, 'click'); } else if (methodName.startsWith('textBox') || methodName.startsWith('input')) { methodName = methodName.replace(/^textBox|input/, 'fill'); } else if (!methodName.startsWith('click') && !methodName.startsWith('fill') && !methodName.startsWith('get')) { methodName = 'click' + methodName.charAt(0).toUpperCase() + methodName.slice(1); } if (seenMethods.has(methodName)) { console.warn(`āš ļø Duplicate method skipped: ${methodName}`); continue; } seenMethods.add(methodName); // Generate method body const action = methodName.startsWith('fill') ? 'fill' : 'click'; const params = action === 'fill' ? 'text: string' : ''; const actionCall = action === 'fill' ? `await this.page.locator(this.selectors.${varName}).fill(text);` : `await this.page.locator(this.selectors.${varName}).click();`; methodBodies.push(` async ${methodName}(${params}) { Logger.log("In ${methodName}"); ${actionCall} }`); } const classCode = ` import { Page } from '@playwright/test'; import { Logger } from './Logger.ts'; const selectors = { ${selectorMap.join('\n')} } as const; export class ${title}Page { private page: Page; private selectors: Record<string, string>; constructor(page: Page) { this.page = page; this.selectors = Object.fromEntries( Object.entries(selectors).map(([key, val]) => [key, val.selector]) ); } ${methodBodies.join('\n')} } `.trim(); console.log('āœ… Page Object class generated with methods.'); return classCode; }