Functions/GenXdev.Webbrowser.Playwright/Open-PlayWrightBrowser.ps1

<##############################################################################
Part of PowerShell module : GenXdev.Webbrowser.Playwright
Original cmdlet filename : Open-PlayWrightBrowser.ps1
Original author : René Vaessen / GenXdev
Version : 3.28.2026
################################################################################
Copyright (c) 2026 René Vaessen / GenXdev

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
################################################################################>


###############################################################################
<#
.SYNOPSIS
Starts a Playwright-managed browser with a persistent user profile.

.DESCRIPTION
Launches a browser instance powered by Playwright. The BrowserType
parameter determines which browser engine and launch mode to use:

- ChromeNormal / EdgeNormal: Launches your OS-installed Chrome or Edge
  via Playwright's Channel API, avoiding the "controlled by automation"
  infobar and anti-bot detection problems.
- ChromiumNormal: Auto-detects Chrome or Edge (whichever is installed
  and set as the system default) and uses the Channel API. Falls back
  to the bundled Playwright Chromium if neither is installed.
- ChromiumPlaywright: Always uses Playwright's bundled Chromium binary
  (never the OS-installed browser).
- FirefoxPlaywright: Uses Playwright's bundled Firefox binary. Channel
  API is not available for Firefox in the Playwright .NET bindings.
- WebKitPlaywright: Uses Playwright's bundled WebKit binary.

Anti-detection measures applied for Chromium-based browsers:
- Disables blink automation flags (removes infobar)
- Sets viewport to null and starts maximized for native window sizing
- Uses 'no-preference' color scheme to avoid white background
- Sets standard browser args (no-first-run, no-default-browser-check)

Persistent profiles per BrowserType are stored under GenXdev AppData.
The launched browser and its context/page are stored in
$Global:GenXdevPlaywright for use by other cmdlets.

.PARAMETER BrowserType
The browser to launch: ChromeNormal, EdgeNormal, ChromiumNormal
(auto-detects Chrome or Edge), ChromiumPlaywright, FirefoxPlaywright,
or WebKitPlaywright.

.PARAMETER Headless
Run the browser in headless mode (no visible window).

.PARAMETER Proxy
Proxy server URL (e.g. "http://proxy:8080").

.PARAMETER AcceptLang
Set the browser's Accept-Language HTTP header.

.PARAMETER Width
Initial viewport width in pixels. Also passed to Set-WindowPosition for
OS window sizing.

.PARAMETER Height
Initial viewport height in pixels. Also passed to Set-WindowPosition for
OS window sizing.

.PARAMETER Force
Close any existing Playwright browser and start a new one.

.PARAMETER AutoConsent
Automatically consent to this installation type and set persistent flag...

.PARAMETER AutoConsentAllPackages
Automatically consent to third-party software installation (Playwright
browsers).

.PARAMETER Monitor
Monitor selection: 0=primary, 1+=specific monitor, -1=current,
-2=secondary.

.PARAMETER X
Window horizontal position in pixels.

.PARAMETER Y
Window vertical position in pixels.

.PARAMETER Left
Places window on left half of screen.

.PARAMETER Right
Places window on right half of screen.

.PARAMETER Top
Places window on top half of screen.

.PARAMETER Bottom
Places window on bottom half of screen.

.PARAMETER Centered
Centers window on screen.

.PARAMETER Fullscreen
Maximizes window to fill entire screen.

.PARAMETER NoBorders
Removes window borders and title bar for a cleaner appearance.

.PARAMETER RestoreFocus
Returns focus to PowerShell window after positioning.

.PARAMETER SideBySide
Positions browser either fullscreen on a different monitor than
PowerShell, or side by side with PowerShell on the same monitor.

.PARAMETER FocusWindow
Focus the window after positioning.

.PARAMETER SetForeground
Set the window to foreground after positioning.

.PARAMETER Minimize
Minimizes the window after positioning.

.PARAMETER Maximize
Maximize the window after positioning.

.PARAMETER SetRestored
Restore the window to normal state after positioning.

.PARAMETER KeysToSend
Keystrokes to send to the window after positioning, see documentation
for cmdlet GenXdev\Send-Key.

.PARAMETER SendKeyEscape
Escape control characters and modifiers when sending keys.

.PARAMETER SendKeyHoldKeyboardFocus
Hold keyboard focus on target window when sending keys.

.PARAMETER SendKeyUseShiftEnter
Use Shift+Enter instead of Enter when sending keys.

.PARAMETER SendKeyDelayMilliSeconds
Delay between different input strings in milliseconds when sending
keys.

.PARAMETER PassThru
Returns window helper object for further manipulation.

.PARAMETER SessionOnly
Use alternative settings stored in session for AI preferences.

.PARAMETER ClearSession
Clear alternative settings stored in session for AI preferences.

.PARAMETER SkipSession
Store settings only in persistent preferences without affecting
session.

.PARAMETER OnlyOutputCoords
Only output the calculated coordinates and size without actually
positioning the window. Returns a hashtable with Left, Top, Width,
and Height properties representing global (multi-monitor)
coordinates.

.EXAMPLE
Open-PlayWrightBrowser
Launches your OS-installed Chrome or Edge (auto-detected) with a
persistent profile and anti-detection measures.

.EXAMPLE
Open-PlayWrightBrowser -BrowserType EdgeNormal
Launches Microsoft Edge via Playwright's Channel API.

.EXAMPLE
Open-PlayWrightBrowser -BrowserType ChromiumPlaywright
Uses the bundled Playwright Chromium binary.

.EXAMPLE
Open-PlayWrightBrowser -BrowserType FirefoxPlaywright -Headless
Launches the bundled Firefox in headless mode.

.EXAMPLE
Open-PlayWrightBrowser -Width 1280 -Height 720 -Force
Restarts the browser with a 1280x720 viewport.
#>

function Open-PlayWrightBrowser {

    [CmdletBinding(SupportsShouldProcess = $true)]
    [OutputType([Hashtable])]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')]
    [Alias('spb')]

    param(
        ########################################################################
        [Parameter(
            Mandatory = $false,
            Position = 0,
            HelpMessage = ('The browser to launch: Chrome, Edge, Chromium ' +
                '(auto-detects Chrome or Edge), Firefox, or WebKit')
        )]
        [ValidateSet('ChromeNormal', 'EdgeNormal', 'ChromiumNormal',
            'ChromiumPlaywright', 'FirefoxPlaywright', 'WebKitPlaywright')]
        [string] $BrowserType = 'ChromiumNormal',

        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Run the browser without a visible window'
        )]
        [Alias('hl')]
        [switch] $Headless,
        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Proxy server URL (e.g. http://proxy:8080)'
        )]
        [string] $Proxy,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Set the browser Accept-Language HTTP header'
        )]
        [Alias('lang', 'locale')]
        [string] $AcceptLang,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Initial viewport width in pixels'
        )]
        [Alias('w')]
        [ValidateRange(320, 7680)]
        [int] $Width = -1,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Initial viewport height in pixels'
        )]
        [Alias('h')]
        [ValidateRange(240, 4320)]
        [int] $Height = -1,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Close existing browser and start fresh'
        )]
        [Alias('f')]
        [switch] $Force,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Automatically consent to this installation type and set persistent flag..'
        )]
        [switch] $AutoConsent,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Automatically consent to third-party software ' +
            'installation (Playwright browsers)'
        )]
        [Alias('Consent')]
        [switch] $AutoConsentAllPackages,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('The monitor to use, 0 = default, -1 is discard, ' +
                '-2 = Configured secondary monitor, defaults to ' +
                "`$Global:DefaultSecondaryMonitor or 2 if not found")
        )]
        [Alias('m', 'mon')]
        [int] $Monitor = -1,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'The initial X position of the webbrowser window'
        )]
        [int] $X = -999999,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'The initial Y position of the webbrowser window'
        )]
        [int] $Y = -999999,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Place browser window on the left side of the screen'
        )]
        [switch] $Left,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Place browser window on the right side of the screen'
        )]
        [switch] $Right,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Place browser window on the top side of the screen'
        )]
        [switch] $Top,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Place browser window on the bottom side of the screen'
        )]
        [switch] $Bottom,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Place browser window in the center of the screen'
        )]
        [switch] $Centered,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Maximizes window to fill entire screen'
        )]
        [Alias('fs')]
        [switch] $Fullscreen,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Removes the borders of the window'
        )]
        [Alias('nb')]
        [switch] $NoBorders,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Restore PowerShell window focus'
        )]
        [Alias('rf', 'bg')]
        [switch] $RestoreFocus,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Position browser window either fullscreen on ' +
                'different monitor than PowerShell, or side by side with ' +
                'PowerShell on the same monitor')
        )]
        [Alias('sbs')]
        [switch] $SideBySide,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Focus the browser window after opening'
        )]
        [Alias('fw', 'focus')]
        [switch] $FocusWindow,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Set the browser window to foreground after opening'
        )]
        [Alias('fg')]
        [switch] $SetForeground,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Minimizes the window after positioning'
        )]
        [switch] $Minimize,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Maximize the window after positioning'
        )]
        [switch] $Maximize,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Restore the window to normal state after positioning'
        )]
        [switch] $SetRestored,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Keystrokes to send to the Window, ' +
                'see documentation for cmdlet GenXdev\Send-Key')
        )]
        [string[]] $KeysToSend,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Escape control characters and modifiers when sending keys'
        )]
        [Alias('Escape')]
        [switch] $SendKeyEscape,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Hold keyboard focus on target window when sending keys'
        )]
        [Alias('HoldKeyboardFocus')]
        [switch] $SendKeyHoldKeyboardFocus,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Use Shift+Enter instead of Enter when sending keys'
        )]
        [Alias('UseShiftEnter')]
        [switch] $SendKeyUseShiftEnter,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Delay between different input strings in ' +
                'milliseconds when sending keys')
        )]
        [Alias('DelayMilliSeconds')]
        [int] $SendKeyDelayMilliSeconds,
        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Returns window helper object for further manipulation'
        )]
        [Alias('pt')]
        [switch] $PassThru,
        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Use alternative settings stored in session for ' +
                'preferences')
        )]
        [switch] $SessionOnly,
        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Clear alternative settings stored in session for AI ' +
                'preferences')
        )]
        [switch] $ClearSession,
        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Store settings only in persistent preferences ' +
                'without affecting session')
        )]
        [Alias('FromPreferences')]
        [switch] $SkipSession,
        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Only output the calculated coordinates and size ' +
                'without actually positioning the window')
        )]
        [switch] $OnlyOutputCoords
        ########################################################################
    )

    begin {
        Microsoft.PowerShell.Utility\Write-Verbose (
            "Starting Playwright ${BrowserType} browser..."
        )

        # ensure Playwright assembly and browser binaries are available
        $pwParams = @{}
        if ($PSBoundParameters.ContainsKey('AutoConsent')) {
            $pwParams.AutoConsent = $AutoConsent
        }
        if ($PSBoundParameters.ContainsKey('AutoConsentAllPackages')) {
            $pwParams.AutoConsentAllPackages = $AutoConsentAllPackages
        }
        GenXdev\EnsurePlaywright @pwParams

        ########################################################################
        # Detect installed browsers for channel support
        ########################################################################

        # Check which real browsers are available on this system
        $script:hasChrome = $false
        $script:hasEdge = $false
        $script:hasFirefox = $false

        try {
            $allBrowsers = @(GenXdev\Get-Webbrowser)
            foreach ($browser in $allBrowsers) {
                if ($browser.Name -match 'Chrome') {
                    $script:hasChrome = $true
                }
                if ($browser.Name -match 'Edge') {
                    $script:hasEdge = $true
                }
                if ($browser.Name -match 'Firefox') {
                    $script:hasFirefox = $true
                }
            }
        }
        catch {
            Microsoft.PowerShell.Utility\Write-Verbose (
                "Could not detect installed browsers: $($_.Exception.Message)"
            )
        }

        Microsoft.PowerShell.Utility\Write-Verbose (
            "Installed browsers — Chrome: ${script:hasChrome}, " +
            "Edge: ${script:hasEdge}, Firefox: ${script:hasFirefox}"
        )

        # initialize global state container if needed
        if ($Global:GenXdevPlaywright -isnot [Hashtable]) {
            $Global:GenXdevPlaywright = @{}
        }

        # copy window positioning parameters for delegation to Set-WindowPosition
        $wpparams = GenXdev\Copy-IdenticalParamValues `
            -BoundParameters $PSBoundParameters `
            -FunctionName 'GenXdev\Set-WindowPosition' `
            -DefaultValues (
            Microsoft.PowerShell.Utility\Get-Variable -Scope Local `
                -ErrorAction SilentlyContinue
        )

        Microsoft.PowerShell.Utility\Write-Verbose (
            'Window positioning parameters copied: ' +
            "$($wpparams.Keys -join ', ')"
        )
    }

    process {

        ########################################################################
        # Step 1: Handle Force — close existing browser
        ########################################################################

        if ($Force -and $Global:GenXdevPlaywright.ContainsKey($BrowserType)) {

            if ($PSCmdlet.ShouldProcess(
                    "${BrowserType} browser",
                    'Close existing instance and start new'
                )) {

                Microsoft.PowerShell.Utility\Write-Verbose (
                    "Force flag set — closing existing ${BrowserType} browser..."
                )

                try {

                    $existing = $Global:GenXdevPlaywright[$BrowserType]

                    if ($existing.Context) {
                        $existing.Context.CloseAsync().GetAwaiter().GetResult()
                    }

                    if ($existing.Browser) {
                        $existing.Browser.CloseAsync().GetAwaiter().GetResult()
                    }
                }
                catch {

                    Microsoft.PowerShell.Utility\Write-Warning (
                        "Error closing existing browser: $($_.Exception.Message)"
                    )
                }

                $Global:GenXdevPlaywright.Remove($BrowserType)
            }
        }

        ########################################################################
        # Step 2: Check if browser is already running
        ########################################################################

        if ($Global:GenXdevPlaywright.ContainsKey($BrowserType)) {

            $existing = $Global:GenXdevPlaywright[$BrowserType]

            if ($existing.Browser -and $existing.Browser.IsConnected) {

                Microsoft.PowerShell.Utility\Write-Verbose (
                    "${BrowserType} browser already running. Use -Force to restart."
                )

                return $existing
            }
            else {

                # stale entry — clean it up
                Microsoft.PowerShell.Utility\Write-Verbose (
                    "Removing stale ${BrowserType} entry from global state."
                )

                $Global:GenXdevPlaywright.Remove($BrowserType)
            }
        }

        ########################################################################
        # Step 3: Get profile directory
        ########################################################################

        $profileDir = GenXdev\Get-PlaywrightProfileDirectory `
            -BrowserType $BrowserType

        Microsoft.PowerShell.Utility\Write-Verbose (
            "Using profile directory: ${profileDir}"
        )

        ########################################################################
        # Step 4: Build browser launch options
        ########################################################################

        if ($PSCmdlet.ShouldProcess(
                "${BrowserType} browser",
                'Launch persistent context'
            )) {

            $launchOptions = [Microsoft.Playwright.BrowserTypeLaunchPersistentContextOptions]::new()

            # headless mode — use ToBool() so -Headless:$false works correctly
            $launchOptions.Headless = $Headless.ToBool()

            # channel and anti-detection logic
            # BrowserType encodes the launch mode in its name:
            # - *Normal: use OS-installed browser via Channel API
            # - *Playwright: use bundled Playwright binary
            $script:useRealBrowser = $false
            $script:channelName = $null

            switch ($BrowserType) {
                'ChromeNormal' {
                    if ($script:hasChrome) {
                        $launchOptions.Channel = 'chrome'
                        $script:useRealBrowser = $true
                        $script:channelName = 'chrome'
                    }
                    else {
                        Microsoft.PowerShell.Utility\Write-Warning (
                            'Chrome is not installed on this system. ' +
                            'Falling back to bundled Playwright Chromium.'
                        )
                    }
                }
                'EdgeNormal' {
                    if ($script:hasEdge) {
                        $launchOptions.Channel = 'msedge'
                        $script:useRealBrowser = $true
                        $script:channelName = 'msedge'
                    }
                    else {
                        Microsoft.PowerShell.Utility\Write-Warning (
                            'Edge is not installed on this system. ' +
                            'Falling back to bundled Playwright Chromium.'
                        )
                    }
                }
                'ChromiumNormal' {
                    # Auto-detect: prefer Chrome, then Edge, then bundled
                    if ($script:hasChrome) {
                        $launchOptions.Channel = 'chrome'
                        $script:useRealBrowser = $true
                        $script:channelName = 'chrome'
                    }
                    elseif ($script:hasEdge) {
                        $launchOptions.Channel = 'msedge'
                        $script:useRealBrowser = $true
                        $script:channelName = 'msedge'
                    }
                    else {
                        Microsoft.PowerShell.Utility\Write-Warning (
                            'Neither Chrome nor Edge is installed. ' +
                            'Falling back to bundled Playwright Chromium.'
                        )
                    }
                }
                'ChromiumPlaywright' {
                    # Always use bundled Playwright Chromium binary
                    Microsoft.PowerShell.Utility\Write-Verbose (
                        'Using bundled Playwright Chromium binary.'
                    )
                }
                'FirefoxPlaywright' {
                    # Playwright .NET does not support channel for Firefox.
                    # Always use bundled binary.
                    Microsoft.PowerShell.Utility\Write-Verbose (
                        'Firefox channel not supported in Playwright ' +
                        '.NET API. Using bundled Playwright Firefox.'
                    )
                }
                'WebKitPlaywright' {
                    # WebKit has no OS-installed equivalent on Windows.
                    # Always use bundled binary.
                }
            }

            if ($script:useRealBrowser) {
                Microsoft.PowerShell.Utility\Write-Verbose (
                    "Using real browser via channel: ${script:channelName}"
                )
            }
            else {
                Microsoft.PowerShell.Utility\Write-Verbose (
                    'Using bundled Playwright browser binary.'
                )
            }

            # viewport size — when using a real browser in headed mode,
            # set viewport to null and maximize to let the native window
            # manager handle sizing (avoids zoom/scale issues)
            if ($script:useRealBrowser -and (-not $Headless.ToBool())) {
                if ($Width -le 0 -and $Height -le 0) {
                    # No custom size requested — let the browser window
                    # size naturally
                    $launchOptions.ViewportSize = $null
                }
                elseif ($Width -gt 0 -and $Height -gt 0) {
                    $launchOptions.ViewportSize = [Microsoft.Playwright.ViewportSize]::new()
                    $launchOptions.ViewportSize.Width = $Width
                    $launchOptions.ViewportSize.Height = $Height
                }
                elseif ($Width -gt 0) {
                    $launchOptions.ViewportSize = [Microsoft.Playwright.ViewportSize]::new()
                    $launchOptions.ViewportSize.Width = $Width
                    $launchOptions.ViewportSize.Height = 720
                }
                elseif ($Height -gt 0) {
                    $launchOptions.ViewportSize = [Microsoft.Playwright.ViewportSize]::new()
                    $launchOptions.ViewportSize.Width = 1280
                    $launchOptions.ViewportSize.Height = $Height
                }
            }
            else {
                # Bundled Playwright browser — use explicit viewport
                if ($Width -gt 0 -and $Height -gt 0) {
                    $launchOptions.ViewportSize = [Microsoft.Playwright.ViewportSize]::new()
                    $launchOptions.ViewportSize.Width = $Width
                    $launchOptions.ViewportSize.Height = $Height
                }
                elseif ($Width -gt 0) {
                    $launchOptions.ViewportSize = [Microsoft.Playwright.ViewportSize]::new()
                    $launchOptions.ViewportSize.Width = $Width
                    $launchOptions.ViewportSize.Height = 720
                }
                elseif ($Height -gt 0) {
                    $launchOptions.ViewportSize = [Microsoft.Playwright.ViewportSize]::new()
                    $launchOptions.ViewportSize.Width = 1280
                    $launchOptions.ViewportSize.Height = $Height
                }
            }

            # color scheme — use NoPreference to avoid the stark white
            # background that Playwright defaults to in automation mode
            $launchOptions.ColorScheme = 'NoPreference'

            # user agent — Playwright's default UA can differ from a real
            # browser's and may include automation markers. Override with
            # a normal Chrome/Edge UA string to avoid detection.
            if ($script:useRealBrowser) {
                # Real browser via channel — the browser uses its own UA,
                # but the context-level override ensures consistency and
                # masks any Playwright-specific headers.
                if ($script:channelName -eq 'msedge') {
                    $launchOptions.UserAgent =
                    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' +
                    'AppleWebKit/537.36 (KHTML, like Gecko) ' +
                    'Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0'
                }
                else {
                    $launchOptions.UserAgent =
                    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' +
                    'AppleWebKit/537.36 (KHTML, like Gecko) ' +
                    'Chrome/131.0.0.0 Safari/537.36'
                }
            }
            elseif ($BrowserType -eq 'ChromiumPlaywright') {
                # Bundled Playwright Chromium — override UA to match a
                # real Chrome install, hiding the Playwright marker.
                $launchOptions.UserAgent =
                'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' +
                'AppleWebKit/537.36 (KHTML, like Gecko) ' +
                'Chrome/131.0.0.0 Safari/537.36'
            }

            # proxy settings
            if ($Proxy) {
                $launchOptions.Proxy = [Microsoft.Playwright.Proxy]::new()
                $launchOptions.Proxy.Server = $Proxy
            }

            # accept-language header
            if ($AcceptLang) {
                $launchOptions.Locale = $AcceptLang
            }


            # args for a normal-looking browser
            $launchOptions.Args = [System.Collections.Generic.List[string]]::new()

            # Clean up basic flags (Removed the flag causing the new warning)
            $launchOptions.Args.Add('--disable-features=Translate')
            $launchOptions.Args.Add('--no-first-run')
            $launchOptions.Args.Add('--no-default-browser-check')

            # FIX THE GRAY BOX: Instead of a null viewport with forced scaling,
            # we explicitly specify a massive default viewport boundary.
            # This forces Chrome to render the document canvas all the way to the edges.
            $launchOptions.ViewportSize = [Microsoft.Playwright.ViewportSize]::new()
            if ($useRealBrowser) {

                $launchOptions.ViewportSize.Width = -1
                $launchOptions.ViewportSize.Height = -1
            }

            # When using a real browser in headed mode, start maximized
            # so the window manager handles native sizing correctly
            if ($script:useRealBrowser -and (-not $Headless.ToBool()) -and
                $Width -le 0 -and $Height -le 0) {
                $launchOptions.Args.Add('--start-maximized')
            }

            Microsoft.PowerShell.Utility\Write-Verbose (
                'Launch options prepared. Launching persistent context...'
            )

            ####################################################################
            # Step 5: Launch the persistent context
            ####################################################################

            try {

                $playwright = [Microsoft.Playwright.Playwright]::CreateAsync().
                GetAwaiter().GetResult()

                # Map BrowserType to Playwright's engine name.
                # Playwright engines: chromium, firefox, webkit.
                $engineMap = @{
                    'ChromeNormal'       = 'chromium'
                    'EdgeNormal'         = 'chromium'
                    'ChromiumNormal'     = 'chromium'
                    'ChromiumPlaywright' = 'chromium'
                    'FirefoxPlaywright'  = 'firefox'
                    'WebKitPlaywright'   = 'webkit'
                }
                $engineName = $engineMap[$BrowserType]
                $browserTypeObj = $playwright[$engineName]

                $context = $browserTypeObj.LaunchPersistentContextAsync(
                    $profileDir,
                    $launchOptions
                ).GetAwaiter().GetResult()

                # get the browser from the context (it's the parent)
                $browser = $context.Browser

                # get or wait for the first page
                $pages = $context.Pages

                if ($pages.Count -gt 0) {
                    $page = $pages[0]
                }
                else {
                    $page = $context.NewPageAsync().GetAwaiter().GetResult()
                }

                Microsoft.PowerShell.Utility\Write-Verbose (
                    "${BrowserType} browser launched successfully."
                )

                # store in global state
                $state = @{
                    Browser    = $browser
                    Context    = $context
                    Page       = $page
                    Playwright = $playwright
                    Port       = $null  # no debugging port — direct Playwright API
                }

                $Global:GenXdevPlaywright[$BrowserType] = $state

                # also set legacy aliases for backward compatibility
               $Global:chromeSession  = $null
                $Global:chromeController = $page
                $Global:chrome = $state

                Microsoft.PowerShell.Utility\Write-Verbose (
                    'Browser state stored in $Global:GenXdevPlaywright'
                )

                ############################################################
                # Step 6: Position the browser window via Set-WindowPosition
                ############################################################

                # Skip window positioning in headless mode — no window to position
                if ($Headless.ToBool()) {
                    Microsoft.PowerShell.Utility\Write-Verbose (
                        'Headless mode — skipping window positioning.'
                    )

                    return $state
                }

                Microsoft.PowerShell.Utility\Write-Verbose (
                    'Looking for browser window...'
                )

                # find the browser window — retry up to 10 seconds
                # search for ms-playwright (bundled) or chrome.exe/msedge.exe
                # (real browser) depending on the launch mode
                $window = $null
                $retries = 0

                do {
                    Microsoft.PowerShell.Utility\Start-Sleep -Milliseconds 200

                    if ($script:useRealBrowser) {
                        # Real browser via channel — look for chrome.exe
                        # or msedge.exe, not ms-playwright
                        $exeName = if ($script:channelName -eq 'msedge') {
                            'msedge'
                        }
                        else {
                            'chrome'
                        }
                        $pwProcesses = @(Microsoft.PowerShell.Management\Get-Process `
                                -Name $exeName -ErrorAction SilentlyContinue |
                                Microsoft.PowerShell.Core\Where-Object {
                                    $PSItem.MainWindowHandle -ne 0
                                } |
                                Microsoft.PowerShell.Utility\Sort-Object {
                                    $PSItem.StartTime
                                } -Descending)
                    }
                    else {
                        # Bundled browser — look for ms-playwright in path
                        $pwProcesses = @(Microsoft.PowerShell.Management\Get-Process `
                                -ErrorAction SilentlyContinue |
                                Microsoft.PowerShell.Core\Where-Object {
                                    $PSItem.Path -and (
                                        $PSItem.Path -like '*\ms-playwright\*'
                                    ) -and $PSItem.MainWindowHandle -ne 0
                                } |
                                Microsoft.PowerShell.Utility\Sort-Object {
                                    $PSItem.StartTime
                                } -Descending)
                    }

                    foreach ($pwProcess in $pwProcesses) {
                        $window = [GenXdev.Helpers.WindowObj]::GetMainWindow(
                            $pwProcess, 1, 80
                        )

                        if ($window -and $window.Length -gt 0) {
                            break
                        }
                    }

                    if ($window -and $window.Length -gt 0) {
                        break
                    }

                    $retries++
                    Microsoft.PowerShell.Utility\Start-Sleep 1
                } while ($retries -lt 50)

                if ($window -and $window.Length -gt 0) {

                    Microsoft.PowerShell.Utility\Write-Verbose (
                        'Browser window found. Delegating positioning to Set-WindowPosition...'
                    )

                    # remove KeysToSend from wpparams — Set-WindowPosition
                    # handles keys separately via its own params
                    if ($wpparams.ContainsKey('KeysToSend')) {
                        $null = $wpparams.Remove('KeysToSend')
                    }

                    $null = GenXdev\Set-WindowPosition @wpparams `
                        -WindowHelper:$window[0] -ErrorAction SilentlyContinue
                }
                else {
                    Microsoft.PowerShell.Utility\Write-Verbose (
                        'Browser window not found within timeout. ' +
                        'Skipping window positioning.'
                    )
                }

                return $state
            }
            catch {

                Microsoft.PowerShell.Utility\Write-Error (
                    "Failed to launch ${BrowserType} browser: $($_.Exception.Message)"
                )

                throw
            }
        }
    }

    end {

        Microsoft.PowerShell.Utility\Write-Verbose (
            'Open-PlayWrightBrowser completed.'
        )
    }
}