Share Notes

chundev

View the Project on GitHub latteouka/share-notes

SmartBPM 表單與流程自動化建置 — Playwright 操作踩坑紀錄

日期:2026-03-22 技術棧: SmartBPM (ExtJS + .NET Core) / Playwright MCP / Monaco Editor


TL;DR

用 Playwright 自動化操作 SmartBPM 建立表單和流程時,遇到多個 ExtJS 特有的坑:表單 Source Code 的 JSON 引號衝突、流程 Import 連線不渲染、formId 與 formName 的映射錯誤。核心教訓是:ExtJS 企業應用的自動化需要混合 UI 操作和 API-level 注入,純 UI 操作或純程式注入都不夠


背景

需要在 SmartBPM (一個 ExtJS + .NET Core 的企業 BPM 系統) 上建立多張量測表單(26 個 + 12 個量測項目)和對應的審核流程。量測項目需要依據「月份」做條件隱藏(月/季/半年/每年頻率)。由於控制項數量龐大,用 Playwright 自動化操作比手動拖放高效得多。


問題與解法

1. 表單 Source Code 注入 — Monaco Editor

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。


2. hiddenExpression 的引號地獄

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
}

3. 流程 Import — .bpmn 格式與連線渲染

SmartBPM 流程設計器的 Import 功能只接受 .bpmn 副檔名(內容是 JSON)。

關鍵發現:

原因: 匯出的 .bpmn 包含完整的 connection sprite 路徑座標資料,Import 時系統靠這些座標畫線。從零建立的 JSON 缺少這些座標。

最佳實踐:

1. Import 基本節點(不含連線座標)
2. 手動在 UI 上拉線
3. Download 匯出(此時包含座標)
4. 修改匯出的 JSON → 重新 Import(連線會正確渲染)

4. formId vs formName — 流程表單綁定

流程中每個節點的表單綁定必須用 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

5. ExtJS Alert 偵測

SmartBPM 使用兩種彈窗機制,Playwright 需要不同方式處理:

類型 觸發情境 Playwright 處理
瀏覽器原生 alert() Login timeout browser_handle_dialog
ExtJS Ext.MessageBox JSON 解析錯誤、儲存確認 browser_snapshot 偵測 alertdialog role

操作慣例: 每次 Save / Delete / Submit 後立即 browser_snapshot,檢查是否出現 alertdialogdialog 元素。


6. ComboBox store vs options 格式

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。


學到的事


參考資料