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
JavaScript
;
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;
}