<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Optics]]></title><description><![CDATA[Optics]]></description><link>https://optics-framework.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6ad6cbc0db12aa209a52e759/d1aa44c1-4128-47d4-82e5-8c1616d95138.png</url><title>Optics</title><link>https://optics-framework.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Thu, 22 Oct 2026 04:14:07 GMT</lastBuildDate><atom:link href="https://optics-framework.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Your AI agent writes the test once. Optics runs it with no model.]]></title><description><![CDATA[An AI agent drives a browser through the Optics Framework MCP server and writes a CSV suite. Replays run in about 3 s with no model.]]></description><link>https://optics-framework.hashnode.dev/your-ai-agent-writes-the-test-once-optics-runs-it-with-no-model</link><guid isPermaLink="true">https://optics-framework.hashnode.dev/your-ai-agent-writes-the-test-once-optics-runs-it-with-no-model</guid><category><![CDATA[Testing]]></category><category><![CDATA[mcp]]></category><category><![CDATA[AI]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Chinmay Jha]]></dc:creator><pubDate>Tue, 20 Oct 2026 15:27:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ad6cbc0db12aa209a52e759/70742344-b76c-4645-83d6-c5c6491120e3.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>This post first appeared on</em> <a href="https://optics-framework.org/blog/ai-agent-writes-the-test-once"><em>optics-framework.org</em></a><em>, the home of Optics Framework. I work on Optics Framework.</em></p>
<p>Many AI tools for UI tests put a model in control of each test run. Thus, each run has a model cost. Also, the model can go through the app in a different sequence each time.</p>
<p>Optics Framework uses the model at a different point. Your AI agent goes through the app one time through the Optics MCP server. Then it writes a CSV test suite. After that, each run replays the suite and uses no model.</p>
<p>I did this procedure on saucedemo.com, a sample web site for tests. The agent was Claude Code with claude-opus-5-5, and the server was Optics Framework 1.11.0. This post shows the recorded session, the files that the agent wrote and the data from 10 replays.</p>
<p><a href="https://optics-framework.org/blog/ai-agent-writes-the-test-once"><img src="https://optics-framework.org/media/blog/agent-session-poster.webp" alt="Screen recording: Claude Code calls Optics MCP tools in a terminal panel while a Chromium window signs in to saucedemo.com, adds a backpack to the cart and completes the checkout. The agent then writes four suite files and the dry run passes." style="display:block;margin:0 auto" /></a></p>
<p><a href="https://optics-framework.org/blog/ai-agent-writes-the-test-once">Watch the recording on optics-framework.org</a></p>
<p><em>The recorded agent session at 1.5× speed. The left panel shows each Optics tool call, and the right panel shows the browser that the agent controls.</em></p>
<h2>The prompt</h2>
<p>I gave Claude Code one prompt and one MCP server. Skills and user settings were off. Thus, the agent had only the information from Optics.</p>
<pre><code class="language-text">Use the optics tools to open https://www.saucedemo.com in Playwright with headless false.
Sign in as standard_user / secret_sauce, add the Sauce Labs Backpack to the cart,
open the cart, check out as Ava Birch with postcode 560001, finish the order,
and check that "Thank you for your order!" appears.
Then write it as an Optics suite in ./checkout, run optics dry_run on it,
and tell me how to run it.
</code></pre>
<h2>What the MCP server gives the agent</h2>
<p><code>optics mcp</code> is a <a href="https://modelcontextprotocol.io">Model Context Protocol</a> server. In version 1.11.0 it has 41 tools. 3 tools control the session, and 38 tools are Optics keywords such as <code>press_element</code>, <code>enter_text</code> and <code>assert_presence</code>. The agent can also read these MCP resources:</p>
<ul>
<li><p><code>optics://session/{id}/elements</code>: a short list of the elements on the screen. Each item has a label, the permitted actions and the position on the screen.</p>
</li>
<li><p><code>optics://session/{id}/screenshot</code> and <code>optics://session/{id}/source</code>: the screen as an image, and the full UI tree.</p>
</li>
<li><p><code>optics://project-format</code>: the rules for the suite files.</p>
</li>
<li><p><code>optics://keywords</code>: the full list of keywords.</p>
</li>
</ul>
<p>On a web page, an item in the element list can also have a <code>loc</code> value. Optics makes this CSS selector in the browser. Then Optics makes sure that the selector finds only that element. This is the item for the Login button on saucedemo.com:</p>
<pre><code class="language-json">{"i": 3, "label": "Login", "cls": "input", "bounds": [494, 302, 786, 351], "act": ["tap"], "rid": "login-button", "loc": "[data-test=\"login-button\"]"}
</code></pre>
<p>The agent used a <code>loc</code> value for each of its 11 clicks and text entries.</p>
<p>The server has no tool to record or save a test. The agent writes the files, and it uses the rules from <code>optics://project-format</code>. The server also does not show the tools that tap a screen coordinate. On a different screen size, a coordinate from a screenshot can point to a different element.</p>
<h2>The session</h2>
<p>The time for the session was 71 seconds, with 32 model turns. The agent made 21 Optics tool calls and read the project format one time. It read the element list 6 times, one time on each new page. Claude Code showed a model cost of $0.44.</p>
<p>First, the agent did the full checkout in the browser. It found "Thank you for your order!" on the last page. Then it wrote 4 files and ran <code>optics dry_run</code>. The dry run passed.</p>
<h2>The files</h2>
<p>The test case uses 5 modules in sequence. The modules file has one row for each action, and all the values are variables:</p>
<p><code>checkout/modules/modules.csv</code>:</p>
<pre><code class="language-csv">module_name,module_step,param_1,param_2
Open Site,Launch App,${site_url}
Login,Enter Text,${username_field},${username}
Login,Enter Text,${password_field},${password}
Login,Press Element,${login_button}
Add Backpack To Cart,Press Element,${add_backpack_button}
Add Backpack To Cart,Press Element,${cart_link}
Add Backpack To Cart,Press Element,${checkout_button}
Enter Checkout Details,Enter Text,${first_name_field},${first_name}
Enter Checkout Details,Enter Text,${last_name_field},${last_name}
Enter Checkout Details,Enter Text,${postal_code_field},${postal_code}
Enter Checkout Details,Press Element,${continue_button}
Finish Order,Press Element,${finish_button}
Finish Order,Assert Presence,${order_complete_header}
</code></pre>
<p>The elements file holds the test data and the locators. For most elements, the agent put the verified <code>loc</code> value first and the label second:</p>
<p><code>checkout/test_data/elements.csv (part)</code>:</p>
<pre><code class="language-csv">Element_Name,Element_ID,Element_ID_2
site_url,https://www.saucedemo.com,
username,standard_user,
first_name,Ava,
username_field,"[data-test=""username""]",Username
login_button,"[data-test=""login-button""]",Login
checkout_button,"[data-test=""checkout""]",Checkout
finish_button,"[data-test=""finish""]",Finish
order_complete_header,text=Thank you for your order!,"[data-test=""complete-header""]"
</code></pre>
<p>For a different user or a different product, write new values in this file. The modules stay the same.</p>
<p>These 4 files are the full result of the session. You can read them, review them in a pull request and keep them in Git with the code of your app.</p>
<h2>The replay</h2>
<p>Then I ran the suite 10 times with <code>optics execute</code>. The environment had no API keys. All 10 runs passed. The shortest run was 2.8 seconds, the longest run was 4.2 seconds, and the median was 3.3 seconds. Each time includes the start of Python and the start of the browser.</p>
<p><a href="https://optics-framework.org/blog/ai-agent-writes-the-test-once"><img src="https://optics-framework.org/media/blog/replay-runs-poster.webp" alt="Screen recording: three runs of optics execute checkout. Each run prints a tree of passed steps while Chromium completes the saucedemo.com checkout in about three seconds, with no model calls." style="display:block;margin:0 auto" /></a></p>
<p><a href="https://optics-framework.org/blog/ai-agent-writes-the-test-once">Watch the recording on optics-framework.org</a></p>
<p><em>3 recorded replays of the suite at 1× speed, with the terminal output of each run.</em></p>
<p>To count the model calls, I recorded each DNS lookup and each network connection from the Optics Python process. In all 10 runs, the record was empty. Only the browser connected to the network, to load saucedemo.com. To make sure that the record works, I sent one request to example.com from the same process. The record showed that request.</p>
<p>The source code agrees with this result. Optics makes an LLM client only when you set <code>ai_self_heal</code> to <code>true</code>. The value is <code>false</code> if you do not set it.</p>
<img src="https://optics-framework.org/media/blog/agent-replay-figure.webp" alt="Diagram: the agent session uses the model for 32 turns and 21 Optics tool calls, and writes 4 files in 71 seconds for $0.44. Each replay runs 13 steps in 3.3 seconds with 0 model calls and costs $0." style="display:block;margin:0 auto" />

<p><em>Data from the agent session and from the 10 replays.</em></p>
<table>
<thead>
<tr>
<th></th>
<th>Agent session</th>
<th>Each replay</th>
</tr>
</thead>
<tbody><tr>
<td>Time</td>
<td>71 s</td>
<td>3.3 s (median of 10)</td>
</tr>
<tr>
<td>Model turns</td>
<td>32</td>
<td>0</td>
</tr>
<tr>
<td>Model cost</td>
<td>$0.44</td>
<td>$0</td>
</tr>
<tr>
<td>Result</td>
<td>4 suite files</td>
<td>10 of 10 runs passed</td>
</tr>
</tbody></table>
<h2>When the page changes</h2>
<p>A suite from an agent has the same problem as a suite from a person: the app can change. To simulate a refactor, I replaced the login button locator in <code>elements.csv</code> with <code>[data-test="login-btn-v2"]</code>. That selector is not on the page.</p>
<p>The run passed. Optics did not find the first locator. Then it tried the second locator, the label "Login". The log shows each locator that Optics tried:</p>
<pre><code class="language-text">Locating element: [data-test="login-btn-v2"] of type: CSS...
Locating element: Login of type: Text...
Pressing element 'Login'
</code></pre>
<p>The time for this step was 10.2 seconds. With a correct first locator, the time for the same step is approximately 0.1 seconds. Optics waits for the first locator until its time limit, and then it tries the label. The run passes with the second locator, but the run is slower. When the log shows a fallback, update the first locator.</p>
<h2>Do it yourself</h2>
<p>Python 3.12 (minimum) and an MCP client are necessary. These steps use Claude Code.</p>
<ol>
<li><p>Install Optics Framework with the MCP server: <code>pip install "optics-framework[mcp]"</code>.</p>
</li>
<li><p>Install the browser engine: <code>optics setup --install playwright</code>.</p>
</li>
<li><p>Add the server to Claude Code: <code>claude mcp add optics -- optics mcp</code>.</p>
</li>
<li><p>Give the agent a prompt with the same parts as the prompt above: the URL, the steps, the last check and the folder for the suite.</p>
</li>
<li><p>Read the files that the agent writes. Examine them as you examine a pull request.</p>
</li>
<li><p>Run the suite: <code>optics execute checkout</code>.</p>
</li>
</ol>
<p>For Cursor, VS Code and Claude Desktop, refer to <a href="https://optics-framework.org/docs/create/agent">Let your AI agent build tests</a>. For CI, set <code>headless: true</code> in <code>config.yaml</code> and refer to <a href="https://optics-framework.org/docs/run/ci">Run tests in CI</a>.</p>
<h2>Limits</h2>
<ul>
<li><p>The quality of the suite is a function of the model and the prompt. Read the files before you commit them.</p>
</li>
<li><p><code>optics dry_run</code> examines the files only. It does not open the app. Run <code>optics execute</code> one time before you use a new suite in CI.</p>
</li>
<li><p><code>loc</code> values are available on web pages only. On Android and iOS, the agent uses the ids and labels from the element list.</p>
</li>
<li><p>Loops, conditions and data files are not MCP tools, because they operate only in a full suite run. The agent can write them into the modules.</p>
</li>
<li><p>The HTTP transport (<code>optics mcp --transport http</code>) has no authentication. Keep it on 127.0.0.1 or on a network that you control.</p>
</li>
</ul>
<p>For an introduction to Optics Framework, read <a href="https://optics-framework.org/blog/introducing-optics-framework">Introducing Optics Framework</a>. To find error screens that no step examines, read <a href="https://optics-framework.org/blog/your-tests-passed-your-app-didnt">Your tests passed. Your app didn't.</a></p>
<ul>
<li><p>Docs: <a href="https://optics-framework.org/docs/create/agent">Let your AI agent build tests</a> and the <a href="https://optics-framework.org/docs/reference/mcp">MCP server reference</a></p>
</li>
<li><p>Source: <a href="https://github.com/mozarkai/optics-framework">github.com/mozarkai/optics-framework</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Your tests passed. Your app didn’t.]]></title><description><![CDATA[Catch crash dialogs, “Session expired” and other error screens that no test step checked for, with one CSV file in Optics.]]></description><link>https://optics-framework.hashnode.dev/your-tests-passed-your-app-didn-t</link><guid isPermaLink="true">https://optics-framework.hashnode.dev/your-tests-passed-your-app-didn-t</guid><category><![CDATA[Testing]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[automation]]></category><category><![CDATA[mobile]]></category><dc:creator><![CDATA[Chinmay Jha]]></dc:creator><pubDate>Tue, 20 Oct 2026 10:18:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ad6cbc0db12aa209a52e759/91766a0b-298e-4118-b924-f561ebbb238e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Catch crash dialogs, “Session expired” and other error screens that no step checked for, with one CSV file in Optics.</em></p>
<p>A checkout test signs in, opens a pair of trail runners, adds them to the cart and checks that “Added to cart” appears. Every step passes. Then the app shows “Session expired”, because last night’s build broke the token refresh. The run ends green and nobody looks at the last screen.</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/1sc6panti3dd5xnfixqu.gif" alt="" style="display:block;margin:0 auto" />

<p><em>Every step passes and CI goes green. The last screen says the shopper was signed out.</em></p>
<p>A test checks for the things you expect to see. Error screens turn up wherever they like: Android’s “Birch has stopped” dialog, an “isn’t responding” prompt, a page that says “Something went wrong”, a “No internet connection” banner. You could assert against each one after every step, and your suite would double in size and still miss the next message someone adds.</p>
<p>Optics, the open-source test automation framework, has a feature for this called error definitions. You list the messages that mean something went wrong, and Optics looks for them on the screen at the end of every run.</p>
<h3>The error definitions file</h3>
<p>Add a CSV anywhere in your Optics project:</p>
<pre><code class="language-csv">error_code,match_string,description,severity
ERR_CRASH,has stopped,The app crashed,critical
ERR_CRASH_AGAIN,keeps stopping,The app crashed again,critical
ERR_ANR,isn't responding,The app froze,critical
ERR_SESSION,Session expired,The user was signed out,high
ERR_SERVER,Something went wrong,Server error page,high
ERR_NETWORK,No internet connection,Offline,medium
</code></pre>
<p>match_string is the text to look for, and error_code is the short name you'll see in reports. description and severity are optional. Optics copies them into the report so whoever reads a failure knows what it means.</p>
<p>Optics recognises the file by its error_code and match_string columns, and optics execute uses it on every run. You don't switch anything on or call a keyword.</p>
<h3>What happens at the end of a run</h3>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/bkpcx076jlymsfr6dqr6.gif" alt="" style="display:block;margin:0 auto" />

<p><em>After the last test case, Optics reads the final screen, finds “Session expired” and records the match. A CI step turns it into a failed build.</em></p>
<p>After the last test case, Optics captures the screen once more and saves a screenshot and the page source with the run’s output. It then looks for every match_string in the screen's text. Matching ignores case and finds the text anywhere, so Session expired also matches "Your session expired. Please sign in again."</p>
<p>For each match, Optics logs a warning, writes the details to execution_output/detected_errors_.json and adds a failing test case named on-screen-error-detection to the JUnit report. The warning looks like this:</p>
<pre><code class="language-plaintext">ON-SCREEN ERROR DETECTED — code: ERR_SESSION | matched_on: match_string | match_string: 'Session expired' | severity: high | The user was signed out
</code></pre>
<p>I ran a checkout test against a small web demo shop. The terminal said Passed: 1 | Failed: 0, and this file appeared next to the report:</p>
<pre><code class="language-json">{
  "timestamp": "2026-10-16T06:05:38.634183+05:30",
  "detected": [
    {
      "error_code": "ERR_SESSION",
      "matched_on": "match_string",
      "match_string": "Session expired",
      "description": "The user was signed out",
      "severity": "high",
      "detected_at": "end_of_run"
    }
  ]
}
</code></pre>
<h3>Which text Optics reads</h3>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/9c6n464ea17qlslhhzdw.png" alt="" style="display:block;margin:0 auto" />

<p><em>Optics keeps only the text a person can see, then checks it against every match_string.</em></p>
<p>The check reads the UI tree the driver already provides, with no OCR, and keeps the text a person can see. On Android and iOS that is the text, content-desc, label, value, hint and name attributes. On the web Optics strips every tag and keeps the page's text. It skips ids, class names and bounds, so a button with the id error_retry or a div with class="error" can't set off a match. The whole check costs one page-source call at the end of the run.</p>
<p>Optics matches the error_code column too. If your app prints its own codes, such as "Error E1042", use E1042 as the code and Optics will catch it even when the wording around it changes.</p>
<h3>Make CI fail on it</h3>
<p>Optics adds each match to the JUnit report as a failing test case named on-screen-error-detection, so any CI tool that reads JUnit reports lists it as a failed test, with the error code and the message. To fail the job itself, add one step after the suite that fails when the JSON file exists:</p>
<pre><code class="language-yaml">- name: Run the suite
  run: optics execute birch-tests
- name: Fail on error screens
  run: |
    if compgen -G "birch-tests/execution_output/detected_errors_*.json" &gt; /dev/null; then
      cat birch-tests/execution_output/detected_errors_*.json
      exit 1
    fi
</code></pre>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/ec20kcw229yvreg95h9q.png" alt="" style="display:block;margin:0 auto" />

<p><em>The suite step passes. The next step fails and prints which message was on screen.</em></p>
<p>Optics writes the file only when something matched, so a clean run passes the step. When the build goes red, the job log already shows the error code and the message, and you don’t have to open a screenshot to find out why. Run it on a fresh checkout, since an old execution_output folder still holds the previous run's file.</p>
<h3>Why it helps</h3>
<p>You get the check on every test you already have, and none of them change. One file covers a project’s Android, iOS, web and TV runs, because Optics reads each driver’s UI tree the same way. Anyone can keep the list up to date: when support starts seeing a new error in tickets, they add a row to a CSV and it goes through review like any other change. And when a run fails, the report says “Session expired”, which is what the user saw.</p>
<p>Most teams do this by hand. In Playwright you write an afterEach hook that reads the page text. With Appium or Espresso you write a teardown that inspects the screen, or a script that searches logcat for FATAL EXCEPTION. Logcat catches a crash, but an app showing "Session expired" looks healthy to the operating system. Firebase Test Lab's Robo crawler reports crash dialogs it meets while it explores an Android app, which is useful, though it runs apart from your regression journeys. In Optics the list is a data file next to your tests, and it runs on every platform your suite runs on.</p>
<h3>Writing good match strings</h3>
<ul>
<li><p>Pick a phrase that only an error would show. “has stopped” works. “Error” matches too much, such as a help page about error messages.</p>
</li>
<li><p>Copy the exact words from a screenshot of the error and keep them short. Matching is plain text, so * and .* match only themselves.</p>
</li>
<li><p>List both Android crash messages. Android says “Birch has stopped” the first time an app crashes and “Birch keeps stopping” when it crashes again.</p>
</li>
<li><p>Leave out text your app shows on purpose. A match anywhere on the last screen counts.</p>
</li>
</ul>
<h3>Tips</h3>
<ul>
<li><p>Leave the app open at the end of the run, so Optics has a screen to read. If your Suite Teardown closes the app, move that step out of teardown.</p>
</li>
<li><p>Optics checks the screen where the run ends. For a message that can come and go in the middle of a flow, assert the screen you expect next.</p>
</li>
<li><p>In Python or Robot Framework, call Add Error Definitions once, then Capture And Detect Errors anywhere you want a check, including halfway through a test.</p>
</li>
</ul>
<h3>Try it</h3>
<pre><code class="language-shell">curl -fsSL https://optics-framework.org/install | sh
optics quickstart
</code></pre>
<p>Add error_definitions.csv to the project and run optics execute. The docs are at <a href="https://optics-framework.org/docs/run/error-detection">optics-framework.org/docs/run/error-detection</a> and the source is at <a href="https://github.com/mozarkai/optics-framework">github.com/mozarkai/optics-framework</a>. Later posts in this series cover the locator fallbacks, AI agents over MCP, and testing phones that can’t run automation software.</p>
]]></content:encoded></item><item><title><![CDATA[Introducing Optics: create a test your way, run it forever]]></title><description><![CDATA[Open-source test automation for Android, iOS, web and TV. Tests are CSV or YAML files, and each element can have several locators.]]></description><link>https://optics-framework.hashnode.dev/introducing-optics-create-a-test-your-way-run-it-forever</link><guid isPermaLink="true">https://optics-framework.hashnode.dev/introducing-optics-create-a-test-your-way-run-it-forever</guid><category><![CDATA[Testing]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[automation]]></category><category><![CDATA[mobile]]></category><dc:creator><![CDATA[Chinmay Jha]]></dc:creator><pubDate>Tue, 20 Oct 2026 10:16:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ad6cbc0db12aa209a52e759/6db01306-7347-4380-8976-6888492c10b5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Optics is an open-source test automation framework for Android, iOS, web and TV apps. Tests are CSV or YAML files, and each element on screen can have several locators.</em></p>
<p>A designer changes “Sign in” to “Log in”, or a developer renames the button’s id in a refactor, and the nightly run fails even though the app works. Someone then spends the next morning updating locators.</p>
<p>At Atlassian, flaky tests caused <a href="https://www.atlassian.com/blog/atlassian-engineering/taming-test-flakiness-how-we-built-a-scalable-tool-to-detect-and-manage-flaky-tests">up to 21% of master build failures</a> in the Jira frontend repo. Mobile teams often write the same flow separately for Android, iOS and the web. Many newer AI testing tools put a model in charge of every step of every run, so each run costs money and can take a different path through the app.</p>
<p>Optics, licensed under Apache 2.0, runs on your laptop or CI runner. It has no paid edition or account, and it sends no telemetry.</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/zec420vub3pbebcp83f3.gif" alt="Animation: four ways to create an Optics test (write, record, describe, delegate), each producing the same CSV test suite" style="display:block;margin:0 auto" />

<p><em>Four ways to create a test. Each one writes the same plain-text suite.</em></p>
<h3>The test format</h3>
<p>A test row describes one action a person takes in the app, and Optics works out how to perform it on the current screen.</p>
<pre><code class="language-csv">module_name,module_step,param_1,param_2
Sign In,Enter Text,${email_field},ava@birch.shop
Sign In,Press Element,${sign_in}
Sign In,Assert Presence,Picked for you
Add To Cart,Press Element,Add to cart
</code></pre>
<p>These rows form modules, which are reusable sequences of steps. A test case lists the modules to run in order, and an elements file maps names like sign_in to the locators that find them. All three are plain text in your repository, so product managers can read them and changes go through pull requests like any other code.</p>
<h3>Four ways to create a test</h3>
<p>All four write the same files, and one project can mix them:</p>
<ul>
<li><p>Write it in a text editor or a spreadsheet.</p>
</li>
<li><p>Record it in optics live, an interactive session next to your device. Each step runs on the device as you type it, and /save stores the steps that worked as a module.</p>
</li>
<li><p>Describe it in plain English. Press Ctrl+N in optics live, type <em>add trail runners to the cart</em>, and an AI model drives the app one keyword at a time.</p>
</li>
<li><p>Delegate it to your AI agent. Add optics mcp to Claude Code, Cursor or another MCP client, and every keyword becomes a tool the agent can call while it explores the app and writes the suite files.</p>
</li>
</ul>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/cbwedv0hdxi210bm78rq.gif" alt="Animation: an optics live terminal session running test steps on a phone app, then saving them to modules.csv" style="display:block;margin:0 auto" />

<p><em>optics live runs each step on the device as you type it, and /save writes the steps into the suite.</em></p>
<p>Teams that prefer code can use the same keywords from Python or Robot Framework.</p>
<h3>How Optics finds an element</h3>
<p>You can give an element several locators. Optics tries them in order, cheapest first:</p>
<ol>
<li><p>XPath, through the app’s UI tree.</p>
</li>
<li><p>The visible text, in the same tree.</p>
</li>
<li><p>OCR, which reads the text from a screenshot.</p>
</li>
<li><p>Image matching against a small picture of the element.</p>
</li>
</ol>
<pre><code class="language-csv">Element_Name,Element_ID
sign_in,//*[@resource-id='login']
sign_in,Sign in
</code></pre>
<p>With these two rows, a refactor that renames the button’s id breaks the XPath, and Optics falls back to the button’s text. Text painted into a banner image never reaches the UI tree, so OCR is the only way to read it. The report for each step names the method that found the element, which tells you when a locator has gone stale.</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/ptlyfp22ftraxwq1yji8.gif" alt="Animation: Optics locator fallback, where an XPath misses, the visible text matches, and OCR reads text inside a banner image" style="display:block;margin:0 auto" />

<p><em>After a redesign the XPath misses and Optics finds the button by its text. OCR reads a product name from a banner image.</em></p>
<h3>AI in Optics</h3>
<p>Optics uses AI to write tests. A model or an agent explores the app and leaves behind keyword rows for you to review, and optics execute replays those rows without calling a model. The browser-use team made a similar choice when they built a deterministic follow-up to their AI browser agent, because <a href="https://news.ycombinator.com/item?id=44007065">in their words</a> "pure LLM agents were slow, expensive, and unpredictable."</p>
<p>AI self-heal is the one exception. You can switch it on as a last step for when every locator fails, and the log records each action the model takes.</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/0ggby7cvozupbx8srer8.png" alt="Diagram comparing an AI model deciding every test run with Optics, where a model helps author the test once and later runs replay keywords" style="display:block;margin:0 auto" />

<p><em>Optics calls a model only while you write a test.</em></p>
<h3>Supported platforms</h3>
<p>A project’s config.yaml names the target, and the same test files run on:</p>
<ul>
<li><p>Android and iOS, through Appium, on emulators, simulators and real phones.</p>
</li>
<li><p>Websites, through Playwright or Selenium.</p>
</li>
<li><p>Android TV, Samsung Tizen and LG webOS, through Appium, using remote-control keys.</p>
</li>
<li><p>Locked production phones, through a Bluetooth mouse and keyboard and a camera pointed at the screen. Nothing gets installed on the phone, so a journey tested in CI can also monitor the live app.</p>
</li>
</ul>
<p>Teams that already run Appium, Playwright or Selenium keep their drivers and devices, since Optics drives them directly.</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/i2woo2gr9zl0zyjf0urs.gif" alt="Animation: the same Optics test running on a phone, a TV with remote keys, and a web browser" style="display:block;margin:0 auto" />

<p><em>The same test on a phone, a TV and a browser.</em></p>
<h3>Running a suite</h3>
<p>optics execute prints a live tree of every test case, module and step. This is a real run on an Android emulator:</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/vt0wc4liiia6e6ahsvhq.gif" alt="Screen recording of optics execute running a test suite on an Android emulator, with a live pass and fail tree in the terminal" style="display:block;margin:0 auto" />

<p><em>A real optics execute run on an Android emulator.</em></p>
<p>A run leaves behind a JUnit report for CI, screenshots with a box around every element it found, and an exit code of 1 when a test fails. You can list error messages such as <em>Session expired</em> in a definitions file, and Optics looks for them on the last screen to catch failures no step asserted.</p>
<p>optics dry_run checks a suite without a device, and loops, conditions and data files run one flow with many inputs. To connect Optics to other tools, Invoke API calls your backend mid-test, optics serve exposes the keywords over REST, and optics generate converts a suite to pytest or Robot Framework code.</p>
<h3>Try it</h3>
<p>Optics needs Python 3.12 or newer and runs on macOS, Linux and Windows:</p>
<pre><code class="language-shell">curl -fsSL https://optics-framework.org/install | sh
optics quickstart
optics execute my-optics-project
</code></pre>
<p>On Windows, install with irm <a href="https://optics-framework.org/install.ps1">https://optics-framework.org/install.ps1</a> | iex. pip install optics-framework works on any system. The quickstart asks whether you're testing a mobile app or a website, installs the engine it needs, and creates a project you can run.</p>
<ul>
<li><p>Docs: <a href="https://optics-framework.org/docs/">optics-framework.org/docs</a></p>
</li>
<li><p>Source: <a href="https://github.com/mozarkai/optics-framework">github.com/mozarkai/optics-framework</a></p>
</li>
</ul>
<p>Later posts in this series cover the locator fallbacks, AI agents over MCP, and testing phones that can’t run automation software. Send bug reports and questions to the GitHub issues.</p>
]]></content:encoded></item></channel></rss>