330 lines
13 KiB
JavaScript
330 lines
13 KiB
JavaScript
|
|
#!/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();
|