experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

add persistent test, more portability migration fixes

+503 -11
+2 -2
app/cmd/panel.js
··· 523 523 function getValidURL(str) { 524 524 if (!str) return { valid: false }; 525 525 526 - // Check if it starts with a valid protocol 527 - const hasValidProtocol = /^(https?|ftp|file):\/\//.test(str); 526 + // Check if it starts with a valid protocol (including peek:// for internal pages) 527 + const hasValidProtocol = /^(https?|ftp|file|peek):\/\//.test(str); 528 528 529 529 if (!hasValidProtocol) { 530 530 // Check if it looks like a domain (e.g., "example.com" or "localhost")
+133
app/diagnostic.html
··· 1 + <!DOCTYPE html> 2 + <html> 3 + <head> 4 + <title>Peek Diagnostic</title> 5 + <style> 6 + body { font-family: monospace; padding: 20px; background: #1a1a1a; color: #e0e0e0; } 7 + h2 { color: #4fc3f7; margin-top: 30px; } 8 + pre { background: #2d2d2d; padding: 15px; border-radius: 5px; overflow-x: auto; max-height: 400px; overflow-y: auto; } 9 + .key { color: #81c784; } 10 + .value { color: #fff59d; } 11 + button { background: #4fc3f7; color: #000; border: none; padding: 10px 20px; margin: 5px; cursor: pointer; border-radius: 5px; } 12 + button:hover { background: #29b6f6; } 13 + .section { margin-bottom: 30px; } 14 + </style> 15 + </head> 16 + <body> 17 + <h1>Peek Diagnostic Tool</h1> 18 + <p>This page helps inspect localStorage and datastore contents.</p> 19 + 20 + <div class="section"> 21 + <button onclick="dumpLocalStorage()">Dump localStorage</button> 22 + <button onclick="dumpDatastore()">Dump extension_settings from datastore</button> 23 + <button onclick="dumpBoth()">Dump Both</button> 24 + <button onclick="copyResults()">Copy Results</button> 25 + </div> 26 + 27 + <div id="output"></div> 28 + 29 + <script type="module"> 30 + const api = window.app; 31 + const output = document.getElementById('output'); 32 + 33 + function log(title, content) { 34 + const section = document.createElement('div'); 35 + section.innerHTML = `<h2>${title}</h2><pre>${content}</pre>`; 36 + output.appendChild(section); 37 + } 38 + 39 + function clearOutput() { 40 + output.innerHTML = ''; 41 + } 42 + 43 + window.dumpLocalStorage = function() { 44 + clearOutput(); 45 + const items = []; 46 + for (let i = 0; i < localStorage.length; i++) { 47 + const key = localStorage.key(i); 48 + const value = localStorage.getItem(key); 49 + items.push({ key, value }); 50 + } 51 + 52 + // Sort by key 53 + items.sort((a, b) => a.key.localeCompare(b.key)); 54 + 55 + // Group by prefix (UUID or name before +) 56 + const grouped = {}; 57 + items.forEach(item => { 58 + const parts = item.key.split('+'); 59 + const prefix = parts[0] || 'other'; 60 + if (!grouped[prefix]) grouped[prefix] = []; 61 + grouped[prefix].push(item); 62 + }); 63 + 64 + let html = `Total items: ${items.length}\n\n`; 65 + 66 + for (const [prefix, groupItems] of Object.entries(grouped)) { 67 + html += `=== ${prefix} ===\n`; 68 + groupItems.forEach(item => { 69 + let displayValue = item.value; 70 + try { 71 + const parsed = JSON.parse(item.value); 72 + if (Array.isArray(parsed)) { 73 + displayValue = `Array[${parsed.length}]: ${JSON.stringify(parsed, null, 2)}`; 74 + } else if (typeof parsed === 'object') { 75 + displayValue = JSON.stringify(parsed, null, 2); 76 + } 77 + } catch {} 78 + html += ` <span class="key">${item.key}</span>:\n <span class="value">${displayValue}</span>\n\n`; 79 + }); 80 + } 81 + 82 + log('localStorage Contents', html); 83 + }; 84 + 85 + window.dumpDatastore = async function() { 86 + clearOutput(); 87 + try { 88 + const result = await api.datastore.getTable('extension_settings'); 89 + if (result.success) { 90 + const data = result.data || {}; 91 + const entries = Object.entries(data); 92 + let html = `Total rows: ${entries.length}\n\n`; 93 + 94 + entries.forEach(([rowId, row]) => { 95 + let displayValue = row.value; 96 + try { 97 + const parsed = JSON.parse(row.value); 98 + if (Array.isArray(parsed)) { 99 + displayValue = `Array[${parsed.length}]: ${JSON.stringify(parsed, null, 2)}`; 100 + } else if (typeof parsed === 'object') { 101 + displayValue = JSON.stringify(parsed, null, 2); 102 + } 103 + } catch {} 104 + html += `<span class="key">${rowId}</span> (${row.extensionId}:${row.key}):\n <span class="value">${displayValue}</span>\n\n`; 105 + }); 106 + 107 + log('extension_settings Table', html); 108 + } else { 109 + log('extension_settings Table', `Error: ${result.error}`); 110 + } 111 + } catch (err) { 112 + log('extension_settings Table', `Error: ${err.message}`); 113 + } 114 + }; 115 + 116 + window.dumpBoth = async function() { 117 + clearOutput(); 118 + window.dumpLocalStorage(); 119 + await window.dumpDatastore(); 120 + }; 121 + 122 + window.copyResults = function() { 123 + const text = output.innerText; 124 + navigator.clipboard.writeText(text).then(() => { 125 + alert('Copied to clipboard!'); 126 + }); 127 + }; 128 + 129 + // Auto-dump on load 130 + window.dumpBoth(); 131 + </script> 132 + </body> 133 + </html>
+2 -2
backend/electron/entry.ts
··· 60 60 } 61 61 }); 62 62 63 - // Get the root directory (two levels up from dist/backend/electron/) 64 - const ROOT_DIR = path.resolve(import.meta.dirname, '..', '..', '..'); 63 + // Get the root directory - app.getAppPath() works in both dev and packaged modes 64 + const ROOT_DIR = app.getAppPath(); 65 65 66 66 const DEBUG = !!process.env.DEBUG; 67 67
+10 -6
backend/electron/ipc.ts
··· 193 193 194 194 ipcMain.handle('datastore-get-table', async (ev, data) => { 195 195 try { 196 - if (!isValidTable(data.table)) { 197 - return { success: false, error: `Invalid table: ${data.table}` }; 196 + const tableName = data.tableName || data.table; 197 + if (!isValidTable(tableName)) { 198 + return { success: false, error: `Invalid table: ${tableName}` }; 198 199 } 199 - const result = getTable(data.table); 200 + const result = getTable(tableName); 200 201 return { success: true, data: result }; 201 202 } catch (error) { 202 203 const message = error instanceof Error ? error.message : String(error); ··· 206 207 207 208 ipcMain.handle('datastore-set-row', async (ev, data) => { 208 209 try { 209 - if (!isValidTable(data.table)) { 210 - return { success: false, error: `Invalid table: ${data.table}` }; 210 + const tableName = data.tableName || data.table; 211 + const rowId = data.rowId || data.id; 212 + const rowData = data.rowData || data.row; 213 + if (!isValidTable(tableName)) { 214 + return { success: false, error: `Invalid table: ${tableName}` }; 211 215 } 212 - const result = setRow(data.table, data.id, data.row); 216 + const result = setRow(tableName, rowId, rowData); 213 217 return { success: true, data: result }; 214 218 } catch (error) { 215 219 const message = error instanceof Error ? error.message : String(error);
+1
package.json
··· 37 37 "smoke:visible": "./scripts/smoke-test.sh --visible", 38 38 "test": "npx playwright test", 39 39 "test:smoke": "npx playwright test tests/smoke.spec.ts", 40 + "test:persistence": "npx playwright test tests/smoke.spec.ts --grep 'Data Persistence' --timeout=120000", 40 41 "test:headed": "npx playwright test --headed", 41 42 "test:debug": "npx playwright test --debug" 42 43 },
+72
scripts/debug-packaged.sh
··· 1 + #!/bin/bash 2 + # Debug packaged app - runs the installed app with DEBUG output 3 + # Usage: ./scripts/debug-packaged.sh [--visible] [--profile NAME] [duration_seconds] 4 + # Example: ./scripts/debug-packaged.sh # 10 seconds, default profile 5 + # Example: ./scripts/debug-packaged.sh --visible # 10 seconds, show UI 6 + # Example: ./scripts/debug-packaged.sh --profile test # fresh test profile 7 + # Example: ./scripts/debug-packaged.sh --visible --profile test 20 8 + 9 + VISIBLE=0 10 + PROFILE="" 11 + 12 + while [[ "$1" == --* ]]; do 13 + case "$1" in 14 + --visible) 15 + VISIBLE=1 16 + shift 17 + ;; 18 + --profile) 19 + PROFILE="$2" 20 + shift 2 21 + ;; 22 + *) 23 + echo "Unknown option: $1" 24 + exit 1 25 + ;; 26 + esac 27 + done 28 + 29 + DURATION=${1:-10} 30 + LOGFILE="/tmp/peek-packaged-debug.log" 31 + 32 + cleanup() { 33 + pkill -f "/Applications/Peek.app" 2>/dev/null || true 34 + } 35 + 36 + trap cleanup EXIT 37 + 38 + # Kill any existing instances first 39 + pkill -f "/Applications/Peek.app" 2>/dev/null || true 40 + sleep 1 41 + 42 + if [ -n "$PROFILE" ]; then 43 + echo "Running packaged Peek.app with DEBUG for ${DURATION}s (profile: $PROFILE)..." 44 + else 45 + echo "Running packaged Peek.app with DEBUG for ${DURATION}s (default profile)..." 46 + fi 47 + echo "Log file: $LOGFILE" 48 + echo "" 49 + 50 + if [ -n "$PROFILE" ]; then 51 + PROFILE="$PROFILE" DEBUG=1 /Applications/Peek.app/Contents/MacOS/Peek > "$LOGFILE" 2>&1 & 52 + else 53 + DEBUG=1 /Applications/Peek.app/Contents/MacOS/Peek > "$LOGFILE" 2>&1 & 54 + fi 55 + PID=$! 56 + 57 + sleep "$DURATION" 58 + 59 + echo "=== Extension manifest loading ===" 60 + grep -E "\[ext:win\].*Creating window" "$LOGFILE" || echo "(no extension window creation found)" 61 + echo "" 62 + 63 + echo "=== Manifest details ===" 64 + grep -E "manifest:" "$LOGFILE" | head -20 || echo "(no manifest details found)" 65 + echo "" 66 + 67 + echo "=== Errors ===" 68 + grep -iE "error|failed|cannot" "$LOGFILE" | head -20 || echo "(no errors found)" 69 + echo "" 70 + 71 + echo "=== Full log available at: $LOGFILE ===" 72 + echo "View with: cat $LOGFILE"
+53
scripts/test-dev.sh
··· 1 + #!/bin/bash 2 + # Test dev app - runs the dev app with a fresh profile 3 + # Usage: ./scripts/test-dev.sh [--visible] [duration_seconds] 4 + # Example: ./scripts/test-dev.sh # 8 seconds, headless 5 + # Example: ./scripts/test-dev.sh --visible # 8 seconds, show UI 6 + # Example: ./scripts/test-dev.sh --visible 15 7 + 8 + VISIBLE=0 9 + if [ "$1" = "--visible" ]; then 10 + VISIBLE=1 11 + shift 12 + fi 13 + 14 + DURATION=${1:-8} 15 + PROFILE="test-$$" 16 + LOGFILE="/tmp/peek-dev-test.log" 17 + 18 + cleanup() { 19 + pkill -f "/Users/dietrich/misc/peek/node_modules/electron" 2>/dev/null || true 20 + } 21 + 22 + trap cleanup EXIT 23 + 24 + # Kill any existing dev instances 25 + pkill -f "/Users/dietrich/misc/peek/node_modules/electron" 2>/dev/null || true 26 + sleep 1 27 + 28 + echo "Running dev Peek with test profile '$PROFILE' for ${DURATION}s..." 29 + echo "Log file: $LOGFILE" 30 + echo "" 31 + 32 + if [ "$VISIBLE" = "1" ]; then 33 + PROFILE="$PROFILE" DEBUG=1 yarn start > "$LOGFILE" 2>&1 & 34 + else 35 + PROFILE="$PROFILE" DEBUG=1 PEEK_HEADLESS=1 yarn start > "$LOGFILE" 2>&1 & 36 + fi 37 + PID=$! 38 + 39 + sleep "$DURATION" 40 + 41 + echo "=== Errors ===" 42 + grep -iE "error|failed|exception" "$LOGFILE" | grep -v "Autofill" | head -20 || echo "(no errors found)" 43 + echo "" 44 + 45 + echo "=== Warnings ===" 46 + grep -iE "warning|warn" "$LOGFILE" | head -10 || echo "(no warnings found)" 47 + echo "" 48 + 49 + echo "=== Key events ===" 50 + grep -E "onReady|Loading|loaded|register" "$LOGFILE" | head -20 51 + echo "" 52 + 53 + echo "=== Full log available at: $LOGFILE ==="
+10
scripts/test-persistence.sh
··· 1 + #!/bin/bash 2 + # Run data persistence tests 3 + # Usage: ./scripts/test-persistence.sh 4 + 5 + echo "Running data persistence tests..." 6 + npx playwright test tests/smoke.spec.ts --grep "Data Persistence" --timeout=120000 7 + 8 + echo "" 9 + echo "Done. Test profiles created in ~/Library/Application Support/Peek/test-persistence-*" 10 + echo "You can delete them with: rm -rf ~/Library/Application\\ Support/Peek/test-*"
+220 -1
tests/smoke.spec.ts
··· 18 18 const __filename = fileURLToPath(import.meta.url); 19 19 const __dirname = path.dirname(__filename); 20 20 const ROOT = path.join(__dirname, '..'); 21 - const MAIN_PATH = path.join(ROOT, 'index.js'); 21 + // Pass the root directory - Electron will use package.json main field 22 + const MAIN_PATH = ROOT; 22 23 23 24 // Helper to wait for a window with specific URL pattern 24 25 async function waitForWindow(app: ElectronApplication, urlPattern: string | RegExp, timeout = 10000): Promise<Page> { ··· 419 420 // Verify background window exists (app started correctly) 420 421 const bgWindow = windows.find(w => w.url().includes('background.html')); 421 422 expect(bgWindow).toBeTruthy(); 423 + 424 + await electronApp.close(); 425 + }); 426 + }); 427 + 428 + // Data Persistence Tests - verify user data survives app restart 429 + test.describe('Data Persistence', () => { 430 + const PERSISTENCE_PROFILE = 'test-persistence-' + Date.now(); 431 + 432 + test('peeks and slides settings persist across restart', async () => { 433 + // PHASE 1: Launch app and add custom peeks/slides configuration 434 + let electronApp = await electron.launch({ 435 + args: [MAIN_PATH], 436 + env: { ...process.env, PROFILE: PERSISTENCE_PROFILE, DEBUG: '1', PEEK_HEADLESS: '1' } 437 + }); 438 + await new Promise(r => setTimeout(r, 4000)); 439 + 440 + let bgWindow = await waitForWindow(electronApp, 'app/background.html'); 441 + 442 + // Add custom peek items to extension_settings 443 + const testPeeks = [ 444 + { title: 'Test Peek 1', uri: 'https://test-peek-1.example.com', shortcut: 'Option+1' }, 445 + { title: 'Test Peek 2', uri: 'https://test-peek-2.example.com', shortcut: 'Option+2' }, 446 + { title: 'Custom Peek', uri: 'https://custom-peek.example.com', shortcut: 'Option+3' } 447 + ]; 448 + 449 + const testSlides = [ 450 + { title: 'Test Slide 1', uri: 'https://test-slide-1.example.com', position: 'right', size: 400 }, 451 + { title: 'Test Slide 2', uri: 'https://test-slide-2.example.com', position: 'bottom', size: 300 } 452 + ]; 453 + 454 + // Save peeks items to extension_settings via datastore 455 + const savePeeksResult = await bgWindow.evaluate(async (items) => { 456 + const api = (window as any).app; 457 + return await api.datastore.setRow('extension_settings', 'peeks:items', { 458 + extensionId: 'peeks', 459 + key: 'items', 460 + value: JSON.stringify(items), 461 + updatedAt: Date.now() 462 + }); 463 + }, testPeeks); 464 + expect(savePeeksResult.success).toBe(true); 465 + 466 + // Save slides items to extension_settings 467 + const saveSlidesResult = await bgWindow.evaluate(async (items) => { 468 + const api = (window as any).app; 469 + return await api.datastore.setRow('extension_settings', 'slides:items', { 470 + extensionId: 'slides', 471 + key: 'items', 472 + value: JSON.stringify(items), 473 + updatedAt: Date.now() 474 + }); 475 + }, testSlides); 476 + expect(saveSlidesResult.success).toBe(true); 477 + 478 + // Also save custom prefs 479 + const savePeeksPrefs = await bgWindow.evaluate(async () => { 480 + const api = (window as any).app; 481 + return await api.datastore.setRow('extension_settings', 'peeks:prefs', { 482 + extensionId: 'peeks', 483 + key: 'prefs', 484 + value: JSON.stringify({ shortcutKeyPrefix: 'Option+' }), 485 + updatedAt: Date.now() 486 + }); 487 + }); 488 + expect(savePeeksPrefs.success).toBe(true); 489 + 490 + const saveSlidesPrefs = await bgWindow.evaluate(async () => { 491 + const api = (window as any).app; 492 + return await api.datastore.setRow('extension_settings', 'slides:prefs', { 493 + extensionId: 'slides', 494 + key: 'prefs', 495 + value: JSON.stringify({ defaultPosition: 'right', defaultSize: 350 }), 496 + updatedAt: Date.now() 497 + }); 498 + }); 499 + expect(saveSlidesPrefs.success).toBe(true); 500 + 501 + // Verify data was saved 502 + const verifyResult = await bgWindow.evaluate(async () => { 503 + const api = (window as any).app; 504 + return await api.datastore.getTable('extension_settings'); 505 + }); 506 + expect(verifyResult.success).toBe(true); 507 + const savedRows = Object.values(verifyResult.data); 508 + expect(savedRows.length).toBeGreaterThanOrEqual(4); 509 + 510 + // Close the app 511 + await electronApp.close(); 512 + await new Promise(r => setTimeout(r, 1000)); 513 + 514 + // PHASE 2: Relaunch with same profile and verify data persisted 515 + electronApp = await electron.launch({ 516 + args: [MAIN_PATH], 517 + env: { ...process.env, PROFILE: PERSISTENCE_PROFILE, DEBUG: '1', PEEK_HEADLESS: '1' } 518 + }); 519 + await new Promise(r => setTimeout(r, 4000)); 520 + 521 + bgWindow = await waitForWindow(electronApp, 'app/background.html'); 522 + 523 + // Query extension_settings to verify persistence 524 + const persistedResult = await bgWindow.evaluate(async () => { 525 + const api = (window as any).app; 526 + return await api.datastore.getTable('extension_settings'); 527 + }); 528 + expect(persistedResult.success).toBe(true); 529 + 530 + const persistedData = persistedResult.data as Record<string, any>; 531 + 532 + // Verify peeks items persisted 533 + const peeksItems = persistedData['peeks:items']; 534 + expect(peeksItems).toBeTruthy(); 535 + expect(peeksItems.extensionId).toBe('peeks'); 536 + expect(peeksItems.key).toBe('items'); 537 + const parsedPeeks = JSON.parse(peeksItems.value); 538 + expect(parsedPeeks.length).toBe(3); 539 + expect(parsedPeeks[0].title).toBe('Test Peek 1'); 540 + expect(parsedPeeks[2].title).toBe('Custom Peek'); 541 + 542 + // Verify slides items persisted 543 + const slidesItems = persistedData['slides:items']; 544 + expect(slidesItems).toBeTruthy(); 545 + expect(slidesItems.extensionId).toBe('slides'); 546 + const parsedSlides = JSON.parse(slidesItems.value); 547 + expect(parsedSlides.length).toBe(2); 548 + expect(parsedSlides[0].position).toBe('right'); 549 + expect(parsedSlides[1].position).toBe('bottom'); 550 + 551 + // Verify prefs persisted 552 + const peeksPrefs = persistedData['peeks:prefs']; 553 + expect(peeksPrefs).toBeTruthy(); 554 + const parsedPeeksPrefs = JSON.parse(peeksPrefs.value); 555 + expect(parsedPeeksPrefs.shortcutKeyPrefix).toBe('Option+'); 556 + 557 + const slidesPrefs = persistedData['slides:prefs']; 558 + expect(slidesPrefs).toBeTruthy(); 559 + const parsedSlidesPrefs = JSON.parse(slidesPrefs.value); 560 + expect(parsedSlidesPrefs.defaultPosition).toBe('right'); 561 + 562 + await electronApp.close(); 563 + }); 564 + 565 + test('addresses and tags persist across restart', async () => { 566 + const ADDR_PROFILE = 'test-addr-persist-' + Date.now(); 567 + 568 + // PHASE 1: Add addresses and tags 569 + let electronApp = await electron.launch({ 570 + args: [MAIN_PATH], 571 + env: { ...process.env, PROFILE: ADDR_PROFILE, DEBUG: '1', PEEK_HEADLESS: '1' } 572 + }); 573 + await new Promise(r => setTimeout(r, 4000)); 574 + 575 + let bgWindow = await waitForWindow(electronApp, 'app/background.html'); 576 + 577 + // Add addresses 578 + const addr1 = await bgWindow.evaluate(async () => { 579 + return await (window as any).app.datastore.addAddress('https://persist-test-1.example.com', { 580 + title: 'Persist Test 1', 581 + starred: 1 582 + }); 583 + }); 584 + expect(addr1.success).toBe(true); 585 + 586 + const addr2 = await bgWindow.evaluate(async () => { 587 + return await (window as any).app.datastore.addAddress('https://persist-test-2.example.com', { 588 + title: 'Persist Test 2' 589 + }); 590 + }); 591 + expect(addr2.success).toBe(true); 592 + 593 + // Create a tag and tag the addresses 594 + const tagResult = await bgWindow.evaluate(async () => { 595 + return await (window as any).app.datastore.getOrCreateTag('persist-tag'); 596 + }); 597 + expect(tagResult.success).toBe(true); 598 + const tagId = tagResult.data?.id; 599 + 600 + if (tagId && addr1.id) { 601 + await bgWindow.evaluate(async ({ addressId, tagId }) => { 602 + return await (window as any).app.datastore.tagAddress(addressId, tagId); 603 + }, { addressId: addr1.id, tagId }); 604 + } 605 + 606 + await electronApp.close(); 607 + await new Promise(r => setTimeout(r, 1000)); 608 + 609 + // PHASE 2: Verify persistence 610 + electronApp = await electron.launch({ 611 + args: [MAIN_PATH], 612 + env: { ...process.env, PROFILE: ADDR_PROFILE, DEBUG: '1', PEEK_HEADLESS: '1' } 613 + }); 614 + await new Promise(r => setTimeout(r, 4000)); 615 + 616 + bgWindow = await waitForWindow(electronApp, 'app/background.html'); 617 + 618 + // Query addresses - use getTable for more reliable results 619 + const tableResult = await bgWindow.evaluate(async () => { 620 + return await (window as any).app.datastore.getTable('addresses'); 621 + }); 622 + expect(tableResult.success).toBe(true); 623 + 624 + const addresses = Object.values(tableResult.data) as any[]; 625 + expect(addresses.length).toBeGreaterThanOrEqual(2); 626 + 627 + const persistedAddr1 = addresses.find((a: any) => 628 + a.uri === 'https://persist-test-1.example.com' || 629 + a.uri?.includes('persist-test-1') 630 + ); 631 + expect(persistedAddr1).toBeTruthy(); 632 + expect(persistedAddr1.title).toBe('Persist Test 1'); 633 + 634 + // Query tags 635 + const tagsResult = await bgWindow.evaluate(async () => { 636 + return await (window as any).app.datastore.getTagsByFrecency(10); 637 + }); 638 + expect(tagsResult.success).toBe(true); 639 + const persistTag = tagsResult.data.find((t: any) => t.name === 'persist-tag'); 640 + expect(persistTag).toBeTruthy(); 422 641 423 642 await electronApp.close(); 424 643 });