#!/usr/bin/env bun /** * CONTACT FORM TEST * ================= * Tests contact form functionality and error handling * - Modal opens correctly * - Form elements and validation * - HTMX attributes for error handling * - Timestamp reset on modal open * - Form submission flow */ import { chromium } from 'playwright'; const URL = "http://localhost:1999"; async function testContactForm() { console.log('šŸ“§ CONTACT FORM TEST\n'); console.log('='.repeat(70)); const browser = await chromium.launch({ headless: true }); const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } }); const errors = []; const testResults = []; page.on('console', msg => { const text = msg.text(); if (msg.type() === 'error') { errors.push(text); console.log(`āŒ ERROR: ${text}`); } }); console.log("\n1ļøāƒ£ Loading page..."); await page.goto(URL); await page.waitForTimeout(2000); // ======================================================================== // TEST 1: Contact modal exists // ======================================================================== console.log("\n2ļøāƒ£ Testing Contact Modal Elements..."); const modalTest = await page.evaluate(() => { const modal = document.querySelector('#contact-modal'); const form = document.querySelector('#contact-form'); const emailField = document.querySelector('#contact-email'); const messageField = document.querySelector('#contact-message'); const submitBtn = document.querySelector('.contact-submit-btn'); return { modalExists: !!modal, formExists: !!form, emailExists: !!emailField, messageExists: !!messageField, submitExists: !!submitBtn }; }); console.log(` Contact modal: ${modalTest.modalExists ? 'āœ…' : 'āŒ'}`); console.log(` Contact form: ${modalTest.formExists ? 'āœ…' : 'āŒ'}`); console.log(` Email field: ${modalTest.emailExists ? 'āœ…' : 'āŒ'}`); console.log(` Message field: ${modalTest.messageExists ? 'āœ…' : 'āŒ'}`); console.log(` Submit button: ${modalTest.submitExists ? 'āœ…' : 'āŒ'}`); const allElementsExist = modalTest.modalExists && modalTest.formExists && modalTest.emailExists && modalTest.messageExists && modalTest.submitExists; console.log(` ${allElementsExist ? 'āœ… PASS' : 'āŒ FAIL'} - Contact form elements exist`); testResults.push({ test: 'Contact Form Elements Exist', passed: allElementsExist }); // ======================================================================== // TEST 2: Contact button opens modal // ======================================================================== console.log("\n3ļøāƒ£ Testing Contact Button Opens Modal..."); const contactBtn = await page.$('.fixed-btn.contact-btn, .contact-btn, [data-modal-trigger="contact"]'); if (contactBtn) { await contactBtn.click(); await page.waitForTimeout(500); const modalOpened = await page.evaluate(() => { const modal = document.querySelector('#contact-modal'); if (!modal) return false; return modal.hasAttribute('open') || modal.classList.contains('open') || window.getComputedStyle(modal).display !== 'none'; }); console.log(` Modal opened: ${modalOpened ? 'āœ…' : 'āŒ'}`); console.log(` ${modalOpened ? 'āœ… PASS' : 'āŒ FAIL'} - Contact modal opens`); testResults.push({ test: 'Contact Modal Opens', passed: modalOpened }); // Close it for next test await page.keyboard.press('Escape'); await page.waitForTimeout(300); } else { console.log(` āš ļø SKIP - Contact button not found`); testResults.push({ test: 'Contact Modal Opens', passed: false }); } // ======================================================================== // TEST 3: Form has hyperscript for success detection (content-based, not HTTP status) // ======================================================================== console.log("\n4ļøāƒ£ Testing Hyperscript Success Detection..."); const hsTest = await page.evaluate(() => { const form = document.querySelector('#contact-form'); if (!form) return { found: false }; const hsAttribute = form.getAttribute('_') || ''; const hasAfterRequest = hsAttribute.includes('htmx:afterRequest'); const checksSuccessElement = hsAttribute.includes('.contact-success'); return { found: true, hasAfterRequest, checksSuccessElement }; }); if (hsTest.found) { console.log(` Handles htmx:afterRequest: ${hsTest.hasAfterRequest ? 'āœ…' : 'āŒ'}`); console.log(` Checks for .contact-success: ${hsTest.checksSuccessElement ? 'āœ…' : 'āŒ'}`); const handlerCorrect = hsTest.hasAfterRequest && hsTest.checksSuccessElement; console.log(` ${handlerCorrect ? 'āœ… PASS' : 'āŒ FAIL'} - Success detection via content (not HTTP status)`); testResults.push({ test: 'Hyperscript Success Detection', passed: handlerCorrect }); } else { console.log(` āŒ FAIL - Form not found`); testResults.push({ test: 'Hyperscript Success Detection', passed: false }); } // ======================================================================== // TEST 4: Timestamp field exists and resets on modal open // ======================================================================== console.log("\n5ļøāƒ£ Testing Timestamp Reset..."); // Get initial timestamp const initialTimestamp = await page.$eval('#contact-form-loaded-at', el => el.value); console.log(` Initial timestamp: ${initialTimestamp}`); // Open modal again if (contactBtn) { await page.waitForTimeout(1000); await contactBtn.click(); await page.waitForTimeout(500); const newTimestamp = await page.$eval('#contact-form-loaded-at', el => el.value); console.log(` After modal open: ${newTimestamp}`); const timestampReset = parseInt(newTimestamp) > parseInt(initialTimestamp); console.log(` Timestamp updated: ${timestampReset ? 'āœ…' : 'āŒ'}`); console.log(` ${timestampReset ? 'āœ… PASS' : 'āŒ FAIL'} - Timestamp resets on modal open`); testResults.push({ test: 'Timestamp Reset on Modal Open', passed: timestampReset }); // Keep modal open for next test } else { console.log(` āš ļø SKIP - Contact button not found`); testResults.push({ test: 'Timestamp Reset on Modal Open', passed: false }); } // ======================================================================== // TEST 5: Form HTMX attributes // ======================================================================== console.log("\n6ļøāƒ£ Testing Form HTMX Configuration..."); const formConfig = await page.evaluate(() => { const form = document.querySelector('#contact-form'); if (!form) return { found: false }; return { found: true, hxPost: form.getAttribute('hx-post'), hxTarget: form.getAttribute('hx-target'), hxSwap: form.getAttribute('hx-swap'), hxIndicator: form.getAttribute('hx-indicator'), hasHeaders: form.hasAttribute('hx-headers') }; }); if (formConfig.found) { console.log(` hx-post: ${formConfig.hxPost || 'N/A'}`); console.log(` hx-target: ${formConfig.hxTarget || 'N/A'}`); console.log(` hx-swap: ${formConfig.hxSwap || 'N/A'}`); console.log(` hx-indicator: ${formConfig.hxIndicator || 'N/A'}`); console.log(` hx-headers: ${formConfig.hasHeaders ? 'āœ…' : 'āŒ'}`); const configCorrect = formConfig.hxPost && formConfig.hxTarget && formConfig.hxSwap; console.log(` ${configCorrect ? 'āœ… PASS' : 'āŒ FAIL'} - Form HTMX configuration`); testResults.push({ test: 'Form HTMX Configuration', passed: configCorrect }); } else { console.log(` āŒ FAIL - Form not found`); testResults.push({ test: 'Form HTMX Configuration', passed: false }); } // ======================================================================== // TEST 6: Bot protection fields // ======================================================================== console.log("\n7ļøāƒ£ Testing Bot Protection..."); const botProtection = await page.evaluate(() => { const honeypot = document.querySelector('#contact-website, [name="website"]'); const timestampField = document.querySelector('#contact-form-loaded-at, [name="form_loaded_at"]'); return { honeypotExists: !!honeypot, honeypotHidden: honeypot ? (honeypot.closest('[style*="left: -9999px"]') !== null || window.getComputedStyle(honeypot.parentElement).position === 'absolute') : false, timestampExists: !!timestampField, timestampHidden: timestampField ? timestampField.type === 'hidden' : false }; }); console.log(` Honeypot field: ${botProtection.honeypotExists ? 'āœ…' : 'āŒ'}`); console.log(` Honeypot hidden: ${botProtection.honeypotHidden ? 'āœ…' : 'āŒ'}`); console.log(` Timestamp field: ${botProtection.timestampExists ? 'āœ…' : 'āŒ'}`); console.log(` Timestamp hidden: ${botProtection.timestampHidden ? 'āœ…' : 'āŒ'}`); const botProtectionOk = botProtection.honeypotExists && botProtection.timestampExists; console.log(` ${botProtectionOk ? 'āœ… PASS' : 'āŒ FAIL'} - Bot protection configured`); testResults.push({ test: 'Bot Protection Fields', passed: botProtectionOk }); // ======================================================================== // TEST 7: REAL submission test - verify no console errors on 400 response // ======================================================================== console.log("\n8ļøāƒ£ Testing Form Submission Error Handling (REAL TEST)..."); // Clear any previous errors for this specific test const errorsBefore = errors.length; // Make sure modal is open if (contactBtn) { await page.keyboard.press('Escape'); await page.waitForTimeout(300); await contactBtn.click(); await page.waitForTimeout(500); // Fill form with invalid data (message too short to trigger 400) await page.fill('#contact-email', 'test@example.com'); await page.fill('#contact-message', 'hi'); // Too short - will trigger 400 // Wait for timing validation (>2s) await page.waitForTimeout(2500); // Submit form via JavaScript to bypass HTML5 validation await page.evaluate(() => { const form = document.querySelector('#contact-form'); if (form) { // Remove HTML5 required attributes temporarily const emailField = form.querySelector('#contact-email'); const msgField = form.querySelector('#contact-message'); emailField.removeAttribute('required'); msgField.removeAttribute('required'); // Trigger HTMX request if (window.htmx) { window.htmx.trigger(form, 'submit'); } } }); await page.waitForTimeout(2000); const errorsAfter = errors.length; const newErrors = errorsAfter - errorsBefore; // With HTTP 200 for validation errors, there should be NO console errors at all // HTMX only logs errors for non-2xx responses console.log(` Errors before submit: ${errorsBefore}`); console.log(` Errors after submit: ${errorsAfter}`); console.log(` New errors during submit: ${newErrors}`); if (newErrors > 0) { const newErrorList = errors.slice(errorsBefore); newErrorList.forEach(e => console.log(` āŒ ${e}`)); } const noErrors = newErrors === 0; console.log(` ${noErrors ? 'āœ… PASS' : 'āŒ FAIL'} - Zero console errors on validation error`); testResults.push({ test: 'Zero Console Errors on Validation', passed: noErrors }); // Check that error message is displayed in the form const errorDisplayed = await page.evaluate(() => { const response = document.querySelector('#contact-response'); return response && response.innerHTML.length > 0; }); console.log(` Error message displayed in form: ${errorDisplayed ? 'āœ…' : 'āŒ'}`); } else { console.log(` āš ļø SKIP - Contact button not found`); testResults.push({ test: 'No Console Errors on 400 Response', passed: false }); } // ======================================================================== // FINAL SUMMARY // ======================================================================== console.log("\n" + "=".repeat(70)); console.log("šŸ“Š TEST SUMMARY\n"); const totalTests = testResults.length; const passedTests = testResults.filter(r => r.passed).length; const failedTests = totalTests - passedTests; testResults.forEach(result => { console.log(` ${result.passed ? 'āœ…' : 'āŒ'} ${result.test}`); }); console.log(`\n Total: ${passedTests}/${totalTests} tests passed`); if (errors.length === 0) { console.log("\nāœ… NO CONSOLE ERRORS"); } else { console.log(`\nāš ļø ${errors.length} CONSOLE ERRORS`); errors.forEach(e => console.log(` - ${e}`)); } console.log("=".repeat(70) + "\n"); if (failedTests === 0) { console.log("šŸŽ‰ CONTACT FORM FUNCTIONALITY VALIDATED!"); } else { console.log("āš ļø SOME TESTS FAILED - See details above"); } // Close browser and exit await browser.close(); // Exit with appropriate code process.exit(failedTests === 0 ? 0 : 1); } await testContactForm();