diff --git a/clis/douban/utils.js b/clis/douban/utils.js index 058fcbbe6..c317c5a33 100644 --- a/clis/douban/utils.js +++ b/clis/douban/utils.js @@ -67,7 +67,8 @@ function extractDoubanPublishYear(value) { const match = normalizeText(value).match(/\b(19|20)\d{2}\b/); return match?.[0] || ''; } -function splitDoubanTitle(fullTitle) { +export function splitDoubanTitle(fullTitle) { + const normalizeText = (value) => String(value || '').replace(/\s+/g, ' ').trim(); const normalized = normalizeText(fullTitle); if (!normalized) return { title: '', originalTitle: '' }; diff --git a/clis/douban/utils.test.js b/clis/douban/utils.test.js index 48722157f..93f99817e 100644 --- a/clis/douban/utils.test.js +++ b/clis/douban/utils.test.js @@ -11,6 +11,7 @@ import { promoteDoubanPhotoUrl, resolveDoubanPhotoAssetUrl, searchDouban, + splitDoubanTitle, } from './utils.js'; function createFakeNode(text = '', attrs = {}) { @@ -436,3 +437,52 @@ describe('inferDoubanSearchResultType', () => { })).toBe('book'); }); }); + +// Regression for #1851: splitDoubanTitle must be self-contained so its +// .toString()-injected form runs inside page.evaluate, where the module scope +// (and therefore the module-level normalizeText) is unavailable. Pre-fix the +// injected copy threw `ReferenceError: normalizeText is not defined`. +describe('splitDoubanTitle — page.evaluate injection (#1851)', () => { + // Rebuild the function from its source in a fresh scope that, like the + // browser page context, has no access to the module-level normalizeText. + function injectViaToString(fn) { + // eslint-disable-next-line no-new-func + return new Function(`return (${fn.toString()})`)(); + } + + it('does not throw when injected into page.evaluate (normalizeText is not defined there)', () => { + const injected = injectViaToString(splitDoubanTitle); + expect(() => injected('盗梦空间 Inception')).not.toThrow(); + }); + + it('splits a CJK + Latin title correctly when injected', () => { + const injected = injectViaToString(splitDoubanTitle); + expect(injected('盗梦空间 Inception')).toEqual({ + title: '盗梦空间', + originalTitle: 'Inception', + }); + }); + + it('returns the same result whether called directly or via injection', () => { + const injected = injectViaToString(splitDoubanTitle); + const samples = [ + '盗梦空间 Inception', + '千与千寻 千と千尋の神隠し', + "哈尔的移动城堡 Howl's Moving Castle", + '无名之辈', + '', + ' ', + ]; + for (const input of samples) { + expect(injected(input)).toEqual(splitDoubanTitle(input)); + } + }); + + it('collapses internal whitespace in the injected copy', () => { + const injected = injectViaToString(splitDoubanTitle); + expect(injected(' 流浪地球 The Wandering Earth ')).toEqual({ + title: '流浪地球', + originalTitle: 'The Wandering Earth', + }); + }); +});