Files
cv-site/tests/mjs/73-contact-form.test.mjs
T
juanatsap 9a848e8c53 feat: Add CMD+K command palette with ninja-keys integration
Implement a command palette accessible via CMD+K/Ctrl+K using the ninja-keys
web component. Features include:

- New /api/cmd-k endpoint serving dynamic CV entries (experiences, projects, courses)
- Language-aware responses with 1-hour cache headers
- Scroll-to-section functionality for quick navigation
- Enhanced keyboard shortcuts modal with CMD+K documentation
- Comprehensive test coverage for API and UI interactions

Also includes cleanup of deprecated debug test files and various UI polish
improvements to contact form, themes, and action bar components.
2025-12-01 13:03:06 +00:00

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();