Files
bookmarksite/tests/test_mobile_interactions.html
Rainer Koschnick 0abee5b794 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
2025-07-19 23:21:50 +02:00

589 lines
23 KiB
HTML

<!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>