<?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: Maciek Chmura</title>
    <description>The latest articles on DEV Community by Maciek Chmura (@maciek_chmura_building).</description>
    <link>https://dev.to/maciek_chmura_building</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%2F1818282%2Fe0de005f-16fc-424d-897d-b1c3c284d533.png</url>
      <title>DEV Community: Maciek Chmura</title>
      <link>https://dev.to/maciek_chmura_building</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/maciek_chmura_building"/>
    <language>en</language>
    <item>
      <title>How to test a web app built with Claude Code or Cursor before you ship</title>
      <dc:creator>Maciek Chmura</dc:creator>
      <pubDate>Tue, 03 Nov 2026 09:16:21 +0000</pubDate>
      <link>https://dev.to/maciek_chmura_building/how-to-test-a-web-app-built-with-claude-code-or-cursor-before-you-ship-1cip</link>
      <guid>https://dev.to/maciek_chmura_building/how-to-test-a-web-app-built-with-claude-code-or-cursor-before-you-ship-1cip</guid>
      <description>&lt;p&gt;Your coding agent says the feature is done. The build is green. Then a user tells you they can't log in.&lt;/p&gt;

&lt;p&gt;This happens a lot with apps built in Claude Code or Cursor. The agent checks that the code compiles and that the tests it knows about pass. It doesn't check that a real person can still sign up, log in and pay. That part is still your job.&lt;/p&gt;

&lt;p&gt;Here's the short answer: &lt;strong&gt;pick the few flows that must never break, make the agent run fast checks before it stops, and run those flows in a real browser against a preview deploy before every release.&lt;/strong&gt; The rest of this post is the step-by-step version I use.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Write down your critical flows first
&lt;/h2&gt;

&lt;p&gt;Before you write any test, write a list. Not every page. Only the journeys a customer would notice first if they broke.&lt;/p&gt;

&lt;p&gt;For most SaaS apps that's something like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sign up with a new email&lt;/li&gt;
&lt;li&gt;Log in and log out&lt;/li&gt;
&lt;li&gt;Reset a password&lt;/li&gt;
&lt;li&gt;The main "create" action (a project, a post, an order)&lt;/li&gt;
&lt;li&gt;Checkout or upgrade to a paid plan&lt;/li&gt;
&lt;li&gt;One settings change people actually use&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep it under ten. Put it in a file in the repo, like &lt;code&gt;CRITICAL_FLOWS.md&lt;/code&gt;, so both you and the agent can read it. A one-line description per flow is enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Critical flows&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Sign up: new email -&amp;gt; confirm -&amp;gt; lands on empty dashboard
&lt;span class="p"&gt;2.&lt;/span&gt; Log in: existing user -&amp;gt; dashboard shows their projects
&lt;span class="p"&gt;3.&lt;/span&gt; Create project: name it -&amp;gt; it appears in the list after reload
&lt;span class="p"&gt;4.&lt;/span&gt; Upgrade: pick Pro -&amp;gt; test card -&amp;gt; plan shows "Pro" in settings
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "after reload" part matters. A lot of agent bugs only show when the data has to come back from the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Tell the agent about the list, and about your test command
&lt;/h2&gt;

&lt;p&gt;Agents follow rules files: &lt;code&gt;CLAUDE.md&lt;/code&gt; for Claude Code, &lt;code&gt;.cursor/rules&lt;/code&gt; or &lt;code&gt;AGENTS.md&lt;/code&gt; for Cursor. Add two things:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Before you finish a task&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Run &lt;span class="sb"&gt;`npm run typecheck &amp;amp;&amp;amp; npm test`&lt;/span&gt; and fix anything that fails.
&lt;span class="p"&gt;-&lt;/span&gt; If you touched auth, billing or the dashboard, list which items in
  CRITICAL_FLOWS.md your change could affect.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second line is cheap and useful. It makes the agent tell you the blast radius of its change, so you know what to check.&lt;/p&gt;

&lt;p&gt;But rules are advice. The agent can still skip them.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Make the fast checks a gate the agent can't skip
&lt;/h2&gt;

&lt;p&gt;Claude Code has hooks, which run a command at fixed points. A &lt;code&gt;Stop&lt;/code&gt; hook runs when the agent is about to finish. If the command exits with code 2, Claude Code keeps the agent working and shows it the error.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;.claude/settings.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;"hooks"&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;span class="nl"&gt;"Stop"&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;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&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;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run typecheck 1&amp;gt;&amp;amp;2 &amp;amp;&amp;amp; npm test --silent 1&amp;gt;&amp;amp;2 || exit 2"&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;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;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;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;Keep this command fast. Typecheck and unit tests, maybe your access-control tests. If it takes five minutes on every turn, you'll turn it off. Browser tests belong later in the pipeline.&lt;/p&gt;

&lt;p&gt;Cursor has its own hooks feature too. Check its docs for the current format, since it has been changing.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Deploy every change to a preview URL
&lt;/h2&gt;

&lt;p&gt;You can't test the app a user sees from inside the repo. You need it running.&lt;/p&gt;

&lt;p&gt;Vercel, Netlify and Cloudflare Pages all build a unique preview URL for each branch or pull request. Turn that on. Now every change the agent makes has a real address you can open, with nothing mocked.&lt;/p&gt;

&lt;p&gt;Use a separate test database or seeded test accounts for previews. You don't want a signup test creating real users in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Run the critical flows in a real browser
&lt;/h2&gt;

&lt;p&gt;This is the step that catches the things unit tests miss: a button whose &lt;code&gt;onClick&lt;/code&gt; was dropped, a form that submits but never saves, a redirect loop after login.&lt;/p&gt;

&lt;p&gt;If you write code-based tests, Playwright is a good default. Here is the login flow from the list above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&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;@playwright/test&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;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PREVIEW_URL&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:3000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;existing user can log in and sees their projects&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/login`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&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="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TEST_USER_EMAIL&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Password&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TEST_USER_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Log in&lt;/span&gt;&lt;span class="dl"&gt;'&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&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="nf"&gt;toHaveURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;dashboard/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&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="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;My first project&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&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;Two habits make these tests worth having:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Assert an outcome, not a page load.&lt;/strong&gt; "The project is in the list" is a check. "The dashboard opened" is barely one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make sure the test can fail.&lt;/strong&gt; Break the page on purpose once, and confirm the test goes red. A test that passes on a broken page is worse than no test.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use role and label selectors (&lt;code&gt;getByRole&lt;/code&gt;, &lt;code&gt;getByLabel&lt;/code&gt;) rather than CSS classes. Agents rename classes all the time. They change visible button text much less often.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Check the API, not just the screen
&lt;/h2&gt;

&lt;p&gt;Some bugs look fine in the browser. The API returns &lt;code&gt;200 OK&lt;/code&gt;, the page shows a spinner and then nothing, and the real problem is a wrong field in the JSON.&lt;/p&gt;

&lt;p&gt;For the endpoints behind your critical flows, check more than the status code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;projects API returns the user&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;s projects&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&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;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/projects`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TEST_USER_TOKEN&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="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBe&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;toHaveProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also try one request as the wrong user. Agent-written backends often forget to check that a record belongs to the person asking for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Wire it into CI and block the merge
&lt;/h2&gt;

&lt;p&gt;Run the same checks on every pull request, whoever or whatever wrote it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checks&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pull_request&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run typecheck &amp;amp;&amp;amp; npm test&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx playwright install --with-deps chromium&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx playwright test&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;PREVIEW_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.PREVIEW_URL }}&lt;/span&gt;
          &lt;span class="na"&gt;TEST_USER_EMAIL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.TEST_USER_EMAIL }}&lt;/span&gt;
          &lt;span class="na"&gt;TEST_USER_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.TEST_USER_PASSWORD }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Getting the preview URL for each PR depends on your host. Most of them post a deployment status you can wait for, then read the URL from. Mark the job as required in your branch protection settings so a red run actually blocks the merge.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Feed failures back to the agent
&lt;/h2&gt;

&lt;p&gt;When a browser check fails, don't just tell the agent "a test is red." Give it the flow, the step that failed, and what the page showed. Paste the Playwright error and the trace or screenshot.&lt;/p&gt;

&lt;p&gt;A message like "After clicking Log in, the page stayed on /login and showed 'Invalid session'" gets you a much better fix than a stack trace alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Still do a quick manual pass before a release
&lt;/h2&gt;

&lt;p&gt;Tests only check what someone wrote down. Before a bigger release, click through your critical flows yourself once, on a phone and in Safari. Try a payment failure in test mode. It takes ten minutes and catches the things nobody thought to automate.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;List your critical flows.&lt;/li&gt;
&lt;li&gt;Put the list and your test command in the agent's rules file.&lt;/li&gt;
&lt;li&gt;Gate the agent on fast checks with a hook.&lt;/li&gt;
&lt;li&gt;Deploy every change to a preview URL.&lt;/li&gt;
&lt;li&gt;Run the critical flows in a real browser and check the API behind them.&lt;/li&gt;
&lt;li&gt;Block merges on red, feed failures back to the agent, and do a manual pass before releases.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this needs a QA team. It needs a short list and the habit of checking it every time.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I'm building &lt;a href="https://alwaysqa.com/" rel="noopener noreferrer"&gt;AlwaysQA&lt;/a&gt;, which checks login, checkout and other critical flows in a real browser after your coding agent ships a change. It only checks the flows you describe, and it doesn't replace your unit tests.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>ai</category>
      <category>webdev</category>
      <category>playwright</category>
    </item>
    <item>
      <title>Automated regression testing for web applications: what to automate and what to keep human</title>
      <dc:creator>Maciek Chmura</dc:creator>
      <pubDate>Mon, 07 Sep 2026 07:08:08 +0000</pubDate>
      <link>https://dev.to/maciek_chmura_building/automated-regression-testing-for-web-applications-what-to-automate-and-what-to-keep-human-4he9</link>
      <guid>https://dev.to/maciek_chmura_building/automated-regression-testing-for-web-applications-what-to-automate-and-what-to-keep-human-4he9</guid>
      <description>&lt;h1&gt;
  
  
  A form took two days to regression test. The hard part wasn’t the clicking.
&lt;/h1&gt;

&lt;p&gt;A multi-page quiz. More than ten questions. Different behavior depending on the account and user role. A calculated result that had to be saved correctly.&lt;/p&gt;

&lt;p&gt;A full regression pass took the QA team about two days.&lt;/p&gt;

&lt;p&gt;That’s how one of &lt;a href="https://alwaysqa.com/" rel="noopener noreferrer"&gt;AlwaysQA’s&lt;/a&gt; founders remembers testing a feature at a former workplace. Each individual path looked manageable. The work grew when the team had to repeat it across accounts, roles, answer combinations, and expected results.&lt;/p&gt;

&lt;p&gt;Other development work waited while QA completed the checks.&lt;/p&gt;

&lt;p&gt;The obvious response is: “Just automate it.”&lt;/p&gt;

&lt;p&gt;But automate &lt;em&gt;what&lt;/em&gt;, exactly?&lt;/p&gt;

&lt;p&gt;Clicking through the pages was only part of the job. Someone still had to define the account state, choose the right answers, know the expected calculation, and verify that the application saved the result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The hard part wasn’t teaching a browser to click Next. It was maintaining a trustworthy definition of what should happen.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That’s where I’d start a regression automation project—not with the tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, separate regression testing from checking a fix
&lt;/h2&gt;

&lt;p&gt;Suppose you fix a bug that calculates the wrong result for one user role.&lt;/p&gt;

&lt;p&gt;Checking that the calculation now works is &lt;strong&gt;confirmation testing&lt;/strong&gt;: did the fix resolve the defect?&lt;/p&gt;

&lt;p&gt;Checking that other roles still receive the correct results is &lt;strong&gt;regression testing&lt;/strong&gt;: did the change break behavior that previously worked?&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://istqb.org/wp-content/uploads/2024/11/ISTQB_CTFL_Syllabus_v4.0.1.pdf" rel="noopener noreferrer"&gt;ISTQB Foundation Level syllabus&lt;/a&gt; makes this distinction. Regression effects can appear in the changed component, elsewhere in the application, or in connected systems.&lt;/p&gt;

&lt;p&gt;Automation changes how those checks run. It doesn’t change their purpose.&lt;/p&gt;

&lt;p&gt;A regression check can be a unit test, an API test, a browser test, or a manual check. You don’t need to drive a browser for every regression risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don’t turn the entire manual checklist into browser tests
&lt;/h2&gt;

&lt;p&gt;In the quiz example, a QA engineer had to sign in with a prepared account, complete the role-specific form, consult a separate source of truth, exercise valid and invalid paths, and verify the calculated and saved result.&lt;/p&gt;

&lt;p&gt;Then repeat the process for the next scenario.&lt;/p&gt;

&lt;p&gt;The team didn’t have automation for this workflow. A code-based browser suite appeared expensive to create and maintain because it would need to represent all those variations.&lt;/p&gt;

&lt;p&gt;That doesn’t mean Playwright or Cypress couldn’t handle it. It means that “write an end-to-end test” wasn’t a complete solution.&lt;/p&gt;

&lt;p&gt;Before choosing a tool, I’d split the problem into three questions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Where I’d start&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Does this answer set produce the correct calculation?&lt;/td&gt;
&lt;td&gt;Unit tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Are permissions enforced and results stored correctly?&lt;/td&gt;
&lt;td&gt;API or integration tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can a user with this role complete the quiz and view the expected saved result?&lt;/td&gt;
&lt;td&gt;Browser end-to-end tests&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These checks overlap intentionally, but they prove different things.&lt;/p&gt;

&lt;p&gt;A unit test can exercise calculation edge cases without signing in and answering ten questions every time. An API test can check authorization and persistence without rendering the form. A browser test can check whether the pieces work together from the user’s perspective.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.cypress.io/app/core-concepts/testing-types" rel="noopener noreferrer"&gt;Cypress’s testing guidance&lt;/a&gt; describes these different scopes and the additional setup, infrastructure, and maintenance that end-to-end testing can involve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use the browser to prove the journey. Don’t make it carry every internal rule.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Your first automated workflow should be bounded, not impressive
&lt;/h2&gt;

&lt;p&gt;The most time-consuming feature isn’t automatically the best first automation project.&lt;/p&gt;

&lt;p&gt;For the quiz, I wouldn’t begin with every role and answer combination. I’d choose one important role, one prepared account, and one defined answer path.&lt;/p&gt;

&lt;p&gt;A useful first candidate has three properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It matters.&lt;/strong&gt; A failure would block a meaningful user task or create release risk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It repeats.&lt;/strong&gt; QA already performs substantially the same check across releases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It can be specified.&lt;/strong&gt; The starting state and expected result are clear.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last property is easy to underestimate.&lt;/p&gt;

&lt;p&gt;“Check that the quiz works” is not a test specification.&lt;/p&gt;

&lt;p&gt;“Using a prepared account with role A, submit answer set 01 and verify that the expected result is displayed and available when the saved submission is reopened” is much closer.&lt;/p&gt;

&lt;p&gt;The first asks a tool to discover the requirements and judge the implementation simultaneously. The second gives it a defined question to answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write the test contract before the test script
&lt;/h2&gt;

&lt;p&gt;For a first browser check, I’d write something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Scenario
  Role A completes answer path 01.

Starting state
  Staging environment.
  Dedicated test account with role A.
  Known quiz version.
  No existing submission that would change this path.

Test data
  A defined answer set.
  An independently established expected result.

Actions
  Sign in.
  Open the quiz.
  Complete the defined answer path.
  Submit.
  Reopen the saved submission.

Pass conditions
  The displayed result matches the expected result.
  The reopened submission shows the expected saved result.

Failure evidence
  Account role and scenario identifier.
  Answers submitted.
  Expected result versus observed result.
  The step where behavior diverged.
  Relevant screenshots, trace, or logs where available.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a test-design example, not a tool-specific configuration format.&lt;/p&gt;

&lt;p&gt;Notice what’s missing: selectors, browser APIs, and prompts.&lt;/p&gt;

&lt;p&gt;Those come later. First, the team needs to agree on what the check must prove.&lt;/p&gt;

&lt;p&gt;Also, keep the expected result independent of the production logic being tested. If your test calculates its expectation by calling the same broken formula, both sides can agree while the behavior is wrong.&lt;/p&gt;

&lt;p&gt;For the quiz, the existing source of truth is therefore part of the automation design—not an annoying detail to resolve after the browser script is finished.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test data is part of the test
&lt;/h2&gt;

&lt;p&gt;A well-written script won’t rescue an uncontrolled starting state.&lt;/p&gt;

&lt;p&gt;Suppose the first run passes, but the second fails because the account has already submitted the quiz. Or another test changes its role. Or someone edits the answer data while the run is in progress.&lt;/p&gt;

&lt;p&gt;You now have a result to investigate, but it may say more about the test setup than the application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://playwright.dev/docs/best-practices" rel="noopener noreferrer"&gt;Playwright’s best practices&lt;/a&gt; recommend isolated tests with their own state to improve reproducibility and prevent failures from cascading between tests.&lt;/p&gt;

&lt;p&gt;For a state-changing workflow, answer this before scheduling it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What makes the next run start from the same meaningful conditions as the previous one?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That could mean resetting a dedicated account, creating fresh test data, or using a supported cleanup process. The implementation depends on the application.&lt;/p&gt;

&lt;p&gt;The requirement doesn’t: the starting state must be deliberate.&lt;/p&gt;

&lt;p&gt;And don’t confuse an interface signal with every underlying guarantee. A success toast is evidence that a message appeared. Reopening a saved submission checks more of the user-visible behavior. Backend persistence rules still deserve their own lower-level checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI can change the execution. It doesn’t remove the assertions.
&lt;/h2&gt;

&lt;p&gt;Once you’ve justified a browser-level check, you can choose how to execute it.&lt;/p&gt;

&lt;p&gt;With code-driven automation, the team specifies actions and assertions explicitly. That can be a good fit when you want precise control over the path and already have the engineering capacity to maintain it.&lt;/p&gt;

&lt;p&gt;With AI-guided browser execution, you can describe the user’s goal and evaluate an alternative to maintaining fixed interaction steps.&lt;/p&gt;

&lt;p&gt;But the underlying contract remains:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Start from this state, attempt this workflow, and verify this observable outcome.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;AI doesn’t remove the need for test data, expected results, safe boundaries, or useful evidence.&lt;/p&gt;

&lt;p&gt;“Use AI to test the quiz” is no more complete than “use Playwright to test the quiz.”&lt;/p&gt;

&lt;p&gt;Neither approach should get to invent the correct result because the page looks plausible.&lt;/p&gt;

&lt;p&gt;Human exploration belongs alongside both approaches, especially when the behavior is new, requirements are ambiguous, or someone needs to notice a problem that wasn’t anticipated in the test.&lt;/p&gt;

&lt;h2&gt;
  
  
  An inconclusive run isn’t the same as a product failure
&lt;/h2&gt;

&lt;p&gt;Before automating a workflow, decide how you’ll interpret the result.&lt;/p&gt;

&lt;p&gt;There are at least three useful distinctions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Passed:&lt;/strong&gt; the check reached the specified outcome and collected sufficient evidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failed:&lt;/strong&gt; the observed behavior contradicted the expected behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inconclusive:&lt;/strong&gt; the run couldn’t establish a reliable verdict.&lt;/p&gt;

&lt;p&gt;For example, an incorrect quiz result is evidence of a failed check. An unavailable test fixture that prevents the scenario from starting is not, by itself, evidence that the quiz calculation is broken.&lt;/p&gt;

&lt;p&gt;Both need attention, but they need different investigations.&lt;/p&gt;

&lt;p&gt;A report should make that distinction visible. Otherwise, QA ends up replaying the entire workflow just to determine whether the application failed, the setup failed, or the runner got stuck.&lt;/p&gt;

&lt;p&gt;The same principle applies to passing results. A green status is only useful when you understand what was actually verified.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One passing path is not a certificate that the application is regression-free.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AlwaysQA fits
&lt;/h2&gt;

&lt;p&gt;Disclosure: I’m building &lt;a href="https://alwaysqa.com/" rel="noopener noreferrer"&gt;AlwaysQA&lt;/a&gt;, an AI-guided browser QA product.&lt;/p&gt;

&lt;p&gt;Its test-case model follows the same basic structure: a starting URL, plain-language instructions, and an observable success condition.&lt;/p&gt;

&lt;p&gt;A run returns &lt;strong&gt;Passed&lt;/strong&gt;, &lt;strong&gt;Failed&lt;/strong&gt;, or &lt;strong&gt;Needs Attention&lt;/strong&gt;, with evidence that can include a summary, observations, an action timeline, and a temporary replay. Needs Attention keeps an inconclusive run separate from an observed failure.&lt;/p&gt;

&lt;p&gt;Checks can run on demand or on daily or weekly schedules. The workflow is described in &lt;a href="https://alwaysqa.com/how-it-works/" rel="noopener noreferrer"&gt;How AlwaysQA works&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That makes it one option for the browser-execution part of this approach. It doesn’t replace the expected-result model, lower-level tests, or human judgment.&lt;/p&gt;

&lt;p&gt;And the two-day quiz example is a recollection of a manual testing problem—not a before-and-after result from using AlwaysQA.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run a one-workflow pilot before expanding
&lt;/h2&gt;

&lt;p&gt;You don’t need a large suite to find out whether automation is helping.&lt;/p&gt;

&lt;p&gt;You need one check that earns the team’s trust.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Record the manual baseline
&lt;/h3&gt;

&lt;p&gt;Measure how often the workflow is checked, how long it takes, what setup it requires, and what other work waits for it.&lt;/p&gt;

&lt;p&gt;Use your team’s actual experience. The quiz’s two-day regression pass isn’t a benchmark for another application.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Implement one complete path
&lt;/h3&gt;

&lt;p&gt;Choose one role and one data variant. Define the starting state, expected result, and failure evidence.&lt;/p&gt;

&lt;p&gt;Handle cleanup or reset before introducing repeated runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Review the early results
&lt;/h3&gt;

&lt;p&gt;Check that Passed means the intended behavior really happened. Check that Failed and inconclusive results provide enough information to investigate.&lt;/p&gt;

&lt;p&gt;Where it’s safe, deliberately introduce a known failure in a controlled test environment. A check that stays green when its success condition is violated hasn’t earned your trust.&lt;/p&gt;

&lt;p&gt;Run the pilot on demand while you work through problems in the specification, data, or execution.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Measure the work that remains
&lt;/h3&gt;

&lt;p&gt;Don’t measure success only by counting automated runs.&lt;/p&gt;

&lt;p&gt;Compare the manual effort removed with the effort spent reviewing results, investigating false alarms, and maintaining the check. Notice whether QA still has to repeat the workflow because the evidence isn’t convincing.&lt;/p&gt;

&lt;p&gt;A test that runs every day but gets manually repeated every day hasn’t removed the task.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Expand one variable at a time
&lt;/h3&gt;

&lt;p&gt;Once the first path is trustworthy, add another role, answer set, or important edge case.&lt;/p&gt;

&lt;p&gt;Change the execution cadence deliberately too. A daily schedule is useful only when the workflow’s risk justifies it and the test state can be prepared safely.&lt;/p&gt;

&lt;p&gt;Small expansions make it easier to identify which new assumption caused a problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automate repetition. Keep judgment.
&lt;/h2&gt;

&lt;p&gt;The goal isn’t to remove QA professionals from the process.&lt;/p&gt;

&lt;p&gt;People still need to decide which risks matter, define correct behavior, explore unexpected paths, investigate failures, and judge whether a release is acceptable.&lt;/p&gt;

&lt;p&gt;The goal is to stop spending the same human attention on a well-understood check every time the application changes.&lt;/p&gt;

&lt;p&gt;For the quiz, I wouldn’t start by asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do we automate every combination?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I’d ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What is one important path we can specify, repeat, and trust?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then I’d build outward from there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What’s the workflow your team keeps regression-testing by hand—and what makes it difficult to automate: the interactions, the test data, or knowing the correct result?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>ai</category>
      <category>automation</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building QuickArchViz: turning 3D models into renders</title>
      <dc:creator>Maciek Chmura</dc:creator>
      <pubDate>Fri, 24 Apr 2026 13:07:08 +0000</pubDate>
      <link>https://dev.to/maciek_chmura_building/building-quickarchviz-turning-3d-models-into-renders-10gp</link>
      <guid>https://dev.to/maciek_chmura_building/building-quickarchviz-turning-3d-models-into-renders-10gp</guid>
      <description>&lt;p&gt;A few months ago, I started building QuickArchViz — a tool that turns 3D model screenshots into architectural visualizations in seconds.&lt;/p&gt;

&lt;p&gt;The idea came from a very simple frustration I had in the past when I was still an architect:&lt;br&gt;
&lt;em&gt;My models were ready, but I still needed to make the visualizations for the client&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Rendering takes time. It costs money. And every client meeting resets the whole process.&lt;/p&gt;

&lt;p&gt;I wanted something faster. Something closer to how architects actually work.&lt;/p&gt;

&lt;p&gt;So I started building it - now that I have over 10+ years of experience in programming.&lt;/p&gt;

&lt;h2&gt;
  
  
  What QuickArchViz does
&lt;/h2&gt;

&lt;p&gt;You upload a screenshot from your 3D model (SketchUp, Revit, Archicad — whatever).&lt;/p&gt;

&lt;p&gt;The system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keeps your geometry and camera&lt;/li&gt;
&lt;li&gt;enhances materials, lighting, environment&lt;/li&gt;
&lt;li&gt;generates a presentation-ready visual&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No exports. No rendering settings. No waiting hours.&lt;/p&gt;

&lt;p&gt;Just:&lt;br&gt;
upload → choose style → generate&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.amazonaws.com%2Fuploads%2Farticles%2Fyzm5b9wkxlu67gbwnxsk.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.amazonaws.com%2Fuploads%2Farticles%2Fyzm5b9wkxlu67gbwnxsk.png" alt=" " width="800" height="516"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;Nothing exotic. The interesting part is not the stack - it’s how the pieces work together.&lt;/p&gt;

&lt;p&gt;Backend&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Laravel (API + orchestration)&lt;/li&gt;
&lt;li&gt;Postgres (multi-tenant, render tracking, token usage)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Frontend&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React + Inertia&lt;/li&gt;
&lt;li&gt;simple UI, focused on speed (upload → result)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI generation&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;image generation via external APIs (currently experimenting heavily)&lt;/li&gt;
&lt;li&gt;prompt engineering is doing most of the heavy lifting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Infra&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VPS (Laravel Forge)&lt;/li&gt;
&lt;li&gt;no overkill, trying to stay lean&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The hard part: making AI behave like an architect
&lt;/h2&gt;

&lt;p&gt;The biggest challenge wasn’t “generate a pretty image”.&lt;/p&gt;

&lt;p&gt;It was:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;don’t break the architecture&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most AI image tools will happily:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;change proportions&lt;/li&gt;
&lt;li&gt;move windows&lt;/li&gt;
&lt;li&gt;redesign the building&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes them useless for real workflows.&lt;/p&gt;

&lt;p&gt;So the core of the system is actually the prompt framework, not the model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core constraints (this took weeks to get right)
&lt;/h3&gt;

&lt;p&gt;Every generation follows strict rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;preserve exact geometry&lt;/li&gt;
&lt;li&gt;preserve camera position&lt;/li&gt;
&lt;li&gt;preserve hard surfaces (paths, roads)&lt;/li&gt;
&lt;li&gt;do not redesign the building&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then only enhance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;lighting&lt;/li&gt;
&lt;li&gt;materials&lt;/li&gt;
&lt;li&gt;environment&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Styles are harder than they look
&lt;/h3&gt;

&lt;p&gt;I thought styles would be easy.&lt;/p&gt;

&lt;p&gt;They’re not.&lt;br&gt;
Example: Golden hour&lt;/p&gt;

&lt;p&gt;Most models go straight into:&lt;/p&gt;

&lt;p&gt;orange Instagram sunset in Bali&lt;/p&gt;

&lt;p&gt;What I needed was:&lt;/p&gt;

&lt;p&gt;subtle, European, architectural light&lt;/p&gt;

&lt;p&gt;So styles became controlled systems, not just “vibes”.&lt;/p&gt;

&lt;p&gt;Current styles&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clean daylight (default, most reliable)&lt;/li&gt;
&lt;li&gt;Golden hour (recently fixed, less saturated)&lt;/li&gt;
&lt;li&gt;Overcast / Nordic&lt;/li&gt;
&lt;li&gt;Evening / blue hour&lt;/li&gt;
&lt;li&gt;Artistic (marker, watercolor, concept)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one has its own prompt logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  UX decisions (what actually matters)
&lt;/h2&gt;

&lt;p&gt;I’m trying to remove every possible friction point.&lt;/p&gt;

&lt;p&gt;Key decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no file exports required → screenshot is enough&lt;/li&gt;
&lt;li&gt;no settings → just styles&lt;/li&gt;
&lt;li&gt;no queue UI → simple “processing” state&lt;/li&gt;
&lt;li&gt;pay-as-you-go (no subscription)&lt;/li&gt;
&lt;/ul&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.amazonaws.com%2Fuploads%2Farticles%2Ft5u6kcnbn9ro93mxyzh6.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.amazonaws.com%2Fuploads%2Farticles%2Ft5u6kcnbn9ro93mxyzh6.png" alt=" " width="800" height="527"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What I’m working on now
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;improving daylight realism (less “CGI”, more “photo”)&lt;/li&gt;
&lt;li&gt;refining golden hour (already toned down)&lt;/li&gt;
&lt;li&gt;adding more artistic styles&lt;/li&gt;
&lt;li&gt;improving landing page conversion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to check it out:&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Final thought
&lt;/h3&gt;

&lt;p&gt;This is one of those products where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the tech is not the hardest part&lt;/li&gt;
&lt;li&gt;the UX is not the hardest part&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hardest part is:&lt;/p&gt;

&lt;p&gt;making AI respect reality&lt;/p&gt;

&lt;p&gt;Still working on that.&lt;/p&gt;

&lt;p&gt;If you’re building something in this space or fighting similar issues (especially consistency / prompting / positioning), curious what worked for you 👇&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>buildinpublic</category>
    </item>
  </channel>
</rss>
