579 lines
20 KiB
JavaScript
579 lines
20 KiB
JavaScript
|
|
#!/usr/bin/env node
|
|||
|
|
/**
|
|||
|
|
* COMPREHENSIVE VERIFICATION TEST SUITE
|
|||
|
|
* Tests both HTMX indicators and shortcuts button visibility fixes
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import { chromium } from 'playwright';
|
|||
|
|
|
|||
|
|
const BASE_URL = 'http://localhost:1999';
|
|||
|
|
const RESULTS = {
|
|||
|
|
passed: [],
|
|||
|
|
failed: [],
|
|||
|
|
warnings: []
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
function log(status, message) {
|
|||
|
|
const timestamp = new Date().toLocaleTimeString();
|
|||
|
|
const icons = { pass: '✅', fail: '❌', warn: '⚠️', info: 'ℹ️' };
|
|||
|
|
console.log(`[${timestamp}] ${icons[status] || icons.info} ${message}`);
|
|||
|
|
|
|||
|
|
if (status === 'pass') RESULTS.passed.push(message);
|
|||
|
|
if (status === 'fail') RESULTS.failed.push(message);
|
|||
|
|
if (status === 'warn') RESULTS.warnings.push(message);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function measureTime(start) {
|
|||
|
|
return `${Date.now() - start}ms`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function sleep(ms) {
|
|||
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function test1_HTMXLoadingIndicators(page) {
|
|||
|
|
log('info', '═══════════════════════════════════════════════════════');
|
|||
|
|
log('info', 'TEST 1: HTMX Loading Indicators (Feature 003)');
|
|||
|
|
log('info', '═══════════════════════════════════════════════════════');
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// Navigate to page
|
|||
|
|
await page.goto(BASE_URL);
|
|||
|
|
await page.waitForLoadState('networkidle');
|
|||
|
|
log('pass', 'Page loaded successfully');
|
|||
|
|
|
|||
|
|
// Test 1.1: Verify indicator elements exist
|
|||
|
|
log('info', 'Test 1.1: Checking indicator elements exist...');
|
|||
|
|
const enIndicator = page.locator('#lang-indicator-en');
|
|||
|
|
const esIndicator = page.locator('#lang-indicator-es');
|
|||
|
|
|
|||
|
|
await enIndicator.waitFor({ state: 'attached', timeout: 5000 });
|
|||
|
|
await esIndicator.waitFor({ state: 'attached', timeout: 5000 });
|
|||
|
|
log('pass', 'Both language indicators found in DOM');
|
|||
|
|
|
|||
|
|
// Test 1.2: Verify initial opacity is 0 (hidden)
|
|||
|
|
log('info', 'Test 1.2: Checking initial indicator opacity...');
|
|||
|
|
const enInitialOpacity = await enIndicator.evaluate(el =>
|
|||
|
|
window.getComputedStyle(el).opacity
|
|||
|
|
);
|
|||
|
|
const esInitialOpacity = await esIndicator.evaluate(el =>
|
|||
|
|
window.getComputedStyle(el).opacity
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (enInitialOpacity === '0' && esInitialOpacity === '0') {
|
|||
|
|
log('pass', `Indicators hidden initially (opacity: ${enInitialOpacity})`);
|
|||
|
|
} else {
|
|||
|
|
log('fail', `Indicators should be hidden (EN: ${enInitialOpacity}, ES: ${esInitialOpacity})`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Test 1.3: Click EN button and verify indicator appears
|
|||
|
|
log('info', 'Test 1.3: Testing EN button loading indicator...');
|
|||
|
|
|
|||
|
|
// Get the ES button (since we're on EN by default)
|
|||
|
|
const esButton = page.locator('button.selector-btn[data-short="ES"]');
|
|||
|
|
await esButton.waitFor({ state: 'visible' });
|
|||
|
|
|
|||
|
|
// Set up monitoring for opacity changes
|
|||
|
|
const opacityPromise = page.evaluate(() => {
|
|||
|
|
return new Promise(resolve => {
|
|||
|
|
const indicator = document.querySelector('#lang-indicator-es');
|
|||
|
|
let maxOpacity = 0;
|
|||
|
|
let opacityChanges = [];
|
|||
|
|
|
|||
|
|
const observer = new MutationObserver(() => {
|
|||
|
|
const currentOpacity = parseFloat(window.getComputedStyle(indicator).opacity);
|
|||
|
|
opacityChanges.push(currentOpacity);
|
|||
|
|
maxOpacity = Math.max(maxOpacity, currentOpacity);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
observer.observe(indicator.parentElement, {
|
|||
|
|
attributes: true,
|
|||
|
|
attributeFilter: ['class'],
|
|||
|
|
subtree: true
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Check opacity every 10ms for 2 seconds
|
|||
|
|
let checks = 0;
|
|||
|
|
const interval = setInterval(() => {
|
|||
|
|
const currentOpacity = parseFloat(window.getComputedStyle(indicator).opacity);
|
|||
|
|
opacityChanges.push(currentOpacity);
|
|||
|
|
maxOpacity = Math.max(maxOpacity, currentOpacity);
|
|||
|
|
checks++;
|
|||
|
|
|
|||
|
|
if (checks > 200) { // 2 seconds
|
|||
|
|
clearInterval(interval);
|
|||
|
|
observer.disconnect();
|
|||
|
|
resolve({ maxOpacity, opacityChanges: opacityChanges.filter(o => o > 0) });
|
|||
|
|
}
|
|||
|
|
}, 10);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Click the button
|
|||
|
|
const clickTime = Date.now();
|
|||
|
|
await esButton.click();
|
|||
|
|
|
|||
|
|
// Wait for the opacity monitoring to complete
|
|||
|
|
const opacityData = await opacityPromise;
|
|||
|
|
const responseTime = measureTime(clickTime);
|
|||
|
|
|
|||
|
|
log('info', `Request completed in ${responseTime}`);
|
|||
|
|
log('info', `Max indicator opacity: ${opacityData.maxOpacity}`);
|
|||
|
|
log('info', `Opacity changes detected: ${opacityData.opacityChanges.length}`);
|
|||
|
|
|
|||
|
|
if (opacityData.maxOpacity >= 0.9) {
|
|||
|
|
log('pass', `Indicator became visible (max opacity: ${opacityData.maxOpacity})`);
|
|||
|
|
} else if (opacityData.maxOpacity > 0) {
|
|||
|
|
log('warn', `Indicator partially visible but not fully (max: ${opacityData.maxOpacity})`);
|
|||
|
|
} else {
|
|||
|
|
log('warn', 'Indicator not visible on fast request (expected on localhost - will verify with throttled test)');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Test 1.4: Verify indicator faded out after request
|
|||
|
|
await sleep(500);
|
|||
|
|
const finalOpacity = await esIndicator.evaluate(el =>
|
|||
|
|
window.getComputedStyle(el).opacity
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (finalOpacity === '0') {
|
|||
|
|
log('pass', `Indicator hidden after request (opacity: ${finalOpacity})`);
|
|||
|
|
} else {
|
|||
|
|
log('warn', `Indicator may not have faded out (opacity: ${finalOpacity})`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Test 1.5: Take screenshot during loading
|
|||
|
|
log('info', 'Test 1.5: Capturing screenshot during loading...');
|
|||
|
|
|
|||
|
|
// Click back to EN to trigger another loading state
|
|||
|
|
await sleep(500);
|
|||
|
|
const enButton = page.locator('button.selector-btn[data-short="EN"]');
|
|||
|
|
|
|||
|
|
// Start click and immediately capture
|
|||
|
|
const screenshotPromise = page.screenshot({
|
|||
|
|
path: '/Users/txeo/Git/yo/cv/test-screenshots/htmx-indicator-loading.png',
|
|||
|
|
fullPage: false
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await enButton.click();
|
|||
|
|
await screenshotPromise;
|
|||
|
|
|
|||
|
|
log('pass', 'Screenshot captured: test-screenshots/htmx-indicator-loading.png');
|
|||
|
|
|
|||
|
|
// Test 1.6: Network throttling test
|
|||
|
|
log('info', 'Test 1.6: Testing with slow 3G network...');
|
|||
|
|
|
|||
|
|
// Slow 3G preset - only delay the specific endpoint
|
|||
|
|
let requestIntercepted = false;
|
|||
|
|
await page.route('**/switch-language**', async route => {
|
|||
|
|
if (!requestIntercepted) {
|
|||
|
|
requestIntercepted = true;
|
|||
|
|
await sleep(800); // Simulate 800ms delay
|
|||
|
|
}
|
|||
|
|
await route.continue();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await sleep(500);
|
|||
|
|
const slowClickTime = Date.now();
|
|||
|
|
|
|||
|
|
// Click and monitor
|
|||
|
|
const slowOpacityPromise = page.evaluate(() => {
|
|||
|
|
return new Promise(resolve => {
|
|||
|
|
const indicator = document.querySelector('#lang-indicator-en');
|
|||
|
|
let maxOpacity = 0;
|
|||
|
|
const interval = setInterval(() => {
|
|||
|
|
const opacity = parseFloat(window.getComputedStyle(indicator).opacity);
|
|||
|
|
maxOpacity = Math.max(maxOpacity, opacity);
|
|||
|
|
}, 10);
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
clearInterval(interval);
|
|||
|
|
resolve(maxOpacity);
|
|||
|
|
}, 1000);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await enButton.click();
|
|||
|
|
const slowOpacity = await slowOpacityPromise;
|
|||
|
|
|
|||
|
|
await page.waitForLoadState('networkidle');
|
|||
|
|
const slowResponseTime = measureTime(slowClickTime);
|
|||
|
|
|
|||
|
|
log('info', `Slow request completed in ${slowResponseTime}`);
|
|||
|
|
log('info', `Mid-request opacity: ${slowOpacity}`);
|
|||
|
|
|
|||
|
|
if (slowOpacity >= 0.9) {
|
|||
|
|
log('pass', `Indicator visible during slow request (opacity: ${slowOpacity})`);
|
|||
|
|
} else {
|
|||
|
|
log('fail', `Indicator not visible during slow request (opacity: ${slowOpacity})`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Unroute to restore normal speed
|
|||
|
|
await page.unroute('**/switch-language**');
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
log('fail', `Test 1 error: ${error.message}`);
|
|||
|
|
console.error(error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function test2_ShortcutsButtonVisibility(page) {
|
|||
|
|
log('info', '═══════════════════════════════════════════════════════');
|
|||
|
|
log('info', 'TEST 2: Shortcuts Button Visibility (Feature 001)');
|
|||
|
|
log('info', '═══════════════════════════════════════════════════════');
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// Ensure we're on the page
|
|||
|
|
await page.goto(BASE_URL);
|
|||
|
|
await page.waitForLoadState('networkidle');
|
|||
|
|
|
|||
|
|
// Test 2.1: Verify button exists and is visible
|
|||
|
|
log('info', 'Test 2.1: Checking shortcuts button exists...');
|
|||
|
|
const shortcutsBtn = page.locator('.shortcuts-btn');
|
|||
|
|
|
|||
|
|
await shortcutsBtn.waitFor({ state: 'visible', timeout: 5000 });
|
|||
|
|
log('pass', 'Shortcuts button found and visible');
|
|||
|
|
|
|||
|
|
// Test 2.2: Measure initial opacity
|
|||
|
|
log('info', 'Test 2.2: Measuring button opacity...');
|
|||
|
|
const opacity = await shortcutsBtn.evaluate(el =>
|
|||
|
|
window.getComputedStyle(el).opacity
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const opacityNum = parseFloat(opacity);
|
|||
|
|
log('info', `Button opacity: ${opacity}`);
|
|||
|
|
|
|||
|
|
if (opacityNum === 0.6) {
|
|||
|
|
log('pass', `Button opacity is exactly 0.6 as expected`);
|
|||
|
|
} else if (opacityNum >= 0.5 && opacityNum <= 0.7) {
|
|||
|
|
log('warn', `Button opacity close to target (${opacity} vs 0.6)`);
|
|||
|
|
} else {
|
|||
|
|
log('fail', `Button opacity incorrect (${opacity}, expected 0.6)`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Test 2.3: Verify button is actually visible to users
|
|||
|
|
log('info', 'Test 2.3: Verifying visual discoverability...');
|
|||
|
|
const boundingBox = await shortcutsBtn.boundingBox();
|
|||
|
|
|
|||
|
|
if (boundingBox) {
|
|||
|
|
log('pass', `Button has dimensions: ${boundingBox.width}x${boundingBox.height}px`);
|
|||
|
|
log('info', `Position: (${boundingBox.x}, ${boundingBox.y})`);
|
|||
|
|
} else {
|
|||
|
|
log('fail', 'Button has no bounding box (may not be rendered)');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Test 2.4: Test hover state
|
|||
|
|
log('info', 'Test 2.4: Testing hover state...');
|
|||
|
|
await shortcutsBtn.hover();
|
|||
|
|
await sleep(500); // Wait for transition
|
|||
|
|
|
|||
|
|
const hoverOpacity = await shortcutsBtn.evaluate(el =>
|
|||
|
|
window.getComputedStyle(el).opacity
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (parseFloat(hoverOpacity) === 1.0) {
|
|||
|
|
log('pass', `Hover opacity is 1.0 (full visibility)`);
|
|||
|
|
} else {
|
|||
|
|
log('warn', `Hover opacity: ${hoverOpacity} (expected 1.0)`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Test 2.5: Take screenshot
|
|||
|
|
log('info', 'Test 2.5: Capturing button screenshot...');
|
|||
|
|
await page.screenshot({
|
|||
|
|
path: '/Users/txeo/Git/yo/cv/test-screenshots/shortcuts-button-visible.png',
|
|||
|
|
fullPage: false
|
|||
|
|
});
|
|||
|
|
log('pass', 'Screenshot captured: test-screenshots/shortcuts-button-visible.png');
|
|||
|
|
|
|||
|
|
// Test 2.6: Verify functionality
|
|||
|
|
log('info', 'Test 2.6: Testing button functionality...');
|
|||
|
|
await shortcutsBtn.click();
|
|||
|
|
await sleep(300);
|
|||
|
|
|
|||
|
|
// Check if modal opened
|
|||
|
|
const modal = page.locator('.shortcuts-modal, [id*="shortcut"], [class*="modal"]');
|
|||
|
|
const modalVisible = await modal.isVisible().catch(() => false);
|
|||
|
|
|
|||
|
|
if (modalVisible) {
|
|||
|
|
log('pass', 'Shortcuts modal opened successfully');
|
|||
|
|
|
|||
|
|
// Test ESC to close
|
|||
|
|
await page.keyboard.press('Escape');
|
|||
|
|
await sleep(300);
|
|||
|
|
|
|||
|
|
const modalClosed = await modal.isVisible().catch(() => false);
|
|||
|
|
if (!modalClosed) {
|
|||
|
|
log('pass', 'Modal closes with ESC key');
|
|||
|
|
} else {
|
|||
|
|
log('warn', 'Modal may not close with ESC');
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
log('fail', 'Modal did not open on button click');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Test 2.7: Check info button consistency
|
|||
|
|
log('info', 'Test 2.7: Verifying info button has same opacity...');
|
|||
|
|
const infoBtn = page.locator('.info-button');
|
|||
|
|
const infoBtnExists = await infoBtn.count();
|
|||
|
|
|
|||
|
|
if (infoBtnExists > 0) {
|
|||
|
|
const infoOpacity = await infoBtn.evaluate(el =>
|
|||
|
|
window.getComputedStyle(el).opacity
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (parseFloat(infoOpacity) === 0.6) {
|
|||
|
|
log('pass', `Info button also has opacity 0.6 (consistency maintained)`);
|
|||
|
|
} else {
|
|||
|
|
log('warn', `Info button opacity: ${infoOpacity} (expected 0.6)`);
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
log('info', 'Info button not found (may not be on this page)');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
log('fail', `Test 2 error: ${error.message}`);
|
|||
|
|
console.error(error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function test3_RegressionTests(page) {
|
|||
|
|
log('info', '═══════════════════════════════════════════════════════');
|
|||
|
|
log('info', 'TEST 3: Regression Testing (Ensure Nothing Broke)');
|
|||
|
|
log('info', '═══════════════════════════════════════════════════════');
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
await page.goto(BASE_URL);
|
|||
|
|
await page.waitForLoadState('networkidle');
|
|||
|
|
|
|||
|
|
// Test 3.1: Skeleton loader still works
|
|||
|
|
log('info', 'Test 3.1: Verifying skeleton loader animation...');
|
|||
|
|
|
|||
|
|
const skeletonExists = await page.locator('#skeleton-loader').count();
|
|||
|
|
if (skeletonExists > 0) {
|
|||
|
|
log('pass', 'Skeleton loader element found');
|
|||
|
|
|
|||
|
|
// Trigger language switch
|
|||
|
|
const esButton = page.locator('button.selector-btn[data-short="ES"]');
|
|||
|
|
|
|||
|
|
const skeletonActivated = await page.evaluate(() => {
|
|||
|
|
return new Promise(resolve => {
|
|||
|
|
const skeleton = document.querySelector('#skeleton-loader');
|
|||
|
|
const observer = new MutationObserver((mutations) => {
|
|||
|
|
for (const mutation of mutations) {
|
|||
|
|
if (mutation.target.classList.contains('active')) {
|
|||
|
|
observer.disconnect();
|
|||
|
|
resolve(true);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
observer.observe(skeleton, { attributes: true, attributeFilter: ['class'] });
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
observer.disconnect();
|
|||
|
|
resolve(false);
|
|||
|
|
}, 2000);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await esButton.click();
|
|||
|
|
await page.waitForLoadState('networkidle');
|
|||
|
|
|
|||
|
|
if (skeletonActivated) {
|
|||
|
|
log('pass', 'Skeleton loader activated during language switch');
|
|||
|
|
} else {
|
|||
|
|
log('warn', 'Skeleton loader may not be activating');
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
log('info', 'Skeleton loader not found (may not be used)');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Test 3.2: No console errors
|
|||
|
|
log('info', 'Test 3.2: Checking for console errors...');
|
|||
|
|
const errors = [];
|
|||
|
|
page.on('console', msg => {
|
|||
|
|
if (msg.type() === 'error') {
|
|||
|
|
errors.push(msg.text());
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await page.reload();
|
|||
|
|
await sleep(1000);
|
|||
|
|
|
|||
|
|
if (errors.length === 0) {
|
|||
|
|
log('pass', 'No console errors detected');
|
|||
|
|
} else {
|
|||
|
|
log('fail', `Console errors found: ${errors.join(', ')}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Test 3.3: No layout shifts
|
|||
|
|
log('info', 'Test 3.3: Measuring Cumulative Layout Shift...');
|
|||
|
|
|
|||
|
|
const cls = await page.evaluate(() => {
|
|||
|
|
return new Promise(resolve => {
|
|||
|
|
let clsValue = 0;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const observer = new PerformanceObserver((list) => {
|
|||
|
|
for (const entry of list.getEntries()) {
|
|||
|
|
if (!entry.hadRecentInput) {
|
|||
|
|
clsValue += entry.value;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
observer.observe({ type: 'layout-shift', buffered: true });
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
observer.disconnect();
|
|||
|
|
resolve(clsValue);
|
|||
|
|
}, 2000);
|
|||
|
|
} catch (e) {
|
|||
|
|
resolve(0);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
log('info', `CLS Score: ${cls.toFixed(3)}`);
|
|||
|
|
|
|||
|
|
if (cls < 0.1) {
|
|||
|
|
log('pass', 'Excellent CLS score (< 0.1)');
|
|||
|
|
} else if (cls < 0.25) {
|
|||
|
|
log('warn', `CLS needs improvement (${cls.toFixed(3)})`);
|
|||
|
|
} else {
|
|||
|
|
log('fail', `Poor CLS score (${cls.toFixed(3)})`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Test 3.4: Page load performance
|
|||
|
|
log('info', 'Test 3.4: Measuring page load performance...');
|
|||
|
|
|
|||
|
|
const perfMetrics = await page.evaluate(() => {
|
|||
|
|
const perf = performance.getEntriesByType('navigation')[0];
|
|||
|
|
return {
|
|||
|
|
loadTime: perf.loadEventEnd - perf.fetchStart,
|
|||
|
|
domContentLoaded: perf.domContentLoadedEventEnd - perf.fetchStart,
|
|||
|
|
firstPaint: performance.getEntriesByType('paint')[0]?.startTime || 0
|
|||
|
|
};
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
log('info', `Load time: ${perfMetrics.loadTime.toFixed(0)}ms`);
|
|||
|
|
log('info', `DOMContentLoaded: ${perfMetrics.domContentLoaded.toFixed(0)}ms`);
|
|||
|
|
log('info', `First Paint: ${perfMetrics.firstPaint.toFixed(0)}ms`);
|
|||
|
|
|
|||
|
|
if (perfMetrics.loadTime < 3000) {
|
|||
|
|
log('pass', 'Page loads in under 3 seconds');
|
|||
|
|
} else {
|
|||
|
|
log('warn', `Page load time: ${perfMetrics.loadTime.toFixed(0)}ms`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
log('fail', `Test 3 error: ${error.message}`);
|
|||
|
|
console.error(error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function generateReport() {
|
|||
|
|
log('info', '═══════════════════════════════════════════════════════');
|
|||
|
|
log('info', 'FINAL TEST REPORT');
|
|||
|
|
log('info', '═══════════════════════════════════════════════════════');
|
|||
|
|
|
|||
|
|
console.log('\n📊 SUMMARY:');
|
|||
|
|
console.log(` ✅ Passed: ${RESULTS.passed.length}`);
|
|||
|
|
console.log(` ❌ Failed: ${RESULTS.failed.length}`);
|
|||
|
|
console.log(` ⚠️ Warnings: ${RESULTS.warnings.length}`);
|
|||
|
|
|
|||
|
|
if (RESULTS.failed.length > 0) {
|
|||
|
|
console.log('\n❌ FAILURES:');
|
|||
|
|
RESULTS.failed.forEach((msg, i) => console.log(` ${i + 1}. ${msg}`));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (RESULTS.warnings.length > 0) {
|
|||
|
|
console.log('\n⚠️ WARNINGS:');
|
|||
|
|
RESULTS.warnings.forEach((msg, i) => console.log(` ${i + 1}. ${msg}`));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log('\n📈 FEATURE GRADES:');
|
|||
|
|
|
|||
|
|
// Feature 003: HTMX Indicators
|
|||
|
|
const indicatorTests = RESULTS.passed.filter(m =>
|
|||
|
|
m.includes('indicator') || m.includes('Indicator')
|
|||
|
|
).length;
|
|||
|
|
const indicatorFails = RESULTS.failed.filter(m =>
|
|||
|
|
m.includes('indicator') || m.includes('Indicator')
|
|||
|
|
).length;
|
|||
|
|
|
|||
|
|
let feature003Grade = 'F';
|
|||
|
|
if (indicatorFails === 0 && indicatorTests >= 5) feature003Grade = 'A';
|
|||
|
|
else if (indicatorFails === 0 && indicatorTests >= 3) feature003Grade = 'B';
|
|||
|
|
else if (indicatorFails <= 1) feature003Grade = 'C';
|
|||
|
|
else if (indicatorFails <= 2) feature003Grade = 'D';
|
|||
|
|
|
|||
|
|
console.log(` Feature 003 (HTMX Indicators): ${feature003Grade} (${indicatorTests} tests passed, ${indicatorFails} failed)`);
|
|||
|
|
|
|||
|
|
// Feature 001: Shortcuts Button
|
|||
|
|
const buttonTests = RESULTS.passed.filter(m =>
|
|||
|
|
m.includes('Button') || m.includes('button') || m.includes('opacity')
|
|||
|
|
).length;
|
|||
|
|
const buttonFails = RESULTS.failed.filter(m =>
|
|||
|
|
m.includes('Button') || m.includes('button') || m.includes('opacity')
|
|||
|
|
).length;
|
|||
|
|
|
|||
|
|
let feature001Grade = 'A-';
|
|||
|
|
if (buttonFails === 0 && buttonTests >= 6) feature001Grade = 'A';
|
|||
|
|
else if (buttonFails === 0 && buttonTests >= 4) feature001Grade = 'A-';
|
|||
|
|
else if (buttonFails <= 1) feature001Grade = 'B+';
|
|||
|
|
else if (buttonFails <= 2) feature001Grade = 'B';
|
|||
|
|
|
|||
|
|
console.log(` Feature 001 (Shortcuts Button): ${feature001Grade} (${buttonTests} tests passed, ${buttonFails} failed)`);
|
|||
|
|
|
|||
|
|
console.log('\n📸 SCREENSHOTS:');
|
|||
|
|
console.log(' - test-screenshots/htmx-indicator-loading.png');
|
|||
|
|
console.log(' - test-screenshots/shortcuts-button-visible.png');
|
|||
|
|
|
|||
|
|
const overallSuccess = RESULTS.failed.length === 0;
|
|||
|
|
console.log(`\n${overallSuccess ? '✅ ALL TESTS PASSED' : '❌ SOME TESTS FAILED'}\n`);
|
|||
|
|
|
|||
|
|
return overallSuccess;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function main() {
|
|||
|
|
console.log('🧪 COMPREHENSIVE VERIFICATION TEST SUITE');
|
|||
|
|
console.log('Testing HTMX Indicators + Shortcuts Button Fixes\n');
|
|||
|
|
|
|||
|
|
// Create screenshots directory
|
|||
|
|
const { mkdir } = await import('fs/promises');
|
|||
|
|
await mkdir('/Users/txeo/Git/yo/cv/test-screenshots', { recursive: true });
|
|||
|
|
|
|||
|
|
const browser = await chromium.launch({
|
|||
|
|
headless: true,
|
|||
|
|
args: ['--disable-dev-shm-usage']
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const context = await browser.newContext({
|
|||
|
|
viewport: { width: 1920, height: 1080 },
|
|||
|
|
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const page = await context.newPage();
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// Run all test suites
|
|||
|
|
await test1_HTMXLoadingIndicators(page);
|
|||
|
|
await test2_ShortcutsButtonVisibility(page);
|
|||
|
|
await test3_RegressionTests(page);
|
|||
|
|
|
|||
|
|
// Generate report
|
|||
|
|
const success = await generateReport();
|
|||
|
|
|
|||
|
|
await browser.close();
|
|||
|
|
|
|||
|
|
process.exit(success ? 0 : 1);
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Fatal error:', error);
|
|||
|
|
await browser.close();
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
main();
|