hermes-agent/apps/desktop/electron/link-title-window.test.cjs
Brooklyn Nicholson 359518beac fix(desktop): guard link-title readTitle against destroyed windows
Grace and timeout timers in runRenderTitleJob can call getTitle after
finish() tears down the hidden BrowserWindow, throwing in the main
process when the Artifacts page resolves many link titles concurrently.
2026-07-03 05:13:23 -05:00

114 lines
3.5 KiB
JavaScript

const assert = require('node:assert/strict')
const test = require('node:test')
const {
createLinkTitleWindow,
guardLinkTitleSession,
linkTitleWindowOptions,
readLinkTitleWindowTitle
} = require('./link-title-window.cjs')
function makeFakeBrowserWindow() {
const calls = { audioMuted: [] }
const FakeBrowserWindow = function (options) {
this.options = options
this.webContents = {
setAudioMuted(value) {
calls.audioMuted.push(value)
}
}
}
return { FakeBrowserWindow, calls }
}
test('linkTitleWindowOptions keeps the offscreen, hardened defaults', () => {
const session = { id: 'link-titles' }
const options = linkTitleWindowOptions(session)
assert.equal(options.show, false)
assert.equal(options.webPreferences.session, session)
assert.equal(options.webPreferences.contextIsolation, true)
assert.equal(options.webPreferences.sandbox, true)
assert.equal(options.webPreferences.nodeIntegration, false)
})
test('createLinkTitleWindow mutes audio so historical links never autoplay sound', () => {
// Regression for #49505: the hidden title-fetch window loaded YouTube/watch
// URLs (to read their <title>) without muting, leaking ~2s of audio on every
// history re-render.
const { FakeBrowserWindow, calls } = makeFakeBrowserWindow()
const window = createLinkTitleWindow(FakeBrowserWindow, { id: 'link-titles' })
assert.ok(window instanceof FakeBrowserWindow)
assert.deepEqual(calls.audioMuted, [true])
})
test('createLinkTitleWindow still returns the window if muting throws', () => {
const ThrowingBrowserWindow = function (options) {
this.options = options
this.webContents = {
setAudioMuted() {
throw new Error('webContents unavailable')
}
}
}
const window = createLinkTitleWindow(ThrowingBrowserWindow, { id: 'link-titles' })
assert.ok(window instanceof ThrowingBrowserWindow)
})
test('guardLinkTitleSession cancels downloads triggered by the title-fetch window', () => {
let cancelled = false
const handlers = {}
guardLinkTitleSession({ on: (e, h) => { handlers[e] = h } })
handlers['will-download'](null, { cancel: () => { cancelled = true } })
assert.ok(cancelled)
})
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')
})