also looking at this
feat: add Linux and Windows named-window routing
#325
29 files
+2250
-200
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -37,20 +37,21 @@ interface Harness { | ||
| 37 | 37 | * they can simply be passed in as parameters — no DOM, no jsdom, and nothing |
| 38 | 38 | * shared between tests. |
| 39 | 39 | */ |
| 40 | function loadBridge(): Harness { | |
| 40 | function loadBridge(platform: 'webkit' | 'webview2' = 'webkit'): Harness { | |
| 41 | 41 | const sent: Envelope[] = [] |
| 42 | 42 | const errors: unknown[][] = [] |
| 43 | 43 | const win: any = { |
| 44 | webkit: { | |
| 45 | messageHandlers: { | |
| 46 | craft: { postMessage: (m: Envelope) => { sent.push(m) } }, | |
| 47 | }, | |
| 48 | }, | |
| 49 | 44 | addEventListener: () => {}, |
| 50 | 45 | removeEventListener: () => {}, |
| 51 | 46 | // The script fires `craft:ready` on load. |
| 52 | 47 | dispatchEvent: () => true, |
| 53 | 48 | } |
| 49 | if (platform === 'webkit') { | |
| 50 | win.webkit = { messageHandlers: { craft: { postMessage: (m: Envelope) => { sent.push(m) } } } } | |
| 51 | } | |
| 52 | else { | |
| 53 | win.chrome = { webview: { postMessage: (m: Envelope) => { sent.push(m) } } } | |
| 54 | } | |
| 54 | 55 | const doc: any = { readyState: 'complete', addEventListener: () => {} } |
| 55 | 56 | const console_: any = { ...console, error: (...a: unknown[]) => { errors.push(a) }, warn: () => {} } |
| 56 | 57 | |
| @@ -84,6 +85,24 @@ function settled(p: Promise<unknown>): Promise<unknown> { | ||
| 84 | 85 | } |
| 85 | 86 | |
| 86 | 87 | describe('bridge reply correlation', () => { |
| 88 | it('uses WebView2 when WebKit is absent and preserves the reply id', async () => { | |
| 89 | const h = loadBridge('webview2') | |
| 90 | const pending = h.craft.tags.get('/windows') | |
| 91 | expect(h.sent).toHaveLength(1) | |
| 92 | expect(h.sent[0]).toMatchObject({ t: 'tags', a: 'get' }) | |
| 93 | expect(h.sent[0].i).toBeGreaterThan(0) | |
| 94 | h.reply('get', { tags: ['native'] }, h.sent[0].i!) | |
| 95 | expect(await pending).toEqual(['native']) | |
| 96 | ||
| 97 | // A page may expose a partial `webkit` object without Craft's handler. | |
| 98 | // It must still use the authenticated WebView2 transport. | |
| 99 | h.win.webkit = { messageHandlers: {} } | |
| 100 | const next = h.craft.tags.get('/windows-next') | |
| 101 | expect(h.sent).toHaveLength(2) | |
| 102 | h.reply('get', { tags: ['again'] }, h.sent[1].i!) | |
| 103 | expect(await next).toEqual(['again']) | |
| 104 | }) | |
| 105 | ||
| 87 | 106 | it('gives each caller its own answer when two bridges share an action name', async () => { |
| 88 | 107 | // `get` is served by both keychain and tags. Before request ids, both |
| 89 | 108 | // callers queued under the string "get" and were matched by arrival order, |
| @@ -8,8 +8,8 @@ | ||
| 8 | 8 | // 1. **Self-contained** — no imports, no transpiler, ES5-friendly |
| 9 | 9 | // (avoid arrow functions, const/let are fine in modern WebKit). |
| 10 | 10 | // 2. **Idempotent** — may run twice if the page reloads. |
| 11 | // 3. **Defensive** — `webkit.messageHandlers.craft` may not exist (e.g. | |
| 12 | // when served outside a Craft window). Don't throw at module load. | |
| 11 | // 3. **Defensive** — neither WebKit's `messageHandlers.craft` nor WebView2's | |
| 12 | // `chrome.webview` need exist (e.g. outside Craft). Don't throw at load. | |
| 13 | 13 | // |
| 14 | 14 | // The native side dispatches messages by `t` (type) and answers async ones |
| 15 | 15 | // via `window.__craftBridgeResult(action, payload, id)`, where `id` is the |
| @@ -171,7 +171,11 @@ | ||
| 171 | 171 | try { |
| 172 | 172 | const msg = { t: t, a: a, d: d || '' } |
| 173 | 173 | if (i) msg.i = i |
| 174 | window.webkit.messageHandlers.craft.postMessage(msg) | |
| 174 | const webkit = window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.craft | |
| 175 | const webview2 = window.chrome && window.chrome.webview | |
| 176 | if (webkit && typeof webkit.postMessage === 'function') webkit.postMessage(msg) | |
| 177 | else if (webview2 && typeof webview2.postMessage === 'function') webview2.postMessage(msg) | |
| 178 | else throw new Error('native bridge unavailable') | |
| 175 | 179 | return true |
| 176 | 180 | } |
| 177 | 181 | catch (e) { |
| @@ -222,6 +222,25 @@ const WEBVIEW_HOST = | ||
| 222 | 222 | \\globalThis.setTimeout = function () { return 0 }; |
| 223 | 223 | \\globalThis.clearTimeout = function () {}; |
| 224 | 224 | ; |
| 225 | ||
| 226 | test "the embedded bridge posts through WebView2 when WebKit is absent" { | |
| 227 | var fx = try Fixture.init(); | |
| 228 | defer fx.deinit(); | |
| 229 | const ctx = fx.ctx; | |
| 230 | ||
| 231 | _ = try ctx.evaluate(WEBVIEW_HOST); | |
| 232 | _ = try ctx.evaluate( | |
| 233 | \\delete window.webkit; | |
| 234 | \\window.chrome = { webview: { postMessage: function (m) { posted.push(m) } } }; | |
| 235 | ); | |
| 236 | _ = try ctx.evaluate(BRIDGE); | |
| 237 | _ = try ctx.evaluate("window.craft.window.show()"); | |
| 238 | ||
| 239 | try testing.expectEqualStrings("1", try fx.text("String(posted.length)")); | |
| 240 | try testing.expectEqualStrings("window", try fx.text("posted[0].t")); | |
| 241 | try testing.expectEqualStrings("show", try fx.text("posted[0].a")); | |
| 242 | try testing.expectEqualStrings("true", try fx.text("posted[0].i > 0")); | |
| 243 | } | |
| 225 | 244 | // The timer stubs are load-bearing rather than cosmetic. `_req` arms a |
| 226 | 245 | // 30-second reaping timeout inside its Promise executor, so without a |
| 227 | 246 | // `setTimeout` the executor throws a ReferenceError and every request-shaped |