Optics Framework
Self-healing test automation for mobile, web, TV — and AI agents.
One keyword engine. Six ways to drive it: CSV/YAML files, a Python SDK, Robot Framework, a REST API, an interactive terminal, or an MCP server your AI agent talks to.
Documentation · Install · Quick Start · Keywords · Architecture
Most frameworks assume a UI element has one true locator, and a test breaks the moment that locator changes. Optics assumes an element has several plausible identities — its XPath, its visible text, what it looks like on screen — and tries all of them before giving up.
That idea runs through the whole framework: locators fall back, drivers fall back, element values fall back. Tests are data (CSV or YAML), so non-coders can write them, and the same keywords are reachable from Python, Robot Framework, HTTP, and MCP.
Why Optics
A locator ladder, not a locator. Every element-based keyword walks a priority-ordered chain until one strategy succeeds:
| # | Strategy | How it finds the element |
|---|---|---|
| 1 | XPathStrategy |
Native XPath query through the driver's accessibility tree |
| 2 | TextElementStrategy |
Direct text / CSS / class lookup through the element source |
| 3 | TextDetectionStrategy |
Screenshot → OCR (EasyOCR, Pytesseract, Google Vision, remote OCR) |
| 4 | ImageDetectionStrategy |
Screenshot → template matching against a reference PNG |
| 5 | AI self-heal (opt-in) | All four failed → an LLM reads the screen and recovers |
Cheap strategies run first, so vision only costs you time when the tree can't help. Steps 1–4 are LocatorStrategy registrations; step 5 is a separate recovery layer, bounded to five turns and a six-keyword allowlist so it re-enters the ladder rather than tapping blind coordinates. Two more fallback axes sit alongside: multiple values per element name, and multiple enabled drivers or element sources, each tried in config order.
Beyond the ladder:
- Tests are data — elements, modules and test cases as plain CSV or YAML. No IDE, no programming.
- Targets — Android, iOS, web (Selenium/Playwright), Android TV, Samsung Tizen, LG webOS.
- Non-intrusive — the
bledriver drives production devices as a Bluetooth HID mouse/keyboard where debugging and screenshots are blocked. Coordinate-only, so pair it withcamera_screenshotand the vision strategies. - Agent-ready —
optics mcpexposes every keyword as a typed MCP tool and device state as MCP resources.
Install
Optics needs Python 3.12+. The core install ships no drivers, OCR, or LLM backends — you add only what you need:
python3 -m venv venv && source venv/bin/activate
pip install "optics-framework[appium,easyocr]"
Extra names match the config.yaml source keys, so the word you install is the word you enable:
| Drivers | appium · selenium · playwright · ble |
| OCR | easyocr · pytesseract · google-vision |
| AI | llm (natural-language mode + self-heal) · mcp (MCP server) |
| Bundles | mobile · web · vision · all |
Or install them by name — optics setup pins to your installed Optics version, and bare optics setup opens a TUI picker:
optics setup --list
optics setup --install appium easyocr
[!IMPORTANT] A driver extra installs only the Python client. Mobile testing also needs the Appium server, a device/emulator, and platform tooling (Node.js, Android SDK/
adb, JDK). See the Installation & Prerequisites guide.
[!WARNING] Conda is not supported for
easyocr+optics-frameworktogether (conflicting NumPy 1.x/2.x requirements). Use a standardvenv.
Quickstart
optics init --name my_test_project --template contact
# point my_test_project/config.yaml at your device/app, start the Appium server, then:
optics dry_run my_test_project # validate keywords, elements and module refs — no device needed
optics execute my_test_project
--template scaffolds a working project from a bundled sample: contact, calendar, youtube (Appium/Android), clock (Android + image templates), gmail_web (Selenium), playwright (Playwright). Omit it for an empty scaffold with a commented starter config.yaml.
Write a test as data
my_test_project/
├── config.yaml
├── test_cases/test_cases.csv
├── modules/modules.csv
└── test_data/
├── elements.csv
├── error_definitions.csv # optional
└── input_templates/*.png # optional, for image matching
test_data/elements.csv — names mapped to locators. Doubles as a general variable store; repeating a name builds a fallback list.
Element_Name,Element_ID
Add_Contact_Button,//android.widget.Button[@content-desc="Create contact"]
First_Name_element,//android.widget.EditText[@text="First name"]
Save_Button,Save
First_Name,John
A locator can be an XPath, text=…, css=…, a plain string, an image filename from input_templates/, or TEXT_ONLY:… to force a vision-based search.
modules/modules.csv — a reusable sequence of keywords; ${name} resolves against elements.csv.
module_name,module_step,param_1,param_2
Add Contact,Press Element,${Add_Contact_Button}
Add Contact,Enter Text,${First_Name_element},${First_Name}
Add Contact,Press Element,${Save_Button}
test_cases/test_cases.csv — modules sequenced into scenarios. A test case whose name contains suite + setup (or teardown) is hoisted to run around the whole suite.
test_case,test_step
Suite Setup,Launch Contact Application
Add Contact with Contact App,Add Contact
Add Contact with Contact App,Verify Contact is Added
Six ways to run the same keywords
| Surface | Command / import | Best for |
|---|---|---|
| CLI runner | optics execute <project> |
CI suites written as CSV/YAML |
| Interactive TUI | optics live [project] |
Building a test by doing it — recording is always on, Ctrl-N toggles natural-language mode |
| Python SDK | from optics_framework import Optics |
Custom logic, embedding in existing suites |
| Robot Framework | Library optics_framework.optics.Optics |
Teams already on Robot |
| REST API | optics serve |
Remote/orchestrated execution, live workspace streaming over SSE |
| MCP server | optics mcp |
Letting an AI agent drive a real device |
optics live — turning a session into a reusable module
Every successful keyword is buffered as you work. To persist the buffer:
/save <test_case> <module_name>
That appends the recorded keywords to modules/modules.csv as <module_name>, adds a (<test_case>, <module_name>) row to test_cases/test_cases.csv, creates a header-only elements/elements.csv stub if none exists, and copies the session's screenshots to execution_output/<module_name>/. The buffer then clears, so the next actions become the next module. If either name already exists, re-run the identical /save to confirm the append.
Other commands: /device [id], /elements, /screenshot, /help, /quit. Full reference: Live Usage.
Python SDK example
from optics_framework import Optics
optics = Optics()
optics.setup(
driver_sources=[{"appium": {"enabled": True, "url": "http://localhost:4723"}}],
elements_sources=[{"appium_find_element": {"enabled": True}}],
)
optics.launch_app("com.example.app")
optics.enter_text("username_field", "testuser")
optics.press_element("submit_button")
optics.validate_element("welcome_message")
optics.quit()
MCP client config
{ "mcpServers": { "optics": { "command": "optics", "args": ["mcp"] } } }
Then: start_session → observe (screenshot, optics://session/{id}/source) → act (press_element, enter_text, …) → terminate_session. For networked use: optics mcp --transport http --port 8090. Sessions are not shared with optics serve — each is its own process.
Keywords
Every public method on the four API classes is automatically a keyword, on every surface above. CSV/YAML uses Title Case (Press Element → press_element).
| Category | Keywords |
|---|---|
| Actions | Press Element · Press By Percentage · Press By Coordinates · Detect And Press · Select Dropdown Option · Swipe · Swipe By Percentage · Swipe From Element · Swipe Until Element Appears · Scroll · Scroll From Element · Scroll Until Element Appears · Enter Text · Enter Text Direct · Enter Text Using Keyboard · Enter Number · Clear Element Text · Press Keycode · Get Text · Sleep · Execute Script |
| Verification | Assert Presence · Assert Visibility · Assert Equality · Validate Element · Validate Screen · Is Element · Get Interactive Elements · Get Screen Elements · Capture Screenshot · Capture Pagesource |
| App lifecycle | Launch App · Launch Other App · Start Appium Session · Get Driver Session Id · Close And Terminate App · Force Terminate App · Get App Version |
| Flow control | Run Loop · Condition · Read Data · Evaluate · Date Evaluate · Invoke API |
Run optics list for the live catalogue with signatures, or read the Keyword Usage guide for parameters and examples. Location keywords accept percentage-based Area-of-Interest bounds (aoi_x/y/width/height, 0–100) to scope a vision search to part of the screen.
[!NOTE]
Press CheckboxandPress Radio Buttonstill resolve but are deprecated aliases ofPress Element— usePress Elementdirectly.Add APIis available on theOpticsPython class only, not to the CSV/YAML runner; define APIs in anapi.yamland call them withInvoke APIinstead.
Configure once, in config.yaml
Every section is a priority-ordered list and every entry has an enabled flag. Enable a second driver and it becomes a fallback.
driver_sources:
- appium:
enabled: true
url: "http://localhost:4723"
capabilities:
platformName: Android
automationName: UiAutomator2
deviceName: emulator-5554
appPackage: com.google.android.contacts
appActivity: com.android.contacts.activities.PeopleActivity
elements_sources:
- appium_find_element: { enabled: true }
- appium_page_source: { enabled: true }
- appium_screenshot: { enabled: true }
text_detection:
- easyocr: { enabled: true }
image_detection:
- templatematch: { enabled: false }
log_level: INFO
| Layer | Available engines |
|---|---|
| Drivers | appium (Android, iOS, Android TV, Tizen, webOS) · selenium · playwright · ble |
| Element sources | appium_find_element · appium_page_source · appium_screenshot · selenium_* · playwright_* · camera_screenshot |
| Text detection | easyocr · pytesseract · google_vision · remote_ocr |
| Image detection | templatematch · remote_oir |
| LLM | gemini |
Enabling the LLM features
The llm_models block powers both natural-language mode in optics live (Ctrl-N) and AI self-heal. Install the extra (pip install "optics-framework[llm]"), then add:
llm_models:
- gemini:
enabled: true
capabilities:
model: gemini-2.5-flash # optional; this is the default
# use_vertexai: true # optional; else read from the environment
# project: my-gcp-project # optional (Vertex)
# location: us-east4 # optional (Vertex)
ai_self_heal: true # opt into the LLM backstop; default false
Credentials are read from the environment by the google-genai SDK — GEMINI_API_KEY (or GOOGLE_API_KEY) for the Gemini Developer API, or GOOGLE_GENAI_USE_VERTEXAI + GOOGLE_CLOUD_PROJECT / GOOGLE_CLOUD_LOCATION / GOOGLE_APPLICATION_CREDENTIALS for Vertex AI. Never commit keys to config.yaml. With every capabilities key omitted the SDK auto-detects the backend. google-genai is imported only when gemini is enabled, and a misconfigured LLM degrades to "no self-heal" rather than a hard failure.
Full reference: Configuration. Adding your own engine is a file drop plus an interface — see Extending the Framework.
Results
An optics execute run writes to <project>/execution_output/:
junit_output.xml— written incrementally, so CI sees progress as it happenslogs.json— structured logs whenjson_log: true- screenshots — pre/post action frames, plus strategy-annotated and AOI overlays
detected_errors_<session_id>.json— on-screen error detection
Drop an error_definitions.csv into test_data/ and Optics scans visible text for crash dialogs, Session expired, network errors, and the like — no assertions required. Matches also land in the JUnit XML as a synthetic failing testcase, so CI fails a build on "the app crashed mid-test" the same way it fails a normal assertion. See Error Detection.
CLI reference
optics init Scaffold a new project (--template, --path, --force, --git-init)
optics setup Install engine backends (--list, --install); bare command opens a TUI
optics dry_run Validate a project without touching a device
optics execute Run a project (--runner test_runner|pytest)
optics live Interactive keyword session against a live target
optics generate Emit pytest or Robot Framework code from a project
optics list Print every discoverable keyword
optics serve Start the REST API server (--host, --port, --workers)
optics mcp Start the MCP server (--transport stdio|http)
optics config Manage global configuration (interactive)
optics completion Install shell autocompletion
optics --version Print the installed version
Details in the CLI guide.
Contributing
git clone git@github.com:mozarkai/optics-framework.git
cd optics-framework
pipx install poetry
poetry install --with dev,test,docs
poetry run pytest # tests + coverage
poetry run ruff check --fix . # lint
poetry run pre-commit run --all-files
poetry run mkdocs serve # docs preview
Commits follow Conventional Commits, enforced by commitizen in the commit-msg hook. Read the Contributing Guidelines, the Developer Guide, and our Code of Conduct before opening a PR. Looking for a place to start? See Help Wanted.
Security issues: please follow SECURITY.md rather than opening a public issue.
License & support
Apache 2.0 — see LICENSE.
Questions and bugs: GitHub Issues. Anything else: lalit@mozark.ai.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file optics_framework-1.9.1.tar.gz.
File metadata
- Download URL: optics_framework-1.9.1.tar.gz
- Upload date:
- Size: 411.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2c000ea155d40bf8f7530bf1ae2986e4e1f7852ca68ec1a66d6ddb0c58ea85fe
|
|
| MD5 |
44ff33631c233500733cc1334a9e49e2
|
|
| BLAKE2b-256 |
7089871dc16bb24c38feac565b61d53e3e31e3eeece8b4b679c7e7057f08c784
|
Provenance
The following attestation bundles were made for optics_framework-1.9.1.tar.gz:
Publisher:
pypi-publish.yml on mozarkai/optics-framework
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
optics_framework-1.9.1.tar.gz -
Subject digest:
2c000ea155d40bf8f7530bf1ae2986e4e1f7852ca68ec1a66d6ddb0c58ea85fe - Sigstore transparency entry: 2335096645
- Sigstore integration time:
-
Permalink:
mozarkai/optics-framework@eb71ced56c1ce938c96bb121b4e44ae486a741ca -
Branch / Tag:
refs/tags/v1.9.1 - Owner: https://github.com/mozarkai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yml@eb71ced56c1ce938c96bb121b4e44ae486a741ca -
Trigger Event:
release
-
Statement type:
File details
Details for the file optics_framework-1.9.1-py3-none-any.whl.
File metadata
- Download URL: optics_framework-1.9.1-py3-none-any.whl
- Upload date:
- Size: 541.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3bd89d195e5396b73e762c9c3c4737689b4d9ff263b3905efd1881dd209c7495
|
|
| MD5 |
90d45108d6b93b21e6b086d2461f27f8
|
|
| BLAKE2b-256 |
ae6f2042f42a2e46a53657b09d9c2e848705ff17c88023ec85bc2179da2ded42
|
Provenance
The following attestation bundles were made for optics_framework-1.9.1-py3-none-any.whl:
Publisher:
pypi-publish.yml on mozarkai/optics-framework
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
optics_framework-1.9.1-py3-none-any.whl -
Subject digest:
3bd89d195e5396b73e762c9c3c4737689b4d9ff263b3905efd1881dd209c7495 - Sigstore transparency entry: 2335096691
- Sigstore integration time:
-
Permalink:
mozarkai/optics-framework@eb71ced56c1ce938c96bb121b4e44ae486a741ca -
Branch / Tag:
refs/tags/v1.9.1 - Owner: https://github.com/mozarkai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yml@eb71ced56c1ce938c96bb121b4e44ae486a741ca -
Trigger Event:
release
-
Statement type: