<?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: Jeff Thoensen</title>
    <description>The latest articles on DEV Community by Jeff Thoensen (@jeffthoensen).</description>
    <link>https://dev.to/jeffthoensen</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3276950%2F96ee1be3-4a19-430a-851d-2fbfcbe774fb.jpg</url>
      <title>DEV Community: Jeff Thoensen</title>
      <link>https://dev.to/jeffthoensen</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jeffthoensen"/>
    <language>en</language>
    <item>
      <title>Testing a LiveView App with Playwright: Fixing Navigation Timeouts</title>
      <dc:creator>Jeff Thoensen</dc:creator>
      <pubDate>Wed, 27 May 2026 22:07:52 +0000</pubDate>
      <link>https://dev.to/jeffthoensen/testing-a-liveview-app-with-playwright-fixing-navigation-timeouts-5b2f</link>
      <guid>https://dev.to/jeffthoensen/testing-a-liveview-app-with-playwright-fixing-navigation-timeouts-5b2f</guid>
      <description>&lt;p&gt;I was building a Playwright suite against a Phoenix LiveView app for the first time. Tests ran fine in isolation. Overnight, the full suite timed out across the board. Every failure was in a navigation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Playwright Waits For by Default
&lt;/h2&gt;

&lt;p&gt;When you call &lt;code&gt;page.goto()&lt;/code&gt; or trigger a navigation through a click, Playwright waits for the &lt;code&gt;load&lt;/code&gt; event before moving on. That assumes a traditional request/response cycle: the browser makes a request, the server returns a full HTML document, the browser fires &lt;code&gt;load&lt;/code&gt; when everything is done.&lt;/p&gt;

&lt;p&gt;LiveView doesn't do that. Navigation happens over a persistent WebSocket connection. The URL changes, the page updates, but there's no new document and no &lt;code&gt;load&lt;/code&gt; event in the way Playwright expects. So Playwright waits, hits the timeout, and the test fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: waitUntil commit
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;waitUntil&lt;/code&gt; option controls what signal Playwright waits for before continuing. Setting it to &lt;code&gt;'commit'&lt;/code&gt; tells Playwright to return as soon as the server has responded and the navigation is committed, without waiting for a full page load cycle that isn't coming.&lt;/p&gt;

&lt;p&gt;The change applies anywhere a navigation happens — &lt;code&gt;goto&lt;/code&gt; and &lt;code&gt;waitForURL&lt;/code&gt; both take the option:&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="c1"&gt;// goto&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;commit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// waitForURL&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;waitForURL&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;library&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&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;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;commit&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;This was a 12-commit PR touching every navigation across the suite. Not a complicated change per file, but it had to be consistent. One nav without &lt;code&gt;waitUntil: 'commit'&lt;/code&gt; is enough to hang the whole overnight run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Second Step: waitForLiveView
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;waitUntil: 'commit'&lt;/code&gt; gets you past the hang, but it returns early. LiveView still needs to establish its WebSocket connection and mount the view. Acting on the page before that happens produces flaky results.&lt;/p&gt;

&lt;p&gt;The pattern is to follow every navigation with a &lt;code&gt;waitForLiveView&lt;/code&gt; helper that waits for the socket to connect before returning control to the test. It was already in the suite — the missing piece was &lt;code&gt;waitUntil: 'commit'&lt;/code&gt; on the line before it.&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;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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;commit&lt;/span&gt;&lt;span class="dl"&gt;'&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;waitForLiveView&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="c1"&gt;// or after a click-triggered navigation&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;waitForURL&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;library&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&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;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;commit&lt;/span&gt;&lt;span class="dl"&gt;'&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;waitForLiveView&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is one exception worth noting. When the navigation is a plain href link transition rather than a LiveView-driven one, &lt;code&gt;waitForURL&lt;/code&gt; governs the transition on its own and &lt;code&gt;waitForLiveView&lt;/code&gt; isn't needed. A comment in the diff flags exactly that case: &lt;code&gt;// href link — waitForURL governs this transition, not waitForLiveView&lt;/code&gt;. Knowing which navigations go through LiveView and which don't matters when you're deciding where to apply the full pattern.&lt;/p&gt;

&lt;p&gt;The actual helper in this suite does more than a basic selector wait. The environment has a brief WebSocket drop around the nightly deploy window, so there's a retry path built in:&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;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;waitForLiveView&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="nx"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;retryWithReload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;retryWithReload&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;}&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;retryWithReload&lt;/span&gt;&lt;span class="p"&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;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.phx-connected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;waitFor&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;try&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;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.phx-connected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;waitFor&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;e&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;reload&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;commit&lt;/span&gt;&lt;span class="dl"&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;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.phx-connected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;waitFor&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&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;The default path just waits for &lt;code&gt;.phx-connected&lt;/code&gt;. Pass &lt;code&gt;retryWithReload: true&lt;/code&gt; when the page is in a clean navigable state and a reload is safe — if the socket connection times out, the helper reloads and tries once more. Avoid it inside open modals, filled forms, or any state a reload would destroy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Broader Point
&lt;/h2&gt;

&lt;p&gt;Playwright's defaults are reasonable for the apps they were designed around. When you add a new framework to your suite, those defaults are the first thing worth checking. The assumption baked into &lt;code&gt;waitUntil: 'load'&lt;/code&gt; is that navigation means a new document. LiveView changes that contract, and the suite breaks until you adjust for it.&lt;/p&gt;

&lt;p&gt;The tool doesn't know what kind of app it's running against. You do. The configuration has to reflect that. More on this kind of thing at &lt;a href="https://jeffthoensen.com" rel="noopener noreferrer"&gt;jeffthoensen.com&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Jeff Thoensen is a Context-Driven QA Engineer focused on automation, API testing, and exploratory testing. Find more at &lt;a href="https://jeffthoensen.com" rel="noopener noreferrer"&gt;jeffthoensen.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>testing</category>
      <category>elixir</category>
      <category>qa</category>
    </item>
    <item>
      <title>Don't Automate Everything</title>
      <dc:creator>Jeff Thoensen</dc:creator>
      <pubDate>Sat, 23 Aug 2025 16:45:57 +0000</pubDate>
      <link>https://dev.to/jeffthoensen/dont-automate-everything-1889</link>
      <guid>https://dev.to/jeffthoensen/dont-automate-everything-1889</guid>
      <description>&lt;p&gt;I went heavy on Playwright early on. Wrote tests for every button and every form that I could. It looked solid until a small UI change broke a dozen of them because I’d used getByText in bad spots. The suite became useless for a while, and we were shipping code without it (so what was the point of all that work?).&lt;/p&gt;

&lt;p&gt;Now I focus on automating only the things I don’t want to do manually: setup flows, core user paths, data checks. If I can’t explain why a test exists, I don’t write it. I also check UI mocks early so I’m not surprised when labels change or elements move. It saves time and keeps the suite stable.&lt;/p&gt;

&lt;p&gt;I keep my suites small on purpose. Five reliable tests are better than fifty flaky ones, and flaky tests destroy trust in QA fast. I also treat tests like real code. Reviews, refactors, small commits. If you skip that part, the suite turns into a mess quickly.&lt;/p&gt;

&lt;p&gt;I use Playwright every day, but I’m not automating to hit numbers. It’s there to save time and keep focus on users, not on constantly broken tests.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.jeffthoensen.com" rel="noopener noreferrer"&gt;jeffthoensen.com&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/jeff-thoensen" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://www.linkedin.com/in/jeffthoensen/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://jeff-thoensen.medium.com/" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://jeffthoensen.substack.com/" rel="noopener noreferrer"&gt;Substack&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://www.indiehackers.com/jeffthoensen" rel="noopener noreferrer"&gt;Indie Hackers&lt;/a&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>playwright</category>
    </item>
    <item>
      <title>Hello QA!</title>
      <dc:creator>Jeff Thoensen</dc:creator>
      <pubDate>Fri, 27 Jun 2025 14:23:46 +0000</pubDate>
      <link>https://dev.to/jeffthoensen/hi-im-jeff-im-in-qa-1m30</link>
      <guid>https://dev.to/jeffthoensen/hi-im-jeff-im-in-qa-1m30</guid>
      <description>&lt;p&gt;I’ve been in QA for about 10 years, with a background in customer support that gave me a real sense of how bugs hit users.&lt;/p&gt;

&lt;p&gt;I care about testing as a way to reduce risk and protect the user experience, not just checking boxes. These days I do a mix of exploratory testing and Playwright automation, but the goal’s the same: find the stuff that actually matters.&lt;/p&gt;

&lt;p&gt;If you're into testing that goes beyond coverage reports, we’re probably on the same page.&lt;/p&gt;

</description>
      <category>qa</category>
      <category>testing</category>
      <category>softwaretesting</category>
      <category>qualityassurance</category>
    </item>
  </channel>
</rss>
