/** * Form detector — extract form metadata from crawled pages. * * For each form found on a page, captures: action URL, method, all input fields * with their types/names/required/validation constraints, and submit button text. * * This data feeds ZeTa's test generator to create form validation test cases * automatically (happy path + boundary/error cases per field type). */ import type { Page } from 'playwright'; export interface FormField { name: string; type: string; // text, email, password, number, select, textarea, checkbox, radio, file, date label?: string; required: boolean; placeholder?: string; minLength?: number; maxLength?: number; min?: string; max?: string; pattern?: string; options?: string[]; // for select/radio autocomplete?: string; } export interface DetectedForm { index: number; action?: string; method: string; fields: FormField[]; submitText?: string; hasFileUpload: boolean; purpose: 'login' | 'search' | 'registration' | 'contact' | 'checkout' | 'settings' | 'unknown'; } function guessPurpose(fields: FormField[], action?: string): DetectedForm['purpose'] { const names = fields.map(f => f.name?.toLowerCase() + (f.label ?? '').toLowerCase()).join(' '); if (/password/.test(names) && /email|username|user/.test(names) && fields.length <= 4) return 'login'; if (/password/.test(names) && /confirm|repeat/.test(names)) return 'registration'; if (/search|query|q\b/.test(names) && fields.length <= 3) return 'search'; if (/card|payment|cvv|expir/.test(names)) return 'checkout'; if (/message|subject|contact/.test(names)) return 'contact'; if (/setting|preference|profile/.test(names)) return 'settings'; return 'unknown'; } export async function detectForms(page: Page): Promise { try { return await page.$$eval('form', (forms) => forms.map((form: any, index: number) => { const inputs = Array.from(form.querySelectorAll('input,select,textarea,button[type="submit"]')); const fields: any[] = []; for (const input of inputs as any[]) { const tag = input.tagName.toLowerCase(); const type = tag === 'button' ? 'submit' : (input.type || tag); if (['submit', 'reset', 'button', 'hidden'].includes(type)) continue; // Find associated label let label = ''; if (input.id) { const lbl = form.querySelector(`label[for="${input.id}"]`); if (lbl) label = (lbl.textContent ?? '').trim(); } if (!label) { const parent = input.closest('label'); if (parent) label = (parent.textContent ?? '').replace(input.value ?? '', '').trim().slice(0, 80); } const options = tag === 'select' ? Array.from(input.options ?? []).map((o: any) => o.text?.trim()).filter(Boolean).slice(0, 20) : tag === 'input' && type === 'radio' ? [input.value] : undefined; fields.push({ name: input.name || input.id || '', type, label: label || undefined, required: !!input.required, placeholder: input.placeholder || undefined, minLength: input.minLength > 0 ? input.minLength : undefined, maxLength: input.maxLength > 0 && input.maxLength < 524288 ? input.maxLength : undefined, min: input.min || undefined, max: input.max || undefined, pattern: input.pattern || undefined, options, autocomplete: input.autocomplete || undefined, }); } const submitBtn = form.querySelector('button[type="submit"],input[type="submit"]') as any; const hasFileUpload = !!form.querySelector('input[type="file"]'); const action = form.action || undefined; const method = (form.method || 'get').toUpperCase(); return { index, action, method, fields, submitText: submitBtn?.textContent?.trim() || submitBtn?.value || undefined, hasFileUpload }; })).then(forms => forms.map((f: any) => ({ ...f, purpose: guessPurpose(f.fields, f.action) }))); } catch { return []; } }