9a848e8c53
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.
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();
|