<?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: faridislas</title>
    <description>The latest articles on DEV Community by faridislas (@faridislas).</description>
    <link>https://dev.to/faridislas</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%2F4009935%2F5659c37b-363b-4472-8fa3-f67ea194984f.png</url>
      <title>DEV Community: faridislas</title>
      <link>https://dev.to/faridislas</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/faridislas"/>
    <language>en</language>
    <item>
      <title>My landing page passed every CI check and was still broken on my customer's phone</title>
      <dc:creator>faridislas</dc:creator>
      <pubDate>Tue, 30 Jun 2026 18:36:02 +0000</pubDate>
      <link>https://dev.to/faridislas/my-landing-page-passed-every-ci-check-and-was-still-broken-on-my-customers-phone-9dp</link>
      <guid>https://dev.to/faridislas/my-landing-page-passed-every-ci-check-and-was-still-broken-on-my-customers-phone-9dp</guid>
      <description>&lt;p&gt;A customer texted me a screenshot last month.&lt;/p&gt;

&lt;p&gt;It was my own landing page, open on their Pixel. The headline — "Financial infrastructure to grow your revenue" — was clipped at "...grow your reven". The signup button below it was gray-on-slightly-lighter-gray, basically unreadable. And the hero image? A broken-image icon.&lt;/p&gt;

&lt;p&gt;Here's the part that stung: &lt;strong&gt;every check I had was green.&lt;/strong&gt; Lighthouse: 98. My Playwright tests: passing. CI: all checkmarks. I had shipped that page an hour earlier feeling good about it.&lt;/p&gt;

&lt;p&gt;None of my tooling caught any of it. I want to walk through &lt;em&gt;why&lt;/em&gt;, because I think a lot of us have this blind spot, and then I'll tell you what I did about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI tests the DOM. It does not test what a human sees.
&lt;/h2&gt;

&lt;p&gt;This is the core issue. My tests asserted things like "the signup button exists" and "the form has an email input." All true. The button was &lt;em&gt;in the DOM&lt;/em&gt;. It just rendered unreadable on a 412px-wide screen with the system in light mode.&lt;/p&gt;

&lt;p&gt;Lighthouse runs one viewport (usually a throttled Moto G4 emulation) and scores performance/SEO/a11y &lt;em&gt;heuristically&lt;/em&gt;. It does not look at your page across the actual range of devices your visitors use and say "this headline is physically clipped on a Pixel 8."&lt;/p&gt;

&lt;p&gt;And my "responsive testing"? I was dragging the Chrome devtools responsive bar to two breakpoints — 375 and 1440 — eyeballing it, and moving on. That's not testing. That's hoping.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three bugs that slipped through
&lt;/h2&gt;

&lt;p&gt;Let me get specific, because the &lt;em&gt;category&lt;/em&gt; of each bug is instructive.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The clipped headline — a measurable, deterministic bug
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.hero-title&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;white-space&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;nowrap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c"&gt;/* the culprit */&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On desktop, the headline fit. On a narrow viewport, &lt;code&gt;white-space: nowrap&lt;/code&gt; refused to wrap, &lt;code&gt;overflow: hidden&lt;/code&gt; clipped the overflow, and the last word vanished. The brutal thing: this is &lt;strong&gt;trivially detectable in code&lt;/strong&gt;. The element's &lt;code&gt;scrollWidth&lt;/code&gt; was greater than its &lt;code&gt;clientWidth&lt;/code&gt;. That's a one-line check:&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;clipped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollWidth&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientWidth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No AI needed. No human eyeball needed. The browser already knows the text doesn't fit. I just was never &lt;em&gt;asking it&lt;/em&gt;, on the viewport where it mattered.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The unreadable CTA — failing WCAG by math
&lt;/h3&gt;

&lt;p&gt;The button was &lt;code&gt;#9aa0a6&lt;/code&gt; text on a &lt;code&gt;#b0b4b8&lt;/code&gt; background. Run the WCAG contrast formula on that and you get a ratio around &lt;strong&gt;1.4:1&lt;/strong&gt;. The minimum for normal text is &lt;strong&gt;4.5:1&lt;/strong&gt;. It's not subjective — the text was, by definition, hard to read.&lt;/p&gt;

&lt;p&gt;Again: deterministic. You can compute the contrast ratio from &lt;code&gt;getComputedStyle&lt;/code&gt; without a single judgment call. I just wasn't computing it on a real render.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The 404 hero image — only on mobile
&lt;/h3&gt;

&lt;p&gt;This one was sneaky:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt;
  &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"/hero-desktop.jpg 1440w, /hero-mobile.jpg 768w"&lt;/span&gt;
  &lt;span class="na"&gt;sizes=&lt;/span&gt;&lt;span class="s"&gt;"(max-width: 768px) 100vw, 1440px"&lt;/span&gt;
  &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/hero-desktop.jpg"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;hero-mobile.jpg&lt;/code&gt; didn't exist — a deploy had dropped it. On desktop the browser picked &lt;code&gt;hero-desktop.jpg&lt;/code&gt; and everything looked fine. On mobile it picked the 768w candidate, got a 404, and rendered a broken-image box. My desktop devtools &lt;em&gt;never requested the broken file&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This shows up in the network tab as a plain &lt;code&gt;404&lt;/code&gt;. A runtime signal, sitting right there, that I wasn't watching on a mobile profile.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern: these bugs are &lt;em&gt;visible&lt;/em&gt;, not &lt;em&gt;logical&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;Notice what all three have in common. They're not logic bugs. The JavaScript ran fine. The data was correct. The bug lived entirely in &lt;strong&gt;what the page looked like on a specific screen&lt;/strong&gt; — and my entire test suite was built to verify behavior, not appearance.&lt;/p&gt;

&lt;p&gt;That's the gap. CI is great at "does the function return the right value." It is structurally blind to "is the headline physically cut off on the third-most-common phone my visitors use."&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually catches this
&lt;/h2&gt;

&lt;p&gt;The only reliable way I found is embarrassingly low-tech in concept: &lt;strong&gt;render the page in a real browser, at the actual device viewports, and look at each one&lt;/strong&gt; — but do the &lt;em&gt;looking&lt;/em&gt; programmatically so it scales past two breakpoints.&lt;/p&gt;

&lt;p&gt;Concretely, the approach that works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Real browser, multiple device profiles.&lt;/strong&gt; Playwright with device descriptors (iPhone 15 Pro, Pixel 8, iPad, etc.) gives you real viewport + DPR + user-agent. Full-page screenshot each one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Run the deterministic checks first.&lt;/strong&gt; Walk the DOM and flag the math-provable stuff: &lt;code&gt;scrollWidth &amp;gt; clientWidth&lt;/code&gt; for clipping, WCAG ratios for contrast, &lt;code&gt;404&lt;/code&gt;s from the network log for broken assets. These are &lt;em&gt;facts&lt;/em&gt;, not opinions — high confidence, zero hallucination risk.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Then, and only then, use vision for the fuzzy stuff.&lt;/strong&gt; Layout overlap, "this section looks empty," visual misalignment — things you genuinely can't measure with a selector. A vision model is good at this &lt;em&gt;if&lt;/em&gt; you constrain it. The trap I fell into: my first version confidently reported bugs that weren't there. The fix was a corroboration step — if a finding only showed up on one device out of many, downgrade it. Real visual bugs tend to appear across multiple similar viewports; one-off "findings" are usually capture artifacts or model noise. I'd rather under-report than cry wolf.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The ordering matters. Lead with what's provable, fall back to what's inferred, and be honest in the output about which is which.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest limits
&lt;/h2&gt;

&lt;p&gt;This approach is &lt;strong&gt;emulation&lt;/strong&gt;, not a physical-device lab. It will not catch hardware-specific GPU rendering quirks, vendor-modified browser builds, or the exact way one OLED panel handles a near-black gradient. If your bug only reproduces on a real Galaxy A14 running Samsung Internet, you need an actual device or a real-device cloud.&lt;/p&gt;

&lt;p&gt;But the vast majority of "broken on the customer's phone" bugs I ship — and I suspect you ship — are not those. They're clipped text, contrast, overflow, broken responsive images, CTAs below the fold. The boring, embarrassing, &lt;em&gt;visible&lt;/em&gt; stuff that CI waves through.&lt;/p&gt;

&lt;h2&gt;
  
  
  I built this into a tool
&lt;/h2&gt;

&lt;p&gt;I got tired of doing this by hand, so I packaged it: you paste a URL you own, it runs the page across up to 17 device profiles, runs the deterministic checks + the constrained vision pass, and gives you a severity-ranked report. It's called &lt;strong&gt;Canaryflux&lt;/strong&gt;. There's a live demo report you can poke at without signing up: &lt;a href="https://canaryflux.com/demo" rel="noopener noreferrer"&gt;canaryflux.com/demo&lt;/a&gt;. Free tier is 3 devices / 3 scans a month if you want to point it at your own page.&lt;/p&gt;

&lt;p&gt;But honestly — even if you never touch it, the lesson stands on its own and you can build a scrappy version in an afternoon with Playwright + a DOM walker:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Your CI tests what your page &lt;em&gt;does&lt;/em&gt;. Add something that tests what your page &lt;em&gt;looks like&lt;/em&gt; — on the screens your visitors actually use.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The customer screenshot was a gut-punch, but it taught me the most useful QA lesson I've learned in a while.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What's the worst "passed CI, broken on the actual device" bug you've shipped? I collect these — drop yours in the comments.&lt;/em&gt;&lt;/p&gt;

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