experiments in a post-browser web
10
fork

Configure Feed

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

test(settings): Playwright regression guard for Settings → Extensions buttons

This surface has regressed three times with three different root
causes (adblocker getStatus, options-command registration, load-order
race). Helper-level unit tests cover the pure-logic layer; this spec
covers the end-to-end "buttons appear in the rendered Settings UI"
contract regardless of which underlying surface broke.

Two tests in tests/desktop/settings-extensions-buttons.spec.ts:

1. UI test — opens Settings via the bgWindow `api.window.open(
'peek://app/settings/settings.html', ...)` path (matches
permissions-settings.spec.ts), clicks the Privacy nav-item, then
`waitForFunction` polls #section-privacy for at least one button
labeled 'Popup' / 'Options' / 'Background' / 'Side Panel'. 8 s
timeout covers the chrome-extensions:loaded re-render race
(Settings opens → empty initially → event fires → re-render). If
buttons never appear, this fails immediately.

2. IPC layer test — calls `api.chromeExtensions.getUiEntries()`
directly and asserts ≥ 1 entry has `openable: true`. Catches a
regression in the load pipeline even before the UI renders, so
future failures pin down whether the bug is in the IPC layer or
the renderer.

Bundled Proton Pass declares both `action.default_popup` and
`options_ui.page`, so both assertions are reliably met when the load
pipeline + render are both healthy.

Test result: 2 passed in 2.8 s.

+112
+112
tests/desktop/settings-extensions-buttons.spec.ts
··· 1 + /** 2 + * Settings → Extensions (Privacy) — per-extension action buttons regression guard. 3 + * 4 + * This surface has regressed three times (different root causes each time). 5 + * The canonical failure mode: Settings is opened before chrome-extensions:loaded 6 + * fires, so getUiEntries() returns empty → no Popup/Options buttons render. 7 + * The current fix re-renders the section when the event fires. 8 + * 9 + * This test asserts the end-state regardless of timing: after Settings opens 10 + * and the user navigates to Extensions, at least one action button (Popup or 11 + * Options) is visible for a bundled extension (Proton Pass ships both). 12 + * 13 + * Run with: yarn test:grep "settings extensions buttons" 14 + */ 15 + 16 + import { test, expect, DesktopApp } from '../fixtures/desktop-app'; 17 + import { Page } from '@playwright/test'; 18 + import { createPerDescribeApp } from '../helpers/test-app'; 19 + 20 + test.describe('Settings Extensions buttons @desktop', () => { 21 + let app: DesktopApp; 22 + let bgWindow: Page; 23 + 24 + test.beforeAll(async () => { 25 + ({ app, bgWindow } = await createPerDescribeApp('settings-extensions-buttons')); 26 + }); 27 + 28 + test.afterAll(async () => { 29 + if (app) await app.close(); 30 + }); 31 + 32 + /** 33 + * Open the Settings window via the same api.window.open path used by 34 + * permissions-settings.spec.ts (the canonical pattern for this codebase). 35 + */ 36 + async function openSettings(): Promise<Page> { 37 + const result = await bgWindow.evaluate(async () => { 38 + return await (window as any).app.window.open('peek://app/settings/settings.html', { 39 + width: 1000, 40 + height: 700, 41 + key: 'settings', 42 + }); 43 + }); 44 + expect(result.success).toBe(true); 45 + const settingsWindow = await app.getWindow('settings/settings.html', 10000); 46 + await settingsWindow.waitForLoadState('domcontentloaded'); 47 + return settingsWindow; 48 + } 49 + 50 + test('settings extensions buttons render for bundled extensions', async () => { 51 + const settings = await openSettings(); 52 + 53 + // Navigate to the Extensions (Privacy) section. The click handler triggers 54 + // showSection('privacy') which makes the section visible. 55 + await settings.locator('a.nav-item[data-section="privacy"]').click(); 56 + 57 + // Wait for the section to be visible — the renderPrivacySettings() call is 58 + // async and chrome-extensions:loaded may re-render after initial open. 59 + // Poll for at least one action button (Popup / Options / etc.) appearing 60 + // inside #section-privacy. Proton Pass bundles both action.default_popup 61 + // and options_ui.page so it should always contribute ≥ 2 buttons. 62 + // Generous 8 s timeout to cover the loaded-before-settings-open race path. 63 + await settings.waitForFunction( 64 + () => { 65 + const section = document.querySelector('#section-privacy'); 66 + if (!section) return false; 67 + // The buttons are plain <button> elements inside .item-card divs. 68 + // Their textContent matches entryTypeLabels: 'Popup', 'Options', etc. 69 + const buttons = Array.from(section.querySelectorAll('button')); 70 + return buttons.some( 71 + (b) => 72 + b.textContent === 'Popup' || 73 + b.textContent === 'Options' || 74 + b.textContent === 'Background' || 75 + b.textContent === 'Side Panel', 76 + ); 77 + }, 78 + undefined, 79 + { timeout: 8000 }, 80 + ); 81 + 82 + // Assert at least one button is present (the waitForFunction above already 83 + // guarantees it, but the explicit expect makes the failure message clear). 84 + const buttonCount = await settings.evaluate(() => { 85 + const section = document.querySelector('#section-privacy'); 86 + if (!section) return 0; 87 + const buttons = Array.from(section.querySelectorAll('button')); 88 + return buttons.filter( 89 + (b) => 90 + b.textContent === 'Popup' || 91 + b.textContent === 'Options' || 92 + b.textContent === 'Background' || 93 + b.textContent === 'Side Panel', 94 + ).length; 95 + }); 96 + 97 + expect(buttonCount).toBeGreaterThanOrEqual(1); 98 + }); 99 + 100 + test('settings extensions buttons IPC layer returns openable entries', async () => { 101 + // Belt-and-suspenders: confirm the IPC layer itself reports ≥ 1 openable 102 + // entry so a future regression in getUiEntries() or the chrome-extensions 103 + // load pipeline is caught even before the UI layer. 104 + const openableCount = await bgWindow.evaluate(async () => { 105 + const result = await (window as any).app.chromeExtensions.getUiEntries(); 106 + if (!result.success || !result.data) return 0; 107 + return result.data.filter((e: any) => e.openable === true).length; 108 + }); 109 + 110 + expect(openableCount).toBeGreaterThanOrEqual(1); 111 + }); 112 + });