<?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: Arafat</title>
    <description>The latest articles on DEV Community by Arafat (@flinthive).</description>
    <link>https://dev.to/flinthive</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%2F3817845%2F1190d78e-9ae7-47fb-8f03-c0acb6486dac.png</url>
      <title>DEV Community: Arafat</title>
      <link>https://dev.to/flinthive</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/flinthive"/>
    <language>en</language>
    <item>
      <title>Half my test suite asserts that nothing happened</title>
      <dc:creator>Arafat</dc:creator>
      <pubDate>Mon, 31 Aug 2026 14:51:45 +0000</pubDate>
      <link>https://dev.to/flinthive/half-my-test-suite-asserts-that-nothing-happened-2g2o</link>
      <guid>https://dev.to/flinthive/half-my-test-suite-asserts-that-nothing-happened-2g2o</guid>
      <description>&lt;p&gt;Last month I shipped a bug where every test passed.&lt;/p&gt;

&lt;p&gt;The extension I maintain fills web forms with test data. It has two modes: use a profile you saved yourself, or generate fresh data. A user picks "Generated test data", the extension fills the form — and quietly uses their saved profile instead.&lt;/p&gt;

&lt;p&gt;Not a crash. Not an error in the console. The form filled. Every field had a value in it. The only thing wrong was that the values were the wrong ones, and nothing anywhere said so.&lt;/p&gt;

&lt;p&gt;Every test in the suite was green.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it passed
&lt;/h2&gt;

&lt;p&gt;The bug was in a fallback. The code resolved which profile to use, and when nothing matched it fell back to the first item in the list. The built-in "generated data" option isn't a saved profile, so it never matched, so it always fell through to the fallback. Once you saved a single profile, you could never choose generated data again.&lt;/p&gt;

&lt;p&gt;Here is the part worth sitting with: the fallback did exactly what a fallback is supposed to do. It picked something reasonable instead of failing. And every test I had written asked the same kind of question — &lt;em&gt;did the form get filled?&lt;/em&gt; Yes. &lt;em&gt;Do the fields have values?&lt;/em&gt; Yes. &lt;em&gt;Is the data valid?&lt;/em&gt; Yes.&lt;/p&gt;

&lt;p&gt;Not one test asked &lt;em&gt;is this the data the user actually asked for?&lt;/em&gt; And no test at all asked the harder version: &lt;em&gt;when the user picks the built-in option, does the code correctly do nothing with the saved profiles?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That is the whole category. I had a suite full of "did it work" tests and nothing that said "did it correctly do nothing".&lt;/p&gt;

&lt;h2&gt;
  
  
  The tests nobody writes
&lt;/h2&gt;

&lt;p&gt;Once you start looking for this, it is everywhere. Test suites are written as a description of what software should do, because that is how features are specified, planned and reviewed. "Should not" is a different question, and it does not turn up just because the "should" list got longer.&lt;/p&gt;

&lt;p&gt;For a tool that writes into forms, the "should not" list is long and it matters more than the other one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A CAPTCHA field must never be filled. Filling it is worse than doing nothing, because it looks like an answer.&lt;/li&gt;
&lt;li&gt;A hidden CSRF token must never be touched.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;disabled&lt;/code&gt; and &lt;code&gt;readonly&lt;/code&gt; fields stay as they are.&lt;/li&gt;
&lt;li&gt;File inputs stay empty.&lt;/li&gt;
&lt;li&gt;Anything invisible stays untouched.&lt;/li&gt;
&lt;li&gt;A form in a cross-origin iframe is not filled, and cannot be — reaching into another site's frame would need standing access to that site.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A filler that fails to fill a field is annoying. A filler that writes into a hidden token field and breaks the submit is worse, and the user will not know why. The failures on that list are all silent, which is exactly why they need explicit tests: nothing else will surface them.&lt;/p&gt;

&lt;p&gt;So now, before a feature gets written, I write the refusal cases as fixtures. The question I ask about a plan is not only "is this right" but "what should this refuse to do, and what happens if it is asked to anyway". The second pass finds things the first one never surfaces. Every time.&lt;/p&gt;

&lt;p&gt;At the time of writing, in September 2026, the suite graded 84 cases and 13 of them asserted refusal rather than success. Those 13 are the ones I would keep if I had to throw the rest away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mocks were part of the problem
&lt;/h2&gt;

&lt;p&gt;The other half of the fix was where the tests run.&lt;/p&gt;

&lt;p&gt;My original suite tested the field classifier and the fill engine against mock DOM objects. That is fast and it is easy to write, and it is also how the entitlement bug survived: the mocks reflected what I believed the DOM did, and the belief was the thing that was wrong.&lt;/p&gt;

&lt;p&gt;A browser extension has a particularly bad case of this. The engine is injected into a page it has never seen, in a browser build I do not control, against a framework that patches native DOM behaviour underneath it. React, to take the most common example, keeps its own record of what is in each input. If something writes to &lt;code&gt;input.value&lt;/code&gt; directly, React compares the DOM against that record, sees no difference, and never fires &lt;code&gt;onChange&lt;/code&gt;. The text is on screen. The application never received it. (I wrote about that specific mechanism &lt;a href="https://dev.to/flinthive/why-inputvalue-x-doesnt-fill-a-react-form-and-what-actually-works-1hl7"&gt;in more detail here&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;None of that is visible to a mock. A mock will happily tell you the value was set.&lt;/p&gt;

&lt;p&gt;So the suite got rebuilt as a real page. It loads the same four files the service worker injects — not a copy, not a test build, the actual engine — and runs them against a page full of deliberately awkward fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React, Vue and Angular controlled inputs&lt;/li&gt;
&lt;li&gt;open shadow roots, including nested ones, and custom elements&lt;/li&gt;
&lt;li&gt;fields classifiable only by an &lt;code&gt;autocomplete&lt;/code&gt; token&lt;/li&gt;
&lt;li&gt;fields with no useful signal at all — no label, no placeholder, no name&lt;/li&gt;
&lt;li&gt;field names containing dots and dashes that break naive selector matching&lt;/li&gt;
&lt;li&gt;same-origin iframes&lt;/li&gt;
&lt;li&gt;and the refusal cases above&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;73 fixtures, 84 graded cases. That run, in September 2026: &lt;strong&gt;71 passed, 0 failed, 13 correctly skipped.&lt;/strong&gt; The current score is not this one — it is whatever the suite reports when you run it yourself, inside the extension or at &lt;a href="https://flinthive.com/form-filler-test" rel="noopener noreferrer"&gt;flinthive.com/form-filler-test&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I shipped the suite inside the product
&lt;/h2&gt;

&lt;p&gt;This was the part I did not expect to be useful, and it turned out to be the most useful.&lt;/p&gt;

&lt;p&gt;The self-test is not a CI artefact. It is a page inside the extension. Anyone who installs it can open it and run the real engine in their own browser, on their own build, and see the score.&lt;/p&gt;

&lt;p&gt;Two reasons that matters.&lt;/p&gt;

&lt;p&gt;The first is honest self-interest: browser updates break extensions. I test on my machine, on my Chrome version, in my OS. A user three versions ahead on a platform I have never touched will find failures I cannot reproduce and probably cannot even imagine. Now they can run the suite and send me a report instead of an uninstall.&lt;/p&gt;

&lt;p&gt;The second reason is about trust, and it is the one I would repeat to anyone building a tool that touches other people's pages. My store listing makes a set of claims: it handles React, it handles shadow DOM, it leaves CAPTCHA fields alone, it makes no network requests. Anyone can write that. Screenshots prove nothing — I made the screenshot. A test suite the user runs themselves is the only version of that claim they do not have to take my word for.&lt;/p&gt;

&lt;p&gt;"Know it works in your browser" turned out to be a better thing to offer than any feature on the list.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it still does not catch
&lt;/h2&gt;

&lt;p&gt;I want to be straight about the limits, because a scoreboard is easy to over-trust.&lt;/p&gt;

&lt;p&gt;The bug that cost me the most money this year was not in the extension at all. A payment webhook fired on a completed transaction, and the payload did not always contain the buyer's email address — sometimes it carried only a customer id. The code was correct. The tests were correct. They were written against the payload shape in the documentation, and the documentation showed the field.&lt;/p&gt;

&lt;p&gt;I found it on a real purchase, which is a bad way to find it.&lt;/p&gt;

&lt;p&gt;No fixture would have caught that, because the fixture would have used the documented shape too. The only thing that catches it is logging raw payloads from live traffic and looking at what actually arrives. Fixtures test your code against your understanding. Where the understanding is wrong, they agree with you.&lt;/p&gt;

&lt;p&gt;So: fixtures for what you control, real payloads for what you do not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell myself a year ago
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Write the refusal cases as fixtures, before the feature.&lt;/strong&gt; Ask "what should this refuse to do" as a separate question from "is this plan right". It surfaces different answers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the real code in the real runtime.&lt;/strong&gt; Mocks encode your assumptions, and your assumptions are what is broken.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A green suite is evidence about the questions you asked&lt;/strong&gt;, not about the software. Mine was fully green while a headline feature was silently broken.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consider shipping the suite.&lt;/strong&gt; If your tool runs inside somebody else's environment, letting them verify it there is worth more than another screenshot.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;I build &lt;a href="https://chromewebstore.google.com/detail/alkelkiiomnmjiajjcecjeicmhfhconb" rel="noopener noreferrer"&gt;FormForge&lt;/a&gt;, an offline form filler for QA and development work. The self-test page described here ships with it — free tier included, no account needed. If you run it and something fails, I would genuinely like to see the report: &lt;a href="mailto:support@flinthive.com"&gt;support@flinthive.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>react</category>
    </item>
    <item>
      <title>Why input.value = 'x' doesn't fill a React form (and what actually works)</title>
      <dc:creator>Arafat</dc:creator>
      <pubDate>Thu, 20 Aug 2026 04:13:45 +0000</pubDate>
      <link>https://dev.to/flinthive/why-inputvalue-x-doesnt-fill-a-react-form-and-what-actually-works-1hl7</link>
      <guid>https://dev.to/flinthive/why-inputvalue-x-doesnt-fill-a-react-form-and-what-actually-works-1hl7</guid>
      <description>&lt;p&gt;If you have ever tried to script a React form — a test helper, a browser extension, a bookmarklet, an onboarding demo — you have probably hit this:&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The field shows the text. It looks filled. Then you submit, and React sends an empty string. Or the "Next" button stays disabled. Or the value vanishes the moment anything else on the page re-renders.&lt;/p&gt;

&lt;p&gt;I ran into this repeatedly while building a form-filling extension, and the fix is not obvious from React's docs. Here is what is actually happening.&lt;/p&gt;

&lt;h2&gt;
  
  
  React keeps its own copy of the value
&lt;/h2&gt;

&lt;p&gt;React attaches a &lt;em&gt;value tracker&lt;/em&gt; to every controlled input. It is an internal object stored on the DOM node under a property React owns, and its only job is to remember what React last saw in that field.&lt;/p&gt;

&lt;p&gt;When a real user types, two things happen:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The browser updates &lt;code&gt;input.value&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The browser fires an &lt;code&gt;input&lt;/code&gt; event&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;React's synthetic event system catches that event, compares the node's current value against the tracker's remembered value, sees they differ, and &lt;em&gt;therefore&lt;/em&gt; concludes something changed and calls your &lt;code&gt;onChange&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That comparison is the whole story. React does not ask "did the value change?" — it asks "does the DOM disagree with what I remember?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Why direct assignment breaks it
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;HTMLInputElement.prototype.value&lt;/code&gt; is an accessor property with a setter. React replaces that setter on the individual node with its own version, one that updates the tracker as it writes.&lt;/p&gt;

&lt;p&gt;So when you do &lt;code&gt;input.value = 'x'&lt;/code&gt;, you go through React's patched setter. It writes the value &lt;strong&gt;and&lt;/strong&gt; updates the tracker to match. The DOM and the tracker now agree.&lt;/p&gt;

&lt;p&gt;Then you dispatch an &lt;code&gt;input&lt;/code&gt; event, React compares the two, finds them identical, and concludes nothing changed. &lt;code&gt;onChange&lt;/code&gt; never fires. React's state still holds the old value, and the next render wipes your text away.&lt;/p&gt;

&lt;p&gt;You did not fail to notify React. You notified React and React decided you were lying.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: bypass the patched setter
&lt;/h2&gt;

&lt;p&gt;You need to write the value &lt;em&gt;without&lt;/em&gt; going through React's setter, so the tracker stays stale and the comparison detects a difference:&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;function&lt;/span&gt; &lt;span class="nf"&gt;setNativeValue&lt;/span&gt;&lt;span class="p"&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;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;HTMLTextAreaElement&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;HTMLTextAreaElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;setter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOwnPropertyDescriptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;proto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;value&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;setter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&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;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatchEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;bubbles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;&lt;code&gt;Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set&lt;/code&gt; is the browser's original setter, untouched. Calling it with &lt;code&gt;.call(el, value)&lt;/code&gt; writes straight to the DOM, leaving React's tracker holding the old value. Now the comparison fails, React believes you, and &lt;code&gt;onChange&lt;/code&gt; fires.&lt;/p&gt;

&lt;p&gt;Note the prototype check. &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt; has its own prototype with its own value setter, and using the input one on a textarea throws.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three more things that bite
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; needs &lt;code&gt;change&lt;/code&gt;, not &lt;code&gt;input&lt;/code&gt;.&lt;/strong&gt; Select elements notify on &lt;code&gt;change&lt;/code&gt;. Dispatch &lt;code&gt;input&lt;/code&gt; at a &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; and nothing happens. Checkboxes and radios need &lt;code&gt;click()&lt;/code&gt; or a &lt;code&gt;change&lt;/code&gt; event, not a value write at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Some libraries want the full keyboard sequence.&lt;/strong&gt; A handful of masked-input and autocomplete components listen for &lt;code&gt;keydown&lt;/code&gt;/&lt;code&gt;keyup&lt;/code&gt; rather than &lt;code&gt;input&lt;/code&gt;. If the native-setter approach alone does not stick, dispatching &lt;code&gt;keydown&lt;/code&gt;, &lt;code&gt;input&lt;/code&gt;, &lt;code&gt;keyup&lt;/code&gt; in order usually does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vue 3 is easier, Angular is different.&lt;/strong&gt; Vue's &lt;code&gt;v-model&lt;/code&gt; listens for plain &lt;code&gt;input&lt;/code&gt; events, so the ordinary path works. Angular's &lt;code&gt;ControlValueAccessor&lt;/code&gt; also listens for &lt;code&gt;input&lt;/code&gt; on most controls, but reactive forms sometimes need a &lt;code&gt;blur&lt;/code&gt; afterwards to run validators — otherwise the field is filled and still marked untouched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the write
&lt;/h2&gt;

&lt;p&gt;This is the part I would most encourage you to steal, because it changed how much time I spent debugging.&lt;/p&gt;

&lt;p&gt;After writing a value, read it back:&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="nf"&gt;setNativeValue&lt;/span&gt;&lt;span class="p"&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;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// let the framework's microtask queue drain&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&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;value&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// the framework rejected or transformed the input&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the value came back different, the page's framework rejected it — a mask reformatted it, a validator cleared it, a controlled component overwrote it on re-render. That is a completely different failure from "the selector did not match", and it needs a completely different fix.&lt;/p&gt;

&lt;p&gt;Reporting those two cases separately turned my most common bug report from "it doesn't work" into "your app rejected this input, here is what it became" — which is actionable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shadow DOM: the other half of the problem
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;document.querySelectorAll('input')&lt;/code&gt; does not see inside shadow roots. A lot of modern component libraries put their real &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; inside one, so a script that works fine on a plain HTML page finds nothing at all on a component-library page.&lt;/p&gt;

&lt;p&gt;Open shadow roots you can walk:&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;function&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;deepInputs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input, textarea, select&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&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;shadowRoot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;deepInputs&lt;/span&gt;&lt;span class="p"&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;shadowRoot&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// nested roots too&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;Shadow roots nest, so this has to recurse — a component inside a component inside a component is common, and a single-level check misses it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Closed shadow roots you cannot reach, at all.&lt;/strong&gt; &lt;code&gt;element.shadowRoot&lt;/code&gt; returns &lt;code&gt;null&lt;/code&gt; when the root was created with &lt;code&gt;{ mode: 'closed' }&lt;/code&gt;, and there is no workaround from page or extension context. This is by design. The honest thing is to detect the likely case and say so rather than silently filling nothing: a custom element with no light-DOM children and no accessible &lt;code&gt;shadowRoot&lt;/code&gt; is probably a closed root, and telling the user that is far better than a silent no-op.&lt;/p&gt;

&lt;p&gt;Same-origin iframes work through &lt;code&gt;iframe.contentDocument&lt;/code&gt;. Cross-origin iframes do not, and no amount of cleverness changes that — you need an explicit host permission for that origin, which is a real cost to weigh rather than something to engineer around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not fill everything you find
&lt;/h2&gt;

&lt;p&gt;The last thing, and the one I got wrong first: a script that fills every field it can find is worse than one that fills nothing.&lt;/p&gt;

&lt;p&gt;Skip these, always:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;disabled&lt;/code&gt; and &lt;code&gt;readonly&lt;/code&gt; fields&lt;/li&gt;
&lt;li&gt;Anything hidden — &lt;code&gt;type="hidden"&lt;/code&gt;, &lt;code&gt;display: none&lt;/code&gt;, zero-size, &lt;code&gt;visibility: hidden&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSRF tokens&lt;/strong&gt; — usually a hidden input with &lt;code&gt;token&lt;/code&gt;, &lt;code&gt;csrf&lt;/code&gt; or &lt;code&gt;authenticity&lt;/code&gt; in the name. Overwriting one breaks the submit in a way that is genuinely hard to diagnose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CAPTCHA fields&lt;/strong&gt; — never touch them&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;input type="file"&amp;gt;&lt;/code&gt; — you cannot set it programmatically for good security reasons, and trying throws&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I ended up writing more test fixtures for the &lt;em&gt;must-not-fill&lt;/em&gt; cases than for the fill cases, and that ratio turned out to be right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test it against adversarial fixtures, not happy paths
&lt;/h2&gt;

&lt;p&gt;Every one of the failures above is invisible on a simple form and obvious on a real one. So the fixtures worth writing are the nasty ones: a React controlled input, an open shadow root, a &lt;em&gt;nested&lt;/em&gt; shadow root, a same-origin iframe, a field whose only signal is an &lt;code&gt;autocomplete&lt;/code&gt; token, a field with no signals at all, an input with dots in its &lt;code&gt;name&lt;/code&gt;, a CAPTCHA, a hidden CSRF token.&lt;/p&gt;

&lt;p&gt;When I wrote this, in August 2026, mine ran 56 fixtures and graded 75 cases — the extras are suite-level invariants like "one report row per discovered field" and "widget selections are deterministic". That run: 65 passed, 0 failed, 10 correctly skipped, where the 10 skips are the guard cases that &lt;em&gt;should&lt;/em&gt; be refused. The suite has grown since, so treat those as a snapshot rather than a current score — the current one is whatever it reports when you run it yourself, in the extension or at &lt;a href="https://flinthive.com/form-filler-test" rel="noopener noreferrer"&gt;flinthive.com/form-filler-test&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%2Fl60njtn8n9oohzyy0jil.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%2Fl60njtn8n9oohzyy0jil.png" alt="FormForge self-test report showing 65 passed, 0 failed, 10 correctly skipped" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The number that matters there is the 10, not the 65. Anyone can make a suite go green by only testing what already works; refusing to fill a CAPTCHA is the harder assertion.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;I packaged all of this into &lt;strong&gt;FormForge&lt;/strong&gt;, a Chrome extension that fills forms with realistic test data. It ships the self-test page described above, so you can run all 56 fixtures in your own browser build rather than taking my word for any of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chrome Web Store:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://chromewebstore.google.com/detail/alkelkiiomnmjiajjcecjeicmhfhconb" rel="noopener noreferrer"&gt;https://chromewebstore.google.com/detail/alkelkiiomnmjiajjcecjeicmhfhconb&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More detail and pricing:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://flinthive.com/formforge" rel="noopener noreferrer"&gt;https://flinthive.com/formforge&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Free tier has no daily limit, no account and no card. It runs entirely locally — no server, no telemetry, no AI, and zero standing host permissions.&lt;/p&gt;

&lt;p&gt;But the native-setter trick above is the useful part whether or not you ever install anything. It is the single fix that solves the most common form-scripting bug in React, and it took me far too long to find.&lt;/p&gt;

&lt;p&gt;If you know a form that breaks any of this, I would genuinely like to hear about it.&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>testing</category>
    </item>
    <item>
      <title>I built 125+ free calculators with React + TanStack Router on Cloudflare Pages — here's what I learned</title>
      <dc:creator>Arafat</dc:creator>
      <pubDate>Mon, 06 Jul 2026 08:33:43 +0000</pubDate>
      <link>https://dev.to/flinthive/i-built-125-free-calculators-with-react-tanstack-router-on-cloudflare-pages-heres-what-i-21lj</link>
      <guid>https://dev.to/flinthive/i-built-125-free-calculators-with-react-tanstack-router-on-cloudflare-pages-heres-what-i-21lj</guid>
      <description>&lt;p&gt;A few months ago I set out to build something simple: a collection of free, useful calculators that anyone could use without signing up or dealing with ads.&lt;/p&gt;

&lt;p&gt;That turned into &lt;strong&gt;freefixo.com&lt;/strong&gt; — 125+ calculators covering finance, health, legal, and travel. Here's the stack I used and what surprised me along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React&lt;/strong&gt; (Vite) — component-per-calculator approach kept things modular&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TanStack Router&lt;/strong&gt; — file-based routing made adding new calculators trivially easy. Each new tool = one new file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Pages&lt;/strong&gt; — free tier, global CDN, deploys in ~30 seconds from a &lt;code&gt;git push&lt;/code&gt;. Zero config for a CSR SPA.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS&lt;/strong&gt; — rapid styling without a separate stylesheet per component&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No backend. No database. Everything runs client-side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why TanStack Router over React Router?
&lt;/h2&gt;

&lt;p&gt;I wanted type-safe routes from day one. TanStack Router's file-based routing + full TypeScript inference meant I never had a mistyped route string again. The tradeoff: slightly steeper learning curve for the search param handling, but worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloudflare Pages for a CSR SPA
&lt;/h2&gt;

&lt;p&gt;One gotcha: Cloudflare Pages serves a 404 for direct URL hits on client-side routes unless you add a &lt;code&gt;_redirects&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="err"&gt;/*&lt;/span&gt; &lt;span class="err"&gt;/&lt;/span&gt;index.html 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one line fixes SPA routing entirely. Took me embarrassingly long to figure out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling to 125+ calculators without losing my mind
&lt;/h2&gt;

&lt;p&gt;The key was a consistent pattern: every calculator is a self-contained React component that receives no props from outside — all state is local. This meant I could build, test, and ship each one independently without touching anything else.&lt;/p&gt;

&lt;p&gt;I also built a simple JSON config file that drives the homepage grid, search, and category filters. Adding a new calculator = add the component + one line in the config.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add meta tags earlier.&lt;/strong&gt; I retrofitted per-page &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;description&amp;gt;&lt;/code&gt; tags after launch. Should've done this from calculator #1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build the search first.&lt;/strong&gt; Users immediately want to search. I built it as an afterthought.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't wait for perfection.&lt;/strong&gt; I had 40 calculators ready weeks before I launched. Ship earlier, iterate publicly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;Live at &lt;strong&gt;&lt;a href="https://freefixo.com" rel="noopener noreferrer"&gt;https://freefixo.com&lt;/a&gt;&lt;/strong&gt; — no signup, no ads, just tools.&lt;/p&gt;

&lt;p&gt;Would love feedback from the DEV community, especially on the tech choices. Anyone else using TanStack Router in production? How are you handling SEO for CSR apps on Cloudflare Pages?&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I Built a Free All-in-One Tool Website with 70+ Tools — Here's the Story</title>
      <dc:creator>Arafat</dc:creator>
      <pubDate>Wed, 11 Mar 2026 05:50:32 +0000</pubDate>
      <link>https://dev.to/flinthive/i-built-a-free-all-in-one-tool-website-with-70-tools-heres-the-story-jej</link>
      <guid>https://dev.to/flinthive/i-built-a-free-all-in-one-tool-website-with-70-tools-heres-the-story-jej</guid>
      <description>&lt;h2&gt;
  
  
  Why I Built FixFlowHub
&lt;/h2&gt;

&lt;p&gt;I was constantly frustrated switching between different websites &lt;br&gt;
for simple tasks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compress an image → TinyPNG&lt;/li&gt;
&lt;li&gt;Merge a PDF → SmallPDF (with watermark 😤)&lt;/li&gt;
&lt;li&gt;Check grammar → Grammarly (paid)&lt;/li&gt;
&lt;li&gt;Generate a QR code → some random sketchy site&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most tools either require sign-up, add watermarks, or lock &lt;br&gt;
basic features behind a paywall.&lt;/p&gt;

&lt;p&gt;So I decided to build &lt;strong&gt;one place where everything is actually free&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://fixflowhub.com" rel="noopener noreferrer"&gt;FixFlowHub&lt;/a&gt;&lt;/strong&gt; — a free all-in-one &lt;br&gt;
online toolbox with 70+ tools across 5 categories.&lt;/p&gt;

&lt;h3&gt;
  
  
  🖼️ Image Tools (23 tools)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Compress images without quality loss&lt;/li&gt;
&lt;li&gt;Convert between JPG, PNG, WebP, HEIC, AVIF, SVG, BMP, TIFF, ICO&lt;/li&gt;
&lt;li&gt;Resize, crop, rotate, flip, watermark&lt;/li&gt;
&lt;li&gt;Grayscale converter, image editor&lt;/li&gt;
&lt;li&gt;Image to Base64 encoder&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  📄 PDF Tools (8 tools)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Merge multiple PDFs into one&lt;/li&gt;
&lt;li&gt;Split PDF into pages&lt;/li&gt;
&lt;li&gt;Compress PDF size&lt;/li&gt;
&lt;li&gt;Convert PDF to image &amp;amp; image to PDF&lt;/li&gt;
&lt;li&gt;Lock/unlock PDF with password&lt;/li&gt;
&lt;li&gt;Reorder PDF pages&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🤖 AI Text Tools (11 tools)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Resume builder&lt;/li&gt;
&lt;li&gt;Cover letter generator&lt;/li&gt;
&lt;li&gt;Grammar checker&lt;/li&gt;
&lt;li&gt;Text rewriter &amp;amp; paraphraser&lt;/li&gt;
&lt;li&gt;Email writer&lt;/li&gt;
&lt;li&gt;LinkedIn profile generator&lt;/li&gt;
&lt;li&gt;YouTube SEO generator&lt;/li&gt;
&lt;li&gt;SEO tag generator&lt;/li&gt;
&lt;li&gt;AI text summarizer&lt;/li&gt;
&lt;li&gt;Business slogan generator&lt;/li&gt;
&lt;li&gt;Product description generator&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🧮 Calculator Tools (15 tools)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Loan EMI calculator&lt;/li&gt;
&lt;li&gt;BMI calculator&lt;/li&gt;
&lt;li&gt;Age calculator&lt;/li&gt;
&lt;li&gt;Compound interest calculator&lt;/li&gt;
&lt;li&gt;GPA calculator&lt;/li&gt;
&lt;li&gt;Calorie calculator&lt;/li&gt;
&lt;li&gt;Pregnancy due date calculator&lt;/li&gt;
&lt;li&gt;Currency converter&lt;/li&gt;
&lt;li&gt;Salary calculator&lt;/li&gt;
&lt;li&gt;And more...&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🛠️ Utility Tools (10 tools)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;QR code generator&lt;/li&gt;
&lt;li&gt;Password generator&lt;/li&gt;
&lt;li&gt;JSON formatter &amp;amp; validator&lt;/li&gt;
&lt;li&gt;Lorem ipsum generator&lt;/li&gt;
&lt;li&gt;Color picker (HEX, RGB, HSL)&lt;/li&gt;
&lt;li&gt;Time zone converter&lt;/li&gt;
&lt;li&gt;Case converter&lt;/li&gt;
&lt;li&gt;Text cleaner&lt;/li&gt;
&lt;li&gt;HTML entities encoder&lt;/li&gt;
&lt;li&gt;YouTube thumbnail downloader&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; React + Vite&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Styling:&lt;/strong&gt; Tailwind CSS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image Processing:&lt;/strong&gt; HTML5 Canvas API (client-side)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF Processing:&lt;/strong&gt; PDF.js&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Tools:&lt;/strong&gt; Anthropic Claude API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosting:&lt;/strong&gt; Lovable + Custom domain&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Key Design Decisions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Privacy First:&lt;/strong&gt; All image and PDF processing happens &lt;br&gt;
in the browser. Files are never uploaded to any server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No Sign-Up:&lt;/strong&gt; I wanted zero friction. Open the tool, &lt;br&gt;
use it, done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No Watermarks:&lt;/strong&gt; Every tool is fully functional for free.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Building is the easy part&lt;/strong&gt; — getting traffic is hard 😅&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SEO takes time&lt;/strong&gt; — don't expect results overnight&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;People appreciate simplicity&lt;/strong&gt; — no popups, no forced 
sign-ups, just working tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free tools attract users&lt;/strong&gt; — but monetization needs thought&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Try It Out
&lt;/h2&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://fixflowhub.com" rel="noopener noreferrer"&gt;fixflowhub.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Would love your feedback! What tools should I add next? &lt;br&gt;
Drop a comment below 👇&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>productivity</category>
      <category>tools</category>
    </item>
  </channel>
</rss>
