From 914e09876f072b4efbdc5c4f6c10f946a5a656e8 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sat, 25 Oct 2025 15:38:51 +0200 Subject: [PATCH] Initial commit: Playwright form automation system with page dumping debugging --- .github/copilot-instructions.md | 371 ++++++++++++++++++++++++++++++++ .gitignore | 6 + README.md | 248 +++++++++++++++++++++ package.json | 23 ++ page-dumps/.gitkeep | 44 ++++ template-automation.js | 270 +++++++++++++++++++++++ 6 files changed, 962 insertions(+) create mode 100644 .github/copilot-instructions.md create mode 100644 .gitignore create mode 100644 README.md create mode 100644 package.json create mode 100644 page-dumps/.gitkeep create mode 100644 template-automation.js diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..def0503 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,371 @@ +# Playwright Form Automation Workflow + +## Overview +Sistema completo per automatizzare submit di form su siti web usando Playwright con debugging intelligente via page dumps. + +## Architettura del Sistema + +### 1. **Page Dumping System** (Core Feature) +Funzione centrale che estrae informazioni strutturali dalla pagina per debugging: + +```javascript +async function dumpPageInfo(page, prefix) { + const timestamp = Date.now(); + const dumpDir = './page-dumps'; + + // 1. Salva HTML completo + const html = await page.content(); + fs.writeFileSync(`${dumpDir}/${prefix}-${timestamp}.html`, html); + + // 2. Screenshot della pagina + await page.screenshot({ + path: `${dumpDir}/${prefix}-${timestamp}.png`, + fullPage: true + }); + + // 3. Estrai TUTTI i form elements (input, textarea, select) + const elements = await page.$$eval('input, textarea, select', els => + els.map(el => ({ + tag: el.tagName.toLowerCase(), + type: el.type || '', + name: el.name || '', + id: el.id || '', + placeholder: el.placeholder || '', + value: el.value || '', + class: el.className || '' + })) + ); + + fs.writeFileSync( + `${dumpDir}/${prefix}-${timestamp}-selectors.txt`, + JSON.stringify(elements, null, 2) + ); +} +``` + +**Quando usarlo:** +- Prima di compilare un form (per capire la struttura) +- Dopo errori di selezione/compilazione +- Per analizzare dropdown dinamici (Select2, react-select, etc.) + +### 2. **Workflow Iterativo di Debugging** + +``` +┌─────────────────────────────────────────────┐ +│ 1. ESEGUI SCRIPT │ +│ node automation-script.js │ +└────────────┬────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────┐ +│ 2. ERRORE? → DUMP AUTOMATICO │ +│ - HTML salvato │ +│ - Screenshot salvato │ +│ - Form selectors estratti │ +└────────────┬────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────┐ +│ 3. ANALISI DUMP │ +│ cat page-dumps/*-selectors.txt │ +│ xdg-open page-dumps/*.png │ +└────────────┬────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────┐ +│ 4. CORREGGI SELETTORI NELLO SCRIPT │ +│ - Usa i nomi/id reali dai selectors.txt │ +│ - Adatta logica per dropdown dinamici │ +└────────────┬────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────┐ +│ 5. REITERA (torna a step 1) │ +└─────────────────────────────────────────────┘ +``` + +### 3. **Pattern per Form Elements Comuni** + +#### A. Input Text Semplici +```javascript +await page.fill('input[name="username"]', 'myuser'); +await page.fill('input[name="email"]', 'user@example.com'); +``` + +#### B. CSRF Token (Django/Rails/Laravel) +```javascript +// Automaticamente gestito dal browser, ma visibile nei dumps: +// { "type": "hidden", "name": "csrfmiddlewaretoken", "value": "..." } +// Non serve compilarlo manualmente - submit button lo include +``` + +#### C. Submit Button +```javascript +// PREFERISCI: click esplicito sul submit button +await page.click('input[type="submit"]'); +await page.click('button[type="submit"]'); + +// EVITA: form.submit() non trigger eventi JavaScript +``` + +#### D. Select2 Dropdown (jQuery plugin) +Select2 nasconde il `` standard. + +**Identificazione** (in selectors.txt): +```json +{ + "tag": "select", + "name": "category", + "class": "select2-hidden-accessible" // ← Questo indica Select2 +} +``` + +**Soluzione**: +```javascript +// ❌ NON funziona +await page.selectOption('select[name="category"]', 'value'); + +// ✅ Funziona +await page.click('span.select2-selection--multiple'); // Apre dropdown +await page.waitForTimeout(500); +await page.click('li.select2-results__option:has-text("Option")'); // Seleziona +``` + +## Common Patterns + +### Login con CSRF Token +```javascript +// Il token viene gestito automaticamente dal browser +// Basta compilare username/password e cliccare submit +await page.fill('input[name="username"]', 'user'); +await page.fill('input[name="password"]', 'pass'); +await page.click('input[type="submit"]'); // Click, NON form.submit()! +``` + +### File Upload +```javascript +await page.setInputFiles('input[type="file"]', '/path/to/file.png'); +``` + +### Checkbox/Radio +```javascript +await page.check('input[name="agree"]'); +await page.check('input[name="gender"][value="male"]'); +``` + +### Wait for Navigation +```javascript +// Dopo submit, aspetta che URL cambi +await page.waitForURL(/success|confirmation/); +``` + +## Debugging Checklist + +Script fallito? Segui questa lista: + +1. ✅ **Leggi selectors.txt** - Nomi/ID corretti? +2. ✅ **Guarda screenshot** - Elemento visibile? Serve scroll? +3. ✅ **Controlla HTML** - Iframe? Shadow DOM? JavaScript che modifica DOM? +4. ✅ **Prova manualmente** - L'azione funziona a mano nel browser? +5. ✅ **Console errors** - Apri DevTools nel browser Playwright + +## Examples + +### Esempio 1: Form Semplice +```javascript +await page.fill('input[name="name"]', 'John Doe'); +await page.fill('input[name="email"]', 'john@example.com'); +await page.fill('textarea[name="message"]', 'Hello world!'); +await page.click('button[type="submit"]'); +``` + +### Esempio 2: Multi-step Form +```javascript +// Step 1 +await page.fill('input[name="email"]', 'user@mail.com'); +await page.click('button:has-text("Next")'); +await page.waitForURL(/step-2/); + +// Step 2 +await page.fill('input[name="address"]', '123 Main St'); +await page.click('button:has-text("Submit")'); +``` + +### Esempio 3: Login + Form +```javascript +// Login +await page.goto('https://site.com/login'); +await page.fill('input[name="username"]', 'user'); +await page.fill('input[name="password"]', 'pass'); +await page.click('button[type="submit"]'); +await page.waitForURL(/dashboard/); + +// Navigate to form +await page.goto('https://site.com/submit'); + +// Fill and submit +await page.fill('input[name="title"]', 'My Title'); +await page.click('button[type="submit"]'); +``` + +## Tips & Tricks + +### Slow Down for Visibility +```javascript +const browser = await chromium.launch({ + headless: false, + slowMo: 100 // Rallenta di 100ms ogni azione +}); +``` + +### Custom Viewport +```javascript +const context = await browser.newContext({ + viewport: { width: 1920, height: 1080 } +}); +``` + +### Network Interception +```javascript +await page.route('**/api/spam', route => route.abort()); +``` + +### Take Extra Screenshots +```javascript +await page.screenshot({ + path: 'debug-step.png', + fullPage: true +}); +``` + +## Troubleshooting + +### "Timeout waiting for selector" +→ Elemento non esiste o ha nome diverso. Controlla `selectors.txt`. + +### "Element is not visible" +→ Serve scroll: `await page.locator('selector').scrollIntoViewIfNeeded();` + +### "Element is covered" +→ Modal/popup sopra. Chiudi: `await page.click('button.close-modal');` + +### Select2 non funziona +→ Usa pattern "click container → click option" (vedi sopra). + +### Submit non fa niente +→ Usa `page.click()` non `form.submit()`. JavaScript handlers servono click event. + +## Real-World Example + +Vedi nella parent folder: +- `publish-rats-lutris.js` - Script completo per pubblicare gioco su Lutris.net +- Include: login, Select2, file upload, multi-step form + +## License + +MIT - Free to use and modify + +--- + +**Created**: October 25, 2025 +**Author**: enne2 +**Tested on**: Debian GNU/Linux 13, Node.js v20.19.2, Playwright 1.40.0 diff --git a/package.json b/package.json new file mode 100644 index 0000000..bfb96fe --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "playwright-form-automation", + "version": "1.0.0", + "description": "Automated form submission with Playwright - intelligent page dumping for debugging", + "main": "template-automation.js", + "scripts": { + "install-browsers": "npx playwright install chromium", + "test": "node template-automation.js", + "clean-dumps": "rm -rf page-dumps/*" + }, + "keywords": [ + "playwright", + "automation", + "form", + "web-scraping", + "testing" + ], + "author": "enne2", + "license": "MIT", + "dependencies": { + "playwright": "^1.40.0" + } +} diff --git a/page-dumps/.gitkeep b/page-dumps/.gitkeep new file mode 100644 index 0000000..0b5f4f4 --- /dev/null +++ b/page-dumps/.gitkeep @@ -0,0 +1,44 @@ +# Playwright Form Automation - Page Dumps + +This directory is automatically created by the automation scripts. + +## Contents + +Each dump consists of 3 files with the same timestamp: + +1. **`-.html`** - Full HTML content of the page +2. **`-.png`** - Full-page screenshot +3. **`--selectors.txt`** - JSON array of all form elements + +## How to Use + +When your script fails or you need to understand page structure: + +1. **Check the screenshot** - Visual representation of what the page looked like +2. **Read selectors.txt** - Find the correct selectors (name, id, class) for form elements +3. **Search HTML** - Deep dive into DOM structure if needed + +## Example + +```bash +# View latest selectors +cat page-dumps/*-selectors.txt | tail -100 + +# Open latest screenshot +xdg-open $(ls -t page-dumps/*.png | head -1) + +# Search for specific field in HTML +grep -i "username" page-dumps/login-*.html +``` + +## Cleanup + +Remove old dumps to save space: + +```bash +# From parent directory +npm run clean-dumps + +# Or manually +rm -rf page-dumps/* +``` diff --git a/template-automation.js b/template-automation.js new file mode 100644 index 0000000..4a2d07f --- /dev/null +++ b/template-automation.js @@ -0,0 +1,270 @@ +const { chromium } = require('playwright'); +const fs = require('fs'); +const path = require('path'); + +// ======================================== +// CONFIGURATION - Customize for your site +// ======================================== + +const SITE_URL = 'https://example.com'; +const LOGIN_REQUIRED = true; + +const LOGIN_CREDENTIALS = { + username: 'your_username', + password: 'your_password' +}; + +const FORM_DATA = { + field1: 'Value 1', + field2: 'Value 2', + description: 'Long description text here...' +}; + +// ======================================== +// CORE UTILITY: Page Dumping System +// ======================================== + +/** + * Dumps page information for debugging: + * - Full HTML content + * - Screenshot (full page) + * - All form elements with their properties + * + * Use this BEFORE filling forms and AFTER errors + */ +async function dumpPageInfo(page, prefix) { + const timestamp = Date.now(); + const dumpDir = path.join(__dirname, 'page-dumps'); + + // Create dumps directory if not exists + if (!fs.existsSync(dumpDir)) { + fs.mkdirSync(dumpDir, { recursive: true }); + } + + console.log('🔍 Dumping page for debugging...'); + + try { + // 1. Save full HTML + const html = await page.content(); + const htmlPath = path.join(dumpDir, `${prefix}-${timestamp}.html`); + fs.writeFileSync(htmlPath, html); + console.log(` 📄 HTML saved: ${htmlPath}`); + + // 2. Take screenshot + const screenshotPath = path.join(dumpDir, `${prefix}-${timestamp}.png`); + await page.screenshot({ + path: screenshotPath, + fullPage: true + }); + console.log(` 📸 Screenshot saved: ${screenshotPath}`); + + // 3. Extract all form elements + const elements = await page.$$eval('input, textarea, select, button', els => + els.map(el => ({ + tag: el.tagName.toLowerCase(), + type: el.type || '', + name: el.name || '', + id: el.id || '', + placeholder: el.placeholder || '', + value: el.value || '', + class: el.className || '' + })) + ); + + const selectorsPath = path.join(dumpDir, `${prefix}-${timestamp}-selectors.txt`); + fs.writeFileSync(selectorsPath, JSON.stringify(elements, null, 2)); + console.log(` 📋 Form elements saved: ${selectorsPath}`); + + } catch (error) { + console.error(`❌ Error during page dump: ${error.message}`); + } +} + +// ======================================== +// LOGIN HANDLER with Fallback +// ======================================== + +async function handleLogin(page, credentials) { + console.log('🔐 Attempting automatic login...'); + + try { + // Dump login page first to understand structure + await dumpPageInfo(page, 'login-page-initial'); + + // Fill login form + await page.fill('input[name="username"]', credentials.username); + await page.fill('input[name="password"]', credentials.password); + + // Submit (click button, don't use form.submit()) + await page.click('input[type="submit"], button[type="submit"]'); + + // Wait for redirect after successful login + // Adjust the URL pattern to match your site's post-login URL + await page.waitForURL(/.*\/(?!login|signin)/, { timeout: 30000 }); + + console.log('✅ Login successful!\n'); + + } catch (loginError) { + console.warn('⚠️ Auto-login failed, trying manual intervention...'); + await dumpPageInfo(page, 'login-error'); + + console.log('\n⏸️ Please log in manually in the browser window.'); + console.log(' Waiting up to 5 minutes for manual login...\n'); + + // Wait for user to login manually + await page.waitForURL(/.*\/(?!login|signin)/, { timeout: 300000 }); + console.log('✅ Manual login detected!\n'); + } +} + +// ======================================== +// MAIN AUTOMATION FUNCTION +// ======================================== + +async function automateFormSubmission() { + console.log('🚀 Starting form automation...\n'); + + // Launch browser (headless: false to see what's happening) + const browser = await chromium.launch({ + headless: false, + slowMo: 100 // Slow down operations for visibility + }); + + const context = await browser.newContext({ + viewport: { width: 1920, height: 1080 } + }); + + const page = await context.newPage(); + + try { + // ======================================== + // STEP 1: Navigate to site + // ======================================== + console.log('📋 Step 1: Navigating to site...'); + await page.goto(SITE_URL); + await page.waitForLoadState('networkidle'); + + // ======================================== + // STEP 2: Handle login if required + // ======================================== + if (LOGIN_REQUIRED) { + // Check if login is needed (adjust selector to your site) + const needsLogin = await page.locator('a:has-text("Login"), a:has-text("Sign in")').count() > 0; + + if (needsLogin) { + console.log('⚠️ Login required!\n'); + // Navigate to login page (adjust URL) + await page.goto(`${SITE_URL}/login`); + await handleLogin(page, LOGIN_CREDENTIALS); + } else { + console.log('✅ Already logged in!\n'); + } + } + + // ======================================== + // STEP 3: Navigate to form page + // ======================================== + console.log('📋 Step 2: Navigating to form page...'); + await page.goto(`${SITE_URL}/submit-form`); // Adjust URL + await page.waitForLoadState('networkidle'); + + // ======================================== + // STEP 4: Dump page to understand structure + // ======================================== + console.log('📝 Step 3: Analyzing form structure...'); + await dumpPageInfo(page, 'form-initial'); + + // ======================================== + // STEP 5: Fill form fields + // ======================================== + console.log('📝 Step 4: Filling form...'); + + try { + // Simple text input + await page.fill('input[name="field1"]', FORM_DATA.field1); + console.log(' ✓ Field1 filled'); + + // Another text input + await page.fill('input[name="field2"]', FORM_DATA.field2); + console.log(' ✓ Field2 filled'); + + // Textarea + await page.fill('textarea[name="description"]', FORM_DATA.description); + console.log(' ✓ Description filled'); + + // Example: Select dropdown (standard) + // await page.selectOption('select[name="category"]', 'option-value'); + // console.log(' ✓ Category selected'); + + // Example: Select2 dropdown (jQuery plugin) + // await page.click('span.select2-selection--multiple'); + // await page.waitForTimeout(500); + // await page.click('li.select2-results__option:has-text("Option Name")'); + // console.log(' ✓ Select2 option selected'); + + // Example: Checkbox + // await page.check('input[name="agree"]'); + // console.log(' ✓ Checkbox checked'); + + // Example: File upload + // await page.setInputFiles('input[type="file"]', '/path/to/file.png'); + // console.log(' ✓ File uploaded'); + + } catch (fillError) { + console.error(`❌ Error filling form: ${fillError.message}`); + await dumpPageInfo(page, 'form-fill-error'); + throw fillError; + } + + // ======================================== + // STEP 6: Review before submit + // ======================================== + console.log('\n⏸️ Form filled! Review in browser and press Enter to submit...'); + await new Promise(resolve => { + process.stdin.once('data', resolve); + }); + + // ======================================== + // STEP 7: Submit form + // ======================================== + console.log('📤 Step 5: Submitting form...'); + await page.click('button[type="submit"], input[type="submit"]'); + + // Wait for success page (adjust URL pattern) + await page.waitForURL(/success|confirmation|thank-you/, { timeout: 30000 }); + console.log('✅ Form submitted successfully!\n'); + + // Take final screenshot + await page.screenshot({ + path: path.join(__dirname, 'page-dumps', 'success.png'), + fullPage: true + }); + + } catch (error) { + console.error(`\n❌ Automation failed: ${error.message}\n`); + await dumpPageInfo(page, 'fatal-error'); + throw error; + + } finally { + console.log('🏁 Closing browser...'); + await browser.close(); + } +} + +// ======================================== +// RUN AUTOMATION +// ======================================== + +if (require.main === module) { + automateFormSubmission() + .then(() => { + console.log('✅ Automation completed successfully!'); + process.exit(0); + }) + .catch(error => { + console.error('❌ Automation failed:', error); + process.exit(1); + }); +} + +module.exports = { automateFormSubmission, dumpPageInfo };