New Features (2026-06) — QA Layer
Nine additions that turn AutoControl’s automation primitives into a
full test framework: assert screen state, drive scripts from data,
detect and quarantine flaky tests, run a scored suite, emit CI-native
reports, audit accessibility / i18n, fan a script across a device
matrix, and assert on audio / video. Every feature ships with a
headless Python API, an AC_* executor command, an ac_* MCP
tool, and a Qt GUI tab — same pattern as the rest of the framework.
Assertions
Assertion DSL
Verify the screen state instead of only driving it. Each assert_*
observes the current state, returns an AssertionResult, and
(by default) raises AutoControlAssertionException on mismatch so a
script / test / scheduled run fails loudly at the broken assumption:
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 accepts regex=True and present=False (assert
absence); every helper takes raise_on_fail and capture_on_fail
(saves a screenshot of the failing screen under
~/.je_auto_control/assertions/).
Executor: AC_assert_text / _image / _pixel / _window.
MCP: ac_assert_*. GUI: Assertions tab.
Off-screen and system assertions
The DSL also verifies state that is not on the screen:
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— clipboard text byequals/contains/regex;present=Falseconfirms a secret was cleared.assert_process— a process whose name contains the argument is (or is not) running, viapsutil.assert_file— existence / substring / SHA-256 / minimum size of a file; the path isrealpath-normalised before any I/O. Verifies a download or export.assert_http— anhttp/httpsendpoint returns a status code (and optional body substring), always with an explicittimeout. Onlyhttp/httpsschemes are accepted; an unreachable host is a failed assertion, not a crash.
Executor: AC_assert_clipboard / _process / _file / _http.
MCP: ac_assert_clipboard / ac_assert_process / ac_assert_file /
ac_assert_http.
Assertion combinators (group / OR / poll)
Compose the eight assertion kinds with declarative specs — plain dicts
like {"kind": "text", "text": "Saved"} — so the same checks are
reachable from Python, JSON, and MCP without passing callables:
from je_auto_control import assert_all, assert_any, assert_eventually
# soft assertions: run the whole batch, collect every failure
assert_all([
{"kind": "window", "title": "Dashboard"},
{"kind": "text", "text": "Welcome"},
])
# OR: pass when at least one spec passes (short-circuits)
assert_any([
{"kind": "text", "text": "Success"},
{"kind": "window", "title": "Redirecting"},
])
# poll any spec until it passes or times out
assert_eventually({"kind": "http", "url": "http://localhost:8080/health"},
timeout=30, interval=0.5)
assert_all (AND) never short-circuits and returns a
GroupAssertionResult summarising every sub-result;
assert_any (OR) stops at the first pass; assert_eventually
re-checks one spec on an interval until it holds — ideal for waiting on a
service to come up or a download file to appear.
Executor: AC_assert_all / AC_assert_any / AC_assert_eventually.
MCP: ac_assert_all / ac_assert_any / ac_assert_eventually.
Media assertions (audio / video)
Assert that something actually played or animated:
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 records from an input device and compares the
RMS level to a threshold (sound vs silence). assert_video_changes
measures mean frame-to-frame difference over a video segment (motion vs
static), with an optional region crop. The numeric cores
(rms, mean_frame_diff, measure_audio_rms,
video_segment_motion) are public and pure. sounddevice /
OpenCV are lazy dependencies.
Executor: AC_assert_audio / AC_assert_video_changes.
MCP: ac_assert_audio / ac_assert_video_changes. GUI: Media
Checks tab.
Data-driven execution
Feed rows from CSV / JSON / SQLite / Excel / inline literals into a
${var} script, then run the same body once per row:
from je_auto_control import load_rows
rows = load_rows({"kind": "csv", "path": "users.csv"})
In a JSON action file the new AC_for_each_row block command loads a
data source and binds each row to a variable whose columns are
addressable as ${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}"}]
]
}]
The SQLite connector accepts a single read-only SELECT / WITH
statement only (multi-statement / write queries are rejected); all file
paths are realpath-validated. ${var} interpolation now resolves
dotted paths into dict keys and list indices (${row.user},
${results.0}) while preserving value types.
Executor: AC_load_data + AC_for_each_row.
MCP: ac_load_data. GUI: Data Sources tab.
Flaky-test detection & quarantine
Flaky report
Score intermittent failures from the SQLite run-history store. Runs are
grouped by script_path (or source_id); the report counts
pass/fail outcomes and pass↔fail flips in chronological order so a
flaky script ranks above one that is consistently green or red:
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 tab.
Quarantine (closing the loop)
A quarantined case name is skipped by the suite runner (recorded as
skipped with reason quarantined) so a known-flaky case stops
poisoning the suite’s red/green status until it is fixed. The store is a
small JSON file (mode 0600 on POSIX) that persists across restarts:
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 reads the flakiness report and
quarantines every group above the flip-rate threshold.
Executor: AC_quarantine_add / _remove / _list / _clear / _auto.
MCP: ac_quarantine_*. GUI: quarantine panel on the Test Suites
tab.
QA suite runner + CI reports
Suite orchestration
Turn flat action lists into scored test cases with setup / teardown,
tags, and per-case pass/fail. A case carrying a data source expands
to one scored case per row:
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)
An AutoControlAssertionException marks a case failed; any other
exception marks it error; a clean run is passed. Quarantined
case names are recorded as skipped.
Executor: AC_run_suite. MCP: ac_run_suite.
GUI: Test Suites tab.
CI-native reports (JUnit / Allure)
Emit reports that Jenkins, GitHub Actions, GitLab CI, and Allure parse natively:
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 writes them inline when given junit_path /
allure_dir:
["AC_run_suite", {"spec": {...}, "junit_path": "reports/junit.xml"}]
Only report generation happens here (never parsing untrusted XML), so
the stdlib xml.etree.ElementTree writer is safe.
Accessibility & i18n audit
Reuse the accessibility tree and OCR layer to inspect a UI for common accessibility / localisation defects rather than to drive it:
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 strings to scan for truncation
)
Checks:
Missing labels — interactive widgets (button, menu item, link, field …) exposed through the a11y tree with no accessible name.
Contrast — WCAG 2.x relative-luminance contrast ratio with AA / AAA thresholds (
contrast_ratio([0,0,0],[255,255,255]) == 21.0).Truncation — OCR strings ending in an ellipsis (clipped after translation).
Executor: AC_audit_accessibility / AC_audit_contrast.
MCP: ac_audit_*. GUI: A11y Audit tab.
Mobile device matrix
Fan a single action list out across many Android / iOS devices in
parallel, each on its own isolated executor (so runtime variable
scopes never collide between threads). The script targets the current
device through a bound ${device.*} variable:
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)
A failure on one device is isolated — it never aborts the others.
Executor: AC_run_device_matrix. MCP: ac_run_device_matrix.
GUI: Device Matrix tab.