================================== 新功能 (2026-06) — 測試框架層 ================================== 新增 9 個功能,把 AutoControl 的自動化原語升級成一套完整的**測試框架**: 驗證畫面狀態、用資料驅動腳本、偵測並隔離不穩定測試、執行計分套件、輸出 CI 原生報告、稽核無障礙 / i18n、跨裝置矩陣並行執行,以及對音訊 / 影片做斷言。 每個功能都遵循框架既有模式:headless Python API、``AC_*`` executor 命令、 ``ac_*`` MCP 工具,以及 Qt GUI 分頁。 .. contents:: :local: :depth: 2 斷言 ==== 斷言 DSL -------- 驗證畫面狀態,而不只是操作畫面。每個 ``assert_*`` 觀察當前狀態、回傳 :class:`AssertionResult`,並(預設)在不符時拋出 ``AutoControlAssertionException``,讓腳本 / 測試 / 排程在錯誤假設處明確失敗:: from je_auto_control import ( assert_text, assert_image, assert_pixel, assert_window, ) assert_text("Login successful", region=[0, 0, 800, 200]) assert_image("checkmark.png", threshold=0.9) assert_pixel(100, 200, [0, 200, 0], tolerance=10) assert_window("Settings", exists=True) ``assert_text`` 支援 ``regex=True`` 與 ``present=False``(斷言「不存在」); 每個函式都接受 ``raise_on_fail`` 與 ``capture_on_fail``(將失敗畫面截圖存到 ``~/.je_auto_control/assertions/``)。 Executor:``AC_assert_text / _image / _pixel / _window``。 MCP:``ac_assert_*``。GUI:**Assertions** 分頁。 畫面外與系統斷言 ---------------- DSL 也能驗證不在畫面上的狀態:: from je_auto_control import ( assert_clipboard, assert_process, assert_file, assert_http, ) assert_clipboard("ORDER-12345", mode="contains") assert_process("chrome", running=True) assert_file("export.csv", min_size=1, contains="total") assert_http("https://localhost:8080/health", status=200) * ``assert_clipboard`` — 以 ``equals`` / ``contains`` / ``regex`` 比對剪貼簿 文字;``present=False`` 可確認機密已被*清除*。 * ``assert_process`` — 名稱含指定字串的程序是否正在執行(透過 ``psutil``)。 * ``assert_file`` — 檔案存在 / 子字串 / SHA-256 / 最小大小;路徑在任何 I/O 前先經 ``realpath`` 正規化。用於驗證下載或匯出。 * ``assert_http`` — ``http``/``https`` 端點回傳狀態碼(與可選內文子字串), 一律帶明確 ``timeout``;僅接受 ``http``/``https`` scheme,主機不可達視為 斷言失敗而非崩潰。 Executor:``AC_assert_clipboard / _process / _file / _http``。 MCP:``ac_assert_clipboard / ac_assert_process / ac_assert_file / ac_assert_http``。 斷言組合器(群組 / OR / 輪詢) ------------------------------ 用宣告式 *spec*(如 ``{"kind": "text", "text": "Saved"}`` 這樣的純 dict) 組合八種斷言,讓相同檢查在 Python、JSON、MCP 中都能使用而不需傳遞 callable:: from je_auto_control import assert_all, assert_any, assert_eventually # 軟斷言:跑完整批、收齊所有失敗 assert_all([ {"kind": "window", "title": "Dashboard"}, {"kind": "text", "text": "Welcome"}, ]) # OR:任一 spec 通過即通過(短路) assert_any([ {"kind": "text", "text": "Success"}, {"kind": "window", "title": "Redirecting"}, ]) # 輪詢單一 spec 直到通過或逾時 assert_eventually({"kind": "http", "url": "http://localhost:8080/health"}, timeout=30, interval=0.5) ``assert_all``(AND)不短路,回傳彙整所有子結果的 :class:`GroupAssertionResult`; ``assert_any``(OR)在第一個通過時停止;``assert_eventually`` 依間隔重複檢查 單一 spec 直到成立 — 適合等待服務啟動或下載檔出現。 Executor:``AC_assert_all / AC_assert_any / AC_assert_eventually``。 MCP:``ac_assert_all / ac_assert_any / ac_assert_eventually``。 媒體斷言(音訊 / 影片) ---------------------- 斷言某個東西確實「播放」或「動」了:: from je_auto_control import assert_audio_activity, assert_video_changes assert_audio_activity(duration_s=1.0, threshold=0.01, expect_sound=True) assert_video_changes("clip.mp4", start_s=0, end_s=3, expect_motion=True) ``assert_audio_activity`` 從輸入裝置錄音並比較 RMS 音量與門檻(有聲 vs 靜音)。 ``assert_video_changes`` 量測影片區段的相鄰影格平均差異(動態 vs 靜止),可選 ``region`` 裁切。數值核心(``rms``、``mean_frame_diff``、``measure_audio_rms``、 ``video_segment_motion``)為公開純函式。``sounddevice`` / OpenCV 為延遲載入依賴。 Executor:``AC_assert_audio / AC_assert_video_changes``。 MCP:``ac_assert_audio / ac_assert_video_changes``。GUI:**Media Checks** 分頁。 資料驅動執行 ============ 將 CSV / JSON / SQLite / Excel / 內嵌字面值的列資料餵進 ``${var}`` 腳本, 然後每列執行同一段 body:: from je_auto_control import load_rows rows = load_rows({"kind": "csv", "path": "users.csv"}) 在 JSON action 檔中,新的 ``AC_for_each_row`` 區塊命令會載入資料來源, 並把每列綁定到一個變數,其欄位可用 ``${row.column}`` 取用:: ["AC_for_each_row", { "source": {"kind": "csv", "path": "users.csv"}, "as": "row", "body": [ ["AC_type_keyboard", {"keys": "${row.username}"}], ["AC_assert_text", {"text": "${row.expected}"}] ] }] SQLite 連接器**只允許單句唯讀** ``SELECT`` / ``WITH``(多語句 / 寫入查詢一律拒絕); 所有檔案路徑皆經 ``realpath`` 驗證。``${var}`` 插值現在能解析點號路徑進 dict 鍵 與 list 索引(``${row.user}``、``${results.0}``),並保留值的型別。 Executor:``AC_load_data`` + ``AC_for_each_row``。 MCP:``ac_load_data``。GUI:**Data Sources** 分頁。 不穩定測試偵測與隔離 ==================== 不穩定報告 ---------- 從 SQLite 執行歷史評分間歇性失敗。執行記錄依 ``script_path``(或 ``source_id``) 分組;報告統計通過 / 失敗次數,以及依時間順序的通過↔失敗*翻轉*次數, 讓不穩定腳本排在持續綠燈或持續紅燈的腳本之上:: from je_auto_control import analyze_flakiness report = analyze_flakiness(min_runs=3) for entry in report.entries: print(entry.key, entry.flip_rate, entry.flaky) Executor:``AC_flaky_report``。MCP:``ac_flaky_report``。 GUI:**Flaky Tests** 分頁。 隔離(閉環處理) ---------------- 被隔離的案例名稱會被套件執行器*跳過*(記為 ``skipped``,原因 ``quarantined``), 讓已知不穩定的案例在修好前不再污染套件的紅 / 綠狀態。隔離區是一個小型 JSON 檔(POSIX 上為 0600 權限),可跨重啟保存:: from je_auto_control import ( default_quarantine_store, auto_quarantine_from_flakiness, ) default_quarantine_store().add("login_suite", reason="under triage") auto_quarantine_from_flakiness(flip_rate_threshold=0.5) ``auto_quarantine_from_flakiness`` 讀取不穩定報告,並隔離所有超過翻轉率門檻的群組。 Executor:``AC_quarantine_add / _remove / _list / _clear / _auto``。 MCP:``ac_quarantine_*``。GUI:**Test Suites** 分頁上的隔離區面板。 QA 套件執行器 + CI 報告 ======================= 套件編排 -------- 把扁平的 action list 變成具備 setup / teardown、標籤與逐案例通過 / 失敗計分的 測試案例。帶有 ``data`` 來源的案例會展開成每列一個計分案例:: from je_auto_control import run_suite spec = { "name": "Login", "setup": [["AC_focus_window", {"title": "MyApp"}]], "teardown": [["AC_close_window", {"title": "MyApp"}]], "cases": [ {"name": "valid login", "tags": ["smoke"], "actions": [["AC_assert_text", {"text": "Welcome"}]]}, {"name": "each user", "as": "row", "data": {"kind": "csv", "path": "users.csv"}, "actions": [["AC_assert_text", {"text": "${row.expected}"}]]}, ], } result = run_suite(spec, tags=["smoke"]) print(result.passed, result.failed, result.errored, result.skipped) ``AutoControlAssertionException`` 將案例標為**失敗**;其他例外標為**錯誤**; 乾淨執行為**通過**;被隔離的案例名稱記為**跳過**。 Executor:``AC_run_suite``。MCP:``ac_run_suite``。GUI:**Test Suites** 分頁。 CI 原生報告(JUnit / Allure) ----------------------------- 輸出 Jenkins、GitHub Actions、GitLab CI 與 Allure 能原生解析的報告:: from je_auto_control import write_junit_xml, write_allure_results write_junit_xml(result, "reports/junit.xml") write_allure_results(result, "reports/allure") ``AC_run_suite`` 在傳入 ``junit_path`` / ``allure_dir`` 時會直接一併寫出:: ["AC_run_suite", {"spec": {...}, "junit_path": "reports/junit.xml"}] 此處只負責報告*產生*(絕不解析不可信 XML),因此使用標準庫 ``xml.etree.ElementTree`` 寫出是安全的。 無障礙 & i18n 稽核 ================== 反向利用無障礙樹與 OCR 層,去*檢查*介面常見的無障礙 / 在地化缺陷, 而非用來操作:: from je_auto_control import run_audit, contrast_ratio report = run_audit( app_name="MyApp", contrast_pairs=[{"foreground": [120, 120, 120], "background": [255, 255, 255], "label": "hint"}], texts=["Save chang…"], # 用來掃描截斷的 OCR 字串 ) 檢查項目: * **缺漏標籤** — 無障礙樹中沒有可存取名稱的互動元件(按鈕、選單項、連結、 輸入框…)。 * **對比度** — WCAG 2.x 相對亮度對比率,含 AA / AAA 門檻 (``contrast_ratio([0,0,0],[255,255,255]) == 21.0``)。 * **截斷** — 以省略號結尾的 OCR 字串(翻譯後被切掉)。 Executor:``AC_audit_accessibility / AC_audit_contrast``。 MCP:``ac_audit_*``。GUI:**A11y Audit** 分頁。 行動裝置矩陣 ============ 將單一 action list **並行**分發到多台 Android / iOS 裝置,每台使用各自獨立的 executor(執行緒間的執行期變數作用域互不衝突)。腳本透過綁定的 ``${device.*}`` 變數鎖定當前裝置:: from je_auto_control import run_on_devices report = run_on_devices( actions=[["AC_android_tap", {"x": 100, "y": 200, "serial": "${device.serial}"}]], devices=[{"platform": "android", "serial": "emulator-5554"}, {"platform": "android", "serial": "emulator-5556"}], max_parallel=4, ) print(report.passed, report.failed) 單台裝置的失敗會被隔離——絕不中斷其他裝置。 Executor:``AC_run_device_matrix``。MCP:``ac_run_device_matrix``。 GUI:**Device Matrix** 分頁。