This guide covers using Playwriter to control visible browsers on Windows from Elixir running in WSL (Windows Subsystem for Linux).
Why WSL-Windows Integration?
When developing in WSL, you often want to:
- See the browser - Watch your automation in real-time for debugging
- Test visual behavior - Verify rendering, animations, and UI
- Debug interactively - Pause and inspect the browser state
- Demo to stakeholders - Show browser automation to non-technical users
The challenge: browsers launched from WSL run headless or in a virtual display, and WSL2's Hyper-V networking blocks most connection attempts to Windows. Playwriter solves this with the :windows mode.
Architecture
┌────────────────────────────────────────────────────────────┐
│ WSL 2 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Your Elixir Application │ │
│ │ │ │
│ │ Playwriter.fetch_html("https://example.com", │ │
│ │ mode: :windows) │ │
│ └───────────────────────┬─────────────────────────────┘ │
│ │ │
│ │ Erlang Port │
│ │ (stdin/stdout via PowerShell) │
└──────────────────────────┼─────────────────────────────────┘
│
┌──────────────────────────┼─────────────────────────────────┐
│ ▼ Windows │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ PowerShell + Node.js │ │
│ │ (runs Playwright directly) │ │
│ └───────────────────────┬─────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Browser Window │ │
│ │ (Visible on Windows Desktop) │ │
│ └─────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘The :windows mode bypasses WSL2's Hyper-V firewall entirely by communicating via PowerShell stdin/stdout instead of network sockets.
Setup
1. Install Node.js on Windows
The Playwright driver runs on Node.js. Install Node.js on Windows (not WSL):
# Using winget
winget install OpenJS.NodeJS.LTS
# Or download from https://nodejs.org2. Install Playwright on Windows
Run the setup script from WSL:
# One-time setup - installs Playwright in Windows temp directory
powershell.exe -ExecutionPolicy Bypass -File priv/scripts/start_server.ps1 -Install
This creates %TEMP%\playwriter-server on Windows with Playwright installed.
3. Use from Elixir
# Simple fetch
{:ok, html} = Playwriter.fetch_html("https://example.com", mode: :windows)
# Screenshot
{:ok, png} = Playwriter.screenshot("https://example.com", mode: :windows)
File.write!("screenshot.png", png)
# Full browser control
Playwriter.with_browser([mode: :windows], fn ctx ->
:ok = Playwriter.goto(ctx, "https://example.com")
:ok = Playwriter.fill(ctx, "input[name=q]", "search term")
:ok = Playwriter.click(ctx, "button[type=submit]")
{:ok, html} = Playwriter.content(ctx)
html
end)Development Workflow
Interactive Development
Start an IEx session and explore:
iex -S mix
# See the browser while you work
{:ok, result} = Playwriter.with_browser([mode: :windows], fn ctx ->
:ok = Playwriter.goto(ctx, "https://example.com")
# Pause here - inspect the browser on Windows
IO.gets("Press Enter to continue...")
{:ok, html} = Playwriter.content(ctx)
html
end)Debugging Tips
- Use
:windowsmode - See what's happening in real-time - Add pauses - Use
Process.sleep/1orIO.gets/1to slow down - Take screenshots - Capture state at key points
Playwriter.with_browser([mode: :windows], fn ctx ->
:ok = Playwriter.goto(ctx, "https://example.com")
# Screenshot before clicking
{:ok, before} = Playwriter.screenshot(ctx)
File.write!("before.png", before)
:ok = Playwriter.click(ctx, "button.submit")
Process.sleep(1000) # Wait for navigation
# Screenshot after
{:ok, after_shot} = Playwriter.screenshot(ctx)
File.write!("after.png", after_shot)
end)How It Works
The :windows mode uses the Playwriter.Transport.WindowsCmd transport which:
- Writes a Node.js script to
%TEMP%\playwriter-server\transport.json Windows - Launches PowerShell with that script via Erlang Ports
- Communicates via JSON messages over stdin/stdout
- The Node.js script controls Playwright/Chromium on Windows
This approach completely bypasses networking, avoiding all WSL2 Hyper-V firewall issues.
Troubleshooting
"Playwright not installed" or "Cannot find module 'playwright'"
Run the setup script with -Install:
powershell.exe -ExecutionPolicy Bypass -File priv/scripts/start_server.ps1 -Install
Or manually install on Windows:
cd $env:TEMP\playwriter-server
npm install playwright
npx playwright install chromium"Timeout waiting for transport to start"
Check that:
- Node.js is installed on Windows and in PATH
- Playwright is installed in
%TEMP%\playwriter-server
Test manually:
# From WSL
powershell.exe -Command "cd $env:TEMP\playwriter-server; node -e 'console.log(require(\"playwright\").chromium)'"
"Browser closes immediately"
Make sure you're not letting the session close. Use with_browser to keep the browser open:
Playwriter.with_browser([mode: :windows], fn ctx ->
:ok = Playwriter.goto(ctx, "https://example.com")
Process.sleep(5000) # Keep browser open for 5 seconds
:ok
end)"PowerShell execution policy error"
Always run with -ExecutionPolicy Bypass:
powershell.exe -ExecutionPolicy Bypass -File script.ps1
Checking Windows User Detection
The transport detects your Windows username from /mnt/c/Users/. If it picks the wrong user:
# Check what user is detected
File.ls!("/mnt/c/Users")
|> Enum.reject(&(&1 in ["Public", "Default", "Default User", "All Users", "desktop.ini"]))Comparison: Windows Mode vs Remote Mode
| Feature | :windows mode | :remote mode |
|---|---|---|
| Setup required | Just npm install | Run a server |
| Network issues | None (stdin/stdout) | WSL2 firewall blocks it |
| Performance | Good | Slightly faster |
| WSL2 compatible | Yes | No (blocked by Hyper-V) |
| Recommended for WSL | Yes | No |
Use :windows mode for WSL-to-Windows browser automation. The :remote mode exists for other distributed scenarios but doesn't work reliably from WSL2.
Next Steps
- Architecture Overview - Understand the full system design
- Function Reference - Complete function documentation
- Examples - More code examples