<?xml version="1.0" encoding="UTF-8" standalone="no"?><rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" version="2.0">

<channel>
	<title>Swacblooms&#129419;</title>
	<atom:link href="https://swacblooms.com/feed/" rel="self" type="application/rss+xml"/>
	<link>https://swacblooms.com/</link>
	<description>Making the Moves</description>
	<lastBuildDate>Mon, 05 Oct 2026 10:01:11 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.3</generator>

<image>
	<url>https://swacblooms.com/wp-content/uploads/2018/09/cropped-SWACBLOOMS1-32x32.jpg</url>
	<title>Swacblooms&#129419;</title>
	<link>https://swacblooms.com/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<itunes:explicit>no</itunes:explicit><itunes:subtitle>Making the Moves</itunes:subtitle><item>
		<title>Zero-downtime deployments in Dokploy with Docker Swarm</title>
		<link>https://swacblooms.com/zero-downtime-deployments-in-dokploy-with-docker-swarm/?utm_source=rss&amp;utm_medium=rss&amp;utm_campaign=zero-downtime-deployments-in-dokploy-with-docker-swarm</link>
		
		<dc:creator><![CDATA[Samson Amaugo]]></dc:creator>
		<pubDate>Mon, 05 Oct 2026 10:01:10 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<guid isPermaLink="false">https://swacblooms.com/?p=1894</guid>

					<description><![CDATA[<p>I was trying to see how to enable zero-downtime deployment in Dokploy using Docker Swarm. Dokploy can deploy a compose file as a Swarm stack, and Swarm has rolling updates built in, so I assumed it would just work. It doesn&#8217;t, not out of the box. To see why, I ran Dokploy locally, deployed a tiny app, and hit it ten times a second while redeploying, then changed one thing at a time and counted the failed requests. This post walks through each change and what it did to that number. The lab Dokploy&#8217;s installer expects a Linux server, so </p>
<p>The post <a href="https://swacblooms.com/zero-downtime-deployments-in-dokploy-with-docker-swarm/">Zero-downtime deployments in Dokploy with Docker Swarm</a> appeared first on <a href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">I was trying to see how to enable zero-downtime deployment in Dokploy using Docker Swarm. Dokploy can deploy a compose file as a Swarm stack, and Swarm has rolling updates built in, so I assumed it would just work. It doesn&#8217;t, not out of the box. To see why, I ran Dokploy locally, deployed a tiny app, and hit it ten times a second while redeploying, then changed one thing at a time and counted the failed requests. This post walks through each change and what it did to that number.</p>



<h2 class="wp-block-heading">The lab</h2>



<p class="wp-block-paragraph">Dokploy&#8217;s installer expects a Linux server, so I ran it inside a Docker-in-Docker container. That keeps everything it does away from your own Docker.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
docker run -d --privileged --name dokploy-lab \
  -p 8088:80 -p 3005:3000 docker:29-dind

docker exec dokploy-lab sh -c &#039;apk add --no-cache bash curl iproute2 openssl&#039;

# the installer refuses to run inside a container, and Docker puts
# this file in every container, which is how it can tell
docker exec dokploy-lab rm -f /.dockerenv

curl -sSL https://dokploy.com/install.sh -o install.sh
docker cp install.sh dokploy-lab:/root/install.sh
docker exec dokploy-lab bash -c \
  &#039;ADVERTISE_ADDR=$(hostname -i | awk &quot;{print \$1}&quot;) bash /root/install.sh&#039;
</pre></div>


<p class="wp-block-paragraph">Skip the <code>rm</code> and the installer stops straight away, with an error that sends you looking in the wrong place:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
Installing Dokploy version: v0.30.8
This script must be run on Linux
</pre></div>


<p class="wp-block-paragraph">It <em>is</em> running on Linux. That&#8217;s the message the installer prints when it finds <code>/.dockerenv</code>, and it exits before touching anything.</p>



<p class="wp-block-paragraph">One thing to know about the installer if you run it outside a container: it runs <code>docker swarm leave --force</code> before setting up its own swarm. That&#8217;s so a reinstall works, because <code>docker swarm init</code> refuses on a machine that&#8217;s already in a swarm:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
Error response from daemon: This node is already part of a swarm.
Use &quot;docker swarm leave&quot; to leave this swarm and join another one.
</pre></div>


<p class="wp-block-paragraph">Inside the lab container that&#8217;s harmless. On a machine already running Swarm services, it takes the machine out of that swarm.</p>



<p class="wp-block-paragraph">Then the dashboard is at <code>http://localhost:3005</code> and Dokploy&#8217;s Traefik answers on <code>localhost:8088</code>. This was Dokploy v0.30.8 with Traefik v3.6.25. It needs a few GB of disk, and mine ran out halfway through the first attempt, so check <code>docker system df</code> first.</p>



<h2 class="wp-block-heading">The test app</h2>



<p class="wp-block-paragraph">In Dokploy I created a project, then a Compose service with the type set to <strong>Stack</strong>. That&#8217;s the setting that makes Dokploy run <code>docker stack deploy</code> instead of plain Compose. The provider is <strong>Raw</strong>, so the compose file is just pasted in:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: yaml; title: ; notranslate">
services:
  web:
    image: httpd:alpine
    command: &#x5B;&quot;sh&quot;, &quot;-c&quot;, &quot;sleep 8; echo $$VERSION &gt; /usr/local/apache2/htdocs/index.html; exec httpd-foreground&quot;]
    environment:
      VERSION: v1
</pre></div>


<p class="wp-block-paragraph">It&#8217;s a plain Apache web server (<code>httpd:alpine</code> is the official Apache image, Alpine flavour) told to do three things when the container starts, one after the other:</p>



<ol class="wp-block-list">
<li><strong><code>sleep 8</code></strong> waits eight seconds before doing anything. A real app doesn&#8217;t answer the moment its container starts either: it connects to a database, runs migrations, warms up. The sleep stands in for that, so the container is <em>running</em> but not yet <em>answering</em>, which is exactly the gap a deploy has to handle.</li>



<li><strong><code>echo $$VERSION > /usr/local/apache2/htdocs/index.html</code></strong> writes the version, <code>v1</code>, into the page Apache serves. The <code>$$</code> is how you write a literal <code>$</code> in a compose file; without it, Compose would try to fill <code>$VERSION</code> in itself when reading the file, instead of leaving it for the container&#8217;s shell. So every request returns just <code>v1</code>, and the probe can see the exact moment traffic moves from the old container to the new one.</li>



<li><strong><code>exec httpd-foreground</code></strong> starts Apache, which listens on port 80. <code>exec</code> makes Apache replace the shell rather than run under it, so it&#8217;s Apache that receives the stop signal when Swarm shuts the container down.</li>
</ol>



<p class="wp-block-paragraph">To deploy a &#8220;new version&#8221;, I only change <code>VERSION</code> to <code>v2</code>, <code>v3</code> and so on. Swarm sees a changed service and replaces the container.</p>



<p class="wp-block-paragraph">Under <strong>Domains</strong> I added <code>lab.localhost</code> on port 80. Notice there are no Traefik labels and no network in the file. Dokploy adds both when it deploys.</p>



<h2 class="wp-block-heading">The probe</h2>



<p class="wp-block-paragraph">A small Node script (Node 18 or later, no packages) that requests the app every 100 milliseconds and writes down what came back:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// probe.mjs, run with: node probe.mjs probe.log 60
import { appendFileSync, writeFileSync } from &quot;node:fs&quot;;

const &#x5B;out, seconds] = process.argv.slice(2);
const end = Date.now() + Number(seconds) * 1000;
writeFileSync(out, &quot;&quot;);

while (Date.now() &lt; end) {
  const started = Date.now();
  let line;
  try {
    const res = await fetch(&quot;http://lab.localhost:8088/&quot;, { signal: AbortSignal.timeout(1000) });
    const body = (await res.text()).trim();
    line = `${res.status} ${res.ok ? body : &quot;-&quot;}`;
  } catch (e) {
    line = `ERR ${e.name}`;
  }
  appendFileSync(out, `${(started / 1000).toFixed(2)} ${line}\n`);
  await new Promise((r) =&gt; setTimeout(r, Math.max(0, 100 - (Date.now() - started))));
}
</pre></div>


<p class="wp-block-paragraph">Start it, change <code>VERSION</code> in Dokploy, press Deploy, then count what came back:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
cut -d&#039; &#039; -f2- probe.log | sort | uniq -c
</pre></div>


<p class="wp-block-paragraph">One thing that cost me a few minutes: the obvious way to write this is <code>fetch("http://localhost:8088/", { headers: { Host: "lab.localhost" } })</code>, and it doesn&#8217;t work. Node&#8217;s <code>fetch</code> quietly replaces your <code>Host</code> header with the one from the URL, so Traefik sees <code>localhost</code> and answers <code>404</code> for every request, which looks exactly like a broken deploy. I checked with a tiny server that echoes the header back: <code>fetch</code> sent <code>localhost:18099</code>, while <code>node:http</code> sent <code>lab.localhost</code>. Putting the hostname in the URL avoids the problem, because <code>*.localhost</code> resolves to your own machine.</p>



<h2 class="wp-block-heading">Reading the results</h2>



<p class="wp-block-paragraph">Each step below shows what the probe saw during one deploy, as a timeline of what came back. A failed request is one of three things, and each has a different cause:</p>



<ul class="wp-block-list">
<li><strong><code>404</code></strong>: Traefik has no container to send the request to, so it answers &#8220;404 page not found&#8221; itself.</li>



<li><strong><code>502</code></strong>: Traefik sent the request to a container that refused it, usually one that&#8217;s stopping or hasn&#8217;t started listening yet. A visitor sees &#8220;Bad Gateway&#8221;.</li>



<li><strong>timeout</strong>: Traefik sent the request to a container address that no longer exists, and nothing came back. The probe gives up after a second; a visitor&#8217;s browser just hangs.</li>
</ul>



<h2 class="wp-block-heading">Step 0: Dokploy&#8217;s defaults</h2>



<p class="wp-block-paragraph">The first deploy, v1 to v2, with nothing changed from what Dokploy does on its own:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
v1 answering
502      for 1s     old container stopping, Traefik still sending to it
timeout  for 1s     old container gone, Traefik still sending to its address
404      for 14s    Traefik has nothing to send to
v2 answering
</pre></div>


<p class="wp-block-paragraph"><strong>145 requests failed, over about 16 seconds.</strong></p>



<p class="wp-block-paragraph">Here&#8217;s why. Swarm&#8217;s default update order is <code>stop-first</code>: it stops the old container <em>first</em>, then starts the new one. So from the moment the old one stopped, there was nothing to serve the app until the new one had started, finished its 8-second <code>sleep</code>, and Traefik had noticed it. That last part is the long <code>404</code>.</p>



<p class="wp-block-paragraph">You can see the order Dokploy deployed with:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
docker service inspect &lt;service&gt; --format &#039;{{.Spec.UpdateConfig.Order}}&#039;
</pre></div>


<p class="wp-block-paragraph">which printed <code>stop-first</code>.</p>



<h2 class="wp-block-heading">Step 1: start-first</h2>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: yaml; title: ; notranslate">
deploy:
  update_config:
    order: start-first
    failure_action: rollback
</pre></div>


<p class="wp-block-paragraph"><code>start-first</code> reverses the order: start the new container, and only then stop the old one. <code>failure_action: rollback</code> means that if the new one fails, Swarm puts the old one back.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
v2 answering
502      for 5s     new container started but not listening yet
v3 answering
</pre></div>


<p class="wp-block-paragraph"><strong>50 requests failed, over about 5 seconds.</strong> Better, but not zero.</p>



<p class="wp-block-paragraph">This time the failures came from the <em>new</em> container. Swarm waits for the new container to be <em>running</em> before stopping the old one, and without a healthcheck, running only means the process has started. Mine was still in its <code>sleep 8</code>, with Apache not started yet. Swarm stopped the old container anyway, Traefik moved traffic to the new one, and the new one refused every request until Apache came up.</p>



<h2 class="wp-block-heading">Step 2: a healthcheck</h2>



<p class="wp-block-paragraph">The fix for that is a healthcheck, so &#8220;ready&#8221; means &#8220;actually answers&#8221;:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: yaml; title: ; notranslate">
healthcheck:
  test: &#x5B;&quot;CMD-SHELL&quot;, &quot;wget -q -O- http://127.0.0.1/ || exit 1&quot;]
  interval: 5s
  timeout: 3s
  retries: 3
  start_period: 20s
</pre></div>


<p class="wp-block-paragraph">Docker runs the <code>test</code> command inside the container every <code>interval</code>. Exit code <code>0</code> means healthy. <code>start_period</code> gives the app 20 seconds to boot before failed checks count against it, and <code>retries</code> is how many failures in a row mark it unhealthy. Swarm now waits for the new container to report healthy before stopping the old one.</p>



<p class="wp-block-paragraph"><code>httpd:alpine</code> ships busybox&#8217;s <code>wget</code>, so that&#8217;s what the check uses. Use whatever your own image actually has: a healthcheck that calls a program the image doesn&#8217;t include can never pass.</p>



<p class="wp-block-paragraph">Some deploys now came through clean, so I ran it 11 times. <strong>7 of the 11 still dropped between 7 and 20 requests, over 0.6 to 10 seconds.</strong> The other 4 dropped none. A typical bad one:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
old container answering
502      for 1s     old container stopping, Traefik still sending to it
timeout  for 10s    old container gone, Traefik still sending to its address
new container answering
</pre></div>


<p class="wp-block-paragraph">The new container was healthy the whole time, so this isn&#8217;t Swarm any more. The failures are all at the <em>old</em> container&#8217;s address, <em>after</em> Swarm stopped it: Traefik kept routing there. A gap that only shows up some of the time is exactly the kind that gets through testing. The next step shows why Traefik lags.</p>



<p class="wp-block-paragraph">One tip if your own app sits behind Traefik: if it redirects HTTP to HTTPS (ASP.NET Core&#8217;s <code>UseHttpsRedirection</code> does), the healthcheck gets a <code>307</code> instead of a <code>200</code> and fails. Send the header Traefik sends, and the app answers instead of redirecting:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
wget -q --header=&#039;X-Forwarded-Proto: https&#039; -O- http://127.0.0.1:5000/health || exit 1
</pre></div>


<h2 class="wp-block-heading">Step 3: the Traefik label</h2>



<p class="wp-block-paragraph">Those failures come from Traefik, not Swarm. Here&#8217;s the config Dokploy writes for its Traefik:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: yaml; title: ; notranslate">
providers:
  swarm:
    exposedByDefault: false
    watch: true
</pre></div>


<p class="wp-block-paragraph">No <code>refreshSeconds</code>, so the Swarm provider uses its default of 15 seconds. Traefik routes to each container&#8217;s own IP address and refreshes that list on its own schedule. To see it happen, I read Traefik&#8217;s backend list from its API every few hundred milliseconds during a redeploy:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
t=0s   traefik: 10.0.1.25  | containers: .25 healthy, .26 starting
t=10s  traefik: 10.0.1.25  | containers: .25 healthy, .26 healthy
t=14s  traefik: 10.0.1.25  | containers: .25 stopping, .26 healthy
t=15s  traefik: 10.0.1.26  | containers: .26 healthy
</pre></div>


<p class="wp-block-paragraph">The new container was healthy at 10 seconds and Traefik didn&#8217;t add it. At 14 seconds the old one was shutting down and Traefik was still sending it traffic. It caught up at 15. How long that gap lasts depends on where the switch lands in Traefik&#8217;s refresh cycle, which is why some runs lose nothing and others lose ten seconds.</p>



<p class="wp-block-paragraph">One label fixes it:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: yaml; title: ; notranslate">
deploy:
  labels:
    traefik.swarm.lbswarm: &quot;true&quot;
</pre></div>


<p class="wp-block-paragraph">With it, Traefik stops tracking individual containers. It sends every request to the service&#8217;s virtual IP, one address that stays the same across deploys, and Swarm&#8217;s own load balancer behind that address picks a container. Swarm is also the one starting and stopping the containers, so it takes the old one out of rotation as part of stopping it. There&#8217;s no second system that has to notice, so no window where traffic goes to an address that&#8217;s gone. Dokploy merges this label with the routing labels it generates, so nothing else needs adding.</p>



<p class="wp-block-paragraph"><strong>0 requests failed, across 10 deploys.</strong> Same app, same healthcheck, same probe. The only difference from the 7-out-of-11 above is this label.</p>



<p class="wp-block-paragraph">That name is for Traefik v3, which is what Dokploy ships. On Traefik v2 the label is <code>traefik.docker.lbswarm</code>.</p>



<h2 class="wp-block-heading">The results</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Setup</th><th>Failed requests</th><th>Window</th></tr></thead><tbody><tr><td>Dokploy defaults (<code>stop-first</code>)</td><td>145</td><td>~16s</td></tr><tr><td><code>start-first</code>, no healthcheck</td><td>50</td><td>~5s</td></tr><tr><td><code>start-first</code> + healthcheck</td><td>7 to 20, on 7 of 11 deploys</td><td>0.6s to 10s</td></tr><tr><td><code>start-first</code> + healthcheck + <code>lbswarm</code></td><td>0, on all 10 deploys</td><td>none</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">A few more things</h2>



<p class="wp-block-paragraph"><strong>Dokploy&#8217;s &#8220;Done&#8221; doesn&#8217;t mean it deployed.</strong> If the new container never becomes healthy, Swarm rolls back and the old one keeps serving, which is what you want. But Dokploy runs <code>docker stack deploy</code>, which returns as soon as the update is handed to Swarm, so its Deployments tab still shows <strong>Done</strong> in green. Ask Swarm instead:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
$ docker service inspect &lt;service&gt; --format &#039;{{.UpdateStatus.State}}&#039;
rollback_completed
</pre></div>


<p class="wp-block-paragraph"><strong>Make sure only one thing triggers a deploy.</strong> Swarm updates a service one change at a time. Each change is made against the service&#8217;s current version, and if two arrive at the same moment, the second is made against a version that no longer exists. Swarm rejects it instead of guessing how to combine them. I tested it by sending two updates to the lab service at the same instant. One went through, and the other failed with:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
Error response from daemon: rpc error: code = Unknown desc = update out of sequence
</pre></div>


<p class="wp-block-paragraph">You won&#8217;t do that by hand, but it&#8217;s easy to set up by accident: two things that each deploy when you push. For example, a CI pipeline that calls Dokploy&#8217;s deploy webhook after building the image, <em>and</em> Dokploy&#8217;s own <strong>Autodeploy</strong>, which deploys on every push to the branch. One push, two deploys a second apart, and one of them fails.</p>



<p class="wp-block-paragraph">The fix is to pick one trigger. If CI calls the webhook, switch <strong>Autodeploy</strong> off on the service in Dokploy, or set it to deploy only on tags, so the webhook is the only thing that starts a deploy.</p>



<p class="wp-block-paragraph"><strong>The old and new versions overlap.</strong> With <code>start-first</code>, both containers run at the same time for a few seconds, against the same database. If your new version&#8217;s migration drops or renames a column the old version still reads, the old container starts erroring during the window that was supposed to be safe. So add first, deploy, and remove the old column in a later release.</p>



<p class="wp-block-paragraph">And if your app has slow requests, <code>stop_grace_period</code> gives the old container longer than the default 10 seconds to finish them after it&#8217;s told to stop.</p>



<h2 class="wp-block-heading">The final compose file</h2>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: yaml; title: ; notranslate">
services:
  web:
    image: httpd:alpine
    command: &#x5B;&quot;sh&quot;, &quot;-c&quot;, &quot;sleep 8; echo $$VERSION &gt; /usr/local/apache2/htdocs/index.html; exec httpd-foreground&quot;]
    environment:
      VERSION: v6
    healthcheck:
      test: &#x5B;&quot;CMD-SHELL&quot;, &quot;wget -q -O- http://127.0.0.1/ || exit 1&quot;]
      interval: 5s
      timeout: 3s
      retries: 3
      start_period: 20s
    deploy:
      labels:
        traefik.swarm.lbswarm: &quot;true&quot;
      update_config:
        order: start-first
        failure_action: rollback
</pre></div>


<p class="wp-block-paragraph">When you&#8217;re done, one command removes the whole lab:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
docker rm -f -v dokploy-lab
</pre></div>


<p class="wp-block-paragraph">Thanks !!!</p>
<p>The post <a href="https://swacblooms.com/zero-downtime-deployments-in-dokploy-with-docker-swarm/">Zero-downtime deployments in Dokploy with Docker Swarm</a> appeared first on <a href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Cloudflare Tunnel to Postgres, plus Swarm’s static IP problem</title>
		<link>https://swacblooms.com/cloudflare-tunnel-to-postgres-plus-swarms-static-ip-problem/?utm_source=rss&amp;utm_medium=rss&amp;utm_campaign=cloudflare-tunnel-to-postgres-plus-swarms-static-ip-problem</link>
		
		<dc:creator><![CDATA[Samson Amaugo]]></dc:creator>
		<pubDate>Mon, 24 Aug 2026 07:31:37 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<guid isPermaLink="false">https://swacblooms.com/?p=1890</guid>

					<description><![CDATA[<p>I wanted to query a Postgres database on my VPS from VS Code without opening a port or publishing any DNS record. Cloudflare Tunnel plus WARP can do this, but the nice hostname feature turned out to be locked behind a closed beta. This post covers the workaround I used (running my own tiny DNS server on the Docker network), and along the way, how to get a fixed IP for a container in Docker Swarm, which officially isn&#8217;t possible. The setup Docker Swarm on one VPS. Postgres and cloudflared on an overlay network, nothing published: On the Cloudflare side, </p>
<p>The post <a href="https://swacblooms.com/cloudflare-tunnel-to-postgres-plus-swarms-static-ip-problem/">Cloudflare Tunnel to Postgres, plus Swarm&#8217;s static IP problem</a> appeared first on <a href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">I wanted to query a Postgres database on my VPS from VS Code without opening a port or publishing any DNS record. Cloudflare Tunnel plus WARP can do this, but the nice hostname feature turned out to be locked behind a closed beta. This post covers the workaround I used (running my own tiny DNS server on the Docker network), and along the way, how to get a fixed IP for a container in Docker Swarm, which officially isn&#8217;t possible.</p>



<h2 class="wp-block-heading">The setup</h2>



<p class="wp-block-paragraph">Docker Swarm on one VPS. Postgres and cloudflared on an overlay network, nothing published:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: yaml; title: ; notranslate">
services:
  postgres:
    image: postgres:18-alpine
    volumes:
      - postgres-data:/var/lib/postgresql   # not /data, postgres:18 changed this
    networks:
      internal-net:
        aliases:
          - postgres.internal.service   # the hostname I want to reach it by

  cloudflared:
    image: cloudflare/cloudflared:latest
    command: tunnel --no-autoupdate run --token ${CLOUDFLARE_TUNNEL_TOKEN}
    networks: &#x5B;internal-net]

networks:
  internal-net:
    driver: overlay
    attachable: true          # matters later
    ipam:
      config:
        - subnet: 10.55.0.0/24
</pre></div>


<p class="wp-block-paragraph">On the Cloudflare side, you tell the tunnel which addresses it owns: in Zero Trust under Networks → Routes, add a CIDR route for <code>10.55.0.0/24</code> pointing at this tunnel. That single route is what lets WARP reach anything on the overlay, database and DNS server alike.</p>



<p class="wp-block-paragraph">It&#8217;s also why the subnet is pinned in the compose file. Cloudflare stores that CIDR by value, so if Docker allocated the subnet dynamically, a recreated network could land on a different range and the route would silently point at nothing.</p>



<p class="wp-block-paragraph">So the flow is: cloudflared dials out to Cloudflare, WARP on the laptop routes <code>10.55.0.0/24</code> into the tunnel, Access policies decide who gets in. In theory you then add a &#8220;private hostname&#8221; route on top and you&#8217;re done.</p>



<h2 class="wp-block-heading">What actually broke</h2>



<p class="wp-block-paragraph"><strong>Split Tunnels blocks everything silently.</strong> WARP&#8217;s default exclude list contains <code>10.0.0.0/8</code>, which covers my overlay, plus most of <code>100.64.0.0/10</code>, the range Cloudflare itself uses for tunnel routing. Excluded means &#8220;don&#8217;t send through the tunnel&#8221;, so WARP treated my database like a local LAN address and the packets went nowhere. No error on either side. You have to delete those entries from the device profile.</p>



<p class="wp-block-paragraph"><strong>Gateway proxy needs UDP.</strong> The dashboard calls it &#8220;recommended&#8221;, the docs say it&#8217;s required for DNS to internal resolvers. Believe the docs.</p>



<p class="wp-block-paragraph"><strong>Don&#8217;t name anything <code>.local</code>.</strong> macOS hardwires <code>.local</code> to multicast DNS:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
$ scutil --dns | grep -A2 &quot;domain.*: local&quot;
resolver #2
  domain   : local
  options  : mdns
</pre></div>


<p class="wp-block-paragraph">The query never leaves your Mac, so no Cloudflare setting can fix it. Cloudflare&#8217;s own docs use <code>wiki.internal.local</code> as their example, which is a trap. <code>.internal</code> also fails, for a different reason: it&#8217;s in WARP&#8217;s Local Domain Fallback list, so queries get diverted to your local resolver. I ended up on a made-up suffix that&#8217;s on nobody&#8217;s special list.</p>



<p class="wp-block-paragraph"><strong>And after all that, private hostnames still didn&#8217;t resolve.</strong> Querying Gateway&#8217;s DoH endpoint directly (the only check that bypasses all the caching):</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
curl -s -H &#039;accept: application/dns-json&#039; \
  &quot;https://&lt;your-team&gt;.cloudflare-gateway.com/dns-query?name=db.internal&amp;type=A&quot;
</pre></div>

<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
{&quot;Status&quot;:3, &quot;Authority&quot;:&#x5B;{&quot;data&quot;:&quot;a.root-servers.net. ...&quot;}]}
</pre></div>


<p class="wp-block-paragraph">Gateway was sending my private hostname to the public DNS root instead of consulting my route. The reason was a banner on a different dashboard page: &#8220;Your account does not have access to Internal DNS. Contact your account team to join the closed beta.&#8221; Private hostname routes depend on that beta. Being account admin doesn&#8217;t help, it&#8217;s an entitlement, not a permission.</p>



<h2 class="wp-block-heading">The workaround: bring your own DNS</h2>



<p class="wp-block-paragraph">Cloudflare supports pointing Gateway at your own DNS server on any plan. And Docker already runs a DNS server that knows every service by name, at <code>127.0.0.11</code> inside each container. It&#8217;s not reachable from outside, but a container on the same network can forward to it.</p>



<p class="wp-block-paragraph">Important part: this container does <strong>not</strong> go into the Swarm stack. It&#8217;s a separate, plain Compose file that you run with <code>docker compose up -d</code> on the same host, attached to the Swarm network from outside. The reason is coming in the next section, but the short version is that Swarm won&#8217;t let you pin its IP and Compose will.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: yaml; title: ; notranslate">
# dns/compose.yaml, run with: docker compose up -d
name: myapp-dns

services:
  dns:
    image: 4km3/dnsmasq:2.90-r3
    command: -k --server=127.0.0.11 --no-resolv --log-queries --log-facility=-
    networks:
      internal-net:
        ipv4_address: 10.55.0.60
    restart: unless-stopped

networks:
  internal-net:
    external: true
    name: myapp_internal-net
</pre></div>


<p class="wp-block-paragraph">That&#8217;s the entire DNS server. 15MB image, half a megabyte of RAM, no config file. <code>--log-facility=-</code> is the flag you&#8217;ll wish you knew about: without it dnsmasq logs to syslog inside the container and <code>docker logs</code> shows nothing, which looks exactly like a broken container.</p>



<p class="wp-block-paragraph">Reaching dnsmasq needs no extra Cloudflare routing, by the way. It sits at <code>10.55.0.60</code>, inside the CIDR route you already created.</p>



<p class="wp-block-paragraph">Where does the hostname itself come from? The <code>aliases</code> line on the postgres service in the first compose file. Docker&#8217;s embedded DNS registers dotted aliases directly, so <code>postgres.internal.service</code> already resolves inside the network with no rewrite rules anywhere. dnsmasq just makes that answer reachable by Gateway.</p>



<p class="wp-block-paragraph">Then one resolver policy in Zero Trust: domain is <code>internal.service</code>, resolver <code>10.55.0.60:53</code>, marked Private on your virtual network. Matching the shared suffix instead of the full name means the policy also covers every subdomain, so a future <code>redis.internal.service</code> or <code>minio.internal.service</code> works the moment you add its alias. No dashboard changes ever again.</p>



<h2 class="wp-block-heading">The Swarm static IP problem</h2>



<p class="wp-block-paragraph">Notice the <code>ipv4_address: 10.55.0.60</code> above. The resolver policy references the DNS server by IP, so that address must never move. Here&#8217;s the catch: Docker Swarm can&#8217;t pin container IPs, and worse, it silently ignores the config. I tested it:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
    networks:
      mynet:
        ipv4_address: 10.66.0.50   # requested
</pre></div>


<p class="wp-block-paragraph">The container came up on <code>10.66.0.3</code>. No error, no warning. <a href="https://github.com/moby/moby/issues/24170">moby/moby#24170</a> has been open about this since 2016.</p>



<p class="wp-block-paragraph">That&#8217;s why the dnsmasq file above is a standalone Compose deployment and not a service in the stack. Standalone containers do honour <code>ipv4_address</code>, and plain <code>docker compose up -d</code> creates standalone containers. Two things make the combination work: the Swarm network is marked <code>attachable: true</code> (now you know why), and the Compose file references it as <code>external</code> instead of creating its own. One gotcha: Swarm prefixes everything a stack creates, so a network declared as <code>internal-net</code> in a stack called <code>myapp</code> really exists as <code>myapp_internal-net</code>, and that&#8217;s the name the compose file must use.</p>



<p class="wp-block-paragraph">If you forget <code>attachable</code>, at least this one fails loudly:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
PermissionDenied: network myapp_internal-net not manually attachable
</pre></div>


<p class="wp-block-paragraph">Trade-off: the DNS container isn&#8217;t part of <code>docker stack deploy</code> anymore, so deploys are two commands instead of one. Fine for one container, bad idea for half your stack.</p>



<h2 class="wp-block-heading">Debugging notes</h2>



<p class="wp-block-paragraph">Local tools lie. <code>dig</code> and <code>ping</code> serve cached answers for up to a minute after a change, and WARP caches on top of that. I twice &#8220;confirmed&#8221; something that was actually broken. The DoH curl above is the only honest check.</p>



<p class="wp-block-paragraph">Also, WARP doesn&#8217;t pick up policy or profile changes until you <code>warp-cli disconnect &amp;&amp; warp-cli connect</code>. I spent a while debugging a policy that was already correct.</p>



<h2 class="wp-block-heading">The result</h2>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
laptop → WARP → Cloudflare Gateway
                   ├── resolver policy: *.internal.service → 10.55.0.60
                   └── tunnel → cloudflared → overlay
                                    ├── dnsmasq (10.55.0.60) → Docker DNS
                                    └── postgres (10.55.0.5:5432)
</pre></div>


<p class="wp-block-paragraph"><code>postgres.internal.service:5432</code> in any database client, with WARP on. No open ports, no public DNS, and adding a new service to the network is one alias line. </p>



<p class="wp-block-paragraph">Thanks !!!</p>
<p>The post <a href="https://swacblooms.com/cloudflare-tunnel-to-postgres-plus-swarms-static-ip-problem/">Cloudflare Tunnel to Postgres, plus Swarm&#8217;s static IP problem</a> appeared first on <a href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>The Topological Sort</title>
		<link>https://swacblooms.com/the-topological-sort/?utm_source=rss&amp;utm_medium=rss&amp;utm_campaign=the-topological-sort</link>
		
		<dc:creator><![CDATA[Samson Amaugo]]></dc:creator>
		<pubDate>Mon, 03 Aug 2026 11:03:39 +0000</pubDate>
				<category><![CDATA[EDUCATION]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://swacblooms.com/?p=1874</guid>

					<description><![CDATA[<p>Let&#8217;s say you&#8217;re cooking jollof rice for a party. There is an order to things, and that order is sacred: Blend peppers → Fry the stew → Combine with parboiled rice → Simmer → Serve You cannot combine the rice with a stew that doesn&#8217;t exist yet. You cannot serve rice that&#8217;s still crunchy. And if you fry the plantain too early, it will be cold and sad by serving time, and people will talk about you. But here&#8217;s the thing. A real cook doesn&#8217;t do these strictly one at a time. While the stew is frying on one burner, </p>
<p>The post <a href="https://swacblooms.com/the-topological-sort/">The Topological Sort</a> appeared first on <a href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Let&#8217;s say you&#8217;re cooking jollof rice for a party. There is an order to things, and that order is sacred:</p>



<p class="wp-block-paragraph"><code>Blend peppers → Fry the stew → Combine with parboiled rice → Simmer → Serve</code></p>



<p class="wp-block-paragraph">You cannot combine the rice with a stew that doesn&#8217;t exist yet. You cannot serve rice that&#8217;s still crunchy. And if you fry the plantain too early, it will be cold and sad by serving time, and people will talk about you.</p>



<p class="wp-block-paragraph">But here&#8217;s the thing. A real cook doesn&#8217;t do these strictly one at a time. While the stew is frying on one burner, the rice is parboiling on another. That&#8217;s not cheating; that&#8217;s efficiency. Every party cook in your family is secretly running a topological sort in their head, a parallel one at that, and they didn&#8217;t even go to computer science school for it.</p>



<p class="wp-block-paragraph">Now imagine you&#8217;re not a human cooking jollof, but a build system trying to figure out which files to compile first or a package manager installing dependencies so your app doesn&#8217;t crumble like a house built on sand</p>



<p class="wp-block-paragraph">This is where Kahn&#8217;s Algorithm comes in. It&#8217;s a beautifully simple way to sort things that depend on other things, using nothing but graphs, patience, and a queue.</p>



<p class="wp-block-paragraph">Here&#8217;s what our jollof &#8220;dependency graph&#8221; actually looks like. Notice it&#8217;s not a straight line, it&#8217;s more like a river with tributaries joining up:</p>



<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" width="1024" height="367" src="https://swacblooms.com/wp-content/uploads/2026/08/image-2-1024x367.png" alt="" class="wp-image-1879" srcset="https://swacblooms.com/wp-content/uploads/2026/08/image-2-1024x367.png 1024w, https://swacblooms.com/wp-content/uploads/2026/08/image-2-300x108.png 300w, https://swacblooms.com/wp-content/uploads/2026/08/image-2-768x275.png 768w, https://swacblooms.com/wp-content/uploads/2026/08/image-2.png 1096w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">Three of these tasks have no arrows pointing at them: &#8220;Blend Peppers&#8221;, &#8220;Parboil Rice&#8221;, and &#8220;Fry Plantain&#8221;. In graph terms, they have an <strong>in-degree of 0</strong>. No prerequisites, free to start immediately. And since none of them depends on each other, they can all happen at the same time on different burners. Meanwhile, &#8220;Combine Rice + Stew&#8221; has an in-degree of 2, so it&#8217;s blocked until both the stew and the rice are ready, no matter how impatient you are.</p>



<h2 class="wp-block-heading">First, What&#8217;s a Topological Sort?</h2>



<p class="wp-block-paragraph">A topological sort takes a <strong>Directed Acyclic Graph</strong> (DAG, fancy term, don&#8217;t panic) and lines up all the nodes in an order where every arrow points forward. If there&#8217;s an arrow from A to B, then A has to show up before B in the final lineup. Like a very strict, very orderly buffet line.</p>



<p class="wp-block-paragraph">Two rules for the graph:</p>



<ol class="wp-block-list">
<li><strong>Directed</strong>: the arrows only go one way. Stew comes before rice, not the other way around, unless you enjoy chaos and soggy disappointment.</li>



<li><strong>Acyclic</strong>: no loops. If the rice depends on the stew and the stew somehow depends on the rice, you&#8217;ve created a paradox, and nobody at this party is eating tonight.</li>
</ol>



<h2 class="wp-block-heading">Kahn&#8217;s Algorithm</h2>



<p class="wp-block-paragraph">Kahn&#8217;s Algorithm is basically how a well-run kitchen operates. Here&#8217;s the plan in plain English:</p>



<ol class="wp-block-list">
<li><strong>Count the &#8220;in-degree&#8221;</strong> of every task, meaning how many arrows are pointing at it. &#8220;Blend peppers&#8221; has zero prerequisites. &#8220;Serve&#8221; has a whole queue of them.</li>



<li><strong>Find the tasks with in-degree 0.</strong> These have no prerequisites and can start immediately. Blending peppers doesn&#8217;t wait for anybody.</li>



<li><strong>Throw those tasks in a queue</strong> and process them one at a time. When you finish a task, remove its outgoing arrows. This might unlock new tasks whose in-degree just dropped to 0, so throw those in the queue as well. Once the stew is fried, the rice is cleared for entry.</li>



<li><strong>Repeat</strong> until the queue is empty.</li>



<li><strong>If you processed every task, you have your cooking order.</strong> If some tasks never got processed, there&#8217;s a cycle somewhere. A group of tasks stuck in an infinite &#8220;no, you first&#8221; standoff, like two aunties at the door insisting the other one enter first.</li>
</ol>



<p class="wp-block-paragraph">That&#8217;s it. No recursion required, no fancy backtracking. Just counting, queueing, and mild bookkeeping. It&#8217;s the algorithmic equivalent of prepping your ingredients before you turn on the stove.</p>



<h2 class="wp-block-heading">Why Do We Even Need This?</h2>



<p class="wp-block-paragraph">Because the real world is obsessed with dependencies:</p>



<ul class="wp-block-list">
<li><strong>Build systems</strong> (like <code>make</code>, MSBuild, or npm scripts) need to compile files in the right order.</li>



<li><strong>Task schedulers</strong> need to know Task B can&#8217;t start until Task A finishes.</li>



<li><strong>Course prerequisites</strong> in a university system.</li>



<li><strong>Package managers</strong> figuring out which dependency to install first.</li>



<li><strong>Spreadsheet formulas</strong>, where cell C1 can&#8217;t calculate until A1 and B1 are done.</li>
</ul>



<p class="wp-block-paragraph">Basically, anywhere you hear &#8220;you can&#8217;t do X until Y is finished&#8221;, Kahn&#8217;s Algorithm is quietly available to save the day, wearing a little apron.</p>



<h2 class="wp-block-heading">A Quick Note on Parallel Cooking</h2>



<p class="wp-block-paragraph">One nice bonus before we write any code. Everything sitting in the queue at the same moment has zero unfinished prerequisites, which means none of those tasks depends on each other. So they can all run in parallel. That&#8217;s why you can blend peppers, parboil rice, and fry plantain all at once on three burners without breaking anything.</p>



<p class="wp-block-paragraph">This is exactly how real systems parallelise work too. When npm installs 47 packages in seconds, or a build system compiles independent modules on all your CPU cores at once, it&#8217;s leaning on this same property of the graph. Your kitchen instincts and a modern CI/CD pipeline are running the same algorithm. The pipeline just doesn&#8217;t get to eat afterwards.</p>



<p class="wp-block-paragraph">For this article, though, we&#8217;ll keep the code simple and stick to the classic one-task-at-a-time version.</p>



<h2 class="wp-block-heading">The C# Code</h2>



<p class="wp-block-paragraph">Let&#8217;s build it. We&#8217;ll represent our graph as a dictionary of nodes to their list of neighbours, meaning &#8220;things that depend on me&#8221;.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
using System;
using System.Collections.Generic;
using System.Linq;

public class KahnsTopologicalSort
{
    // The graph: key = a task, value = list of tasks that depend on it
    private readonly Dictionary&lt;string, List&lt;string&gt;&gt; _graph = new();

    // Track how many prerequisites each task has (in-degree)
    private readonly Dictionary&lt;string, int&gt; _inDegree = new();

    public void AddNode(string node)
    {
        if (!_graph.ContainsKey(node))
        {
            _graph&#x5B;node] = new List&lt;string&gt;();
            _inDegree&#x5B;node] = 0;
        }
    }

    // &quot;from&quot; must happen before &quot;to&quot; (e.g., AddEdge(&quot;Fry Stew&quot;, &quot;Combine Rice + Stew&quot;))
    public void AddEdge(string from, string to)
    {
        AddNode(from);
        AddNode(to);

        _graph&#x5B;from].Add(to);
        _inDegree&#x5B;to]++;
    }

    public List&lt;string&gt; Sort()
    {
        var result = new List&lt;string&gt;();
        var queue = new Queue&lt;string&gt;();

        // Step 1: find every task with no prerequisites
        foreach (var node in _inDegree.Keys)
        {
            if (_inDegree&#x5B;node] == 0)
                queue.Enqueue(node);
        }

        // Step 2: process the queue like a head chef calling out orders
        while (queue.Count &gt; 0)
        {
            var current = queue.Dequeue();
            result.Add(current);

            foreach (var neighbor in _graph&#x5B;current])
            {
                _inDegree&#x5B;neighbor]--;

                // If this task&#039;s prerequisites are all cleared, it&#039;s go time
                if (_inDegree&#x5B;neighbor] == 0)
                    queue.Enqueue(neighbor);
            }
        }

        // Step 3: if we didn&#039;t process every task, there&#039;s a cycle
        if (result.Count != _graph.Count)
        {
            throw new InvalidOperationException(
                &quot;Cycle detected! Two tasks are stuck saying &#039;no, after you&#039; &quot; +
                &quot;forever, like aunties at a doorway. Go untangle your graph.&quot;);
        }

        return result;
    }
}

</pre></div>


<h3 class="wp-block-heading">Trying It Out: Cooking Jollof, Programmatically</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
var jollof = new KahnsTopologicalSort();

jollof.AddEdge(&quot;Blend Peppers&quot;, &quot;Fry Stew&quot;);
jollof.AddEdge(&quot;Fry Stew&quot;, &quot;Combine Rice + Stew&quot;);
jollof.AddEdge(&quot;Parboil Rice&quot;, &quot;Combine Rice + Stew&quot;);
jollof.AddEdge(&quot;Combine Rice + Stew&quot;, &quot;Simmer&quot;);
jollof.AddEdge(&quot;Simmer&quot;, &quot;Serve&quot;);
jollof.AddEdge(&quot;Fry Plantain&quot;, &quot;Serve&quot;);

var order = jollof.Sort();
Console.WriteLine(string.Join(&quot; -&gt; &quot;, order));
// Something like:
// Blend Peppers -&gt; Parboil Rice -&gt; Fry Plantain -&gt; Fry Stew
//   -&gt; Combine Rice + Stew -&gt; Simmer -&gt; Serve

</pre></div>


<p class="wp-block-paragraph">Notice how all three prerequisite-free tasks come out first, before anything that depends on them. The output isn&#8217;t uniquely determined either. &#8220;Fry Plantain&#8221; could show up anywhere before &#8220;Serve&#8221;, since nothing else depends on it. Kahn&#8217;s Algorithm guarantees a valid order, not the only valid order. Nobody said this algorithm was bossy.</p>



<h3 class="wp-block-heading">Bonus: Catching a Cycle</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
var brokenRecipe = new KahnsTopologicalSort();
brokenRecipe.AddEdge(&quot;Learn Grandma&#039;s Recipe&quot;, &quot;Cook the Jollof&quot;);
brokenRecipe.AddEdge(&quot;Cook the Jollof&quot;, &quot;Learn Grandma&#039;s Recipe&quot;);

try
{
    brokenRecipe.Sort();
}
catch (InvalidOperationException ex)
{
    Console.WriteLine(ex.Message);
    // &quot;Cycle detected! Two tasks are stuck saying &#039;no, after you&#039; forever...&quot;
}

</pre></div>


<p class="wp-block-paragraph">Grandma says you can&#8217;t cook the jollof until she&#8217;s taught you the recipe. Grandma also insists the only real way to learn the recipe is to cook it yourself. So cooking is waiting on learning, and learning is waiting on cooking. Neither task can ever start, neither in-degree ever reaches 0, the queue starts empty and stays empty, and Kahn&#8217;s Algorithm throws an exception. It&#8217;s the same energy as needing experience to get a job and a job to get experience. Some graphs cannot be sorted, only endured.</p>



<h2 class="wp-block-heading">Wrapping Up</h2>



<p class="wp-block-paragraph">Kahn&#8217;s Algorithm is really just:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">&#8220;Find the tasks with no prerequisites, do them, cross them off, see what opens up next, repeat.&#8221;</p>
</blockquote>



<p class="wp-block-paragraph">Which, honestly, is also how every great party cook operates. Three burners going, one eye on the stew, and a plan for what unlocks next. Whether you&#8217;re compiling code, scheduling parallel tasks, or making sure the stew and the rice meet each other at exactly the right moment, Kahn&#8217;s got your back. And now, so does your C# code.</p>



<p class="wp-block-paragraph">Go forth and sort responsibly. And keep the &#8220;tasters&#8221; away from the pot.</p>
<p>The post <a href="https://swacblooms.com/the-topological-sort/">The Topological Sort</a> appeared first on <a href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Building a Perceptron in .NET</title>
		<link>https://swacblooms.com/building-a-perceptron-in-net/?utm_source=rss&amp;utm_medium=rss&amp;utm_campaign=building-a-perceptron-in-net</link>
		
		<dc:creator><![CDATA[Samson Amaugo]]></dc:creator>
		<pubDate>Mon, 27 Apr 2026 07:12:37 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<guid isPermaLink="false">https://swacblooms.com/?p=1860</guid>

					<description><![CDATA[<p>Machine learning can feel intimidating, but at its core it starts with something surprisingly simple: the&#160;perceptron. It&#8217;s the oldest and most basic building block of neural networks, and building one from scratch is the best way to understand what&#8217;s actually happening under the hood. In this post we&#8217;ll build a perceptron in C#, train it on the famous Iris dataset, and understand every line along the way. The Algorithm — Before Any Code The basic idea Imagine you&#8217;re trying to teach a friend to tell apart apples and oranges, but they can only feel the fruit — not see it. </p>
<p>The post <a href="https://swacblooms.com/building-a-perceptron-in-net/">Building a Perceptron in .NET</a> appeared first on <a href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Machine learning can feel intimidating, but at its core it starts with something surprisingly simple: the&nbsp;<strong>perceptron</strong>. It&#8217;s the oldest and most basic building block of neural networks, and building one from scratch is the best way to understand what&#8217;s actually happening under the hood.</p>



<p class="wp-block-paragraph">In this post we&#8217;ll build a perceptron in C#, train it on the famous Iris dataset, and understand every line along the way.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="the-algorithm--before-any-code">The Algorithm — Before Any Code</h2>



<h3 class="wp-block-heading" id="the-basic-idea">The basic idea</h3>



<p class="wp-block-paragraph">Imagine you&#8217;re trying to teach a friend to tell apart apples and oranges, but they can only feel the fruit — not see it. You let them feel the weight and the texture, and each time they guess wrong, you say &#8220;warmer&#8221; or &#8220;colder&#8221; and they adjust their judgment slightly.</p>



<p class="wp-block-paragraph">That&#8217;s exactly what a perceptron does. It looks at numbers (the measurements), makes a guess, gets told whether it was right or wrong, and nudges itself to do better next time.</p>



<h3 class="wp-block-heading" id="what-learning-actually-means">What &#8220;learning&#8221; actually means</h3>



<p class="wp-block-paragraph">The perceptron has two things it can adjust:</p>



<ul class="wp-block-list">
<li><strong>Weights</strong>&nbsp;— one number per input feature. A high weight means &#8220;this feature matters a lot&#8221;. A weight close to zero means &#8220;this feature barely matters&#8221;.</li>



<li><strong>Bias</strong>&nbsp;— a single extra number that shifts the decision up or down, independent of the inputs.</li>
</ul>



<p class="wp-block-paragraph">At the start, weights are random small numbers. The perceptron has no idea what it&#8217;s doing. After seeing enough examples and correcting itself each time, the weights settle into values that produce the right answer.</p>



<h3 class="wp-block-heading" id="the-three-steps-it-repeats">The three steps it repeats</h3>



<p class="wp-block-paragraph">For every example in the training data, the perceptron does exactly three things:</p>



<p class="wp-block-paragraph"><strong>1. Make a prediction</strong></p>



<p class="wp-block-paragraph">Multiply each input by its weight, add them all up, add the bias. If the total is 0 or above, predict class 1. If below zero, predict class 0.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
prediction = (input₁ × weight₁) + (input₂ × weight₂) + bias  ≥ 0 ?  → 1 : 0

</pre></div>


<p class="wp-block-paragraph"><strong>2. Calculate the error</strong></p>



<p class="wp-block-paragraph">Compare the prediction to the correct answer:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
error = correct answer - prediction

</pre></div>


<p class="wp-block-paragraph">If the prediction was right, error = 0 and nothing changes. If it predicted 0 but the answer was 1, error = +1 → weights need to go up. If it predicted 1 but the answer was 0, error = -1 → weights need to come down.</p>



<p class="wp-block-paragraph"><strong>3. Update the weights</strong></p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
new weight = old weight + (learning rate × error × input)
new bias   = old bias   + (learning rate × error)

</pre></div>


<p class="wp-block-paragraph">The learning rate is a small number (like 0.1) that controls how aggressively it corrects itself. Too large and it overcorrects and oscillates. Too small and it learns very slowly.</p>



<p class="wp-block-paragraph">Notice that&nbsp;<strong>inputs with larger values cause larger weight updates</strong>. If one feature is always tiny, it won&#8217;t influence the weights much. This is intentional, big inputs carry more signal.</p>



<h3 class="wp-block-heading" id="a-concrete-example">A concrete example</h3>



<p class="wp-block-paragraph">Say we have one flower with sepal length 5.1 and petal length 1.4, and it&#8217;s a Setosa (correct answer = 0).</p>



<ul class="wp-block-list">
<li>Current weights:&nbsp;<code>[0.02, -0.01]</code>, bias:&nbsp;<code>0.0</code></li>



<li>Net input:&nbsp;<code>(5.1 × 0.02) + (1.4 × -0.01) + 0.0 = 0.102 - 0.014 = 0.088</code></li>



<li>Prediction:&nbsp;<code>0.088 ≥ 0</code>&nbsp;→ predict&nbsp;<strong>1</strong></li>



<li>Error:&nbsp;<code>0 - 1 = -1</code>&nbsp;(wrong — should have been 0)</li>



<li>Update with learning rate 0.1:
<ul class="wp-block-list">
<li><code>weight₁ = 0.02 + (0.1 × -1 × 5.1) = 0.02 - 0.51 = -0.49</code></li>



<li><code>weight₂ = -0.01 + (0.1 × -1 × 1.4) = -0.01 - 0.14 = -0.15</code></li>



<li><code>bias = 0.0 + (0.1 × -1) = -0.1</code></li>
</ul>
</li>
</ul>



<p class="wp-block-paragraph">Next time it sees this flower, the lower weights will produce a lower net input, making it less likely to predict 1. Over many examples it finds the right balance.</p>



<h3 class="wp-block-heading" id="when-does-it-stop-learning">When does it stop learning?</h3>



<p class="wp-block-paragraph">Either after a fixed number of passes through the data (epochs), or when it makes zero mistakes on a full pass, meaning it has found weights that correctly classify every example.</p>



<p class="wp-block-paragraph">This is only guaranteed to happen if the two classes can be separated by a straight line. If they can&#8217;t, the perceptron will never settle, it will keep adjusting forever. That limitation is what led to the invention of multi-layer neural networks.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="what-is-a-perceptron">What is a Perceptron?</h2>



<p class="wp-block-paragraph">A perceptron is a binary classifier,  it looks at some input numbers and decides:&nbsp;<strong>is this thing in group A or group B?</strong></p>



<p class="wp-block-paragraph">Think of it like a light switch with a dimmer. You feed it some numbers, it adds them all up (with some weights applied), and if the total is high enough, the switch flips on (class 1). If not, it stays off (class 0).</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
input 1 × weight 1 ┐
input 2 × weight 2 ├──→ sum + bias ──→ is it ≥ 0? ──→ yes=1 / no=0
input 3 × weight 3 ┘

</pre></div>


<p class="wp-block-paragraph">The clever part: if it gets the answer wrong, it&nbsp;<strong>adjusts the weights</strong>&nbsp;slightly so it does better next time. Do this enough times and it learns to classify correctly.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="the-dataset">The Dataset</h2>



<p class="wp-block-paragraph">We&#8217;ll use the&nbsp;<strong>Iris dataset</strong>, a classic. It contains measurements of 150 iris flowers across three species. We&#8217;ll only use two species (Setosa and Versicolor) to keep it a binary problem, and two features: sepal length and petal length.</p>



<p class="wp-block-paragraph">Let&#8217;s load it directly from the UCI repository:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
static async Task&lt;(double&#x5B;]&#x5B;] X, int&#x5B;] y)&gt; LoadIrisAsync()
{
    const string url = &quot;https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data&quot;;
    using var client = new HttpClient();
    string csv = await client.GetStringAsync(url);

    var X = new List&lt;double&#x5B;]&gt;();
    var y = new List&lt;int&gt;();

    foreach (var line in csv.Split(&#039;\n&#039;))
    {
        var parts = line.Trim().Split(&#039;,&#039;);
        if (parts.Length &lt; 5) continue;

        string label = parts&#x5B;4];
        if (label != &quot;Iris-setosa&quot; &amp;&amp; label != &quot;Iris-versicolor&quot;) continue;

        // sepal length (col 0) and petal length (col 2)
        X.Add(&#x5B;double.Parse(parts&#x5B;0]), double.Parse(parts&#x5B;2])]);
        y.Add(label == &quot;Iris-setosa&quot; ? 0 : 1);
    }

    return (X.ToArray(), y.ToArray());
}

</pre></div>


<p class="wp-block-paragraph"><code>X</code>&nbsp;is our feature matrix, each row is one flower, each column is one measurement.&nbsp;<code>y</code>&nbsp;is our label array — 0 for Setosa, 1 for Versicolor.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="the-perceptron-class">The Perceptron Class</h2>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
public class Perceptron(double eta = 0.01, int epochs = 50, int randomState = 1)

</pre></div>


<p class="wp-block-paragraph">Three parameters:</p>



<ul class="wp-block-list">
<li><strong><code>eta</code></strong>&nbsp;(learning rate) — how big a step the perceptron takes when it adjusts weights. Too high and it overshoots; too low and it learns painfully slowly.</li>



<li><strong><code>epochs</code></strong>&nbsp;— how many full passes through the training data.</li>



<li><strong><code>randomState</code></strong>&nbsp;— a seed for the random number generator so results are reproducible.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="step-1-initialise-the-weights">Step 1: Initialise the Weights</h2>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
Weights = new double&#x5B;X&#x5B;0].Length];
for (int i = 0; i &lt; Weights.Length; i++)
    Weights&#x5B;i] = NextGaussian(rng, mean: 0.0, stdDev: 0.01);

Bias = 0.0;

</pre></div>


<p class="wp-block-paragraph">We start with tiny random weights (very close to zero) rather than all zeros. If everything started at zero, every weight would update by exactly the same amount each step and they&#8217;d never diverge — the perceptron would never learn different things from different features.</p>



<p class="wp-block-paragraph"><code><strong>Bias</strong></code>&nbsp;is  an extra number added to the sum that lets the decision boundary shift away from the origin.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="step-2-train-the-fit-loop">Step 2: Train (the Fit loop)</h2>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
for (int _ = 0; _ &lt; epochs; _++)
{
    int errors = 0;
    for (int i = 0; i &lt; X.Length; i++)
    {
        double update = eta * (y&#x5B;i] - Predict(X&#x5B;i]));
        for (int j = 0; j &lt; Weights.Length; j++)
            Weights&#x5B;j] += update * X&#x5B;i]&#x5B;j];
        Bias += update;
        if (update != 0.0) errors++;
    }
    ErrorsPerEpoch.Add(errors);
}

</pre></div>


<p class="wp-block-paragraph">For each flower in the training data:</p>



<ol class="wp-block-list">
<li><strong>Make a prediction</strong>&nbsp;— does it think this is Setosa (0) or Versicolor (1)?</li>



<li><strong>Calculate the error</strong>&nbsp;—&nbsp;<code>target - prediction</code>. If it was right, this is 0. If wrong, it&#8217;s either +1 or -1.</li>



<li><strong>Adjust the weights</strong>&nbsp;— multiply the error by the learning rate and by each input feature, then add that to the weight. Features that were large get a bigger nudge.</li>



<li><strong>Adjust the bias</strong>&nbsp;— same idea, just without the feature multiplication.</li>
</ol>



<p class="wp-block-paragraph">If&nbsp;<code>update</code>&nbsp;is 0 the prediction was correct and nothing changes. The perceptron only learns from its mistakes.</p>



<p class="wp-block-paragraph">After each epoch we record how many mistakes were made (<code>ErrorsPerEpoch</code>). A well-trained perceptron will show this number trending down toward zero.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="step-3-predict">Step 3: Predict</h2>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
public double NetInput(double&#x5B;] X)
{
    double result = Bias;
    for (int i = 0; i &lt; Weights.Length; i++)
        result += X&#x5B;i] * Weights&#x5B;i];
    return result;
}

public int Predict(double&#x5B;] X) =&gt; NetInput(X) &gt;= 0.0 ? 1 : 0;

</pre></div>


<p class="wp-block-paragraph"><code>NetInput</code>&nbsp;is the weighted sum: multiply each feature by its weight, add the bias, and return the total.</p>



<p class="wp-block-paragraph"><code>Predict</code>&nbsp;applies the&nbsp;<strong>unit step function</strong>: if the sum is 0 or above, predict class 1; otherwise predict class 0. That&#8217;s the threshold the perceptron uses to make its decision.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="putting-it-all-together">Putting it all together</h2>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
var (X, y) = await LoadIrisAsync();

var ppn = new Perceptron(eta: 0.1, epochs: 10, randomState: 42);
ppn.Fit(X, y);

Console.WriteLine(&quot;Weights: &quot; + string.Join(&quot;, &quot;, ppn.Weights.Select(w =&gt; w.ToString(&quot;F4&quot;))));
Console.WriteLine(&quot;Bias: &quot; + ppn.Bias);
Console.WriteLine(&quot;Errors per epoch: &quot; + string.Join(&quot;, &quot;, ppn.ErrorsPerEpoch));
Console.WriteLine(&quot;Prediction for &#x5B;5.1, 1.4]: &quot; + ppn.Predict(&#x5B;5.1, 1.4]));

</pre></div>


<p class="wp-block-paragraph">Run this and you&#8217;ll see something like:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
Loaded 100 samples from UCI
Weights: -0.3715, 0.9307
Bias: -0.2
Errors per epoch: 1, 3, 3, 2, 1, 0, 0, 0, 0, 0
Prediction for &#x5B;5.1, 1.4]: 0

</pre></div>


<p class="wp-block-paragraph">The errors start at 1, climb slightly as the weights adjust, then settle to zero by epoch 6, the perceptron has learned to correctly separate the two flower species. The prediction of 0 for&nbsp;<code>[5.1, 1.4]</code>&nbsp;is correct — that&#8217;s a Setosa flower.</p>



<figure class="wp-block-image size-full"><img decoding="async" width="700" height="450" src="https://swacblooms.com/wp-content/uploads/2026/04/errors_per_epoch.png" alt="" class="wp-image-1861" style="aspect-ratio:1" srcset="https://swacblooms.com/wp-content/uploads/2026/04/errors_per_epoch.png 700w, https://swacblooms.com/wp-content/uploads/2026/04/errors_per_epoch-300x193.png 300w" sizes="(max-width: 700px) 100vw, 700px" /></figure>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="why-does-this-work">Why Does This Work?</h2>



<p class="wp-block-paragraph">The two flower species happen to be&nbsp;<strong>linearly separable</strong>: if you plot them on a graph by sepal length and petal length, you can draw a straight line that separates them cleanly. The perceptron finds that line.</p>



<p class="wp-block-paragraph">This is also the perceptron&#8217;s main limitation: if the data can&#8217;t be separated by a straight line, it will never converge. That&#8217;s what motivated the invention of multi-layer neural networks — stacking perceptrons so they can learn curved, complex boundaries.</p>



<p class="wp-block-paragraph">But for linearly separable problems, the original 1958 perceptron still works perfectly.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="full-code">Full Code</h2>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
var (X, y) = await LoadIrisAsync();

var ppn = new Perceptron(eta: 0.1, epochs: 10, randomState: 42);
ppn.Fit(X, y);

Console.WriteLine(&quot;Weights: &quot; + string.Join(&quot;, &quot;, ppn.Weights.Select(w =&gt; w.ToString(&quot;F4&quot;))));
Console.WriteLine(&quot;Bias: &quot; + ppn.Bias);
Console.WriteLine(&quot;Errors per epoch: &quot; + string.Join(&quot;, &quot;, ppn.ErrorsPerEpoch));
Console.WriteLine(&quot;Prediction for &#x5B;5.1, 1.4]: &quot; + ppn.Predict(&#x5B;5.1, 1.4]));

static async Task&lt;(double&#x5B;]&#x5B;] X, int&#x5B;] y)&gt; LoadIrisAsync()
{
    const string url = &quot;https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data&quot;;
    using var client = new HttpClient();
    string csv = await client.GetStringAsync(url);

    var X = new List&lt;double&#x5B;]&gt;();
    var y = new List&lt;int&gt;();

    foreach (var line in csv.Split(&#039;\n&#039;))
    {
        var parts = line.Trim().Split(&#039;,&#039;);
        if (parts.Length &lt; 5) continue;

        string label = parts&#x5B;4];
        if (label != &quot;Iris-setosa&quot; &amp;&amp; label != &quot;Iris-versicolor&quot;) continue;

        X.Add(&#x5B;double.Parse(parts&#x5B;0]), double.Parse(parts&#x5B;2])]);
        y.Add(label == &quot;Iris-setosa&quot; ? 0 : 1);
    }

    return (X.ToArray(), y.ToArray());
}

public class Perceptron(double eta = 0.01, int epochs = 50, int randomState = 1)
{
    public double&#x5B;] Weights { get; private set; } = &#x5B;];
    public double Bias { get; private set; }
    public List&lt;int&gt; ErrorsPerEpoch { get; private set; } = &#x5B;];

    public Perceptron Fit(double&#x5B;]&#x5B;] X, int&#x5B;] y)
    {
        var rng = new Random(randomState);
        Weights = new double&#x5B;X&#x5B;0].Length];
        for (int i = 0; i &lt; Weights.Length; i++)
            Weights&#x5B;i] = NextGaussian(rng, mean: 0.0, stdDev: 0.01);

        Bias = 0.0;
        ErrorsPerEpoch = &#x5B;];

        for (int _ = 0; _ &lt; epochs; _++)
        {
            int errors = 0;
            for (int i = 0; i &lt; X.Length; i++)
            {
                double update = eta * (y&#x5B;i] - Predict(X&#x5B;i]));
                for (int j = 0; j &lt; Weights.Length; j++)
                    Weights&#x5B;j] += update * X&#x5B;i]&#x5B;j];
                Bias += update;
                if (update != 0.0) errors++;
            }
            ErrorsPerEpoch.Add(errors);
        }
        return this;
    }

    public double NetInput(double&#x5B;] X)
    {
        double result = Bias;
        for (int i = 0; i &lt; Weights.Length; i++)
            result += X&#x5B;i] * Weights&#x5B;i];
        return result;
    }

    public int Predict(double&#x5B;] X) =&gt; NetInput(X) &gt;= 0.0 ? 1 : 0;

    private static double NextGaussian(Random rng, double mean, double stdDev)
    {
        double u1 = 1.0 - rng.NextDouble();
        double u2 = 1.0 - rng.NextDouble();
        double z = Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2);
        return mean + stdDev * z;
    }
}
</pre></div>


<p class="has-large-font-size wp-block-paragraph"><strong>Explore SharpLsp</strong> </p>



<p class="wp-block-paragraph">SharpLsp is a modern language server built on a three-tier architecture with a Rust host for ultra-fast syntax parsing and .NET sidecars for rich semantic analysis. It supports C# and F# with features like completions, diagnostics, refactoring, semantic tokens, and project system integration. Check out the link below to try it out!!</p>



<p class="wp-block-paragraph"><a href="https://sharplsp.dev/docs">https://sharplsp.dev/docs</a></p>
<p>The post <a href="https://swacblooms.com/building-a-perceptron-in-net/">Building a Perceptron in .NET</a> appeared first on <a href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Part 4 — Assertions: Verifying What You See</title>
		<link>https://swacblooms.com/part-4-assertions-verifying-what-you-see/?utm_source=rss&amp;utm_medium=rss&amp;utm_campaign=part-4-assertions-verifying-what-you-see</link>
		
		<dc:creator><![CDATA[Samson Amaugo]]></dc:creator>
		<pubDate>Mon, 20 Apr 2026 06:18:05 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<guid isPermaLink="false">https://swacblooms.com/?p=1856</guid>

					<description><![CDATA[<p>Introduction In Part 3, you learned how to interact with the page using actions like&#160;click(),&#160;fill(), and&#160;check(). Now you need to prove those actions produced the right result. That is where assertions come in. An assertion is a check that confirms something is true. In Playwright, assertions are built on&#160;expect()&#160;and are designed for the web. Instead of checking once and failing immediately, they retry for a short period while the UI catches up. This is one of the biggest reasons Playwright tests are reliable. The&#160;expect()&#160;API at a Glance You already saw this pattern: The structure is always the same: For UI </p>
<p>The post <a href="https://swacblooms.com/part-4-assertions-verifying-what-you-see/">Part 4 — Assertions: Verifying What You See</a> appeared first on <a href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading" id="introduction">Introduction</h2>



<p class="wp-block-paragraph">In Part 3, you learned how to interact with the page using actions like&nbsp;<code>click()</code>,&nbsp;<code>fill()</code>, and&nbsp;<code>check()</code>. Now you need to prove those actions produced the right result.</p>



<p class="wp-block-paragraph">That is where assertions come in.</p>



<p class="wp-block-paragraph">An assertion is a check that confirms something is true. In Playwright, assertions are built on&nbsp;<code>expect()</code>&nbsp;and are designed for the web. Instead of checking once and failing immediately, they retry for a short period while the UI catches up.</p>



<p class="wp-block-paragraph">This is one of the biggest reasons Playwright tests are reliable.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="the-expect-api-at-a-glance">The&nbsp;<code>expect()</code>&nbsp;API at a Glance</h2>



<p class="wp-block-paragraph">You already saw this pattern:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await expect(page).toHaveTitle(/Playwright/);

</pre></div>


<p class="wp-block-paragraph">The structure is always the same:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await expect(target).matcher(value)

</pre></div>


<ul class="wp-block-list">
<li><code>target</code> is what you are asserting on (<code>page</code>, a <code>locator</code>, a response, etc.)</li>



<li><code>matcher</code> is the condition (<code>toBeVisible</code>, <code>toHaveText</code>, <code>toHaveURL</code>, &#8230;)</li>



<li><code>value</code> is the expected value when required</li>
</ul>



<p class="wp-block-paragraph">For UI tests, your most common&nbsp;<code>target</code>&nbsp;is a locator:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await expect(page.getByRole(&#039;heading&#039;, { name: &#039;Dashboard&#039; })).toBeVisible();

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="web-first-assertions-auto-retry-by-default">Web-First Assertions (Auto-Retry by Default)</h2>



<p class="wp-block-paragraph">Playwright assertions are web-first. That means they keep re-checking until either:</p>



<ol class="wp-block-list">
<li>The assertion passes, or</li>



<li>The assertion timeout is reached</li>
</ol>



<p class="wp-block-paragraph">So this:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await expect(page.getByText(&#039;Saved&#039;)).toBeVisible();

</pre></div>


<p class="wp-block-paragraph">does&nbsp;<strong>not</strong>&nbsp;mean &#8220;check once right now.&#8221; It means &#8220;wait for this to become visible within the assertion timeout.&#8221;</p>



<p class="wp-block-paragraph">Note: auto-retry applies to Playwright&#8217;s async web matchers (like&nbsp;<code>toBeVisible</code>&nbsp;and&nbsp;<code>toHaveText</code>). Generic value matchers such as&nbsp;<code>expect(value).toBe(...)</code>&nbsp;do not auto-retry.</p>



<p class="wp-block-paragraph">By default, Playwright uses a 5 second assertion timeout unless you configure it.</p>



<p class="wp-block-paragraph">You can override per assertion:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await expect(page.getByText(&#039;Saved&#039;)).toBeVisible({ timeout: 10_000 });

</pre></div>


<p class="wp-block-paragraph">or globally in config:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// playwright.config.js
export default defineConfig({
  expect: {
    timeout: 10_000,
  },
});

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="core-assertions-you-will-use-most">Core Assertions You Will Use Most</h2>



<h3 class="wp-block-heading" id="tohavetitle"><code>toHaveTitle()</code></h3>



<p class="wp-block-paragraph">Asserts the page title.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await expect(page).toHaveTitle(&#039;Settings&#039;);
await expect(page).toHaveTitle(/Settings/);

</pre></div>


<h3 class="wp-block-heading" id="tohaveurl"><code>toHaveURL()</code></h3>



<p class="wp-block-paragraph">Asserts the current URL.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await expect(page).toHaveURL(&#039;https://example.com/dashboard&#039;);
await expect(page).toHaveURL(/\/dashboard$/);

</pre></div>


<h3 class="wp-block-heading" id="tobevisible"><code>toBeVisible()</code></h3>



<p class="wp-block-paragraph">Checks that an element is visible to the user.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await expect(page.getByRole(&#039;alert&#039;)).toBeVisible();

</pre></div>


<h3 class="wp-block-heading" id="tohavetext"><code>toHaveText()</code></h3>



<p class="wp-block-paragraph">Checks exact or regex text content.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await expect(page.getByTestId(&#039;status&#039;)).toHaveText(&#039;Saved&#039;);
await expect(page.getByTestId(&#039;status&#039;)).toHaveText(/saved/i);

</pre></div>


<h3 class="wp-block-heading" id="tobeenabled"><code>toBeEnabled()</code></h3>



<p class="wp-block-paragraph">Checks that an element is enabled and can be interacted with.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await expect(page.getByRole(&#039;button&#039;, { name: &#039;Submit&#039; })).toBeEnabled();

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="a-realistic-end-to-end-example">A Realistic End-to-End Example</h2>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
import { test, expect } from &#039;@playwright/test&#039;;

test(&#039;user can save profile changes&#039;, async ({ page }) =&gt; {
  await page.goto(&#039;/profile&#039;);

  await page.getByLabel(&#039;Display name&#039;).fill(&#039;swacblooms&#039;);
  await page.getByRole(&#039;button&#039;, { name: &#039;Save changes&#039; }).click();

  await expect(page).toHaveURL(/\/profile$/);
  await expect(page.getByRole(&#039;status&#039;)).toHaveText(&#039;Changes saved&#039;);
  await expect(page.getByRole(&#039;button&#039;, { name: &#039;Save changes&#039; })).toBeEnabled();
});

</pre></div>


<p class="wp-block-paragraph">Notice there are no manual sleeps. Assertions do the waiting.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="negative-assertions">Negative Assertions</h2>



<p class="wp-block-paragraph">You can assert that something is&nbsp;<em>not</em>&nbsp;true using&nbsp;<code>.not</code>:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await expect(page.getByText(&#039;Loading...&#039;)).not.toBeVisible();
await expect(page.getByRole(&#039;button&#039;, { name: &#039;Delete&#039; })).not.toBeEnabled();

</pre></div>


<p class="wp-block-paragraph">Use negative assertions carefully. They are useful, but positive checks are often clearer when possible.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="soft-assertions">Soft Assertions</h2>



<p class="wp-block-paragraph">A regular assertion fails the test immediately. A soft assertion records the failure and allows the test to continue.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await expect.soft(page.getByTestId(&#039;first-name&#039;)).toHaveValue(&#039;Ada&#039;);
await expect.soft(page.getByTestId(&#039;last-name&#039;)).toHaveValue(&#039;Lovelace&#039;);
await expect.soft(page.getByTestId(&#039;email&#039;)).toHaveValue(&#039;ada@example.com&#039;);

// test continues even if one soft check fails
await page.getByRole(&#039;button&#039;, { name: &#039;Continue&#039; }).click();

</pre></div>


<p class="wp-block-paragraph">Soft assertions are useful when you want to collect multiple mismatches in one run, for example in long forms or summary pages.</p>



<p class="wp-block-paragraph">Use them intentionally. For critical checks, prefer normal assertions so failures stop immediately.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="polling-for-non-ui-conditions">Polling for Non-UI Conditions</h2>



<p class="wp-block-paragraph">Sometimes you need to verify something that is not a locator state, such as a value that changes over time.</p>



<p class="wp-block-paragraph">Use&nbsp;<code>expect.poll()</code>:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await expect
  .poll(async () =&gt; {
    const response = await page.request.get(&#039;https://example.com/api/jobs/123&#039;);
    const data = await response.json();
    return data.status;
  })
  .toBe(&#039;completed&#039;);

</pre></div>


<p class="wp-block-paragraph">Playwright keeps polling until the value becomes&nbsp;<code>'completed'</code>&nbsp;or times out.</p>



<p class="wp-block-paragraph">This gives you retry behavior for computed values, API state, and other async conditions.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="common-mistakes-to-avoid">Common Mistakes to Avoid</h2>



<ol class="wp-block-list">
<li><strong>Using manual sleeps instead of assertions</strong></li>
</ol>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// avoid
await page.waitForTimeout(2000);

// prefer
await expect(page.getByText(&#039;Saved&#039;)).toBeVisible();

</pre></div>


<ol start="2" class="wp-block-list">
<li><strong>Asserting raw values too early</strong></li>
</ol>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// fragile
const text = await page.getByTestId(&#039;status&#039;).textContent();
expect(text).toBe(&#039;Saved&#039;);

</pre></div>


<p class="wp-block-paragraph">Prefer Playwright locator assertions because they auto-retry:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await expect(page.getByTestId(&#039;status&#039;)).toHaveText(&#039;Saved&#039;);

</pre></div>


<ol start="3" class="wp-block-list">
<li><strong>Overusing soft assertions</strong></li>
</ol>



<p class="wp-block-paragraph">If everything is soft, failures can be easy to miss. Keep core checks hard.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="under-the-hood-why-retry-ability-matters">Under the Hood: Why Retry-ability Matters</h2>



<p class="wp-block-paragraph">Modern UIs are asynchronous.</p>



<ul class="wp-block-list">
<li>React/Vue/Angular re-render after network calls</li>



<li>buttons enable after validation</li>



<li>toast messages appear after state updates</li>
</ul>



<p class="wp-block-paragraph">If assertions checked only once, tests would be flaky.</p>



<p class="wp-block-paragraph">Playwright&#8217;s retry model aligns your checks with real browser timing. Instead of adding arbitrary sleeps, you describe the desired end state and let Playwright wait for it.</p>



<p class="wp-block-paragraph">That is the foundation of stable E2E tests.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="whats-next">What&#8217;s Next</h2>



<p class="wp-block-paragraph">In&nbsp;<strong>Part 5</strong>&nbsp;you will learn&nbsp;<code>playwright.config.js</code>&nbsp;in depth. You will configure retries, workers, projects,&nbsp;<code>baseURL</code>, traces, screenshots, and other settings that shape how your whole suite runs locally and in CI.</p>
<p>The post <a href="https://swacblooms.com/part-4-assertions-verifying-what-you-see/">Part 4 — Assertions: Verifying What You See</a> appeared first on <a href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Part 3 — Actions and Auto-Waiting: Putting Locators to Work</title>
		<link>https://swacblooms.com/part-3-actions-and-auto-waiting-putting-locators-to-work/?utm_source=rss&amp;utm_medium=rss&amp;utm_campaign=part-3-actions-and-auto-waiting-putting-locators-to-work</link>
		
		<dc:creator><![CDATA[Samson Amaugo]]></dc:creator>
		<pubDate>Mon, 13 Apr 2026 07:12:56 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<guid isPermaLink="false">https://swacblooms.com/?p=1853</guid>

					<description><![CDATA[<p>Introduction In Part 2 you learned how to find elements on the page. In this article, you will put those locators to work by performing actions on them — clicking, typing, hovering, selecting, uploading files, and more. You will also learn one of Playwright&#8217;s most important features:&#160;auto-waiting. Playwright does not blindly fire an action the moment you call it. It waits until the element is ready. This is why Playwright tests are far less flaky than tests written with older tools. Auto-Waiting — Why You Rarely Need&#160;waitFor Before covering individual actions, it is worth understanding what happens under the hood </p>
<p>The post <a href="https://swacblooms.com/part-3-actions-and-auto-waiting-putting-locators-to-work/">Part 3 — Actions and Auto-Waiting: Putting Locators to Work</a> appeared first on <a href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading" id="introduction">Introduction</h2>



<p class="wp-block-paragraph">In Part 2 you learned how to find elements on the page. In this article, you will put those locators to work by performing actions on them — clicking, typing, hovering, selecting, uploading files, and more.</p>



<p class="wp-block-paragraph">You will also learn one of Playwright&#8217;s most important features:&nbsp;<strong>auto-waiting</strong>. Playwright does not blindly fire an action the moment you call it. It waits until the element is ready. This is why Playwright tests are far less flaky than tests written with older tools.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="auto-waiting--why-you-rarely-need-waitfor">Auto-Waiting — Why You Rarely Need&nbsp;<code>waitFor</code></h2>



<p class="wp-block-paragraph">Before covering individual actions, it is worth understanding what happens under the hood every time you call one.</p>



<p class="wp-block-paragraph">When you write:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
await page.getByRole(&#039;button&#039;, { name: &#039;Submit&#039; }).click();

</pre></div>


<p class="wp-block-paragraph">Playwright does not just fire a click immediately. It first checks that the element:</p>



<ul class="wp-block-list">
<li><strong>Exists</strong> in the DOM</li>



<li><strong>Is visible</strong> (not hidden with <code>display: none</code> or <code>visibility: hidden</code>)</li>



<li><strong>Is stable</strong> (not animating or moving)</li>



<li><strong>Is enabled</strong> (not disabled)</li>



<li><strong>Is not obscured</strong> by another element</li>
</ul>



<p class="wp-block-paragraph">Only when all of those conditions are met does Playwright act. If the element is not ready yet, Playwright keeps retrying until the configured timeout is reached before failing.</p>



<p class="wp-block-paragraph">This means you rarely need to write:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
await page.waitForSelector(&#039;.submit-button&#039;); // rarely needed
await page.waitForTimeout(2000);              // almost never needed

</pre></div>


<p class="wp-block-paragraph">Just write the action. Playwright handles the waiting.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="clicking">Clicking</h2>



<p class="wp-block-paragraph"><code>click()</code>&nbsp;is the most common action. It simulates a real mouse click — move, press, release.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// single click
await page.getByRole(&#039;button&#039;, { name: &#039;Submit&#039; }).click();

// double click
await page.getByRole(&#039;button&#039;, { name: &#039;Edit&#039; }).dblclick();

// right click (context menu)
await page.getByRole(&#039;listitem&#039;, { name: &#039;File&#039; }).click({ button: &#039;right&#039; });

// click a specific position within an element
await page.getByRole(&#039;canvas&#039;).click({ position: { x: 100, y: 200 } });

</pre></div>


<p class="wp-block-paragraph">If the element is covered by an overlay like a cookie banner, Playwright will throw rather than silently clicking the wrong thing. Fix the root cause — dismiss the overlay first.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="typing-and-filling-inputs">Typing and Filling Inputs</h2>



<p class="wp-block-paragraph">There are two ways to put text into an input:&nbsp;<code>fill()</code>&nbsp;and&nbsp;<code>pressSequentially()</code>.</p>



<h3 class="wp-block-heading" id="fill--use-this-first"><code>fill()</code>&nbsp;— Use This First</h3>



<p class="wp-block-paragraph"><code>fill()</code>&nbsp;clears the current value and sets the new one in a single operation. It is fast and reliable.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await page.getByLabel(&#039;Email address&#039;).fill(&#039;user@example.com&#039;);
await page.getByLabel(&#039;Password&#039;).fill(&#039;secret123&#039;);

</pre></div>


<h3 class="wp-block-heading" id="presssequentially--when-you-need-key-by-key-input"><code>pressSequentially()</code>&nbsp;— When You Need Key-by-Key Input</h3>



<p class="wp-block-paragraph">Some inputs respond to individual keystrokes — autocomplete dropdowns, for example.&nbsp;<code>pressSequentially()</code>&nbsp;types character by character, triggering keyboard events as it goes.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await page.getByLabel(&#039;Search&#039;).pressSequentially(&#039;play&#039;, { delay: 50 });

</pre></div>


<p class="wp-block-paragraph">The&nbsp;<code>delay</code>&nbsp;option adds a pause between keystrokes in milliseconds. Without it the typing is instantaneous, which some inputs do not handle correctly.</p>



<h3 class="wp-block-heading" id="clear--emptying-a-field"><code>clear()</code>&nbsp;— Emptying a Field</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await page.getByLabel(&#039;Username&#039;).clear();

</pre></div>


<h3 class="wp-block-heading" id="press--single-key-presses"><code>press()</code>&nbsp;— Single Key Presses</h3>



<p class="wp-block-paragraph">Use&nbsp;<code>press()</code>&nbsp;when you need to hit a specific key — Enter, Tab, Escape, or an arrow key.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await page.getByLabel(&#039;Search&#039;).press(&#039;Enter&#039;);
await page.getByRole(&#039;textbox&#039;).press(&#039;Tab&#039;);
await page.getByRole(&#039;dialog&#039;).press(&#039;Escape&#039;);

</pre></div>


<p class="wp-block-paragraph">Key names follow the&nbsp;<a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values">KeyboardEvent.key</a>&nbsp;standard. Common ones:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Key</th><th>String to use</th></tr></thead><tbody><tr><td>Enter</td><td><code>'Enter'</code></td></tr><tr><td>Tab</td><td><code>'Tab'</code></td></tr><tr><td>Escape</td><td><code>'Escape'</code></td></tr><tr><td>Arrow keys</td><td><code>'ArrowUp'</code>&nbsp;/&nbsp;<code>'ArrowDown'</code>&nbsp;/&nbsp;<code>'ArrowLeft'</code>&nbsp;/&nbsp;<code>'ArrowRight'</code></td></tr><tr><td>Backspace</td><td><code>'Backspace'</code></td></tr><tr><td>Modifier + key</td><td><code>'Control+a'</code>,&nbsp;<code>'Meta+c'</code></td></tr></tbody></table></figure>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="checkboxes-and-radio-buttons">Checkboxes and Radio Buttons</h2>



<p class="wp-block-paragraph">Use&nbsp;<code>check()</code>&nbsp;and&nbsp;<code>uncheck()</code>&nbsp;rather than&nbsp;<code>click()</code>&nbsp;for checkboxes and radio buttons. They are more explicit and Playwright verifies the resulting state.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// check a checkbox
await page.getByLabel(&#039;Remember me&#039;).check();

// uncheck it
await page.getByLabel(&#039;Remember me&#039;).uncheck();

// select a radio button
await page.getByLabel(&#039;Credit card&#039;).check();

</pre></div>


<p class="wp-block-paragraph">To verify the current state:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await expect(page.getByLabel(&#039;Remember me&#039;)).toBeChecked();
await expect(page.getByLabel(&#039;Remember me&#039;)).not.toBeChecked();

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="dropdowns--selectoption">Dropdowns —&nbsp;<code>selectOption()</code></h2>



<p class="wp-block-paragraph">For a native&nbsp;<code>&lt;select&gt;</code>&nbsp;element, use&nbsp;<code>selectOption()</code>.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// select by visible text
await page.getByLabel(&#039;Country&#039;).selectOption(&#039;United Kingdom&#039;);

// select by value attribute
await page.getByLabel(&#039;Country&#039;).selectOption({ value: &#039;uk&#039; });

// select by index (zero-based)
await page.getByLabel(&#039;Country&#039;).selectOption({ index: 2 });

// select multiple options (if the select allows it)
await page.getByLabel(&#039;Interests&#039;).selectOption(&#x5B;&#039;Music&#039;, &#039;Sport&#039;]);

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="hovering">Hovering</h2>



<p class="wp-block-paragraph"><code>hover()</code>&nbsp;moves the mouse over an element without clicking. Use it to trigger tooltips, reveal dropdown menus, or test hover states.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await page.getByRole(&#039;button&#039;, { name: &#039;More options&#039; }).hover();

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="focus-and-blur">Focus and Blur</h2>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// move keyboard focus to an element
await page.getByLabel(&#039;Email&#039;).focus();

// remove focus
await page.getByLabel(&#039;Email&#039;).blur();

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="file-uploads">File Uploads</h2>



<p class="wp-block-paragraph"><code>setInputFiles()</code>&nbsp;sets files on an&nbsp;<code>&lt;input type="file"&gt;</code>&nbsp;element.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// upload a single file
await page.getByLabel(&#039;Upload document&#039;).setInputFiles(&#039;path/to/file.pdf&#039;);

// upload multiple files
await page.getByLabel(&#039;Upload photos&#039;).setInputFiles(&#x5B;
  &#039;photo1.jpg&#039;,
  &#039;photo2.jpg&#039;,
]);

// clear a previously selected file
await page.getByLabel(&#039;Upload document&#039;).setInputFiles(&#x5B;]);

</pre></div>


<p class="wp-block-paragraph">For upload dialogs triggered by a button rather than a direct file input:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
const &#x5B;fileChooser] = await Promise.all(&#x5B;
  page.waitForEvent(&#039;filechooser&#039;),
  page.getByRole(&#039;button&#039;, { name: &#039;Upload&#039; }).click(),
]);
await fileChooser.setFiles(&#039;path/to/file.pdf&#039;);
</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="drag-and-drop">Drag and Drop</h2>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await page.getByRole(&#039;listitem&#039;, { name: &#039;Task A&#039; }).dragTo(
  page.getByRole(&#039;region&#039;, { name: &#039;Done&#039; })
);

</pre></div>


<p class="wp-block-paragraph">For fine-grained control:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await page.getByRole(&#039;slider&#039;).dragTo(
  page.getByRole(&#039;slider&#039;),
  { targetPosition: { x: 200, y: 0 } }
);

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="scrolling">Scrolling</h2>



<p class="wp-block-paragraph">Playwright scrolls automatically when it needs to bring an element into view before acting. For explicit scrolling:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// scroll an element into view
await page.getByRole(&#039;heading&#039;, { name: &#039;Pricing&#039; }).scrollIntoViewIfNeeded();

// scroll the page by a pixel amount
await page.mouse.wheel(0, 500);

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="summary">Summary</h2>



<p class="wp-block-paragraph">In Part 3, you learned how to interact with the page in a reliable way.</p>



<ul class="wp-block-list">
<li>Playwright waits for elements to be ready before it performs an action.</li>



<li><code>fill()</code> is the default choice for text inputs, while <code>pressSequentially()</code> is useful when key-by-key typing is required.</li>



<li><code>check()</code> and <code>uncheck()</code> are the right tools for checkboxes and radio buttons.</li>



<li><code>selectOption()</code> is used for native <code>&lt;select></code> dropdowns.</li>



<li><code>press()</code> lets you send single keys and key combinations such as <code>Control+a</code>.</li>



<li><code>setInputFiles()</code> works for file inputs, and the <code>filechooser</code> event pattern works for upload buttons.</li>



<li>In most tests, you can skip manual waits like <code>waitForTimeout</code>.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="whats-next">What&#8217;s Next</h2>



<p class="wp-block-paragraph">In&nbsp;<strong>Part 4</strong>&nbsp;you will learn assertions — how to verify that the right things appear on the page after your actions. Playwright&#8217;s web-first assertions retry automatically, and you will learn why that matters and how to use them correctly.</p>
<p>The post <a href="https://swacblooms.com/part-3-actions-and-auto-waiting-putting-locators-to-work/">Part 3 — Actions and Auto-Waiting: Putting Locators to Work</a> appeared first on <a href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Part 2 — Understanding Locators: Finding Things on the Page</title>
		<link>https://swacblooms.com/part-2-understanding-locators-finding-things-on-the-page/?utm_source=rss&amp;utm_medium=rss&amp;utm_campaign=part-2-understanding-locators-finding-things-on-the-page</link>
		
		<dc:creator><![CDATA[Samson Amaugo]]></dc:creator>
		<pubDate>Mon, 30 Mar 2026 06:21:56 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://swacblooms.com/?p=1849</guid>

					<description><![CDATA[<p>Introduction Before you can click a button, fill a form, or assert that something is visible, you need to tell Playwright&#160;which&#160;element you are talking about. That is what locators are for. A locator is not just a selector — it is a smart reference to an element that re-queries the DOM every time you use it. This means if the page re-renders between steps, Playwright finds the fresh element automatically rather than holding a stale reference that no longer exists. In this article, you will learn some of the most common locator methods Playwright provides, when to use each one, </p>
<p>The post <a href="https://swacblooms.com/part-2-understanding-locators-finding-things-on-the-page/">Part 2 — Understanding Locators: Finding Things on the Page</a> appeared first on <a href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading" id="introduction">Introduction</h2>



<p class="wp-block-paragraph">Before you can click a button, fill a form, or assert that something is visible, you need to tell Playwright&nbsp;<em>which</em>&nbsp;element you are talking about. That is what locators are for.</p>



<p class="wp-block-paragraph">A locator is not just a selector — it is a smart reference to an element that re-queries the DOM every time you use it. This means if the page re-renders between steps, Playwright finds the fresh element automatically rather than holding a stale reference that no longer exists.</p>



<p class="wp-block-paragraph">In this article, you will learn some of the most common locator methods Playwright provides, when to use each one, and the priority order the Playwright team recommends.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="recommended-built-in-locators">Recommended Built-in Locators</h2>



<p class="wp-block-paragraph">Playwright&#8217;s docs recommend these built-in locators over CSS and XPath because they are tied to what users actually see and interact with:</p>



<ol class="wp-block-list">
<li><code>getByRole</code></li>



<li><code>getByText</code></li>



<li><code>getByLabel</code></li>



<li><code>getByPlaceholder</code></li>



<li><code>getByAltText</code></li>



<li><code>getByTitle</code></li>



<li><code>getByTestId</code></li>
</ol>



<p class="wp-block-paragraph">The guiding principle:&nbsp;<strong>the closer a locator is to how a real user perceives the page, the better.</strong>&nbsp;Users read text and interact with buttons and links — they do not think in CSS class names.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="getbyrole--start-here"><code>getByRole</code>&nbsp;— Start Here</h2>



<p class="wp-block-paragraph"><code>getByRole</code>&nbsp;finds elements by their ARIA role and accessible name. This is the recommended first choice for almost every interactive element on the page.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
// finds &amp;lt;a href=&quot;/docs/intro&quot;&gt;Get started&amp;lt;/a&gt;
page.getByRole(&#039;link&#039;, { name: &#039;Get started&#039; })

// finds &amp;lt;button&gt;Submit&amp;lt;/button&gt;
page.getByRole(&#039;button&#039;, { name: &#039;Submit&#039; })

// finds &amp;lt;h1&gt;Installation&amp;lt;/h1&gt;
page.getByRole(&#039;heading&#039;, { name: &#039;Installation&#039; })

// finds &amp;lt;input type=&quot;checkbox&quot;&gt; with label &quot;Remember me&quot;
page.getByRole(&#039;checkbox&#039;, { name: &#039;Remember me&#039; })

</pre></div>


<p class="wp-block-paragraph">The role comes from the HTML element itself — no&nbsp;<code>aria-*</code>&nbsp;attributes needed. The browser assigns roles automatically:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>HTML element</th><th>Implicit ARIA role</th></tr></thead><tbody><tr><td><code>&lt;a href="..."&gt;</code></td><td><code>link</code></td></tr><tr><td><code>&lt;button&gt;</code></td><td><code>button</code></td></tr><tr><td><code>&lt;h1&gt;</code>&nbsp;–&nbsp;<code>&lt;h6&gt;</code></td><td><code>heading</code></td></tr><tr><td><code>&lt;input type="checkbox"&gt;</code></td><td><code>checkbox</code></td></tr><tr><td><code>&lt;input type="text"&gt;</code></td><td><code>textbox</code></td></tr><tr><td><code>&lt;img&gt;</code></td><td><code>img</code></td></tr><tr><td><code>&lt;ul&gt;</code>&nbsp;/&nbsp;<code>&lt;ol&gt;</code></td><td><code>list</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">The accessible name comes from the element&#8217;s visible text content (as you saw in Part 1 with the&nbsp;<code>Get started</code>&nbsp;link). If the element has an&nbsp;<code>aria-label</code>&nbsp;or&nbsp;<code>aria-labelledby</code>, that takes priority over text content.</p>



<p class="wp-block-paragraph">The&nbsp;<code>name</code>&nbsp;option accepts a string or a regular expression:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
// exact string match
page.getByRole(&#039;button&#039;, { name: &#039;Sign in&#039; })

// regex — useful when the text is dynamic
page.getByRole(&#039;button&#039;, { name: /sign in/i })

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="getbytext--for-non-interactive-elements"><code>getByText</code>&nbsp;— For Non-Interactive Elements</h2>



<p class="wp-block-paragraph">Use&nbsp;<code>getByText</code>&nbsp;when you want to find an element by its visible text content and there is no meaningful role to target — typically a&nbsp;<code>&lt;div&gt;</code>,&nbsp;<code>&lt;span&gt;</code>, or&nbsp;<code>&lt;p&gt;</code>.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
// finds any element containing the text &quot;Welcome back&quot;
page.getByText(&#039;Welcome back&#039;)

// exact match only
page.getByText(&#039;Welcome back&#039;, { exact: true })

// regex
page.getByText(/welcome back/i)

</pre></div>


<p class="wp-block-paragraph"><strong>Important:</strong>&nbsp;Playwright normalises whitespace automatically, even with&nbsp;<code>{ exact: true }</code>. Multiple spaces become one, line breaks become spaces, and leading/trailing whitespace is ignored.</p>



<p class="wp-block-paragraph">Avoid using&nbsp;<code>getByText</code>&nbsp;on buttons and links — use&nbsp;<code>getByRole</code>&nbsp;for those.&nbsp;<code>getByText</code>&nbsp;is for reading content, not for triggering actions.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="getbylabel--for-form-fields"><code>getByLabel</code>&nbsp;— For Form Fields</h2>



<p class="wp-block-paragraph"><code>getByLabel</code>&nbsp;finds an input by the text of its associated&nbsp;<code>&lt;label&gt;</code>. This is the right tool any time you are filling in a form.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
// finds the input associated with &amp;lt;label&gt;Email address&amp;lt;/label&gt;
page.getByLabel(&#039;Email address&#039;)

// usage in a test
await page.getByLabel(&#039;Email address&#039;).fill(&#039;user@example.com&#039;);
await page.getByLabel(&#039;Password&#039;).fill(&#039;secret&#039;);

</pre></div>


<p class="wp-block-paragraph">The label association can be done two ways in HTML, and&nbsp;<code>getByLabel</code>&nbsp;handles both:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
&amp;lt;!-- wrapping label --&gt;
&amp;lt;label&gt;
  Email address
  &amp;lt;input type=&quot;email&quot; /&gt;
&amp;lt;/label&gt;

&amp;lt;!-- linked by for/id --&gt;
&amp;lt;label for=&quot;email&quot;&gt;Email address&amp;lt;/label&gt;
&amp;lt;input id=&quot;email&quot; type=&quot;email&quot; /&gt;

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="getbyplaceholder--when-there-is-no-label"><code>getByPlaceholder</code>&nbsp;— When There Is No Label</h2>



<p class="wp-block-paragraph">Some inputs skip a visible label and rely on placeholder text instead. Use&nbsp;<code>getByPlaceholder</code>&nbsp;for those.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
&amp;lt;input type=&quot;email&quot; placeholder=&quot;Enter your email&quot; /&gt;

</pre></div>

<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
await page.getByPlaceholder(&#039;Enter your email&#039;).fill(&#039;user@example.com&#039;);

</pre></div>


<p class="wp-block-paragraph">Prefer&nbsp;<code>getByLabel</code>&nbsp;over&nbsp;<code>getByPlaceholder</code>&nbsp;when both are available — a visible label is better for accessibility and more stable as a test target.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="getbyalttext--for-images"><code>getByAltText</code>&nbsp;— For Images</h2>



<p class="wp-block-paragraph"><code>getByAltText</code>&nbsp;finds elements by their&nbsp;<code>alt</code>&nbsp;attribute. In practice this means images.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
&amp;lt;img src=&quot;logo.png&quot; alt=&quot;Company logo&quot; /&gt;

</pre></div>

<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
await expect(page.getByAltText(&#039;Company logo&#039;)).toBeVisible();

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="getbytitle--for-title-attributes"><code>getByTitle</code>&nbsp;— For Title Attributes</h2>



<p class="wp-block-paragraph"><code>getByTitle</code>&nbsp;matches elements with a&nbsp;<code>title</code>&nbsp;attribute. Less common, but useful for icon buttons and tooltips that carry no visible text.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
&amp;lt;button title=&quot;Close dialog&quot;&gt;✕&amp;lt;/button&gt;

</pre></div>

<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
await page.getByTitle(&#039;Close dialog&#039;).click();

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="getbytestid--the-explicit-contract"><code>getByTestId</code>&nbsp;— The Explicit Contract</h2>



<p class="wp-block-paragraph"><code>getByTestId</code>&nbsp;finds elements by a&nbsp;<code>data-testid</code>&nbsp;attribute (or a custom attribute you configure).</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
&amp;lt;button data-testid=&quot;submit-order&quot;&gt;Place order&amp;lt;/button&gt;

</pre></div>

<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
await page.getByTestId(&#039;submit-order&#039;).click();

</pre></div>


<p class="wp-block-paragraph">This locator is the most resilient to UI changes — the visible text and styling can change entirely without breaking your test. The trade-off: it requires you to add&nbsp;<code>data-testid</code>&nbsp;attributes to your HTML, and it is not based on anything a real user perceives.</p>



<p class="wp-block-paragraph">Use it when:</p>



<ul class="wp-block-list">
<li>A role or text locator is not specific enough</li>



<li>You want an explicit, stable contract between your tests and a specific element</li>



<li>The element has no accessible role or visible text</li>
</ul>



<p class="wp-block-paragraph">You can configure a custom attribute name in&nbsp;<code>playwright.config.js</code>:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
use: {
  testIdAttribute: &#039;data-cy&#039;, // use data-cy instead of data-testid
}

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="css-and-xpath--last-resort">CSS and XPath — Last Resort</h2>



<p class="wp-block-paragraph">When none of the above fit, you can fall back to CSS selectors or XPath via&nbsp;<code>locator()</code>.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
// ID selector
page.locator(&#039;#submit-button&#039;)

// CSS selector
page.locator(&#039;.nav-menu &gt; li:first-child&#039;)

// XPath
page.locator(&#039;//button&#x5B;@type=&quot;submit&quot;]&#039;)

</pre></div>


<p class="wp-block-paragraph">Playwright auto-detects whether you passed CSS or XPath — no prefix needed.</p>



<p class="wp-block-paragraph"><strong>Why avoid these?</strong>&nbsp;The DOM changes. Class names get renamed, elements get restructured, and your tests break for reasons unrelated to the actual behaviour of the app. The role and text-based locators are far more stable because they are tied to what users see, not to implementation details.</p>



<p class="wp-block-paragraph">One additional limitation:&nbsp;<strong>XPath does not pierce Shadow DOM.</strong>&nbsp;All other Playwright locators work inside shadow roots by default.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="filtering-locators">Filtering Locators</h2>



<p class="wp-block-paragraph">When a locator matches more than one element, use&nbsp;<code>filter()</code>&nbsp;to narrow it down.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
// all list items that contain the text &quot;In stock&quot;
page.getByRole(&#039;listitem&#039;).filter({ hasText: &#039;In stock&#039; })

// list items that do NOT contain &quot;Sold out&quot;
page.getByRole(&#039;listitem&#039;).filter({ hasNotText: &#039;Sold out&#039; })

// list items that contain a child button labelled &quot;Add to cart&quot;
page.getByRole(&#039;listitem&#039;).filter({
  has: page.getByRole(&#039;button&#039;, { name: &#039;Add to cart&#039; })
})

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="chaining-locators">Chaining Locators</h2>



<p class="wp-block-paragraph">All locator methods are also available on a locator itself, so you can scope a search inside a specific part of the page.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
// find the navigation section first, then look for a link inside it
page.getByRole(&#039;navigation&#039;).getByRole(&#039;link&#039;, { name: &#039;Pricing&#039; })

// find a form, then find the submit button inside it
page.getByRole(&#039;form&#039;, { name: &#039;Checkout&#039; }).getByRole(&#039;button&#039;, { name: &#039;Place order&#039; })

</pre></div>


<p class="wp-block-paragraph">This is cleaner and more precise than a deeply nested CSS selector, and it reads like plain English.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="strictness--one-element-at-a-time">Strictness — One Element at a Time</h2>



<p class="wp-block-paragraph">Playwright locators are strict by default. If a locator matches more than one element and you call an action on it (like&nbsp;<code>.click()</code>), Playwright throws an error rather than silently picking the first match.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
Error: locator.click: Error: strict mode violation:
getByRole(&#039;button&#039;, { name: &#039;Submit&#039; }) resolved to 2 elements

</pre></div>


<p class="wp-block-paragraph">This is intentional — it forces you to write specific locators. The fix is to make the locator more precise rather than reaching for&nbsp;<code>.first()</code>&nbsp;or&nbsp;<code>.nth(0)</code>:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
// vague — which Submit button?
page.getByRole(&#039;button&#039;, { name: &#039;Submit&#039; })

// specific — scoped to the checkout form
page.getByRole(&#039;form&#039;, { name: &#039;Checkout&#039; }).getByRole(&#039;button&#039;, { name: &#039;Submit&#039; })

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="summary">Summary</h2>



<p class="wp-block-paragraph">Here is what you learned in Part 2:</p>



<ul class="wp-block-list">
<li>A locator re-queries the DOM every time it is used — no stale element references</li>



<li>Use <code>getByRole</code> first: it targets elements the same way users and screen readers do</li>



<li>Use <code>getByLabel</code> for form inputs, <code>getByText</code> for static content, <code>getByAltText</code> for images</li>



<li>Use <code>getByTestId</code> when you need an explicit, stable testing contract</li>



<li>Avoid CSS and XPath unless nothing else fits — they are tied to implementation details</li>



<li><code>filter()</code> and chaining let you narrow down locators precisely</li>



<li>Locators are strict: if more than one element matches, Playwright errors rather than guessing</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="whats-next">What&#8217;s Next</h2>



<p class="wp-block-paragraph">In <strong>Part 3</strong> we put locators to work. You will learn some of the actions Playwright can perform — clicking, typing, hovering, selecting, uploading files — and how Playwright&#8217;s auto-waiting means you almost never need to add a manual wait.</p>
<p>The post <a href="https://swacblooms.com/part-2-understanding-locators-finding-things-on-the-page/">Part 2 — Understanding Locators: Finding Things on the Page</a> appeared first on <a href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>What Is Playwright and Why Should You Care?</title>
		<link>https://swacblooms.com/what-is-playwright-and-why-should-you-care/?utm_source=rss&amp;utm_medium=rss&amp;utm_campaign=what-is-playwright-and-why-should-you-care</link>
		
		<dc:creator><![CDATA[Samson Amaugo]]></dc:creator>
		<pubDate>Mon, 23 Mar 2026 06:31:03 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://swacblooms.com/?p=1845</guid>

					<description><![CDATA[<p>Introduction If you have ever shipped a feature that looked perfect in your local browser but broke immediately in production, you already understand why automated browser testing exists. Playwright is the answer to the question: &#8220;How do we make that kind of testing fast, reliable, and not painful to write?&#8221; This first article walks you through what Playwright is, why it stands out from its competitors, how to install it, and how to read your very first test. By the end, you will have a working setup and a solid mental model of what each piece does. What Is Playwright? Playwright </p>
<p>The post <a href="https://swacblooms.com/what-is-playwright-and-why-should-you-care/">What Is Playwright and Why Should You Care?</a> appeared first on <a href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading" id="introduction">Introduction</h2>



<p class="wp-block-paragraph">If you have ever shipped a feature that looked perfect in your local browser but broke immediately in production, you already understand why automated browser testing exists. Playwright is the answer to the question: <em>&#8220;How do we make that kind of testing fast, reliable, and not painful to write?&#8221;</em></p>



<p class="wp-block-paragraph">This first article walks you through what Playwright is, why it stands out from its competitors, how to install it, and how to read your very first test. By the end, you will have a working setup and a solid mental model of what each piece does.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="what-is-playwright">What Is Playwright?</h2>



<p class="wp-block-paragraph">Playwright is an open-source end-to-end (E2E) testing framework built by Microsoft. &#8220;End-to-end&#8221; means it controls a real browser — Chrome, Firefox, or Safari — the same way a real user would: clicking links, filling forms, navigating pages, and checking that the right content appears.</p>



<p class="wp-block-paragraph">Unlike unit tests (which test isolated functions) or integration tests (which test modules together), E2E tests validate the entire stack from the UI down to the database and back. They are the closest thing to&nbsp;<em>&#8220;a human actually using your app.&#8221;</em></p>



<p class="wp-block-paragraph">Playwright supports:</p>



<ul class="wp-block-list">
<li>Chromium (Chrome / Edge)</li>



<li>Firefox</li>



<li>WebKit (Safari)</li>



<li>Mobile viewports</li>



<li>Multiple languages: JavaScript, TypeScript, Python, Java, .NET</li>
</ul>



<p class="wp-block-paragraph">This series uses JavaScript/TypeScript.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="why-playwright">Why Playwright?</h2>



<p class="wp-block-paragraph">There are other browser automation tools out there — Selenium and Cypress being the most well-known. This series focuses on Playwright, and here is what makes it worth your time:</p>



<ul class="wp-block-list">
<li>Supports all three major browser engines — Chromium, Firefox, and WebKit — out of the box</li>



<li>Auto-waits for elements to be ready before acting, so no manual sleeps or brittle waits</li>



<li>Runs tests in parallel across browsers simultaneously</li>



<li>First-class TypeScript support with no extra configuration</li>



<li>Built-in network interception, trace recording, and a visual debugger</li>



<li>Actively maintained by Microsoft with frequent releases</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="installing-playwright">Installing Playwright</h2>



<p class="wp-block-paragraph">Playwright requires Node.js 20.x, 22.x, or 24.x. Check your version:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
node --version

</pre></div>


<p class="wp-block-paragraph">To scaffold a new Playwright project, run:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
npm init playwright@latest

</pre></div>


<p class="wp-block-paragraph">The CLI will ask a few questions:</p>



<ul class="wp-block-list">
<li>TypeScript or JavaScript? <em>(choose either — this series uses both)</em></li>



<li>Where to put your tests? <em>(default: <code>tests/</code>)</em></li>



<li>Add a GitHub Actions workflow? <em>(yes — we cover this in Part 11)</em></li>



<li>Install Playwright browsers? <em>(yes)</em></li>
</ul>



<p class="wp-block-paragraph">That last step downloads Chromium, Firefox, and WebKit — about 300 MB total. Playwright ships its own browser binaries, so your system browsers never interfere.</p>



<p class="wp-block-paragraph">After installation, your project looks like this:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
your-project/
├── node_modules/
├── tests/
│   └── example.spec.js       ← your starter test
├── playwright.config.js      ← all configuration lives here
├── package.json
└── package-lock.json

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="your-first-test--line-by-line">Your First Test — Line by Line</h2>



<p class="wp-block-paragraph">Open&nbsp;<code>tests/example.spec.js</code>. Here is what it contains:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// @ts-check
import { test, expect } from &#039;@playwright/test&#039;;

test(&#039;has title&#039;, async ({ page }) =&gt; {
  await page.goto(&#039;https://playwright.dev/&#039;);
  await expect(page).toHaveTitle(/Playwright/);
});

test(&#039;get started link&#039;, async ({ page }) =&gt; {
  await page.goto(&#039;https://playwright.dev/&#039;);
  await page.getByRole(&#039;link&#039;, { name: &#039;Get started&#039; }).click();
  await expect(page.getByRole(&#039;heading&#039;, { name: &#039;Installation&#039; })).toBeVisible();
});

</pre></div>


<p class="wp-block-paragraph">Let&#8217;s break each part down.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading" id="-ts-check"><code>// @ts-check</code></h3>



<p class="wp-block-paragraph">A JSDoc comment that tells VS Code to type-check this plain JavaScript file using TypeScript&#8217;s engine. You get autocomplete and inline errors without needing a&nbsp;<code>tsconfig</code>. Optional, but useful.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading" id="import--test-expect--from-playwrighttest"><code>import { test, expect } from '@playwright/test'</code></h3>



<p class="wp-block-paragraph">Two things are imported:</p>



<ul class="wp-block-list">
<li><code>test</code> — the function you use to define a test case (like <code>it()</code> in Jest)</li>



<li><code>expect</code> — the assertion library (like Jest&#8217;s <code>expect</code>, extended with browser-aware matchers)</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading" id="testhas-title-async--page-----"><code>test('has title', async ({ page }) =&gt; { ... })</code></h3>



<ul class="wp-block-list">
<li>The first argument is the <strong>test name</strong> — what shows up in the report.</li>



<li>The second argument is an async function that receives <strong>fixtures</strong>. The <code>page</code> fixture is Playwright&#8217;s central object: it represents a single browser tab.</li>



<li>Every browser interaction is async, so you use <code>await</code> throughout.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading" id="await-pagegotohttpsplaywrightdev"><code>await page.goto('https://playwright.dev/')</code></h3>



<p class="wp-block-paragraph">Navigates the browser to a URL. Playwright waits for the page to reach a &#8220;load&#8221; state before moving on.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading" id="await-expectpagetohavetitleplaywright"><code>await expect(page).toHaveTitle(/Playwright/)</code></h3>



<p class="wp-block-paragraph">Asserts that the page&#8217;s&nbsp;<code>&lt;title&gt;</code>&nbsp;tag matches the regular expression&nbsp;<code>/Playwright/</code>. This is a&nbsp;<strong>web-first assertion</strong>&nbsp;— if the title is not there yet, Playwright retries the check for up to 5 seconds before failing. No manual waits needed.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading" id="await-pagegetbyrolelink--name-get-started-click"><code>await page.getByRole('link', { name: 'Get started' }).click()</code></h3>



<p class="wp-block-paragraph">Finds an&nbsp;<code>&lt;a&gt;</code>&nbsp;element whose accessible name is &#8220;Get started&#8221; and clicks it.&nbsp;<code>getByRole</code>&nbsp;is the preferred way to select elements — it mirrors how assistive technologies see your page and is far more resilient than CSS selectors.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading" id="await-expectpagegetbyroleheading--name-installation-tobevisible"><code>await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible()</code></h3>



<p class="wp-block-paragraph">After clicking, the test navigates to the installation page. This assertion checks that a heading with the text &#8220;Installation&#8221; becomes visible. Again, Playwright retries this automatically.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="understanding-the-playwrightconfigjs-file">Understanding the&nbsp;<code>playwright.config.js</code>&nbsp;File</h2>



<p class="wp-block-paragraph">The&nbsp;<code>playwright.config.js</code>&nbsp;file controls how your entire test suite runs. The default configuration sets up three test projects — one for each browser engine.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
export default defineConfig({
  testDir: &#039;./tests&#039;,
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: &#039;html&#039;,
  use: {
    trace: &#039;on-first-retry&#039;,
  },
  projects: &#x5B;
    { name: &#039;chromium&#039;, use: { ...devices&#x5B;&#039;Desktop Chrome&#039;] } },
    { name: &#039;firefox&#039;,  use: { ...devices&#x5B;&#039;Desktop Firefox&#039;] } },
    { name: &#039;webkit&#039;,   use: { ...devices&#x5B;&#039;Desktop Safari&#039;] } },
  ],
});

</pre></div>


<p class="wp-block-paragraph">Key settings to know right now:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Option</th><th>What it does</th></tr></thead><tbody><tr><td><code>testDir</code></td><td>Where Playwright looks for test files (<code>*.spec.js</code>&nbsp;/&nbsp;<code>*.test.js</code>)</td></tr><tr><td><code>fullyParallel</code></td><td>Run all tests in all files at the same time — faster, but tests must not share state</td></tr><tr><td><code>retries</code></td><td>How many times to retry a failing test. Set to&nbsp;<code>2</code>&nbsp;on CI to reduce flakiness noise</td></tr><tr><td><code>reporter</code></td><td><code>"html"</code>&nbsp;generates a rich clickable report in&nbsp;<code>playwright-report/</code></td></tr><tr><td><code>trace</code></td><td>Records screenshots, network, and console logs on the first retry of a failure</td></tr><tr><td><code>projects</code></td><td>Each project runs the full suite in one browser — your tests automatically run 3×</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">We will explore every option in detail in Part 5.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="running-your-tests">Running Your Tests</h2>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
# Run the full suite across all browsers
npx playwright test

# Run only in one browser
npx playwright test --project=chromium

# Run a specific file
npx playwright test tests/example.spec.js

# Watch the browser open (headed mode)
npx playwright test --headed

# Open the interactive HTML report after a run
npx playwright show-report

# Open UI Mode — live debugging with a built-in browser and test explorer
npx playwright test --ui

</pre></div>


<p class="wp-block-paragraph">When tests pass, you will see:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
Running 6 tests using 3 workers
  6 passed (8.3s)

</pre></div>


<p class="wp-block-paragraph">That &#8220;6 tests&#8221; comes from&nbsp;<strong>2 test cases × 3 browser projects</strong>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="reading-a-failure">Reading a Failure</h2>



<p class="wp-block-paragraph">Break one of the assertions intentionally:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
await expect(page).toHaveTitle(/NotARealTitle/);

</pre></div>


<p class="wp-block-paragraph">Run again. Playwright will print:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
Error: expect(page).toHaveTitle(expected)

Expected pattern: /NotARealTitle/
Received string:  &quot;Fast and reliable end-to-end testing for modern web apps | Playwright&quot;

</pre></div>


<p class="wp-block-paragraph">The diff is clear and actionable. The HTML report adds a screenshot of the page at the moment of failure, and if tracing is enabled, a full step-by-step replay.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="summary">Summary</h2>



<p class="wp-block-paragraph">Here is what you learned in Part 1:</p>



<ul class="wp-block-list">
<li>Playwright is an E2E testing framework that drives real browsers</li>



<li>It supports Chromium, Firefox, and WebKit with auto-waiting built in</li>



<li><code>npm init playwright@latest</code> scaffolds a working project in seconds</li>



<li>A test is an async function that receives a <code>page</code> fixture</li>



<li><code>expect()</code> assertions are web-first: they retry until they pass or time out</li>



<li><code>playwright.config.js</code> controls browsers, parallelism, retries, and more</li>



<li><code>npx playwright test</code> runs everything; <code>--project</code> and <code>--headed</code> are useful flags</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="whats-next">What&#8217;s Next</h2>



<p class="wp-block-paragraph">In <strong>Part 2,</strong> we dive deep into locators — the API you will use in every single test to find elements on the page. You will learn why <code>getByRole</code> is almost always the right choice, and when to reach for other strategies.</p>
<p>The post <a href="https://swacblooms.com/what-is-playwright-and-why-should-you-care/">What Is Playwright and Why Should You Care?</a> appeared first on <a href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>WebRTC Renegotiation: When You Need It and When You Don’t</title>
		<link>https://swacblooms.com/webrtc-renegotiation-when-you-need-it-and-when-you-dont/?utm_source=rss&amp;utm_medium=rss&amp;utm_campaign=webrtc-renegotiation-when-you-need-it-and-when-you-dont</link>
		
		<dc:creator><![CDATA[Samson Amaugo]]></dc:creator>
		<pubDate>Mon, 26 Jan 2026 08:42:50 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<guid isPermaLink="false">https://swacblooms.com/?p=1831</guid>

					<description><![CDATA[<p>The Question After implementing track toggling with&#160;replaceTrack(), a crucial question emerges:&#160;When do I need to renegotiate (create a new offer/answer) vs when can I just swap tracks? Understanding this is critical for building efficient WebRTC applications. Unnecessary renegotiation adds latency and complexity, while missing required renegotiation breaks your connection. Quick Answer Use&#160;replaceTrack()&#160;(no renegotiation)&#160;when: Renegotiate (new offer/answer)&#160;when: What is a Transceiver? A transceiver is a&#160;bidirectional media slot&#160;in your peer connection: Key insight: Once a transceiver exists, you can swap tracks in/out without renegotiation. The slot is reserved for the connection lifetime. The SDP Connection Renegotiation is required when the&#160;SDP (Session Description </p>
<p>The post <a href="https://swacblooms.com/webrtc-renegotiation-when-you-need-it-and-when-you-dont/">WebRTC Renegotiation: When You Need It and When You Don&#8217;t</a> appeared first on <a href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><strong>The Question</strong></p>



<p class="wp-block-paragraph">After implementing track toggling with&nbsp;<code>replaceTrack()</code>, a crucial question emerges:&nbsp;<strong>When do I need to renegotiate (create a new offer/answer) vs when can I just swap tracks?</strong></p>



<p class="wp-block-paragraph">Understanding this is critical for building efficient WebRTC applications. Unnecessary renegotiation adds latency and complexity, while missing required renegotiation breaks your connection.</p>



<p class="wp-block-paragraph"><strong>Quick Answer</strong></p>



<p class="wp-block-paragraph"><strong>Use&nbsp;<code>replaceTrack()</code>&nbsp;(no renegotiation)</strong>&nbsp;when:</p>



<ul class="wp-block-list">
<li>Swapping tracks of the same type (camera 1 → camera 2)</li>



<li>Toggling tracks on/off (video → null → video)</li>



<li>The transceiver slot already exists</li>
</ul>



<p class="wp-block-paragraph"><strong>Renegotiate (new offer/answer)</strong>&nbsp;when:</p>



<ul class="wp-block-list">
<li>Adding new transceivers (new media types)</li>



<li>Changing transceiver direction</li>



<li>Stopping transceivers permanently</li>



<li>Any structural SDP changes</li>
</ul>



<h3 class="wp-block-heading" id="what-is-a-transceiver">What is a Transceiver?</h3>



<p class="wp-block-paragraph">A transceiver is a&nbsp;<strong>bidirectional media slot</strong>&nbsp;in your peer connection:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// Creates a slot for video communication
const transceiver = peerConnection.addTransceiver(&#039;video&#039;, {
  direction: &#039;sendrecv&#039;  // Can send AND receive
})

// The transceiver has:
// - sender: sends your video to the peer
// - receiver: receives peer&#039;s video
// - direction: sendrecv, sendonly, recvonly, inactive
</pre></div>


<p class="wp-block-paragraph"><strong>Key insight</strong>: Once a transceiver exists, you can swap tracks in/out without renegotiation. The slot is reserved for the connection lifetime.</p>



<h3 class="wp-block-heading" id="the-sdp-connection">The SDP Connection</h3>



<p class="wp-block-paragraph">Renegotiation is required when the&nbsp;<strong>SDP (Session Description Protocol) structure changes</strong>:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
v=0
o=- 123456 2 IN IP4 127.0.0.1
s=-
t=0 0
m=video 9 UDP/TLS/RTP/SAVPF 96 97    ← Video m-line (transceiver slot)
m=audio 9 UDP/TLS/RTP/SAVPF 111      ← Audio m-line (transceiver slot)
</pre></div>


<ul class="wp-block-list">
<li>Each&nbsp;<code>m=</code>&nbsp;line represents a transceiver</li>



<li>Adding/removing m-lines = renegotiation required</li>



<li>Changing direction in m-line = renegotiation required</li>



<li>Swapping tracks within existing m-line =&nbsp;<strong>NO</strong>&nbsp;renegotiation needed</li>
</ul>



<h2 class="wp-block-heading" id="scenarios-no-renegotiation-needed-">Scenarios: No Renegotiation Needed <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></h2>



<h3 class="wp-block-heading" id="1-camera-toggling-on--off--on">1. Camera Toggling (On → Off → On)</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
let videoSender: RTCRtpSender | null = null

// Initial setup: create transceiver
const videoTransceiver = peerA.addTransceiver(videoTrack, {
  direction: &#039;sendrecv&#039;,
  streams: &#x5B;localStream]
})
videoSender = videoTransceiver.sender

// Later: Turn camera OFF
await videoSender.replaceTrack(null)  //  NO renegotiation

// Later: Turn camera ON
const newStream = await getUserMedia({ video: true })
const newTrack = newStream.getVideoTracks()&#x5B;0]
await videoSender.replaceTrack(newTrack)  //  NO renegotiation
</pre></div>


<p class="wp-block-paragraph"><strong>Why it works</strong>: The video transceiver slot exists. We&#8217;re just changing what goes into it (track → null → track).</p>



<h3 class="wp-block-heading" id="2-switching-cameras">2. Switching Cameras</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
// Front camera → Back camera
const constraints = { video: { facingMode: &#039;environment&#039; } }
const newStream = await getUserMedia(constraints)
const newTrack = newStream.getVideoTracks()&#x5B;0]

await videoSender.replaceTrack(newTrack)  //  NO renegotiation
</pre></div>


<p class="wp-block-paragraph"><strong>Why it works</strong>: Same transceiver, different video source.</p>



<h3 class="wp-block-heading" id="3-switching-microphones">3. Switching Microphones</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// Default mic → Headset mic
const constraints = { audio: { deviceId: headsetMicId } }
const newStream = await getUserMedia(constraints)
const newTrack = newStream.getAudioTracks()&#x5B;0]

await audioSender.replaceTrack(newTrack)  //  NO renegotiation
</pre></div>


<p class="wp-block-paragraph"><strong>Why it works</strong>: Same transceiver, different audio source.</p>



<h3 class="wp-block-heading" id="4-changing-video-resolutionframe-rate">4. Changing Video Resolution/Frame Rate</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// 720p → 1080p
const constraints = {
  video: { width: 1920, height: 1080, frameRate: 30 }
}
const newStream = await getUserMedia(constraints)
const newTrack = newStream.getVideoTracks()&#x5B;0]

await videoSender.replaceTrack(newTrack)  //  NO renegotiation

</pre></div>


<p class="wp-block-paragraph"><strong>Why it works</strong>: Still using the same video transceiver.</p>



<h2 class="wp-block-heading" id="scenarios-renegotiation-required-">Scenarios: Renegotiation Required <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f504.png" alt="🔄" class="wp-smiley" style="height: 1em; max-height: 1em;" /></h2>



<h3 class="wp-block-heading" id="1-adding-screen-sharing">1. Adding Screen Sharing</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// Initial: video + audio
const videoTransceiver = peerA.addTransceiver(videoTrack, {
  direction: &#039;sendrecv&#039;,
  streams: &#x5B;localStream]
})
const audioTransceiver = peerA.addTransceiver(audioTrack, {
  direction: &#039;sendrecv&#039;,
  streams: &#x5B;localStream]
})

// Create offer/answer...

// Later: Add screen sharing (NEW transceiver)
const screenStream = await navigator.mediaDevices.getDisplayMedia({
  video: true
})
const screenTrack = screenStream.getVideoTracks()&#x5B;0]

// Adding a new transceiver
const screenTransceiver = peerA.addTransceiver(screenTrack, {
  direction: &#039;sendrecv&#039;,
  streams: &#x5B;screenStream]
})

//  MUST renegotiate!
const offer = await peerA.createOffer()
await peerA.setLocalDescription(offer)

// Send offer to peer B
// Wait for answer from peer B
const answer = await receiveAnswerFromPeerB()
await peerA.setRemoteDescription(answer)

</pre></div>


<p class="wp-block-paragraph"><strong>Why renegotiation is needed</strong>: We added a&nbsp;<strong>new m-line</strong>&nbsp;to the SDP. Peer B needs to know about this new media stream.</p>



<h3 class="wp-block-heading" id="2-changing-transceiver-direction">2. Changing Transceiver Direction</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// Initial: bidirectional
const transceiver = peerA.addTransceiver(&#039;video&#039;, {
  direction: &#039;sendrecv&#039;  // Can send AND receive
})

// Later: Make it send-only
transceiver.direction = &#039;sendonly&#039;

// MUST renegotiate!
const offer = await peerA.createOffer()
await peerA.setLocalDescription(offer)
// ... complete negotiation

</pre></div>


<p class="wp-block-paragraph"><strong>Why renegotiation is needed</strong>: The SDP m-line direction attribute changed. Peer B needs to update its expectations.</p>



<h3 class="wp-block-heading" id="3-stopping-a-transceiver">3. Stopping a Transceiver</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
const transceiver = peerA.getTransceivers()&#x5B;0]

transceiver.stop()  // Permanently stops the transceiver

//  MUST renegotiate!
const offer = await peerA.createOffer()
await peerA.setLocalDescription(offer)
// ... complete negotiation

</pre></div>


<p class="wp-block-paragraph"><strong>Why renegotiation is needed</strong>: The m-line is marked as inactive in SDP. This is a structural change.</p>



<p class="wp-block-paragraph"><strong>Note</strong>: You rarely need to stop transceivers. Usually&nbsp;<code>replaceTrack(null)</code>&nbsp;is sufficient.</p>



<h3 class="wp-block-heading" id="4-adding-data-channels-after-connection">4. Adding Data Channels After Connection</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// Initial connection with video/audio

// Later: Add data channel
const dataChannel = peerA.createDataChannel(&#039;fileTransfer&#039;)

//  MUST renegotiate!
const offer = await peerA.createOffer()
await peerA.setLocalDescription(offer)
// ... complete negotiation

</pre></div>


<p class="wp-block-paragraph"><strong>Why renegotiation is needed</strong>: Data channels add application m-lines to SDP.</p>



<h2 class="wp-block-heading" id="performance-implications">Performance Implications</h2>



<h3 class="wp-block-heading" id="replacetrack-no-renegotiation"><code>replaceTrack()</code>&nbsp;(No Renegotiation)</h3>



<p class="wp-block-paragraph"><strong>Performance</strong>:</p>



<ul class="wp-block-list">
<li>Near-instant (typically &lt; 50ms)</li>



<li>No network round-trip</li>



<li>No SDP parsing</li>



<li>Seamless for users</li>
</ul>



<p class="wp-block-paragraph"><strong>Use for</strong>:</p>



<ul class="wp-block-list">
<li>Real-time track toggling</li>



<li>Device switching</li>



<li>Quality changes</li>
</ul>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// Fast and seamless
console.time(&#039;replaceTrack&#039;)
await videoSender.replaceTrack(newTrack)
console.timeEnd(&#039;replaceTrack&#039;)  // ~10-30ms

</pre></div>


<h3 class="wp-block-heading" id="renegotiation-offeranswer">Renegotiation (Offer/Answer)</h3>



<p class="wp-block-paragraph"><strong>Performance</strong>:</p>



<ul class="wp-block-list">
<li>Slow (typically 500ms &#8211; 2000ms)</li>



<li>Multiple network round-trip</li>



<li>SDP parsing on both sides</li>



<li>Potential for brief disruption</li>
</ul>



<p class="wp-block-paragraph"><strong>Steps involved</strong>:</p>



<ol class="wp-block-list">
<li>Create offer (100-200ms)</li>



<li>Set local description (50-100ms)</li>



<li>Send offer to peer (network latency)</li>



<li>Peer processes offer (50-100ms)</li>



<li>Peer creates answer (100-200ms)</li>



<li>Peer sets local description (50-100ms)</li>



<li>Answer sent back (network latency)</li>



<li>Set remote description (50-100ms)</li>
</ol>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// Slower due to network round trips
console.time(&#039;renegotiate&#039;)
const offer = await peerA.createOffer()
await peerA.setLocalDescription(offer)
await sendOfferToPeer(offer)
const answer = await waitForAnswer()
await peerA.setRemoteDescription(answer)
console.timeEnd(&#039;renegotiate&#039;)  // ~500-2000ms

</pre></div>


<h2 class="wp-block-heading" id="decision-tree">Decision Tree</h2>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
Do you need to add a NEW type of media?
├─ YES → Renegotiate (add transceiver, then offer/answer)
└─ NO
   │
   Do you need to change transceiver direction?
   ├─ YES → Renegotiate (change direction, then offer/answer)
   └─ NO
      │
      Do you need to permanently stop a transceiver?
      ├─ YES → Renegotiate (stop transceiver, then offer/answer)
      └─ NO
         │
         Just changing/toggling existing track?
         └─ YES → Use replaceTrack() 

</pre></div>


<h2 class="wp-block-heading" id="summary">Summary</h2>



<p class="wp-block-paragraph"><strong>The Golden Rule</strong>: If the transceiver slot exists, use&nbsp;<code>replaceTrack()</code>. If you need a new slot (or to modify slot properties), renegotiate.</p>
<p>The post <a href="https://swacblooms.com/webrtc-renegotiation-when-you-need-it-and-when-you-dont/">WebRTC Renegotiation: When You Need It and When You Don&#8217;t</a> appeared first on <a href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Migrating from Cloudflare R2 to Garage in .NET</title>
		<link>https://swacblooms.com/migrating-from-cloudflare-r2-to-garage-in-net/?utm_source=rss&amp;utm_medium=rss&amp;utm_campaign=migrating-from-cloudflare-r2-to-garage-in-net</link>
		
		<dc:creator><![CDATA[Samson Amaugo]]></dc:creator>
		<pubDate>Sun, 21 Dec 2025 05:16:15 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://swacblooms.com/?p=1820</guid>

					<description><![CDATA[<p>I recently migrated my .NET side project&#8217;s object storage from Cloudflare R2 to Garage (self-hosted S3-compatible storage). The main driver was cost predictability &#8211; I wanted fixed infrastructure costs instead of variable charges. Here&#8217;s how I configured it. Garage Bucket-to-Website Garage doesn&#8217;t currently support anonymous S3 access, so I used the bucket-to-website feature for public file access. This works by: 1. Setting the bucket name to match my public domain (e.g., cdn.myapp.com) 2. Enabling website mode via the Garage web UI 3. Pointing my DNS to Garage&#8217;s web endpoint 4. Uploading files via S3 API 5. Accessing files directly via </p>
<p>The post <a href="https://swacblooms.com/migrating-from-cloudflare-r2-to-garage-in-net/">Migrating from Cloudflare R2 to Garage in .NET</a> appeared first on <a href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">I recently migrated my .NET side project&#8217;s object storage from Cloudflare R2 to Garage (self-hosted S3-compatible storage). The main driver was <strong>cost predictability</strong> &#8211; I wanted fixed infrastructure costs instead of variable charges.</p>



<p class="wp-block-paragraph">Here&#8217;s how I configured it.</p>



<p class="wp-block-paragraph"><strong>Garage Bucket-to-Website</strong></p>



<p class="wp-block-paragraph">Garage <a href="https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/263">doesn&#8217;t currently support anonymous S3 access</a>, so I used the <strong>bucket-to-website</strong> feature for public file access. This works by:</p>



<p class="wp-block-paragraph">1. Setting the bucket name to match my public domain (e.g., <strong>cdn.myapp.com</strong>)</p>



<p class="wp-block-paragraph">2. Enabling website mode via the Garage web UI</p>



<p class="wp-block-paragraph">3. Pointing my DNS to Garage&#8217;s web endpoint</p>



<p class="wp-block-paragraph">4. Uploading files via S3 API</p>



<p class="wp-block-paragraph">5. Accessing files directly via public HTTP URLs on my domain</p>



<p class="wp-block-paragraph">This provides <strong>persistent public access</strong> without pre-signed URLs &#8211; perfect for truly public assets like logos.</p>



<p class="wp-block-paragraph"><strong>Environment Variables</strong></p>



<p class="wp-block-paragraph"><strong>Before (Cloudflare R2):</strong></p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
CLOUDFLARE_R2_ENDPOINT

CLOUDFLARE_R2_ACCESS_KEY_ID

CLOUDFLARE_R2_SECRET_ACCESS_KEY

CLOUDFLARE_R2_BUCKET_NAME
</pre></div>


<p class="wp-block-paragraph"><strong>After (Garage):</strong></p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
GARAGE_S3_ENDPOINT=https://cdn.myapp.com # Public web endpoint (matches bucket name)

GARAGE_S3_API_ENDPOINT=https://s3-api.myapp.com # S3 API endpoint (optional)

GARAGE_S3_ACCESS_KEY_ID=your_key_id

GARAGE_S3_SECRET_ACCESS_KEY=your_secret

GARAGE_S3_BUCKET_NAME=cdn.myapp.com # Must match public domain for website mode
</pre></div>


<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph"><strong>Important:</strong> The bucket name must match your public domain for bucket-to-website to work.</p>



<p class="wp-block-paragraph"><strong>S3 Client Configuration</strong></p>



<p class="wp-block-paragraph">The critical configuration for Garage compatibility:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
var config = new AmazonS3Config

{

   ServiceURL = apiServiceUrl, // Garage S3 API endpoint

   ForcePathStyle = true, // Required for Garage

   AuthenticationRegion = &quot;garage&quot;

};

var s3Client = new AmazonS3Client(accessKeyId, secretAccessKey, config);
</pre></div>


<p class="wp-block-paragraph"><strong>Key points:</strong></p>



<p class="wp-block-paragraph">&#8211; <strong>ForcePathStyle = true</strong> is <strong>required</strong> for Garage</p>



<p class="wp-block-paragraph">&#8211; <strong>AuthenticationRegion = &#8220;garage&#8221;</strong> This is particularly important for non-AWS S3-compatible services like Garage that use custom region naming</p>



<p class="wp-block-paragraph">&#8211; Separate API endpoint for uploads vs. public website endpoint for downloads</p>



<p class="wp-block-paragraph"><strong>Upload Implementation</strong></p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
public async Task&lt;string&gt; UploadImageAsync(Stream fileStream, string fileName)

{

   var uniqueFileName = $&quot;logos/{Guid.NewGuid()}{Path.GetExtension(fileName)}&quot;;

   var putRequest = new PutObjectRequest

   {

      BucketName = _bucketName,

      Key = uniqueFileName,

      InputStream = fileStream,

      ContentType = contentType,

      UseChunkEncoding = false // Important for compatibility

   };

   await _s3Client.PutObjectAsync(putRequest);

   // Return public website URL

   return $&quot;{_publicEndpoint}/{uniqueFileName}&quot;;

}
</pre></div>


<p class="wp-block-paragraph">The returned URL (<strong>https://cdn.myapp.com/logos/abc123.png</strong>) is directly accessible &#8211; no auth needed.</p>



<p class="wp-block-paragraph"><strong>Migrating Existing Assets</strong></p>



<p class="wp-block-paragraph">I used a simple script to transfer existing files from R2 to Garage. The script downloaded files from R2 and re-uploaded them to Garage using the S3 API, preserving the same file paths for zero downtime.</p>



<p class="wp-block-paragraph"><strong>Key Takeaways</strong></p>



<p class="wp-block-paragraph"><strong>Critical configuration:</strong></p>



<p class="wp-block-paragraph">&#8211; <strong>ForcePathStyle = true</strong> and <strong>AuthenticationRegion = &#8220;garage&#8221;</strong> are essential</p>



<p class="wp-block-paragraph">&#8211; <strong>UseChunkEncoding = false</strong> prevents upload issues</p>



<p class="wp-block-paragraph">&#8211; Bucket name <strong>must match</strong> your public domain for website mode</p>



<p class="wp-block-paragraph">&#8211; DNS must point to Garage&#8217;s web endpoint</p>



<p class="wp-block-paragraph"><strong>Why bucket-to-website:</strong></p>



<p class="wp-block-paragraph">&#8211; Garage doesn&#8217;t support anonymous S3 access yet</p>



<p class="wp-block-paragraph">&#8211; Provides persistent public access without pre-signed URLs</p>



<p class="wp-block-paragraph">&#8211; Perfect for truly public assets (logos, images, etc.)</p>



<p class="wp-block-paragraph"><strong>Why Garage:</strong></p>



<p class="wp-block-paragraph">&#8211; <strong>Fixed costs</strong> instead of variable API/bandwidth charges</p>



<p class="wp-block-paragraph">&#8211; <strong>Full control</strong> over storage infrastructure</p>



<p class="wp-block-paragraph">&#8211; <strong>S3 is compatible</strong> with minimal code changes</p>



<p class="wp-block-paragraph">For side projects where cost predictability matters, Garage is excellent. Self-hosting means I know exactly what I&#8217;m paying each month.</p>



<p class="wp-block-paragraph">&#8212;</p>



<p class="wp-block-paragraph"><strong>Resources:</strong></p>



<p class="wp-block-paragraph">&#8211; <a href="https://garagehq.deuxfleurs.fr/">Garage Documentation</a></p>



<p class="wp-block-paragraph">&#8211; <a href="https://www.nuget.org/packages/AWSSDK.S3/">AWSSDK.S3 NuGet Package</a></p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://swacblooms.com/migrating-from-cloudflare-r2-to-garage-in-net/">Migrating from Cloudflare R2 to Garage in .NET</a> appeared first on <a href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>