# scripts 只用于本地开发与验证,不参与打包。 ## verify-pages.cjs — 浏览器真实渲染验证 **为什么需要**:`lint` / `vue-tsc` / `build` 通过 ≠ 页面能跑。数据加载失败导致列表空白、 解析逻辑只处理部分数据、组件渲染时抛错,这些问题在类型检查里完全看不出来, 只有真实打开浏览器渲染才发现。 **用法**: ```bash # 1. 起 dev server(另开一个终端) npm run dev # 2. 抓验证码图片,然后自己看图认出数字 node scripts/login.cjs # 图片在 scripts/.captcha.png,例如显示 8944 # 3. 带验证码登录,会话写入 scripts/.session.json node scripts/login.cjs 8944 # 4. 跑渲染验证:打开页面、截图、断言关键内容、收集运行时错误 node scripts/verify-pages.cjs ``` 截图输出到仓库根目录的 `screenshots/`。 **环境变量**: | 变量 | 默认值 | 说明 | | ------------------------------------- | ------------------------------------------ | ---------------------- | | `VERIFY_URL` | `http://127.0.0.1:5173/` | 目标地址 | | `VERIFY_ROUTES` | `/management/documents,/workbench/signing` | 要验证的路由,逗号分隔 | | `VERIFY_OUT` | `/screenshots` | 截图输出目录 | | `CHROME_PATH` | Chrome 默认安装路径 | Chrome 可执行文件 | | `VERIFY_USERNAME` / `VERIFY_PASSWORD` | `admin` / `admin` | 登录账号 | | `VERIFY_BASE_URL` | `http://127.0.0.1:5173` | 登录接口所在地址 | **依赖**:需要 `ws`(CDP 通信)。装到隔离 workspace,不要装进项目: ```bash cd C:/Users//.workbuddy-ai/binaries/node/workspace /npm.cmd install ws ``` 运行脚本时指定 `NODE_PATH` 指向该目录: ```bash NODE_PATH=C:/Users//.workbuddy-ai/binaries/node/workspace/node_modules \ node scripts/verify-pages.cjs ``` **验证真实模式**(关掉演示数据)另起一个 server: ```bash VITE_MOCK_DOCUMENTS=false VITE_MOCK_SIGNING=false npx vite --port 5199 VERIFY_URL=http://127.0.0.1:5199/ node scripts/verify-pages.cjs ``` ## verify-editor-marks.cjs — 占位符 / 勾选框胶囊验证 **为什么需要**:占位符胶囊(`utils/signing-editor-marks`)是**只改渲染、不改数据**的实现—— 它靠 wangEditor 的 `decorate` 把文本切成叶子再挂样式。这种写法有三类问题编译期完全看不出来: - 叶子切分 / 样式挂载没生效 → 胶囊根本没渲染出来(看起来"功能没做"); - 装饰破坏了 Slate 的光标偏移计算 → 点击胶囊后光标落点错位、编辑时字符插到别处; - 装饰挂上去容易、**摘下来难** → 内容删了,DOM 上的类名还赖着不走(见下方「踩过的坑 · 5」)。 所以必须真实打开编辑器,一边看渲染一边点进去打字、删字符。 **用法**(登录步骤同 `verify-pages.cjs`,注意端口要和实际 dev server 一致): ```bash node scripts/login.cjs # 抓验证码,看图认数字 node scripts/login.cjs 8944 # 登录 VERIFY_URL=http://127.0.0.1:5174/ node scripts/verify-editor-marks.cjs ``` 脚本会:打开文档库 → 新增文档(弹出 Word 编辑器)→ 依次插入 5 个占位符与勾选框 → 输出编辑区探针与「占位符检查」面板文案 → 点进胶囊中部插入一个 `X` → 按退格删掉最后插入的 勾选框 → 再 dump 一遍叶子结构 → 打印 PASS/FAIL 台账并以退出码反映结果。 **断言清单**(全通过才算过,失败会列出未通过项并 `exit 1`): | 断言 | 它防的是什么 | | ---------------------------------------------------- | ----------------------------------------------- | | 占位符 / 勾选框都渲染成胶囊 | 装饰完全没生效,功能像没做 | | 每个胶囊都有类型角标(`::after` 有文案) | 角标挂错通道(`content: attr(...)` 拿不到值) | | 所有胶囊配色 / 边框 / 角标色**完全一致** | 又冒出「必填红」「签名位橙」这类分色分支 | | 胶囊与插入按钮上都没有 `*`、面板里没有「必填」 | 必填标记从别的地方漏回来 | | 面板能数出「N 个填写项」(N≠0) | 装饰把序列化结果改脏了,保存 / 签署链路口径被改 | | 点进胶囊后插入 `X` 得到 `{…X…}` | 装饰破坏了 Slate 的光标偏移计算 | | 退格后无 `.ms-sign-box`、无叶子残留 `ms-sign-*` 类名 | **本轮回归重点**:内容删了类名还挂着 | | 退格后正文不再有 `□` | 数据层确实删掉了(区别于「只有 DOM 脏」) | 截图:`04-editor-marks.png`(整窗)、`04b-editor-marks-zoom.png`(编辑区 4 倍放大,用来核对胶囊细节)、 `05-editor-marks-caret.png`(插入字符后)、`05b-editor-marks-delete.png`(退格后)。 ## verify-signature-click.cjs — 点击文中签名位采集签名 **为什么需要**:签名位的点击采集横跨三层(`utils/signing-document` 渲染 → `SigningDocumentInteractive` 事件委托 → 签署页弹窗与回灌),每一层都可能在编译期看不出问题: - 渲染层挂了 `data-sign-action` 但没人监听 → 看着像按钮,点了没反应; - 事件委托的 `closest()` 没做容器校验 → 容器外的事件也会误触发; - 回灌走了「整篇重渲染」而不是就地改写 → 正在输入的填写项连同光标一起丢。 所以必须真的用鼠标点、真的在签字板上画一笔、再看签名图有没有回到那一个签名位上。 **用法**(登录步骤同上,端口要和实际 dev server 一致): ```bash node scripts/login.cjs # 抓验证码,看图认数字 node scripts/login.cjs 8944 # 登录 VERIFY_URL=http://127.0.0.1:5173/ node scripts/verify-signature-click.cjs ``` 脚本会:打开签署工作台 → 选一个「待签署」任务 → 输出签名位探针 → 真实点击「医师签名」→ 在签字板上画一笔并确认 → 复检回显与页脚提示 → 用键盘 `Enter` 触发「患者签名」的采集入口。 **关键断言**: - 空签名位必须同时有 `data-sign-action=signature`、`role=button`、`tabindex=0`、 `cursor: pointer` 与 `✍ 点击签名` 文案——少一样就是「看着能点其实不能点」; - 采集后该签名位出现 `img.sign-doc-signature` 且 `src` 是 `data:image/png`, 同时采集入口被摘掉(`data-sign-action` 与 `sign-field--actionable` 都消失); - **患者签名位的采集入口不能被误伤**——医师签名采完,患者签名还得能点; - 页脚不再把已采集的签名位列进「还有签名位待采集」。 截图:`06-signature-actions.png`(初始:两个签名位都是可点入口)、 `07-signature-dialog.png`(弹窗标题指向被点的签名位)、`08-signature-applied.png`(回显 + 页脚更新)、 `09-signature-keyboard.png`(键盘触发)。 ## 踩过的坑 改脚本前先读这段,都是实际踩出来的。 ### 1. 登录不要 stub,走真实登录 MEDISIGN 后端对无效 token 返回 **401**,而 `utils/request.ts` 的响应拦截器收到 401 会 `clearAuthStorage()` + 跳登录页。伪造会话很容易被踢出去。 如果确实要 stub XHR(比如只想验证某个组件),**必须实现完整响应**: `getAllResponseHeaders` / `getResponseHeader` / `onloadend` 一个都不能少。 只设 `responseText` 会让 axios 在解析响应头时抛错、**Promise 永不 settle**, 路由守卫的 `await` 一直挂着 → `#app` 只剩 ``、**整页空白**。 这个现象极易被误判成「页面渲染挂了」,实际是验证脚本自己的 bug。 ### 2. 验证码是强制的 不传 `captchaId` / `captchaCode` 会报「验证码错误或已过期」。 验证码有时效,抓图和登录要连着做。 ### 3. CDP 连续导航在 SPA 里不可靠 `Page.navigate` 连续调用时: - `pushState` 跳转**不触发** `Page.loadEventFired` - 轮询 `document.readyState` 会立刻通过,但读到的是**旧文档** 结果就是断言拿到上一个页面的内容,看起来像「导航了但没渲染」。 正确做法(脚本已采用):只做**一次**整页 `Page.navigate`,后续切页取 ```js document.getElementById('app').__vue_app__.config.globalProperties.$router ``` 调 `router.push()`。注意 `history.pushState` + 手动派发 `popstate` 对 Vue Router **无效**。 ### 4. 文件用 .cjs 后缀 `package.json` 里有 `"type": "module"`,`.js` 会被当成 ES 模块, 而这里用 `require` 更简单,所以显式声明成 CommonJS。 ### 5. 自定义渲染器里改类名 / 属性,只能走 `data.attrs` 这条是排查「插入勾选框后删不干净」时挖出来的,两个坑叠在一起,值得单独记一笔。 #### 坑一:`data.class` 会被规范化函数搬走,根本到不了 class 模块 wangEditor 在渲染前会用一个内部函数(压缩后叫 `RT`)**递归重写整棵 vnode 树的 data**: ```js var LT = ["props", "attrs", "style", "dataset", "on", "hook"] // 保留键白名单 function RT(vnode) { var data = vnode.data || {} Object.keys(data).forEach(function (key) { if (key === 'key') { vnode.key = data[key]; return } if (LT.includes(key)) return // 白名单里的原样透传 if (key.startsWith('data-')) { Object.assign(vnode.data.dataset, …) // data-* 归入 dataset } else { Object.assign(vnode.data.props, …) // 其余一律塞进 props } delete vnode.data[key] // 原键被删掉 }) vnode.children.forEach(RT) // 递归 } ``` `class` **不在白名单里**,所以 `data.class = { 'ms-sign-chip': true }` 会被搬成 `data.props.class` 并 `delete data.class` —— snabbdom 的 class 模块读的是 `data.class`, 读到 `undefined` 就什么都不做。**结果是类名压根不出现在 DOM 上,胶囊整个不渲染。** (`attrs` 与 `dataset` 都在白名单里,能原样透传。) #### 坑二:`data.props` 只单向写入,旧值摘不掉 wangEditor 内嵌的 snabbdom 用 `fg([yg, Ag, Tg, wg, xg, gg])` 初始化,六个模块依次是 **class / style / dataset / eventlisteners / props / attrs**。其中 `props` 模块: ```js function propsModule(oldVnode, vnode) { for (key in newProps) { old === cur || (elm[key] = cur) } // 只遍历「新」props } ``` 它从不清理已经消失的旧 prop;而 `className` / `title` 在 DOM 元素上是原型访问器, `delete elm.className` 是个空操作。所以类名一旦通过 `props` 挂上去,就**再也摘不掉**: 勾选框被退格删掉后,那个叶子已经空了、`decorate` 也早已不再返回它, DOM 里却还留着 `` —— 编辑器里表现为一个擦不掉的浅蓝小方块。 注意这两条是**先后被踩**的:最早用 `props.className`(渲染出来了,但删不掉), 改成 `data.class`(删得掉,但压根渲染不出来),最后才落到 `data.attrs`。 | 通道 | 挂得上吗 | 摘得掉吗 | 结论 | | ---------------------- | ------------------ | -------- | -------------------------- | | `data.props.className` | ✅ | ❌ | 会留下擦不掉的残留 | | `data.class` | ❌(被 `RT` 搬走) | — | 完全不渲染 | | `data.dataset` | ✅ | ✅ | 可用,适合放 `data-*` 标记 | | **`data.attrs`** | ✅ | ✅ | **类名与 title 走这里** | `attrs` 模块两头都正常: ```js for (n in newAttrs) oldAttrs[n] !== v ? setAttribute(n, v) : … for (n in oldAttrs) n in newAttrs || removeAttribute(n) // ← 摘得掉 ``` #### 现象为什么长成「编辑器有问题、签署没问题」 `decorate` 侧其实一直是对的(数据层干净、签署侧按文本解析也一直正常), 出问题的只有渲染通道。用户的原话「在编辑中显示侧,签署的时候没问题」, 正是这个机制决定的:**问题不在数据,在渲染通道。** 排查手法:反编译 `node_modules/@wangeditor/editor/dist/index.esm.js`, 搜 `registerRenderStyle` / `fg([` / `removeAttribute` / `classList.remove`, 把 snabbdom 的六个模块和 `RT` 的白名单逐个打印出来看。 对应的回归断言在 `verify-editor-marks.cjs` 的「删掉勾选框后 DOM 不残留」一段。 ## 安全提醒 `scripts/.session.json` 里是**真实 token**,已在 `.gitignore` 中忽略,**不要提交**。 `.captcha.json` / `.captcha.png` 是临时文件,同样已忽略。