diff --git a/apps/desktop/electron/link-title-window.cjs b/apps/desktop/electron/link-title-window.cjs index 3aeabcfe6..c6792bf98 100644 --- a/apps/desktop/electron/link-title-window.cjs +++ b/apps/desktop/electron/link-title-window.cjs @@ -49,4 +49,23 @@ function guardLinkTitleSession(partitionSession) { } } -module.exports = { createLinkTitleWindow, guardLinkTitleSession, linkTitleWindowOptions } +// Read the page title from a title-fetch window. Callers schedule this from +// timers that can fire after finish() destroys the window, so every access must +// guard isDestroyed and swallow Electron's "Object has been destroyed" throws. +function readLinkTitleWindowTitle(window) { + try { + if (!window || window.isDestroyed()) return '' + const contents = window.webContents + if (!contents || contents.isDestroyed()) return '' + return contents.getTitle() || '' + } catch { + return '' + } +} + +module.exports = { + createLinkTitleWindow, + guardLinkTitleSession, + linkTitleWindowOptions, + readLinkTitleWindowTitle +} diff --git a/apps/desktop/electron/link-title-window.test.cjs b/apps/desktop/electron/link-title-window.test.cjs index 64228ce4a..1682e5abb 100644 --- a/apps/desktop/electron/link-title-window.test.cjs +++ b/apps/desktop/electron/link-title-window.test.cjs @@ -1,7 +1,12 @@ const assert = require('node:assert/strict') const test = require('node:test') -const { createLinkTitleWindow, guardLinkTitleSession, linkTitleWindowOptions } = require('./link-title-window.cjs') +const { + createLinkTitleWindow, + guardLinkTitleSession, + linkTitleWindowOptions, + readLinkTitleWindowTitle +} = require('./link-title-window.cjs') function makeFakeBrowserWindow() { const calls = { audioMuted: [] } @@ -66,3 +71,44 @@ test('guardLinkTitleSession cancels downloads triggered by the title-fetch windo test('guardLinkTitleSession is a no-op when session.on throws', () => { assert.doesNotThrow(() => guardLinkTitleSession({ on() { throw new Error() } })) }) + +test('readLinkTitleWindowTitle returns empty for missing or destroyed windows', () => { + assert.equal(readLinkTitleWindowTitle(null), '') + assert.equal(readLinkTitleWindowTitle(undefined), '') + assert.equal(readLinkTitleWindowTitle({ isDestroyed: () => true }), '') +}) + +test('readLinkTitleWindowTitle returns empty when webContents is destroyed', () => { + const window = { + isDestroyed: () => false, + webContents: { isDestroyed: () => true, getTitle: () => 'Should Not Read' } + } + + assert.equal(readLinkTitleWindowTitle(window), '') +}) + +test('readLinkTitleWindowTitle swallows getTitle throws after teardown', () => { + const window = { + isDestroyed: () => false, + webContents: { + isDestroyed: () => false, + getTitle: () => { + throw new Error('Object has been destroyed') + } + } + } + + assert.equal(readLinkTitleWindowTitle(window), '') +}) + +test('readLinkTitleWindowTitle returns trimmed page title', () => { + const window = { + isDestroyed: () => false, + webContents: { + isDestroyed: () => false, + getTitle: () => 'Example Domain' + } + } + + assert.equal(readLinkTitleWindowTitle(window), 'Example Domain') +}) diff --git a/apps/desktop/electron/main.cjs b/apps/desktop/electron/main.cjs index 29b2891d7..da642b33a 100644 --- a/apps/desktop/electron/main.cjs +++ b/apps/desktop/electron/main.cjs @@ -36,7 +36,11 @@ const { SESSION_WINDOW_MIN_WIDTH } = require('./session-windows.cjs') const { canImportHermesCli, verifyHermesCli } = require('./backend-probes.cjs') -const { createLinkTitleWindow, guardLinkTitleSession } = require('./link-title-window.cjs') +const { + createLinkTitleWindow, + guardLinkTitleSession, + readLinkTitleWindowTitle +} = require('./link-title-window.cjs') const { probeGatewayWebSocket } = require('./gateway-ws-probe.cjs') const { adoptServedDashboardToken } = require('./dashboard-token.cjs') const { waitForDashboardPortAnnouncement } = require('./backend-ready.cjs') @@ -3557,13 +3561,13 @@ function runRenderTitleJob(rawUrl) { return finish('') } - const readTitle = () => window?.webContents?.getTitle?.() || '' + const finishWithTitle = () => finish(readLinkTitleWindowTitle(window)) const scheduleGrace = () => { if (graceTimer) clearTimeout(graceTimer) - graceTimer = setTimeout(() => finish(readTitle()), RENDER_TITLE_GRACE_MS) + graceTimer = setTimeout(finishWithTitle, RENDER_TITLE_GRACE_MS) } - hardTimer = setTimeout(() => finish(readTitle()), RENDER_TITLE_TIMEOUT_MS) + hardTimer = setTimeout(finishWithTitle, RENDER_TITLE_TIMEOUT_MS) window.webContents.setUserAgent(TITLE_USER_AGENT) window.webContents.on('page-title-updated', scheduleGrace)