From 018e5f4a9136b9477d211e8012e215052460f743 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 23 May 2026 10:23:01 +0800 Subject: [PATCH] fix(wechat): wait for the body editor before pasting --- package.json | 5 +- packages/core/src/platforms/wechat.js | 71 +++++++++++++-------- tests/wechat.test.mjs | 90 +++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 28 deletions(-) create mode 100644 tests/wechat.test.mjs diff --git a/package.json b/package.json index a2db27f..56eec0c 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,10 @@ "dev": "pnpm -C apps/extension dev", "dev:chrome": "pnpm -C apps/extension dev:chrome", "dev:watch": "pnpm -C apps/extension dev:watch", - "lint": "pnpm -r lint" + "lint": "pnpm -r lint", + "test": "node --test" }, "devDependencies": { "typescript": "^5.0.0" } -} \ No newline at end of file +} diff --git a/packages/core/src/platforms/wechat.js b/packages/core/src/platforms/wechat.js index 375b91f..e3a9ffb 100644 --- a/packages/core/src/platforms/wechat.js +++ b/packages/core/src/platforms/wechat.js @@ -12,43 +12,58 @@ const WechatPlatform = { type: 'wechat', } +function getEditorArea(editor) { + return (editor?.clientHeight || 0) * (editor?.clientWidth || 0) +} + +function isWechatTitleEditor(editor, titleEditor) { + return Boolean(editor) + && (editor === titleEditor || Boolean(editor.closest?.('.title-editor__input'))) +} + +function pickWechatBodyProseMirrorCandidate(nodes, { titleInput, titleEditor } = {}) { + const bodyCandidates = nodes.filter(editor => !isWechatTitleEditor(editor, titleEditor)) + if (bodyCandidates.length === 0) + return null + if (bodyCandidates.length === 1) + return bodyCandidates[0] + + const byPlaceholder = bodyCandidates.find(editor => + (editor.textContent || '').includes('从这里开始写正文'), + ) + if (byPlaceholder) + return byPlaceholder + + if (titleInput) { + const band = titleInput.getBoundingClientRect() + const belowTitle = bodyCandidates.filter((editor) => { + const rect = editor.getBoundingClientRect() + return rect.top >= band.bottom - 8 + }) + if (belowTitle.length > 0) { + return belowTitle.sort((a, b) => getEditorArea(b) - getEditorArea(a))[0] + } + } + + return bodyCandidates.sort((a, b) => getEditorArea(b) - getEditorArea(a))[0] +} + // 微信公众号内容填充函数(在页面主世界中执行) // 注意:需要先调用 injectUtils 注入 window.waitFor async function fillWechatContent(title, htmlBody) { /** * 后台改版后可能存在多个 `.ProseMirror`(标题区也可能是 ProseMirror), * `querySelector('.ProseMirror')` 常会命中标题编辑器,导致正文 HTML 被贴进标题。 + * 另外,正文编辑器有时会比标题编辑器晚挂载,这时也要继续等待,不能把唯一节点误判成正文。 */ function pickWechatBodyProseMirror() { const nodes = [...document.querySelectorAll('.ProseMirror')] if (nodes.length === 0) return null - if (nodes.length === 1) - return nodes[0] - - const byPlaceholder = nodes.find(el => - (el.textContent || '').includes('从这里开始写正文'), - ) - if (byPlaceholder) - return byPlaceholder const titleInput = document.querySelector('#title') - if (titleInput) { - const band = titleInput.getBoundingClientRect() - const belowTitle = nodes.filter((el) => { - const r = el.getBoundingClientRect() - return r.top >= band.bottom - 8 - }) - if (belowTitle.length > 0) { - return belowTitle.sort( - (a, b) => (b.clientHeight * b.clientWidth) - (a.clientHeight * a.clientWidth), - )[0] - } - } - - return nodes.sort( - (a, b) => (b.clientHeight * b.clientWidth) - (a.clientHeight * a.clientWidth), - )[0] + const titleEditor = document.querySelector('.title-editor__input .ProseMirror') + return pickWechatBodyProseMirrorCandidate(nodes, { titleInput, titleEditor }) } async function waitForBodyEditor(timeout = 15000) { @@ -351,5 +366,9 @@ async function syncWechatContent(tab, content, helpers) { } // 导出 -export { WechatPlatform, fillWechatContent, syncWechatContent } - +export { + WechatPlatform, + fillWechatContent, + pickWechatBodyProseMirrorCandidate, + syncWechatContent, +} diff --git a/tests/wechat.test.mjs b/tests/wechat.test.mjs new file mode 100644 index 0000000..99b1f38 --- /dev/null +++ b/tests/wechat.test.mjs @@ -0,0 +1,90 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { pickWechatBodyProseMirrorCandidate } from '../packages/core/src/platforms/wechat.js' + +function createEditor({ + textContent = '', + top = 0, + height = 120, + width = 640, + classes = [], +} = {}) { + const classSet = new Set(classes) + const rect = { + top, + bottom: top + height, + left: 0, + right: width, + width, + height, + } + + return { + textContent, + clientHeight: height, + clientWidth: width, + getBoundingClientRect: () => rect, + closest: (selector) => { + const selectors = selector.split(',').map(item => item.trim().replace(/^\./, '')) + return selectors.some(item => classSet.has(item)) ? { selector } : null + }, + } +} + +function createTitleInput({ top = 0, height = 48 } = {}) { + return { + getBoundingClientRect: () => ({ + top, + bottom: top + height, + left: 0, + right: 640, + width: 640, + height, + }), + } +} + +test('只有标题编辑器时返回 null,避免把正文贴到标题', () => { + const titleInput = createTitleInput() + const titleEditor = createEditor({ + top: 8, + height: 40, + width: 640, + classes: ['title-editor__input'], + }) + + const picked = pickWechatBodyProseMirrorCandidate([titleEditor], { titleInput, titleEditor }) + assert.equal(picked, null) +}) + +test('标题和正文同时存在时优先选择正文编辑器', () => { + const titleInput = createTitleInput() + const titleEditor = createEditor({ + top: 8, + height: 40, + width: 640, + classes: ['title-editor__input'], + }) + const bodyEditor = createEditor({ + textContent: '从这里开始写正文', + top: 180, + height: 520, + width: 720, + }) + + const picked = pickWechatBodyProseMirrorCandidate([titleEditor, bodyEditor], { titleInput, titleEditor }) + assert.equal(picked, bodyEditor) +}) + +test('没有标题编辑器时保留单编辑器场景的兼容性', () => { + const bodyEditor = createEditor({ + textContent: '正文内容', + top: 120, + height: 480, + width: 720, + }) + + const picked = pickWechatBodyProseMirrorCandidate([bodyEditor], {}) + assert.equal(picked, bodyEditor) +})