<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dima M</title>
    <description>The latest articles on DEV Community by Dima M (@dima_m_c82ad9d0f9b7ba6abf).</description>
    <link>https://dev.to/dima_m_c82ad9d0f9b7ba6abf</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4048275%2F29c81a33-8d32-4d67-bfa1-82eb2d352067.png</url>
      <title>DEV Community: Dima M</title>
      <link>https://dev.to/dima_m_c82ad9d0f9b7ba6abf</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dima_m_c82ad9d0f9b7ba6abf"/>
    <language>en</language>
    <item>
      <title>How I vibecoded Lighthouse in a world of UI tests</title>
      <dc:creator>Dima M</dc:creator>
      <pubDate>Sun, 26 Jul 2026 18:24:48 +0000</pubDate>
      <link>https://dev.to/dima_m_c82ad9d0f9b7ba6abf/how-i-vibecoded-lighthouse-in-a-world-of-ui-tests-3k5k</link>
      <guid>https://dev.to/dima_m_c82ad9d0f9b7ba6abf/how-i-vibecoded-lighthouse-in-a-world-of-ui-tests-3k5k</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft7gbe7ty0n34ub9oqtqb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft7gbe7ty0n34ub9oqtqb.png" alt=" " width="800" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the idea came from
&lt;/h2&gt;

&lt;p&gt;There are still .NET MAUI developers out there. They may be listed in the Red Book, but they have not gone extinct yet. I am one of them.&lt;br&gt;
My name is Dima.&lt;/p&gt;

&lt;p&gt;I work at a small company that builds a cross-platform app on top of an ERP system. That detail barely matters for this article.&lt;br&gt;
At work we were looking for an automated UI testing tool that a QA engineer could use without turning into a full-time automation developer. Why now? Because of the pain of moving from Xamarin to MAUI.&lt;br&gt;
The target scenario was deliberately simple: open the app, record a test by hand, and run it on the server with every release.&lt;/p&gt;

&lt;p&gt;Our product is on &lt;strong&gt;.NET MAUI&lt;/strong&gt;, so “every release” is not one environment — it is three: desktop, Android, and iOS. With a shared UI you would like to run the same test on different platforms. You can also record the same tests three times; that is not the most critical part.&lt;/p&gt;

&lt;p&gt;The first candidates were the usual enterprise products — &lt;strong&gt;Ranorex&lt;/strong&gt;, &lt;strong&gt;TestComplete&lt;/strong&gt;, and the like. They are powerful systems, but hard to learn (Android there is a special kind of horror), expensive to license, and often a poor fit for a QA who is strong at finding defects but does not want — and should not have — to dig through the tool’s structure and visual tree just to add a control for verification. We wanted something closer to record-and-run, with a clear story for cross-platform control identity.&lt;/p&gt;

&lt;p&gt;After a few evaluations it was clear that the combination “simple for QA + cross-platform + stable ids” is almost nowhere on the shelf.&lt;br&gt;
That is when I asked myself: how hard is it to build what I am looking for? That is when the vibecoder in me woke up.&lt;/p&gt;

&lt;p&gt;I tried to do it &lt;strong&gt;in the evenings, outside work hours&lt;/strong&gt;. The result is &lt;strong&gt;Lighthouse Tests Suite&lt;/strong&gt;, early beta: &lt;a href="https://lighthousetests.com/" rel="noopener noreferrer"&gt;lighthousetests.com&lt;/a&gt;. All in all I spent about six days, two to four hours at a time — whenever I could.&lt;/p&gt;


&lt;h2&gt;
  
  
  Vibecoding without rose-colored glasses
&lt;/h2&gt;

&lt;p&gt;I built this in Cursor, model Grok 4.5 (auto-select in newer versions). The model is genuinely fairly “smart” and quick. It thinks faster than the previous ones. Periodically you still have to poke it with a stick so it does not get lazy; sometimes it suggests options that are simply not a fit — but that is rare. Overall I am more satisfied than not.&lt;/p&gt;

&lt;p&gt;If someone thinks AI will take over the world — you can relax for a bit; there is still some time. So the scheme is simple: I decide what we build and where we cannot afford to be wrong; the model prints kilometers of glue. A classic junior on steroids, only without coffee and perks.&lt;/p&gt;

&lt;p&gt;In a weekend you can knock out “I attached to Calculator, recorded a click, here is a screenshot in Telegram.”&lt;br&gt;
But to have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;recording and playback on Windows;&lt;/li&gt;
&lt;li&gt;at least some Android path via Appium;&lt;/li&gt;
&lt;li&gt;a site, an installer, CLI/API to “run on the server”;&lt;/li&gt;
&lt;li&gt;a UI where QA does not cry at an empty screen —&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;that is no longer a weekend. Ask me how I know.&lt;/p&gt;

&lt;p&gt;Below is not a full repo tour — just the places where vibecoding without engineering hits a wall fast.&lt;/p&gt;


&lt;h2&gt;
  
  
  What came out
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Lighthouse&lt;/strong&gt; is a Windows desktop app (.NET 10, WPF). The idea is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Attach to a Windows process (FlaUI / UIA3) or to Android (Appium UiAutomator2).&lt;/li&gt;
&lt;li&gt;See a UI map — screenshot plus element overlays.&lt;/li&gt;
&lt;li&gt;Record steps (or right-click the map: assert / wait, and so on).&lt;/li&gt;
&lt;li&gt;Run again. Check the dashboard.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The bet is &lt;strong&gt;AutomationId&lt;/strong&gt;. In MAUI that is a normal story: put an id on a button, and you have a chance to run the same intent on Windows and Android without rewriting everything from scratch. Flutter with &lt;code&gt;Semantics(identifier: …)&lt;/code&gt; is the same song. &lt;strong&gt;iOS is not there yet.&lt;/strong&gt; I wrote myself the credo “write once, play everywhere” before I had dragged the third platform across the finish line. It happens.&lt;/p&gt;

&lt;p&gt;One small thing that annoyed me: the menu showed a pretty name like &lt;code&gt;Display is 1&lt;/code&gt;, while playback went by &lt;code&gt;#CalculatorResults&lt;/code&gt;. It looks as if the test is lying. The preview now shows the id as well — more honest that way. Playback was already finding by AutomationId; the lie was only in the UI.&lt;/p&gt;

&lt;p&gt;Guide and docs: &lt;a href="https://lighthousetests.com/guide.html" rel="noopener noreferrer"&gt;guide&lt;/a&gt;, &lt;a href="https://lighthousetests.com/docs.html" rel="noopener noreferrer"&gt;docs&lt;/a&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Stack and solution layout
&lt;/h2&gt;

&lt;p&gt;Roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lighthouse.Tests.Suite.slnx
├── Domain          — steps, suite, UiElementNode (snapshots)
├── Application     — interfaces, script, portability helpers
├── Infrastructure  — FlaUI, Appium, UiAutomationWorker, persistence
├── App             — WPF + ViewModels (MVVM, CommunityToolkit)
├── Cli / Api       — headless runs
└── website/        — product site (Cloudflare Pages)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stack: &lt;strong&gt;.NET 10&lt;/strong&gt;, &lt;strong&gt;WPF&lt;/strong&gt;, &lt;strong&gt;FlaUI (UIA3)&lt;/strong&gt;, &lt;strong&gt;Appium&lt;/strong&gt; for Android, &lt;strong&gt;Velopack&lt;/strong&gt; for Setup, NLog, local HTTP API on loopback.&lt;/p&gt;

&lt;p&gt;Why layers: Domain does not reference FlaUI/WPF. Only immutable &lt;code&gt;UiElementNode&lt;/code&gt; / overlays live on the UI thread. All COM/UIA runs on a dedicated worker. A month later that saves you from wanting to burn the repo down.&lt;/p&gt;

&lt;p&gt;The user Workspace is three columns: test list · steps/script/Run · Inspector (map + Start Recording). Script in the center is Monaco. For releases there is also a CLI and &lt;code&gt;POST /v1/runs&lt;/code&gt; (one active run, SSE for progress) — so “run on the server” is not only a WPF button.&lt;/p&gt;




&lt;h2&gt;
  
  
  Windows UIA: threading, snapshots, locators
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Worker + message pump
&lt;/h3&gt;

&lt;p&gt;You cannot casually call FlaUI/COM from the WPF UI thread “however it goes.” In Lighthouse all access goes through &lt;code&gt;UiAutomationWorker&lt;/code&gt; (STA + message pump): &lt;code&gt;Invoke&lt;/code&gt; / &lt;code&gt;InvokeAsync&lt;/code&gt;. From the worker you must not synchronously call back into &lt;code&gt;Dispatcher.Invoke&lt;/code&gt; — classic deadlock path. Long tree walks call &lt;code&gt;PumpMessages()&lt;/code&gt; periodically, or UI Automation and the app start to choke.&lt;/p&gt;

&lt;p&gt;Product rule: &lt;strong&gt;never expose a live &lt;code&gt;AutomationElement&lt;/code&gt;.&lt;/strong&gt; ViewModels and the canvas get snapshots. Otherwise within a week you have a race: the tree is already dead, and WPF is still painting a “live” element.&lt;/p&gt;

&lt;h3&gt;
  
  
  Locator priority
&lt;/h3&gt;

&lt;p&gt;On playback the condition is built roughly like this (simplified):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;AutomationId&lt;/strong&gt; present → &lt;code&gt;ByAutomationId&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;else Name + ControlType;&lt;/li&gt;
&lt;li&gt;else Name;&lt;/li&gt;
&lt;li&gt;else ClassName / ControlType.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Same rule for the Windows path and for scoring selector “quality” in the inspector. Name is fine for humans or a Calculator demo; for release suites without ids, building repeatable tests gets painful fast.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recording: why not &lt;code&gt;TreeScope.Subtree&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The “logical” suggestion from a model or a textbook: on record start, hang text-change handlers on the whole tree (&lt;code&gt;TreeScope.Subtree&lt;/code&gt;). On a live UI that easily turns into multi-second freezes.&lt;/p&gt;

&lt;p&gt;Recording ended up closer to a practical minimum: hit-test on click / map + text capture around focus-leave, without heavy subtree listeners for the whole attach. Less magic — fewer freezes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Android and AutomationId portability
&lt;/h2&gt;

&lt;p&gt;Android path: Appium + UiAutomator2, screenshot on the canvas, recording from the map (plus optional live taps via adb &lt;code&gt;getevent&lt;/code&gt; and polling the focused EditText for EnterText when the session is alive).&lt;/p&gt;

&lt;p&gt;Main portability contract: &lt;strong&gt;if a step has an AutomationId, Android locates by id only&lt;/strong&gt;, with no Windows class/bounds fallthrough. Otherwise “move the test” becomes a geometry lottery.&lt;/p&gt;

&lt;p&gt;In the app under test it looks familiar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// MAUI&lt;/span&gt;
&lt;span class="n"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AutomationId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"login_button"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Flutter&lt;/span&gt;
&lt;span class="n"&gt;Semantics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;identifier:&lt;/span&gt; &lt;span class="s"&gt;'login_button'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tool has a portability check on import and a separate id probe on the connected device. Steps without an id and platform-only kinds (screenshot assert, Windows drag, Android Back/Swipe) are honestly marked as “will not migrate by themselves.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No iOS in beta.&lt;/strong&gt; I wrote the credo; iOS is still only in the plans. The problem is not really implementation — it is that I have nothing to verify it on (for now).&lt;/p&gt;




&lt;h2&gt;
  
  
  Everything around the inspector
&lt;/h2&gt;

&lt;p&gt;Without this, “run on every release” stays a slide:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JSON persistence&lt;/td&gt;
&lt;td&gt;suite under &lt;code&gt;%LocalAppData%&lt;/code&gt;, autosave&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLI&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;run&lt;/code&gt; against exe / Android package, report&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local API&lt;/td&gt;
&lt;td&gt;loopback, &lt;code&gt;POST /v1/runs&lt;/code&gt;, SSE progress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Velopack&lt;/td&gt;
&lt;td&gt;Setup + update feed (public URL, not private GitHub Releases)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Website&lt;/td&gt;
&lt;td&gt;guide, FAQ, changelog, download&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The inspector is the heart — most of the work goes through it. The rest is polish and attempts to make life easier for the user. That is how you quietly end up with six evenings instead of one Saturday night and “hello, Calculator.”&lt;/p&gt;




&lt;h2&gt;
  
  
  What I took away (not prescribing it to you)
&lt;/h2&gt;

&lt;p&gt;About Ranorex / TestComplete: they do their job, and our goals were different. Ours was to give QA a “record — run on release” button, not a second career of “living inside the combine harvester.” Sometimes an expensive combine is the right answer. Sometimes it is an elephant you send to fetch a bag from the shop.&lt;/p&gt;

&lt;p&gt;Next, ids. You can talk about “write once, play everywhere” all you like, but if the buttons have no AutomationId, you do not have cross-platform — you have two (or three) separate sets of fragile selectors under one pretty slide. After Xamarin, on MAUI that hurts especially: the UI is shared, and the tests somehow drift apart again. Ids are boring. Ids are also what the shared-test fairy tale dies without — usually on the second emulator.&lt;/p&gt;

&lt;p&gt;About vibecoding. Yes, the model ships fast. No, it does not remove your responsibility for a choice that is “logical on paper.” &lt;code&gt;TreeScope.Subtree&lt;/code&gt; on record start is a good example: sounds right; on a live tree it is heavy. AI already almost rules the world in slide decks; in code it still sometimes suggests something elegant but expensive in latency. Review was not cancelled. Not even in the evening. Especially in the evening.&lt;/p&gt;

&lt;p&gt;And last — a small thing from the “it costs time now, it costs more later” series. While Lighthouse lived only as a folder on disk, it was a pet project. When a site with a guide and a real URL appeared, it started to look like a product you can send a colleague without a half-hour explanation.&lt;/p&gt;




&lt;p&gt;How product look now:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwm9j9ajphbkci62b6zap.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwm9j9ajphbkci62b6zap.png" alt=" " width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkhm207ryys9gwwt9vw2e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkhm207ryys9gwwt9vw2e.png" alt=" " width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvnnmiz1oh12kcoqlyb0d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvnnmiz1oh12kcoqlyb0d.png" alt=" " width="800" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Plans for the future
&lt;/h2&gt;

&lt;p&gt;This is beta, so the roadmap is honest — not “everything at once,” but where I am looking next.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Platforms.&lt;/strong&gt; The main gap versus the credo is &lt;strong&gt;iOS&lt;/strong&gt;. Same idea: AutomationId / semantics identifier as logical identity, a separate backend next to Windows UIA and Android Appium. Until iOS ships, “everywhere” in the slogan is ahead of the fact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Android.&lt;/strong&gt; Today that is inspect + map + basic playback and pieces of live recording. I want it denser: more stable gesture/text capture, fewer surprises after on-device navigation, less manual Appium hassle for someone who only came to “record a smoke.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assertions and selectors.&lt;/strong&gt; &lt;code&gt;assert visible&lt;/code&gt; on an element with an id is fine for “the screen is there.” For calculators and forms you often need the next step: assert by &lt;strong&gt;value&lt;/strong&gt; / text (do not confuse Name in the preview with the locator). Also keep sharpening UX so the step preview always matches how playback actually finds the control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Release and trust.&lt;/strong&gt; Code-sign the Setup (SmartScreen is a typical unsigned-beta story). A calm public update feed. Post-beta: real payment / activation (Lemon Squeezy infra was already sketched; UI is not there yet) alongside the trial and offline LH1 keys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Headless / CI.&lt;/strong&gt; CLI and local API already exist; next is less friction for “drop in a suite → runs on the pipeline,” reports, and QA/dev wiring without mandatory clicking in WPF.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Product for QA.&lt;/strong&gt; Fewer empty states, clearer playback errors (“did not find #id” vs “found the wrong thing”), import/portability without a quest.&lt;br&gt;&lt;br&gt;
Separately — a &lt;strong&gt;friendly test editor&lt;/strong&gt;: not only Monaco/script and the step list “as recorded,” but a form where QA can edit click / text / assert / wait without feeling stuffed into a developer IDE. Same goal as at the start: record — tweak — run on release, without a second career inside the combine.&lt;/p&gt;

&lt;p&gt;I am deliberately not carving dates in stone. Evenings are finite; the priority is not to break what already works on Windows and Android.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;I started this not because I wanted another side project for the portfolio, but because nothing on the shelf matched what we wanted for QA after Xamarin → MAUI. In the evenings Lighthouse appeared: beta, no iOS — but with a real attach → record → run path on Windows and Android, a clear AutomationId contract, and a headless path for the server.&lt;/p&gt;

&lt;p&gt;If the models take over the planet tomorrow — at least their tests will have AutomationIds. Until then: &lt;a href="https://lighthousetests.com/" rel="noopener noreferrer"&gt;lighthousetests.com&lt;/a&gt;, and please — ids on the buttons.&lt;/p&gt;

&lt;p&gt;P.S. If you do not feel a sudden urge to kick the author, I would welcome constructive criticism in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>vibecoding</category>
      <category>testing</category>
      <category>wpf</category>
    </item>
  </channel>
</rss>
