Playwright 自动填表踩坑:layui 政务表单五个反直觉的坑
接了一个 RPA 自动填表需求:用 Playwright 驱动某省级政务申报网,把本地收集好的数据批量填进去。技术栈是标准的 layui + jQuery SSR 老表单,看起来没什么特别。结果第一次跑完,文本字段全部绿灯,日期字段全部报错——错误只写 Element is not editable,不说 readonly,排查花了不少时间。
后来发现不只一个坑,而是五个,且每一个都有自己的"不讲道理"。
现象
- 所有文本输入和普通
<select>都填成功,唯独日期字段全部失败,报Element is not editable或直接 timeout。 - 点击「新增」按钮用
page.evaluate(() => btn.click())执行后,弹框没有出现,页面纹丝不动。 selectOption('中级')偶尔选中错误选项,或者命中失败。- 级联下拉(如"级别→奖项名称")选完父项后,子项仍然为空,接着选子项报找不到选项。
- 同一个 tab 有多个新增入口,第二个弹框里的字段 id 全部查不到。
根因
坑 1:layui datepicker 给原生 input 挂了 readonly="readonly"。
layui 的日期组件不让用户直接敲键盘,把原生 <input> 设为 readonly,自己渲染浮层日历。Playwright 的 locator.fill() 在填写前会做 actionability 检查,readonly 属性直接拒填,报 not editable。错误信息没有说"这是因为 readonly",只说"不可编辑",所以非常容易误以为是 selector 写错了。
坑 2:layui/jQuery 的按钮事件绑在框架初始化之后,原生 el.click() 的事件链对不上。
page.evaluate(() => document.querySelector('#addBtn').click()) 走的是原生 DOM dispatchEvent,但 layui modal 的触发逻辑绑在 jQuery 的 .on('click', ...) 上,有初始化时序依赖——框架没完全 ready 时直接调原生 click,事件冒泡路径不同,modal 不会打开。
坑 3:政务表单 <option> 的 value 是数字 code,可见文本才是中文。
selectOption('中级') 会先按 value 匹配,再按 label 匹配。如果存的是可见文字而对应 option 的 value 是 "3",严格匹配就失败或选错。
坑 4:级联 select 子项依赖父项选完后的 AJAX 请求。
父项选完之前,子项 <option> 根本不在 DOM 里,直接 selectOption 子项必然失败。
坑 5:同页面第二个弹框渲染在 #myModal1,字段 id 带 _1 后缀。
两个新增入口共用相似 DOM 结构,但 id 加了后缀区分,只查 #myModal 或 #column8 找不到第二个弹框的字段。
layui datepicker 把原生 <input> 设为 readonly="readonly",Playwright actionability 检查直接拒填。错误只报 "not editable / timeout",不说是 readonly 导致的。如果按"selector 没对上"方向排查会浪费很多时间。
正解
① readonly 日期框:填前移除 readonly
const loc = page.locator('#startDate') // 或 getByPlaceholder('输入开始时间')
await loc.evaluate(el => el.removeAttribute('readonly')).catch(() => {})
await loc.fill('2024-03-01')removeAttribute 对普通 input 是 no-op,统一加在 RPA runner 的 fill_field handler 里,所有日期字段一次修复,历史流程全部受益。
② 新增按钮:用 Playwright 的 actionable click,而非原生 evaluate
// 等框架初始化完毕
await page.waitForTimeout(1500)
// 用 role/text locator,走 Playwright actionable 事件链
await page.getByRole('button', { name: '新增' }).first().click()
// 或者:
await page.getByText('新增学位', { exact: true }).first().click()③ select_option 的值必须严格等于 option 可见文本
提前抓取目标网站下拉的完整文本(含全角括号等特殊字符),存入配置。运行时 selectOption 传可见文字,可以命中 label 匹配路径,不依赖 value 数字。
④ 级联 select:选父之后等 AJAX,再选子
await page.locator('#levelSelect').selectOption('省部级')
await page.waitForTimeout(2000) // 等子项 AJAX 加载
await page.locator('#awardSelect').selectOption('省科学技术奖')⑤ 多弹框:按 placeholder 或全局 id 后缀定位
不依赖 #myModal 容器,改用 getByPlaceholder 或直接查带 _1 后缀的 id,两个弹框都能稳定命中。
RPA runner 的 fill_field handler 加一行 el.removeAttribute('readonly') 再 fill,日期字段全部通过。新增按钮改用 getByRole('button').click(),modal 稳定弹出。级联 select 在父项和子项之间插入适当 waitForTimeout。
一句话外卖
用 Playwright 自动填老式 UI 框架(layui、jQuery UI 等)时,先检查 input 是否有 readonly/disabled 属性——框架为了接管 UI 经常这样做,而 Playwright 的 actionability 检查不会告诉你根因是 readonly。