// 无头测试用的 electron 桩: 只提供 src/main 里真正用到的 API // (app.getPath / app.getVersion / BrowserWindow / ipcMain / shell ...) const os = require('node:os') const path = require('node:path') const fs = require('node:fs') const root = process.env.LNM_SELFTEST_HOME || path.join(os.tmpdir(), 'lnm-selftest') fs.mkdirSync(root, { recursive: true }) function ensure(p) { fs.mkdirSync(p, { recursive: true }) return p } // ---- nativeImage / clipboard 桩 ---- // // 真的 Electron 用 Chromium 的图片解码器。桩里只按**魔数**认 PNG/JPEG (别的格式 // 一律给空图, 和真实现一样), 尺寸和编码后体积按**源文件大小**等比给 —— 目的不是 // 还原真图, 而是让自测能确定性地覆盖 "选 PNG 还是 JPEG / 降级重缩 / 复制" 这几条分支。 function emptyImage() { return { isEmpty: () => true, getSize: () => ({ width: 0, height: 0 }), resize: () => emptyImage(), toPNG: () => Buffer.alloc(0), toJPEG: () => Buffer.alloc(0), toDataURL: () => '', } } function fakeImage(opts) { const { width, height, pngSize, jpegSize } = opts return { isEmpty: () => false, getSize: () => ({ width, height }), resize: (o) => fakeImage({ width: o.width || width, height: Math.round(height * ((o.width || width) / width)), pngSize, jpegSize, }), toPNG: () => Buffer.alloc(pngSize, 0x89), toJPEG: (q) => Buffer.alloc(Math.round(jpegSize * ((q == null ? 80 : q) / 80)), 0xff), toDataURL: () => 'data:image/png;base64,' + Buffer.alloc(32, 1).toString('base64'), } } function sniffImage(p) { try { const fd = fs.openSync(p, 'r') const buf = Buffer.alloc(8) fs.readSync(fd, buf, 0, 8, 0) fs.closeSync(fd) if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) return 'png' if (buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) return 'jpeg' return null } catch { return null } } function fakeImageForPath(p) { const size = fs.statSync(p).size return fakeImage({ width: 1200, height: 900, // PNG 通常比源文件还大 (照片尤其), JPEG 大约三成 —— 够触发不同分支 pngSize: Math.round(size * 1.5), jpegSize: Math.round(size * 0.3), }) } const clipboardState = { image: null, text: '' } module.exports = { app: { isPackaged: false, getPath(name) { if (name === 'userData') return ensure(path.join(root, 'userData')) if (name === 'documents') return ensure(path.join(root, 'documents')) if (name === 'downloads') return ensure(path.join(root, 'downloads')) if (name === 'home') return os.homedir() return ensure(path.join(root, name)) }, getVersion: () => '0.1.0', getName: () => 'local-net-msg', getAppPath: () => process.cwd(), on: () => {}, once: () => {}, whenReady: async () => {}, quit: () => {}, }, BrowserWindow: class BrowserWindow { static getAllWindows() { return [] } static getFocusedWindow() { return null } isMinimized() { return false } restore() {} show() {} focus() {} webContents = { send: () => {} } }, // 注意: 故意不提供 ipcRenderer —— electron-store 见到它会以为是渲染进程 ipcMain: { on: () => {}, once: () => {}, handle: () => {}, removeHandler: () => {} }, contextBridge: { exposeInMainWorld: () => {} }, dialog: { showErrorBox: () => {}, showOpenDialog: async () => ({ canceled: true, filePaths: [] }) }, shell: { openPath: async () => '', showItemInFolder: () => {} }, Notification: class { static isSupported() { return false } constructor() {} show() {} }, nativeImage: { createFromPath(p) { try { if (!fs.existsSync(p)) return emptyImage() if (!sniffImage(p)) return emptyImage() return fakeImageForPath(p) } catch { return emptyImage() } }, createFromDataUrl(u) { // 只认 data:image/png;base64 和 data:image/jpeg;base64 const m = /^data:image\/(png|jpeg);base64,/.exec(String(u || '')) if (!m) return emptyImage() return fakeImage({ width: 1200, height: 900, pngSize: 40_000, jpegSize: 12_000 }) }, createFromBuffer(b) { if (!b || b.length < 8) return emptyImage() const kind = b[0] === 0x89 && b[1] === 0x50 ? 'png' : (b[0] === 0xff && b[1] === 0xd8 ? 'jpeg' : null) if (!kind) return emptyImage() return fakeImage({ width: 1200, height: 900, pngSize: Math.round(b.length * 1.5), jpegSize: Math.round(b.length * 0.3), }) }, }, clipboard: { writeImage(img) { clipboardState.image = img }, readImage() { return clipboardState.image }, writeText(t) { clipboardState.text = String(t) }, readText() { return clipboardState.text }, }, // 只给自测用: 看剪贴板里到底写进去了什么 __clipboardState: clipboardState, }