Add comprehensive database setup and user management system
- Implement PostgreSQL database schema with users and bookmarks tables - Add database connection pooling with retry logic and error handling - Create migration system with automatic schema initialization - Add database CLI tools for management (init, status, validate, etc.) - Include comprehensive error handling and diagnostics - Add development seed data and testing utilities - Implement health monitoring and connection pool statistics - Create detailed documentation and troubleshooting guide Database features: - Users table with authentication fields and email verification - Bookmarks table with user association and metadata - Proper indexes for performance optimization - Automatic timestamp triggers - Transaction support with rollback handling - Connection pooling (20 max connections, 30s idle timeout) - Graceful shutdown handling CLI commands available: - npm run db:init - Initialize database - npm run db:status - Check database status - npm run db:validate - Validate schema - npm run db:test - Run database tests - npm run db:diagnostics - Full diagnostics
This commit is contained in:
589
tests/test_mobile_interactions.html
Normal file
589
tests/test_mobile_interactions.html
Normal file
@ -0,0 +1,589 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mobile Touch Interactions Test - Bookmark Manager</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<style>
|
||||
/* Test-specific styles */
|
||||
.test-container {
|
||||
max-width: 400px;
|
||||
margin: 20px auto;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.test-section {
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.test-section h3 {
|
||||
margin-top: 0;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.test-instructions {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 15px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.test-bookmark {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin: 10px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-height: 60px;
|
||||
touch-action: pan-x;
|
||||
transition: transform 0.3s ease, background-color 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.test-bookmark.swiping {
|
||||
transform: translateX(var(--swipe-offset, 0));
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.test-bookmark.swipe-left {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.test-bookmark.swipe-right {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.test-bookmark::before,
|
||||
.test-bookmark::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.test-bookmark::before {
|
||||
left: 20px;
|
||||
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" fill="white" viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>') no-repeat center;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.test-bookmark::after {
|
||||
right: 20px;
|
||||
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" fill="white" viewBox="0 0 24 24"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>') no-repeat center;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.test-bookmark.swipe-right::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.test-bookmark.swipe-left::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.test-bookmark-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.test-bookmark-title {
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.test-bookmark-url {
|
||||
font-size: 14px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.test-status {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pull-refresh-area {
|
||||
height: 200px;
|
||||
border: 2px dashed #dee2e6;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #6c757d;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
background: #f8f9fa;
|
||||
touch-action: pan-y;
|
||||
}
|
||||
|
||||
.test-results {
|
||||
background: #e3f2fd;
|
||||
border: 1px solid #2196f3;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin-top: 15px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.test-results h4 {
|
||||
margin-top: 0;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.test-log {
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.device-info {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffc107;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.device-info h4 {
|
||||
margin-top: 0;
|
||||
color: #856404;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>Mobile Touch Interactions Test</h1>
|
||||
<p>Test the mobile touch features of the Bookmark Manager</p>
|
||||
</header>
|
||||
|
||||
<div class="test-container">
|
||||
<div class="device-info">
|
||||
<h4>Device Information</h4>
|
||||
<div id="deviceInfo">Loading device information...</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>1. Swipe Gestures Test</h3>
|
||||
<div class="test-instructions">
|
||||
<strong>Instructions:</strong><br>
|
||||
• Swipe RIGHT on a bookmark to test the link<br>
|
||||
• Swipe LEFT on a bookmark to delete it<br>
|
||||
• Tap normally to show context menu<br>
|
||||
• Minimum swipe distance: 100px
|
||||
</div>
|
||||
|
||||
<div class="test-bookmark" data-bookmark-id="test1">
|
||||
<div class="test-bookmark-info">
|
||||
<div class="test-bookmark-title">Test Bookmark 1</div>
|
||||
<div class="test-bookmark-url">https://example.com</div>
|
||||
</div>
|
||||
<div class="test-status">?</div>
|
||||
</div>
|
||||
|
||||
<div class="test-bookmark" data-bookmark-id="test2">
|
||||
<div class="test-bookmark-info">
|
||||
<div class="test-bookmark-title">Test Bookmark 2</div>
|
||||
<div class="test-bookmark-url">https://google.com</div>
|
||||
</div>
|
||||
<div class="test-status">?</div>
|
||||
</div>
|
||||
|
||||
<div class="test-results">
|
||||
<h4>Swipe Test Results</h4>
|
||||
<div id="swipeResults">No swipe actions detected yet.</div>
|
||||
<div class="test-log" id="swipeLog"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>2. Pull-to-Refresh Test</h3>
|
||||
<div class="test-instructions">
|
||||
<strong>Instructions:</strong><br>
|
||||
• Pull down from the top of the page<br>
|
||||
• Pull at least 80px to trigger refresh<br>
|
||||
• Watch for the refresh indicator
|
||||
</div>
|
||||
|
||||
<div class="pull-refresh-area" id="pullRefreshArea">
|
||||
Pull down from the top of the page to test pull-to-refresh
|
||||
</div>
|
||||
|
||||
<div class="test-results">
|
||||
<h4>Pull-to-Refresh Results</h4>
|
||||
<div id="pullResults">No pull-to-refresh actions detected yet.</div>
|
||||
<div class="test-log" id="pullLog"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>3. Touch Target Size Test</h3>
|
||||
<div class="test-instructions">
|
||||
<strong>Instructions:</strong><br>
|
||||
• All interactive elements should be at least 44px in size<br>
|
||||
• Buttons should be easy to tap on mobile devices
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 10px; margin: 15px 0;">
|
||||
<button class="btn btn-primary">Primary Button</button>
|
||||
<button class="btn btn-secondary">Secondary</button>
|
||||
<button class="btn btn-small">Small Button</button>
|
||||
</div>
|
||||
|
||||
<div class="test-results">
|
||||
<h4>Touch Target Analysis</h4>
|
||||
<div id="touchTargetResults">Analyzing touch targets...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
class MobileTouchTester {
|
||||
constructor() {
|
||||
this.touchState = {
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
currentX: 0,
|
||||
currentY: 0,
|
||||
isDragging: false,
|
||||
swipeThreshold: 100,
|
||||
currentBookmark: null,
|
||||
swipeDirection: null
|
||||
};
|
||||
|
||||
this.pullToRefresh = {
|
||||
startY: 0,
|
||||
currentY: 0,
|
||||
threshold: 80,
|
||||
isActive: false,
|
||||
isPulling: false
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.displayDeviceInfo();
|
||||
this.initializeSwipeTest();
|
||||
this.initializePullToRefreshTest();
|
||||
this.analyzeTouchTargets();
|
||||
}
|
||||
|
||||
displayDeviceInfo() {
|
||||
const info = document.getElementById('deviceInfo');
|
||||
const isMobile = this.isMobileDevice();
|
||||
const hasTouch = 'ontouchstart' in window;
|
||||
const maxTouchPoints = navigator.maxTouchPoints || 0;
|
||||
|
||||
info.innerHTML = `
|
||||
<strong>User Agent:</strong> ${navigator.userAgent}<br>
|
||||
<strong>Is Mobile Device:</strong> ${isMobile ? 'Yes' : 'No'}<br>
|
||||
<strong>Touch Support:</strong> ${hasTouch ? 'Yes' : 'No'}<br>
|
||||
<strong>Max Touch Points:</strong> ${maxTouchPoints}<br>
|
||||
<strong>Screen Size:</strong> ${window.screen.width}x${window.screen.height}<br>
|
||||
<strong>Viewport Size:</strong> ${window.innerWidth}x${window.innerHeight}
|
||||
`;
|
||||
}
|
||||
|
||||
isMobileDevice() {
|
||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
|
||||
('ontouchstart' in window) ||
|
||||
(navigator.maxTouchPoints > 0);
|
||||
}
|
||||
|
||||
initializeSwipeTest() {
|
||||
const bookmarks = document.querySelectorAll('.test-bookmark');
|
||||
|
||||
bookmarks.forEach(bookmark => {
|
||||
bookmark.addEventListener('touchstart', this.handleTouchStart.bind(this), { passive: false });
|
||||
bookmark.addEventListener('touchmove', this.handleTouchMove.bind(this), { passive: false });
|
||||
bookmark.addEventListener('touchend', this.handleTouchEnd.bind(this), { passive: false });
|
||||
});
|
||||
}
|
||||
|
||||
handleTouchStart(e) {
|
||||
const touch = e.touches[0];
|
||||
const bookmarkItem = e.currentTarget;
|
||||
|
||||
this.touchState.startX = touch.clientX;
|
||||
this.touchState.startY = touch.clientY;
|
||||
this.touchState.currentX = touch.clientX;
|
||||
this.touchState.currentY = touch.clientY;
|
||||
this.touchState.isDragging = false;
|
||||
this.touchState.currentBookmark = bookmarkItem;
|
||||
this.touchState.swipeDirection = null;
|
||||
|
||||
bookmarkItem.classList.add('swiping');
|
||||
this.logSwipeAction(`Touch start at (${touch.clientX}, ${touch.clientY})`);
|
||||
}
|
||||
|
||||
handleTouchMove(e) {
|
||||
if (!this.touchState.currentBookmark) return;
|
||||
|
||||
const touch = e.touches[0];
|
||||
const bookmarkItem = e.currentTarget;
|
||||
|
||||
this.touchState.currentX = touch.clientX;
|
||||
this.touchState.currentY = touch.clientY;
|
||||
|
||||
const deltaX = this.touchState.currentX - this.touchState.startX;
|
||||
const deltaY = this.touchState.currentY - this.touchState.startY;
|
||||
|
||||
if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 10) {
|
||||
e.preventDefault();
|
||||
this.touchState.isDragging = true;
|
||||
|
||||
bookmarkItem.style.setProperty('--swipe-offset', `${deltaX}px`);
|
||||
|
||||
if (deltaX > 30) {
|
||||
bookmarkItem.classList.add('swipe-right');
|
||||
bookmarkItem.classList.remove('swipe-left');
|
||||
this.touchState.swipeDirection = 'right';
|
||||
} else if (deltaX < -30) {
|
||||
bookmarkItem.classList.add('swipe-left');
|
||||
bookmarkItem.classList.remove('swipe-right');
|
||||
this.touchState.swipeDirection = 'left';
|
||||
} else {
|
||||
bookmarkItem.classList.remove('swipe-left', 'swipe-right');
|
||||
this.touchState.swipeDirection = null;
|
||||
}
|
||||
|
||||
this.logSwipeAction(`Swiping ${this.touchState.swipeDirection || 'neutral'}: ${deltaX}px`);
|
||||
}
|
||||
}
|
||||
|
||||
handleTouchEnd(e) {
|
||||
const bookmarkItem = e.currentTarget;
|
||||
const deltaX = this.touchState.currentX - this.touchState.startX;
|
||||
|
||||
bookmarkItem.classList.remove('swiping', 'swipe-left', 'swipe-right');
|
||||
bookmarkItem.style.removeProperty('--swipe-offset');
|
||||
|
||||
if (this.touchState.isDragging && Math.abs(deltaX) > this.touchState.swipeThreshold) {
|
||||
if (this.touchState.swipeDirection === 'right') {
|
||||
this.handleSwipeRight(bookmarkItem);
|
||||
} else if (this.touchState.swipeDirection === 'left') {
|
||||
this.handleSwipeLeft(bookmarkItem);
|
||||
}
|
||||
} else if (!this.touchState.isDragging) {
|
||||
this.handleTap(bookmarkItem);
|
||||
}
|
||||
|
||||
this.resetTouchState();
|
||||
}
|
||||
|
||||
handleSwipeRight(bookmark) {
|
||||
const title = bookmark.querySelector('.test-bookmark-title').textContent;
|
||||
this.logSwipeAction(`✅ Swipe RIGHT detected on "${title}" - Testing link`);
|
||||
this.showFeedback('Testing link...', 'success');
|
||||
}
|
||||
|
||||
handleSwipeLeft(bookmark) {
|
||||
const title = bookmark.querySelector('.test-bookmark-title').textContent;
|
||||
this.logSwipeAction(`❌ Swipe LEFT detected on "${title}" - Deleting bookmark`);
|
||||
this.showFeedback('Bookmark deleted', 'danger');
|
||||
}
|
||||
|
||||
handleTap(bookmark) {
|
||||
const title = bookmark.querySelector('.test-bookmark-title').textContent;
|
||||
this.logSwipeAction(`👆 TAP detected on "${title}" - Showing context menu`);
|
||||
this.showFeedback('Context menu opened', 'info');
|
||||
}
|
||||
|
||||
resetTouchState() {
|
||||
this.touchState = {
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
currentX: 0,
|
||||
currentY: 0,
|
||||
isDragging: false,
|
||||
swipeThreshold: 100,
|
||||
currentBookmark: null,
|
||||
swipeDirection: null
|
||||
};
|
||||
}
|
||||
|
||||
initializePullToRefreshTest() {
|
||||
document.addEventListener('touchstart', this.handlePullStart.bind(this), { passive: false });
|
||||
document.addEventListener('touchmove', this.handlePullMove.bind(this), { passive: false });
|
||||
document.addEventListener('touchend', this.handlePullEnd.bind(this), { passive: false });
|
||||
}
|
||||
|
||||
handlePullStart(e) {
|
||||
if (window.scrollY > 0) return;
|
||||
|
||||
const touch = e.touches[0];
|
||||
this.pullToRefresh.startY = touch.clientY;
|
||||
this.pullToRefresh.currentY = touch.clientY;
|
||||
this.pullToRefresh.isPulling = false;
|
||||
|
||||
this.logPullAction(`Pull start at Y: ${touch.clientY}`);
|
||||
}
|
||||
|
||||
handlePullMove(e) {
|
||||
if (window.scrollY > 0 || !this.pullToRefresh.startY) return;
|
||||
|
||||
const touch = e.touches[0];
|
||||
this.pullToRefresh.currentY = touch.clientY;
|
||||
const deltaY = this.pullToRefresh.currentY - this.pullToRefresh.startY;
|
||||
|
||||
if (deltaY > 0) {
|
||||
e.preventDefault();
|
||||
this.pullToRefresh.isPulling = true;
|
||||
|
||||
const progress = Math.min(deltaY / this.pullToRefresh.threshold, 1);
|
||||
const area = document.getElementById('pullRefreshArea');
|
||||
|
||||
if (deltaY > this.pullToRefresh.threshold) {
|
||||
area.style.background = '#d4edda';
|
||||
area.style.borderColor = '#28a745';
|
||||
area.innerHTML = `🔄 Release to refresh (${Math.round(deltaY)}px)`;
|
||||
this.logPullAction(`Pull threshold exceeded: ${deltaY}px`);
|
||||
} else {
|
||||
area.style.background = '#fff3cd';
|
||||
area.style.borderColor = '#ffc107';
|
||||
area.innerHTML = `⬇️ Pull to refresh (${Math.round(deltaY)}px / ${this.pullToRefresh.threshold}px)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handlePullEnd(e) {
|
||||
if (!this.pullToRefresh.isPulling) return;
|
||||
|
||||
const deltaY = this.pullToRefresh.currentY - this.pullToRefresh.startY;
|
||||
const area = document.getElementById('pullRefreshArea');
|
||||
|
||||
area.style.background = '#f8f9fa';
|
||||
area.style.borderColor = '#dee2e6';
|
||||
area.innerHTML = 'Pull down from the top of the page to test pull-to-refresh';
|
||||
|
||||
if (deltaY > this.pullToRefresh.threshold) {
|
||||
this.triggerPullToRefresh();
|
||||
}
|
||||
|
||||
this.pullToRefresh.startY = 0;
|
||||
this.pullToRefresh.currentY = 0;
|
||||
this.pullToRefresh.isPulling = false;
|
||||
}
|
||||
|
||||
triggerPullToRefresh() {
|
||||
this.logPullAction('✅ Pull-to-refresh triggered!');
|
||||
this.showFeedback('Refreshing...', 'info');
|
||||
}
|
||||
|
||||
analyzeTouchTargets() {
|
||||
const buttons = document.querySelectorAll('.btn');
|
||||
const results = document.getElementById('touchTargetResults');
|
||||
let analysis = '';
|
||||
|
||||
buttons.forEach((button, index) => {
|
||||
const rect = button.getBoundingClientRect();
|
||||
const width = rect.width;
|
||||
const height = rect.height;
|
||||
const meetsStandard = width >= 44 && height >= 44;
|
||||
|
||||
analysis += `Button ${index + 1}: ${width.toFixed(0)}x${height.toFixed(0)}px ${meetsStandard ? '✅' : '❌'}<br>`;
|
||||
});
|
||||
|
||||
results.innerHTML = analysis;
|
||||
}
|
||||
|
||||
showFeedback(message, type = 'info') {
|
||||
let feedback = document.getElementById('mobile-feedback');
|
||||
if (!feedback) {
|
||||
feedback = document.createElement('div');
|
||||
feedback.id = 'mobile-feedback';
|
||||
feedback.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
z-index: 1002;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
`;
|
||||
document.body.appendChild(feedback);
|
||||
}
|
||||
|
||||
const colors = {
|
||||
success: '#28a745',
|
||||
danger: '#dc3545',
|
||||
info: '#17a2b8',
|
||||
warning: '#ffc107'
|
||||
};
|
||||
|
||||
feedback.textContent = message;
|
||||
feedback.style.backgroundColor = colors[type] || colors.info;
|
||||
feedback.style.opacity = '1';
|
||||
|
||||
setTimeout(() => {
|
||||
feedback.style.opacity = '0';
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
logSwipeAction(message) {
|
||||
const log = document.getElementById('swipeLog');
|
||||
const results = document.getElementById('swipeResults');
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
|
||||
log.innerHTML += `[${timestamp}] ${message}\n`;
|
||||
log.scrollTop = log.scrollHeight;
|
||||
|
||||
results.textContent = `Last action: ${message}`;
|
||||
}
|
||||
|
||||
logPullAction(message) {
|
||||
const log = document.getElementById('pullLog');
|
||||
const results = document.getElementById('pullResults');
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
|
||||
log.innerHTML += `[${timestamp}] ${message}\n`;
|
||||
log.scrollTop = log.scrollHeight;
|
||||
|
||||
results.textContent = `Last action: ${message}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the tester when the page loads
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new MobileTouchTester();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user