Saturday, August 22, 2026

WinAppDriver Locators: Perils, Playbook, and AI Help

While the world has generally moved to mobile devices, desktop computers and applications still play a key role inside offices doing business. This is true for SMEs and enterprise alike. That's why the need for efficient test automation of all sorts of desktop applications, whether they work on Mac or Windows, has not gone away, it has just gone quiet. Nobody writes hype posts about desktop automation anymore, but somewhere behind almost every insurance company, bank, hospital, and manufacturing floor, a WPF, WinForms, or Win32 app is still running the actual business, and someone still has to test it.



For Windows apps specifically, WinAppDriver has been the default place teams start since Coded UI was retired. Worth saying up front, WinAppDriver itself has not seen much active development in a few years, releases have slowed and the open issue list is long, but it stays the practical starting point simply because there are not many real alternatives for driving native Windows UI. It is free, it is Microsoft's own tool, and it speaks the same WebDriver protocol Selenium and Appium already use, so C#, Java, and Python testers all feel at home with it fairly fast. What nobody tells you on day one is that the actual skill in WinAppDriver automation is not the driver setup, it is locators. Get those right and a suite runs for years. Get them wrong and you spend more time fixing "element not found" than writing new tests.

This post is about that exact skill: finding, choosing, and maintaining locators for WinAppDriver automation, plus a look at how AI and the Model Context Protocol (MCP) are starting to change how that discovery work gets done.

The Five Locator Strategies WinAppDriver Gives You

WinAppDriver, being a WebDriver-protocol server sitting on top of Windows UI Automation (UIA), exposes five ways to find an element:

  • Accessibility ID, matching the control's AutomationId. This is the one you want most of the time.
  • Name, matching the control's visible Name property.
  • Class Name, matching the control's ClassName (for example TextBlock or Button).
  • ID, matching RuntimeId, a value Windows assigns to a control at runtime.
  • XPath, which can match on any attribute at all.

That list looks flat on paper, but the strategies are not equally reliable, and the gap between them is where most WinAppDriver flakiness comes from.

Accessibility ID is the strategy to build your suite on. It is developer-set, stable across app restarts, and independent of language or window layout. If a screen has good AutomationIds, use them, full stop.

Name breaks the moment your app is localized, or even the moment a product manager tweaks copy from "Save" to "Save Changes." Fine as a fallback, risky as a primary strategy for anything beyond a quick script.

RuntimeId is a trap many people fall into once, and only once. It looks like a stable identifier because it is a specific-looking string, but it is assigned fresh by Windows each time the control is created, so it can, and often does, change between app restarts. Do not build locators on it.

XPath is the one to reach for last, and to use sparingly, for two reasons. First, an XPath query in WinAppDriver has to walk the full UIA tree with no indexing, which real-world reports on the WinAppDriver GitHub issue tracker describe as dramatically slower than a direct AccessibilityId or Name lookup, sometimes the difference between a near-instant find and a multi-second one. Second, an XPath tied to sibling position or index is exactly the kind of structural locator that breaks the moment a designer adds a new control to a panel. If you must use XPath, anchor it to a stable ancestor with a known AutomationId, and keep the path as short as you can. (There is a real exception to "use it sparingly," covered just below, if your project runs on Appium 2.0 or newer.)

A Real Gotcha: Appium 2.0+ Changes the Rules

Everything above assumes the classic setup: WinAppDriver.exe running as its own server, with your test project talking to it directly through a Selenium-style WebDriver client. A lot of what is out there still teaches exactly that.



If your project instead runs on Appium 2.0 or newer with the appium-windows-driver plugin, so an Appium server sits in front of WinAppDriver rather than your tests talking to it directly, watch out for a real, documented quirk: By.Id and By.Name locators can get translated into CSS selectors under the hood and simply fail to find anything. In that specific setup, XPath stops being a last resort and becomes the strategy that actually works.

I put together a full, working example of this exact Appium 5 plus WinAppDriver setup in C#, including the AppiumServiceBuilder startup pattern and a small EasyXPath helper class for building the XPath locators this setup needs, on GitHub: Appium5-WinAppDriver. Worth cloning and running side by side with whatever setup you are on, so you know which locator rules actually apply to your project before you build fifty tests on the wrong assumption.

Why WinAppDriver Locators Get Fragile in the First Place

A few realities make Windows desktop locators harder than the web locators most testers cut their teeth on, and they show up constantly in WinAppDriver suites specifically.

A lot of apps simply never set AutomationIds. Setting an AutomationId is a deliberate developer choice, and in older or legacy Win32, MFC, and even plenty of WPF codebases, nobody ever did it. You end up locating by Name, which is fine until localization or a copy change breaks half the suite in one release.

Custom-drawn controls can be invisible to UIA entirely. Canvas-based UI, some CAD and design tools, and heavily custom-rendered WPF controls can expose almost nothing to the accessibility tree WinAppDriver relies on. When that happens, none of the five strategies above help, and coordinate-based clicks become the only fallback, which is the most fragile option there is.

Coordinates and window layout drift. A locator anchored to screen position breaks the moment DPI scaling, resolution, or window size changes. This is common enough on shared CI machines that it deserves its own line: if your test farm runs at a different resolution than your dev machine, coordinate-based clicks are a ticking clock, not a strategy.

Timing is part of the locator problem, not a separate one. A control can exist in the UIA tree and still not be safe to interact with, disabled, off-screen, or sitting behind a modal dialog that has not finished rendering. WinAppDriver will happily hand you a reference to an element you cannot actually click yet.

A WinAppDriver Locator Playbook

Inspect before you write a single locator. WinAppDriver ships a bundled UI Recorder specifically for this, point it at your app and it shows you the AutomationId, Name, and ClassName for anything you click. The Windows SDK's Inspect.exe does the same job with a bit more detail on the full ancestor chain. Five minutes here saves an hour of trial and error later.



Centralize your locators in Page Objects. Whatever language you're driving WinAppDriver from, keep every screen's locators in one class or module, not copy-pasted string literals scattered across test methods. When an AutomationId changes, you want to fix it in one place, not grep your whole repo.

Search relative to a container, not the whole window. Finding a button by searching the entire application window is slower and more ambiguous than first finding the containing dialog or panel by its AutomationId, then searching within that. It is also safer if two screens in the same app happen to reuse a button labeled "OK."

Push for AutomationIds from the dev side. If a screen has none, that is not a testing problem to quietly route around forever, it is a product defect, the same category as a missing API endpoint. Teams that add AutomationIds in XAML or code from day one save their QA function months of pain over an app's life.

Treat your RuntimeId and coordinate-based locators as expiring debt. If you have to fall back to either while a screen lacks proper accessibility support, flag it, track it, and replace it once the real AutomationId lands. Do not let it quietly become permanent.

FlaUI as a WinAppDriver Sidekick

You do not need to choose between WinAppDriver and FlaUI, the open source .NET library built on Microsoft's UIA APIs. Plenty of teams run WinAppDriver as their actual test driver and lean on FlaUI, or a small standalone FlaUI script, purely as an inspection tool during locator discovery. Because FlaUI runs in-process rather than through a separate driver, it is quick to spin up a throwaway console app that walks a window's tree, prints every control's AutomationId, ClassName, and Name, and lets you confirm exactly what to plug into your WinAppDriver FindElementByAccessibilityId call, without touching Inspect.exe at all.

If you want to go deep on the WinAppDriver side specifically, from setup through real, running C# tests, that is exactly what my course, Windows Desktop Applications Test Automation in WinAppDriver, covers end to end. I also wrote a full breakdown of how WinAppDriver compares to the now-retired Coded UI if you want the background on why WinAppDriver became the default choice in the first place.

Waiting for Controls to Actually Show Up

A locator that finds the wrong moment in time is just as broken as one that finds the wrong element. Since WinAppDriver speaks the WebDriver protocol, the same explicit-wait pattern Selenium and Appium testers already know applies directly:

var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
var saveButton = wait.Until(d => d.FindElementByAccessibilityId("btnSave"));

That poll-until-true-or-timeout pattern, not a fixed sleep, is what keeps a suite fast on a quick CI machine and still reliable on a slow one. A few habits matter more than which exact wait class you reach for:

  • Wait on a condition, not a clock. "Wait until this element exists and is enabled" survives a loaded CI box. "Wait three seconds" does not.
  • For test steps that involve more than one flaky element, or that depend on a slow backend the app talks to, wrap the whole step in a retry policy with backoff rather than one fixed wait. Polly is the standard .NET library for this and composes cleanly around any WinAppDriver call.
  • If you are also using FlaUI for inspection or verification steps alongside WinAppDriver, its own Retry helper (Retry.WhileNull, Retry.WhileFalse, Retry.WhileException, each with a timeout and polling interval) is worth knowing, since it is purpose-built for exactly this kind of polling.
  • Wait for interactability, not just existence. WinAppDriver can hand you a reference to a control that is disabled or off-screen. Check the element's enabled state before you act on it, not just that the find call did not throw.

AI and MCP: Finding WinAppDriver Locators Faster

The most useful recent shift here is not a new locator syntax, it is tooling that lets an AI agent read the accessibility tree directly and reason about it, instead of guessing from a screenshot the way early "AI testing" tools did, and then hand you back exactly the AutomationId or path you plug into a WinAppDriver By call.

FlaUI-MCP, an open source MCP server built on FlaUI by Scott Hanselman, is a clean example. It exposes tools like windows_snapshot, which returns the live accessibility tree as structured element references, so an AI coding assistant can walk the real UIA tree, the same tree WinAppDriver itself queries under the hood, and hand back accurate AutomationIds, ClassNames, and ancestor paths for the controls you care about. Point it at your app during locator discovery, and it can shortcut a lot of manual Inspect.exe spelunking before you write the actual WinAppDriver test. Similar projects like windows-mcp-server extend this further with synthetic input and process control on top of the same accessibility-tree-first approach.

For scenarios no MCP server covers well yet, custom-written tree walker code is still a valid, sometimes necessary, fallback. Windows exposes System.Windows.Automation.TreeWalker directly in .NET, letting you write a small utility that recursively crawls a window's UIA tree and dumps every control's properties to JSON, then hand that JSON to an LLM with a prompt like "which of these elements is most likely the login button," and get back a suggested AutomationId or path to verify. It is a lighter-weight, roll-your-own version of what FlaUI-MCP does out of the box.

Two cautions matter more as this tooling gets more impressive. First, an AI suggesting a locator is not the same as an AI verifying one, always confirm a suggested AutomationId against a real inspector tool or an actual WinAppDriver test run before it lands in your suite; a language model can produce a plausible-looking but entirely wrong selector with total confidence. Second, an AI-suggested locator still belongs in your centralized Page Object, not pasted inline into a test method, or you have just automated your way into the same brittle mess faster.

Things You Must Never Do With WinAppDriver Automation

A short, hard-earned list:

  • Never rely on Thread.Sleep as your primary wait strategy. It is either too short and flaky, or too long and slow, never both correct and fast. Use WebDriverWait every time.
  • Never build a locator on RuntimeId. It looks stable and usually is not, it can change on every app restart.
  • Never automate purely by screen coordinates when a real locator is available. Coordinates break on the first DPI or resolution change, and they break silently, the click just lands on the wrong thing.
  • Never scatter the same locator string across dozens of test files. Centralize in Page Objects so a UI change costs one edit, not a search-and-replace across the whole suite.
  • Never trust visible Name text as your primary locator in an app that ships in more than one language. What is stable in English can vanish entirely in a localized build.
  • Never assume your suite can run against a locked or logged-off machine. WinAppDriver needs a real, interactive, unlocked session to drive UI at all. A CI runner that locks the screen on idle will fail your whole suite for reasons that have nothing to do with your app.
  • Never leave WinAppDriver.exe or the app under test running after a failed session. A crashed test that skips driver.Quit() leaves orphaned processes behind that steal focus and intercept clicks meant for the next run. Clean up in a finally block, every time, even on failure.
  • Never wrap a flaky locator in more retries without asking why it is flaky. Sometimes it is hiding a real race condition or a bug in the app itself. Blind retries just turn a real defect into an intermittent one nobody ever files.
  • Never skip explicit handling for modal dialogs. "Element not found" is, more often than not, a focus problem in disguise, a dialog stole input focus and your script kept looking at the window behind it.
  • Never treat a missing AutomationId as purely your problem to work around. It is a testability gap in the product. Raise it, do not just quietly build a fragile workaround and move on.

Wrapping Up

WinAppDriver gives you five ways to find an element, but only one of them, AccessibilityId, deserves to be your default. Everything else in a stable suite follows from that one fact: centralize locators so a UI change costs one edit, wait on conditions instead of clocks, and treat a missing AutomationId as a product defect rather than a personal problem to route around before a release.

What has genuinely changed recently is how fast the discovery part of that work goes. Tools like FlaUI-MCP put a real accessibility tree in front of an AI agent instead of a screenshot, which means the tedious part, figuring out what a control is actually called under the hood, is finally starting to get faster without getting less accurate, and the AutomationId it hands you drops straight into a WinAppDriver test.

If you want to see real, working code first, my Appium5-WinAppDriver repo on GitHub is free and shows the full Appium 5 plus WinAppDriver setup in C#, XPath helper class included.

If you want the full setup explained end to end, from installing WinAppDriver through writing real, running C# tests against a live desktop app, that is exactly what my course walks through.

Windows Desktop Applications Test Automation in WinAppDriver (code CBP2026AU) →

Friday, August 21, 2026

Turn Off Your Windows Display With One Line of PowerShell (2026 Update — No Compiler, No .exe Download Required)

A rewrite of a post I originally wrote back when the only way to do this was to compile a C++ .exe and host it on a file-sharing site. Times have changed — so has the method. (And if you're the kind of person who likes automating things on a computer just because you can, you might also like my Playwright TypeScript Automation Testing: E2E, API, AI, MCP course — automation is automation, whether it's a browser or a monitor.)



The Old Problem

Years ago I wanted a simple way to turn my laptop's screen off on demand — no such button exists on almost any laptop. Back then the fix was: write a tiny C++ program that calls SendMessage from User32.dll, compile it, and run the .exe. It worked, but it meant opening Visual Studio (or trusting a stranger's compiled binary from a file-sharing link) just to flip one bit.

Here's what that original C++ looked like:

#include <Windows.h>
int main(int argc, char* argv[]) {
    SendMessage(HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, (LPARAM) 2);
    // this turns the monitor off
    return 0;
}

One line of real logic, wrapped in a whole compile-and-download workflow just to run it.

The 2026 Way: Just Use PowerShell

Every Windows machine already ships with PowerShell, and PowerShell can call the exact same Win32 messaging API — this time via PostMessage rather than SendMessage (more on that swap below) — directly in user32.dll, with no compiler and no .exe to distribute or trust. That's the real update to this post: the "download an executable" step is obsolete.

Quick one-liner, run from an elevated or regular PowerShell/Terminal prompt:

(Add-Type '[DllImport("user32.dll")]public static extern int PostMessage(int h,int m,int w,int l);' -Name Win32 -PassThru)::PostMessage(0xffff,0x0112,0xF170,2)

That single line compiles a throwaway C# stub in memory (via Add-Type) and immediately calls it. 0xffff is HWND_BROADCAST, 0x0112 is WM_SYSCOMMAND, 0xF170 is SC_MONITORPOWER, and 2 means "power off."

If you want something reusable — a function you can drop in your PowerShell profile, alias, or save as a .ps1 — here's the tidier version:

Add-Type -TypeDefinition '
using System;
using System.Runtime.InteropServices;
namespace Utilities {
    public static class Display {
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        private static extern IntPtr PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
        public static void PowerOff() {
            PostMessage((IntPtr)0xffff, 0x0112, (IntPtr)0xF170, (IntPtr)2);
        }
    }
}'
[Utilities.Display]::PowerOff()

Save that as Turn-Off-Display.ps1, right-click → Run with PowerShell, or pin it (see below) — and your monitor goes dark, same as the old .exe, minus the download.

How does it come back on?

Same as before: Windows brings the display back automatically the moment you move the mouse or press a key — no code needed for that part. This is standard OS/driver behavior and holds true no matter which Win32 function turned the display off. If you do want to trigger it programmatically (say, from a script that also wakes the screen), the correct wParam values for SC_MONITORPOWER are worth getting right, since the original post glossed over this:

  • 2 — power off
  • 1 — low-power / standby state
  • -1 — fully on

So to force it back on rather than relying on mouse/keyboard input, call the same function with -1, not 1.

Why PostMessage, Not SendMessage

Worth being explicit about a change from the original post: the code above uses PostMessage, not SendMessage — even though the 2010 C++ version above (and most copies of it still floating around the internet) uses SendMessage. That's not a stylistic choice — it fixes a real reliability gotcha.

SendMessage is a blocking call. When you target HWND_BROADCAST, the calling thread has to wait for every single top-level window on the desktop to acknowledge the message before it returns. Microsoft's own driver documentation has a dedicated compiler warning about this exact pattern:

"This warning indicates that the application called SendMessage with the HWND_BROADCAST flag, which blocks the thread until all the windows to which this message was broadcast respond. However, if there is another window that is not responding, the current thread will also be blocked." — C28601, Microsoft Learn

Microsoft's own fix for that warning is: use PostMessage instead. It fires the message and returns immediately, rather than waiting on every window in the broadcast — so there's no risk of the call hanging because some unrelated background window is slow to respond. There are also real-world reports on Microsoft's own Q&A forums of SendMessage plus HWND_BROADCAST causing the monitor to flash, or turn straight back on again right after SC_MONITORPOWER fires, with nobody touching the mouse or keyboard. Switching to PostMessage sidesteps it.

To be clear on the actual wake-up behavior, since that's a separate question from which function you call: moving the mouse or pressing a key reliably brings the display back regardless of whether you used SendMessage or PostMessage — that part is standard OS/driver behavior, confirmed directly on Microsoft's own Q&A. PostMessage just makes the "turn it off cleanly" half of the trick behave reliably too, instead of occasionally flashing the display straight back on.

Why This Works (Still True in 2026)

The underlying Windows architecture hasn't changed: you never talk to an application directly. You send a message to the OS, and the OS routes it to whichever window (or, in this case, every top-level window, since we're broadcasting) is meant to handle it. Every Windows application runs a message loop that watches for these messages; if it doesn't recognize one, it just ignores it harmlessly.

SendMessage(HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, 2) is really saying: "to whoever's listening — here's a system command related to monitor power." The only listener that cares is the same OS subsystem that already dims your display after your power-plan idle timeout. Nothing else on your system reacts to it, no ports are opened, and nothing is written to disk or the registry.

What has changed is that PowerShell's Add-Type cmdlet gives you a first-class, built-in way to reach that same Win32 API surface — you're no longer limited to C++ or hand-rolled C# with a separate build step. Add-Type compiles inline C# on the fly using .NET, so any P/Invoke call you could make from a compiled app, you can make from a script. It's this same "script something a compiled app used to do" instinct that pulled me toward test automation in the first place — if that clicks with you too, my Playwright course covers the same idea applied to browsers, APIs, and even AI/MCP-driven testing.

Since We're Here: What Else Can PowerShell Do to Your System?

Once you see that Add-Type unlocks any Win32 or WinRT API call from a script, "turn off the monitor" turns out to be one small example of a much bigger category. A few others worth knowing about in 2026:

Power and session control — sleep, hibernate, or lock the machine without touching the Start menu:

rundll32.exe powrprof.dll,SetSuspendState 0,1,0   # sleep
rundll32.exe user32.dll,LockWorkStation           # lock the session

Screen brightness, via WMI on laptops with supported drivers:

(Get-CimInstance -Namespace root/WMI -ClassName WmiMonitorBrightnessMethods).WmiSetBrightness(1, 50)

Process and service management — inspect or kill anything running, or control Windows services:

Get-Process | Sort-Object CPU -Descending | Select-Object -First 10
Stop-Process -Name "app" -Force
Get-Service | Where-Object Status -eq 'Running'

Finding and clearing duplicate files, by hashing rather than by filename:

Get-ChildItem -Recurse | Get-FileHash | Group-Object Hash | Where-Object Count -gt 1

Seeing (and removing) what Explorer hides, like the full startup program list or bundled Windows apps that don't have an uninstall button in Settings:

Get-CimInstance Win32_StartupCommand | Select-Object Name, Command, Location
Get-AppxPackage -AllUsers | Where-Object Name -like "*Bloatware*" | Remove-AppxPackage

Automating anything on a schedule, no separate Task Scheduler GUI needed:

Register-ScheduledTask -TaskName "NightlyBackup" -Trigger (New-ScheduledTaskTrigger -Daily -At 2am) -Action (New-ScheduledTaskAction -Execute "robocopy.exe" -Argument "C:\Data D:\Backup /MIR")

Managing other machines, not just the one in front of you — PowerShell remoting lets one script reach across a whole fleet:

Invoke-Command -ComputerName Server01,Server02 -ScriptBlock { Get-Service -Name spooler }

Talking to web APIs and cloud services directly, with JSON handled for you:

Invoke-RestMethod -Uri "https://api.example.com/status" -Method Get

And since PowerShell 7, it isn't even Windows-only anymore — pwsh runs the same language on macOS and Linux, so a chunk of this automation knowledge now travels with you across platforms, even though the Add-Type/user32.dll trick above is obviously Windows-specific. If scripting your OS into doing your bidding is fun for you, scripting a browser and an API into doing your bidding is the natural next step — that's most of what I teach in Playwright TypeScript Automation Testing: E2E, API, AI, MCP.

The Windows-Key Shortcut Trick Still Works Too

The old Windows 7 tip from the original post — pin something to the taskbar and launch it instantly with Win + [number], counting from the left — still applies today. Pin your Turn-Off-Display.ps1 shortcut (or a .lnk pointing at powershell.exe -File Turn-Off-Display.ps1) to the taskbar, note its position, and Win + 1 (or whichever slot it lands in) fires it instantly. No taskbar real estate wasted on a mystery .exe anymore — it's obviously "your" script if anyone ever looks at it.

Is This Actually Safe to Run?

Short answer: yes, but it's worth being precise about why, rather than just asserting it.

SC_MONITORPOWER isn't some obscure hack — it's the exact message Windows itself broadcasts every time your screen-saver or power-plan idle timer fires. Running it manually just triggers that same, already-trusted code path on demand. The call itself doesn't touch the network, doesn't read or write files, doesn't need admin rights, and doesn't persist anything — it's a single message asking the OS to change one power state, nothing more.

That said, "safe" isn't the same as "zero side effects in every situation." A few real edge cases worth knowing before you pin this to your taskbar:

  • Fullscreen apps and games: because the message is broadcast to every top-level window (HWND_BROADCAST), a handful of poorly-behaved fullscreen apps or older games have been known to minimize, stutter, or occasionally crash when they unexpectedly receive a WM_SYSCOMMAND. Well-written software just ignores it, same as it ignores the automatic idle-timeout version — but "well-written" isn't universal.
  • Multi-monitor setups: some GPU driver combinations don't reliably wake every display afterward — occasionally one monitor stays blank until you nudge the mouse harder, unplug/replug, or explicitly send the -1 ("fully on") message described above instead of waiting on Windows to do it.
  • Remote Desktop / RDP sessions: this message targets a local display session, so running it inside an RDP window doesn't behave the same way (there's no local monitor for it to power down) and isn't a meaningful use case.
  • Antivirus/EDR false positives: this is the one most worth flagging. Add-Type compiling C# on the fly is a completely legitimate .NET feature, but it's also a technique malware uses to slip code past static signature scanning. Strict corporate security tooling may quarantine the script or alert on it — not because it's doing anything harmful, but because the pattern looks the same either way from the outside. If you're on a managed corporate machine, expect friction here.

None of this means "don't run it" — it means: it's a narrow, well-documented, single-purpose API call, but it's still asking the whole desktop to listen for a moment, so test it outside of anything fullscreen first, and don't be alarmed if a locked-down work laptop's security software wants a second look at it.

A Couple More 2026 Caveats

  • Execution policy: depending on your system's PowerShell execution policy, running a .ps1 may need Set-ExecutionPolicy RemoteSigned (per user) or launching with -ExecutionPolicy Bypass. That's a deliberate safety rail, not a bug.
  • Just like the original post's promise about the .exe: this script opens no network ports, reads and writes nothing to disk beyond the script file itself, and touches nothing in the registry. It's one API call, nothing more.

Bottom Line

What used to require Visual Studio, a compiled .exe, and a file-hosting link now takes one line in a terminal window that's already installed on every Windows PC. The API call is the same one from 2010; the delivery mechanism — and now, the specific function used — is what finally caught up.

If you liked this kind of "make the computer do the boring thing" trick, that's basically the whole premise of test automation. My Udemy course walks through Playwright with TypeScript for end-to-end testing, API testing, AI-assisted testing, and MCP — from zero to a real automation suite.

Playwright TypeScript Automation Testing: E2E, API, AI, MCP →


Disclaimer: the code in this post is free, provided as-is, with no guarantees or warranties of any kind — express or implied — including any warranty of fitness for a particular purpose or of working identically across every Windows build, driver, or hardware configuration. You run it at your own risk; the author is not responsible for any damage, data loss, or other consequences arising from its use.

Wednesday, August 19, 2026

14 Painful Playwright Mistakes to Avoid


Someone on r/Playwright asked a good question: what painful mistakes should you avoid? Forty-plus engineers answered — people running Playwright against real production, not demos. I read the whole thread and pulled out the mistakes that came up again and again.

None of this is theory. It's scar tissue. Most of these are the difference between a suite that works on your laptop and one that survives CI, real traffic, and a login wall. Here they are, each with the fix the thread landed on.

01 Hard-coded waits everywhere

The most-cited mistake by a mile: waitForTimeout(3000) sprinkled through the suite to "make it stable." It passes locally because your machine is fast, then breaks in CI because the container is slower. You've traded a real fix for a timer that's wrong on both ends — too slow when things are fine, too fast when they aren't.

FIX → Trust Playwright's auto-waiting. Use web-first assertions like expect(locator).toBeVisible() and let the framework wait for actionability.

02 Brittle selectors

Targeting something like div.css-1a2b3c > span:nth-child(3) is asking for pain. That selector encodes your DOM's exact shape and a hashed class name — both change the moment a developer touches the component. Your test breaks, and it isn't even a real bug.

FIX → Use getByRole, getByText, and getByTestId. They describe intent, not structure, so they survive UI refactors.

03 Over-abstracting the Locator API

This one drew a crowd. Playwright's Locator API is already rich, but people wrap it in layers of helpers anyway — defining a locator, then an accessor method, then a method that calls the accessor. One commenter described building a beautiful page-object hierarchy in year one and spending year two deleting it. The worst offenders come from Selenium and bring fifteen years of habits to a framework that already does the work.

FIX → Default to Playwright's built-ins. Delete a wrapper before you add one — earn every layer of abstraction.

04 Trusting storageState naively

A sharp production story: relying on storageState while the app rotates tokens or triggers step-up auth mid-session. Tests pass in staging and fail live, because the real auth flow only kicks in against production. Anything behind a real login is exposed to this.

FIX → Keep saved auth ephemeral, not permanent. Add a cheap validity check — hit an API endpoint and expect a 200 — before the suite leans on it.

05

05 Running the full suite serially in CI

Someone ran their suite serially for nine months to "save resources." The result: 90-minute pipelines, and a 1-in-50 flake dragging average merge time to roughly 2.5 hours. Moving to parallel workers with proper isolation cut it to twelve minutes — and made flakes diagnosable on their own instead of as a cascade.

FIX → Run parallel workers with real test isolation from day one. Serial-to-save-money almost always costs more.

06 Building test data through the UI

Clicking through signup to create a user for every test is slow, flaky, and couples your setup to the very UI you're testing. When it breaks, the whole run cascades and you can't tell setup failures from real ones.

FIX → Pre-create users and state via API, not the UI. Every test gets its own clean data, and failures stay isolated.

07 Screenshot diffs full of noise

A first visual baseline that picked up a relative timestamp ("2 hours ago") and a chart that animated over 800ms. Every diff came back 100% noise, and the team learned to ignore visual failures — which defeats the point of having them.

FIX → Make the page deterministic first. Freeze the clock in beforeEach, set animation-duration: 0, and mask anything backed by a third-party widget.


08 Treating test code as second-class

The most underrated point in the thread: suites die from neglect more often than from any selector strategy. Tests that never get reviewed or refactored rot fast. A test goes red on a Friday, someone adds test.skip() with a comment nobody revisits, and after enough of those the suite is a vibes-only signal.

FIX → Review, own, and prune test code like production code. Discipline beats framework choice every time.

09 Shipping codegen output as-is

Record-and-playback is genuinely useful, but the generated code is a starting point, not a suite. Ship it raw and you inherit brittle selectors and hard waits by default — the exact things at the top of this list.

FIX → Treat codegen as a scaffold. Rewrite the selectors and structure before you commit it.

10 Ignoring production realities

Real traffic brings slower API responses, rate limits, and captcha or MFA challenges that never existed in lower environments. A suite tuned only against a quiet staging box falls over the first time it meets production conditions.

FIX → Simulate production-like load before release, monitor slow endpoints, and plan for the auth challenges you'll actually hit.

11 Wasting type safety in TypeScript

If you're on TypeScript but writing it like untyped JavaScript — any everywhere, no typed page objects, no typed fixtures — you've kept the compile step and thrown away the reason for it.

FIX → Type your fixtures, helpers, and data. Let the compiler catch the mistakes before the test run does.

12 Over-testing

Chasing a coverage number with low-value tests that don't map to real user flows. Each one is a maintenance liability forever, and together they slow the suite and bury the failures that matter.

FIX → Cover the flows that matter and skip the vanity ones. Fewer, meaningful tests beat a wall of green noise.

13

Blindly relying on AI-generated code

AI writes Playwright fast, and that's exactly the trap. The output looks plausible, so it lands without a second read — and quietly ships the brittle selectors and hard waits from the top of this list. When an agent generates your suite and nobody reviews it, you don't have tests, you have a pile of guesses that happened to pass once. The thread's take was blunt: the discipline that keeps production code clean matters more than whatever tool wrote it.

FIX → Treat AI output like a junior dev's pull request. Read every line, prune it, and make sure you understand it before it lands.

14

Not learning Playwright properly in the first place

You can absolutely piece this together from Reddit threads, docs, and trial-and-error in CI. It works — slowly, and usually after you've made most of the thirteen mistakes above yourself. The faster path is a structured one that covers the whole picture: end-to-end, API testing, and the AI and MCP side of modern automation, in the order that actually builds on itself.

FIX → Learn it in one structured path — my Playwright TypeScript Automation Testing: E2E, API, AI, MCP course.

The one that split the room

Page objects: discipline or dead weight?

The thread agreed on almost everything above. On one topic it went to war: the Page Object Model. Half the room called skipping POM the cardinal sin. The other half argued that fixating on OOP layers rots suites just as fast — factory functions are simpler, and heavy class hierarchies fight you when the app changes constantly.

The most interesting version of the argument tied it to AI: when an agent is generating your test code and your pages change weekly, deep abstraction becomes a liability, not an asset. There's no settled answer here — which is exactly why it's worth thinking through for your own suite instead of copying a template. Where do you land?

If there's a thread running through all twelve, it's this: the framework isn't your problem. Determinism, isolation, and treating tests like real code are. Get those right and most of this list never happens to you.

I go deep on the right way to do this — auto-waiting, resilient locators, API-driven setup, and CI — in my Playwright with TypeScript course, including the AI and MCP side of modern test automation. If this list was useful, the course is the structured version of it. Check it out here.

Friday, August 14, 2026

How to Install Claude Code (CLI + VS Code Extension) for Playwright MCP

Before You Start: Installing Claude Code

The lectures in this section use Claude Code to drive the Playwright MCP server. Before the next video, take five minutes to install it and sign in. Everything else in this section builds on these steps.

Read this first

Course URL playwright.testautomationtv.com

Claude Code requires a paid Anthropic account — a Pro, Max, Team, or Enterprise Claude subscription, or a Claude Console (API) account. The free Claude.ai plan does not include Claude Code access.

I am telling you this up front so it is not a surprise halfway through an install. If you would rather not subscribe, read the section "If you are using a different AI tool" at the bottom of this article — the Playwright MCP is an open protocol and it is not tied to Claude.

You already installed Node.js in Section 1. If you skipped that lecture, install the Node.js LTS version now — the npm install option below needs Node.js 22 or later.

Step 1 — Install the Claude Code CLI

Open a terminal and run the command for your operating system.

macOS, Linux, or WSL:

curl -fsSL https://claude.ai/install.sh | bash

Windows PowerShell:

irm https://claude.ai/install.ps1 | iex

If you are in PowerShell, your prompt starts with PS C:\. If it starts with C:\ without the PS, you are in the older Command Prompt — open PowerShell instead and run the command above.

If you prefer npm (any operating system, Node.js 22 or later):

npm install -g @anthropic-ai/claude-code

Do not put sudo in front of the npm command. It causes permission problems later.

Step 2 — Confirm the install worked

claude --version

You should see a version number printed back, something like 2.1.211 (Claude Code). If you see command not found, close the terminal, open a new one, and try again — the installer adds Claude Code to your PATH, and an already-open terminal will not have picked that up.

If it still fails, run this command, which prints a diagnostic report:

claude doctor

Step 3 — Sign in

From your project folder, start Claude Code:

claude

The first run opens your browser and asks you to authorise the account. Complete that, return to the terminal, and you are logged in. You only do this once per machine.

Step 4 — Install the VS Code extension

Claude Code VS Code Extension


You need VS Code 1.94.0 or higher.

  1. In Visual Studio Code, press Ctrl+Shift+X on Windows and Linux, or Cmd+Shift+X on Mac, to open the Extensions view.

  2. Search for Claude Code.

  3. Click Install on the extension published by Anthropic.

  4. If the extension does not appear afterwards, restart VS Code, or open the Command Palette and run Developer: Reload Window.

Open the panel by clicking the Claude Code icon in the Activity Bar on the left, or by clicking Claude Code in the status bar at the bottom right. Sign in when prompted — same account as Step 3.

An important point about the extension

The VS Code extension and the command-line tool are two separate things. The extension gives you the chat panel inside the editor. It does not give you the claude command in your terminal.

In the next lecture we register the Playwright MCP server by running a terminal command that starts with claude. That command comes from the CLI you installed in Step 1, so please do not skip Step 1 and install only the extension.

Your checklist before the next lecture

  • claude --version prints a version number

  • Running claude in a terminal opens an interactive session

  • The Claude Code panel opens inside VS Code and shows you as signed in

If all three are true, you are ready.

One thing that will save you time later

When you register an MCP server, an already-open Claude session will not always see it. If claude mcp list shows the server as connected but your assistant cannot reach it, close the session and start a new one. This comes up again later in the section, and it is almost always the explanation.

If you are using a different AI tool

The Model Context Protocol is an open standard, and Playwright's MCP server is not tied to Claude. Other agentic coding tools support MCP as well — some register a server with a single command like the one you will see in the next lecture, and others expect you to edit a configuration file directly.

The concepts in this section — what the MCP server does, how it reads the accessibility tree, how to turn an exploration into a real test file — apply the same way regardless of which assistant you use. The commands on screen are the Claude Code versions, so check your own tool's documentation for its equivalent registration step.

Official documentation

Installation steps change from time to time. If a command in this article does not work, the current instructions are always here:

See you in the next lecture.

Naeem Malik — Course URL playwright.testautomationtv.com

Saturday, August 8, 2026

Playwright + TypeScript, from Zero to AI-Powered E2E Testing

Playwright with Typescript Udemy Course Overview


I just launched a new Udemy course, "Playwright with TypeScript – Automation Testing, E2E, API & AI/MCP," and I wanted to write a bit about why I built it, what's inside, and where you can grab the code that goes along with every lecture.

🚀 Enroll in the course on Udemy  |  💻 Get the companion code on GitHub

Why Playwright, and why now?

Playwright has quietly become one of the most reliable tools for end-to-end testing — fast, resilient to flakiness, and genuinely pleasant to write tests in once you get past the first few locators. But most tutorials stop at "click a button, assert some text." Real test suites need to survive real applications: dialogs, iframes, drag-and-drop, shopping carts, custom fixtures, and CI pipelines that don't fall over every time the UI shifts a pixel.

This course is built around that gap. It starts from the fundamentals and works all the way up to using AI — specifically the Playwright MCP server and Claude Code — to generate and even self-heal failing tests inside GitHub Actions.

What you'll learn

  • Writing reliable E2E tests with Playwright + TypeScript, from locators to assertions
  • Handling real-world UI patterns: checkboxes, radios, drag-and-drop, dialogs, iframes
  • Building maintainable test suites with custom fixtures and the Page Object pattern
  • Testing a full shopping cart flow end-to-end
  • Debugging with traces, the HTML reporter, and soft assertions
  • Generating and healing tests with AI, using the Playwright MCP server and Claude Code
  • Wiring up a self-healing CI workflow so failing tests fix themselves via GitHub Actions

The companion repo

Every lecture has real, working code behind it, all in one place:

github.com/naeemakram/playwright-typescript-e2e-ai

tests/
  cart/            # Full shopping cart E2E test suite
  *.spec.ts        # Section-by-section lecture exercises
specs/             # AI-generated test plans (used with the Playwright MCP planner)
.github/workflows/ # CI, including an AI-powered self-healing test workflow
playwright.config.ts  # Base URL, projects, tracing, and reporter setup
.mcp.json          # Playwright MCP server config used for AI test generation

Commits roughly track the course section-by-section, so you can check out the history and watch each feature get built incrementally rather than just staring at a finished pile of code. Clone it, follow along, and — most importantly — break things and fix them. That's still the fastest way to actually learn a testing tool.

Getting started with the code

npm install
npx playwright install
npx playwright test

And once a run finishes, open the HTML report to see exactly what happened:

npx playwright show-report

Who this is for

If you've never written an automated test before, you'll be fine — the course starts at the very beginning. If you already know Playwright but haven't touched fixtures, Page Objects, or CI, there's a lot here for you too. And if you're curious how AI tooling like MCP and Claude Code fits into a real test workflow rather than just being a novelty, the later sections are built specifically for that.

Let's go

Enroll in the course on Udemy and pair it with the GitHub repo as you go. If you find it useful, a rating or review genuinely helps other learners find it — thank you in advance! ⭐⭐⭐⭐⭐