<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ifeanyi Chima</title>
    <description>The latest articles on DEV Community by Ifeanyi Chima (@ifeanyichima).</description>
    <link>https://dev.to/ifeanyichima</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F763463%2F47ea4de3-e833-48d3-8a9b-507ac915483f.jpg</url>
      <title>DEV Community: Ifeanyi Chima</title>
      <link>https://dev.to/ifeanyichima</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ifeanyichima"/>
    <language>en</language>
    <item>
      <title>I solved a 5 year old bug</title>
      <dc:creator>Ifeanyi Chima</dc:creator>
      <pubDate>Thu, 30 Jul 2026 22:48:44 +0000</pubDate>
      <link>https://dev.to/ifeanyichima/i-solved-a-5-year-old-bug-356h</link>
      <guid>https://dev.to/ifeanyichima/i-solved-a-5-year-old-bug-356h</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Let me start by saying I am an Open-source contributor at Mozilla, I am trying to get better as a software engineer. I will be using the words &lt;strong&gt;Site&lt;/strong&gt; or &lt;strong&gt;Tile&lt;/strong&gt; interchangeably, they mean the same thing. &lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Report
&lt;/h2&gt;

&lt;p&gt;The title of the &lt;a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1628669" rel="noopener noreferrer"&gt;bug&lt;/a&gt; report was "&lt;strong&gt;Cannot remove amazon.com from top sites list&lt;/strong&gt;" on Bugzilla and I believe it really captures the pain-point that users experienced because of the bug.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvmt25jbxqyaolmqn8lg7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvmt25jbxqyaolmqn8lg7.png" alt="Bug report" width="800" height="278"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this bug report the user is complaining that he cannot remove &lt;u&gt;amazon.com&lt;/u&gt; from &lt;code&gt;Top Sites&lt;/code&gt; in the &lt;strong&gt;New tab page&lt;/strong&gt; of his Mozilla Firefox browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;A little back-story of what &lt;code&gt;Top Sites&lt;/code&gt;, &lt;code&gt;New tab page&lt;/code&gt; and &lt;code&gt;Places&lt;/code&gt; is: &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Top Sites&lt;/strong&gt; is that grid of website shortcuts you see on Firefox's New Tab Page. It shows the sites you visit most often, plus some sponsored tiles (called "spocs," short for sponsored content) that Mozilla partners provide. The image below shows a row of Top Sites on the New Tab page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb2e764iiwkxsp94k97rr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb2e764iiwkxsp94k97rr.png" alt="Top Sites in Firefox" width="800" height="217"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New tab page&lt;/strong&gt; is the page that opens when you open a new tab or click the home button.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Places&lt;/strong&gt; is a SQLite database that Firefox uses to store browsing history.  &lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Imagine you open a new tab in Firefox and see a grid of your most-visited websites (called "Top Sites"). &lt;u&gt;amazon.com&lt;/u&gt; keeps showing up there, even though you never visit it and you keep trying to delete it.&lt;/p&gt;

&lt;p&gt;You right-click on the Amazon icon, click "Delete from History," but... nothing happens. It just sits there, refusing to leave. You try again. Still there. You try "Dismiss." Still there. It's like a stubborn guest who won't leave your house.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Happens
&lt;/h2&gt;

&lt;p&gt;Firefox's Top Sites can come from three places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your actual browsing history (sites you really visited)&lt;/li&gt;
&lt;li&gt;Pinned sites (sites you manually pinned to stay there)&lt;/li&gt;
&lt;li&gt;Default/curated sites (sites Firefox puts there automatically, like Amazon, because they think you might want them)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stay with me and we would find out which one of these is the culprit. &lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the Bug (Part A)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Investigation:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By reading through the thread on the bug report, I discovered a trick other users on the thread had tried&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one user on the thread had the idea of changing the URL and then the problem went away, but after a new firefox release Firefox 82.0 (64-bit) the trick could no longer work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Reproducing:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To reproduce the bug locally, I had to visit &lt;code&gt;https://amazon.com&lt;/code&gt; multiple times (up to 20 times) on my local PC so that I can get &lt;code&gt;amazon.com&lt;/code&gt; to show up as a &lt;strong&gt;Top Site&lt;/strong&gt; on my New Tab page. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reviewer Feedback&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My first attempt was to block the &lt;code&gt;url&lt;/code&gt; whenever a user clicked "Delete History" so that it will never show up again in &lt;strong&gt;Top Sites&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;mak&lt;/strong&gt; (a mozilla engineer) pushed back on this, pointing out that if a user just wants to delete history, silently also blocking the site (removing it entirely from Top Sites without saying so) gives them unexpected behavior.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The problem with the fix is that the dialog asks to the user if they want to remove from history, but then the code proceed also blocking the site, that is not told to the user and may be unexpected.&lt;br&gt;
If the user wanted to effectively just remove history, not block the site, they will get the wrong behavior.&lt;/p&gt;

&lt;p&gt;I'm not expert of this code, but it looks like the proposed solution is broad and may cause unexpected behavior, actually removing control from the user rather than increasing it.&lt;/p&gt;

&lt;p&gt;It's touching the right problem though, someone should eval a UX solution.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I got a second feedback from another reviewer &lt;strong&gt;thecount&lt;/strong&gt; (a mozilla engineer) and he was the one who led me to the right fix for the bug.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you log &lt;code&gt;link.url&lt;/code&gt; and &lt;code&gt;searchProvider.url&lt;/code&gt;, like this &lt;code&gt;console.log(link.url, searchProvider.url);&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Right before this line: &lt;a href="https://searchfox.org/firefox-main/source/toolkit/modules/NewTabUtils.sys.mjs#1093" rel="noopener noreferrer"&gt;https://searchfox.org/firefox-main/source/toolkit/modules/NewTabUtils.sys.mjs#1093&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And then on startup, I see this in the logs: "&lt;a href="https://www.amazon.com/" rel="noopener noreferrer"&gt;https://www.amazon.com/&lt;/a&gt; &lt;a href="https://amazon.com" rel="noopener noreferrer"&gt;https://amazon.com&lt;/a&gt;".&lt;/p&gt;

&lt;p&gt;So this is the line that causes the url to be changed, and now the &lt;code&gt;url&lt;/code&gt; is no longer removable because it no longer matches the original places &lt;code&gt;url&lt;/code&gt;.&lt;br&gt;
one solution might be to store "originalUrl" &lt;code&gt;link.originalUrl = link.url&lt;/code&gt; in the block I linked above, and then when we delete the url, check if we have "originalUrl" and use that instead"? So something like &lt;code&gt;link.originalUrl || link.url&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;With this feedback, I was able to finally understand the bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Issue&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// toolkit/modules/NewTabUtils.sys.mjs&lt;/span&gt;

&lt;span class="nx"&gt;links&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;searchProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getSearchProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;NewTabUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shortURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchProvider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;searchProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;= swap is done here&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The issue was from here: you see Firefox does a check to see if &lt;u&gt;amazon.com&lt;/u&gt; is already stored as a search-shortcut provider and then it quietly drops the &lt;code&gt;url&lt;/code&gt; and replaces it with the internal &lt;code&gt;searchProvider.url&lt;/code&gt;. This is why when we click "Delete History" nothing happens because we have lost the &lt;code&gt;url&lt;/code&gt; that was supposed to match what we have in &lt;code&gt;Places&lt;/code&gt; (a database in Firefox).&lt;/p&gt;

&lt;p&gt;What is a search-shortcut provider, It means "a site Firefox knows how to search directly", and Amazon qualifies because Amazon has its own product search.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this means&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before a tile ever reaches the New Tab page, Firefox checks each link against its built-in list of search-shortcut sites (Google, Amazon, Bing, DuckDuckGo, eBay, Twitter, Wikipedia). If the link matches one of these, Firefox swaps that tile's url to a version meant to trigger a search on that site, not to visit the homepage you actually browsed to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the Bug (Part B)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mental model&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The mental model to have in Firefox is&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frontend:&lt;/strong&gt; the New Tab Page (React), shows the tiles, builds the request&lt;br&gt;
&lt;strong&gt;The request itself:&lt;/strong&gt; the payload sent from frontend to backend&lt;br&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; &lt;code&gt;PlacesFeed.sys.mjs&lt;/code&gt;, receives the request, touches the actual database (your browsing history)&lt;/p&gt;

&lt;p&gt;Like when building a To-do App, when you click on an item to delete from the database, you send the &lt;code&gt;id&lt;/code&gt; of the item as a payload to the backend (either in the URL or Request Body). But here, the real &lt;code&gt;id&lt;/code&gt; (&lt;code&gt;original_url&lt;/code&gt;) was quietly being generated and never forwarded, the frontend only ever knew about &lt;code&gt;url&lt;/code&gt;, which by that point pointed to the wrong thing. &lt;/p&gt;

&lt;p&gt;So, for the fix we are going to be borrowing heavily from the full-stack MVC architecture &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Breaking down the fix&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We make sure that we do not lose reference to the &lt;code&gt;url&lt;/code&gt; before it gets silently swapped out by Firefox, so we do something like this &lt;code&gt;link.original_url = link.url;&lt;/code&gt; here, we are storing away the &lt;code&gt;url&lt;/code&gt; in a variable called &lt;code&gt;original_url&lt;/code&gt; and it will be referenced later when we want to delete a &lt;strong&gt;site&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Allow me to break down the fix using REST API from Nodejs.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;A&lt;/strong&gt; This is like a backend endpoint that serves your frontend a list which we use to create the &lt;strong&gt;Tiles&lt;/strong&gt;. Here is where we create the &lt;strong&gt;Top Sites&lt;/strong&gt; and send to the frontend, but we quietly rename a field along the way.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/topsites&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getLinksFromDatabase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;searchProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getSearchProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchProvider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;original_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// &amp;lt;=backup the real id/url&lt;/span&gt;
        &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;searchProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// &amp;lt;=overwrite what the frontend will see&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;B&lt;/strong&gt; A user clicks a button which fires off a delete request and then we send both the &lt;code&gt;url&lt;/code&gt; and &lt;code&gt;original_url&lt;/code&gt; to the backend server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/delete-history&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DELETE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;original_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;original_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;C&lt;/strong&gt; The backend receives the request and decides what to actually delete&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/delete-history&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;original_url&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deleteEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;original_url&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It checks: "do I have an original_url? If yes, use that to delete. If not, fall back to url." &lt;/p&gt;

&lt;p&gt;This is important, because if it only used url, it would try to delete the search provider's link instead of the real saved site, and the deletion would silently fail or delete the wrong thing.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;Finally !!!!, now that we have a solid understanding of the issue and the fix, let us go ahead and implement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A.&lt;/strong&gt; Equivalent to Step 1 from the Rest API&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// toolkit/modules/NewTabUtils.sys.mjs&lt;/span&gt;
&lt;span class="nx"&gt;links&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;searchProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getSearchProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;NewTabUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shortURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchProvider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;original_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;searchProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;B.&lt;/strong&gt; Equivalent to Step 2 from the Rest API&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// browser/extensions/newtab/content-src/lib/link-menu-options.mjs&lt;/span&gt;
&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;onConfirm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="nx"&gt;ac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AlsoToMain&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DELETE_HISTORY_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;pocket_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pocket_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;forceBlock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bookmarkGuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;original_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;original_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;C.&lt;/strong&gt; Equivalent to Step 3 from the Rest API&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// browser/extensions/newtab/lib/PlacesFeed.sys.mjs&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DELETE_HISTORY_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;forceBlock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pocket_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;original_url&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NewTabUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activityStreamLinks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deleteHistoryEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;original_url&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I waited for my patch to be approved and landed. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu2p2jja42oync33gyao3.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu2p2jja42oync33gyao3.gif" alt="Bug Fix" width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TLDR:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The issue was that Amazon was in the user's actual history, but the displayed &lt;code&gt;url&lt;/code&gt; got silently swapped to the &lt;code&gt;search-shortcut&lt;/code&gt; version, so the delete call looked for a &lt;code&gt;url&lt;/code&gt; that no longer existed in &lt;code&gt;Places&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;🕷️ BUG: &lt;a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1628669" rel="noopener noreferrer"&gt;https://bugzilla.mozilla.org/show_bug.cgi?id=1628669&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💗 PATCH: &lt;a href="https://phabricator.services.mozilla.com/D292910" rel="noopener noreferrer"&gt;https://phabricator.services.mozilla.com/D292910&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👤 MY PROFILE ON BUGZILLA: &lt;a href="https://bugzilla.mozilla.org/user_profile?user_id=786766" rel="noopener noreferrer"&gt;https://bugzilla.mozilla.org/user_profile?user_id=786766&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Thank you, please follow me
&lt;/h2&gt;

</description>
      <category>mozilla</category>
      <category>browser</category>
      <category>devchallenge</category>
      <category>bugsmash</category>
    </item>
    <item>
      <title>Ask me anything (RAG)</title>
      <dc:creator>Ifeanyi Chima</dc:creator>
      <pubDate>Sat, 25 Jul 2026 16:15:19 +0000</pubDate>
      <link>https://dev.to/ifeanyichima/ask-me-anything-rag-32jl</link>
      <guid>https://dev.to/ifeanyichima/ask-me-anything-rag-32jl</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-gemma-2026-05-06"&gt;Gemma 4 Challenge: Build with Gemma 4&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;At LASU, students constantly ask each other things like "is this lecturer strict," "is this hostel safe," "what's SIWES actually like at this company," but that knowledge lives scattered across WhatsApp groups, Twitter threads, and word of mouth. It gets lost the moment a graduating class leaves. AMA was built to solve that: a shared memory for the school, where real student experiences get collected once and stay searchable by anyone, forever, instead of vanishing into a thousand disconnected chats.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/XlswFvh2mfU"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/MasterIfeanyi/hackathon-today" rel="noopener noreferrer"&gt;https://github.com/MasterIfeanyi/hackathon-today&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I used Gemma 4
&lt;/h2&gt;

&lt;p&gt;We used Gemma 4 (via OpenRouter) as the answer-generation model, and built the project around Retrieval-Augmented Generation (RAG), not fine-tuning, since we needed the answers to update instantly as new stories get submitted, without retraining anything.&lt;/p&gt;

&lt;p&gt;The pipeline works like this: when a student submits a story or review, we generate an embedding for it using Google's Gemini embedding model and store both the text and the embedding in MongoDB Atlas, using MongoDB Vector Search as our vector database. When someone asks a question, we embed the question the same way, run a vector similarity search to pull the most relevant stories, and pass only those into Gemma 4 as context, so every answer is grounded in real submissions instead of the model guessing.&lt;/p&gt;

&lt;p&gt;On the frontend, we used Next.js (App Router), Tailwind CSS v4 for styling, Zustand for lightweight UI state, and TanStack React Query for handling data fetching and caching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hackathon
&lt;/h2&gt;

&lt;p&gt;This was done as part of a university hackathon hosted by Google. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffbq82re8jb7n9hrstwqx.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffbq82re8jb7n9hrstwqx.jpg" alt="LASU Google Gemma Hackathon" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Thank you, please follow me
&lt;/h2&gt;

</description>
      <category>devchallenge</category>
      <category>gemmachallenge</category>
      <category>gemma</category>
      <category>ai</category>
    </item>
    <item>
      <title>Fullscreen icon RTL language support in Firefox browser</title>
      <dc:creator>Ifeanyi Chima</dc:creator>
      <pubDate>Tue, 14 Jul 2026 11:34:31 +0000</pubDate>
      <link>https://dev.to/ifeanyichima/fullscreen-icon-rtl-language-support-in-firefox-browser-247</link>
      <guid>https://dev.to/ifeanyichima/fullscreen-icon-rtl-language-support-in-firefox-browser-247</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy593f7jw14pebfx828e0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy593f7jw14pebfx828e0.png" alt="Zoom Icon in LTR mode" width="389" height="85"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our target is to make sure that when the firefox browser is set to RTL mode the fullscreen icon flips as well to follow suite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;When Firefox is set to an RTL (right-to-left) language like Arabic, the fullscreen icon inside the zoom sidebar still looks like it's designed for LTR (left-to-right) languages. It should be mirrored/flipped to match RTL direction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's RTL?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Languages like Arabic and Hebrew read from right to left. Browsers have a concept called &lt;code&gt;dir="rtl"&lt;/code&gt; that flips the layout. Icons that have a "direction" to them (like arrows, expand icons) should also flip to feel natural.&lt;/p&gt;

&lt;h3&gt;
  
  
  The plan:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;First, let's find where the zoom sidebar and its fullscreen icon live in the Firefox codebase&lt;/li&gt;
&lt;li&gt;See how the icon is currently being used&lt;/li&gt;
&lt;li&gt;Figure out how to make it flip for RTL&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can set your firefox browser to read from right-to-left using this in the console &lt;code&gt;CTRL+SHIFT+ALT+I&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dir&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rtl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  1. Find the files:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Zoom sidebar HTML file&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We can start by searching Searchfox (Mozilla's online code search) for terms like "zoom sidebar fullscreen" and "appmenu zoom controls." That search led me to &lt;code&gt;appmenu-viewcache.inc.xhtml&lt;/code&gt;, and inside it I saw this markup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;toolbaritem&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"appMenu-zoom-controls"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"subviewbutton toolbaritem-combined-buttons"&lt;/span&gt; &lt;span class="na"&gt;closemenu=&lt;/span&gt;&lt;span class="s"&gt;"none"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"toolbarbutton-text"&lt;/span&gt; &lt;span class="na"&gt;data-l10n-id=&lt;/span&gt;&lt;span class="s"&gt;"appmenuitem-zoom"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  ...
  &lt;span class="nt"&gt;&amp;lt;toolbarbutton&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"appMenu-fullscreen-button2"&lt;/span&gt;
                       &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"subviewbutton subviewbutton-iconic"&lt;/span&gt;
                       &lt;span class="na"&gt;data-l10n-id=&lt;/span&gt;&lt;span class="s"&gt;"appmenuitem-fullscreen"&lt;/span&gt;
                       &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"checkbox"&lt;/span&gt;
                       &lt;span class="na"&gt;tooltip=&lt;/span&gt;&lt;span class="s"&gt;"dynamic-shortcut-tooltip"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
           &lt;span class="nt"&gt;&amp;lt;observes&lt;/span&gt; &lt;span class="na"&gt;element=&lt;/span&gt;&lt;span class="s"&gt;"View:FullScreen"&lt;/span&gt; &lt;span class="na"&gt;attribute=&lt;/span&gt;&lt;span class="s"&gt;"checked"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/toolbarbutton&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/toolbaritem&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;FLUENT:&lt;/strong&gt; Firefox uses the &lt;code&gt;fluent&lt;/code&gt; localization system (more in later articles) &lt;/p&gt;

&lt;p&gt;&lt;code&gt;appmenuitem-zoom&lt;/code&gt; is the localization label for the word "Zoom" that sits above the row of buttons (&lt;code&gt;-&lt;/code&gt;, &lt;code&gt;100%&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt;, fullscreen). It's a distinctive, low-noise string and searching for it locally in VS code is a good way to land on the exact zoom row markup without pulling in unrelated matches.&lt;/p&gt;

&lt;p&gt;Let's start by searching for the zoom sidebar files using this &lt;code&gt;grep&lt;/code&gt; syntax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"appmenuitem-zoom"&lt;/span&gt; browser/base/content/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What we found: &lt;/p&gt;

&lt;p&gt;The fullscreen icon lives inside the zoom row of the hamburger menu (&lt;code&gt;☰&lt;/code&gt;). In the HTML file at &lt;code&gt;browser/base/content/appmenu-viewcache.inc.xhtml&lt;/code&gt;, the button is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;toolbarbutton&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"appMenu-fullscreen-button2"&lt;/span&gt;
                       &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"subviewbutton subviewbutton-iconic"&lt;/span&gt;
                       &lt;span class="na"&gt;data-l10n-id=&lt;/span&gt;&lt;span class="s"&gt;"appmenuitem-fullscreen"&lt;/span&gt;
                       &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"checkbox"&lt;/span&gt;
                       &lt;span class="na"&gt;tooltip=&lt;/span&gt;&lt;span class="s"&gt;"dynamic-shortcut-tooltip"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
           &lt;span class="nt"&gt;&amp;lt;observes&lt;/span&gt; &lt;span class="na"&gt;element=&lt;/span&gt;&lt;span class="s"&gt;"View:FullScreen"&lt;/span&gt; &lt;span class="na"&gt;attribute=&lt;/span&gt;&lt;span class="s"&gt;"checked"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/toolbarbutton&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ID of the fullscreen button in the zoom row of the hamburger menu is &lt;code&gt;appMenu-fullscreen-button2&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;This is very important to keep in mind.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zoom sidebar CSS file&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let us find the CSS file where its icon is defined using this &lt;code&gt;grep&lt;/code&gt; syntax&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"appMenu-fullscreen-button2"&lt;/span&gt; browser/themes/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What we found: &lt;/p&gt;

&lt;p&gt;This finds &lt;code&gt;browser/themes/shared/menupanel.css&lt;/code&gt;, the file where the fullscreen icon's image is actually set, which is where we will make our fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fullscreen icon&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"fullscreen.svg"&lt;/span&gt; browser/themes/shared/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can see that there is an image &lt;code&gt;fullscreen.svg&lt;/code&gt; that serves as the icon, located at &lt;code&gt;browser/themes/shared/icons/fullscreen.svg&lt;/code&gt; and gets applied via &lt;code&gt;browser/themes/shared/menupanel.css&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Here, we see the fullscreen icon, which looks like pointing outward from a center. &lt;/p&gt;

&lt;p&gt;Left-to-right &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F15pgzm4czgdduwx28nd3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F15pgzm4czgdduwx28nd3.png" alt="LTR" width="43" height="54"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Right-to-left&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvazo4hobdx0izwdtwyuu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvazo4hobdx0izwdtwyuu.png" alt="RTL" width="51" height="67"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Figuring out how to make it flip
&lt;/h3&gt;

&lt;p&gt;We need to take note of two things that will guide us to the actual fix.&lt;/p&gt;

&lt;p&gt;From what we saw here, we know that the zoom icon could live inside of &lt;code&gt;toolbarbutton&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;toolbarbutton&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"appMenu-fullscreen-button2"&lt;/span&gt;
                       &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"subviewbutton subviewbutton-iconic"&lt;/span&gt;
                       &lt;span class="na"&gt;data-l10n-id=&lt;/span&gt;&lt;span class="s"&gt;"appmenuitem-fullscreen"&lt;/span&gt;
                       &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"checkbox"&lt;/span&gt;
                       &lt;span class="na"&gt;tooltip=&lt;/span&gt;&lt;span class="s"&gt;"dynamic-shortcut-tooltip"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
           &lt;span class="nt"&gt;&amp;lt;observes&lt;/span&gt; &lt;span class="na"&gt;element=&lt;/span&gt;&lt;span class="s"&gt;"View:FullScreen"&lt;/span&gt; &lt;span class="na"&gt;attribute=&lt;/span&gt;&lt;span class="s"&gt;"checked"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/toolbarbutton&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;According to Mozilla &lt;a href="https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/toolbarbutton" rel="noopener noreferrer"&gt;guidelines&lt;/a&gt;, &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;toolbarbutton&amp;gt;&lt;/code&gt; is a Firefox custom element (defined in toolkit code, not in this XHTML file), and it generates its own internal "anonymous content" at runtime; meaning the &lt;code&gt;&amp;lt;image class="toolbarbutton-icon"&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;label class="toolbarbutton-text"&amp;gt;&lt;/code&gt; children are injected by the custom element's own implementation at render time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We can confirm the actual DOM structure using&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;appMenu-fullscreen-button2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;outerHTML&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This returns&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;toolbarbutton&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"appMenu-fullscreen-button2"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"subviewbutton subviewbutton-iconic"&lt;/span&gt; &lt;span class="na"&gt;data-l10n-id=&lt;/span&gt;&lt;span class="s"&gt;"appmenuitem-fullscreen"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"checkbox"&lt;/span&gt; &lt;span class="na"&gt;tooltip=&lt;/span&gt;&lt;span class="s"&gt;"dynamic-shortcut-tooltip"&lt;/span&gt; &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Full screen"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;observes&lt;/span&gt; &lt;span class="na"&gt;element=&lt;/span&gt;&lt;span class="s"&gt;"View:FullScreen"&lt;/span&gt; &lt;span class="na"&gt;attribute=&lt;/span&gt;&lt;span class="s"&gt;"checked"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;image&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"toolbarbutton-icon"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"checkbox"&lt;/span&gt; &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Full screen"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"toolbarbutton-text"&lt;/span&gt; &lt;span class="na"&gt;crop=&lt;/span&gt;&lt;span class="s"&gt;"end"&lt;/span&gt; &lt;span class="na"&gt;flex=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"Full screen"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/toolbarbutton&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we know &lt;code&gt;&amp;lt;toolbarbutton&amp;gt;&lt;/code&gt; elements in Firefox's UI don't render the icon directly on themselves; they render it inside a child element with the class &lt;code&gt;.toolbarbutton-icon&lt;/code&gt; &lt;/p&gt;

&lt;p&gt;We can confirm this once again by inspecting the element in the Browser Toolbox, when we run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;appMenu-fullscreen-button2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.toolbarbutton-icon&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;it returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;image&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"toolbarbutton-icon"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"checkbox"&lt;/span&gt; &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Full screen"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This confirms that's where the icon or image actually lives and we can target it using this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nf"&gt;#appMenu-fullscreen-button2&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;.toolbarbutton-icon&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This leads us to the next step in finding the fix.&lt;/p&gt;

&lt;p&gt;According to Mozilla &lt;a href="https://firefox-source-docs.mozilla.org/code-quality/coding-style/rtl_guidelines.html" rel="noopener noreferrer"&gt;guidelines&lt;/a&gt;,&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;icons implying direction should mirror in RTL, using this exact pattern as the recommended technique&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.element-with-icon&lt;/span&gt;&lt;span class="nd"&gt;:dir&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;rtl&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scaleX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;-1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c"&gt;/** scale: -1 1; does the same horizontal flip **/&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we know what our fix should be&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;The fix, then, is to add a CSS rule that uses &lt;code&gt;:dir(rtl)&lt;/code&gt; to horizontally flip the fullscreen icon whenever Firefox is in RTL mode. Since the icon itself lives inside the &lt;code&gt;.toolbarbutton-icon&lt;/code&gt; child element, that's the element we need to target, using &lt;code&gt;scale: -1 1&lt;/code&gt; to perform the flip.&lt;/p&gt;

&lt;p&gt;The file to edit is: &lt;code&gt;browser/themes/shared/menupanel.css&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* To flip an icon for RTL using scale: */&lt;/span&gt;
&lt;span class="nf"&gt;#appMenu-fullscreen-button2&lt;/span&gt;&lt;span class="nd"&gt;:dir&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;rtl&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;.toolbarbutton-icon&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-1&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means "when this specific button is inside an RTL context, flip its icon child horizontally."&lt;/p&gt;

&lt;h4&gt;
  
  
  Summary
&lt;/h4&gt;

&lt;p&gt;When you open the hamburger menu (&lt;code&gt;☰&lt;/code&gt;) in Firefox, there's a zoom row that looks like: &lt;code&gt;Zoom — [-] 100% [+] [⤢]&lt;/code&gt;. That last icon &lt;code&gt;[⤢]&lt;/code&gt; is the fullscreen button (appMenu-fullscreen-button2).&lt;/p&gt;

&lt;p&gt;In Arabic and other RTL languages, this icon doesn't flip to match the reading direction, it stays the same as in English (LTR). It should be mirrored.&lt;/p&gt;

&lt;p&gt;💗 PATCH: &lt;a href="https://phabricator.services.mozilla.com/D309258" rel="noopener noreferrer"&gt;https://phabricator.services.mozilla.com/D309258&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🕷️ BUG: &lt;a href="https://bugzilla.mozilla.org/show_bug.cgi?id=2029514" rel="noopener noreferrer"&gt;https://bugzilla.mozilla.org/show_bug.cgi?id=2029514&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👤 MY PROFILE ON BUGZILLA: &lt;a href="https://bugzilla.mozilla.org/user_profile?user_id=786766" rel="noopener noreferrer"&gt;https://bugzilla.mozilla.org/user_profile?user_id=786766&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>firefox</category>
      <category>devchallenge</category>
      <category>bugsmash</category>
    </item>
    <item>
      <title>Writing unit test for Firefox private browsing window</title>
      <dc:creator>Ifeanyi Chima</dc:creator>
      <pubDate>Sun, 05 Jul 2026 17:35:35 +0000</pubDate>
      <link>https://dev.to/ifeanyichima/writing-unit-test-for-firefox-private-browsing-window-25oj</link>
      <guid>https://dev.to/ifeanyichima/writing-unit-test-for-firefox-private-browsing-window-25oj</guid>
      <description>&lt;p&gt;Our indicator looks like this: an icon + a label. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3gd1anw6rzz19qhkawbb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3gd1anw6rzz19qhkawbb.png" alt="private window indicator" width="235" height="63"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The browser has an indicator for when a user is in private mode. The indicator lives in two places under different instances: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The first instance is when the user is in "Horizontal Tabs" when the browser tabs are in the &lt;code&gt;TabsToolbar&lt;/code&gt; (tab-bar or toolbar). So, the indicator shows both the icon and it's label. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The second instance is when the user has "Vertical Tabs" turned on. So, the indicator moves or shows up in the &lt;code&gt;Navbar&lt;/code&gt; (which contains extensions, address bar) and this time only the icon shows up, no label. &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Our task is to write a unit test to confirm that the icon shows up in both instance. &lt;/p&gt;

&lt;p&gt;We start off by running this search &lt;code&gt;grep -r "private-browsing-indicator-with-label" --include="*.xhtml" --include="*.js" --include="*.css" .&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;We found two things. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The "Shared" CSS file that controls the visibility of this icon &lt;code&gt;browser/themes/shared/browser-shared.css&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;The &lt;code&gt;browser/base/content/private-browsing-indicator.inc.xhtml&lt;/code&gt; — this is where the indicator's actual markup (HTML-like structure) lives.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now, we can check the CSS file for where it controls the visibility of the indicator by using this syntax &lt;code&gt;grep -n -A 10 "private-browsing-indicator-with-label" browser/themes/shared/browser-shared.css&lt;/code&gt; &lt;/p&gt;

&lt;p&gt;What we find out in the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.private-browsing-indicator-with-label&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="py"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;not&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;privatebrowsingmode&lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"temporary"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="err"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="n"&gt;navigator-toolbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;not&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;tabs-hidden&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="n"&gt;nav-bar&lt;/span&gt; &lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;&amp;amp;&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;#navigator-toolbox&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;tabs-hidden&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="nf"&gt;#nav-bar&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;margin-inline-end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--space-medium&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;.private-browsing-indicator-label&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this means: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;When the window isn't a private window (we are in normal mode) - it will hide the indicator completely.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When we have horizontal tabs in private window, hide the indicator from the navbar (which contains extensions, address bar) and then the indicator will move to the &lt;code&gt;TabsToolbar&lt;/code&gt; (which actually contains browser tabs).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvf2v4een8lrkp5lwrry5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvf2v4een8lrkp5lwrry5.png" alt="Horizontal Tabs enabled" width="800" height="286"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The image above shows that we are still in horizontal tabs&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When we have vertical tabs while still in private window, the &lt;code&gt;private-browsing-indicator&lt;/code&gt; will move to the navbar and then hide the label part of the indicator and keep only the icon.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcv3izr2pj29ar99jzunu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcv3izr2pj29ar99jzunu.png" alt="Vertical Tab is enabled" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The above image shows vertical tabs enabled&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing the tests
&lt;/h2&gt;

&lt;p&gt;Before we begin writing the tests, we would need to find other existing tests in the codebase to take inspiration from them, by using the search query: &lt;/p&gt;

&lt;p&gt;&lt;code&gt;grep -n "is_visible\|isVisible\|is_hidden\|isHidden" testing/mochitest/BrowserTestUtils/BrowserTestUtils.sys.mjs&lt;/code&gt; and then we read the file &lt;code&gt;sed -n '325,350p' testing/mochitest/BrowserTestUtils/BrowserTestUtils.sys.mjs&lt;/code&gt; &lt;/p&gt;

&lt;p&gt;This gives us a lot of information:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;What to check: both copies of &lt;code&gt;.private-browsing-indicator-with-label&lt;/code&gt; (one inside &lt;code&gt;TabsToolbar&lt;/code&gt;, one inside &lt;code&gt;nav-bar&lt;/code&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How to toggle vertical tabs: by using the pref &lt;code&gt;sidebar.verticalTabs&lt;/code&gt; as seen here&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;SpecialPowers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pushPrefEnv&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sidebar.revamp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sidebar.verticalTabs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Task structure: &lt;code&gt;add_task(async function () {...})&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Check visibility: &lt;code&gt;BrowserTestUtils.isVisible(element)&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How to open a private window (modern style): &lt;code&gt;BrowserTestUtils.openNewBrowserWindow({ private: true })&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, we can write the tests as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;
&lt;span class="cm"&gt;/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */&lt;/span&gt;

&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use strict&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// The private browsing indicator lives in two possible spots:&lt;/span&gt;
&lt;span class="c1"&gt;// - Inside #TabsToolbar (shown with icon + label, for horizontal tabs)&lt;/span&gt;
&lt;span class="c1"&gt;// - Inside #nav-bar (shown with icon only, for vertical tabs)&lt;/span&gt;
&lt;span class="c1"&gt;// This test makes sure at least one copy is always visible in a&lt;/span&gt;
&lt;span class="c1"&gt;// private window, no matter which tabs layout is active.&lt;/span&gt;

&lt;span class="nf"&gt;add_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;test_indicator_horizontal_tabs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;win&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;BrowserTestUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openNewBrowserWindow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;private&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;tabsToolbarIndicator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;win&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#TabsToolbar .private-browsing-indicator-with-label&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;navBarIndicator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;win&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#nav-bar .private-browsing-indicator-with-label&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;BrowserTestUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isVisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tabsToolbarIndicator&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Indicator should be visible in TabsToolbar with horizontal tabs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;BrowserTestUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isHidden&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;navBarIndicator&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Indicator in nav-bar should be hidden with horizontal tabs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;BrowserTestUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closeWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;win&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;add_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;test_indicator_vertical_tabs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;SpecialPowers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pushPrefEnv&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sidebar.revamp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sidebar.verticalTabs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;win&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;BrowserTestUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openNewBrowserWindow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;private&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;tabsToolbarIndicator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;win&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#TabsToolbar .private-browsing-indicator-with-label&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;navBarIndicator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;win&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#nav-bar .private-browsing-indicator-with-label&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;BrowserTestUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isHidden&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tabsToolbarIndicator&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Indicator in TabsToolbar should be hidden with vertical tabs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;BrowserTestUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isVisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;navBarIndicator&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Indicator should be visible in nav-bar with vertical tabs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;BrowserTestUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closeWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;win&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;SpecialPowers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;popPrefEnv&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;add_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;test_indicator_not_shown_in_normal_window&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;win&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;BrowserTestUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openNewBrowserWindow&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;indicators&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;win&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.private-browsing-indicator-with-label&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;indicator&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;indicators&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;BrowserTestUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isHidden&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;indicator&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Indicator should never be visible in a normal (non-private) window&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;BrowserTestUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closeWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;win&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Explaining the test:
&lt;/h2&gt;

&lt;p&gt;Now, here is what this test is doing. Remember at the beginning, I said our private browsing lives in two possible spots: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Inside #TabsToolbar (shown with icon + label, for horizontal tabs): This is when we have private browsing enabled, but we are still using horizontal browser tabs (the default)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Inside #nav-bar (shown with icon only, for vertical tabs): This is when the user has enabled / turned on Vertical browser tabs. &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h5&gt;
  
  
  Test 1: Horizontal Browser Tabs
&lt;/h5&gt;

&lt;p&gt;The first test is saying open a private browser window using &lt;code&gt;await BrowserTestUtils.openNewBrowserWindow({ private: true });&lt;/code&gt;  and then we are making sure that when we are in Horizontal Mode - the navbar indicator IS NOT showing. The only indicator that should be showing in Horizontal Mode is the &lt;code&gt;tabsToolbarIndicator&lt;/code&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  Test 2: Vertical Browser Tabs
&lt;/h5&gt;

&lt;p&gt;We open a private browser window using &lt;code&gt;await BrowserTestUtils.openNewBrowserWindow({ private: true });&lt;/code&gt; and now, we assume that the user has turned on vertical tabs, how do we do that in our test ?.... by using this &lt;code&gt;pref&lt;/code&gt; to turn on vertical tabs during the test run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;SpecialPowers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pushPrefEnv&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sidebar.revamp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sidebar.verticalTabs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, in Vertical Mode the only indicator that should show is the &lt;code&gt;navBarIndicator&lt;/code&gt; and the other one should not show. Also, do not forget to close the browser window and cleanup the &lt;code&gt;Pref&lt;/code&gt; using&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;BrowserTestUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closeWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;win&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;SpecialPowers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;popPrefEnv&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  Test 3: Normal Browser Window
&lt;/h5&gt;

&lt;p&gt;In this test, we open a normal browsing window and we make sure that none of those labels either &lt;code&gt;navBarIndicator&lt;/code&gt; or &lt;code&gt;tabsToolbarIndicator&lt;/code&gt; show up when the user is NOT using private browsing.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;TLDR:&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;What each test does, briefly&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;test_indicator_horizontal_tabs&lt;/code&gt;: opens a private window in normal (default) tabs mode, checks the TabsToolbar indicator is visible and the nav-bar indicator is hidden.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test_indicator_vertical_tabs&lt;/code&gt;: opens a private window, turns on vertical tabs mode, check that the nav-bar indicator is visible, TabsToolbar indicator hidden.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test_indicator_not_shown_in_normal_window&lt;/code&gt;: opens a completely normal (non-private window) and makes sure neither indicator shows up at all since this isn't a private window in the first place.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Registering the test
&lt;/h2&gt;

&lt;p&gt;In Firefox, whenever you write a test, you must register the test in &lt;code&gt;browser.toml&lt;/code&gt; which is meant for that particular component - We are writing a test for the private browsing component; hence the file is located at &lt;code&gt;browser/components/privatebrowsing/test/browser/browser.toml&lt;/code&gt; &lt;/p&gt;

&lt;p&gt;Components are registered in an alphabetical order in this file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;["browser_privatebrowsing_favicon.js"]&lt;/span&gt;

&lt;span class="nn"&gt;["browser_privatebrowsing_history_shift_click.js"]&lt;/span&gt;

&lt;span class="nn"&gt;["browser_privatebrowsing_indicator.js"]&lt;/span&gt; &lt;span class="c"&gt;# our unit test &lt;/span&gt;

&lt;span class="nn"&gt;["browser_privatebrowsing_last_private_browsing_context_exited.js"]&lt;/span&gt;

&lt;span class="nn"&gt;["browser_privatebrowsing_lastpbcontextexited.js"]&lt;/span&gt;

&lt;span class="nn"&gt;["browser_privatebrowsing_localStorage.js"]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;SUMMARY:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a user switches Firefox to private browsing mode, the indicator shows up, and the bug report was that there was never a unit test to ensure that the indicator is present in various browsing modes and so we wrote unit tests for those instances of the browser mode&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Horizontal tabs mode (while private browsing is turned on)&lt;/li&gt;
&lt;li&gt;Vertical tabs mode (while private browsing is turned on)&lt;/li&gt;
&lt;li&gt;Normal browsing mode. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Find the Patch Here: &lt;a href="https://phabricator.services.mozilla.com/D310523" rel="noopener noreferrer"&gt;https://phabricator.services.mozilla.com/D310523&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Find the Bug Here: &lt;a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1927615" rel="noopener noreferrer"&gt;https://bugzilla.mozilla.org/show_bug.cgi?id=1927615&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;THANK YOU&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>css</category>
      <category>html</category>
      <category>browser</category>
      <category>firefox</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Ifeanyi Chima</dc:creator>
      <pubDate>Mon, 29 Jun 2026 08:29:49 +0000</pubDate>
      <link>https://dev.to/ifeanyichima/-3pbj</link>
      <guid>https://dev.to/ifeanyichima/-3pbj</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/devengers/dev-opportunity-radar-5-a-fully-funded-trip-to-aws-reinvent-google-cloud-career-launchpad-and-3p6e" class="crayons-story__hidden-navigation-link"&gt;Dev Opportunity Radar #5: A Fully Funded Trip to AWS re:Invent, Google Cloud Career Launchpad, and a $1,000 Award&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
      &lt;a href="https://dev.to/devengers/dev-opportunity-radar-5-a-fully-funded-trip-to-aws-reinvent-google-cloud-career-launchpad-and-3p6e" class="crayons-article__context-note crayons-article__context-note__feed"&gt;&lt;p&gt;Weekly community-sourced tech resources&lt;/p&gt;

&lt;/a&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;
          &lt;a class="crayons-logo crayons-logo--l" href="/devengers"&gt;
            &lt;img alt="The DEVengers logo" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F12483%2Fb3a03d2d-fbb6-4355-9593-dff0f92638e9.webp" class="crayons-logo__image" width="800" height="800"&gt;
          &lt;/a&gt;

          &lt;a href="/hemapriya_kanagala" class="crayons-avatar  crayons-avatar--s absolute -right-2 -bottom-2 border-solid border-2 border-base-inverted  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3307586%2F2dffaf97-946d-44a6-8a39-07d94a72e07d.png" alt="hemapriya_kanagala profile" class="crayons-avatar__image" width="800" height="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/hemapriya_kanagala" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Hemapriya Kanagala
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Hemapriya Kanagala
                
                
              
              &lt;div id="story-author-preview-content-3999153" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/hemapriya_kanagala" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3307586%2F2dffaf97-946d-44a6-8a39-07d94a72e07d.png" class="crayons-avatar__image" alt="" width="800" height="800"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Hemapriya Kanagala&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;span&gt;
              &lt;span class="crayons-story__tertiary fw-normal"&gt; for &lt;/span&gt;&lt;a href="/devengers" class="crayons-story__secondary fw-medium"&gt;The DEVengers&lt;/a&gt;
            &lt;/span&gt;
          &lt;/div&gt;
          &lt;a href="https://dev.to/devengers/dev-opportunity-radar-5-a-fully-funded-trip-to-aws-reinvent-google-cloud-career-launchpad-and-3p6e" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jun 26&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/devengers/dev-opportunity-radar-5-a-fully-funded-trip-to-aws-reinvent-google-cloud-career-launchpad-and-3p6e" id="article-link-3999153"&gt;
          Dev Opportunity Radar #5: A Fully Funded Trip to AWS re:Invent, Google Cloud Career Launchpad, and a $1,000 Award
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag crayons-tag--filled  " href="/t/discuss"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;discuss&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/community"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;community&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/opportunities"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;opportunities&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/resources"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;resources&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/devengers/dev-opportunity-radar-5-a-fully-funded-trip-to-aws-reinvent-google-cloud-career-launchpad-and-3p6e" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;71&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/devengers/dev-opportunity-radar-5-a-fully-funded-trip-to-aws-reinvent-google-cloud-career-launchpad-and-3p6e#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              26&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            9 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Ifeanyi Chima</dc:creator>
      <pubDate>Tue, 23 Jun 2026 13:37:49 +0000</pubDate>
      <link>https://dev.to/ifeanyichima/-54id</link>
      <guid>https://dev.to/ifeanyichima/-54id</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/kristinz/from-react-developer-to-ai-engineer-what-actually-changes-581n" class="crayons-story__hidden-navigation-link"&gt;From React Developer to AI Engineer: What Actually Changes&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/kristinz" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3973719%2F2ff45f78-40d5-4877-bb58-746f577abeee.png" alt="kristinz profile" class="crayons-avatar__image" width="800" height="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/kristinz" class="crayons-story__secondary fw-medium m:hidden"&gt;
              KristinZ
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                KristinZ
                
                
              
              &lt;div id="story-author-preview-content-3846873" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/kristinz" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3973719%2F2ff45f78-40d5-4877-bb58-746f577abeee.png" class="crayons-avatar__image" alt="" width="800" height="800"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;KristinZ&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/kristinz/from-react-developer-to-ai-engineer-what-actually-changes-581n" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jun 8&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/kristinz/from-react-developer-to-ai-engineer-what-actually-changes-581n" id="article-link-3846873"&gt;
          From React Developer to AI Engineer: What Actually Changes
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/typescript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;typescript&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/career"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;career&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/kristinz/from-react-developer-to-ai-engineer-what-actually-changes-581n" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;4&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/kristinz/from-react-developer-to-ai-engineer-what-actually-changes-581n#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              3&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            6 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Solstice cipher: Turing complete</title>
      <dc:creator>Ifeanyi Chima</dc:creator>
      <pubDate>Mon, 08 Jun 2026 20:49:21 +0000</pubDate>
      <link>https://dev.to/ifeanyichima/solstice-cipher-turing-complete-2k1b</link>
      <guid>https://dev.to/ifeanyichima/solstice-cipher-turing-complete-2k1b</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/june-game-jam-2026-06-03"&gt;June Solstice Game Jam&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Solstice Cipher is a browser-based game built around the tension between light and darkness — the core theme of the June Solstice.&lt;/p&gt;

&lt;p&gt;The screen is split into two halves: pitch black on the left, golden yellow on the right. A Caesar cipher is presented to the player in a floating modal. Decode it correctly and the light grows. Guess wrong and the darkness creeps forward.&lt;/p&gt;

&lt;p&gt;The game ends when either side fully consumes the screen. Darkness wins by default if you keep getting answers wrong.&lt;/p&gt;

&lt;p&gt;The cipher words are selected randomly from a pool of over 6,500 common English words and encrypted using Caesar cipher with a shift of 3. Google's Gemma 4 model handles the encryption of each word on the server, with a verified fallback to ensure the player always gets a mathematically correct puzzle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Video Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/tFLnM4Jbg5I"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/MasterIfeanyi" rel="noopener noreferrer"&gt;
        MasterIfeanyi
      &lt;/a&gt; / &lt;a href="https://github.com/MasterIfeanyi/solstice-game" rel="noopener noreferrer"&gt;
        solstice-game
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;This is a &lt;a href="https://nextjs.org" rel="nofollow noopener noreferrer"&gt;Next.js&lt;/a&gt; project bootstrapped with &lt;a href="https://github.com/vercel/next.js/tree/canary/packages/create-next-app" rel="noopener noreferrer"&gt;&lt;code&gt;create-next-app&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Getting Started&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;First, run the development server:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;npm run dev
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; or&lt;/span&gt;
yarn dev
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; or&lt;/span&gt;
pnpm dev
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; or&lt;/span&gt;
bun dev&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Open &lt;a href="http://localhost:3000" rel="nofollow noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt; with your browser to see the result.&lt;/p&gt;
&lt;p&gt;You can start editing the page by modifying &lt;code&gt;app/page.js&lt;/code&gt;. The page auto-updates as you edit the file.&lt;/p&gt;
&lt;p&gt;This project uses &lt;a href="https://nextjs.org/docs/app/building-your-application/optimizing/fonts" rel="nofollow noopener noreferrer"&gt;&lt;code&gt;next/font&lt;/code&gt;&lt;/a&gt; to automatically optimize and load &lt;a href="https://vercel.com/font" rel="nofollow noopener noreferrer"&gt;Geist&lt;/a&gt;, a new font family for Vercel.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Learn More&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;To learn more about Next.js, take a look at the following resources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://nextjs.org/docs" rel="nofollow noopener noreferrer"&gt;Next.js Documentation&lt;/a&gt; - learn about Next.js features and API.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nextjs.org/learn" rel="nofollow noopener noreferrer"&gt;Learn Next.js&lt;/a&gt; - an interactive Next.js tutorial.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can check out &lt;a href="https://github.com/vercel/next.js" rel="noopener noreferrer"&gt;the Next.js GitHub repository&lt;/a&gt; - your feedback and contributions are welcome!&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Deploy on Vercel&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;The easiest way to deploy your Next.js app is to use the &lt;a href="https://vercel.com/new?utm_medium=default-template&amp;amp;filter=next.js&amp;amp;utm_source=create-next-app&amp;amp;utm_campaign=create-next-app-readme" rel="nofollow noopener noreferrer"&gt;Vercel Platform&lt;/a&gt; from the creators of Next.js.&lt;/p&gt;
&lt;p&gt;Check out our &lt;a href="https://nextjs.org/docs/app/building-your-application/deploying" rel="nofollow noopener noreferrer"&gt;Next.js deployment documentation&lt;/a&gt; for more…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/MasterIfeanyi/solstice-game" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;The stack is Next.js 16 with JavaScript, plain CSS modules, and Tailwind v4.&lt;/p&gt;

&lt;p&gt;The architecture is built around a single GameContext using React's Context API, which broadcasts game state to every component without prop drilling. localStorage persists the light and dark widths across page refreshes so your progress survives an accidental reload.&lt;/p&gt;

&lt;p&gt;The word selection pipeline works like this: a random word is picked on the server from a curated list of 6,500+ words sourced from Google's open source 10,000 most common English words list. That word is sent to Gemma 4 via OpenRouter to encrypt using Caesar cipher shift 3. Our server then verifies Gemma's encryption independently and corrects it silently if there is a mismatch, so the player always gets a valid puzzle.&lt;/p&gt;

&lt;p&gt;For true randomness, a unique seed and offset generated with Math.floor(Math.random() * 1000000) is passed with every request. A rate limit fallback handles OpenRouter 429 responses gracefully by falling back to local encryption, and a network failure fallback handles complete internet loss.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;DRY principle applied throughout with reusable Button, Modal, Header, and Navbar components.&lt;/li&gt;
&lt;li&gt;A storage.js utility abstracts all localStorage operations with SSR safety guards.&lt;/li&gt;
&lt;li&gt;Caesar cipher logic lives in one encryptCaesar function used everywhere on the server.&lt;/li&gt;
&lt;li&gt;useRef tracks recently used words to prevent Gemma from repeating itself without triggering re-renders.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prize Category
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best Ode to Alan Turing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As a computer science grad myself, I look up to Turing. His work in cryptography inspired this mini-game. I am in awe of his ability to turn a problem of decrypting German messages into a mathematical problem and then how he built a physical machine that took his instructions and was able to decrypt German signals.&lt;/p&gt;

&lt;p&gt;That same spirit lives in Solstice Cipher. The Caesar cipher is one of the oldest encryption algorithms in history, and this game puts the player in the position Turing's team faced at Bletchley Park: staring at encrypted text and racing to decode it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best Google AI Usage&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Google's Gemma 4 model powers the encryption engine of every round via OpenRouter. Each word selected for the player is sent to Gemma 4 with a structured prompt instructing it to apply Caesar cipher shift 3 and return a verified JSON response. The server independently verifies Gemma's output and corrects any mismatch, ensuring the AI is genuinely doing cryptographic work on every single round rather than acting as a simple random number generator.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>gamechallenge</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Copilot with an Eagle eye</title>
      <dc:creator>Ifeanyi Chima</dc:creator>
      <pubDate>Mon, 25 May 2026 19:31:49 +0000</pubDate>
      <link>https://dev.to/ifeanyichima/copilot-with-an-eagle-eye-1j0d</link>
      <guid>https://dev.to/ifeanyichima/copilot-with-an-eagle-eye-1j0d</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/github-2026-05-21"&gt;GitHub Finish-Up-A-Thon Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;In Nigeria today, insecurity and corruption is a very big issue with no hope in sight of solving it as they both go in tandem. I have been in multiple situations where I wished I could contact emergency services as soon as possible but to no avail. Also, to be honest when I started this project in 2025, the goal was to provide a very simple way to combat corruption by turning even the layman into a watchful vigilante. So, the goal of this project was to prototype a simple system called "Eagle Eye" that will allow you to make reports to the appropriate authorities about corruption and any insecurity in an area. &lt;/p&gt;

&lt;p&gt;I really hope to make this platform or website actually grow into a nationwide tool complete with an admin dashboard and a response team. I took massive inspiration from the London underground railway system: "see something, say something".&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/VFb07sPWJzU"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Live link: &lt;a href="https://ifeanyi-eagle-eye.netlify.app/home" rel="noopener noreferrer"&gt;https://ifeanyi-eagle-eye.netlify.app/home&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GitHub Repo: &lt;a href="https://github.com/MasterIfeanyi/Eagle-Eye-App" rel="noopener noreferrer"&gt;https://github.com/MasterIfeanyi/Eagle-Eye-App&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Comeback Story
&lt;/h2&gt;

&lt;p&gt;The project was previously using Firebase before and I racked up a bill of $180 which hurt massively, so I decided to re-write it using Nextjs and MongoDB, and my assistant of choice was GitHub Co-pilot. I chose Nextjs and MongoDB because Nextjs to me feels like a full-stack option with a built-in /api folder and MongoDB Atlas is a free database service that I could use to store reports and perform authentication of users. Along the way I decided to add a page where you could get phone number of emergency services. If not for this hack-a-thon, I may have never come back to this project because I have been putting it off for some time as I am was busy.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Experience with GitHub Copilot
&lt;/h2&gt;

&lt;p&gt;Building Eagle Eye was not a smooth process. There were moments where the project felt completely stuck, and honestly, GitHub Copilot was a massive part of that process, as I primarily write Nextjs, but Co-pilot guided me and took away my fears. GitHub Copilot helped me think through the architecture from the ground up, guiding decisions like how to structure the Next.js app directory, how to handle authentication with JWT and cookies, and how to connect the frontend to MongoDB. When I hit bugs, like a broken scroll on the hotlines page, a sticky header that refused to stick, or a location detector pulling the wrong city, GitHub Copilot did not just hand me answers. It explained why things were breaking so I actually understood the fix. What I appreciate most is that GitHub Copilot treated me like a developer who was learning, not one who needed things done for them. This project genuinely would not exist in its current state without that support. Although, I was limited to the Claude Haiku 4.5 model (free plan), I feel like it was sufficient enought to help me.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>githubchallenge</category>
    </item>
    <item>
      <title>How to use Google Gemma 4 model</title>
      <dc:creator>Ifeanyi Chima</dc:creator>
      <pubDate>Thu, 14 May 2026 08:58:30 +0000</pubDate>
      <link>https://dev.to/ifeanyichima/how-to-use-google-gemma-4-model-2o3l</link>
      <guid>https://dev.to/ifeanyichima/how-to-use-google-gemma-4-model-2o3l</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-gemma-2026-05-06"&gt;Gemma 4 Challenge: Write About Gemma 4&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I Built a Celebrity AI Chat App with Google Gemma 4 26B-A4B, Here's Everything You Need to Know&lt;/p&gt;

&lt;p&gt;I've always been fascinated by the idea of talking to your Elon Musk, but for now we would have to settle down for a &lt;strong&gt;chatbot&lt;/strong&gt; that actually &lt;em&gt;feels&lt;/em&gt; like them. So when Google dropped Gemma 4 in April 2026, I saw my chance. I built a web app with a chat UI where you pick a celebrity, and Gemma 4 slips into their persona and holds a full conversation with you. The results were genuinely impressive and sometimes hilarious.&lt;/p&gt;

&lt;p&gt;But before I walk you through how I built it, let me give you a proper introduction to the models that made it possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Understanding the Google Gemma 4 Model Family
&lt;/h2&gt;

&lt;p&gt;Gemma 4 is Google DeepMind's latest generation of open models, released on April 2, 2026. It comes in &lt;strong&gt;four distinct sizes&lt;/strong&gt;, each targeting a different use case, from running on your phone to powering a server-grade workstation. All four models are multimodal (they can process text and images), licensed under Apache 2.0, and support over 140 languages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quick Comparison Table
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Architecture&lt;/th&gt;
&lt;th&gt;Context&lt;/th&gt;
&lt;th&gt;Audio&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;E2B&lt;/td&gt;
&lt;td&gt;Efficient Dense&lt;/td&gt;
&lt;td&gt;128K&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Mobile / Edge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E4B&lt;/td&gt;
&lt;td&gt;Efficient Dense&lt;/td&gt;
&lt;td&gt;128K&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;On-device assistants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;26B A4B&lt;/td&gt;
&lt;td&gt;Mixture of Experts&lt;/td&gt;
&lt;td&gt;256K&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;High-throughput APIs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;31B&lt;/td&gt;
&lt;td&gt;Dense&lt;/td&gt;
&lt;td&gt;256K&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;Maximum quality&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  2. How to Get an API Key
&lt;/h2&gt;

&lt;p&gt;Before you can start building anything with Gemma 4, you need API access. The good news? You have a couple of options and one of them is completely free to start.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option A: Google AI Studio (Direct from Google)
&lt;/h3&gt;

&lt;p&gt;Google provides direct API access to Gemma 4 through &lt;strong&gt;Google AI Studio&lt;/strong&gt;. Here's how to get your key:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://aistudio.google.com" rel="noopener noreferrer"&gt;aistudio.google.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sign in with your Google account&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Get API key"&lt;/strong&gt; in the left sidebar&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Create API key"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Copy your key and store it somewhere safe (you won't be able to see it again)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmip5p7ed8ne4dzfj82v1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmip5p7ed8ne4dzfj82v1.png" alt="Google AI Studio" width="800" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's it. Google AI Studio gives you a generous free tier to experiment with before you need to think about billing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Never hardcode your API key in your frontend code. Always store it in an environment variable (&lt;code&gt;.env&lt;/code&gt; file) and access it from your backend.&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;.env&lt;/code&gt; to your &lt;code&gt;.gitignore&lt;/code&gt; before your first commit, not after.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Option B: OpenRouter
&lt;/h3&gt;

&lt;p&gt;OpenRouter is a unified API gateway that gives you access to hundreds of models including all Gemma 4 variants through a single, OpenAI-compatible endpoint. That means you can switch models with one line of code change, get automatic fallbacks if a provider goes down, and even access the &lt;strong&gt;free tier&lt;/strong&gt; of Gemma 4 31B without spending a cent to start.&lt;/p&gt;

&lt;p&gt;To get your OpenRouter API key:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://openrouter.ai" rel="noopener noreferrer"&gt;openrouter.ai&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Sign In"&lt;/strong&gt; and create an account&lt;/li&gt;
&lt;li&gt;Once logged in, click &lt;strong&gt;"Get API Key"&lt;/strong&gt; button from your dashboard&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"New Key"&lt;/strong&gt;, give it a name (e.g. &lt;code&gt;celeb-chat-dev&lt;/code&gt;), and hit create&lt;/li&gt;
&lt;li&gt;Copy and save your key it starts with &lt;code&gt;sk-or-...&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can optionally add credits to your account for paid usage, but to start experimenting, the free-tier models (including &lt;code&gt;google/gemma-4-26b-a4b-it:free&lt;/code&gt;) are more than enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. How to Use Google Gemma 4 Through OpenRouter
&lt;/h2&gt;

&lt;p&gt;Alright, this is where the fun begins. Let me walk you through exactly how I wired up Gemma 4 via OpenRouter to power my celebrity chat app, from the first API call all the way to a working chat UI.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Base URL and Model IDs
&lt;/h3&gt;

&lt;p&gt;OpenRouter exposes Gemma 4 through a single endpoint that's fully compatible with the OpenAI Chat API format. If you've ever called GPT-4 or any other model via OpenAI's SDK, this will feel immediately familiar, you're literally changing one URL and one model string.&lt;/p&gt;

&lt;p&gt;The base URL is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;https://openrouter.ai/api/v1/chat/completions
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the Gemma 4 model IDs on OpenRouter are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Model ID&lt;/th&gt;
&lt;th&gt;Free Tier?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 E2B&lt;/td&gt;
&lt;td&gt;&lt;code&gt;google/gemma-4-e2b-it&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 E4B&lt;/td&gt;
&lt;td&gt;&lt;code&gt;google/gemma-4-e4b-it&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 26B A4B&lt;/td&gt;
&lt;td&gt;&lt;code&gt;google/gemma-4-26b-a4b-it&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅ (&lt;code&gt;:free&lt;/code&gt; suffix)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 31B&lt;/td&gt;
&lt;td&gt;&lt;code&gt;google/gemma-4-31b-it&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅ (&lt;code&gt;:free&lt;/code&gt; suffix)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To use the free tier of the 26B model for example, use &lt;code&gt;google/gemma-4-26b-a4b-it:free&lt;/code&gt; as the model string.&lt;/p&gt;




&lt;h3&gt;
  
  
  Switching Models Without Rewriting Your Code
&lt;/h3&gt;

&lt;p&gt;This is the quiet superpower of going through OpenRouter. Because the API schema is identical across all models, switching from the free 31B to the 26B MoE (which is faster and cheaper in production) is literally one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// During development&lt;/span&gt;
&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;google/gemma-4-31b-it:free&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;// In production (faster, cheaper MoE)&lt;/span&gt;
&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;google/gemma-4-26b-a4b-it:free&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No refactoring. No new SDK. Just change the string. This is why I recommend OpenRouter for any serious project, you can prototype on free-tier models and graduate to the right production model without touching your integration code.&lt;/p&gt;




&lt;h3&gt;
  
  
  Your First API Call
&lt;/h3&gt;

&lt;p&gt;Let's start simple with a raw &lt;code&gt;fetch&lt;/code&gt; call in JavaScript, no SDKs needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://openrouter.ai/api/v1/chat/completions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPENROUTER_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;google/gemma-4-26b-a4b-it:free&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;What's the capital of Nigeria?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Three things to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your API key goes in the &lt;code&gt;Authorization&lt;/code&gt; header as a Bearer token&lt;/li&gt;
&lt;li&gt;The response shape mirrors OpenAI, your answer lives at &lt;code&gt;data.choices[0].message.content&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;You're billed (or rate-limited on free tier) per token, so &lt;code&gt;max_tokens&lt;/code&gt; is worth setting explicitly&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;I was getting this error&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;google/gemma-4-26b-a4b-it:free is temporarily rate-limited upstream.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The free tier of Gemma 4 on OpenRouter is being hammered by too many users right now and Google AI Studio is throttling it. When you use the free tier on OpenRouter &lt;code&gt;(google/gemma-4-26b-a4b-it:free)&lt;/code&gt;, you are sharing a pool of rate limits with every other developer in the world using that same free model. So when too many people hit it at once, Google throttles it and you get that &lt;code&gt;429&lt;/code&gt; error. But you already have a Google AI Studio API key sitting in your &lt;code&gt;.env.local&lt;/code&gt; doing nothing right now. OpenRouter lets you connect your own Google AI Studio key so that your requests go through your personal quota instead of the shared free pool.&lt;/p&gt;

&lt;p&gt;You have two options:&lt;/p&gt;

&lt;p&gt;Option 1 — Add your Google AI Studio key to OpenRouter&lt;/p&gt;

&lt;p&gt;Option 2 — Add retry logic to your API route&lt;/p&gt;

&lt;p&gt;I went with Option 1 which is adding my Google AI Studio key to OpenRouter integrations&lt;/p&gt;

&lt;p&gt;Go to: &lt;code&gt;openrouter.ai/settings/integrations&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Find Google AI Studio&lt;/li&gt;
&lt;li&gt;Paste your Google AI Studio API key there&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives you your own dedicated rate limit instead of sharing the free pool with everyone&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm8ghwn94mnwxihpsmxkw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm8ghwn94mnwxihpsmxkw.png" alt="OpenRouter integration" width="799" height="326"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  The System Prompt; The Secret Weapon for My Project
&lt;/h3&gt;

&lt;p&gt;Here's where Gemma 4 genuinely impressed me. Gemma 4 officially supports the &lt;code&gt;system&lt;/code&gt; role natively; a big deal for an open model, since earlier generations required hacky workarounds. However, in my actual project I used a slightly different pattern that I found worked even better for persona-heavy apps: the &lt;strong&gt;user/assistant injection pattern&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of passing the persona as a &lt;code&gt;system&lt;/code&gt; message, I injected it as the first &lt;code&gt;user&lt;/code&gt; message and followed it with a primed &lt;code&gt;assistant&lt;/code&gt; response where the model explicitly "agrees" to become the character before the real conversation even starts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formattedMessages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// persona instructions injected here&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;assistant&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// the model "confirms" the persona before the conversation begins&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Understood. I am &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;personName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. I will only speak about my own life, work, and experiences. Ask me anything about who I am.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// then the real conversation history follows&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The subtle advantage of this approach over a plain &lt;code&gt;system&lt;/code&gt; message: the model has explicitly &lt;em&gt;agreed&lt;/em&gt; to the role as part of the conversation context. That in-context confirmation produces noticeably stronger character lock-in across long conversations. The model is far less likely to slip out of persona when it has already "committed" to it in an early assistant turn.&lt;/p&gt;

&lt;p&gt;The results were honestly uncanny. The model held the persona across multiple turns, matched the celebrity's tone and vocabulary, and even pushed back on out-of-character questions in a way that felt natural. The 256K context window meant I could carry a long conversation without the model ever "forgetting" who it was supposed to be.&lt;/p&gt;




&lt;h3&gt;
  
  
  Enabling Reasoning Mode
&lt;/h3&gt;

&lt;p&gt;One of Gemma 4's standout features is its configurable thinking mode, you can ask the model to reason through a problem before giving its final answer. OpenRouter exposes this through a &lt;code&gt;reasoning&lt;/code&gt; parameter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;google/gemma-4-26b-a4b-it&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;reasoning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// turn on chain-of-thought&lt;/span&gt;
&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When enabled, the response includes a &lt;code&gt;reasoning_details&lt;/code&gt; array showing the model's internal thought process before the final answer. I didn't use this for the celebrity chat (you don't want Morgan Freeman visibly deliberating), but it's incredibly useful for any app where you want to show &lt;em&gt;why&lt;/em&gt; the model said something for example; tutoring apps, code explainers, or decision-support tools.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One tip from the OpenRouter docs: if you're building an agentic tool or integrating with something like Claude Code or Cline, &lt;strong&gt;turn reasoning off&lt;/strong&gt;. It adds latency and those tools are optimized for fast back-and-forth, not deep deliberation.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Thanks for Reading !
&lt;/h3&gt;

</description>
      <category>devchallenge</category>
      <category>gemmachallenge</category>
      <category>gemma</category>
    </item>
    <item>
      <title>My Buddy and I</title>
      <dc:creator>Ifeanyi Chima</dc:creator>
      <pubDate>Wed, 13 May 2026 16:33:04 +0000</pubDate>
      <link>https://dev.to/ifeanyichima/my-buddy-and-i-5i5</link>
      <guid>https://dev.to/ifeanyichima/my-buddy-and-i-5i5</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-gemma-2026-05-06"&gt;Gemma 4 Challenge: Build with Gemma 4&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I chose this project because as a kid (since age: 17) I have always had this vivid dreams of speaking with Elon Musk, he is kind of like my Idol and I would cherish the opportunity to have a conversation with him, until that day comes, this is what we would manage. When I came across this hackathon to test the lates model built by Google (Gemma 4) it was a no-brainer for me. I want the opportunity to understand the memory and context management of this model. I decided not to limit it to Elon Musk and included as much celebrities as possible. I want to give my friends the opportunity to test and use it. &lt;/p&gt;

&lt;p&gt;Buddy is a web application that lets you have real, in-character conversations with iconic public figures such as athletes, billionaires, historical leaders, artists, and innovators. You pick a person, and the AI steps into their shoes completely. It speaks the way they speak, stays grounded in their real life and career, and refuses to break character.&lt;/p&gt;

&lt;p&gt;Although, with this project I am not solving a problem like climate change or malaria cure, The problem Buddy solves is simple. Most AI chatbots are general-purpose. You can ask them anything, and they will answer as a neutral assistant. That is useful, but it is not immersive. Buddy flips that entirely. The AI is not an assistant anymore, it is a person. Ask Lionel Messi about his childhood in Rosario. Ask Steve Jobs why he believed design was everything. You get answers that feel like a real conversation, not a Wikipedia summary read back to you.&lt;/p&gt;

&lt;p&gt;The experience Buddy creates is curiosity without friction. It is the feeling of sitting across from someone remarkable and just... asking them things.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Link: &lt;a href="https://ifeanyi-celeb-chat.netlify.app" rel="noopener noreferrer"&gt;https://ifeanyi-celeb-chat.netlify.app&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/MasterIfeanyi/google-gemma4-hackathon" rel="noopener noreferrer"&gt;https://github.com/MasterIfeanyi/google-gemma4-hackathon&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I used Gemma 4
&lt;/h2&gt;

&lt;p&gt;Buddy runs on Gemma 4 26B, accessed via OpenRouter using the &lt;code&gt;google/gemma-4-26b-a4b-it:free model&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The choice of Gemma 4 over the smaller variants came down to one core requirement; persona consistency. Keeping an AI locked inside a specific person's personality across a full multi-turn conversation is genuinely hard. The model needs to remember who it is, match the person's tone and vocabulary, deflect off-topic questions naturally without sounding robotic, and stay grounded in real publicly known facts all at the same time. The smaller E2B and E4B variants are impressive for their size, but they tend to drift out of character more easily under pressure, especially when a user deliberately tries to break the persona with trick questions or unrelated topics.&lt;/p&gt;

&lt;p&gt;The 26B model holds the character much more firmly. When you ask Messi a question in Spanish, it does not just translate, it responds with the rhythm and warmth of how Messi actually speaks. When you push Steve Jobs on a topic he would find irrelevant, it redirects with the kind of sharp confidence Jobs was known for. That level of nuance needs a bigger model.&lt;/p&gt;

&lt;p&gt;One specific technical challenge worth mentioning is Gemma 4 does not support a native system role in its message format. To get around this, Buddy injects the personality prompt as a user / assistant turn pair at the very start of every request, effectively tricking the model into treating the persona as established context before the real conversation begins. The 26B model responds to this pattern reliably and stays locked in. Smaller models were noticeably less consistent with the same approach.&lt;/p&gt;

&lt;p&gt;What &lt;strong&gt;Gemma 4&lt;/strong&gt; ultimately unlocked for Buddy was believability. Because the model is large enough to hold complex persona context across many turns, we never had to simplify the experience to make it work. We did not have to limit conversation length, restrict the types of questions users could ask, or hardcode fallback responses. The model handled edge cases we never explicitly planned for. When a user asked Messi to speak Spanish, &lt;strong&gt;Gemma 4&lt;/strong&gt; switched languages naturally and stayed in character at the same time, responding with the warmth and rhythm of how Messi actually communicates, without any instruction from us to do so. When users tried to break the persona with off-topic questions, the model deflected in the voice of the person, not with a generic AI refusal. That level of emergent, in-character behaviour is what a smaller model cannot reliably give you and it is what makes Buddy feel like a conversation rather than a demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thank you.
&lt;/h2&gt;

</description>
      <category>devchallenge</category>
      <category>gemmachallenge</category>
      <category>gemma</category>
    </item>
    <item>
      <title>Google Next '26 - My thoughts</title>
      <dc:creator>Ifeanyi Chima</dc:creator>
      <pubDate>Tue, 28 Apr 2026 08:01:47 +0000</pubDate>
      <link>https://dev.to/ifeanyichima/google-next-26-my-thoughts-5dj3</link>
      <guid>https://dev.to/ifeanyichima/google-next-26-my-thoughts-5dj3</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-cloud-next-2026-04-22"&gt;Google Cloud NEXT Writing Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I have been eyeing to participate in a DEV.TO hackathon for a while now and when I saw this come across my timeline, I jumped up excitedly. I want the badge of honor. The big focus for the Google Cloud NEXT event is on what Google is debuting in the A.I. race especially agents. The future of developing on the cloud is exciting !! &lt;/p&gt;

&lt;h2&gt;
  
  
  Keynote &amp;amp; Demos
&lt;/h2&gt;

&lt;p&gt;In this article, I’ll break down the Developer Keynote because that stood out most to me and why.&lt;/p&gt;

&lt;p&gt;I tuned in for the &lt;a href="https://www.youtube.com/watch?v=A01DQ8_xy7Q" rel="noopener noreferrer"&gt;Developer Keynote (April, 23)&lt;/a&gt; which discussed building a marathon planning agent. A Gemini Enterprise Agent planned an entire Las Vegas marathon experience, coordinating hotels, routes, schedules, and logistics using multiple connected agents. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Demo 1 (Mofi Rahman) - Building agents with Agent Platform&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We can design an agent in agent designer. When building an agent you need three things: Instructions; to help the agent understand its role as the marathon planner. Skills; help the agent progressively understand what is available to the agent to complete the work. such as using Google maps, handling Geo-spatial data. Map skills can make your agent an expert at using Google maps. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recap&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Give the agent a prompt&lt;/li&gt;
&lt;li&gt;Agent loads the skills&lt;/li&gt;
&lt;li&gt;Agent executes the tools to find viable routes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What this means for developers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For developers, this means they can build specialized AI agents by composing three core components: instructions, skills, and tools, rather than writing complex logic from scratch. The agent designer platform handles how the agent loads skills, processes prompts, and decides which tools to call, so developers can focus on defining what the agent should do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Demo 2 (Ivan Nardini &amp;amp; Casey West) - Creating multi-agent systems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We have a planner agent that needs to talk to 2 other agents: the simulator agent and the evaluator agent. The planner agent creates potential routes for the race. Evaluator sub-agent will judge routes based on specific criteria that we pick. Simulator agent works with the planner to get approved routes, run them and show results. How can we facilitate communication between all agents, we need two components; A2A protocol and agent registry. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A2A Protocol:&lt;/strong&gt; Google introduced an open-source protocol called A2A protocol. This protocol eliminates API code to connect agents. Agents can just share a card that represents the capabilities of that agent. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Agent Registry:&lt;/strong&gt; This is a directory where agents get registered and it resolves every agent identity and maps their specific skillset across the agent network.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;What this means for developers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For developers, this means they can build multi-agent systems where agents communicate and collaborate without writing custom API integration code. The A2A protocol lets agents share capability cards with each other, while the agent registry acts as a directory that tracks every agent's identity and skillset across the network, so the planner, simulator, and evaluator agents can find and talk to each other automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Demo 3 (Lucia Subatin &amp;amp; Jack Wotherspoon) - Enhancing agents with memory&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When using gemini to do my job in the office, I usually copy all the files one-by-one and shove it into one prompt, in a bid to give gemini as much context as possible. For our planner agent we improve the context by giving it the ability to manage sessions, add memory and access other data sources. In this demo, we saw how we can use a skill to make the agent an expert at using alloyDB and vector functions. Basically what this Demo was showing is that by managing context efficiently and adding memory to our agent you can see the simulator agent adjust its route based on the memory and data it now had. The memory aspect comes from how the agent uses alloyDB together with vector functions to store and recall context from previous sessions, so it (alloyDB) behaves like a memory bank. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this means for developers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For developers, this means they can replace the manual process of dumping files into a prompt by giving agents structured memory and database access instead. By connecting the agent to tools like AlloyDB and vector functions, the agent automatically manages its own context across sessions, so it can make smarter, more informed decisions without the developer having to feed it everything upfront.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Demo 4 (Megan O'keefe) - Debugging agents at scale&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We all use &lt;code&gt;console.log("here")&lt;/code&gt; to debug a point in our codebase, well in artificial intelligence, we can not do that but, Google provides tools for us to debug; Agent observability and Cloud assist investigation agent. Using these tools we will find and investigate the root cause of the error and then we will deploy a proactive fix. In this demo, we had an error and we were suggested a fix by the agent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error:&lt;/strong&gt; The simulator agent is failing to call the gemini model API due to a request error. Something is wrong that the payloads were sending to the model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloud assist prompt:&lt;/strong&gt; Resume the Gemini cloud assist investigation about the simulator agent. What is wrong with agent.py&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; The agent is suggesting a fix that we add a token threshold parameter to our event compaction config so that we are periodically compressing context more often with each invocation. This is because we have a 1 million context token limit.&lt;/p&gt;

&lt;p&gt;From this demo, I learned that Google cloud provides a full observability suite for agents - cloud assist, coding agent. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this means for developers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For developers, this means they have a proper debugging suite for AI agents rather than guessing what went wrong. Google Cloud's observability tools let developers investigate agent failures, understand why an agent misbehaved, and get suggested fixes, so in this case the tool spotted that the simulator agent was hitting Gemini's 1 million token context limit and recommended compressing context more frequently to stay within that boundary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Google Cloud NEXT made one thing clear: building AI agents is becoming a first class developer experience. You no longer have to stitch together APIs, manually manage context, or guess why your agent broke. Google is giving developers a full stack for agent development, from designing and connecting agents, to giving them memory, to debugging them when things go wrong. The A2A protocol, agent registry, AlloyDB memory, and Cloud observability tools all point in the same direction: agents are becoming software components that developers can build, scale, and maintain just like any other part of their stack. The marathon planner was just a demo but the underlying tools are real and they are ready to be used.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>cloudnextchallenge</category>
      <category>googlecloud</category>
      <category>ai</category>
    </item>
    <item>
      <title>Breaking web apps</title>
      <dc:creator>Ifeanyi Chima</dc:creator>
      <pubDate>Thu, 23 Apr 2026 21:41:59 +0000</pubDate>
      <link>https://dev.to/ifeanyichima/breaking-web-apps-26bl</link>
      <guid>https://dev.to/ifeanyichima/breaking-web-apps-26bl</guid>
      <description>&lt;h3&gt;
  
  
  Introduction
&lt;/h3&gt;

&lt;p&gt;THOR (The Hand Book of Open-source Repositories) is a website that provides world-class guidance on open-source contributions. This is very near and dear to my heart. Ever since I was 18 years old in 2019, I watched my friend get selected for G-soc. I struggled to understand how to go about it and I was always wishing I had a medium that would provide a very clear understanding of how to go about open-source. This buggled my mind for 7 years, until I finally sat down to build a website to help other people in the future that would want to understand open-source and how to go about it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Project setup
&lt;/h3&gt;

&lt;p&gt;I already had Nodejs installed on my computer, which is really all you need to use &lt;code&gt;playwright&lt;/code&gt; and &lt;code&gt;passmark&lt;/code&gt;. We were provided API keys through the mail, keep the API KEY safe inside your env (add .env to .gitignore)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;OPENROUTER_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;sk&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;or&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;v1&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;your&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;actual&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;here&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In three easy steps, you can setup your project.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; You run this command. It will create a new folder called &lt;code&gt;my-hackathon-tests&lt;/code&gt; with everything Playwright needs:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;npm&lt;/span&gt; &lt;span class="nx"&gt;init&lt;/span&gt; &lt;span class="nx"&gt;playwright&lt;/span&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;latest&lt;/span&gt; &lt;span class="nx"&gt;my&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;hackathon&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;tests&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Answer the prompts.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Move into your project folder
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;cd&lt;/span&gt; &lt;span class="nx"&gt;my&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;hackathon&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;tests&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt; Install passmark and dotenv
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;npm&lt;/span&gt; &lt;span class="nx"&gt;install&lt;/span&gt; &lt;span class="nx"&gt;passmark&lt;/span&gt; &lt;span class="nx"&gt;dotenv&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is a look at my folder structure, I have three test files in my project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MY-HACKATHON-TESTS/
├── .sixth/
├── node_modules/
├── test-results/
├── tests/
│   ├── 01-homepage.spec.js
│   ├── 02-guest-flow.spec.js
│   └── 03-navbar.spec.js
├── .env
├── .gitignore
├── package-lock.json
├── package.json
└── playwright.config.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Testing strategy
&lt;/h3&gt;

&lt;p&gt;Writing tests in passmark is as easy as ABC literally, you write your descriptions in plain english sentences and the AI tries to understand it and do it for you. Although, the shortcoming in passmark is that the AI takes screenshots of your website and makes it's assertions which can burn through credits / tokens.&lt;/p&gt;

&lt;p&gt;In my website, I had a flow that was repetitive, essentially, the user lands on the homepage and clicks a button to be taken to the dashboard, I did not want to keep repeating myself writing descriptions and assertions for that flow, so I created a helper function called &lt;code&gt;navigateAsGuest&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Now, here is where the shortcomings of passmark comes in because it takes screenshots of your website, repeating this flow of the user landing on the homepage and clicking a button can burn through credit / token very fast, so I landed on a split-mechanism approach, where I used &lt;code&gt;playwright&lt;/code&gt; for navigation steps and only used &lt;code&gt;passmark&lt;/code&gt; to make assertions. This was done because &lt;code&gt;playwright&lt;/code&gt; has direct access to the DOM and can do DOM manipulations which saves on compute credit.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I Tested
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Homepage test:&lt;/strong&gt; This test was done in a a build-driven development testing strategy because I am the main / lead developer of the website, I know what will be seen on the landing page (a logo, a "continue as guest" button, a large banner image) so I tested for the presence of these using a mixture of playwright and passmark, but I will not advice anyone to do it this way because in tech, we should follow a test-driven development style.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 01-homepage.spec.js&lt;/span&gt;
&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Homepage loads with key elements&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// give it 2 minutes total&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://ifeanyi-thor.netlify.app/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForSelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text=THOR&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// wait for specific text, not networkidle&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runSteps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;userFlow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Verify homepage elements&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;assertions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;assertion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The heading 'THOR' is visible on the page&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;assertion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The subtitle 'The Handbook of Open-source Repositories.' is visible on the page&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;assertion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;There is a hammer/logo icon image visible on the page&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;assertion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;There is a large illustrated image of an astronaut cat visible on the left side&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Guest flow test:&lt;/strong&gt; This is a tricky test because you have to test for edge cases and user pattern is weird and hard to imagine, but here we are testing for when a user enters the website without authenticating with GitHub, we check that they are not able to use the following features: leave a comment, the markdown playground, but they can still use the sidebar to navigate throughout the documentation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 02-guest-flow.spec.js&lt;/span&gt;
&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Sidebar link 'introduction' loads the correct page&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;navigateAsGuest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;introduction&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForLoadState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;networkidle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// wait for content&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runSteps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;userFlow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Verify introduction page content&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;assertions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;assertion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The main content area shows the Introduction page content&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Nav bar test:&lt;/strong&gt; Here, I checked if the navigation links worked by correctly taking a user to the page they are meant to go to. The dark mode toggle button Oh my Gosh, this stressed me the most because I was trying to test that the dark / light mode toggle button works. So I ended up having to directly manipulate the DOM in the test instead of fighting the button.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 03-navbar.spec.js&lt;/span&gt;
&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Playground nav link shows Authentication Required modal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;navigateAsGuest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;link&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Playground&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForSelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text=Authentication Required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runSteps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;userFlow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Verify auth modal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;assertions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;assertion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A modal is visible on the page&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;assertion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The modal contains the heading 'Authentication Required'&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;assertion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The modal contains the text 'Please sign in with GitHub to use this feature'&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;assertion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A 'Sign in with GitHub' button is visible inside the modal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We have three test files, with a single command we can run them&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;npx&lt;/span&gt; &lt;span class="nx"&gt;playwright&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What I learned
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Run tests sequentially&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Without this, Playwright runs tests in parallel by default which caused our tests to interfere with each other.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// playwright.config.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;workers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Use passmark only for assertions&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// wrong — costs credits just to click a link&lt;/span&gt;
&lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Click the Docs link&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt;

&lt;span class="c1"&gt;// right — let Playwright click, let Passmark only assess&lt;/span&gt;
&lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Direct DOM manipulation when the UI fights you&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// when clicking the actual button is unreliable&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data-theme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;theme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;waitForLoadState("networkidle")&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// waits for ALL network requests to finish&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForLoadState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;networkidle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Results
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
## Test Results

All 12 tests passed successfully across 3 test files in 3.1 minutes.

**12 passed — 0 failed — Total time: 3.1 minutes**
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;p&gt;Github: &lt;a href="https://github.com/MasterIfeanyi/thor-passmark-tests" rel="noopener noreferrer"&gt;https://github.com/MasterIfeanyi/thor-passmark-tests&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;THOR: &lt;a href="https://ifeanyi-thor.netlify.app" rel="noopener noreferrer"&gt;https://ifeanyi-thor.netlify.app&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;I have written unit tests before this hackathon but it was a long time ago back in 2022 and this is the first time since 2022 that I am writing unit tests again. Oh, this really reminded me of how much I have missed it. This was the first time I saw that I can create multiple test files and they would all still run in parallel or sequentially. Seven years ago I watched my friend get into GSoC and had no roadmap to follow. I built THOR so nobody else has to feel that way. This hackathon pushed me to do something I hadn't done since 2022, write tests and it reminded me that shipping a product is only half the job. The other half is proving it works. Passmark made that second half genuinely enjoyable. If you have built something you care about, test it. Future you will be grateful.&lt;/p&gt;

</description>
      <category>breakingappshackathon</category>
      <category>ai</category>
      <category>programming</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
