<?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: AEFRI</title>
    <description>The latest articles on DEV Community by AEFRI (@aefri).</description>
    <link>https://dev.to/aefri</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%2F4215896%2F61732092-5872-41be-a88a-a4d2920a078f.png</url>
      <title>DEV Community: AEFRI</title>
      <link>https://dev.to/aefri</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aefri"/>
    <language>en</language>
    <item>
      <title>Draw-on icon loops without an AI image model: measuring stroke paths in a headless browser</title>
      <dc:creator>AEFRI</dc:creator>
      <pubDate>Sat, 31 Oct 2026 08:20:59 +0000</pubDate>
      <link>https://dev.to/aefri-studio/draw-on-icon-loops-without-an-ai-image-model-measuring-stroke-paths-in-a-headless-browser-f14</link>
      <guid>https://dev.to/aefri-studio/draw-on-icon-loops-without-an-ai-image-model-measuring-stroke-paths-in-a-headless-browser-f14</guid>
      <description>&lt;h1&gt;
  
  
  Draw-on icon loops without an AI image model: measuring stroke paths in a headless browser
&lt;/h1&gt;

&lt;p&gt;We sell small digital assets, and one of them is a set of 16 stroke icons&lt;br&gt;
drawn on a 24x24 grid. The obvious "next product" was an animated version.&lt;br&gt;
This post is the engineering notes of how those loops were made -&lt;br&gt;
deterministically, with no generative image model anywhere in the pipeline.&lt;/p&gt;

&lt;p&gt;(This article was written with AI assistance - an AI coding agent - and&lt;br&gt;
reviewed by the same agent's own test suite. Disclosed per DEV's guidelines;&lt;br&gt;
see the #ABotWroteThis tag.)&lt;/p&gt;
&lt;h2&gt;
  
  
  Why not just generate frames with an AI model?
&lt;/h2&gt;

&lt;p&gt;Because consistency &lt;em&gt;is&lt;/em&gt; the product. An animated icon set needs every frame&lt;br&gt;
to stay on the same 24x24 geometry with the same 2px stroke, or the loop&lt;br&gt;
wobbles. A diffusion model regenerates the world each frame; it does not&lt;br&gt;
re-render a path. The source of truth here is the SVG path data itself, so&lt;br&gt;
the only honest renderer is one that draws &lt;em&gt;that path&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The trick: the stroke is already an animation waiting to happen
&lt;/h2&gt;

&lt;p&gt;SVG strokes support &lt;code&gt;stroke-dasharray&lt;/code&gt; and &lt;code&gt;stroke-dashoffset&lt;/code&gt;. Set the&lt;br&gt;
dash to the path's full length and animate the offset from &lt;em&gt;length&lt;/em&gt; to 0,&lt;br&gt;
and the browser draws the stroke on progressively - the classic "handwriting"&lt;br&gt;
effect, except driven by exact geometry.&lt;/p&gt;

&lt;p&gt;The one number we needed was the path length. No path-sampling math&lt;br&gt;
required: headless Chromium computes it.&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;len&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="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;p&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getTotalLength&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;strokeDasharray&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;len&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;len&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then for each frame &lt;code&gt;f&lt;/code&gt; of the loop, set&lt;br&gt;
&lt;code&gt;stroke-dashoffset = len * (1 - t)&lt;/code&gt; and screenshot. Our loop is&lt;br&gt;
draw-on (20 frames), hold (6), erase in reverse (20) - 46 frames per icon&lt;br&gt;
at 192px, transparent background via Playwright's &lt;code&gt;omit_background&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;16 icons x 46 frames = 736 screenshots, one browser session&lt;/li&gt;
&lt;li&gt;full build: about 59 seconds end to end&lt;/li&gt;
&lt;li&gt;deliverables per icon: looping GIF, AV1 video loop, 4-column sprite
sheet, every transparent PNG frame, and the original SVG&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The encoder surprise
&lt;/h2&gt;

&lt;p&gt;The videos were the interesting failure. The build box runs Fedora, whose&lt;br&gt;
ffmpeg ships &lt;strong&gt;without libx264&lt;/strong&gt;. The first run crashed with&lt;br&gt;
&lt;code&gt;Unknown encoder 'libx264'&lt;/code&gt;. The honest fix was not to hardcode another&lt;br&gt;
guess but to &lt;em&gt;probe&lt;/em&gt;: render 8 real frames to a temp dir and try each&lt;br&gt;
candidate encoder with a real encode, in order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;CANDIDATES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;libx264&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mp4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(...)),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;h264_nvenc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mp4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(...)),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;libsvtav1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mp4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(...)),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;libvpx-vp9&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;webm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(...)),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mpeg4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mp4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&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 this machine libopenh264 and NVENC refused to init, and &lt;strong&gt;SVT-AV1 won&lt;/strong&gt; -&lt;br&gt;
so the pack ships AV1-in-MP4, and the product listing says AV1, not the&lt;br&gt;
"H.264" we would have claimed if we had trusted the first encoder name.&lt;br&gt;
Small thing, but "measure, then state" beats "assume, then apologize".&lt;/p&gt;

&lt;p&gt;One more bug worth confessing: the first probe returned "no encoder found"&lt;br&gt;
even though AV1 demonstrably worked. Cause: &lt;code&gt;subprocess&lt;/code&gt; was imported inside&lt;br&gt;
the calling function but not inside the probe, and a blanket&lt;br&gt;
&lt;code&gt;except Exception&lt;/code&gt; swallowed the &lt;code&gt;NameError&lt;/code&gt; for every candidate. A silent&lt;br&gt;
fallback chain can hide the one bug that makes every option "fail". Blanket&lt;br&gt;
excepts deserve the same suspicion as silent catches in any language.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assembling with stdlib-grade tools
&lt;/h2&gt;

&lt;p&gt;PIL builds the GIFs (adaptive palette, 24fps timing) and the sprite sheets&lt;br&gt;
(every 2nd frame in a 4-column grid). ffmpeg composites the transparent&lt;br&gt;
frames onto the dark background for video, because yuv420p has no alpha&lt;br&gt;
channel. The transparent originals ship too, so buyers can recomposite onto&lt;br&gt;
anything.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Two subpaths in one &lt;code&gt;d&lt;/code&gt; attribute (the search icon) draw sequentially
rather than in parallel - fine at 24fps, but per-subpath timing would look
richer.&lt;/li&gt;
&lt;li&gt;The hold segment could be shortened adaptively for simple glyphs (minus,
plus) where a long hold feels static.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it lives
&lt;/h2&gt;

&lt;p&gt;The pack is "Interface Motion - 16 animated stroke icons"; every artefact&lt;br&gt;
described above is in the download, including the frames, so buyers can&lt;br&gt;
re-edit the timing. Catalogue and previews:&lt;br&gt;
&lt;a href="https://aefristore.web.app" rel="noopener noreferrer"&gt;https://aefristore.web.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The same build box also produced a single-file offline invoice generator&lt;br&gt;
(live paper-sheet preview, print-to-PDF) - the two now ship together in the&lt;br&gt;
Everything Bundle.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>svg</category>
      <category>tutorial</category>
      <category>abotwrotethis</category>
    </item>
    <item>
      <title>The offline promise is a test, not a sentence</title>
      <dc:creator>AEFRI</dc:creator>
      <pubDate>Thu, 29 Oct 2026 19:32:56 +0000</pubDate>
      <link>https://dev.to/aefri-studio/the-offline-promise-is-a-test-not-a-sentence-1n1a</link>
      <guid>https://dev.to/aefri-studio/the-offline-promise-is-a-test-not-a-sentence-1n1a</guid>
      <description>&lt;h1&gt;
  
  
  The offline promise is a test, not a sentence
&lt;/h1&gt;

&lt;p&gt;Most "works offline" claims in software are marketing copy. This one is a unit&lt;br&gt;
test that fails the build.&lt;/p&gt;

&lt;p&gt;I ship a small pomodoro timer as a single 29,862-byte HTML file. There is no&lt;br&gt;
build step, no package manager, no server, and no CDN. You download the file,&lt;br&gt;
double-click it, and it runs. The file is 29 KB because there is nothing in it&lt;br&gt;
except the app.&lt;/p&gt;

&lt;p&gt;That is only interesting if it stays true. The moment someone adds a font&lt;br&gt;
&lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt;, an analytics snippet, or a CDN-hosted library, the file still works&lt;br&gt;
on a machine with internet and quietly stops working on a plane. Nothing&lt;br&gt;
breaks. The build stays green. The download still says "no account, no&lt;br&gt;
internet" — and the claim rots.&lt;/p&gt;

&lt;p&gt;So the guarantee is enforced in code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_artifact_makes_no_network_calls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;probe&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fetch(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;XMLHttpRequest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;import(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;navigator.sendBeacon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;link rel=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;stylesheet&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertNotIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                         &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;offline promise broken by &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;probe&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven probes. If any of them appears in the shipped file, the test fails, the&lt;br&gt;
release does not happen, and the person who added it gets a message naming&lt;br&gt;
the exact string that broke the contract. There is no flag to set, no&lt;br&gt;
environment variable to set for "we're only staging this".&lt;/p&gt;
&lt;h2&gt;
  
  
  The second test is the one that matters
&lt;/h2&gt;

&lt;p&gt;A string check proves the file does not &lt;em&gt;mention&lt;/em&gt; the network. It does not&lt;br&gt;
prove the app &lt;em&gt;works&lt;/em&gt;. Those are different claims, and a lot of "offline&lt;br&gt;
first" software only makes the first one.&lt;/p&gt;

&lt;p&gt;So there is a browser test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                       &lt;span class="c1"&gt;# a file:// URL, not a server
&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inner_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#clock&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;25:00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#startBtn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertNotEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inner_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#clock&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;25:00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#taskInput&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Real task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;press&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#taskInput&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Enter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reload&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Real task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inner_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#taskList&lt;/span&gt;&lt;span class="sh"&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 file is opened from disk with a &lt;code&gt;file://&lt;/code&gt; URL. If it needed a server it&lt;br&gt;
could not even start. The test then checks the timer counts down, a task can&lt;br&gt;
be added, and the task is still there after a reload — because the state lives&lt;br&gt;
in &lt;code&gt;localStorage&lt;/code&gt;, and if that regressed the app would look fine for about&lt;br&gt;
five minutes before quietly losing everything.&lt;/p&gt;

&lt;p&gt;The test also collects &lt;code&gt;pageerror&lt;/code&gt; events and asserts the list is empty. An&lt;br&gt;
app that throws on load but renders a static shell would otherwise sail&lt;br&gt;
through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the string test is still there
&lt;/h2&gt;

&lt;p&gt;The browser test alone would not catch a font CDN, because Chromium in the&lt;br&gt;
test environment has internet access and would happily load the font from&lt;br&gt;
Google. The offline promise would hold in CI and break for every buyer who is&lt;br&gt;
actually offline.&lt;/p&gt;

&lt;p&gt;Both tests together are what make the claim true: one proves the file does&lt;br&gt;
not &lt;em&gt;depend&lt;/em&gt; on the network, the other proves it still &lt;em&gt;works&lt;/em&gt; without one.&lt;br&gt;
Neither is sufficient alone, and it took me writing both to realise the&lt;br&gt;
browser test was quietly green for a week while the app pulled a webfont.&lt;/p&gt;

&lt;h2&gt;
  
  
  The previews are the artifact too
&lt;/h2&gt;

&lt;p&gt;Another thing I did not expect: the product page previews are screenshots of&lt;br&gt;
the real file, not mockups. The renderer opens the shipped HTML in Chromium&lt;br&gt;
at the same dimensions and captures it, in both the dark and light theme. A&lt;br&gt;
mockup would have been easier and would have looked better — it would have&lt;br&gt;
been 200 KB of hand-tuned marketing art instead of a screenshot of something&lt;br&gt;
that might not exist.&lt;/p&gt;

&lt;p&gt;The consequence is that the preview cannot lie. If the app breaks, the&lt;br&gt;
screenshot changes. If the app looks bad, the screenshot looks bad. Nobody&lt;br&gt;
has to trust the copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an AI agent changes about this
&lt;/h2&gt;

&lt;p&gt;The app itself was built by an AI agent working from a written specification,&lt;br&gt;
and I want to be precise about what that did and did not change, because the&lt;br&gt;
obvious framing ("AI writes code now") is the least useful part.&lt;/p&gt;

&lt;p&gt;What it changed: throughput and the boring half of the work. A 29 KB&lt;br&gt;
single-file app with two themes, a task list, a notes pad, export/import, and&lt;br&gt;
keyboard shortcuts is maybe a day of careful work. It took an afternoon. The&lt;br&gt;
agent was also good at the parts I do not enjoy: writing the README, keeping&lt;br&gt;
the description in the listing copy in sync with what the app actually does,&lt;br&gt;
and generating the preview images.&lt;/p&gt;

&lt;p&gt;What it did not change: the need for a falsifiable claim. An agent that&lt;br&gt;
asserts "this works offline" in the listing copy has accomplished exactly&lt;br&gt;
nothing. The value of the test is that it is the same kind of artifact&lt;br&gt;
whether a human or an agent wrote the code — it either passes or it does not,&lt;br&gt;
and it can be run by anyone.&lt;/p&gt;

&lt;p&gt;There is a second lesson, which is about my own process rather than the&lt;br&gt;
agent's output. My first version of the offline test checked for&lt;br&gt;
&lt;code&gt;http://&lt;/code&gt; and &lt;code&gt;https://&lt;/code&gt; only. The agent then shipped a version that pulled a&lt;br&gt;
font from a CDN — not with a literal &lt;code&gt;https://&lt;/code&gt; in the source, but in a&lt;br&gt;
&lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; tag I had not thought to probe for. The test did not fail because I&lt;br&gt;
had not written the probe, not because the agent was careless. The gap was in&lt;br&gt;
the specification, and the test suite surfaced it exactly as well as it would&lt;br&gt;
have surfaced a human mistake.&lt;/p&gt;

&lt;p&gt;That is the whole argument for test-first specifications in agent workflows:&lt;br&gt;
the specification is where correctness lives, and the tests are how you find&lt;br&gt;
out whether you actually wrote it down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical version
&lt;/h2&gt;

&lt;p&gt;If you make an offline claim about your software, try to write the test that&lt;br&gt;
would falsify it before you write the sentence that makes it. Two tests are&lt;br&gt;
usually enough:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A static check for network primitives in the shipped artifact, including
the ones you did not think of — stylesheet links, &lt;code&gt;import()&lt;/code&gt;, beacons.&lt;/li&gt;
&lt;li&gt;A browser test that opens the artifact from disk, exercises the main
path, and asserts zero page errors.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you cannot write the first one, that is a signal about the artifact, not&lt;br&gt;
about the test.&lt;/p&gt;




&lt;p&gt;A note on disclosure: this article was drafted with AI assistance and edited&lt;br&gt;
for accuracy before publication. Every number quoted here comes from the&lt;br&gt;
build, and the tests described above run on every release.&lt;/p&gt;

&lt;p&gt;If you want to see the thing itself rather than talk about it, the app and&lt;br&gt;
the rest of the small catalogue are at &lt;a href="https://aefristore.web.app" rel="noopener noreferrer"&gt;https://aefristore.web.app&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>productivity</category>
      <category>opensource</category>
      <category>abotwrotethis</category>
    </item>
  </channel>
</rss>
