- 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
508 lines
24 KiB
HTML
508 lines
24 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Advanced Import Integration Test</title>
|
|
<link rel="stylesheet" href="styles.css">
|
|
<style>
|
|
.test-container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
}
|
|
.test-section {
|
|
margin: 30px 0;
|
|
padding: 20px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 8px;
|
|
background: #f9f9f9;
|
|
}
|
|
.test-result {
|
|
margin: 15px 0;
|
|
padding: 10px;
|
|
border-radius: 4px;
|
|
}
|
|
.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
|
|
.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
|
|
.info { background: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; }
|
|
.sample-data {
|
|
background: #f8f9fa;
|
|
border: 1px solid #e9ecef;
|
|
border-radius: 4px;
|
|
padding: 10px;
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
margin: 10px 0;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="test-container">
|
|
<h1>Advanced Import/Export Integration Test</h1>
|
|
<p>This page tests the advanced import/export features integrated with the main bookmark manager.</p>
|
|
|
|
<div class="test-section">
|
|
<h2>Test 1: Enhanced Import Modal</h2>
|
|
<p>Test the new import modal with multiple format support and preview functionality.</p>
|
|
<button id="testImportModal" class="btn btn-primary">Open Import Modal</button>
|
|
<div id="importModalResult" class="test-result"></div>
|
|
</div>
|
|
|
|
<div class="test-section">
|
|
<h2>Test 2: Chrome Bookmarks Import</h2>
|
|
<p>Test importing Chrome bookmarks JSON format.</p>
|
|
<div class="sample-data">
|
|
<strong>Sample Chrome Data:</strong><br>
|
|
{"roots":{"bookmark_bar":{"children":[{"type":"url","name":"Google","url":"https://www.google.com"},{"type":"folder","name":"Dev","children":[{"type":"url","name":"GitHub","url":"https://github.com"}]}]}}}
|
|
</div>
|
|
<button id="testChromeImport" class="btn btn-primary">Test Chrome Import</button>
|
|
<div id="chromeImportResult" class="test-result"></div>
|
|
</div>
|
|
|
|
<div class="test-section">
|
|
<h2>Test 3: Firefox Bookmarks Import</h2>
|
|
<p>Test importing Firefox bookmarks JSON format.</p>
|
|
<div class="sample-data">
|
|
<strong>Sample Firefox Data:</strong><br>
|
|
[{"type":"text/x-moz-place-container","title":"Menu","children":[{"type":"text/x-moz-place","title":"Mozilla","uri":"https://mozilla.org"}]}]
|
|
</div>
|
|
<button id="testFirefoxImport" class="btn btn-primary">Test Firefox Import</button>
|
|
<div id="firefoxImportResult" class="test-result"></div>
|
|
</div>
|
|
|
|
<div class="test-section">
|
|
<h2>Test 4: Import Preview</h2>
|
|
<p>Test the import preview functionality with duplicate detection.</p>
|
|
<button id="testImportPreview" class="btn btn-primary">Test Import Preview</button>
|
|
<div id="previewResult" class="test-result"></div>
|
|
</div>
|
|
|
|
<div class="test-section">
|
|
<h2>Test 5: Incremental Import</h2>
|
|
<p>Test incremental import with smart duplicate handling.</p>
|
|
<button id="testIncrementalImport" class="btn btn-primary">Test Incremental Import</button>
|
|
<div id="incrementalResult" class="test-result"></div>
|
|
</div>
|
|
|
|
<div class="test-section">
|
|
<h2>Test 6: Device Sync Features</h2>
|
|
<p>Test the device synchronization functionality.</p>
|
|
<button id="testSyncFeatures" class="btn btn-primary">Test Sync Features</button>
|
|
<div id="syncResult" class="test-result"></div>
|
|
</div>
|
|
|
|
<div class="test-section">
|
|
<h2>Current Bookmarks</h2>
|
|
<p>View current bookmarks in the manager:</p>
|
|
<button id="showBookmarks" class="btn btn-secondary">Show Current Bookmarks</button>
|
|
<div id="bookmarksDisplay" class="test-result"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Include the main application -->
|
|
<div style="display: none;">
|
|
<div id="importModal" class="modal">
|
|
<div class="modal-content advanced-import-content">
|
|
<button class="close">×</button>
|
|
<h2>Import Bookmarks</h2>
|
|
<div class="import-tabs">
|
|
<button class="import-tab active" data-tab="file">File Import</button>
|
|
<button class="import-tab" data-tab="sync">Device Sync</button>
|
|
</div>
|
|
<div id="fileImportTab" class="import-tab-content active">
|
|
<div class="form-group">
|
|
<label for="importFormat">Import Format:</label>
|
|
<select id="importFormat">
|
|
<option value="netscape">Netscape HTML</option>
|
|
<option value="chrome">Chrome JSON</option>
|
|
<option value="firefox">Firefox JSON</option>
|
|
<option value="auto">Auto-detect</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<input type="file" id="fileInput" accept=".html,.htm,.json">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="importMode">Import Mode:</label>
|
|
<select id="importMode">
|
|
<option value="preview">Preview before import</option>
|
|
<option value="merge">Merge with existing</option>
|
|
<option value="replace">Replace all</option>
|
|
<option value="incremental">Incremental import</option>
|
|
</select>
|
|
</div>
|
|
<div class="duplicate-handling" id="duplicateHandling" style="display: none;">
|
|
<h3>Duplicate Detection:</h3>
|
|
<label><input type="checkbox" id="normalizeUrls" checked> Normalize URLs</label>
|
|
<label><input type="checkbox" id="fuzzyTitleMatch"> Fuzzy title matching</label>
|
|
<select id="duplicateStrategy">
|
|
<option value="skip">Skip duplicates</option>
|
|
<option value="update">Update existing</option>
|
|
<option value="keep_newer">Keep newer</option>
|
|
<option value="keep_older">Keep older</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div id="syncImportTab" class="import-tab-content">
|
|
<div class="sync-section">
|
|
<h3>Device Synchronization</h3>
|
|
<select id="syncMethod">
|
|
<option value="cloud">Cloud Storage</option>
|
|
<option value="qr">QR Code Transfer</option>
|
|
<option value="local">Local Network</option>
|
|
</select>
|
|
<div id="cloudSyncOptions" class="sync-options">
|
|
<select id="cloudProvider">
|
|
<option value="gdrive">Google Drive</option>
|
|
<option value="dropbox">Dropbox</option>
|
|
</select>
|
|
<button id="connectCloudBtn" class="btn btn-secondary">Connect</button>
|
|
<div id="cloudStatus" class="cloud-status"></div>
|
|
</div>
|
|
<div id="qrSyncOptions" class="sync-options" style="display: none;">
|
|
<button id="generateQRBtn" class="btn btn-primary">Generate QR</button>
|
|
<div id="qrCodeDisplay" class="qr-display"></div>
|
|
<button id="scanQRBtn" class="btn btn-secondary">Scan QR</button>
|
|
<div id="qrScanArea" class="qr-scan-area"></div>
|
|
</div>
|
|
<div id="localSyncOptions" class="sync-options" style="display: none;">
|
|
<button id="startLocalServerBtn" class="btn btn-primary">Start Server</button>
|
|
<div id="localSyncStatus" class="sync-status"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button id="importFileBtn" class="btn btn-primary">Import</button>
|
|
<button id="previewImportBtn" class="btn btn-secondary">Preview</button>
|
|
<button id="cancelImportBtn" class="btn btn-secondary">Cancel</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="importPreviewModal" class="modal">
|
|
<div class="modal-content import-preview-content">
|
|
<button class="close">×</button>
|
|
<h2>Import Preview</h2>
|
|
<div class="import-summary">
|
|
<div class="import-stats">
|
|
<div class="stat-item">
|
|
<span class="stat-number" id="previewTotalCount">0</span>
|
|
<span class="stat-label">Total</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-number" id="previewNewCount">0</span>
|
|
<span class="stat-label">New</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-number" id="previewDuplicateCount">0</span>
|
|
<span class="stat-label">Duplicates</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-number" id="previewFolderCount">0</span>
|
|
<span class="stat-label">Folders</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="preview-tabs">
|
|
<button class="preview-tab active" data-tab="new">New Bookmarks</button>
|
|
<button class="preview-tab" data-tab="duplicates">Duplicates</button>
|
|
<button class="preview-tab" data-tab="folders">Folders</button>
|
|
</div>
|
|
<div class="preview-content">
|
|
<div id="newBookmarksPreview" class="preview-tab-content active">
|
|
<div id="newBookmarksList" class="preview-list"></div>
|
|
</div>
|
|
<div id="duplicatesPreview" class="preview-tab-content">
|
|
<div id="duplicatesList" class="preview-list"></div>
|
|
</div>
|
|
<div id="foldersPreview" class="preview-tab-content">
|
|
<div id="foldersList" class="preview-list"></div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button id="confirmImportBtn" class="btn btn-primary">Confirm Import</button>
|
|
<button id="modifyImportBtn" class="btn btn-secondary">Modify Settings</button>
|
|
<button id="cancelPreviewBtn" class="btn btn-secondary">Cancel</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="script.js"></script>
|
|
<script>
|
|
// Initialize the bookmark manager
|
|
const manager = new BookmarkManager();
|
|
|
|
// Test 1: Import Modal
|
|
document.getElementById('testImportModal').addEventListener('click', () => {
|
|
try {
|
|
manager.showModal('importModal');
|
|
document.getElementById('importModalResult').innerHTML =
|
|
'<div class="success">✅ Import modal opened successfully! Check if tabs and options are visible.</div>';
|
|
} catch (error) {
|
|
document.getElementById('importModalResult').innerHTML =
|
|
`<div class="error">❌ Failed to open import modal: ${error.message}</div>`;
|
|
}
|
|
});
|
|
|
|
// Test 2: Chrome Import
|
|
document.getElementById('testChromeImport').addEventListener('click', async () => {
|
|
const sampleData = {
|
|
"roots": {
|
|
"bookmark_bar": {
|
|
"children": [
|
|
{
|
|
"type": "url",
|
|
"name": "Google",
|
|
"url": "https://www.google.com",
|
|
"date_added": "13285166270000000"
|
|
},
|
|
{
|
|
"type": "folder",
|
|
"name": "Development",
|
|
"children": [
|
|
{
|
|
"type": "url",
|
|
"name": "GitHub",
|
|
"url": "https://github.com",
|
|
"date_added": "13285166280000000"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
};
|
|
|
|
try {
|
|
const bookmarks = manager.parseChromeBookmarks(JSON.stringify(sampleData));
|
|
document.getElementById('chromeImportResult').innerHTML = `
|
|
<div class="success">✅ Chrome import successful!</div>
|
|
<div class="info">Parsed ${bookmarks.length} bookmarks:
|
|
<ul>
|
|
${bookmarks.map(b => `<li><strong>${b.title}</strong> - ${b.url} (${b.folder || 'Root'})</li>`).join('')}
|
|
</ul>
|
|
</div>
|
|
`;
|
|
} catch (error) {
|
|
document.getElementById('chromeImportResult').innerHTML =
|
|
`<div class="error">❌ Chrome import failed: ${error.message}</div>`;
|
|
}
|
|
});
|
|
|
|
// Test 3: Firefox Import
|
|
document.getElementById('testFirefoxImport').addEventListener('click', async () => {
|
|
const sampleData = [
|
|
{
|
|
"type": "text/x-moz-place-container",
|
|
"title": "Bookmarks Menu",
|
|
"children": [
|
|
{
|
|
"type": "text/x-moz-place",
|
|
"title": "Mozilla Firefox",
|
|
"uri": "https://www.mozilla.org/firefox/",
|
|
"dateAdded": 1642534567890000
|
|
},
|
|
{
|
|
"type": "text/x-moz-place-container",
|
|
"title": "Development",
|
|
"children": [
|
|
{
|
|
"type": "text/x-moz-place",
|
|
"title": "MDN Web Docs",
|
|
"uri": "https://developer.mozilla.org/",
|
|
"dateAdded": 1642534577890000
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
];
|
|
|
|
try {
|
|
const bookmarks = manager.parseFirefoxBookmarks(JSON.stringify(sampleData));
|
|
document.getElementById('firefoxImportResult').innerHTML = `
|
|
<div class="success">✅ Firefox import successful!</div>
|
|
<div class="info">Parsed ${bookmarks.length} bookmarks:
|
|
<ul>
|
|
${bookmarks.map(b => `<li><strong>${b.title}</strong> - ${b.url} (${b.folder || 'Root'})</li>`).join('')}
|
|
</ul>
|
|
</div>
|
|
`;
|
|
} catch (error) {
|
|
document.getElementById('firefoxImportResult').innerHTML =
|
|
`<div class="error">❌ Firefox import failed: ${error.message}</div>`;
|
|
}
|
|
});
|
|
|
|
// Test 4: Import Preview
|
|
document.getElementById('testImportPreview').addEventListener('click', () => {
|
|
// Add some existing bookmarks for duplicate testing
|
|
manager.bookmarks = [
|
|
{
|
|
id: 'existing1',
|
|
title: 'Google Search',
|
|
url: 'https://www.google.com/',
|
|
folder: 'Search',
|
|
addDate: Date.now() - 1000000,
|
|
status: 'valid'
|
|
}
|
|
];
|
|
|
|
const importData = {
|
|
bookmarks: [
|
|
{
|
|
id: 'import1',
|
|
title: 'New Site',
|
|
url: 'https://example.com',
|
|
folder: 'Examples',
|
|
addDate: Date.now(),
|
|
status: 'unknown'
|
|
},
|
|
{
|
|
id: 'import2',
|
|
title: 'Google',
|
|
url: 'https://google.com',
|
|
folder: 'Search',
|
|
addDate: Date.now(),
|
|
status: 'unknown'
|
|
}
|
|
],
|
|
format: 'test',
|
|
originalCount: 2
|
|
};
|
|
|
|
try {
|
|
const analysis = manager.analyzeImportData(importData, 'merge');
|
|
document.getElementById('previewResult').innerHTML = `
|
|
<div class="success">✅ Import preview analysis successful!</div>
|
|
<div class="info">Analysis results:
|
|
<ul>
|
|
<li>Total bookmarks to import: ${importData.bookmarks.length}</li>
|
|
<li>New bookmarks: ${analysis.newBookmarks.length}</li>
|
|
<li>Duplicates found: ${analysis.duplicates.length}</li>
|
|
<li>Folders: ${analysis.folders.length}</li>
|
|
</ul>
|
|
${analysis.duplicates.length > 0 ?
|
|
`<strong>Duplicates:</strong><ul>${analysis.duplicates.map(d =>
|
|
`<li>${d.imported.title} (${d.reason})</li>`
|
|
).join('')}</ul>` : ''}
|
|
</div>
|
|
`;
|
|
} catch (error) {
|
|
document.getElementById('previewResult').innerHTML =
|
|
`<div class="error">❌ Import preview failed: ${error.message}</div>`;
|
|
}
|
|
});
|
|
|
|
// Test 5: Incremental Import
|
|
document.getElementById('testIncrementalImport').addEventListener('click', async () => {
|
|
const originalCount = manager.bookmarks.length;
|
|
|
|
const importData = {
|
|
bookmarks: [
|
|
{
|
|
id: 'incremental1',
|
|
title: 'Stack Overflow',
|
|
url: 'https://stackoverflow.com',
|
|
folder: 'Development',
|
|
addDate: Date.now(),
|
|
lastModified: Date.now(),
|
|
status: 'unknown'
|
|
}
|
|
],
|
|
format: 'test',
|
|
originalCount: 1
|
|
};
|
|
|
|
try {
|
|
await manager.performAdvancedImport(importData, 'incremental', 'keep_newer');
|
|
const newCount = manager.bookmarks.length;
|
|
|
|
document.getElementById('incrementalResult').innerHTML = `
|
|
<div class="success">✅ Incremental import successful!</div>
|
|
<div class="info">
|
|
<ul>
|
|
<li>Bookmarks before: ${originalCount}</li>
|
|
<li>Bookmarks after: ${newCount}</li>
|
|
<li>Added: ${newCount - originalCount} bookmarks</li>
|
|
</ul>
|
|
</div>
|
|
`;
|
|
} catch (error) {
|
|
document.getElementById('incrementalResult').innerHTML =
|
|
`<div class="error">❌ Incremental import failed: ${error.message}</div>`;
|
|
}
|
|
});
|
|
|
|
// Test 6: Sync Features
|
|
document.getElementById('testSyncFeatures').addEventListener('click', () => {
|
|
try {
|
|
const deviceId = manager.getDeviceId();
|
|
const syncData = manager.exportForSync();
|
|
const hash = manager.calculateDataHash();
|
|
const testData = { test: 'sync' };
|
|
const compressed = manager.compressAndEncodeData(testData);
|
|
const decompressed = manager.decodeAndDecompressData(compressed);
|
|
|
|
document.getElementById('syncResult').innerHTML = `
|
|
<div class="success">✅ Sync features working!</div>
|
|
<div class="info">Test results:
|
|
<ul>
|
|
<li>Device ID: ${deviceId}</li>
|
|
<li>Sync data exported: ${syncData.bookmarks.length} bookmarks</li>
|
|
<li>Data hash: ${hash}</li>
|
|
<li>Compression test: ${decompressed.test === 'sync' ? 'Passed' : 'Failed'}</li>
|
|
<li>Sync metadata: Version ${syncData.version}, ${syncData.metadata.totalBookmarks} bookmarks</li>
|
|
</ul>
|
|
</div>
|
|
`;
|
|
} catch (error) {
|
|
document.getElementById('syncResult').innerHTML =
|
|
`<div class="error">❌ Sync features failed: ${error.message}</div>`;
|
|
}
|
|
});
|
|
|
|
// Show current bookmarks
|
|
document.getElementById('showBookmarks').addEventListener('click', () => {
|
|
const bookmarks = manager.bookmarks;
|
|
if (bookmarks.length === 0) {
|
|
document.getElementById('bookmarksDisplay').innerHTML =
|
|
'<div class="info">No bookmarks currently loaded.</div>';
|
|
} else {
|
|
const folderStats = {};
|
|
bookmarks.forEach(b => {
|
|
const folder = b.folder || 'Uncategorized';
|
|
folderStats[folder] = (folderStats[folder] || 0) + 1;
|
|
});
|
|
|
|
document.getElementById('bookmarksDisplay').innerHTML = `
|
|
<div class="info">
|
|
<strong>Current bookmarks: ${bookmarks.length}</strong>
|
|
<br><strong>Folders:</strong>
|
|
<ul>
|
|
${Object.entries(folderStats).map(([folder, count]) =>
|
|
`<li>${folder}: ${count} bookmark${count > 1 ? 's' : ''}</li>`
|
|
).join('')}
|
|
</ul>
|
|
<strong>Recent bookmarks:</strong>
|
|
<ul>
|
|
${bookmarks.slice(-5).map(b =>
|
|
`<li><strong>${b.title}</strong> - ${b.url} (${b.folder || 'Root'})</li>`
|
|
).join('')}
|
|
</ul>
|
|
</div>
|
|
`;
|
|
}
|
|
});
|
|
|
|
// Auto-run some tests on page load
|
|
setTimeout(() => {
|
|
document.getElementById('showBookmarks').click();
|
|
}, 1000);
|
|
</script>
|
|
</body>
|
|
</html> |