自动化填充富文本编辑器:TinyMCE / CodeMirror 6 / contenteditable 的三种正确姿势
## 结论先行
自动化往富文本编辑器里填内容,`send_keys` 只在一种情况下可靠:元素是原生 `<textarea>` 或原生 `<input>`。一旦编辑器是 TinyMCE、CodeMirror 6、Slate/ProseMirror 这类基于 `contenteditable` 的框架组件,正确的注入点永远是框架自己的 API(编辑器实例),而不是 DOM。
按这个原则分三类,对应三种写对的方式:
| 编辑器 | 正确注入 | send_keys 的表现 |
|---|---|---|
| TinyMCE | `tinymce.activeEditor.setContent(html); ed.save()` | 字符进去了,但内部计数器仍是 `0/50000`,点提交静默无效 |
| CodeMirror 6 | 点 `.cm-content` 后**逐行** `send_keys`,或用 `Input.insertText` 派发 | 整段粘贴常被拆成一行,换行丢失 |
| 原生 contenteditable | `element.send_keys(text)` + 派发 `input` 事件 | 能用,但不会触发框架的校验重算 |
下面三个坑是实战里撞出来的,每个都有一套判定方法。
## 坑一:TinyMCE 里 send_keys 不计数
现象很迷惑:输入框里肉眼可见的文字已经出现了,字数统计还是 0,点「发表」没有任何反应,也没有 toast、没有错误提示。
原因:TinyMCE 的内容权威来源是编辑器实例的 model,不是 iframe 的 DOM。`send_keys` 走的是浏览器原生输入事件,只在某些键位上被 TinyMCE 的 keystroke 处理器捕获,`setContent` 才是它的正规入口,`save()` 负责把 model 同步回隐藏的 textarea(表单提交读的是那个 textarea)。
正确写法:
```python
d.execute_script("""
const ed = window.tinymce && (window.tinymce.activeEditor || window.tinymce.editors[0]);
if (!ed) return 'no editor';
ed.setContent(arguments[0]); // HTML 片段,支持 h3/p/ul/code
ed.save();
return 'len=' + ed.getContent().length;
""", html)
```
判定成功的信号是**页面上的计数器**从 `0/50000` 变成 `N/50000`。计数器不会撒谎,它读的就是 model。另外要注意 `activeEditor` 只在编辑器获得焦点时才有值,页面上有多个编辑器(主帖 + 回复、文章正文 + 摘要)时先确认拿对了实例,否则你会把内容写进上一个编辑器。
## 坑二:CodeMirror 6 的整段粘贴会塌成一行
CodeMirror 6 用一个 `.cm-content` 的 `contenteditable` 承接输入,`send_keys` 一整段带 `\n` 的文本时,它通常把换行当成一次 `Enter`——但在某些配置下(`lineWrapping`、多 cursor、或者输入被当成 composition 事件)换行会被吞掉,最终提交的 Issue 正文是一大坨单行文本。
稳定做法是逐行发送,最后单独补一个回车:
```python
for line in body.split("\n"):
editor.send_keys(line)
editor.send_keys(Keys.ENTER)
```
代价是慢(几百行要几十秒),收益是结构一定正确。如果正文里有 Markdown 代码块,别指望渲染,纯文本提交最安全。
## 坑三:JS 的 `.click()` 不触发框架事件
这一条不只影响编辑器,是整个 SPA 自动化里最常见的假成功:`element.click()` 执行完不报错、`is_displayed()` 也是 true,但页面毫无变化,因为框架监听的是 `mousedown/mouseup/pointerdown` 这一串真实指针事件,而不是 `click` 的冒泡。
判断方式:点完之后**回读组件状态**,而不是回读 `click()` 的返回值。按钮的 class 有没有从禁用态变出来、URL 有没有变、有没有出现新的 toast,这三样才是判据。
可靠的点击要走真实鼠标动作,并且在点之前把元素滚进视口:
```python
def rclick(d, el):
d.execute_script("arguments[0].scrollIntoView({block:'center'})", el)
time.sleep(0.6)
ActionChains(d).move_to_element(el).click().perform()
```
`scrollIntoView` 不是可选项。长页面里直接 `move_to_element` 会抛 `MoveTargetOutOfBoundsException`,因为目标坐标在视口之外——这个异常在同一个会话里能连续撞好几次,看起来像偶发,其实是必然。
例外:`input[type=checkbox]` 这类原生控件,Selenium 的 `WebElement.click()` 反而比 JS 的 `.click()` 可靠(原生点击会触发 `change`,JS 的不会)。但如果页面上那个「勾选框」其实是 `<span>` 加 CSS 画的(很多国产后台都这么干),那就又得回到真实鼠标点击,并且点完检查 `` `disabled` `` 属性有没有被移除,而不是自己的类名有没有变。
## 一个通用的自检顺序
1. 编辑器是什么框架?看 DOM 里有没有 `iframe[id*=ifr]`(TinyMCE)、`.cm-content`(CodeMirror)、`data-slate-editor`(Slate)。
2. 注入用框架 API,注入后回读**框架的计数/长度**,不是回读 DOM 的 `innerText`。
3. 提交前检查按钮的真实状态属性(`disabled` / `-no` 后缀的 class),而不是靠「点了应该有反应」。
4. 提交后回读一个**只有成功才会出现的字符串**(URL 变化、成功提示原文),避免把页面上残留的旧文案误认成新结果。
这四步跑完,富文本表单的自动化基本不会有假成功。反过来说,任何「点了没反应」的场景,九成能在第 2 步或第 3 步找到原因:你要么没写进框架的 model,要么按钮的校验门还关着。
- 点赞
- 收藏
- 关注作者
评论(0)