chundev
日期:2026-03-22 技術棧: SmartBPM (ExtJS + .NET Core) / Playwright MCP / Monaco Editor
用 Playwright 自動化操作 SmartBPM 建立表單和流程時,遇到多個 ExtJS 特有的坑:表單 Source Code 的 JSON 引號衝突、流程 Import 連線不渲染、formId 與 formName 的映射錯誤。核心教訓是:ExtJS 企業應用的自動化需要混合 UI 操作和 API-level 注入,純 UI 操作或純程式注入都不夠。
需要在 SmartBPM (一個 ExtJS + .NET Core 的企業 BPM 系統) 上建立多張量測表單(26 個 + 12 個量測項目)和對應的審核流程。量測項目需要依據「月份」做條件隱藏(月/季/半年/每年頻率)。由於控制項數量龐大,用 Playwright 自動化操作比手動拖放高效得多。
SmartBPM 表單設計器的 Source Code 模式使用 Monaco Editor。要大量注入控制項定義,最佳方式是直接用 Monaco API。
偵測 Monaco:
const editors = document.querySelectorAll('[role="textbox"][aria-label="Editor content"]');
// className 包含 "inputarea monaco-mouse-cursor-text"
注入內容:
const model = window.monaco.editor.getModels()[0];
model.setValue(sourceCode);
⚠️ 不能用 CodeMirror API(document.querySelector('.CodeMirror').CodeMirror),SmartBPM 用的是 Monaco。
SmartBPM 的條件隱藏功能使用 hiddenExpression 屬性,值是 JavaScript 表達式字串。但這個值存在 JSON 結構中,所以雙引號會衝突。
❌ "hiddenExpression": "viewmodel.form.get("月份") !== "1""
→ JSON 解析錯誤:Ext.decode() 失敗
嘗試過的方案:
| 嘗試 | 做法 | 結果 | 原因 |
|---|---|---|---|
| ❌ 雙引號 | viewmodel.form.get("月份") |
JSON parse error | 雙引號衝突 |
| ❌ 單引號 | viewmodel.form.get('月份') |
仍然失敗 | 舊值殘留導致混合 |
| ❌ 正則替換 | 先移除再加入 | 殘留碎片 | 正則沒完全匹配 |
| ✅ escaped 雙引號 | viewmodel.form.get(\"月份\") |
成功 | JSON 標準轉義 |
正確的寫法(在 JSON 值中):
{
"hiddenExpression": "viewmodel.form.get(\"月份\") !== \"1\"",
"ctype": "segmentbar"
}
驗證方式 — 注入前先用 JSON.parse 驗證:
const defMatch = src.match(/definition:\s*(\{[\s\S]*\})\s*,\s*initComponent/);
if (defMatch) {
JSON.parse(defMatch[1]); // 如果不拋錯就是合法 JSON
}
SmartBPM 流程設計器的 Import 功能只接受 .bpmn 副檔名(內容是 JSON)。
關鍵發現:
from/to 定義正確.bpmn 重新 Import 時,連線會正確渲染原因: 匯出的 .bpmn 包含完整的 connection sprite 路徑座標資料,Import 時系統靠這些座標畫線。從零建立的 JSON 缺少這些座標。
最佳實踐:
1. Import 基本節點(不含連線座標)
2. 手動在 UI 上拉線
3. Download 匯出(此時包含座標)
4. 修改匯出的 JSON → 重新 Import(連線會正確渲染)
流程中每個節點的表單綁定必須用 formId(UUID),不能用 formName。
❌ "form": { "type": "Xform", "formName": "我的表單" }
→ Error mapping types: FormIdentity -> FormIdentityPB
✅ "form": { "type": "Xform", "formId": "3a202545-13d9-d428-1af9-172d24e4bb94" }
陷阱: 在流程設計器 UI 中設定 Default Form 只會更新流程層級的 property.defaultForm.formId,不會自動填入各 activity 節點的 property.form.formId(仍為 null)。
修正方式: Download → 用腳本把 defaultForm.formId 複製到所有 activity → Import → Save Overwrite。
formId = data['property']['defaultForm']['formId']
for act in data['activities']:
if act['property'].get('form', {}).get('type') == 'Xform':
act['property']['form']['formId'] = formId
if act['property'].get('mobileForm', {}).get('type') == 'Xform':
act['property']['mobileForm']['formId'] = formId
SmartBPM 使用兩種彈窗機制,Playwright 需要不同方式處理:
| 類型 | 觸發情境 | Playwright 處理 |
|---|---|---|
瀏覽器原生 alert() |
Login timeout | browser_handle_dialog |
ExtJS Ext.MessageBox |
JSON 解析錯誤、儲存確認 | browser_snapshot 偵測 alertdialog role |
操作慣例: 每次 Save / Delete / Submit 後立即 browser_snapshot,檢查是否出現 alertdialog 或 dialog 元素。
SmartBPM 的 ComboBox 在 Source Code 中有兩種格式,store 格式在儲存後會被系統轉換成 options 格式:
// 輸入時用 store 格式
"store": [["1","1月"],["2","2月"]]
// 儲存後被轉換成 options 格式
"options": [
{ "text": "Option 1", "value": "1", "checked": true },
{ "text": "Option 2", "value": "2" }
]
⚠️ 轉換後 text 變成預設的 “Option N”,原始的顯示文字(”1月”)會遺失。需要在儲存後手動修正 options 的 text。
\" escaped 雙引號 — 不要用單引號,雖然 JS 允許但容易在工具鏈中出問題.bpmn 是 JSON 不是標準 BPMN XML — 副檔名有誤導性getModels()[0].setValue() 是注入程式碼的標準方式