Files
claude-code-haha/desktop/electron/services/workspaceBrowserGuest.test.ts
T
程序员阿江-Relakkes 7e50c31986 fix(desktop): composite the workspace browser with the app UI (#1437)
The workspace browser was a native WebContentsView, which always paints
above the DOM. Switching to the skills or settings page left the page
floating over it, and menus over the page needed a screenshot swap that
showed up clipped and flashed white.

Pages are now <webview> guests kept in a layer inside the session panel
that is never unmounted, so other pages, menus and dialogs draw over
them like any element. The main process adopts each guest and keeps all
in-page behaviour: navigation, history, find, zoom, capture, PDF,
downloads, shortcuts and annotation.

- Guard every attach in the main window: browser partition only, start
  at about:blank, preload and sandbox pinned, reported ids validated.
- Match the native page: drop the blank history entry, keep page zoom
  independent of app zoom, allow popups so they still become tabs.
- Keep app drags working over a page and close menus on a click into it.
- Tell the side dock it is off screen when the session page is hidden.
- Remove the overlay snapshot machinery and native bounds syncing.
2026-10-04 03:24:50 +08:00

128 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it, vi } from 'vitest'
import {
WORKSPACE_BROWSER_INITIAL_SRC,
WORKSPACE_BROWSER_PARTITION,
} from '../../src/lib/workspace/browserGuestContract'
import {
applyWorkspaceBrowserAttachPolicy,
installWorkspaceBrowserGuestPolicy,
resolveWorkspaceBrowserGuest,
} from './workspaceBrowserGuest'
const PRELOAD = '/app/electron-dist/preview-preload.cjs'
const browserAttach = { partition: WORKSPACE_BROWSER_PARTITION, src: WORKSPACE_BROWSER_INITIAL_SRC }
describe('workspace browser attach policy', () => {
it('pins the sandboxed preferences and the preview preload, whatever the element asked for', () => {
// Everything a `<webview>` attribute or `webpreferences` string could
// request on its way here.
const requested: Record<string, unknown> = {
preload: '/tmp/evil.js',
preloadURL: 'file:///tmp/evil.js',
nodeIntegration: true,
nodeIntegrationInSubFrames: true,
nodeIntegrationInWorker: true,
contextIsolation: false,
sandbox: false,
webSecurity: false,
allowRunningInsecureContent: true,
experimentalFeatures: true,
enableBlinkFeatures: 'Foo',
disableBlinkFeatures: 'Bar',
additionalArguments: ['--evil'],
plugins: true,
webviewTag: true,
zoomFactor: 3,
}
expect(applyWorkspaceBrowserAttachPolicy(requested, browserAttach, { preload: PRELOAD })).toBe(true)
expect(requested).toEqual({
preload: PRELOAD,
nodeIntegration: false,
nodeIntegrationInSubFrames: false,
contextIsolation: true,
sandbox: true,
webSecurity: true,
allowRunningInsecureContent: false,
experimentalFeatures: false,
plugins: false,
webviewTag: false,
zoomFactor: 1,
})
})
it.each([
['the renderer session', { partition: undefined, src: WORKSPACE_BROWSER_INITIAL_SRC }],
['another partition', { partition: 'persist:other', src: WORKSPACE_BROWSER_INITIAL_SRC }],
['a page the renderer picked', { partition: WORKSPACE_BROWSER_PARTITION, src: 'https://example.com/' }],
['a local file', { partition: WORKSPACE_BROWSER_PARTITION, src: 'file:///etc/passwd' }],
])('denies a guest for %s', (_name, params) => {
const preferences: Record<string, unknown> = { nodeIntegration: true }
expect(applyWorkspaceBrowserAttachPolicy(preferences, params, { preload: PRELOAD })).toBe(false)
})
it('cancels a denied attach and confines every attached guest until it is adopted', () => {
const handlers = new Map<string, (...args: never[]) => void>()
installWorkspaceBrowserGuestPolicy({
on: (event, handler) => { handlers.set(event, handler) },
}, { preload: PRELOAD })
const willAttach = handlers.get('will-attach-webview') as unknown as (
event: { preventDefault(): void }, preferences: Record<string, unknown>, params: unknown,
) => void
const denied = { preventDefault: vi.fn() }
willAttach(denied, {}, { partition: 'persist:other', src: WORKSPACE_BROWSER_INITIAL_SRC })
expect(denied.preventDefault).toHaveBeenCalledTimes(1)
const allowed = { preventDefault: vi.fn() }
willAttach(allowed, {}, browserAttach)
expect(allowed.preventDefault).not.toHaveBeenCalled()
let openHandler: ((details: { url: string }) => unknown) | undefined
const navigateHandlers: Array<(event: { preventDefault(): void }, url: string) => void> = []
const didAttach = handlers.get('did-attach-webview') as unknown as (event: unknown, guest: unknown) => void
didAttach({}, {
setWindowOpenHandler: (handler: (details: { url: string }) => unknown) => { openHandler = handler },
on: (_event: string, handler: (event: { preventDefault(): void }, url: string) => void) => { navigateHandlers.push(handler) },
})
expect(openHandler?.({ url: 'https://popup.example/' })).toEqual({ action: 'deny' })
const blocked = { preventDefault: vi.fn() }
navigateHandlers[0]!(blocked, 'file:///etc/passwd')
const allowedNavigation = { preventDefault: vi.fn() }
navigateHandlers[0]!(allowedNavigation, 'https://example.com/')
expect(blocked.preventDefault).toHaveBeenCalledTimes(1)
expect(allowedNavigation.preventDefault).not.toHaveBeenCalled()
})
})
describe('workspace browser guest resolution', () => {
const host = { id: 1 }
const browserSession = { name: 'browser' }
const guest = (overrides: Partial<{ type: string, host: unknown, session: unknown, destroyed: boolean }> = {}) => ({
isDestroyed: () => overrides.destroyed ?? false,
getType: () => overrides.type ?? 'webview',
hostWebContents: 'host' in overrides ? overrides.host : host,
session: 'session' in overrides ? overrides.session : browserSession,
})
it('adopts a webview of the main window in the browser partition', () => {
const page = guest()
expect(resolveWorkspaceBrowserGuest(7, { fromId: () => page, host, session: browserSession })).toBe(page)
})
it.each([
['a malformed id', 0, guest()],
['a fractional id', 1.5, guest()],
['a string id', '7', guest()],
['a missing guest', 7, null],
['a destroyed guest', 7, guest({ destroyed: true })],
// The renderer's own webContents carries the local access token.
['the renderer itself', 7, guest({ type: 'window', host: null })],
['another window’s webview', 7, guest({ host: { id: 2 } })],
['a webview in another session', 7, guest({ session: { name: 'renderer' } })],
])('refuses %s', (_name, id, candidate) => {
expect(() => resolveWorkspaceBrowserGuest(id, {
fromId: () => candidate,
host,
session: browserSession,
})).toThrow()
})
})