<?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: Danilo Lapegna</title>
    <description>The latest articles on DEV Community by Danilo Lapegna (@danilolapegna).</description>
    <link>https://dev.to/danilolapegna</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%2F3907090%2Fa9c37645-db5b-4e9a-a2a3-eab007419455.jpg</url>
      <title>DEV Community: Danilo Lapegna</title>
      <link>https://dev.to/danilolapegna</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/danilolapegna"/>
    <language>en</language>
    <item>
      <title>A passing smoke test can still miss your broken page</title>
      <dc:creator>Danilo Lapegna</dc:creator>
      <pubDate>Mon, 05 Oct 2026 13:00:54 +0000</pubDate>
      <link>https://dev.to/danilolapegna/a-passing-smoke-test-can-still-miss-your-broken-page-5332</link>
      <guid>https://dev.to/danilolapegna/a-passing-smoke-test-can-still-miss-your-broken-page-5332</guid>
      <description>&lt;p&gt;A shop's home page opens. The About page opens. The smoke test passes. Meanwhile, &lt;code&gt;/orders&lt;/code&gt; shows an error screen, and the test never went there.&lt;/p&gt;

&lt;p&gt;That's the small example we'll build here: the same site, the same broken page, two test runs with different route lists. You'll see why a passing result needs a second question: &lt;strong&gt;did the test visit the pages that matter?&lt;/strong&gt; Start by writing down one critical URL and what should appear there.&lt;/p&gt;

&lt;p&gt;I wrote &lt;a href="https://github.com/danilolapegna/deep-smoke" rel="noopener noreferrer"&gt;deep-smoke&lt;/a&gt; to open web-app pages in a browser and check for visible failures, including empty screens, error screens and broken assets. A smoke test is a quick check that the basics still work. It saves time, but the pages it finds depend on the links and starting URLs you give it. That limit matters just as much as the things it detects.&lt;/p&gt;

&lt;p&gt;You can follow this example if you use Git, npm and a terminal. Use Node.js 20 or later for the current Playwright release. We'll use a tiny local site with no customer data, accounts or AI service. Playwright, the library that drives the test browser, needs an installed Chromium browser; the installation below downloads it. The software is free, though the download takes disk space and network access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the page you would hate to lose
&lt;/h2&gt;

&lt;p&gt;Before installing anything, write down one critical URL and what someone should see there. For an orders page, that might be the order list for a signed-in customer and a sign-in page for someone without a session. Those are two different checks.&lt;/p&gt;

&lt;p&gt;For a small, one-off change, opening those URLs manually may be enough for your immediate review. If you keep making changes to shared navigation or page layout, a repeatable browser check becomes useful. Either way, you need to decide which pages and user states count. A crawler cannot infer that an unlinked screen is essential to your business.&lt;/p&gt;

&lt;p&gt;Our demonstration leaves out sign-in deliberately. It isolates one problem: a broken page that has no link from the home page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a site with an easy-to-miss failure
&lt;/h2&gt;

&lt;p&gt;For a reproducible run, use this specific version of the public repository. As of 5 October 2026, the package name &lt;code&gt;deep-smoke&lt;/code&gt; is not available on the public npm registry, so these instructions run it from source.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/danilolapegna/deep-smoke.git
&lt;span class="nb"&gt;cd &lt;/span&gt;deep-smoke
git checkout a73bf08efbf8c561670cb58db8eecf47ddd36e10
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-save&lt;/span&gt; &lt;span class="nt"&gt;--ignore-scripts&lt;/span&gt; playwright
npx playwright &lt;span class="nb"&gt;install &lt;/span&gt;chromium
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside that directory, create &lt;code&gt;demo-server.mjs&lt;/code&gt; with this content:&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:http&lt;/span&gt;&lt;span class="dl"&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;pages&lt;/span&gt; &lt;span class="o"&gt;=&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;h1&amp;gt;Demo shop&amp;lt;/h1&amp;gt;&amp;lt;a href="/about"&amp;gt;About&amp;lt;/a&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/about&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;h1&amp;gt;About&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;A small test shop.&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;main data-error-boundary&amp;gt;&amp;lt;h1&amp;gt;Orders unavailable&amp;lt;/h1&amp;gt;&amp;lt;/main&amp;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="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pages&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&amp;lt;!doctype html&amp;gt;&amp;lt;html&amp;gt;&amp;lt;body&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;h1&amp;gt;Not found&amp;lt;/h1&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4173&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;127.0.0.1&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Demo: http://127.0.0.1:4173&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 home page links to &lt;code&gt;/about&lt;/code&gt;, but neither page links to &lt;code&gt;/orders&lt;/code&gt;. The &lt;code&gt;data-error-boundary&lt;/code&gt; attribute marks an error fallback: the screen shown when the intended page cannot render. deep-smoke treats that marker as a failure even though the server returns HTTP 200, which only says the request succeeded.&lt;/p&gt;

&lt;p&gt;Start the site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node demo-server.mjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Leave that terminal running. Open &lt;code&gt;http://127.0.0.1:4173/orders&lt;/code&gt; in your browser to see the failure we planted. If the server reports &lt;code&gt;EADDRINUSE&lt;/code&gt;, port 4173 is already occupied. Stop here and use a free local port in both the server and the configurations below; don't stop an unrelated process to make room.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch the incomplete test pass
&lt;/h2&gt;

&lt;p&gt;In the same directory, create &lt;code&gt;demo.config.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"baseUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://127.0.0.1:4173"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"routes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &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="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a second terminal, also inside the repository directory, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node bin/deep-smoke.js &lt;span class="nt"&gt;--level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3 &lt;span class="nt"&gt;--config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;demo.config.json &lt;span class="nt"&gt;--evidence&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;demo-before.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Level 3 follows links from the starting routes. In this site, it visits &lt;code&gt;/&lt;/code&gt; and &lt;code&gt;/about&lt;/code&gt;. The tested run reports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;routes      2 visited, 0 failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The process exits with code 0, meaning this run passed. It also writes its evidence to &lt;code&gt;demo-before.json&lt;/code&gt;: the routes visited, the configured checks and each route's result.&lt;/p&gt;

&lt;p&gt;Open that JSON file and look at &lt;code&gt;results&lt;/code&gt;. &lt;code&gt;/orders&lt;/code&gt; is absent. &lt;strong&gt;A page missing from the report has not passed; it has not been tested.&lt;/strong&gt; The crawler has done what we asked, and what we asked was incomplete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add the missing route, without fixing the site
&lt;/h2&gt;

&lt;p&gt;Now change only &lt;code&gt;routes&lt;/code&gt; in &lt;code&gt;demo.config.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"baseUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://127.0.0.1:4173"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"routes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &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="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/orders"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the same check and save a separate report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node bin/deep-smoke.js &lt;span class="nt"&gt;--level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3 &lt;span class="nt"&gt;--config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;demo.config.json &lt;span class="nt"&gt;--evidence&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;demo-after.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time the tested run visits three routes and fails on one. The relevant output is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;routes      3 visited, 1 failed

Failing routes (1):
  x [anonymous] /orders
      error-boundary: error fallback rendered instead of the page
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It exits with code 1. That's the expected result: the test finally saw the failure. The site's code stayed identical between the two runs. We changed the coverage, not the defect.&lt;/p&gt;

&lt;p&gt;If you still see a passing result, check the config file named in the command, the URL and the route list in the new evidence file. Don't disable the error-boundary check to make this example pass. Seeing the deliberate failure is the point.&lt;/p&gt;

&lt;p&gt;When you're finished, press Ctrl+C in the server terminal. The demo files and reports stay in your local clone; the original repository is unchanged.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I want a green result to tell me
&lt;/h2&gt;

&lt;p&gt;For a real release, I want a passing result attached to an explicit list of important pages and user states. &lt;strong&gt;The route list is part of the test.&lt;/strong&gt; Reviewing it is engineering work, particularly when an assistant has added a screen that never made it into the navigation.&lt;/p&gt;

&lt;p&gt;There are limits to that evidence. This demo tests only a visitor without a session. It doesn't check whether submitting an order works, whether the totals are correct or whether the backend enforces permissions. A page can render perfectly and still get all of those wrong. Those behaviors need their own tests.&lt;/p&gt;

&lt;p&gt;deep-smoke supports configured signed-in test users (personas) and checks for expected redirects, but a redirect check is not an authorization audit. Likewise, a crawl stopped by its route or time budget cannot support a claim that it visited the whole intended surface. Keep those distinctions visible when reading the report.&lt;/p&gt;

&lt;p&gt;Your next move can be small: take one critical URL from your own application, add it explicitly, run against a test environment using the production build, then confirm that the report contains the expected page and user state. If either is missing, repair the test coverage before using its result to approve a release. Use synthetic data; keep real customer sessions out of a shared example.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/danilolapegna/deep-smoke/tree/a73bf08efbf8c561670cb58db8eecf47ddd36e10" rel="noopener noreferrer"&gt;source and assertions in deep-smoke&lt;/a&gt; are public and MIT-licensed. If you find a healthy page it rejects, a reproducible example is useful: it gives us something concrete to test and fix.&lt;/p&gt;

&lt;p&gt;I'm &lt;a href="https://danilolapegna.com/ai-automation" rel="noopener noreferrer"&gt;Danilo Lapegna, founder of DL Solutions&lt;/a&gt;. I build AI automations in Amsterdam and work internationally. This is one of the release checks I publish alongside that work.&lt;/p&gt;

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