experiments in a post-browser web
10
fork

Configure Feed

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

test(smoke): per-describe Electron isolation to kill cross-test state leaks

+363 -194
+363 -194
tests/desktop/smoke.spec.ts
··· 9 9 * BACKEND=tauri yarn test:desktop 10 10 */ 11 11 12 - import { test, expect, DesktopApp, launchDesktopApp, getSharedApp, closeSharedApp } from '../fixtures/desktop-app'; 12 + import { test, expect, DesktopApp, launchDesktopApp } from '../fixtures/desktop-app'; 13 13 import { Page } from '@playwright/test'; 14 14 import path from 'path'; 15 15 import { fileURLToPath } from 'url'; 16 - import { spawn } from 'child_process'; 17 16 import { waitForCommandResults, waitForWindowCount, waitForVisible, waitForClass, waitForResultsWithContent, waitForSelectionChange, sleep, waitForWindow, waitForExtensionsReady, queryCommandsWithRetry, waitForAppReady, waitForCommand, waitForPanelCommandsLoaded } from '../helpers/window-utils'; 18 17 19 18 const __filename = fileURLToPath(import.meta.url); ··· 21 20 const ROOT = path.join(__dirname, '../..'); 22 21 23 22 // ============================================================================ 24 - // SHARED APP INSTANCE 25 - // Most tests use a single shared app to avoid startup overhead. 26 - // Only tests that need fresh state or test lifecycle use isolated instances. 23 + // PER-DESCRIBE APP INSTANCES 24 + // Each describe block launches its own Electron instance so that window leaks, 25 + // stale lastFocusedVisibleWindowId, and datastore pollution cannot cross 26 + // describe boundaries. launchDesktopApp() is called in each describe's 27 + // beforeAll and the app is closed in afterAll. 27 28 // ============================================================================ 28 29 29 - // Shared app and window for tests that don't need isolation 30 - let sharedApp: DesktopApp; 31 - let sharedBgWindow: Page; 32 - 33 - // Initialize shared app once before all tests 34 - test.beforeAll(async () => { 35 - sharedApp = await getSharedApp(); 36 - sharedBgWindow = await sharedApp.getBackgroundWindow(); 37 - await waitForExtensionsReady(sharedBgWindow); 38 - }); 39 - 40 - // Clean up shared app after all tests 41 - test.afterAll(async () => { 42 - await closeSharedApp(); 43 - }); 30 + /** 31 + * Launch a fresh app + bgWindow for a single describe block. 32 + * Call from test.beforeAll; close result.app in test.afterAll. 33 + */ 34 + async function createPerDescribeApp(label: string): Promise<{ app: DesktopApp; bgWindow: Page }> { 35 + // Profile MUST start with "test" — `isTestProfile()` in backend/electron/config.ts 36 + // keys on that prefix to skip the single-instance lock. Without it, parallel 37 + // Playwright workers would all contend for the same machine-wide lock and 38 + // only one Electron launch would succeed. 39 + const profile = `test-smoke-${label.replace(/[^a-z0-9]/gi, '-')}-${Date.now()}`; 40 + const app = await launchDesktopApp(profile); 41 + const bgWindow = await app.getBackgroundWindow(); 42 + await waitForExtensionsReady(bgWindow); 43 + return { app, bgWindow }; 44 + } 44 45 45 46 // ============================================================================ 46 - // Settings Tests (uses shared app) 47 + // Settings Tests 47 48 // ============================================================================ 48 49 49 50 test.describe('Settings @desktop', () => { 51 + let app: DesktopApp; 52 + let bgWindow: Page; 53 + 54 + test.beforeAll(async () => { 55 + ({ app, bgWindow } = await createPerDescribeApp('settings')); 56 + }); 57 + 58 + test.afterAll(async () => { 59 + if (app) await app.close(); 60 + }); 61 + 50 62 test('open and close settings', async () => { 51 63 // Settings opens on start in debug mode 52 - const settingsWindow = await sharedApp.getWindow('settings/settings.html'); 64 + const settingsWindow = await app.getWindow('settings/settings.html'); 53 65 expect(settingsWindow).toBeTruthy(); 54 66 55 67 // Verify content loaded ··· 63 75 }); 64 76 65 77 // ============================================================================ 66 - // Cross-Origin Fetch Tests (uses shared app) 78 + // Cross-Origin Fetch Tests 67 79 // ============================================================================ 68 80 69 81 test.describe('Cross-Origin Fetch @desktop', () => { 82 + let app: DesktopApp; 83 + let bgWindow: Page; 84 + 85 + test.beforeAll(async () => { 86 + ({ app, bgWindow } = await createPerDescribeApp('cors')); 87 + }); 88 + 89 + test.afterAll(async () => { 90 + if (app) await app.close(); 91 + }); 92 + 70 93 test('peek:// pages can fetch from https:// origins', async () => { 71 94 // peek:// scheme has corsEnabled: false, so fetch() to external origins should work. 72 95 // If corsEnabled were true, this would throw "Failed to fetch" due to CORS. 73 - const result = await sharedBgWindow.evaluate(async () => { 96 + const result = await bgWindow.evaluate(async () => { 74 97 try { 75 98 const res = await fetch('https://public.api.bsky.app/xrpc/_health'); 76 99 return { ok: res.ok, status: res.status, error: null }; ··· 86 109 }); 87 110 88 111 // ============================================================================ 89 - // Command Palette Tests (uses shared app) 112 + // Command Palette Tests 90 113 // ============================================================================ 91 114 92 115 test.describe('Cmd Palette @desktop', () => { 116 + let app: DesktopApp; 117 + let bgWindow: Page; 118 + 119 + test.beforeAll(async () => { 120 + ({ app, bgWindow } = await createPerDescribeApp('cmd-palette')); 121 + }); 122 + 123 + test.afterAll(async () => { 124 + if (app) await app.close(); 125 + }); 126 + 93 127 test('open cmd and execute gallery command', async () => { 94 128 // Wait for cmd extension to be ready (critical for packaged mode where startup is slower) 95 - await waitForExtensionsReady(sharedBgWindow, 15000); 129 + await waitForExtensionsReady(bgWindow, 15000); 96 130 97 131 // Open cmd panel via window API 98 - const openResult = await sharedBgWindow.evaluate(async () => { 132 + const openResult = await bgWindow.evaluate(async () => { 99 133 return await (window as any).app.window.open('peek://cmd/panel.html', { 100 134 modal: true, 101 135 width: 600, ··· 109 143 expect(openResult.success).toBe(true); 110 144 111 145 // Find the cmd window (getWindow already polls until found) 112 - const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 146 + const cmdWindow = await app.getWindow('cmd/panel.html', 5000); 113 147 expect(cmdWindow).toBeTruthy(); 114 148 115 149 // Wait for input to be ready and commands to be loaded ··· 133 167 134 168 // Close the cmd window 135 169 if (openResult.id) { 136 - await sharedBgWindow.evaluate(async (id: number) => { 170 + await bgWindow.evaluate(async (id: number) => { 137 171 return await (window as any).app.window.close(id); 138 172 }, openResult.id); 139 173 } 140 174 }); 141 175 142 176 test('edit command Tab-completion shows autocomplete and opens editor', async () => { 143 - await waitForExtensionsReady(sharedBgWindow, 15000); 177 + await waitForExtensionsReady(bgWindow, 15000); 144 178 145 179 // Create a test note so the edit command has something to autocomplete 146 - const addResult = await sharedBgWindow.evaluate(async () => { 180 + const addResult = await bgWindow.evaluate(async () => { 147 181 return await (window as any).app.datastore.addItem('text', { 148 182 content: '# Edit Tab Test Note\nThis is a note for testing edit tab-completion.' 149 183 }); ··· 152 186 const noteId = addResult.data?.id; 153 187 154 188 // Set up editor:open event capture BEFORE opening the cmd panel 155 - await sharedBgWindow.evaluate(() => { 189 + await bgWindow.evaluate(() => { 156 190 (window as any).__editorOpenCaptured = []; 157 191 (window as any).__editorOpenUnsub = (window as any).app.subscribe('editor:open', (data: any) => { 158 192 (window as any).__editorOpenCaptured.push(data); ··· 160 194 }); 161 195 162 196 // Open cmd panel 163 - const openResult = await sharedBgWindow.evaluate(async () => { 197 + const openResult = await bgWindow.evaluate(async () => { 164 198 return await (window as any).app.window.open('peek://cmd/panel.html', { 165 199 modal: true, 166 200 width: 600, ··· 173 207 }); 174 208 expect(openResult.success).toBe(true); 175 209 176 - const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 210 + const cmdWindow = await app.getWindow('cmd/panel.html', 5000); 177 211 expect(cmdWindow).toBeTruthy(); 178 212 179 213 await cmdWindow.waitForSelector('input', { timeout: 5000 }); ··· 202 236 await cmdWindow.keyboard.press('Enter'); 203 237 204 238 // Verify editor:open was published by polling the captured events 205 - await sharedBgWindow.waitForFunction(() => { 239 + await bgWindow.waitForFunction(() => { 206 240 return (window as any).__editorOpenCaptured && (window as any).__editorOpenCaptured.length > 0; 207 241 }, undefined, { timeout: 10000 }); 208 242 209 - const editorOpenData = await sharedBgWindow.evaluate(() => { 243 + const editorOpenData = await bgWindow.evaluate(() => { 210 244 return (window as any).__editorOpenCaptured[0]; 211 245 }); 212 246 expect(editorOpenData).toBeTruthy(); 213 247 214 248 // Clean up event listener 215 - await sharedBgWindow.evaluate(() => { 249 + await bgWindow.evaluate(() => { 216 250 if ((window as any).__editorOpenUnsub) { 217 251 (window as any).__editorOpenUnsub(); 218 252 } ··· 223 257 // Close cmd window if still open 224 258 try { 225 259 if (openResult.id) { 226 - await sharedBgWindow.evaluate(async (id: number) => { 260 + await bgWindow.evaluate(async (id: number) => { 227 261 return await (window as any).app.window.close(id); 228 262 }, openResult.id); 229 263 } ··· 233 267 234 268 // Clean up the test note 235 269 if (noteId) { 236 - await sharedBgWindow.evaluate(async (id: string) => { 270 + await bgWindow.evaluate(async (id: string) => { 237 271 return await (window as any).app.datastore.deleteItem(id); 238 272 }, noteId); 239 273 } ··· 245 279 // ============================================================================ 246 280 247 281 test.describe('Peeks @desktop', () => { 282 + let app: DesktopApp; 283 + let bgWindow: Page; 284 + 285 + test.beforeAll(async () => { 286 + ({ app, bgWindow } = await createPerDescribeApp('peeks')); 287 + }); 288 + 289 + test.afterAll(async () => { 290 + if (app) await app.close(); 291 + }); 292 + 248 293 test('add a peek and test it opens', async () => { 249 294 // Add a peek address to the datastore 250 - const addResult = await sharedBgWindow.evaluate(async () => { 295 + const addResult = await bgWindow.evaluate(async () => { 251 296 return await (window as any).app.datastore.addAddress('https://example.com', { 252 297 title: 'Example Peek', 253 298 description: 'Test peek for smoke tests' ··· 256 301 expect(addResult.success).toBe(true); 257 302 258 303 // Verify peeks extension is loaded (hybrid mode: may be iframe or separate window) 259 - const runningExts = await sharedBgWindow.evaluate(async () => { 304 + const runningExts = await bgWindow.evaluate(async () => { 260 305 return await (window as any).app.extensions.list(); 261 306 }); 262 307 const peeksRunning = runningExts.data?.some((ext: any) => ext.id === 'peeks'); 263 308 expect(peeksRunning).toBe(true); 264 309 265 310 // Open a peek window for the address we created 266 - const peekResult = await sharedBgWindow.evaluate(async () => { 311 + const peekResult = await bgWindow.evaluate(async () => { 267 312 return await (window as any).app.window.open('https://example.com', { 268 313 width: 800, 269 314 height: 600, ··· 273 318 expect(peekResult.success).toBe(true); 274 319 275 320 // Wait for window to open (getWindow polls) 276 - const peekWindow = await sharedApp.getWindow('example.com', 5000); 321 + const peekWindow = await app.getWindow('example.com', 5000); 277 322 expect(peekWindow).toBeTruthy(); 278 323 279 324 // Close the peek 280 325 if (peekResult.id) { 281 - await sharedBgWindow.evaluate(async (id: number) => { 326 + await bgWindow.evaluate(async (id: number) => { 282 327 return await (window as any).app.window.close(id); 283 328 }, peekResult.id); 284 329 } ··· 290 335 // ============================================================================ 291 336 292 337 test.describe('Slides @desktop', () => { 338 + let app: DesktopApp; 339 + let bgWindow: Page; 340 + 341 + test.beforeAll(async () => { 342 + ({ app, bgWindow } = await createPerDescribeApp('slides')); 343 + }); 344 + 345 + test.afterAll(async () => { 346 + if (app) await app.close(); 347 + }); 348 + 293 349 test('add slides and test they work', async () => { 294 350 // Add multiple addresses to use as slides 295 351 const urls = [ ··· 299 355 ]; 300 356 301 357 for (const url of urls) { 302 - const result = await sharedBgWindow.evaluate(async (uri: string) => { 358 + const result = await bgWindow.evaluate(async (uri: string) => { 303 359 return await (window as any).app.datastore.addAddress(uri, { 304 360 title: `Slide: ${uri}`, 305 361 starred: 1 ··· 309 365 } 310 366 311 367 // Verify slides extension is loaded (hybrid mode: may be iframe or separate window) 312 - const runningExts = await sharedBgWindow.evaluate(async () => { 368 + const runningExts = await bgWindow.evaluate(async () => { 313 369 return await (window as any).app.extensions.list(); 314 370 }); 315 371 const slidesRunning = runningExts.data?.some((ext: any) => ext.id === 'slides'); 316 372 expect(slidesRunning).toBe(true); 317 373 318 374 // Query addresses to verify they were added 319 - const queryResult = await sharedBgWindow.evaluate(async () => { 375 + const queryResult = await bgWindow.evaluate(async () => { 320 376 return await (window as any).app.datastore.queryAddresses({ starred: 1, limit: 10 }); 321 377 }); 322 378 expect(queryResult.success).toBe(true); ··· 329 385 // ============================================================================ 330 386 331 387 test.describe('Groups Navigation @desktop', () => { 388 + let app: DesktopApp; 389 + let bgWindow: Page; 390 + 391 + test.beforeAll(async () => { 392 + ({ app, bgWindow } = await createPerDescribeApp('groups-nav')); 393 + }); 394 + 395 + test.afterAll(async () => { 396 + if (app) await app.close(); 397 + }); 398 + 332 399 test('groups to group to url and back navigation', async () => { 333 - const bgWindow = sharedBgWindow; 334 400 // Create a tag/group with some items and promote it to a group 335 401 const tagResult = await bgWindow.evaluate(async () => { 336 402 const result = await (window as any).app.datastore.getOrCreateTag('test-group'); ··· 386 452 expect(groupsResult.success).toBe(true); 387 453 388 454 // Find the groups window (getWindow polls) 389 - const groupsWindow = await sharedApp.getWindow('groups/home.html', 5000); 455 + const groupsWindow = await app.getWindow('groups/home.html', 5000); 390 456 expect(groupsWindow).toBeTruthy(); 391 457 await groupsWindow.waitForLoadState('domcontentloaded'); 392 458 ··· 417 483 const addressCard = await groupsWindow.$('peek-card.address-card'); 418 484 expect(addressCard).toBeTruthy(); 419 485 420 - const windowCountBefore = sharedApp.windows().length; 486 + const windowCountBefore = app.windows().length; 421 487 await addressCard!.click(); 422 488 423 489 // Wait for new window to open 424 - await waitForWindowCount(() => sharedApp.windows(), windowCountBefore + 1, 5000); 490 + await waitForWindowCount(() => app.windows(), windowCountBefore + 1, 5000); 425 491 426 492 // Verify a new window was opened 427 - const windowCountAfter = sharedApp.windows().length; 493 + const windowCountAfter = app.windows().length; 428 494 expect(windowCountAfter).toBeGreaterThan(windowCountBefore); 429 495 430 496 // Navigate back to groups view ··· 477 543 // ============================================================================ 478 544 479 545 test.describe('IZUI Escape Protocol @desktop', () => { 546 + let app: DesktopApp; 547 + let bgWindow: Page; 548 + 549 + test.beforeAll(async () => { 550 + ({ app, bgWindow } = await createPerDescribeApp('izui-escape')); 551 + }); 552 + 553 + test.afterAll(async () => { 554 + if (app) await app.close(); 555 + }); 556 + 480 557 test('navigate mode: escape navigates internally before requesting close', async () => { 481 - const bgWindow = sharedBgWindow; 482 558 483 559 // Create a group with items so we can navigate into it 484 560 const tagResult = await bgWindow.evaluate(async () => { ··· 511 587 }); 512 588 expect(groupsResult.success).toBe(true); 513 589 514 - const groupsWindow = await sharedApp.getWindow('groups/home.html', 5000); 590 + const groupsWindow = await app.getWindow('groups/home.html', 5000); 515 591 expect(groupsWindow).toBeTruthy(); 516 592 await groupsWindow.waitForLoadState('domcontentloaded'); 517 593 await groupsWindow.waitForSelector('.cards', { timeout: 5000 }); ··· 567 643 }); 568 644 569 645 test('peek-card: Enter key activates card via card-click event', async () => { 570 - const bgWindow = sharedBgWindow; 571 646 572 647 // Create a group with an item 573 648 const tagResult = await bgWindow.evaluate(async () => { ··· 600 675 }); 601 676 expect(groupsResult.success).toBe(true); 602 677 603 - const groupsWindow = await sharedApp.getWindow('groups/home.html', 5000); 678 + const groupsWindow = await app.getWindow('groups/home.html', 5000); 604 679 expect(groupsWindow).toBeTruthy(); 605 680 await groupsWindow.waitForLoadState('domcontentloaded'); 606 681 await groupsWindow.waitForSelector('peek-card.group-card', { timeout: 5000 }); ··· 636 711 }); 637 712 638 713 test('active mode: ESC at root does NOT close window', async () => { 639 - const bgWindow = sharedBgWindow; 640 714 641 715 // Open groups window with role: 'workspace' (like the real groups extension does) 642 716 // In headless/test mode, appFocused defaults to true → session is 'active' ··· 648 722 }); 649 723 }); 650 724 expect(groupsResult.success).toBe(true); 651 - const groupsWindow = await sharedApp.getWindow('groups/home.html', 5000); 725 + const groupsWindow = await app.getWindow('groups/home.html', 5000); 652 726 expect(groupsWindow).toBeTruthy(); 653 727 await groupsWindow.waitForLoadState('domcontentloaded'); 654 728 await groupsWindow.waitForSelector('.cards', { timeout: 5000 }); ··· 688 762 }); 689 763 690 764 test('active mode: ESC on child-content window does NOT close it (regression)', async () => { 691 - const bgWindow = sharedBgWindow; 692 765 693 766 // First open a workspace window (like groups) to establish an active session 694 767 const workspaceResult = await bgWindow.evaluate(async () => { ··· 699 772 }); 700 773 }); 701 774 expect(workspaceResult.success).toBe(true); 702 - const workspaceWindow = await sharedApp.getWindow('groups/home.html', 5000); 775 + const workspaceWindow = await app.getWindow('groups/home.html', 5000); 703 776 expect(workspaceWindow).toBeTruthy(); 704 777 await workspaceWindow.waitForLoadState('domcontentloaded'); 705 778 ··· 719 792 }); 720 793 }); 721 794 expect(contentResult.success).toBe(true); 722 - const contentWindow = await sharedApp.getWindow('search/home.html', 5000); 795 + const contentWindow = await app.getWindow('search/home.html', 5000); 723 796 expect(contentWindow).toBeTruthy(); 724 797 await contentWindow.waitForLoadState('domcontentloaded'); 725 798 ··· 754 827 }); 755 828 756 829 test('navigate mode: timeout does not close window', async () => { 757 - const bgWindow = sharedBgWindow; 758 830 759 831 // Open a window with navigate escape mode but NO escape handler registered 760 832 // This simulates what happens when a window hasn't finished loading its IZUI ··· 767 839 }); 768 840 expect(result.success).toBe(true); 769 841 770 - const testWindow = await sharedApp.getWindow('groups/home.html', 5000); 842 + const testWindow = await app.getWindow('groups/home.html', 5000); 771 843 expect(testWindow).toBeTruthy(); 772 844 await testWindow.waitForLoadState('domcontentloaded'); 773 845 ··· 800 872 // ============================================================================ 801 873 802 874 test.describe('External URL Opening @desktop', () => { 875 + let app: DesktopApp; 876 + let bgWindow: Page; 877 + 878 + test.beforeAll(async () => { 879 + ({ app, bgWindow } = await createPerDescribeApp('external-url')); 880 + }); 881 + 882 + test.afterAll(async () => { 883 + if (app) await app.close(); 884 + }); 885 + 803 886 test('open URL by calling executable', async () => { 804 887 // Verify app is ready with background window 805 - expect(sharedBgWindow).toBeTruthy(); 888 + expect(bgWindow).toBeTruthy(); 806 889 // Ensure the API is ready 807 - await waitForAppReady(sharedBgWindow); 890 + await waitForAppReady(bgWindow); 808 891 }); 809 892 810 893 test('cmd panel detects and opens domain without protocol (youtube.com)', async () => { 811 - await waitForExtensionsReady(sharedBgWindow, 15000); 894 + await waitForExtensionsReady(bgWindow, 15000); 812 895 813 896 // Open cmd panel 814 - const openResult = await sharedBgWindow.evaluate(async () => { 897 + const openResult = await bgWindow.evaluate(async () => { 815 898 return await (window as any).app.window.open('peek://cmd/panel.html', { 816 899 modal: true, 817 900 width: 600, ··· 824 907 }); 825 908 expect(openResult.success).toBe(true); 826 909 827 - const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 910 + const cmdWindow = await app.getWindow('cmd/panel.html', 5000); 828 911 await cmdWindow.waitForSelector('input', { timeout: 5000 }); 829 912 830 913 // Type a domain without protocol ··· 835 918 await sleep(1000); 836 919 837 920 // Verify URL was opened (check window list for the URL) 838 - const windowList = await sharedBgWindow.evaluate(async () => { 921 + const windowList = await bgWindow.evaluate(async () => { 839 922 return await (window as any).app.window.list(); 840 923 }); 841 924 ··· 848 931 849 932 // Clean up 850 933 if (exampleWindow) { 851 - await sharedBgWindow.evaluate(async (id: number) => { 934 + await bgWindow.evaluate(async (id: number) => { 852 935 await (window as any).app.window.close(id); 853 936 }, exampleWindow.id); 854 937 } 855 938 }); 856 939 857 940 test('cmd panel opens URL with http protocol', async () => { 858 - await waitForExtensionsReady(sharedBgWindow, 15000); 941 + await waitForExtensionsReady(bgWindow, 15000); 859 942 860 943 // Open cmd panel 861 - const openResult = await sharedBgWindow.evaluate(async () => { 944 + const openResult = await bgWindow.evaluate(async () => { 862 945 return await (window as any).app.window.open('peek://cmd/panel.html', { 863 946 modal: true, 864 947 width: 600, ··· 871 954 }); 872 955 expect(openResult.success).toBe(true); 873 956 874 - const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 957 + const cmdWindow = await app.getWindow('cmd/panel.html', 5000); 875 958 await cmdWindow.waitForSelector('input', { timeout: 5000 }); 876 959 877 960 // Type URL with http protocol ··· 882 965 await sleep(1000); 883 966 884 967 // Verify URL was opened (should preserve http://) 885 - const windowList = await sharedBgWindow.evaluate(async () => { 968 + const windowList = await bgWindow.evaluate(async () => { 886 969 return await (window as any).app.window.list(); 887 970 }); 888 971 ··· 894 977 895 978 // Clean up 896 979 if (httpWindow) { 897 - await sharedBgWindow.evaluate(async (id: number) => { 980 + await bgWindow.evaluate(async (id: number) => { 898 981 await (window as any).app.window.close(id); 899 982 }, httpWindow.id); 900 983 } 901 984 }); 902 985 903 986 test('cmd panel opens URL with https protocol', async () => { 904 - await waitForExtensionsReady(sharedBgWindow, 15000); 987 + await waitForExtensionsReady(bgWindow, 15000); 905 988 906 989 // Open cmd panel 907 - const openResult = await sharedBgWindow.evaluate(async () => { 990 + const openResult = await bgWindow.evaluate(async () => { 908 991 return await (window as any).app.window.open('peek://cmd/panel.html', { 909 992 modal: true, 910 993 width: 600, ··· 917 1000 }); 918 1001 expect(openResult.success).toBe(true); 919 1002 920 - const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 1003 + const cmdWindow = await app.getWindow('cmd/panel.html', 5000); 921 1004 await cmdWindow.waitForSelector('input', { timeout: 5000 }); 922 1005 923 1006 // Type URL with https protocol ··· 928 1011 await sleep(1000); 929 1012 930 1013 // Verify URL was opened 931 - const windowList = await sharedBgWindow.evaluate(async () => { 1014 + const windowList = await bgWindow.evaluate(async () => { 932 1015 return await (window as any).app.window.list(); 933 1016 }); 934 1017 ··· 940 1023 941 1024 // Clean up 942 1025 if (httpsWindow) { 943 - await sharedBgWindow.evaluate(async (id: number) => { 1026 + await bgWindow.evaluate(async (id: number) => { 944 1027 await (window as any).app.window.close(id); 945 1028 }, httpsWindow.id); 946 1029 } 947 1030 }); 948 1031 949 1032 test('cmd panel opens localhost URLs', async () => { 950 - await waitForExtensionsReady(sharedBgWindow, 15000); 1033 + await waitForExtensionsReady(bgWindow, 15000); 951 1034 952 1035 // Open cmd panel 953 - const openResult = await sharedBgWindow.evaluate(async () => { 1036 + const openResult = await bgWindow.evaluate(async () => { 954 1037 return await (window as any).app.window.open('peek://cmd/panel.html', { 955 1038 modal: true, 956 1039 width: 600, ··· 963 1046 }); 964 1047 expect(openResult.success).toBe(true); 965 1048 966 - const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 1049 + const cmdWindow = await app.getWindow('cmd/panel.html', 5000); 967 1050 await cmdWindow.waitForSelector('input', { timeout: 5000 }); 968 1051 969 1052 // Type localhost with port ··· 974 1057 await sleep(1000); 975 1058 976 1059 // Verify URL was opened (normalized to https://localhost:3000) 977 - const windowList = await sharedBgWindow.evaluate(async () => { 1060 + const windowList = await bgWindow.evaluate(async () => { 978 1061 return await (window as any).app.window.list(); 979 1062 }); 980 1063 ··· 986 1069 987 1070 // Clean up 988 1071 if (localhostWindow) { 989 - await sharedBgWindow.evaluate(async (id: number) => { 1072 + await bgWindow.evaluate(async (id: number) => { 990 1073 await (window as any).app.window.close(id); 991 1074 }, localhostWindow.id); 992 1075 } 993 1076 }); 994 1077 995 1078 test('cmd panel ignores non-URL non-command text on Enter', async () => { 996 - await waitForExtensionsReady(sharedBgWindow, 15000); 1079 + await waitForExtensionsReady(bgWindow, 15000); 997 1080 998 1081 // Snapshot window list before 999 - const beforeList = await sharedBgWindow.evaluate(async () => { 1082 + const beforeList = await bgWindow.evaluate(async () => { 1000 1083 return await (window as any).app.window.list(); 1001 1084 }); 1002 1085 const beforeCount = beforeList.windows?.length || 0; 1003 1086 1004 1087 // Open cmd panel 1005 - const openResult = await sharedBgWindow.evaluate(async () => { 1088 + const openResult = await bgWindow.evaluate(async () => { 1006 1089 return await (window as any).app.window.open('peek://cmd/panel.html', { 1007 1090 modal: true, 1008 1091 width: 600, ··· 1015 1098 }); 1016 1099 expect(openResult.success).toBe(true); 1017 1100 1018 - const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 1101 + const cmdWindow = await app.getWindow('cmd/panel.html', 5000); 1019 1102 await cmdWindow.waitForSelector('input', { timeout: 5000 }); 1020 1103 1021 1104 // Type non-URL text (no dots, no protocol) — not a command either ··· 1026 1109 await sleep(500); 1027 1110 1028 1111 // Verify no new windows were opened (non-URL text is ignored, not routed anywhere) 1029 - const afterList = await sharedBgWindow.evaluate(async () => { 1112 + const afterList = await bgWindow.evaluate(async () => { 1030 1113 return await (window as any).app.window.list(); 1031 1114 }); 1032 1115 ··· 1044 1127 1045 1128 // Close cmd panel if still open 1046 1129 if (openResult.id) { 1047 - await sharedBgWindow.evaluate(async (id: number) => { 1130 + await bgWindow.evaluate(async (id: number) => { 1048 1131 try { 1049 1132 await (window as any).app.window.close(id); 1050 1133 } catch (e) { ··· 1059 1142 // in another app when Peek is set as default browser). 1060 1143 // Tests the fix for: first click focuses app but doesn't open URL, second click works. 1061 1144 1062 - await waitForExtensionsReady(sharedBgWindow, 15000); 1145 + await waitForExtensionsReady(bgWindow, 15000); 1063 1146 1064 1147 // Get initial window count 1065 - const initialList = await sharedBgWindow.evaluate(async () => { 1148 + const initialList = await bgWindow.evaluate(async () => { 1066 1149 return await (window as any).app.window.list(); 1067 1150 }); 1068 1151 const initialCount = initialList.windows?.length || 0; ··· 1072 1155 const testUrl = 'https://example.com/external-test'; 1073 1156 1074 1157 // Trigger external:open-url event directly (simulates what handleExternalUrl does) 1075 - await sharedBgWindow.evaluate(async (url: string) => { 1158 + await bgWindow.evaluate(async (url: string) => { 1076 1159 const api = (window as any).app; 1077 1160 // Publish the same event that handleExternalUrl publishes — core 1078 1161 // renderer subscribes on GLOBAL scope, so publish with GLOBAL explicitly ··· 1089 1172 await sleep(500); 1090 1173 1091 1174 // Verify URL was opened 1092 - const finalList = await sharedBgWindow.evaluate(async () => { 1175 + const finalList = await bgWindow.evaluate(async () => { 1093 1176 return await (window as any).app.window.list(); 1094 1177 }); 1095 1178 ··· 1107 1190 1108 1191 // Clean up 1109 1192 if (externalWindow) { 1110 - await sharedBgWindow.evaluate(async (id: number) => { 1193 + await bgWindow.evaluate(async (id: number) => { 1111 1194 await (window as any).app.window.close(id); 1112 1195 }, externalWindow.id); 1113 1196 } ··· 1120 1203 // The previous test simulates via pubsub from the renderer; this test 1121 1204 // exercises the full main-process -> pubsub -> renderer -> window-open flow. 1122 1205 1123 - await waitForExtensionsReady(sharedBgWindow, 15000); 1206 + await waitForExtensionsReady(bgWindow, 15000); 1124 1207 1125 1208 // Get initial window count (include internal to see ALL windows) 1126 - const initialList = await sharedBgWindow.evaluate(async () => { 1209 + const initialList = await bgWindow.evaluate(async () => { 1127 1210 return await (window as any).app.window.list(); 1128 1211 }); 1129 1212 const initialCount = initialList.windows?.length || 0; ··· 1131 1214 const testUrl = 'https://example.com/main-process-external-test'; 1132 1215 1133 1216 // Call handleExternalUrl from the main process (simulates real OS open-url) 1134 - const mainResult = await sharedApp.evaluateMain!(({ app }) => { 1217 + const mainResult = await app.evaluateMain!(({ app }) => { 1135 1218 const { handleExternalUrl } = (globalThis as any).__peek_test; 1136 1219 if (!handleExternalUrl) return { error: 'handleExternalUrl not found on __peek_test' }; 1137 1220 handleExternalUrl('https://example.com/main-process-external-test', 'os'); ··· 1145 1228 let externalWindow: any = null; 1146 1229 const deadline = Date.now() + 5000; 1147 1230 while (Date.now() < deadline) { 1148 - const list = await sharedBgWindow.evaluate(async () => { 1231 + const list = await bgWindow.evaluate(async () => { 1149 1232 return await (window as any).app.window.list(); 1150 1233 }); 1151 1234 externalWindow = list.windows?.find((w: any) => ··· 1159 1242 1160 1243 // Clean up 1161 1244 if (externalWindow) { 1162 - await sharedBgWindow.evaluate(async (id: number) => { 1245 + await bgWindow.evaluate(async (id: number) => { 1163 1246 await (window as any).app.window.close(id); 1164 1247 }, externalWindow.id); 1165 1248 } ··· 1379 1462 // ============================================================================ 1380 1463 1381 1464 test.describe('Core Functionality @desktop', () => { 1465 + let app: DesktopApp; 1466 + let bgWindow: Page; 1467 + 1468 + test.beforeAll(async () => { 1469 + ({ app, bgWindow } = await createPerDescribeApp('core')); 1470 + }); 1471 + 1472 + test.afterAll(async () => { 1473 + if (app) await app.close(); 1474 + }); 1475 + 1382 1476 test('app launches and extensions load', async () => { 1383 1477 // After v2 tile migration: 1384 1478 // - V2 features load as separate background BrowserWindows (peek://{id}/background.html) ··· 1388 1482 // Check that at least one eager v2 background tile window exists. 1389 1483 // peeks and slides are eager v2 background tiles that launch at startup. 1390 1484 const v2BgWindow = await waitForWindow( 1391 - () => sharedApp.windows(), 1485 + () => app.windows(), 1392 1486 'peek://peeks/background.html', 1393 1487 15000 1394 1488 ); ··· 1396 1490 }); 1397 1491 1398 1492 test('database is accessible', async () => { 1399 - const result = await sharedBgWindow.evaluate(async () => { 1493 + const result = await bgWindow.evaluate(async () => { 1400 1494 return await (window as any).app.datastore.getStats(); 1401 1495 }); 1402 1496 expect(result.success).toBe(true); ··· 1406 1500 test('commands are registered', async () => { 1407 1501 // Commands are now owned by the cmd extension via pubsub 1408 1502 // Query via cmd:query-commands topic with retry for extension loading 1409 - const result = await sharedBgWindow.evaluate(async () => { 1503 + const result = await bgWindow.evaluate(async () => { 1410 1504 const api = (window as any).app; 1411 1505 1412 1506 const queryCommands = () => new Promise((resolve) => { ··· 1438 1532 test('quit and restart commands are registered', async () => { 1439 1533 // quit/restart are registered asynchronously during app boot (app/index.js). 1440 1534 // Poll via waitForCommand before querying details to avoid startup-race flake. 1441 - await waitForCommand(sharedBgWindow, 'quit', 10000); 1442 - await waitForCommand(sharedBgWindow, 'restart', 10000); 1535 + await waitForCommand(bgWindow, 'quit', 10000); 1536 + await waitForCommand(bgWindow, 'restart', 10000); 1443 1537 1444 1538 // Query commands via cmd extension to verify descriptions 1445 - const result = await sharedBgWindow.evaluate(async () => { 1539 + const result = await bgWindow.evaluate(async () => { 1446 1540 const api = (window as any).app; 1447 1541 1448 1542 return new Promise((resolve) => { ··· 1467 1561 }); 1468 1562 1469 1563 test('reload extension command is registered', async () => { 1470 - const result = await sharedBgWindow.evaluate(async () => { 1564 + const result = await bgWindow.evaluate(async () => { 1471 1565 const api = (window as any).app; 1472 1566 1473 1567 return new Promise((resolve) => { ··· 1489 1583 }); 1490 1584 1491 1585 test('api.quit and api.restart functions exist', async () => { 1492 - const result = await sharedBgWindow.evaluate(() => { 1586 + const result = await bgWindow.evaluate(() => { 1493 1587 const api = (window as any).app; 1494 1588 return { 1495 1589 hasQuit: typeof api.quit === 'function', ··· 1503 1597 1504 1598 test('window management works', async () => { 1505 1599 // Open a test window 1506 - const openResult = await sharedBgWindow.evaluate(async () => { 1600 + const openResult = await bgWindow.evaluate(async () => { 1507 1601 return await (window as any).app.window.open('about:blank', { 1508 1602 width: 400, 1509 1603 height: 300 ··· 1513 1607 expect(openResult.id).toBeDefined(); 1514 1608 1515 1609 // Wait for window to open 1516 - await sharedApp.getWindow('about:blank', 5000); 1610 + await app.getWindow('about:blank', 5000); 1517 1611 1518 1612 // List windows 1519 - const listResult = await sharedBgWindow.evaluate(async () => { 1613 + const listResult = await bgWindow.evaluate(async () => { 1520 1614 return await (window as any).app.window.list(); 1521 1615 }); 1522 1616 expect(listResult.success).toBe(true); 1523 1617 expect(Array.isArray(listResult.windows)).toBe(true); 1524 1618 1525 1619 // Close the window 1526 - await sharedBgWindow.evaluate(async (id: number) => { 1620 + await bgWindow.evaluate(async (id: number) => { 1527 1621 return await (window as any).app.window.close(id); 1528 1622 }, openResult.id); 1529 1623 }); ··· 1534 1628 // ============================================================================ 1535 1629 1536 1630 test.describe('Tag Command @desktop', () => { 1631 + let app: DesktopApp; 1537 1632 let bgWindow: Page; 1538 1633 1539 1634 test.beforeAll(async () => { 1540 - bgWindow = sharedBgWindow; 1635 + ({ app, bgWindow } = await createPerDescribeApp('tag-cmd')); 1636 + }); 1637 + 1638 + test.afterAll(async () => { 1639 + if (app) await app.close(); 1541 1640 }); 1542 1641 1543 1642 test('creates address if not exists when tagging', async () => { ··· 1744 1843 // ============================================================================ 1745 1844 1746 1845 test.describe('Command Execution @desktop', () => { 1846 + let app: DesktopApp; 1747 1847 let bgWindow: Page; 1748 1848 let pageWindowId: number | null = null; 1749 1849 const testPageUrl = `https://cmd-exec-test-${Date.now()}.example.com/`; 1750 1850 1751 1851 test.beforeAll(async () => { 1752 - bgWindow = sharedBgWindow; 1753 - await waitForExtensionsReady(bgWindow); 1852 + ({ app, bgWindow } = await createPerDescribeApp('cmd-exec')); 1754 1853 1755 1854 // Open a page window so tag commands have an "active window" to work with 1756 1855 const openResult = await bgWindow.evaluate(async (url: string) => { ··· 1773 1872 1774 1873 test.afterAll(async () => { 1775 1874 // Close the page window we opened 1776 - if (pageWindowId) { 1777 - await bgWindow.evaluate(async (id: number) => { 1778 - return await (window as any).app.window.close(id); 1779 - }, pageWindowId); 1875 + if (pageWindowId && bgWindow && !bgWindow.isClosed()) { 1876 + try { 1877 + await bgWindow.evaluate(async (id: number) => { 1878 + return await (window as any).app.window.close(id); 1879 + }, pageWindowId); 1880 + } catch { /* app may already be closing */ } 1780 1881 } 1882 + if (app) await app.close(); 1781 1883 }); 1782 1884 1783 1885 test('tag command with # prefixed tags stores tags without prefix', async () => { ··· 2084 2186 // listener is live. Electron's `webContents.send` is fire-and-forget — 2085 2187 // a pubsub event that arrives before the listener is attached is 2086 2188 // silently dropped, which is the root cause of the full-suite flake. 2087 - const pageWindow = await sharedApp.getWindow(dynamicKey, 10000); 2189 + const pageWindow = await app.getWindow(dynamicKey, 10000); 2088 2190 expect(pageWindow).toBeTruthy(); 2089 2191 await pageWindow.waitForFunction( 2090 2192 () => (window as unknown as { __pageModuleReady?: boolean }).__pageModuleReady === true, ··· 2291 2393 // ============================================================================ 2292 2394 2293 2395 test.describe('Tag Events @desktop', () => { 2396 + let app: DesktopApp; 2294 2397 let bgWindow: Page; 2295 2398 2296 2399 test.beforeAll(async () => { 2297 - bgWindow = sharedBgWindow; 2400 + ({ app, bgWindow } = await createPerDescribeApp('tag-events')); 2401 + }); 2402 + 2403 + test.afterAll(async () => { 2404 + if (app) await app.close(); 2298 2405 }); 2299 2406 2300 2407 test('tag:created is emitted when new tag is created', async () => { ··· 2496 2603 // ============================================================================ 2497 2604 2498 2605 test.describe('Item Events @desktop', () => { 2606 + let app: DesktopApp; 2499 2607 let bgWindow: Page; 2500 2608 2501 2609 test.beforeAll(async () => { 2502 - bgWindow = sharedBgWindow; 2610 + ({ app, bgWindow } = await createPerDescribeApp('item-events')); 2611 + }); 2612 + 2613 + test.afterAll(async () => { 2614 + if (app) await app.close(); 2503 2615 }); 2504 2616 2505 2617 test('item:created is emitted when item is added', async () => { ··· 2632 2744 // ============================================================================ 2633 2745 2634 2746 test.describe('Groups View @desktop', () => { 2747 + let app: DesktopApp; 2635 2748 let bgWindow: Page; 2636 2749 2637 2750 test.beforeAll(async () => { 2638 - bgWindow = sharedBgWindow; 2751 + ({ app, bgWindow } = await createPerDescribeApp('groups-view')); 2752 + }); 2753 + 2754 + test.afterAll(async () => { 2755 + if (app) await app.close(); 2639 2756 }); 2640 2757 2641 2758 test('empty groups are not shown in groups list', async () => { ··· 2683 2800 expect(groupsResult.success).toBe(true); 2684 2801 2685 2802 // Find the groups window (getWindow polls) 2686 - const groupsWindow = await sharedApp.getWindow('groups/home.html', 5000); 2803 + const groupsWindow = await app.getWindow('groups/home.html', 5000); 2687 2804 expect(groupsWindow).toBeTruthy(); 2688 2805 await groupsWindow.waitForLoadState('domcontentloaded'); 2689 2806 ··· 2742 2859 expect(groupsResult.success).toBe(true); 2743 2860 2744 2861 // Find the groups window (getWindow polls) 2745 - const groupsWindow = await sharedApp.getWindow('groups/home.html', 5000); 2862 + const groupsWindow = await app.getWindow('groups/home.html', 5000); 2746 2863 expect(groupsWindow).toBeTruthy(); 2747 2864 await groupsWindow.waitForLoadState('domcontentloaded'); 2748 2865 ··· 2889 3006 // ============================================================================ 2890 3007 2891 3008 test.describe('Command Chaining @desktop', () => { 3009 + let app: DesktopApp; 2892 3010 let bgWindow: Page; 2893 3011 2894 3012 test.beforeAll(async () => { 2895 - bgWindow = sharedBgWindow; 3013 + ({ app, bgWindow } = await createPerDescribeApp('cmd-chain')); 3014 + }); 3015 + 3016 + test.afterAll(async () => { 3017 + if (app) await app.close(); 2896 3018 }); 2897 3019 2898 3020 test('cmd panel loads with chain state initialized', async () => { ··· 2910 3032 }); 2911 3033 expect(openResult.success).toBe(true); 2912 3034 2913 - const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 3035 + const cmdWindow = await app.getWindow('cmd/panel.html', 5000); 2914 3036 expect(cmdWindow).toBeTruthy(); 2915 3037 2916 3038 // Verify state object has chain properties ··· 2947 3069 expect(openResult.success).toBe(true); 2948 3070 2949 3071 // Find the cmd window 2950 - const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 3072 + const cmdWindow = await app.getWindow('cmd/panel.html', 5000); 2951 3073 expect(cmdWindow).toBeTruthy(); 2952 3074 2953 3075 // Test MIME type matching function (if exposed, or test via behavior) ··· 2985 3107 expect(openResult.success).toBe(true); 2986 3108 2987 3109 // Find the cmd window 2988 - const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 3110 + const cmdWindow = await app.getWindow('cmd/panel.html', 5000); 2989 3111 expect(cmdWindow).toBeTruthy(); 2990 3112 2991 3113 // Wait for input to be ready ··· 3019 3141 }); 3020 3142 expect(openResult.success).toBe(true); 3021 3143 3022 - const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 3144 + const cmdWindow = await app.getWindow('cmd/panel.html', 5000); 3023 3145 expect(cmdWindow).toBeTruthy(); 3024 3146 3025 3147 // Check chain indicator element exists ··· 3073 3195 }); 3074 3196 expect(openResult.success).toBe(true); 3075 3197 3076 - const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 3198 + const cmdWindow = await app.getWindow('cmd/panel.html', 5000); 3077 3199 expect(cmdWindow).toBeTruthy(); 3078 3200 3079 3201 // Wait for input to be ready and commands to be loaded ··· 3135 3257 }); 3136 3258 expect(openResult.success).toBe(true); 3137 3259 3138 - const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 3260 + const cmdWindow = await app.getWindow('cmd/panel.html', 5000); 3139 3261 await cmdWindow.waitForSelector('input', { timeout: 5000 }); 3140 3262 await waitForPanelCommandsLoaded(cmdWindow); 3141 3263 ··· 3183 3305 }); 3184 3306 expect(openResult.success).toBe(true); 3185 3307 3186 - const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 3308 + const cmdWindow = await app.getWindow('cmd/panel.html', 5000); 3187 3309 await cmdWindow.waitForSelector('input', { timeout: 5000 }); 3188 3310 await waitForPanelCommandsLoaded(cmdWindow); 3189 3311 ··· 3260 3382 }); 3261 3383 expect(openResult.success).toBe(true); 3262 3384 3263 - const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 3385 + const cmdWindow = await app.getWindow('cmd/panel.html', 5000); 3264 3386 await cmdWindow.waitForSelector('input', { timeout: 5000 }); 3265 3387 await waitForPanelCommandsLoaded(cmdWindow); 3266 3388 ··· 3313 3435 }); 3314 3436 expect(openResult.success).toBe(true); 3315 3437 3316 - const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 3438 + const cmdWindow = await app.getWindow('cmd/panel.html', 5000); 3317 3439 await cmdWindow.waitForSelector('input', { timeout: 5000 }); 3318 3440 await waitForPanelCommandsLoaded(cmdWindow); 3319 3441 ··· 3360 3482 // ============================================================================ 3361 3483 3362 3484 test.describe('Type-Specific Noun Commands @desktop', () => { 3485 + let app: DesktopApp; 3363 3486 let bgWindow: Page; 3364 3487 3365 3488 test.beforeAll(async () => { 3366 - bgWindow = sharedBgWindow; 3489 + ({ app, bgWindow } = await createPerDescribeApp('nouns')); 3490 + }); 3491 + 3492 + test.afterAll(async () => { 3493 + if (app) await app.close(); 3367 3494 }); 3368 3495 3369 3496 test('list notes command produces array output', async () => { ··· 3390 3517 }); 3391 3518 expect(openResult.success).toBe(true); 3392 3519 3393 - const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 3520 + const cmdWindow = await app.getWindow('cmd/panel.html', 5000); 3394 3521 expect(cmdWindow).toBeTruthy(); 3395 3522 3396 3523 await cmdWindow.waitForSelector('input', { timeout: 5000 }); ··· 3443 3570 }); 3444 3571 expect(openResult.success).toBe(true); 3445 3572 3446 - const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 3573 + const cmdWindow = await app.getWindow('cmd/panel.html', 5000); 3447 3574 await cmdWindow.waitForSelector('input', { timeout: 5000 }); 3448 3575 await waitForPanelCommandsLoaded(cmdWindow); 3449 3576 ··· 3517 3644 }); 3518 3645 expect(openResult.success).toBe(true); 3519 3646 3520 - const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 3647 + const cmdWindow = await app.getWindow('cmd/panel.html', 5000); 3521 3648 expect(cmdWindow).toBeTruthy(); 3522 3649 3523 3650 await cmdWindow.waitForSelector('input', { timeout: 5000 }); ··· 3579 3706 }); 3580 3707 expect(openResult.success).toBe(true); 3581 3708 3582 - const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 3709 + const cmdWindow = await app.getWindow('cmd/panel.html', 5000); 3583 3710 expect(cmdWindow).toBeTruthy(); 3584 3711 3585 3712 await cmdWindow.waitForSelector('input', { timeout: 5000 }); ··· 3624 3751 // ============================================================================ 3625 3752 3626 3753 test.describe('Edit Command Param Mode @desktop', () => { 3754 + let app: DesktopApp; 3627 3755 let bgWindow: Page; 3628 3756 3629 3757 test.beforeAll(async () => { 3630 - bgWindow = sharedBgWindow; 3758 + ({ app, bgWindow } = await createPerDescribeApp('edit-param')); 3759 + }); 3760 + 3761 + test.afterAll(async () => { 3762 + if (app) await app.close(); 3631 3763 }); 3632 3764 3633 3765 test('Tab in param mode fills text, does NOT execute', async () => { ··· 3654 3786 }); 3655 3787 expect(openResult.success).toBe(true); 3656 3788 3657 - const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 3789 + const cmdWindow = await app.getWindow('cmd/panel.html', 5000); 3658 3790 expect(cmdWindow).toBeTruthy(); 3659 3791 3660 3792 await cmdWindow.waitForSelector('input', { timeout: 5000 }); ··· 3734 3866 }); 3735 3867 expect(openResult.success).toBe(true); 3736 3868 3737 - const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 3869 + const cmdWindow = await app.getWindow('cmd/panel.html', 5000); 3738 3870 expect(cmdWindow).toBeTruthy(); 3739 3871 3740 3872 await cmdWindow.waitForSelector('input', { timeout: 5000 }); ··· 3807 3939 }); 3808 3940 expect(openResult.success).toBe(true); 3809 3941 3810 - const cmdWindow = await sharedApp.getWindow('cmd/panel.html', 5000); 3942 + const cmdWindow = await app.getWindow('cmd/panel.html', 5000); 3811 3943 expect(cmdWindow).toBeTruthy(); 3812 3944 3813 3945 await cmdWindow.waitForSelector('input', { timeout: 5000 }); ··· 3845 3977 // ============================================================================ 3846 3978 3847 3979 test.describe('Themes @desktop', () => { 3980 + let app: DesktopApp; 3848 3981 let bgWindow: Page; 3849 3982 3850 3983 test.beforeAll(async () => { 3851 - bgWindow = sharedBgWindow; 3984 + ({ app, bgWindow } = await createPerDescribeApp('themes')); 3985 + }); 3986 + 3987 + test.afterAll(async () => { 3988 + if (app) await app.close(); 3852 3989 }); 3853 3990 3854 3991 test('theme API is available', async () => { ··· 3991 4128 // ============================================================================ 3992 4129 3993 4130 test.describe('Command Registration Performance @desktop', () => { 4131 + let app: DesktopApp; 3994 4132 let bgWindow: Page; 3995 4133 3996 4134 test.beforeAll(async () => { 3997 - bgWindow = sharedBgWindow; 4135 + ({ app, bgWindow } = await createPerDescribeApp('cmd-perf')); 3998 4136 // Wait for cmd extension to be fully ready before running performance tests 3999 4137 await waitForExtensionsReady(bgWindow, 15000); 4138 + }); 4139 + 4140 + test.afterAll(async () => { 4141 + if (app) await app.close(); 4000 4142 }); 4001 4143 4002 4144 test('cmd:register-batch is handled by cmd extension', async () => { ··· 4055 4197 // ============================================================================ 4056 4198 4057 4199 test.describe('Startup Phase Events @desktop', () => { 4200 + let app: DesktopApp; 4058 4201 let bgWindow: Page; 4059 4202 4060 4203 test.beforeAll(async () => { 4061 - bgWindow = sharedBgWindow; 4204 + ({ app, bgWindow } = await createPerDescribeApp('startup-phase')); 4062 4205 // Wait for extensions to be fully ready 4063 4206 await waitForExtensionsReady(bgWindow); 4207 + }); 4208 + 4209 + test.afterAll(async () => { 4210 + if (app) await app.close(); 4064 4211 }); 4065 4212 4066 4213 test('ext:startup:phase events are available for subscription', async () => { ··· 4159 4306 // ============================================================================ 4160 4307 4161 4308 test.describe('Hybrid Extension Mode @desktop', () => { 4309 + let app: DesktopApp; 4162 4310 let bgWindow: Page; 4163 4311 4164 4312 test.beforeAll(async () => { 4165 - bgWindow = sharedBgWindow; 4313 + ({ app, bgWindow } = await createPerDescribeApp('hybrid-mode')); 4314 + }); 4315 + 4316 + test.afterAll(async () => { 4317 + if (app) await app.close(); 4166 4318 }); 4167 4319 4168 4320 test('v2 background tile windows exist as separate BrowserWindows', async () => { 4169 4321 // V2 background tiles (peeks, slides) launch as separate hidden BrowserWindows 4170 4322 // at peek://{id}/background.html — NOT as iframes in the extension host. 4171 4323 const peeksWin = await waitForWindow( 4172 - () => sharedApp.windows(), 4324 + () => app.windows(), 4173 4325 'peek://peeks/background.html', 4174 4326 15000 4175 4327 ); 4176 4328 expect(peeksWin).toBeDefined(); 4177 4329 4178 4330 const slidesWin = await waitForWindow( 4179 - () => sharedApp.windows(), 4331 + () => app.windows(), 4180 4332 'peek://slides/background.html', 4181 4333 15000 4182 4334 ); 4183 4335 expect(slidesWin).toBeDefined(); 4184 - }); 4185 - 4186 - test.skip('example extension loads as separate window (external)', async () => { 4187 - // SKIPPED: After v2 tile migration, the example extension is a v2 tile 4188 - // (manifestVersion: 2) with a lazy background tile. It no longer loads at 4189 - // startup as an "external" v1 window at peek://ext/example/background.html. 4190 - // Instead it launches at peek://example/background.html on first command 4191 - // invocation. See the "v2 background tile windows" test for eager v2 tiles. 4192 - // 4193 - // No extension currently uses the v1 external-window pattern after the 4194 - // v2 migration, so there is no equivalent to test here. 4195 4336 }); 4196 4337 4197 4338 test('api.extensions.reload() reloads external extension', async () => { ··· 4289 4430 // - Lazy v2 tiles (including 'example') do NOT load at startup; they 4290 4431 // only launch at peek://{id}/background.html on first command invoke. 4291 4432 // - Plus any UI windows (settings, etc.) 4292 - const windows = sharedApp.windows(); 4433 + const windows = app.windows(); 4293 4434 4294 4435 const coreBgWindows = windows.filter(w => w.url().includes('peek://app/background.html')); 4295 4436 expect(coreBgWindows.length).toBe(1); ··· 4528 4669 // the user was looking at before opening the palette. 4529 4670 4530 4671 test.describe('Window Targeting @desktop', () => { 4672 + let app: DesktopApp; 4531 4673 let bgWindow: Page; 4532 4674 4533 4675 test.beforeAll(async () => { 4534 - bgWindow = sharedBgWindow; 4676 + ({ app, bgWindow } = await createPerDescribeApp('window-targeting')); 4535 4677 await sleep(500); // Wait for app to stabilize 4536 4678 }); 4537 4679 4680 + test.afterAll(async () => { 4681 + if (app) await app.close(); 4682 + }); 4683 + 4538 4684 test('setWindowColorScheme returns success with windowId', async () => { 4539 4685 // Test that setWindowColorScheme works and returns expected data 4540 4686 const result = await bgWindow.evaluate(async () => { ··· 4684 4830 // ============================================================================ 4685 4831 4686 4832 test.describe('Backup @desktop', () => { 4833 + let app: DesktopApp; 4687 4834 let bgWindow: Page; 4688 4835 4689 4836 test.beforeAll(async () => { 4690 - bgWindow = sharedBgWindow; 4837 + ({ app, bgWindow } = await createPerDescribeApp('backup')); 4838 + }); 4839 + 4840 + test.afterAll(async () => { 4841 + if (app) await app.close(); 4691 4842 }); 4692 4843 4693 4844 test('backup-get-config returns config object', async () => { ··· 4818 4969 // ============================================================================ 4819 4970 4820 4971 test.describe('IZUI Behavior @desktop', () => { 4972 + let app: DesktopApp; 4973 + let bgWindow: Page; 4974 + 4975 + test.beforeAll(async () => { 4976 + ({ app, bgWindow } = await createPerDescribeApp('izui-behavior')); 4977 + }); 4978 + 4979 + test.afterAll(async () => { 4980 + if (app) await app.close(); 4981 + }); 4982 + 4821 4983 test('parentWindowId is set when opened from a content window', async () => { 4822 - const bgWindow = sharedBgWindow; 4823 4984 4824 4985 // Step 1: Open a content window (groups home) from the background window. 4825 4986 // Since background.html is an internal URL, parentWindowId should be null. ··· 4832 4993 expect(groupsResult.success).toBe(true); 4833 4994 const groupsWindowId = groupsResult.id; 4834 4995 4835 - const groupsWindow = await sharedApp.getWindow('groups/home.html', 5000); 4996 + const groupsWindow = await app.getWindow('groups/home.html', 5000); 4836 4997 expect(groupsWindow).toBeTruthy(); 4837 4998 await groupsWindow.waitForLoadState('domcontentloaded'); 4838 4999 ··· 4879 5040 }); 4880 5041 4881 5042 test('onEscape registers callback without changing backend escapeMode (role-based)', async () => { 4882 - const bgWindow = sharedBgWindow; 4883 5043 4884 5044 // Open a plain window with no escapeMode set (defaults to 'auto') 4885 5045 const result = await bgWindow.evaluate(async () => { ··· 4891 5051 expect(result.success).toBe(true); 4892 5052 const windowId = result.id; 4893 5053 4894 - const contentWindow = await sharedApp.getWindow('about:blank', 5000); 5054 + const contentWindow = await app.getWindow('about:blank', 5000); 4895 5055 expect(contentWindow).toBeTruthy(); 4896 5056 4897 5057 // Get escapeMode before registering handler ··· 4938 5098 }); 4939 5099 4940 5100 test('izui-close-self closes the window', async () => { 4941 - const bgWindow = sharedBgWindow; 4942 5101 4943 5102 // Open a content window 4944 5103 const result = await bgWindow.evaluate(async () => { ··· 4951 5110 expect(result.success).toBe(true); 4952 5111 const windowId = result.id; 4953 5112 4954 - const contentWindow = await sharedApp.getWindow('groups/home.html', 5000); 5113 + const contentWindow = await app.getWindow('groups/home.html', 5000); 4955 5114 expect(contentWindow).toBeTruthy(); 4956 5115 await contentWindow.waitForLoadState('domcontentloaded'); 4957 5116 ··· 4975 5134 }); 4976 5135 4977 5136 test('item:created fires from trackWindowLoad when opening external URL', async () => { 4978 - const bgWindow = sharedBgWindow; 4979 5137 const timestamp = Date.now(); 4980 5138 const testUrl = `https://track-window-load-${timestamp}.example.com`; 4981 5139 ··· 5032 5190 }); 5033 5191 5034 5192 test('ESC debouncing: two rapid presses trigger only one handler call', async () => { 5035 - const bgWindow = sharedBgWindow; 5036 5193 5037 5194 // Open a groups window with navigate escape mode 5038 5195 const result = await bgWindow.evaluate(async () => { ··· 5045 5202 expect(result.success).toBe(true); 5046 5203 const windowId = result.id; 5047 5204 5048 - const groupsWindow = await sharedApp.getWindow('groups/home.html', 5000); 5205 + const groupsWindow = await app.getWindow('groups/home.html', 5000); 5049 5206 expect(groupsWindow).toBeTruthy(); 5050 5207 await groupsWindow.waitForLoadState('domcontentloaded'); 5051 5208 await groupsWindow.waitForSelector('.cards', { timeout: 5000 }); ··· 5143 5300 // ============================================================================ 5144 5301 5145 5302 test.describe('Shortcut Roundtrip @desktop', () => { 5303 + let app: DesktopApp; 5304 + let bgWindow: Page; 5305 + 5306 + test.beforeAll(async () => { 5307 + ({ app, bgWindow } = await createPerDescribeApp('shortcut-roundtrip')); 5308 + }); 5309 + 5310 + test.afterAll(async () => { 5311 + if (app) await app.close(); 5312 + }); 5313 + 5146 5314 test('local shortcut from background window roundtrip', async () => { 5147 5315 // Register a local shortcut from bgWindow, trigger it via handleLocalShortcut 5148 5316 // in the main process, verify callback fires in the renderer. 5149 5317 // This tests the basic ev.reply roundtrip for a normal BrowserWindow WebContents. 5150 - const bgWindow = sharedBgWindow; 5151 5318 5152 5319 // Register a local shortcut from the bgWindow 5153 5320 await bgWindow.evaluate(() => { ··· 5168 5335 // process "evaluate" context as destroyed if we return the raw result. To 5169 5336 // avoid this flakiness, wrap the call in setImmediate + return via a 5170 5337 // pre-computed flag so the evaluate settles cleanly before IPC fans out. 5171 - const handled = await sharedApp.evaluateMain!(({ app }) => { 5338 + const handled = await app.evaluateMain!(({ app }) => { 5172 5339 try { 5173 5340 const { handleLocalShortcut } = (globalThis as any).__peek_test; 5174 5341 const result = handleLocalShortcut({ ··· 5211 5378 }); 5212 5379 }); 5213 5380 5214 - test.skip('shortcut from external extension window roundtrip', async () => { 5215 - // SKIPPED: After v2 tile migration, no extension uses the v1 external-window 5216 - // pattern (peek://ext/{id}/background.html). The example extension is now a 5217 - // v2 tile launched at peek://example/background.html. v2 tile windows use 5218 - // tile-preload.ts which does NOT expose api.shortcuts (capability-scoped API 5219 - // does not currently include shortcut registration), so this scenario can't 5220 - // be reproduced. The general BrowserWindow ev.reply roundtrip is still 5221 - // covered by "local shortcut from background window roundtrip" above. 5222 - }); 5223 5381 }); 5224 5382 5225 5383 // ============================================================================ ··· 5227 5385 // ============================================================================ 5228 5386 5229 5387 test.describe('Scripts Extension @desktop', () => { 5388 + let app: DesktopApp; 5389 + let bgWindow: Page; 5390 + 5391 + test.beforeAll(async () => { 5392 + ({ app, bgWindow } = await createPerDescribeApp('scripts')); 5393 + }); 5394 + 5395 + test.afterAll(async () => { 5396 + if (app) await app.close(); 5397 + }); 5398 + 5230 5399 test('create, save, and execute script', async () => { 5231 5400 // Wait for scripts extension to be ready 5232 - await waitForExtensionsReady(sharedBgWindow, 15000); 5401 + await waitForExtensionsReady(bgWindow, 15000); 5233 5402 5234 5403 // Create a new script directly via datastore 5235 - const scriptId = await sharedBgWindow.evaluate(async () => { 5404 + const scriptId = await bgWindow.evaluate(async () => { 5236 5405 const api = (window as any).app; 5237 5406 const scriptId = `script_test_${Date.now()}`; 5238 5407 ··· 5272 5441 expect(scriptId).toBeTruthy(); 5273 5442 5274 5443 // Verify script was saved 5275 - const savedScript = await sharedBgWindow.evaluate(async (scriptId) => { 5444 + const savedScript = await bgWindow.evaluate(async (scriptId) => { 5276 5445 const api = (window as any).app; 5277 5446 const settingsTable = await api.datastore.getTable('feature_settings'); 5278 5447 const scriptsRow = settingsTable.data?.['scripts:scripts']; ··· 5284 5453 expect(savedScript.name).toBe('Test Script'); 5285 5454 5286 5455 // Execute script - test executor directly 5287 - const executeResult = await sharedBgWindow.evaluate(async (scriptId) => { 5456 + const executeResult = await bgWindow.evaluate(async (scriptId) => { 5288 5457 const api = (window as any).app; 5289 5458 const { scriptExecutor } = await import('peek://scripts/script-executor.js'); 5290 5459 ··· 5312 5481 expect((executeResult as any).data.status).toBe('success'); 5313 5482 5314 5483 // Clean up - delete script 5315 - await sharedBgWindow.evaluate(async (scriptId) => { 5484 + await bgWindow.evaluate(async (scriptId) => { 5316 5485 const api = (window as any).app; 5317 5486 const settingsTable = await api.datastore.getTable('feature_settings'); 5318 5487 const scriptsRow = settingsTable.data?.['scripts:scripts']; ··· 5329 5498 5330 5499 test('script pattern matching works', async () => { 5331 5500 // Test pattern matching directly 5332 - const patternTests = await sharedBgWindow.evaluate(async () => { 5501 + const patternTests = await bgWindow.evaluate(async () => { 5333 5502 // Import the script executor module 5334 5503 const { ScriptExecutor } = await import('peek://scripts/script-executor.js'); 5335 5504 const executor = new ScriptExecutor(); ··· 5350 5519 5351 5520 test('script timeout protection works', async () => { 5352 5521 // Create a script that runs forever 5353 - const scriptId = await sharedBgWindow.evaluate(async () => { 5522 + const scriptId = await bgWindow.evaluate(async () => { 5354 5523 const api = (window as any).app; 5355 5524 const scriptId = `script_timeout_test_${Date.now()}`; 5356 5525 ··· 5385 5554 }); 5386 5555 5387 5556 // Execute with short timeout - test executor directly 5388 - const executeResult = await sharedBgWindow.evaluate(async (scriptId) => { 5557 + const executeResult = await bgWindow.evaluate(async (scriptId) => { 5389 5558 const api = (window as any).app; 5390 5559 const { scriptExecutor } = await import('peek://scripts/script-executor.js'); 5391 5560 ··· 5415 5584 expect((executeResult as any).data.error).toContain('timeout'); 5416 5585 5417 5586 // Clean up 5418 - await sharedBgWindow.evaluate(async (scriptId) => { 5587 + await bgWindow.evaluate(async (scriptId) => { 5419 5588 const api = (window as any).app; 5420 5589 const settingsTable = await api.datastore.getTable('feature_settings'); 5421 5590 const scriptsRow = settingsTable.data?.['scripts:scripts'];