experiments in a post-browser web
10
fork

Configure Feed

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

test(slides): cover edge-anchor placement + key=address:edge identity

Extends tests/desktop/slides.spec.ts with the slides-specific contracts
that the existing single test (data setup verification) didn't touch:

- Up / Down / Left / Right screenEdge values produce bounds anchored to
the corresponding work-area edge with the perpendicular axis centered
(matching anchorToEdge in window-placement.ts). Reads the cursor
display's workArea via electron.screen and asserts each axis exactly.
- Re-invoking with the same `${address}:${screenEdge}` key reuses the
existing window (returned id matches the first open).
- Same address with a different edge produces a distinct window
(returned ids differ).

Generic close-on-blur is already covered by transient-on-blur.spec.ts
(slides have role: 'quick-view', in TRANSIENT_ROLES) so it isn't
duplicated here.

Fixture: corrected the evaluateMain type to reflect what Playwright
actually passes — the first arg of the callback is the electron module
itself, not a `{ app, require }` wrapper. All existing callers were
already destructuring fields from the electron module
(`{ BrowserWindow }`, `{ webContents }`, `{ app }`, etc.) so this is a
type-correctness fix, no runtime change.

+140 -2
+134
tests/desktop/slides.spec.ts
··· 2 2 import { Page } from '@playwright/test'; 3 3 import { createPerDescribeApp } from '../helpers/test-app'; 4 4 5 + // Slides feature contract — tests for the surface area that's not covered 6 + // by the generic transient-on-blur autoclose: 7 + // 8 + // 1. screenEdge: 'Up'|'Down'|'Left'|'Right' anchors the window to the 9 + // corresponding work-area edge, perpendicular-axis centered. Main 10 + // process owns placement (renderer no longer computes x/y); test 11 + // drives the window-open pipeline directly with the same params 12 + // slides emit. 13 + // 14 + // 2. The slides identity contract is `key = ${address}:${screenEdge}`. 15 + // Re-invoking with the same key reuses the existing window (same 16 + // BrowserWindow id); changing the edge produces a different window. 17 + // 18 + // Close-on-blur is covered generically by transient-on-blur.spec.ts — 19 + // slides have role: 'quick-view', which is in TRANSIENT_ROLES, so the 20 + // general autoclose loop fires when any non-utility window focuses next. 21 + 5 22 test.describe('Slides @desktop', () => { 6 23 let app: DesktopApp; 7 24 let bgWindow: Page; ··· 45 62 }); 46 63 expect(queryResult.success).toBe(true); 47 64 expect(queryResult.data.length).toBeGreaterThanOrEqual(3); 65 + }); 66 + 67 + // ── Edge-anchor placement ────────────────────────────────────────────── 68 + 69 + /** 70 + * Open a slide-style window mimicking what features/slides/background.js 71 + * passes to api.window.open: role 'quick-view', modal panel, key-identity, 72 + * with the given screenEdge. 73 + */ 74 + async function openSlide(screenEdge: string, address: string): Promise<number> { 75 + const result = await bgWindow.evaluate(async (args: { edge: string; addr: string }) => { 76 + return await (window as any).app.window.open('about:blank', { 77 + role: 'quick-view', 78 + address: args.addr, 79 + modal: true, 80 + type: 'panel', 81 + width: 600, 82 + height: 400, 83 + key: `${args.addr}:${args.edge}`, 84 + screenEdge: args.edge, 85 + }); 86 + }, { edge: screenEdge, addr: address }); 87 + if (!result.success) { 88 + throw new Error(`slide open failed for edge=${screenEdge}: ${JSON.stringify(result)}`); 89 + } 90 + return result.id as number; 91 + } 92 + 93 + /** Read a window's current bounds via the main process. */ 94 + async function getBounds(windowId: number): Promise<{ x: number; y: number; width: number; height: number } | null> { 95 + if (!app.evaluateMain) throw new Error('evaluateMain not available'); 96 + return await app.evaluateMain<{ x: number; y: number; width: number; height: number } | null, number>( 97 + ({ BrowserWindow }, wid) => { 98 + const w = BrowserWindow.fromId(wid); 99 + return w && !w.isDestroyed() ? w.getBounds() : null; 100 + }, 101 + windowId, 102 + ); 103 + } 104 + 105 + /** Read the work area of the display the cursor is on (matches main's pickCursorOrPrimaryDisplay). */ 106 + async function getCursorWorkArea(): Promise<{ x: number; y: number; width: number; height: number }> { 107 + if (!app.evaluateMain) throw new Error('evaluateMain not available'); 108 + return await app.evaluateMain<{ x: number; y: number; width: number; height: number }>( 109 + ({ screen }) => { 110 + const cursor = screen.getCursorScreenPoint(); 111 + const display = screen.getDisplayNearestPoint(cursor); 112 + return display.workArea; 113 + }, 114 + ); 115 + } 116 + 117 + async function closeWindow(windowId: number): Promise<void> { 118 + try { 119 + await bgWindow.evaluate(async (wid: number) => { 120 + await (window as any).app.window.close(wid); 121 + }, windowId); 122 + } catch { /* ignore */ } 123 + } 124 + 125 + test('Up screenEdge anchors to top, X-centered on cursor display', async () => { 126 + const id = await openSlide('Up', 'https://slide-up.example.com'); 127 + const bounds = await getBounds(id); 128 + const work = await getCursorWorkArea(); 129 + expect(bounds).not.toBeNull(); 130 + expect(bounds!.y).toBe(work.y); 131 + expect(bounds!.x).toBe(work.x + Math.round((work.width - bounds!.width) / 2)); 132 + await closeWindow(id); 133 + }); 134 + 135 + test('Down screenEdge anchors to bottom, X-centered on cursor display', async () => { 136 + const id = await openSlide('Down', 'https://slide-down.example.com'); 137 + const bounds = await getBounds(id); 138 + const work = await getCursorWorkArea(); 139 + expect(bounds).not.toBeNull(); 140 + expect(bounds!.y).toBe(work.y + work.height - bounds!.height); 141 + expect(bounds!.x).toBe(work.x + Math.round((work.width - bounds!.width) / 2)); 142 + await closeWindow(id); 143 + }); 144 + 145 + test('Left screenEdge anchors to left, Y-centered on cursor display', async () => { 146 + const id = await openSlide('Left', 'https://slide-left.example.com'); 147 + const bounds = await getBounds(id); 148 + const work = await getCursorWorkArea(); 149 + expect(bounds).not.toBeNull(); 150 + expect(bounds!.x).toBe(work.x); 151 + expect(bounds!.y).toBe(work.y + Math.round((work.height - bounds!.height) / 2)); 152 + await closeWindow(id); 153 + }); 154 + 155 + test('Right screenEdge anchors to right, Y-centered on cursor display', async () => { 156 + const id = await openSlide('Right', 'https://slide-right.example.com'); 157 + const bounds = await getBounds(id); 158 + const work = await getCursorWorkArea(); 159 + expect(bounds).not.toBeNull(); 160 + expect(bounds!.x).toBe(work.x + work.width - bounds!.width); 161 + expect(bounds!.y).toBe(work.y + Math.round((work.height - bounds!.height) / 2)); 162 + await closeWindow(id); 163 + }); 164 + 165 + // ── key=address:edge identity contract ──────────────────────────────── 166 + 167 + test('reopening with the same address+edge reuses the same window', async () => { 168 + const addr = 'https://slide-identity.example.com'; 169 + const first = await openSlide('Up', addr); 170 + const second = await openSlide('Up', addr); 171 + expect(second).toBe(first); 172 + await closeWindow(first); 173 + }); 174 + 175 + test('opening the same address with a different edge produces a distinct window', async () => { 176 + const addr = 'https://slide-distinct.example.com'; 177 + const up = await openSlide('Up', addr); 178 + const down = await openSlide('Down', addr); 179 + expect(down).not.toBe(up); 180 + await closeWindow(up); 181 + await closeWindow(down); 48 182 }); 49 183 });
+6 -2
tests/fixtures/desktop-app.ts
··· 50 50 /** Get extension background windows */ 51 51 getExtensionWindows(): Page[]; 52 52 53 - /** Evaluate code in the main (Node) process (Electron only) */ 53 + /** 54 + * Evaluate code in the main (Node) process (Electron only). 55 + * First arg of `fn` is the electron module itself — destructure to grab 56 + * `BrowserWindow`, `screen`, `app`, etc. 57 + */ 54 58 evaluateMain?<R, A = void>( 55 - fn: (ctx: { app: Electron.App; require: NodeRequire }, arg: A) => R, 59 + fn: (electron: typeof import('electron'), arg: A) => R, 56 60 arg?: A, 57 61 ): Promise<R>; 58 62