const test = require('node:test'); const assert = require('node:assert'); const EventEmitter = require('node:events'); const Module = require('node:module'); test('阅读器全局只创建一个窗口,新书与删除请求路由到标签事件', () => { const instances = []; let webContentsId = 0; class FakeWindow extends EventEmitter { constructor(options) { super(); this.options = options; this.destroyed = false; this.focused = 0; this.webContents = new EventEmitter(); this.webContents.id = ++webContentsId; this.webContents.loading = true; this.webContents.sent = []; this.webContents.setWindowOpenHandler = (handler) => { this.webContents.windowOpenHandler = handler; }; this.webContents.isLoadingMainFrame = () => this.webContents.loading; this.webContents.send = (channel, payload) => this.webContents.sent.push([channel, payload]); instances.push(this); } loadFile(file, options) { this.loaded = { file, options }; } isDestroyed() { return this.destroyed; } isMinimized() { return false; } focus() { this.focused += 1; } close() { const event = { prevented: false, preventDefault() { this.prevented = true; } }; this.emit('close', event); if (!event.prevented) { this.destroyed = true; this.emit('closed'); } } destroy() { this.destroyed = true; this.emit('closed'); } } const originalLoad = Module._load; Module._load = function mock(request, parent, isMain) { if (request === 'electron') return { BrowserWindow: FakeWindow }; return originalLoad.call(this, request, parent, isMain); }; const modulePath = require.resolve('../reader/window.js'); delete require.cache[modulePath]; let windows; try { windows = require(modulePath); } finally { Module._load = originalLoad; } const firstLocator = { kind: 'pdf', page: 4 }; const secondLocator = { kind: 'epub', chapter: 2, offset: 180 }; const first = windows.open('book-a', 'C:\\app', 0, firstLocator); const second = windows.open('book-b', 'C:\\app', 1, secondLocator); assert.strictEqual(first, second); assert.strictEqual(instances.length, 1); assert.deepStrictEqual(first.loaded.options.query, { entryId: 'book-a', fileIndex: '0', locator: JSON.stringify(firstLocator) }); assert.deepStrictEqual(first.webContents.sent, [], '加载完成前不应丢失事件或过早发送'); assert.deepStrictEqual(first.webContents.windowOpenHandler(), { action: 'deny' }); const navigation = { prevented: false, preventDefault() { this.prevented = true; } }; first.webContents.emit('will-navigate', navigation, 'https://untrusted.example/'); assert.strictEqual(navigation.prevented, true); first.webContents.loading = false; assert.strictEqual(windows.markReady(first.webContents), true); assert.strictEqual(windows.isReady(first), true); assert.deepStrictEqual(first.webContents.sent[0], [ 'reader:openEntry', { entryId: 'book-b', fileIndex: 1, locator: secondLocator } ]); windows.closeFor('book-a'); assert.deepStrictEqual(first.webContents.sent[1], ['reader:closeEntry', 'book-a']); windows.purgeFor('book-b', 'purge-1'); assert.deepStrictEqual(first.webContents.sent[2], [ 'reader:purgeEntry', { entryId: 'book-b', requestId: 'purge-1' } ]); assert.strictEqual(first.destroyed, false, '删除一个条目不应关闭整个阅读器窗口'); assert.strictEqual(windows.fromWebContents(first.webContents), 'reader'); assert.deepStrictEqual(windows.all(), [first]); first.close(); assert.strictEqual(first.destroyed, false, '关闭前应等待渲染器排空保存队列'); assert.deepStrictEqual(first.webContents.sent[3], ['reader:prepareClose', null]); assert.strictEqual(windows.shutdownReady(first.webContents), true); assert.strictEqual(first.destroyed, true); });