<?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: yusufcemres</title>
    <description>The latest articles on DEV Community by yusufcemres (@yusufcemres).</description>
    <link>https://dev.to/yusufcemres</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%2F4111563%2Ff9b7c66e-11cf-4f2b-b88d-54ee90570493.png</url>
      <title>DEV Community: yusufcemres</title>
      <link>https://dev.to/yusufcemres</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yusufcemres"/>
    <language>en</language>
    <item>
      <title>I pointed my UI-auditing tool at three sites it had never seen. All seven bugs it found were its own.</title>
      <dc:creator>yusufcemres</dc:creator>
      <pubDate>Sat, 05 Sep 2026 19:48:45 +0000</pubDate>
      <link>https://dev.to/yusufcemres/i-pointed-my-ui-auditing-tool-at-three-sites-it-had-never-seen-all-seven-bugs-it-found-were-its-12cb</link>
      <guid>https://dev.to/yusufcemres/i-pointed-my-ui-auditing-tool-at-three-sites-it-had-never-seen-all-seven-bugs-it-found-were-its-12cb</guid>
      <description>&lt;p&gt;The line at the bottom of the report said:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Records with automated findings: 8/4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eight out of four. More screens with findings than screens.&lt;/p&gt;

&lt;p&gt;I wrote this tool. It measures web UIs — contrast ratios against the real composited background, touch targets under 44px, controls that something else is painting over, text clipped by its own container. It has 142 tests. Every one of them passed while it printed that number.&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%2Fg6myz914dpklf7dnkipw.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%2Fg6myz914dpklf7dnkipw.png" alt="The panel: a desktop and a phone view of the same page side by side, with the measured findings listed beside them — contrast ratios to two decimal places, touch targets under 44px, counted per device" width="800" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is what was underneath it, and what the exercise taught me about the kind of bug tests are structurally bad at catching.&lt;/p&gt;

&lt;h2&gt;
  
  
  The counter was counting the wrong noun
&lt;/h2&gt;

&lt;p&gt;The fix took two minutes: the counter incremented once per finding &lt;em&gt;type&lt;/em&gt; while its label said &lt;em&gt;records&lt;/em&gt;. Two problems on one screen counted twice.&lt;/p&gt;

&lt;p&gt;The interesting part was one line below. The report also prints &lt;code&gt;automated checks clean&lt;/code&gt; when a screen has nothing wrong — and that decision was made by a &lt;strong&gt;completely separate list&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;anyFinding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;horizontalOverflow&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;smallTargets&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tinyText&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
  &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;invisibleText&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lowContrast&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buttonIssues&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
  &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
  &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coveredControls&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clippedText&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coveredByFixed&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A hand-maintained list of what counts as a finding. The engine had grown past it. So a screen whose only problem was one of the newer checks would print the finding — and then, directly underneath, say it was clean.&lt;/p&gt;

&lt;p&gt;Both come from one flag now, set by every branch that actually prints something. It cannot drift, because there is nothing left to drift from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then the same shape, three more times
&lt;/h2&gt;

&lt;p&gt;Once you have seen it, you start finding it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The touch-target check built its own label.&lt;/strong&gt; There is a &lt;code&gt;shortLabel(el)&lt;/code&gt; function whose whole job is "what text does a person read here". The touch-target check did not call it; it carried its own copy of the same idea, written earlier. That is why a &lt;code&gt;select&lt;/code&gt; element came back with newlines inside its label while every other finding had them collapsed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The contrast rule had its own icon-font list.&lt;/strong&gt; So did the label code. The contrast one knew about &lt;code&gt;lucide&lt;/code&gt; and &lt;code&gt;feather&lt;/code&gt;; the label one knew about class names like &lt;code&gt;material-symbols-outlined&lt;/code&gt;. Each missed exactly what the other caught.&lt;/p&gt;

&lt;p&gt;That last one produced my favourite finding of the round. Two of the three apps reported button labels like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;button 71x34 — "add Şarkı"
button 36x34 — "light_mode"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Icon fonts put the icon's &lt;em&gt;name&lt;/em&gt; in the text node and draw a glyph over it. So &lt;code&gt;innerText&lt;/code&gt; reads what nobody sees, and half the label's 45 characters go to something invisible. Labels skip icon elements now and keep the words around them — and for an icon-only button, where there is nothing left to read, they fall back to the accessible name. The theme toggle went from &lt;code&gt;light_mode&lt;/code&gt; to &lt;code&gt;Tema değiştir&lt;/code&gt;. The report ended up more useful than it was before the bug existed.&lt;/p&gt;

&lt;p&gt;Those labels appear all over the output. Run from the command line instead of the editor, the tool walks every device and both themes in one pass and writes a gallery — one card per screen, its own findings underneath it — so a bad label is a bad label in forty places at once:&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%2Fvbif7g0w1xy9b2zbivyo.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%2Fvbif7g0w1xy9b2zbivyo.png" alt="A one-shot audit: four cards, iPhone and Pixel in light and dark, each with its findings listed underneath — the element named, the measurement given" width="800" height="513"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why 142 tests caught none of it
&lt;/h2&gt;

&lt;p&gt;Every one of these is a bug in the &lt;em&gt;relationship between two pieces of code that agree with themselves&lt;/em&gt;. The counter agreed with itself. The &lt;code&gt;anyFinding&lt;/code&gt; list agreed with itself. Both label builders were individually correct.&lt;/p&gt;

&lt;p&gt;A unit test asks "does this function do what it says". None of these functions lied. What was wrong was that there were two of them.&lt;/p&gt;

&lt;p&gt;What caught them was running the tool on something it had not seen. Not more tests — &lt;em&gt;use&lt;/em&gt;. Three sites, an hour, seven defects, all mine.&lt;/p&gt;

&lt;p&gt;The tests I added afterwards have a different shape from the ones I had. They do not test functions; they compare two sides of the program against each other. One reads the finding types the engine produces and fails if any consumer does not print them. One reads the &lt;code&gt;npx&lt;/code&gt; commands out of every doc and refuses any that names a bin instead of a package — because the README's headline command, &lt;code&gt;npx -y uisight-mcp&lt;/code&gt;, had never worked. &lt;code&gt;uisight-mcp&lt;/code&gt; is a bin &lt;em&gt;inside&lt;/em&gt; the &lt;code&gt;uisight&lt;/code&gt; package, not a package of its own. Every new user hit a 404 on step one.&lt;/p&gt;

&lt;p&gt;That one had a second edge, worth saying out loud. The docs pointed at a name nobody owned on npm, so whoever published it would have got code execution on the machine of anyone following our own instructions. Fixing the text does not retire that, because the instruction lives in the git history and in every published tarball's README. The three names are taken now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The half a person found
&lt;/h2&gt;

&lt;p&gt;I could not have found the rest by running anything. Someone opened the panel and said the mobile screen looked wrong.&lt;/p&gt;

&lt;p&gt;It did. Frames are captured below 1:1 to keep their token cost down, and the card filled whatever width the side bar had. A 412px capture was being drawn at 792px. The blur is the lesser problem: &lt;strong&gt;a 44px touch target looks like 110px there&lt;/strong&gt;. The one view whose entire purpose is judging a phone layout was quietly lying about it.&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%2Fuxzoeyxjwpnrl0uxjj4h.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%2Fuxzoeyxjwpnrl0uxjj4h.png" alt="Before and after, same side bar and same capture: on the left the phone frame stretched to 792px, on the right capped at the device width of 412px" width="800" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then: "the refresh button reopens the old site." I had put a refresh glyph on the panel switcher, a few pixels above the panel's own refresh button, which reloads the page. Two identical controls, different actions — the exact thing this tool flags on other people's interfaces.&lt;/p&gt;

&lt;p&gt;Then: "there is nowhere to choose." I had hidden the switcher when only one panel was running — &lt;em&gt;a single panel needs no chooser&lt;/em&gt; — and the only way to notice a second panel was the rescan button, which lived on the bar that appeared only once a rescan had already found something. Someone who started a second panel could not reach it at all.&lt;/p&gt;

&lt;p&gt;That is twice in one day that I hid a control to keep things tidy and removed the only way back along with it. It is a written rule now rather than something I trust myself to remember: &lt;strong&gt;before hiding a control, ask how you return to that state without it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The other thing worth measuring
&lt;/h2&gt;

&lt;p&gt;This tool exists so an AI agent can measure a page instead of guessing from a screenshot, which makes its own token cost part of the product.&lt;/p&gt;

&lt;p&gt;An image costs roughly &lt;code&gt;width × height / 750&lt;/code&gt; tokens, and it is not paid once — it stays in the conversation and is re-sent on every later turn. So the cost falls with the &lt;em&gt;square&lt;/em&gt; of the scale. Cutting a frame to 0.75 costs about 44% of the tokens, and I compared four scales by eye before picking one, because "still readable" is not a thing you can assert from a number.&lt;/p&gt;

&lt;p&gt;Tool schemas are the other fixed cost: they go out with every request whether the tools get called or not. Measured while writing this: the full set is 9 tools at about 1,050 tokens; &lt;code&gt;UISIGHT_TOOLS=core&lt;/code&gt; is 4 tools at about 470. For a session that only needs to measure pages, that is more than half of a cost you pay on every single turn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things I would tell myself in the morning
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A passing test is not evidence the product is right.&lt;/strong&gt; It is evidence that one function matches one expectation. The bugs that survive live between things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it on something you did not write.&lt;/strong&gt; An hour on three unfamiliar sites found more than the previous day of writing tests. The unfamiliar part is load-bearing: on my own pages I unconsciously avoid the paths that break.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When you write the same idea twice, one copy will rot.&lt;/strong&gt; Every duplication I found had been correct on the day it was written. That is what makes it hard — you are not looking for a mistake, you are looking for an agreement that has since become a disagreement.&lt;/p&gt;




&lt;p&gt;The tool is &lt;a href="https://github.com/sololabstr/uisight" rel="noopener noreferrer"&gt;uisight&lt;/a&gt; — MIT, runs entirely locally, no account, nothing leaves your machine.&lt;/p&gt;

&lt;p&gt;It comes in three shapes, and they are the same engine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An editor extension.&lt;/strong&gt; The panel in the screenshots lives in your side bar — &lt;a href="https://marketplace.visualstudio.com/items?itemName=sololabstr.uisight" rel="noopener noreferrer"&gt;VS Code Marketplace&lt;/a&gt;, or &lt;a href="https://open-vsx.org/extension/sololabstr/uisight" rel="noopener noreferrer"&gt;Open VSX&lt;/a&gt; for Cursor, Windsurf, Antigravity and VSCodium.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An MCP server&lt;/strong&gt;, so your agent reads measurements as text instead of guessing from pixels:
&lt;code&gt;claude mcp add --scope user uisight -- npx -y -p uisight@latest uisight-mcp&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A CLI&lt;/strong&gt;, for the one-shot audit that produced the gallery above:
&lt;code&gt;npx uisight https://yourapp.com --theme both&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try it and a check fires when it should have stayed quiet, that is the report I want most. A tool that cries wolf on every bottom navigation bar gets ignored — and then its real findings go unread too.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>webdev</category>
      <category>mcp</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
