<?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: xiuhai shi</title>
    <description>The latest articles on DEV Community by xiuhai shi (@xiuhai_shi_a30442601c2bec).</description>
    <link>https://dev.to/xiuhai_shi_a30442601c2bec</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%2F4208852%2F0488e756-214e-492a-90f2-b9a722ea4f27.png</url>
      <title>DEV Community: xiuhai shi</title>
      <link>https://dev.to/xiuhai_shi_a30442601c2bec</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/xiuhai_shi_a30442601c2bec"/>
    <language>en</language>
    <item>
      <title>How we test 102 browser-only tools: unit, property, oracle, and real-browser tests</title>
      <dc:creator>xiuhai shi</dc:creator>
      <pubDate>Mon, 26 Oct 2026 13:44:20 +0000</pubDate>
      <link>https://dev.to/xiuhai_shi_a30442601c2bec/how-we-test-102-browser-only-tools-unit-property-oracle-and-real-browser-tests-5en2</link>
      <guid>https://dev.to/xiuhai_shi_a30442601c2bec/how-we-test-102-browser-only-tools-unit-property-oracle-and-real-browser-tests-5en2</guid>
      <description>&lt;p&gt;A static tool site has no server to catch mistakes. If the Base64 decoder is wrong,&lt;br&gt;
there is no API error, no 500, no server log — just a user who quietly leaves. On&lt;br&gt;
&lt;a href="https://yizhiweb.top/" rel="noopener noreferrer"&gt;yizhiweb.top&lt;/a&gt; the correctness of the engine &lt;em&gt;is&lt;/em&gt; the product,&lt;br&gt;
so the test suite has to play the role a backend usually plays.&lt;/p&gt;

&lt;p&gt;The site is 102 tools and 162 pages, built with Astro as static files. Every engine&lt;br&gt;
is pure TypeScript with no runtime dependencies. This is the four-layer test setup&lt;br&gt;
we use, the bugs each layer actually caught, and what it still does not cover.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: unit tests
&lt;/h2&gt;

&lt;p&gt;Each engine is a pure module: input in, output out, no DOM. The test runner bundles&lt;br&gt;
the engine barrel with esbuild and runs it in Node. That keeps the fast loop fast —&lt;br&gt;
no browser, no build of the site.&lt;/p&gt;

&lt;p&gt;Typical cases are boring on purpose: empty strings, one character, the boundary just&lt;br&gt;
below and just above a limit, and the exact error message for invalid input. Boring&lt;br&gt;
tests are cheap and they catch the majority of regressions when an engine is edited.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: property and fuzz tests
&lt;/h2&gt;

&lt;p&gt;Examples prove the cases you thought of. Property tests look for the cases you did&lt;br&gt;
not.&lt;/p&gt;

&lt;p&gt;For every codec we assert a round-trip: &lt;code&gt;decode(encode(x)) === x&lt;/code&gt; for generated&lt;br&gt;
inputs, including empty input, maximum-length input, non-BMP Unicode, and malformed&lt;br&gt;
byte sequences. For hashing and formatting we assert invariants: output length,&lt;br&gt;
character set, stable formatting, and no &lt;code&gt;NaN&lt;/code&gt;/&lt;code&gt;undefined&lt;/code&gt; leaking into user-facing&lt;br&gt;
strings.&lt;/p&gt;

&lt;p&gt;The fuzz layer runs tens of thousands of generated cases. It is not a proof, but it&lt;br&gt;
is very good at finding crashes and silent corruption — the two failure modes that&lt;br&gt;
matter most for a local tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: oracle tests
&lt;/h2&gt;

&lt;p&gt;This is the layer that gives the most confidence per line of test code: compare the&lt;br&gt;
implementation against an independent reference.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hashing / HMAC&lt;/strong&gt; — compare against Node's &lt;code&gt;crypto&lt;/code&gt; for the same input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AES-256-GCM encryption&lt;/strong&gt; — encrypt in the browser engine, decrypt with Node's
crypto; then the reverse. The envelope is self-describing, so the test also checks
tamper rejection and wrong-passphrase rejection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Punycode&lt;/strong&gt; — compare against Node's built-in &lt;code&gt;punycode&lt;/code&gt; implementation,
including non-BMP characters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TOTP&lt;/strong&gt; — the six RFC 6238 test vectors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subnet maths&lt;/strong&gt; — a second, deliberately naive implementation that manipulates
the address as a 32-character bit string, compared with the production version
that uses integer arithmetic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Financial and date maths&lt;/strong&gt; — closed-form values and hand-computed boundary
cases (leap years, ISO week 53, month-end rollovers).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point is not that the reference is perfect. The point is that the two&lt;br&gt;
implementations fail differently. When they disagree, one of them is probably wrong,&lt;br&gt;
and the test tells you exactly where.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 4: real-browser E2E, plus a coverage gate
&lt;/h2&gt;

&lt;p&gt;The last layer runs Chrome against the built site. It clicks every control on every&lt;br&gt;
tool page, types into inputs, toggles options, checks empty and invalid states, and&lt;br&gt;
verifies that Copy buttons copy what they claim to copy.&lt;/p&gt;

&lt;p&gt;That suite is currently more than 1,500 assertions. It also enforces a simple rule:&lt;br&gt;
&lt;strong&gt;every control and every Copy button must be covered by a test spec.&lt;/strong&gt; If a new&lt;br&gt;
button appears without a test, the coverage gate fails. This is the only way we have&lt;br&gt;
found to stop the test suite from drifting behind the UI as tools are added.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four bugs the layers caught
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. A diff engine that could exhaust memory.&lt;/strong&gt; The text-diff page had a comment&lt;br&gt;
claiming the algorithm was capped. It was not. A 20,000-line comparison took about&lt;br&gt;
7 seconds and 1.5 GB; 50,000 lines was roughly 10 GB and would crash the tab. The fix&lt;br&gt;
was to trim the common prefix and suffix first, and fall back to a linear comparison&lt;br&gt;
above a hard limit. The same case dropped from 7,023 ms to about 10 ms. The page now&lt;br&gt;
reports when the result was linearised, and the FAQ says so instead of pretending the&lt;br&gt;
result is always a minimal diff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A Copy button that lied.&lt;/strong&gt; The button showed "Copied!" before checking whether&lt;br&gt;
the Clipboard API actually succeeded. On browsers where &lt;code&gt;navigator.clipboard&lt;/code&gt; is&lt;br&gt;
missing or permission is denied, it reported success and copied nothing. The fix&lt;br&gt;
wraps &lt;code&gt;writeText&lt;/code&gt; with a feature check, adds a hidden-textarea fallback, and only&lt;br&gt;
shows success after the write resolves. The E2E suite now tests the normal path, the&lt;br&gt;
missing-API path, and the rejected-permission path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Emoji counted as seven characters.&lt;/strong&gt; The text counter used &lt;code&gt;String.length&lt;/code&gt;, so a&lt;br&gt;
family emoji built from a ZWJ sequence was counted as seven. For a text tool, that is&lt;br&gt;
a real correctness bug. The counter now reports both code units and user-perceived&lt;br&gt;
characters using &lt;code&gt;Intl.Segmenter&lt;/code&gt;, and the page explains the difference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Small lies in formatting and file detection.&lt;/strong&gt; &lt;code&gt;formatNumber(-0)&lt;/code&gt; returned&lt;br&gt;
&lt;code&gt;"-0"&lt;/code&gt;; the EAN-13 renderer ignored the required quiet zone; CSV export did not&lt;br&gt;
escape headers containing commas; an image format fallback reported the requested&lt;br&gt;
format instead of the actual output MIME type. None of these would crash anything.&lt;br&gt;
All of them would make a user distrust the tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not cover
&lt;/h2&gt;

&lt;p&gt;Tests cannot tell you whether a tool is useful, whether the copy is clear, or whether&lt;br&gt;
the page answers the query someone typed into Google. They also cannot prove the&lt;br&gt;
absence of a bug — they only make the common failure modes expensive to reintroduce.&lt;/p&gt;

&lt;p&gt;Two practical limits are worth stating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Browser APIs vary.&lt;/strong&gt; Clipboard, canvas, file readers and &lt;code&gt;Intl&lt;/code&gt; differ across
browsers and versions. The E2E suite runs one Chrome build; it is not a browser
matrix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heavy dependencies are lazy-loaded.&lt;/strong&gt; Image processing and other large modules
sit outside the base page bundle. That keeps the first load small, but it means the
E2E suite must wait for the lazy module before asserting — a real source of flaky
tests if not handled explicitly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The trade-off
&lt;/h2&gt;

&lt;p&gt;A static, browser-only tool site gives up server-side validation, per-user logs and&lt;br&gt;
easy A/B testing. In exchange it gets a strong privacy story: input never leaves the&lt;br&gt;
device, and there is no account or backend to breach. For a certain class of tools —&lt;br&gt;
encoding, hashing, formatting, image conversion, calculators — that trade is worth&lt;br&gt;
it. The test suite is what makes the trade defensible.&lt;/p&gt;

&lt;p&gt;The full method, including the limits above, is published at&lt;br&gt;
&lt;a href="https://yizhiweb.top/methodology/" rel="noopener noreferrer"&gt;yizhiweb.top/methodology&lt;/a&gt;. The tools are at&lt;br&gt;
&lt;a href="https://yizhiweb.top/" rel="noopener noreferrer"&gt;yizhiweb.top&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I build and maintain yizhiweb.top. The article describes its test&lt;br&gt;
setup; it is not a paid placement.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>privacy</category>
    </item>
  </channel>
</rss>
