cd450837a2
Added comprehensive tests for remaining core functionality: ✅ 3-hyperscript.test.mjs - Parse error detection - Function definition verification - Keyboard event handler validation - Def statement count (≤3 limit) - Operator precedence checks ✅ 4-htmx.test.mjs - HTMX library loaded - Element presence (hx-get, hx-post, hx-swap, hx-target) - Request/response cycle validation - Loading indicators ✅ 5-language.test.mjs - Language toggle controls - Default language (English) - Spanish via URL parameter (?lang=es) - Toggle button functionality - localStorage/cookie persistence ✅ 6-modals.test.mjs - Modal elements (info, shortcuts, PDF) - ? key opens shortcuts modal - ESC key closes modals - Accessibility attributes (role, aria-label, aria-modal) Updated TEST-SUMMARY.md: - Now 7 active tests (0-6) - Complete core feature coverage - Updated coverage gaps (removed completed items) All tests follow established patterns: - Playwright browser automation - Real-time validation - Clear pass/fail indicators - Browser stays open for manual verification - Auto-discovered by master runner Master runner: bun tests/run-all.mjs
226 lines
8.4 KiB
JavaScript
Executable File
226 lines
8.4 KiB
JavaScript
Executable File
#!/usr/bin/env bun
|
|
/**
|
|
* HYPERSCRIPT VALIDATION TEST
|
|
* ============================
|
|
* Tests hyperscript integrity and parse error detection
|
|
* - Verifies no parse errors on page load
|
|
* - Validates max 3 def statements rule
|
|
* - Checks keyboard event handlers
|
|
* - Verifies operator precedence (parentheses)
|
|
*/
|
|
|
|
import { chromium } from 'playwright';
|
|
|
|
const URL = "http://localhost:1999";
|
|
|
|
async function testHyperscript() {
|
|
console.log('📜 HYPERSCRIPT VALIDATION TEST\n');
|
|
console.log('='.repeat(70));
|
|
|
|
const browser = await chromium.launch({ headless: false });
|
|
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}`);
|
|
}
|
|
});
|
|
|
|
page.on('pageerror', err => {
|
|
errors.push(err.message);
|
|
console.log(`❌ PAGE ERROR: ${err.message}`);
|
|
});
|
|
|
|
console.log("\n1️⃣ Loading page...");
|
|
await page.goto(URL);
|
|
await page.waitForTimeout(2000);
|
|
|
|
// ========================================================================
|
|
// TEST 1: No hyperscript parse errors
|
|
// ========================================================================
|
|
console.log("\n2️⃣ Testing Parse Error Detection...");
|
|
const parseErrors = errors.filter(err =>
|
|
err.includes('hyperscript') ||
|
|
err.includes('_hyperscript') ||
|
|
err.includes('parse error') ||
|
|
err.includes('Unexpected token')
|
|
);
|
|
|
|
const parseTest = parseErrors.length === 0;
|
|
console.log(` Parse errors: ${parseErrors.length}`);
|
|
if (parseErrors.length > 0) {
|
|
parseErrors.forEach((err, i) => {
|
|
console.log(` ${i + 1}. ${err}`);
|
|
});
|
|
}
|
|
console.log(` ${parseTest ? '✅ PASS' : '❌ FAIL'} - No parse errors`);
|
|
testResults.push({ test: 'No Parse Errors', passed: parseTest });
|
|
|
|
// ========================================================================
|
|
// TEST 2: Hyperscript functions are defined
|
|
// ========================================================================
|
|
console.log("\n3️⃣ Testing Function Definitions...");
|
|
const functionsTest = await page.evaluate(() => {
|
|
const hasToggleCVLength = typeof toggleCVLength === 'function';
|
|
const hasToggleIcons = typeof toggleIcons === 'function';
|
|
const hasToggleTheme = typeof toggleTheme === 'function';
|
|
const hasKeyboardHandler = typeof handleKeyboardShortcut === 'function';
|
|
|
|
return {
|
|
toggleCVLength: hasToggleCVLength,
|
|
toggleIcons: hasToggleIcons,
|
|
toggleTheme: hasToggleTheme,
|
|
keyboardHandler: hasKeyboardHandler,
|
|
allDefined: hasToggleCVLength && hasToggleIcons && hasToggleTheme && hasKeyboardHandler
|
|
};
|
|
});
|
|
|
|
console.log(` toggleCVLength: ${functionsTest.toggleCVLength ? '✅' : '❌'}`);
|
|
console.log(` toggleIcons: ${functionsTest.toggleIcons ? '✅' : '❌'}`);
|
|
console.log(` toggleTheme: ${functionsTest.toggleTheme ? '✅' : '❌'}`);
|
|
console.log(` handleKeyboardShortcut: ${functionsTest.keyboardHandler ? '✅' : '❌'}`);
|
|
console.log(` ${functionsTest.allDefined ? '✅ PASS' : '❌ FAIL'} - All functions defined`);
|
|
testResults.push({ test: 'Function Definitions', passed: functionsTest.allDefined });
|
|
|
|
// ========================================================================
|
|
// TEST 3: Keyboard event handlers work
|
|
// ========================================================================
|
|
console.log("\n4️⃣ Testing Keyboard Event Handlers...");
|
|
const keyboardTest = await page.evaluate(async () => {
|
|
const body = document.body;
|
|
const initialTheme = body.classList.contains('theme-clean');
|
|
|
|
// Simulate 'v' key press
|
|
const event = new KeyboardEvent('keydown', { key: 'v', bubbles: true });
|
|
document.body.dispatchEvent(event);
|
|
|
|
await new Promise(r => setTimeout(r, 300));
|
|
const afterTheme = body.classList.contains('theme-clean');
|
|
|
|
return {
|
|
initialTheme,
|
|
afterTheme,
|
|
toggled: initialTheme !== afterTheme
|
|
};
|
|
});
|
|
|
|
console.log(` Before: ${keyboardTest.initialTheme ? 'clean' : 'default'}`);
|
|
console.log(` After: ${keyboardTest.afterTheme ? 'clean' : 'default'}`);
|
|
console.log(` ${keyboardTest.toggled ? '✅ PASS' : '❌ FAIL'} - Keyboard handler works`);
|
|
testResults.push({ test: 'Keyboard Event Handlers', passed: keyboardTest.toggled });
|
|
|
|
// ========================================================================
|
|
// TEST 4: Check for def statements (should be 0 in HTML)
|
|
// ========================================================================
|
|
console.log("\n5️⃣ Testing Def Statement Count...");
|
|
const defTest = await page.evaluate(() => {
|
|
const scripts = Array.from(document.querySelectorAll('script[type="_hyperscript"]'));
|
|
const defCount = scripts.reduce((count, script) => {
|
|
const matches = script.textContent.match(/\bdef\b/g);
|
|
return count + (matches ? matches.length : 0);
|
|
}, 0);
|
|
|
|
const inlineElements = Array.from(document.querySelectorAll('[_]'));
|
|
const inlineDefCount = inlineElements.reduce((count, el) => {
|
|
const matches = el.getAttribute('_').match(/\bdef\b/g);
|
|
return count + (matches ? matches.length : 0);
|
|
}, 0);
|
|
|
|
return {
|
|
scriptDefs: defCount,
|
|
inlineDefs: inlineDefCount,
|
|
total: defCount + inlineDefCount
|
|
};
|
|
});
|
|
|
|
console.log(` Script tag defs: ${defTest.scriptDefs}`);
|
|
console.log(` Inline defs: ${defTest.inlineDefs}`);
|
|
console.log(` Total: ${defTest.total}`);
|
|
console.log(` ${defTest.total <= 3 ? '✅ PASS' : '❌ FAIL'} - Within 3 def limit`);
|
|
testResults.push({ test: 'Def Statement Count (≤3)', passed: defTest.total <= 3 });
|
|
|
|
// ========================================================================
|
|
// TEST 5: Operator precedence validation
|
|
// ========================================================================
|
|
console.log("\n6️⃣ Testing Operator Precedence...");
|
|
const precedenceTest = await page.evaluate(() => {
|
|
const elements = Array.from(document.querySelectorAll('[_]'));
|
|
const problematicPatterns = [];
|
|
|
|
elements.forEach(el => {
|
|
const script = el.getAttribute('_');
|
|
|
|
// Check for unparenthesized 'or' and 'and'
|
|
if (script.includes(' or ') || script.includes(' and ')) {
|
|
// Look for patterns like: a or b and c (should be (a or b) and c)
|
|
const hasProblematicPattern = /\w+\s+or\s+\w+\s+and\s+\w+/.test(script) ||
|
|
/\w+\s+and\s+\w+\s+or\s+\w+/.test(script);
|
|
|
|
if (hasProblematicPattern) {
|
|
problematicPatterns.push({
|
|
element: el.tagName,
|
|
id: el.id || 'no-id',
|
|
script: script.substring(0, 100)
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
return {
|
|
problematicCount: problematicPatterns.length,
|
|
patterns: problematicPatterns
|
|
};
|
|
});
|
|
|
|
console.log(` Problematic patterns: ${precedenceTest.problematicCount}`);
|
|
if (precedenceTest.problematicCount > 0) {
|
|
precedenceTest.patterns.forEach((p, i) => {
|
|
console.log(` ${i + 1}. <${p.element}${p.id !== 'no-id' ? ` id="${p.id}"` : ''}>`);
|
|
});
|
|
}
|
|
console.log(` ${precedenceTest.problematicCount === 0 ? '✅ PASS' : '❌ FAIL'} - Proper operator precedence`);
|
|
testResults.push({ test: 'Operator Precedence', passed: precedenceTest.problematicCount === 0 });
|
|
|
|
// ========================================================================
|
|
// 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 (see details above)`);
|
|
}
|
|
|
|
console.log("=".repeat(70) + "\n");
|
|
|
|
if (failedTests === 0 && parseErrors.length === 0) {
|
|
console.log("🎉 HYPERSCRIPT VALIDATION PASSED!");
|
|
} else {
|
|
console.log("⚠️ SOME TESTS FAILED - See details above");
|
|
}
|
|
|
|
console.log("\nBrowser will stay open for manual inspection.");
|
|
console.log("Press Ctrl+C when done.\n");
|
|
|
|
await new Promise(() => {}); // Keep browser open
|
|
}
|
|
|
|
await testHyperscript();
|