华为云社区 Markdown 编辑器的自动化适配:结构、写入与校验
华为云社区的博客编辑器有两套:富文本和 Markdown。手工写文章时这个设计很友好,但一旦要把「发一篇博客」塞进浏览器自动化流程,就会撞上一连串结构性问题:编辑器在 iframe 里、正文是 contenteditable、内容区还带虚拟滚动、发布按钮受一个自定义复选框控制。这篇把这些结构一个个拆开,给出可复用的定位与校验方法。
一、先看结构:两个 iframe 与一次模式切换
编辑器页 https://bbs.huaweicloud.cn/blogs/article 加载后默认是富文本模式,正文在 #editor_ifr 里。点「Markdown格式编辑」之后,DOM 上出现一个并列的 iframe:#editor_markdown_ifr,而 #editor_ifr 变成隐藏。
这个细节很有用:不需要读任何内部变量,只要判断哪个 iframe 的 offsetParent 非空,就能确认当前处于哪种模式。
const mode = await page.evaluate(() => {
const rt = document.querySelector('#editor_ifr');
const md = document.querySelector('#editor_markdown_ifr');
return {
rich: !!rt && rt.offsetParent !== null,
markdown: !!md && md.offsetParent !== null
};
});
两个 iframe 都是跨域的(编辑器静态资源在 res-hd.hc-cdn.cn)。这意味着顶层 document 拿不到里面的 DOM,元素的 contentDocument 是 null。习惯用 page.evaluate 一把梭的脚本会在这里断掉,必须走 frame 定位:
const frame = page.frameLocator('#editor_markdown_ifr');
二、正文写入:为什么 fill 比 type 合适
Markdown 模式下的正文是一个 CodeMirror 6 实例,可编辑元素是 .cm-content(contenteditable="true" 的 div),不是 textarea,也不是 iframe 的 body。这一点容易踩:对 body 调 fill() 会直接报「Element is not an <input>…」。
写入方式有两种,差别比想象中大:
- 逐字
type():模拟真实按键,但两千字要跑几十秒,而且会自动触发 CodeMirror 的自动补全、括号配对、Markdown 列表续行等输入法级别的处理,偶尔多出字符; fill():一次性写入,速度快,Markdown 的换行、代码块、表格都能完整保留。
实测两千四百字左右的文章,fill() 一次写入不到一秒。需要注意 fill() 之前最好先 click() 一下 .cm-content 让编辑器拿到焦点——不点也能写进去,但点了之后光标与滚动位置更符合预期,后续如果要追加内容不至于插到文档开头。
三、校验:innerText 会骗你
写入之后第一反应是读回来比对。这一步有个坑:CodeMirror 6 只渲染视口附近的行,.cm-content 的 innerText 只包含已渲染的部分。
我第一次校验时就遇到:本地 Markdown 四千余字符,读回来只有两千出头,而且开头不是文章第一句,是中间某一段——因为编辑器自动滚到了光标处,虚拟滚动把上半部分卸载了。如果按 innerText 判断,会误以为「只写进去一半」,然后重复写入,结果文章里出现两份内容。
可靠的判据是编辑器自己的文档模型:
const len = await frame.locator('.cm-content')
.evaluate(el => el.cmView ? el.cmView.view.state.doc.length : -1);
.cm-content 元素上挂着 CodeMirror 的 cmView,view.state.doc.length 是整个文档的字符数,与虚拟滚动无关。只要它和本地文件的字符数一致,就可以确认写入完整。返回 -1 表示没拿到 cmView(编辑器版本不同或尚未初始化完),这时退回去用站点自带的「统计字数」按钮文案(形如 统计字数2100/100000)交叉验证。
四、标题:别依赖 placeholder
标题输入框是 input.write-blog-blog-title。它的 placeholder 属性是空的——页面上看到的灰字「请输入标题」是外层 div 的视觉占位,所以 input[placeholder="请输入标题"] 永远匹配不到。按 class 定位最稳。
另一个细节:标题填完之后,页面右侧会出现 N/64 的计数。这个计数既是长度校验,也是「标题确实进了输入框」的证据。标题超过 64 字时「发表」按钮不会变灰,但提交会被服务端拒绝,所以脚本里最好在提交前主动判断一次长度。
五、免责声明:一个必须点的自定义复选框
「发表」按钮在三种情况下处于 disabled:标题为空、正文为空、没有勾选免责声明。前两个好判断,第三个最麻烦——它不是一个真正的 <input type="checkbox">,而是一个 span,点击后 class 从 unchecked 变成 checked。
await page.evaluate(() => document.querySelector('#agreementBox').click());
const cls = await page.evaluate(() => document.querySelector('#agreementBox').className);
用 Playwright 的 click() 打在这个 span 上,有时会因为被装饰元素覆盖而报「element intercepts pointer events」,JS click() 反而稳定。判定只看 class 里有没有 checked,不要去看有没有打勾图标——图标是伪元素,读不到。
六、提交后的成功判据
点「发表」(#submit-btn,同样是 JS click 更稳)之后,页面会跳到 processcomplete?type=blog。这是最干净的成功信号,比等 toast 靠谱:toast 只显示两秒,脚本稍慢一点就错过了。
跳转成功不代表文章已上架。内容会进人工审核,通常几分钟到一小时不等。这段时间里博客列表看不到文章、成长中心的「每日发布文章」也还显示「去完成」,都属于正常现象。判断是否真的完成,只能看积分明细里有没有对应的那一笔。
七、沉淀下来的四条
- 先确认模式,再定位元素。 双编辑器页面里,同一个语义(正文)对应两套完全不同的 DOM,不确认模式就开始找元素,报错会非常迷惑。
- 跨域 iframe 一律走 frame 定位。 顶层
document读不到就是读不到,不要试图绕过同源策略。 - 校验用数据模型,不用渲染结果。 虚拟滚动、懒加载、折叠面板都会让「看得见的内容」少于「实际的内容」。
- 成功判据选 URL,不选 toast。 一次性提示对自动化极不友好,路由变化才是稳定信号。
这套方法不只适用于这一个编辑器。凡是 CodeMirror、Monaco 这类自己管理文档模型的编辑器,fill() 加文档长度校验的组合都成立;凡是双模式编辑器,「先判模式再定位」都能省掉大量调试时间。
- 点赞
- 收藏
- 关注作者
评论(0)