GenXdev.Webbrowser.Playwright.psm1

<##############################################################################
Part of PowerShell module : GenXdev.Webbrowser.Playwright
Original cmdlet filename : Connect-PlaywrightViaDebuggingPort.ps1
Original author : René Vaessen / GenXdev
Version : 3.29.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.
################################################################################>


using namespace System.Management.Automation
using namespace System.Collections.Concurrent

<#
.SYNOPSIS
Connects to an existing browser instance via debugging port.
 
.DESCRIPTION
Establishes a connection to a running Chromium-based browser instance using the
WebSocket debugger URL. Creates a Playwright instance and connects over CDP
(Chrome DevTools Protocol). The connected browser instance is stored in a global
dictionary for later reference.
 
.PARAMETER WsEndpoint
The WebSocket URL for connecting to the browser's debugging port. This URL
typically follows the format 'ws://hostname:port/devtools/browser/<id>'.
 
.EXAMPLE
Connect-PlaywrightViaDebuggingPort `
    -WsEndpoint "ws://localhost:9222/devtools/browser/abc123"
###############################################################################>

function Connect-PlaywrightViaDebuggingPort {

    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')]
    param(
        [Parameter(
            Mandatory = $true,
            Position = 0,
            HelpMessage = 'WebSocket URL for browser debugging connection'
        )]
        [ValidateNotNullOrEmpty()]
        [string]$WsEndpoint,
        [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 ' +
                'installations. Useful for unattended or CI/CD scenarios.')
        )]
        [switch]$AutoConsentAllPackages,
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Use alternative settings stored in session for ' +
                'preferences')
        )]
        [switch]$SessionOnly
    )

    begin {
        # log connection attempt for debugging purposes
        Microsoft.PowerShell.Utility\Write-Verbose "Attempting to connect to browser at: $WsEndpoint"

        $ensureParams = GenXdev\Copy-IdenticalParamValues `
            -BoundParameters $PSBoundParameters `
            -FunctionName 'GenXdev\EnsurePlaywright'
        GenXdev\EnsurePlaywright @ensureParams

       $Global:GenXdevPlaywrightBrowserDictionary = $Global:GenXdevPlaywrightBrowserDictionary ?
       $Global:GenXdevPlaywrightBrowserDictionary :
       [System.Collections.Concurrent.ConcurrentDictionary[string, Microsoft.Playwright.IBrowser]]::new()
    }


    process {
        try {
            # create new playwright instance
            Microsoft.PowerShell.Utility\Write-Verbose 'Creating Playwright instance'
            $playwright = [Microsoft.Playwright.Playwright]::CreateAsync().Result

            # connect to browser using CDP protocol
            Microsoft.PowerShell.Utility\Write-Verbose 'Connecting to browser via CDP'
            $browser = $playwright.Chromium.ConnectOverCDPAsync($WsEndpoint).Result

            # store browser instance for module-wide access
            Microsoft.PowerShell.Utility\Write-Verbose 'Storing browser instance in global dictionary'
            $Global:GenXdevPlaywrightBrowserDictionary[$WsEndpoint] = $browser

            return $browser
        }
        catch {
            Microsoft.PowerShell.Utility\Write-Error "Failed to connect via debugging port: $_"
            throw
        }
    }

    end {
    }
}
<##############################################################################
Part of PowerShell module : GenXdev.Webbrowser.Playwright
Original cmdlet filename : Open-PlayWrightBrowser.ps1
Original author : René Vaessen / GenXdev
Version : 3.29.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.'
        )
    }
}
<##############################################################################
Part of PowerShell module : GenXdev.Webbrowser.Playwright
Original cmdlet filename : Resume-WebbrowserTabVideo.ps1
Original author : René Vaessen / GenXdev
Version : 3.29.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
Resumes video playback in a YouTube browser tab.
 
.DESCRIPTION
Finds the active YouTube browser tab and resumes video playback by executing the
play() method on any video elements found in the page. If no YouTube tab is
found, the function throws an error. This function is particularly useful for
automating video playback control in browser sessions.
 
.EXAMPLE
Resume-WebbrowserTabVideo
 
.EXAMPLE
wbvideoplay
 
.NOTES
Requires an active Chrome browser session with at least one YouTube tab open.
The function will throw an error if no YouTube tab is found.
###############################################################################>

function Resume-WebbrowserTabVideo {

    [CmdletBinding()]
    [Alias('wbvideoplay')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')]
    param (
    )

    begin {

        # search for a youtube tab in the current browser session
        Microsoft.PowerShell.Utility\Write-Verbose 'Attempting to locate an active YouTube tab...'
        $null = GenXdev\Select-WebbrowserTab -Name '*youtube*'
    }


    process {

        # verify that a youtube tab was successfully found and selected
        if ($null -eq $Global:chromeSession) {

            throw 'No YouTube tab found in current browser session'
        }

        Microsoft.PowerShell.Utility\Write-Verbose 'YouTube tab found - initiating video playback...'

        # execute the play() method on all video elements in the current page
        $null = GenXdev\Get-WebbrowserTabDomNodes 'video' 'e.play()'

        Microsoft.PowerShell.Utility\Write-Verbose 'Video playback successfully resumed'
    }

    end {
    }
}
<##############################################################################
Part of PowerShell module : GenXdev.Webbrowser.Playwright
Original cmdlet filename : Stop-WebbrowserVideos.ps1
Original author : René Vaessen / GenXdev
Version : 3.29.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
Pauses video playback in all active browser sessions.
 
.DESCRIPTION
Iterates through all active browser sessions and pauses any playing videos by
executing JavaScript commands. The function maintains the original session state
and handles errors gracefully.
 
.EXAMPLE
Stop-WebbrowserVideos
 
.EXAMPLE
wbsst
###############################################################################>

function Stop-WebbrowserVideos {

    [CmdletBinding(SupportsShouldProcess)]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')]
    [Alias('wbsst')]
    [Alias('ssst')]
    [Alias('wbvideostop')]
    param(
        [Alias('e')]
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Opens in Microsoft Edge'
        )]
        [switch] $Edge,
        [Alias('ch')]
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Opens in Google Chrome'
        )]
        [switch] $Chrome,
        [Alias('c')]
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Opens in Microsoft Edge or Google Chrome, ' +
                'depending on what the default browser is')
        )]
        [switch] $Chromium,
        [Alias('ff')]
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Opens in Firefox'
        )]
        [switch] $Firefox,
        [Alias('wk')]
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Uses the Playwright-managed WebKit browser'
        )]
        [switch] $Webkit
    )

    begin {
        Microsoft.PowerShell.Utility\Write-Verbose 'Starting video pause operation across browser sessions'

        # store the current session reference to restore it later
        $originalSession = $Global:chromeSession
        $originalController = $Global:chromeController

        # ensure we have an active browser session
        if (($null -eq $Global:chromeSessions) -or
            ($Global:chromeSessions.Count -eq 0)) {

            # select a browser tab if none are active
            $null = GenXdev\Select-WebbrowserTab -Chrome:$chrome -Edge:$edge
        }
    }


    process {
        # iterate through each browser session and pause videos
        $Global:chromeSessions | Microsoft.PowerShell.Core\ForEach-Object {

            $currentSession = $_
            if ($null -eq $_) { return }
            if ($PSCmdlet.ShouldProcess('Browser session', 'Pause videos')) {

                try {
                    Microsoft.PowerShell.Utility\Write-Verbose "Attempting to pause videos in session: $currentSession"

                    # select the current tab for processing
                    $null = GenXdev\Select-WebbrowserTab -ByReference $currentSession

                    # execute pause() command on all video elements
                    GenXdev\Get-WebbrowserTabDomNodes 'video' 'e.pause()' -NoAutoSelectTab
                }
                catch {
                    Microsoft.PowerShell.Utility\Write-Warning "Failed to pause videos in session: $currentSession `r`n$($_.Exception.Message)"
                }
            }
        }
    }

    end {
        Microsoft.PowerShell.Utility\Write-Verbose 'Restoring original browser session reference'
        $Global:chromeSession = $originalSession
        $Global:chromeController = $originalController
    }
}
<##############################################################################
Part of PowerShell module : GenXdev.Webbrowser.Playwright
Original cmdlet filename : Unprotect-WebbrowserTab.ps1
Original author : René Vaessen / GenXdev
Version : 3.29.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
Takes control of a selected web browser tab for interactive manipulation.
 
.DESCRIPTION
This function enables interactive control of a browser tab that was previously
selected using Select-WebbrowserTab. It provides direct access to the Microsoft
Playwright Page object's properties and methods, allowing for automated browser
interaction.
 
.PARAMETER UseCurrent
When specified, uses the currently assigned browser tab instead of prompting to
select a new one. This is useful for continuing work with the same tab.
 
.PARAMETER Force
Forces a browser restart by closing all tabs if no debugging server is detected.
Use this when the browser connection is in an inconsistent state.
 
.EXAMPLE
Unprotect-WebbrowserTab -UseCurrent
 
.EXAMPLE
wbctrl -Force
#>

function Unprotect-WebbrowserTab {

    [CmdletBinding(DefaultParameterSetName = 'Default')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')]
    [Alias('wbctrl')]
    param(
        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'Default',
            HelpMessage = 'Use current tab instead of selecting a new one'
        )]
        [Alias('current')]
        [switch] $UseCurrent,
        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'Default',
            HelpMessage = 'Restart browser if no debugging server detected'
        )]
        [switch] $Force
    )

    begin {

        Microsoft.PowerShell.Utility\Write-Verbose 'Initializing browser tab control sequence...'

        # get reference to powershell window for manipulation
        $pwshW = GenXdev\Get-PowershellMainWindow
    }


    process {

        if (-not $UseCurrent) {

            Clear-Host

            Microsoft.PowerShell.Utility\Write-Verbose 'Prompting user to select a browser tab...'
            Microsoft.PowerShell.Utility\Write-Host 'Select to which browser tab you want to send commands to'

            # attempt to get list of available browser tabs
            GenXdev\Select-WebbrowserTab -Force:$Force

            if ($Global:ChromeSessions.Length -eq 0) {

                Microsoft.PowerShell.Utility\Write-Host 'No browser tabs are open'
                return
            }

            # get valid tab selection from user
            $tabNumber = 0
            do {
                $tabNumber = Microsoft.PowerShell.Utility\Read-Host 'Enter the number of the tab you want to control'
                $tabNumber = $tabNumber -as [int]
                $tabCount = $Global:ChromeSessions.Length

                if ($tabNumber -lt 0 -or $tabNumber -gt $tabCount - 1) {
                    Microsoft.PowerShell.Utility\Write-Host ('Invalid tab number. Please enter a number ' +
                        "between 0 and $($tabCount-1)")
                    continue
                }
                break
            } while ($true)

            # activate the selected browser tab
            GenXdev\Select-WebbrowserTab $tabNumber
        }

        if (-not $Global:chromeController) {

            Microsoft.PowerShell.Utility\Write-Host 'No ChromeController object found'
            return
        }

        try {
            # maximize the powershell window
            $null = $pwshW.Maximize()
        }
        catch {
            Microsoft.PowerShell.Utility\Write-Verbose "Failed to maximize PowerShell window: $_"
        }

        # create background job for keyboard input
        $null = Microsoft.PowerShell.Core\Start-Job {

            # send keyboard sequence to expose chrome controller object
            $null = GenXdev\Send-Key `
                '{ESCAPE}', 'Clear-Host', '{ENTER}', "`$ChromeController", '.',
            '^( )', 'y' `
                -SendKeyDelayMilliSeconds 500 `
                -WindowHandle ((GenXdev\Get-PowershellMainWindow).Handle)

            # allow time for commands to complete
            $null = Microsoft.PowerShell.Utility\Start-Sleep 3
        }

        try {
            # attempt to bring powershell window to front
            $null = GenXdev\Get-PowershellMainWindow | Microsoft.PowerShell.Core\ForEach-Object {

                $null = $_.Focus()
                $null = GenXdev\Set-ForegroundWindow $_.handle
            }
        }
        catch {
            Microsoft.PowerShell.Utility\Write-Verbose "Failed to set PowerShell window focus: $_"
        }
    }

    end {
    }
}