<?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>Forem: Eric Elikplim Sunu</title>
    <description>The latest articles on Forem by Eric Elikplim Sunu (@bugslayer).</description>
    <link>https://forem.com/bugslayer</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F606786%2F7a8d3ac9-b9da-42d9-96ba-7345c9631e79.png</url>
      <title>Forem: Eric Elikplim Sunu</title>
      <link>https://forem.com/bugslayer</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/bugslayer"/>
    <language>en</language>
    <item>
      <title>From Kotlin to BrightScript: How I Ported a Modern Android TV App to Roku</title>
      <dc:creator>Eric Elikplim Sunu</dc:creator>
      <pubDate>Fri, 06 Feb 2026 14:05:17 +0000</pubDate>
      <link>https://forem.com/bugslayer/from-kotlin-to-brightscript-how-i-ported-a-modern-android-tv-app-to-roku-2o50</link>
      <guid>https://forem.com/bugslayer/from-kotlin-to-brightscript-how-i-ported-a-modern-android-tv-app-to-roku-2o50</guid>
      <description>&lt;p&gt;We were having our daily standup when my boss dropped a suggestion that would &lt;strong&gt;define my next three months&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;"We need to tap more into the US market," he said. "We should really build this on Roku."&lt;/p&gt;

&lt;p&gt;We built &lt;strong&gt;LiveSpaces&lt;/strong&gt;, a digital signage solution that turns screens into remotely controlled billboards. We already had a polished, modern Android TV app built with &lt;strong&gt;Kotlin&lt;/strong&gt; and &lt;strong&gt;Jetpack Compose&lt;/strong&gt;. It was reactive, declarative, and beautiful.&lt;/p&gt;

&lt;p&gt;Then came the kicker. &lt;em&gt;"How hard can it be?"&lt;/em&gt; I thought to myself as the meeting wrapped up. &lt;em&gt;"Roku holds roughly 50% of the connected TV market in the United States. If we’re targeting the waiting room or the hotel lobby in the West, we can't ignore the purple box. I’ll probably just find a cross-platform library, wrap our existing logic, and ship it."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I was dead wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;First Contact: No IDE, Just a Zip File&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I went to download the SDK, expecting an installer like Android Studio or Xcode. Instead, the documentation just said:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"To get started, download the Hello World sample app and unzip it..."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That was it. No heavy environment. Just a folder structure that looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;components/&lt;/strong&gt;: The XML files that define the layout (SceneGraph).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;source/&lt;/strong&gt;: The BrightScript code (the logic).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;images/&lt;/strong&gt;: Assets and splash screens.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;manifest&lt;/strong&gt;: The app configuration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It felt raw. To write code, I couldn't use a dedicated Roku IDE because one doesn't really exist. I fired up &lt;strong&gt;VS Code&lt;/strong&gt; and installed the &lt;strong&gt;BrightScript Extension&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The docs pitched it as a "suite of tools," and to be fair, the community extension for VS Code is a lifesaver: it gives you debugging, code formatting, and a telnet log. But coming from the rich, auto-completing embrace of Android Studio, I felt like I had been handed a text editor and a prayer&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Stack: A Primer for Web &amp;amp; Android Devs&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Before diving into the bugs, you need to understand what "Native Roku Development" actually looks like. It relies on two pillars: &lt;strong&gt;BrightScript&lt;/strong&gt; and &lt;strong&gt;SceneGraph&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you come from the &lt;strong&gt;Web World&lt;/strong&gt;, think of &lt;strong&gt;SceneGraph&lt;/strong&gt; as the HTML DOM. It is a tree of nodes defined in XML. &lt;strong&gt;BrightScript&lt;/strong&gt; is the JavaScript, it’s the scripting language used to manipulate those nodes. But unlike React or Vue, there is no Virtual DOM or data binding engine. You are manually selecting nodes and updating their properties, closer to the days of jQuery.&lt;/p&gt;

&lt;p&gt;If you come from Android, think of &lt;strong&gt;SceneGraph&lt;/strong&gt; as the old res/layout XML system, but strictly enforced. There is no Jetpack Compose here. BrightScript is your Java/Kotlin, but with syntax that looks like Visual Basic. It’s case-insensitive, uses &lt;code&gt;End Sub&lt;/code&gt; and &lt;code&gt;End If&lt;/code&gt;, and has no concept of generic types.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Paradigm Shift: Reactive vs. The Node Tree&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The hardest part of the migration was the shift in mental model. In Android, I live in a world of Coroutines, Flow, and StateFlow. If I want to update the UI, I update the state, and the UI reacts.&lt;/p&gt;

&lt;p&gt;Roku uses &lt;strong&gt;SceneGraph&lt;/strong&gt;. It’s a node-based architecture. But here is the catch: &lt;strong&gt;Threading is manual and strict.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Concurrency: Coroutines vs. Task Nodes&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;In Android, fetching data is a simple suspend function.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Android (Kotlin):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Modern and clean&lt;/span&gt;
&lt;span class="n"&gt;viewModelScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchPlaylist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;_uiState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Roku, you can't just run a background thread. You have to create a &lt;strong&gt;Task Node&lt;/strong&gt; (literally an XML file representing a thread), spin it up, pass it data, and then have your main thread &lt;em&gt;observe&lt;/em&gt; a field on that task to see when it changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Roku (BrightScript):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight brightscript"&gt;&lt;code&gt;&lt;span class="c1"&gt;' 1. Create the task object&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;contentTask&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;CreateObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"roSGNode"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ContentTask"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c1"&gt;' 2. Set the observer (callback)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;contentTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observeField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"response"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"onContentResponse"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c1"&gt;' 3. Trigger the task&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;contentTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;control&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RUN"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;2. The Missing Operators&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;I caught myself constantly trying to use Kotlin conveniences. &lt;br&gt;
I’d type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight brightscript"&gt;&lt;code&gt;&lt;span class="kr"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;posterUri&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thumbnail_url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;??&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pkg:/images/placeholder.png"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;BrightScript: &lt;em&gt;"We don't do that here."&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.amazonaws.com%2Fuploads%2Farticles%2F5tf719w6rwd6bhiock8s.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.amazonaws.com%2Fuploads%2Farticles%2F5tf719w6rwd6bhiock8s.jpg" alt="We don't do that here" width="742" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I had to write helper functions just to check if a string was valid without crashing the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Trenches: 3 Bugs That Almost Broke Me&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Developing for digital signage is different from developing a streaming app like Netflix. Netflix expects you to watch for 2 hours and turn it off. Digital signage runs &lt;strong&gt;24/7&lt;/strong&gt;. It must never sleep, never crash, and never complain.&lt;/p&gt;

&lt;p&gt;Here are the specific technical hurdles I hit, and the "MacGyver" workarounds I used to fix them.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Bug #1: The "Double JSON" Parsing Error&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Issue&lt;/strong&gt;: One of the data streams we consumed had a serialization inconsistency. Specifically, JSON data sometimes arrived as native JSON objects, and other times as Strings containing JSON.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario A:&lt;/strong&gt; API returns "&lt;code&gt;{ 'id': 1 }&lt;/code&gt;" (String)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario B&lt;/strong&gt;: API returns &lt;code&gt;{ 'id': 1 }&lt;/code&gt; (Object)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Roku Crash&lt;/strong&gt;: Roku's ParseJson() is strict. If you pass it an object that is already parsed, or try to access properties on a string as if it were an object, the app crashes immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt; I implemented "Defensive Parsing" logic that checks the type at runtime.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight brightscript"&gt;&lt;code&gt;&lt;span class="c1"&gt;' The "Double Parse" Workaround&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serverData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"roString"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="c1"&gt;' It is a string, so we must parse it manually&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;serverData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;ParseJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serverData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serverData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"roAssociativeArray"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="c1"&gt;' It is already an object, do nothing&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Bug #2: The Battle Against the Screensaver&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Issue:&lt;/strong&gt; Roku is aggressive about preventing screen burn-in. If no user input is detected for 10 minutes, the screensaver kicks in. For a digital signage app, this is a death sentence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Complication&lt;/strong&gt;: While we advise our users to maximize their screensaver timeout settings, many Roku devices generally do not offer a "Never" option in the consumer menu. The OS will eventually force a sleep mode if no media is playing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Failed Attempt:&lt;/strong&gt; I tried setting &lt;code&gt;screensaver_mode=disabled&lt;/code&gt; in the manifest. While documented, this flag is often ignored by the OS or flagged by the Store Certification team as a policy violation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution:&lt;/strong&gt; I found a legendary hack deep in the developer forums. &lt;strong&gt;The 1x1 Pixel Ghost Video.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Roku will not trigger the screensaver if a video is playing. So, I created a component that plays a video that is invisible to the human eye but visible to the OS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- MainScene.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;Video&lt;/span&gt; 
    &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"keepAwakeVideo"&lt;/span&gt; 
    &lt;span class="na"&gt;uri=&lt;/span&gt;&lt;span class="s"&gt;"pkg:/images/silent_1sec_black.mp4"&lt;/span&gt; 
    &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; 
    &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; 
    &lt;span class="na"&gt;translation=&lt;/span&gt;&lt;span class="s"&gt;"[-10, -10]"&lt;/span&gt; 
    &lt;span class="na"&gt;opacity=&lt;/span&gt;&lt;span class="s"&gt;"0.01"&lt;/span&gt; 
    &lt;span class="na"&gt;loop=&lt;/span&gt;&lt;span class="s"&gt;"true"&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;&lt;em&gt;Logic:&lt;/em&gt; It’s 1 pixel wide, 99% transparent, positioned off-screen, and loops forever.&lt;br&gt;&lt;br&gt;
&lt;em&gt;Result:&lt;/em&gt; The Roku thinks, "The user is watching a movie," and keeps the screen awake 24/7.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;Bug #3: No WebViews Allowed&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Issue:&lt;/strong&gt; Our Android app relies heavily on WebView components to render &lt;strong&gt;dynamic web dashboards&lt;/strong&gt;, embedded presentation slides, and third-party widgets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Reality:&lt;/strong&gt; Roku &lt;strong&gt;does not support WebViews.&lt;/strong&gt; Period. There is no HTML rendering engine available to developers in the SDK. You cannot simply "embed" a website.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Workaround:&lt;/strong&gt; We had to implement a simple fallback system.&lt;br&gt;&lt;br&gt;
If the content type is "Web Page," the Roku app automatically generates a QR Code using an external API (api.qrserver.com).&lt;br&gt;&lt;br&gt;
The UI displays a message: &lt;em&gt;"Web content not natively supported on Roku. Scan to view on mobile."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It wasn't the seamless experience we had on Android, but it saved the feature from being cut entirely.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;UI Limitations: The Font Struggle&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Polishing the UI made me miss Jetpack Compose dearly.&lt;/p&gt;

&lt;p&gt;In Compose, if I want a text header to be 24sp, I just write &lt;code&gt;fontSize = 24.sp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On Roku, you technically &lt;em&gt;can&lt;/em&gt; use custom fonts, but the implementation is incredibly verbose. According to the docs, you have to define a &lt;code&gt;Font&lt;/code&gt; node, set the URI to a &lt;code&gt;.ttf&lt;/code&gt; file, and assign it to the label.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- The XML way --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;Label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Font&lt;/span&gt; &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"font"&lt;/span&gt; &lt;span class="na"&gt;uri=&lt;/span&gt;&lt;span class="s"&gt;"pkg:/fonts/my_font.ttf"&lt;/span&gt; &lt;span class="na"&gt;size=&lt;/span&gt;&lt;span class="s"&gt;"24"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Label&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But what if you just want to use the System font, but slightly bigger?&lt;br&gt;&lt;br&gt;
Roku provides presets like MediumBoldSystemFont or LargeSystemFont. But if Large is too small and ExtraLarge is too big, you are stuck.&lt;/p&gt;

&lt;p&gt;To get our branding right without importing custom TTF files for every single variation, I ended up using a programmatic override in the BrightScript logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight brightscript"&gt;&lt;code&gt;&lt;span class="c1"&gt;' The BrightScript way: Manually instantiating nodes&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;CreateObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"roSGNode"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Label"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;CreateObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"roSGNode"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Font"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"font:MediumBoldSystemFont"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;' Start with a preset&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;58&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;' Override the size manually&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works, but it’s 5 lines of code for what takes 1 line in Compose.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Verdict: Android vs. Roku&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;After three months of wrestling with the emulator and finally testing on a real TV, I created this mental map for any Android dev attempting this migration:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Android TV (Kotlin)&lt;/th&gt;
&lt;th&gt;Roku (BrightScript)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Threading&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Coroutines (Easy)&lt;/td&gt;
&lt;td&gt;Task Nodes (Manual, XML-based)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;State&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;StateFlow / LiveData&lt;/td&gt;
&lt;td&gt;observeField (Callback hell)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTML&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;WebView (Full support)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Unsupported&lt;/strong&gt; (QR Code fallback)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fonts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;.ttf / Dynamic Sizing&lt;/td&gt;
&lt;td&gt;Verbose XML setup or Manual overrides&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Robust Garbage Collection&lt;/td&gt;
&lt;td&gt;Strict ref-counting (Watch for mem leaks!!)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Background&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;WorkManager&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;None&lt;/strong&gt; (App freezes on exit)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;My Advice to My Past Self&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;If I could travel back in time back to November, I would tell myself:&lt;/p&gt;

&lt;p&gt;"Don't fight the platform. It is not Android."&lt;/p&gt;

&lt;p&gt;In the beginning, I tried to force Android architecture patterns (MVVM, Repositories) into BrightScript, and it caused nothing but friction. Once I accepted &lt;strong&gt;SceneGraph&lt;/strong&gt; for what it was: a hierarchical, event-driven state machine, the code started to flow.&lt;/p&gt;

&lt;p&gt;It’s like learning a language from a different era. It requires patience, but there is a strange satisfaction in mastering its strict rules. Plus, seeing our app running flawlessly on a $25 device in a busy hotel lobby? That makes the struggle worth it.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>android</category>
      <category>kotlin</category>
      <category>roku</category>
    </item>
    <item>
      <title>Building a Comprehensive E2E Test Suite with Playwright: Lessons from 100+ Test Cases</title>
      <dc:creator>Eric Elikplim Sunu</dc:creator>
      <pubDate>Mon, 24 Nov 2025 23:51:43 +0000</pubDate>
      <link>https://forem.com/bugslayer/building-a-comprehensive-e2e-test-suite-with-playwright-lessons-from-100-test-cases-171k</link>
      <guid>https://forem.com/bugslayer/building-a-comprehensive-e2e-test-suite-with-playwright-lessons-from-100-test-cases-171k</guid>
      <description>&lt;h2&gt;
  
  
  The Journey
&lt;/h2&gt;

&lt;p&gt;While developing the platform for &lt;strong&gt;LiveSpaces&lt;/strong&gt;, I identified a critical gap in our delivery workflow. As the application grew, our reliance on manual verification and our existing CI/CD checks proved insufficient. We needed a way to streamline release cycles without sacrificing quality.&lt;/p&gt;

&lt;p&gt;I took the initiative to build a comprehensive End-to-End (E2E) test suite using &lt;strong&gt;&lt;a href="https://playwright.dev/" rel="noopener noreferrer"&gt;Playwright&lt;/a&gt;&lt;/strong&gt;, but I underestimated the complexity. It wasn't just about clicking buttons; it was about handling authentication flows, managing device states, and dealing with third-party integrations securely and reliably.&lt;/p&gt;

&lt;p&gt;From authentication flows to device management, every feature presented unique challenges. Here is a retrospective on the architecture I built to solve these bottlenecks, the decisions I made, and the lessons I learned.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;This post focuses on &lt;strong&gt;advanced patterns and architectural decisions&lt;/strong&gt; rather than basic installation. I assume you are familiar with the basics of Playwright or similar E2E tools (Cypress, Selenium).&lt;/p&gt;

&lt;p&gt;If you are brand new to Playwright, I highly recommend checking out the &lt;strong&gt;&lt;a href="https://playwright.dev/docs/intro" rel="noopener noreferrer"&gt;official documentation&lt;/a&gt;&lt;/strong&gt; first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick Context:&lt;/strong&gt;&lt;br&gt;
The examples below use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Framework:&lt;/strong&gt; Playwright (Node.js)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Language:&lt;/strong&gt; JavaScript/TypeScript&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Pattern:&lt;/strong&gt; Page Object Model (POM)&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;
&lt;h3&gt;
  
  
  100+ Test Cases Across 6 Major Areas
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Email Signup Flow&lt;/strong&gt; — The complete journey from registration to business setup.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;OAuth Integration&lt;/strong&gt; — Signup and signin flows with comprehensive mocking.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Device Pairing&lt;/strong&gt; — Device pairing logic using 6-character OTP codes.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Device Management&lt;/strong&gt; — Renaming, deleting, and managing device settings.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Account Type Selection&lt;/strong&gt; — Complex modal handling for onboarding flows.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Business Information Forms&lt;/strong&gt; — Industry, demographics, and capacity selection logic.&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  Key Challenges &amp;amp; Solutions
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Challenge 1: Email Verification in Tests
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Problem&lt;/strong&gt;: I could not access real email inboxes (Gmail/Outlook) reliably in automated tests. Using real inboxes made the tests slow, flaky, and prone to security blocks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution&lt;/strong&gt;: I implemented a smart mocking strategy.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Development&lt;/strong&gt;: I mocked the email verification endpoint to return a fixed success response.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Production/Integration&lt;/strong&gt;: I documented hooks for services like &lt;strong&gt;&lt;a href="https://mailosaur.com/" rel="noopener noreferrer"&gt;Mailosaur&lt;/a&gt;&lt;/strong&gt; for when real delivery testing is strictly necessary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Code Example:&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;// Mock the verification code endpoint to bypass email delivery&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;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**/api/verification/send&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;route&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;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fulfill&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;status&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="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;success&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="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;123456&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="c1"&gt;// Fixed mock code for testing&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;h3&gt;
  
  
  Challenge 2: OAuth Testing
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Problem&lt;/strong&gt;: Automating third-party providers (Google, Facebook) is often a violation of their Terms of Service. Furthermore, dealing with 2FA, captchas, or external popups leads to incredibly flaky tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution&lt;/strong&gt;: Comprehensive OAuth flow mocking. Instead of visiting the provider, I intercepted the callback that the provider &lt;em&gt;would&lt;/em&gt; have sent to our application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code Example:&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;// Mock the OAuth callback&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;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**/api/auth/oauth/callback**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;route&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="c1"&gt;// If the app is trying to exchange a token, give it a mock user&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;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;access_token&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;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fulfill&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;status&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="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;jwt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mock_jwt_token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test@example.com&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Challenge 3: Integration vs. Mocked Tests
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Problem&lt;/strong&gt;: I needed fast feedback loops during development (mocks) but actual verification before deployment (integration). I didn't want to maintain two separate test suites.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution&lt;/strong&gt;: &lt;strong&gt;Dual-mode test files.&lt;/strong&gt; I designed the tests to auto-detect the presence of an authentication token in the environment variables to decide whether to mock network requests or hit the real backend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Logic Flow:&lt;/strong&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.amazonaws.com%2Fuploads%2Farticles%2Fm4kcxd4jj0nc8kddeijc.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%2Fm4kcxd4jj0nc8kddeijc.png" alt="Dual-mode flowchart" width="800" height="1008"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code Example:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;AUTH_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AUTH_TOKEN&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;USE_REAL_BACKEND&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;AUTH_TOKEN&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;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;USE_REAL_BACKEND&lt;/span&gt;&lt;span class="p"&gt;)&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Using REAL backend&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// No mocks; allow requests to pass through&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Using MOCKED mode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Apply mocks defined in separate helper files&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;applyNetworkMocks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Challenge 4: Test Data Cleanup
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Problem&lt;/strong&gt;: Integration tests running against a real backend leave behind "zombie" data (e.g., created devices), causing subsequent tests to fail due to duplicate name errors or database bloat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Strict &lt;code&gt;try/finally&lt;/code&gt; blocks for guaranteed cleanup within the test.&lt;/li&gt;
&lt;li&gt;  &lt;em&gt;(Pro Tip: For larger suites, moving this logic into &lt;a href="https://playwright.dev/docs/test-fixtures" rel="noopener noreferrer"&gt;Playwright Fixtures&lt;/a&gt; is the preferred pattern).&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Code Example:&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="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;should create and manage device&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="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;testDeviceId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. Create test device via API&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;API_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/devices`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;AUTH_TOKEN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Test Device&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TEST123&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;testDeviceId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Test the UI flow interacting with this device...&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reload&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Test Device&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 3. Always cleanup, even if the assertion above fails&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;testDeviceId&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;request&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;API_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/devices/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;testDeviceId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;AUTH_TOKEN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="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;h3&gt;
  
  
  Challenge 5: Async Component Loading
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: Third-party components (maps, custom dropdowns, modals) load asynchronously. Tests were failing because they tried to interact with elements before they were truly interactive.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Use explicit synchronization points with &lt;code&gt;locator.waitFor()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Avoid &lt;code&gt;waitForTimeout&lt;/code&gt; (hard sleeps)&lt;/li&gt;
&lt;li&gt;Distinguish between &lt;em&gt;assertions&lt;/em&gt; (&lt;code&gt;expect&lt;/code&gt;) and &lt;em&gt;waiting&lt;/em&gt; (&lt;code&gt;waitFor&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: Reliable tests that handle real-world timing without flakiness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code Example:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mapInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;map-input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ❌ Bad Practice: Hard wait&lt;/span&gt;
&lt;span class="c1"&gt;// await page.waitForTimeout(1000);&lt;/span&gt;

&lt;span class="c1"&gt;// ❌ Common Mistake: Using assertion as a wait&lt;/span&gt;
&lt;span class="c1"&gt;// await expect(mapInput).toBeVisible(); // This is an assertion, not a strict wait step&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Best Practice: Explicit synchronization&lt;/span&gt;
&lt;span class="c1"&gt;// Wait specifically for the element to be ready before proceeding&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;mapInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitFor&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;visible&lt;/span&gt;&lt;span class="dl"&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="c1"&gt;// Now safe to interact&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;mapInput&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Update: Thanks to &lt;a class="mentioned-user" href="https://dev.to/dirtydiesel46"&gt;@dirtydiesel46&lt;/a&gt; in the comments for pointing out the distinction between &lt;code&gt;expect&lt;/code&gt; (assertion) and &lt;code&gt;waitFor&lt;/code&gt; (synchronization). The examples above have been updated to reflect this best practice.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 6: Portal-Based UI Components
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Problem&lt;/strong&gt;: Modern UI libraries (like &lt;strong&gt;&lt;a href="https://www.radix-ui.com/" rel="noopener noreferrer"&gt;Radix UI&lt;/a&gt;&lt;/strong&gt; or Headless UI) often render dropdowns and modals in "portals" at the bottom of the &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; tag, physically outside the component that triggered them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  I used specific ARIA roles.&lt;/li&gt;
&lt;li&gt;  I scoped locators correctly to escape the current container and search the document root.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Code Example:&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;// The dropdown trigger is in the main container&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Options&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="c1"&gt;// The menu itself is in a portal at the document root&lt;/span&gt;
&lt;span class="c1"&gt;// We wait for the menu specifically to be visible&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;menu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;menu&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;menu&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Click the item inside the menu&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;menu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;menuitem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Rename&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What Worked Really Well
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Page Object Model (POM) Pattern
&lt;/h3&gt;

&lt;p&gt;Centralizing selectors in reusable classes was the single highest-ROI decision I made. When the UI changed (and it did often), I updated one file instead of 20.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Architecture:&lt;/strong&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.amazonaws.com%2Fuploads%2Farticles%2Fadbr085bjc69v4b0ytt6.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%2Fadbr085bjc69v4b0ytt6.png" alt="Page Object Model architecture" width="800" height="168"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code Example:&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;// pages/AuthPage.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AuthPage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emailInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByPlaceholder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loginButton&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Login&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;async&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emailInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loginButton&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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Comprehensive Documentation
&lt;/h3&gt;

&lt;p&gt;Tests without documentation become legacy code the moment they are written. I created 15+ markdown files covering "How to run," "How to mock," and "How to debug." This dramatically reduced onboarding time for future developers and prevented "fear of the test suite."&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Error Case Testing
&lt;/h3&gt;

&lt;p&gt;Testing the "Happy Path" is easy. Testing the "Sad Path" is where value lies. I systematically mocked 404s, 500s, and network timeouts to ensure the UI handled errors gracefully (e.g., showing Toast notifications) rather than crashing.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Dual-Mode Testing
&lt;/h3&gt;

&lt;p&gt;By using environment variables to switch between Mocks and Real Backends, a single test file serves as both a &lt;strong&gt;Unit-like Test&lt;/strong&gt; (Mocked: fast, stable, deterministic) and an &lt;strong&gt;Integration Test&lt;/strong&gt; (Real: slower, verifies backend contracts).&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Learnings &amp;amp; Tips
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Mock External Services&lt;/strong&gt;: Never test Google's login page. Mock the OIDC response. You are testing &lt;em&gt;your&lt;/em&gt; app, not Google's uptime.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Prioritize User-Visible Locators&lt;/strong&gt;: Use &lt;code&gt;getByRole&lt;/code&gt; and &lt;code&gt;getByText&lt;/code&gt; over CSS classes (&lt;code&gt;.btn-primary&lt;/code&gt;). This mimics how users find elements and makes tests resilient to styling refactors.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Always Cleanup&lt;/strong&gt;: Test data pollution causes flaky tests. If you create it, delete it (preferably in a &lt;code&gt;finally&lt;/code&gt; block or a fixture).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Wait Properly&lt;/strong&gt;: Never use &lt;code&gt;waitForTimeout(5000)&lt;/code&gt;. If you find yourself doing this, you are likely missing a proper state assertion or an &lt;code&gt;await expect(...)&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Test Files&lt;/strong&gt;: 10+&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Test Cases&lt;/strong&gt;: 100+&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Lines of Code&lt;/strong&gt;: ~5,000+&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Coverage Areas&lt;/strong&gt;: 6 major features&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Confidence Level&lt;/strong&gt;: High&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;p&gt;If you want to dive deeper into the concepts mentioned here, I recommend these resources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;&lt;a href="https://playwright.dev/docs/pom" rel="noopener noreferrer"&gt;Playwright Page Object Models&lt;/a&gt;&lt;/strong&gt; — Official documentation on structuring tests.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;a href="https://martinfowler.com/bliki/PageObject.html" rel="noopener noreferrer"&gt;Martin Fowler on Page Objects&lt;/a&gt;&lt;/strong&gt; — The theory behind the pattern.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;a href="https://mailosaur.com/docs" rel="noopener noreferrer"&gt;Mailosaur Documentation&lt;/a&gt;&lt;/strong&gt; — For handling email in tests.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;a href="https://kentcdodds.com/blog/the-testing-trophy-and-testing-classifications" rel="noopener noreferrer"&gt;The Testing Trophy&lt;/a&gt;&lt;/strong&gt; — Kent C. Dodds' approach to balancing static, unit, integration, and E2E tests.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This post is based on real-world experience building a production E2E suite. All code examples are simplified for clarity.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>playwright</category>
      <category>devops</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
