fix(browser): restore configured zoom after page reload (#10800)

Reviewed with an independent reproduction. Rewrote the reload-zoom reassert to be per-pane instead of sharing the value zoom in/out writes, fixing Cmd/Ctrl+0 reset and cross-tab zoom leakage, with E2E coverage proven to fail on revert.
This commit is contained in:
Luiz Eduardo de Oliveira Fonseca 2026-07-27 22:42:51 -03:00 committed by GitHub
parent 872a9c3930
commit 60c7faf930
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 284 additions and 12 deletions

View File

@ -2827,8 +2827,10 @@ function BrowserPagePane({
const setBrowserDefaultZoomLevel = useAppStore((state) => state.setBrowserDefaultZoomLevel)
const normalizedBrowserDefaultZoomLevel = normalizeBrowserPageZoomLevel(browserDefaultZoomLevel)
const browserDefaultZoomPercent = browserPageZoomLevelToPercent(normalizedBrowserDefaultZoomLevel)
const browserDefaultZoomLevelRef = useRef(normalizedBrowserDefaultZoomLevel)
browserDefaultZoomLevelRef.current = normalizedBrowserDefaultZoomLevel
// Why: the level THIS pane should hold. Seeded once from the configured default ("applied to newly
// opened browser tabs") and moved only by zooming this pane, so a reload can't broadcast another
// tab's zoom through the shared setting.
const paneZoomLevelRef = useRef(normalizedBrowserDefaultZoomLevel)
const grabElementShortcut = useShortcutLabel('browser.grabElement')
const faviconUrlRef = useRef<string | null>(browserTab.faviconUrl)
const initialBrowserUrlRef = useRef(browserTab.url)
@ -3560,8 +3562,10 @@ function BrowserPagePane({
if (!isActiveRef.current) {
return
}
// Why: reset targets 100% like Chromium; the configured default is a new-tab seed, not a reset target.
const nextLevel = applyBrowserPageZoom(webviewRef.current, direction)
if (nextLevel !== null) {
paneZoomLevelRef.current = nextLevel
setBrowserDefaultZoomLevel(nextLevel)
showBrowserZoomFeedback(nextLevel)
}
@ -3659,7 +3663,6 @@ function BrowserPagePane({
container = ensuredWebview.container
const webview = ensuredWebview.webview
const needsInitialNavigation = ensuredWebview.created
let needsInitialDefaultZoom = ensuredWebview.created
if (!ensuredWebview.created) {
// pointerEvents already applied inside ensureBrowserPageWebview for the reused-webview path.
@ -3739,12 +3742,12 @@ function BrowserPagePane({
if (!queuedAnnotationViewportBridgeSync) {
syncBrowserAnnotationViewportBridge()
}
if (needsInitialDefaultZoom) {
const appliedLevel = setBrowserPageZoomLevel(webview, browserDefaultZoomLevelRef.current)
if (appliedLevel !== null) {
setBrowserZoomPercent(browserPageZoomLevelToPercent(appliedLevel))
}
needsInitialDefaultZoom = false
// Why: Chromium restores per-origin zoom on reload/navigation, so reassert THIS pane's level after
// every guest load. Uses the pane-local level, not the shared setting, so reloading one tab never
// adopts a zoom the user applied to a different tab.
const appliedLevel = setBrowserPageZoomLevel(webview, paneZoomLevelRef.current)
if (appliedLevel !== null) {
setBrowserZoomPercent(browserPageZoomLevelToPercent(appliedLevel))
}
// Why: CDP viewport overrides are scoped to the debugger session and don't survive cross-origin nav, so reapply (idempotently) on dom-ready.
const presetId = viewportPresetIdRef.current

View File

@ -106,6 +106,121 @@ describe('setBrowserPageZoomLevel', () => {
expect(setBrowserPageZoomLevel(webview, 1.26)).toBe(1.5)
expect(webview.setZoomLevel).toHaveBeenCalledWith(1.5)
})
it('restores the configured level when Chromium carries zoom across reloads', () => {
const webview = {
getZoomLevel: vi.fn(() => 0.5),
setZoomLevel: vi.fn()
}
expect(setBrowserPageZoomLevel(webview, 0)).toBe(0)
expect(setBrowserPageZoomLevel(webview, 0)).toBe(0)
expect(webview.setZoomLevel).toHaveBeenNthCalledWith(1, 0)
expect(webview.setZoomLevel).toHaveBeenNthCalledWith(2, 0)
})
})
/**
* Models BrowserPagePane's zoom wiring: each pane keeps its own level, dom-ready reasserts
* that level, and zooming also writes the shared `browserDefaultZoomLevel` setting.
*/
describe('browser pane zoom across reloads', () => {
function createPane(sharedSetting: { level: number }) {
// Chromium remembers zoom per origin and replays it on reload.
const originZoom = new Map<string, number>()
let url = 'https://a.example'
let live = 0
const webview = {
getZoomLevel: () => live,
setZoomLevel: (level: number) => {
live = level
originZoom.set(url, level)
}
}
let paneLevel = sharedSetting.level
webview.setZoomLevel(paneLevel)
return {
get level() {
return live
},
zoom(direction: 'in' | 'out' | 'reset') {
const next = applyBrowserPageZoom(webview, direction)
if (next !== null) {
paneLevel = next
sharedSetting.level = next
}
},
load(nextUrl = url) {
url = nextUrl
live = originZoom.get(url) ?? 0
setBrowserPageZoomLevel(webview, paneLevel)
}
}
}
it('reasserts the pane level after normal and hard reloads', () => {
const setting = { level: 0 }
const pane = createPane(setting)
// Chromium hands back a stale 150% for this origin on reload.
pane.load()
expect(pane.level).toBe(0)
pane.load()
expect(pane.level).toBe(0)
})
it('keeps an explicit non-default configured zoom across a reload', () => {
const setting = { level: 1.5 }
const pane = createPane(setting)
expect(pane.level).toBe(1.5)
pane.load()
expect(pane.level).toBe(1.5)
})
it('resets to 100% on reset even after zooming moved the shared setting', () => {
const setting = { level: 0 }
const pane = createPane(setting)
pane.zoom('in')
pane.zoom('in')
expect(pane.level).toBe(1)
expect(setting.level).toBe(1)
// Regression: resetting toward the shared setting would be a fixed point and never move.
pane.zoom('reset')
expect(pane.level).toBe(0)
})
it('does not adopt another tab zoom when reloading an untouched tab', () => {
const setting = { level: 0 }
const tabA = createPane(setting)
const tabB = createPane(setting)
tabB.zoom('in')
tabB.zoom('in')
expect(tabB.level).toBe(1)
expect(setting.level).toBe(1)
expect(tabA.level).toBe(0)
// Regression: reasserting the shared setting would silently zoom tab A to tab B's level.
tabA.load()
expect(tabA.level).toBe(0)
})
it('keeps a zoomed tab at its own level across reload and cross-origin navigation', () => {
const setting = { level: 0 }
const pane = createPane(setting)
pane.zoom('in')
expect(pane.level).toBe(0.5)
pane.load()
expect(pane.level).toBe(0.5)
pane.load('https://b.example')
expect(pane.level).toBe(0.5)
})
})
describe('getBrowserPageZoomIndicatorState', () => {

View File

@ -97,7 +97,7 @@ async function switchToBrowserTab(
)
}
async function startBrowserFormServer(): Promise<{
async function startBrowserFormServer(host = '127.0.0.1'): Promise<{
url: (label: string) => string
close: () => Promise<void>
}> {
@ -113,10 +113,10 @@ async function startBrowserFormServer(): Promise<{
</html>
`)
})
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
await new Promise<void>((resolve) => server.listen(0, host, resolve))
const port = (server.address() as AddressInfo).port
return {
url: (label: string) => `http://127.0.0.1:${port}/${encodeURIComponent(label)}`,
url: (label: string) => `http://${host}:${port}/${encodeURIComponent(label)}`,
close: () => closeServer(server)
}
}
@ -461,6 +461,160 @@ test.describe('Browser Tab', () => {
}
})
test('browser page reload restores the configured 100% zoom', async ({ orcaPage }) => {
const formServer = await startBrowserFormServer()
try {
const worktreeId = (await getActiveWorktreeId(orcaPage))!
const browserTab = await createBrowserTab(
orcaPage,
worktreeId,
formServer.url('Zoom reload'),
'Zoom Reload'
)
expect(browserTab?.id).toBeTruthy()
await expect
.poll(async () => readBrowserInputValue(orcaPage, browserTab!.id), { timeout: 5_000 })
.not.toBeNull()
const zoomLevels = await orcaPage.evaluate(async (browserTabId) => {
const slot = document.querySelector(`[data-browser-overlay-tab-id="${browserTabId}"]`)
const webview = slot?.querySelector('webview') as Electron.WebviewTag | null
if (!webview) {
throw new Error(`Missing webview for browser tab ${browserTabId}`)
}
const levels = [webview.getZoomLevel()]
webview.setZoomLevel(0.5)
for (let reload = 0; reload < 3; reload += 1) {
await new Promise<void>((resolve) => {
webview.addEventListener('dom-ready', () => resolve(), { once: true })
if (reload === 1) {
webview.reloadIgnoringCache()
} else {
webview.reload()
}
})
levels.push(webview.getZoomLevel())
}
return levels
}, browserTab!.id)
expect(zoomLevels).toEqual([0, 0, 0, 0])
} finally {
await formServer.close()
}
})
test('Cmd/Ctrl+0 resets a zoomed browser page to 100%', async ({ orcaPage }) => {
const formServer = await startBrowserFormServer()
try {
const worktreeId = (await getActiveWorktreeId(orcaPage))!
const browserTab = await createBrowserTab(
orcaPage,
worktreeId,
formServer.url('Zoom reset'),
'Zoom Reset'
)
expect(browserTab?.id).toBeTruthy()
await expect
.poll(async () => readBrowserInputValue(orcaPage, browserTab!.id), { timeout: 5_000 })
.not.toBeNull()
await orcaPage.evaluate(
async ({ browserTabId, browserPageId, modifier }) => {
const slot = document.querySelector(`[data-browser-overlay-tab-id="${browserTabId}"]`)
const webview = slot?.querySelector('webview') as Electron.WebviewTag | null
if (!webview) {
throw new Error(`Missing webview for browser tab ${browserTabId}`)
}
window.dispatchEvent(
new CustomEvent('orca:browser-page-zoom', {
detail: { browserPageId, direction: 'in' }
})
)
await webview.sendInputEvent({ type: 'keyDown', keyCode: '0', modifiers: [modifier] })
await webview.sendInputEvent({ type: 'keyUp', keyCode: '0', modifiers: [modifier] })
},
{
browserTabId: browserTab!.id,
browserPageId: browserTab!.pageId ?? browserTab!.id,
modifier: process.platform === 'darwin' ? 'meta' : 'control'
}
)
await expect
.poll(() =>
orcaPage.evaluate((browserTabId) => {
const slot = document.querySelector(`[data-browser-overlay-tab-id="${browserTabId}"]`)
return (slot?.querySelector('webview') as Electron.WebviewTag | null)?.getZoomLevel()
}, browserTab!.id)
)
.toBe(0)
} finally {
await formServer.close()
}
})
test('reloading one browser tab does not adopt another tab zoom', async ({ orcaPage }) => {
const [formServerA, formServerB] = await Promise.all([
startBrowserFormServer(),
startBrowserFormServer('localhost')
])
try {
const worktreeId = (await getActiveWorktreeId(orcaPage))!
const tabA = await createBrowserTab(orcaPage, worktreeId, formServerA.url('Zoom A'), 'Zoom A')
const tabB = await createBrowserTab(orcaPage, worktreeId, formServerB.url('Zoom B'), 'Zoom B')
expect(tabA?.id).toBeTruthy()
expect(tabB?.id).toBeTruthy()
for (const tab of [tabA, tabB]) {
await expect
.poll(async () => readBrowserInputValue(orcaPage, tab!.id), { timeout: 5_000 })
.not.toBeNull()
}
const levels = await orcaPage.evaluate(
async ({ tabAId, tabBId, pageBId }) => {
const webviewFor = (id: string): Electron.WebviewTag => {
const slot = document.querySelector(`[data-browser-overlay-tab-id="${id}"]`)
const webview = slot?.querySelector('webview') as Electron.WebviewTag | null
if (!webview) {
throw new Error(`Missing webview for browser tab ${id}`)
}
return webview
}
const webviewA = webviewFor(tabAId)
const webviewB = webviewFor(tabBId)
// Zoom only tab B through the real renderer zoom path (also writes the shared setting).
for (let step = 0; step < 2; step += 1) {
window.dispatchEvent(
new CustomEvent('orca:browser-page-zoom', {
detail: { browserPageId: pageBId, direction: 'in' }
})
)
await new Promise((resolve) => setTimeout(resolve, 100))
}
const zoomedB = webviewB.getZoomLevel()
const untouchedA = webviewA.getZoomLevel()
await new Promise<void>((resolve) => {
webviewA.addEventListener('dom-ready', () => resolve(), { once: true })
webviewA.reload()
})
return { zoomedB, untouchedA, reloadedA: webviewA.getZoomLevel() }
},
{ tabAId: tabA!.id, tabBId: tabB!.id, pageBId: tabB!.pageId ?? tabB!.id }
)
expect(levels.zoomedB).toBeGreaterThan(0)
expect(levels.untouchedA).toBe(0)
// Regression: reasserting the shared default would drag tab A to tab B's zoom.
expect(levels.reloadedA).toBe(0)
} finally {
await Promise.all([formServerA.close(), formServerB.close()])
}
})
test('plain links stay current while explicit new-tab gestures activate Orca tabs', async ({
electronApp,
orcaPage