Modules/businessdev.ALbuild.Apps/Resources/TestRunner/BcTestClientContext.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS In-container client-services session used to drive the AL Test Tool page. .DESCRIPTION This is an ALbuild payload script: it runs *inside* the Business Central Windows container only. It is a clean, from-scratch reimplementation of the client-services session that the test runner uses to open the AL Test Tool page (130455), set its controls and invoke its actions through Microsoft.Dynamics.Framework.UI.Client. Those types exist only inside the BC container, so this file is never dot-sourced on the host (it lives under Resources/ and is not part of the module loader). The Microsoft.* types are referenced only inside method bodies so the class can be defined before the client DLL is fully resolved. Only the surface needed by the runner is implemented (open/close forms, read/write controls, invoke actions, dismiss dialogs) - deliberately smaller than the legacy reference. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '', Justification = 'Register-ObjectEvent action blocks run in a separate scope and can only share the open session via $Global:.')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '', Justification = 'Session disposal is best-effort; a session that is already torn down must not surface a new error.')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUsePSCredentialType', '', Justification = 'Constructor overloads model the client-services auth schemes (credential, token and Windows) explicitly.')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '', Justification = 'Initialize accepts an [object] credential (NetworkCredential, TokenCredential or null) as required by the client-services API; no plaintext password is handled here.')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'ClientDllPath documents the load contract; callers add-type the DLL before dot-sourcing this file.')] param( [Parameter(Mandatory)] [string] $ClientDllPath ) # Reference the parameter so analyzers see it as used; callers load the DLL before dot-sourcing. $null = $ClientDllPath class BcTestClientContext { [object[]] $events = @() [object] $clientSession = $null [string] $culture = 'en-US' [string] $timezone = '' [bool] $debugMode = $false [object] $addressUri = $null BcTestClientContext([string] $serviceUrl, [pscredential] $credential, [timespan] $interactionTimeout, [string] $culture, [string] $timezone) { $networkCredential = New-Object System.Net.NetworkCredential -ArgumentList $credential.UserName, $credential.Password $this.Initialize($serviceUrl, [Microsoft.Dynamics.Framework.UI.Client.AuthenticationScheme]::UserNamePassword, $networkCredential, $interactionTimeout, $culture, $timezone) } BcTestClientContext([string] $serviceUrl, [string] $accessToken, [timespan] $interactionTimeout, [string] $culture, [string] $timezone) { $tokenCredential = New-Object Microsoft.Dynamics.Framework.UI.Client.TokenCredential -ArgumentList $accessToken $this.Initialize($serviceUrl, [Microsoft.Dynamics.Framework.UI.Client.AuthenticationScheme]::AzureActiveDirectory, $tokenCredential, $interactionTimeout, $culture, $timezone) } BcTestClientContext([string] $serviceUrl, [timespan] $interactionTimeout, [string] $culture, [string] $timezone) { $this.Initialize($serviceUrl, [Microsoft.Dynamics.Framework.UI.Client.AuthenticationScheme]::Windows, $null, $interactionTimeout, $culture, $timezone) } [void] Initialize([string] $serviceUrl, [object] $authenticationScheme, [object] $credential, [timespan] $interactionTimeout, [string] $sessionCulture, [string] $sessionTimezone) { $uri = New-Object System.Uri -ArgumentList $serviceUrl $this.addressUri = [Microsoft.Dynamics.Framework.UI.Client.ServiceAddressProvider]::ServiceAddress($uri) $jsonClient = New-Object Microsoft.Dynamics.Framework.UI.Client.JsonHttpClient -ArgumentList $this.addressUri, $credential, $authenticationScheme $httpClientField = $jsonClient.GetType().GetField('httpClient', [Reflection.BindingFlags]::NonPublic -bor [Reflection.BindingFlags]::Instance) $httpClient = $httpClientField.GetValue($jsonClient) $httpClient.Timeout = $interactionTimeout $this.clientSession = New-Object Microsoft.Dynamics.Framework.UI.Client.ClientSession -ArgumentList $jsonClient, (New-Object Microsoft.Dynamics.Framework.UI.Client.NonDispatcher), (New-Object 'Microsoft.Dynamics.Framework.UI.Client.TimerFactory[Microsoft.Dynamics.Framework.UI.Client.TaskTimer]') $this.culture = $sessionCulture if ([string]::IsNullOrEmpty($sessionTimezone)) { $tz = Get-TimeZone $match = Get-TimeZone -ListAvailable | Where-Object { $_.BaseUtcOffset -eq $tz.BaseUtcOffset -and $_.SupportsDaylightSavingTime -eq $tz.SupportsDaylightSavingTime } | Select-Object -First 1 if ($match) { $this.timezone = $match.Id } } else { $this.timezone = $sessionTimezone } $this.OpenSession() } [void] OpenSession() { $Global:BcTestOpenContext = $this $parameters = New-Object Microsoft.Dynamics.Framework.UI.Client.ClientSessionParameters $parameters.CultureId = $this.culture $parameters.UICultureId = $this.culture $parameters.TimeZoneId = $this.timezone $parameters.AdditionalSettings.Add('IncludeControlIdentifier', $true) $this.events += @(Register-ObjectEvent -InputObject $this.clientSession -EventName MessageToShow -Action { Write-Host "Message: $($EventArgs.Message)" }) $this.events += @(Register-ObjectEvent -InputObject $this.clientSession -EventName CommunicationError -Action { Write-Host -ForegroundColor Red "CommunicationError: $($EventArgs.Exception.Message)" }) $this.events += @(Register-ObjectEvent -InputObject $this.clientSession -EventName UnhandledException -Action { Write-Host -ForegroundColor Red "UnhandledException: $($EventArgs.Exception.Message)" }) $this.events += @(Register-ObjectEvent -InputObject $this.clientSession -EventName InvalidCredentialsError -Action { Write-Host -ForegroundColor Red 'InvalidCredentialsError' }) $this.events += @(Register-ObjectEvent -InputObject $this.clientSession -EventName DialogToShow -Action { $form = $EventArgs.DialogToShow $context = $Global:BcTestOpenContext if (-not $context) { return } $errorDialogId = '00000000-0000-0000-0800-0000836bd2d2' $warningDialogId = '00000000-0000-0000-0300-0000836bd2d2' $infoDialogId = '8da61efd-0002-0003-0507-0b0d1113171d' if ($form.ControlIdentifier -eq $errorDialogId) { $errorControl = $form.ContainedControls | Where-Object { $_ -is [Microsoft.Dynamics.Framework.UI.Client.ClientStaticStringControl] } | Select-Object -First 1 $context.CloseForm($form) throw "$($errorControl.StringValue)" } elseif ($form.ControlIdentifier -eq $warningDialogId) { $warningControl = $form.ContainedControls | Where-Object { $_ -is [Microsoft.Dynamics.Framework.UI.Client.ClientStaticStringControl] } | Select-Object -First 1 Write-Host -ForegroundColor Yellow "Warning: $($warningControl.StringValue)" $context.CloseForm($form) } elseif ($form.ControlIdentifier -eq $infoDialogId) { $context.CloseForm($form) } else { $okAction = $context.GetActionByName($form, 'OK') if ($okAction) { $context.InvokeAction($okAction) } else { $context.CloseForm($form) } } }) $this.clientSession.OpenSessionAsync($parameters) $this.AwaitState([Microsoft.Dynamics.Framework.UI.Client.ClientSessionState]::Ready) } [void] AwaitState([object] $state) { $start = [DateTime]::Now while ($this.clientSession.State -ne $state) { Start-Sleep -Milliseconds 100 $current = $this.clientSession.State if ($current -eq [Microsoft.Dynamics.Framework.UI.Client.ClientSessionState]::InError) { throw "ClientSession entered the InError state (waited $(([DateTime]::Now - $start).TotalSeconds) seconds)." } if ($current -eq [Microsoft.Dynamics.Framework.UI.Client.ClientSessionState]::TimedOut) { throw "ClientSession entered the TimedOut state (waited $(([DateTime]::Now - $start).TotalSeconds) seconds)." } if ($current -eq [Microsoft.Dynamics.Framework.UI.Client.ClientSessionState]::Uninitialized -and ([DateTime]::Now - $start).TotalSeconds -ge 120) { throw "ClientSession remained Uninitialized for $(([DateTime]::Now - $start).TotalSeconds) seconds." } } Start-Sleep -Milliseconds 100 } [void] Dispose() { $Global:BcTestOpenContext = $null $this.events | ForEach-Object { Unregister-Event -SourceIdentifier $_.Name -ErrorAction SilentlyContinue } $this.events = @() try { if ($this.clientSession -and $this.clientSession.State -ne [Microsoft.Dynamics.Framework.UI.Client.ClientSessionState]::Closed) { $this.clientSession.CloseSessionAsync() $this.AwaitState([Microsoft.Dynamics.Framework.UI.Client.ClientSessionState]::Closed) } } catch { # The session may already be torn down; disposal is best-effort. } } [void] InvokeInteraction([object] $interaction) { $this.clientSession.InvokeInteractionAsync($interaction) $this.AwaitState([Microsoft.Dynamics.Framework.UI.Client.ClientSessionState]::Ready) } [object] InvokeInteractionAndCatchForm([object] $interaction) { $Global:BcTestCaughtForm = $null $formEvent = Register-ObjectEvent -InputObject $this.clientSession -EventName FormToShow -Action { $Global:BcTestCaughtForm = $EventArgs.FormToShow } try { $this.InvokeInteraction($interaction) if (-not $Global:BcTestCaughtForm) { $this.CloseAllWarningForms() } } finally { Unregister-Event -SourceIdentifier $formEvent.Name -ErrorAction SilentlyContinue } $form = $Global:BcTestCaughtForm Remove-Variable -Name BcTestCaughtForm -Scope Global -ErrorAction SilentlyContinue return $form } [object] OpenForm([int] $page) { try { $interaction = New-Object Microsoft.Dynamics.Framework.UI.Client.Interactions.OpenFormInteraction $interaction.Page = $page return $this.InvokeInteractionAndCatchForm($interaction) } catch { return $null } } [void] CloseForm([object] $form) { $this.InvokeInteraction((New-Object Microsoft.Dynamics.Framework.UI.Client.Interactions.CloseFormInteraction -ArgumentList $form)) } [object[]] GetAllForms() { $forms = @() $this.clientSession.OpenedForms.GetEnumerator() | ForEach-Object { $forms += $_ } return $forms } [void] CloseAllForms() { $this.GetAllForms() | ForEach-Object { $this.CloseForm($_) } } [void] CloseAllWarningForms() { $warningDialogId = '00000000-0000-0000-0300-0000836bd2d2' $this.GetAllForms() | ForEach-Object { if ($_.ControlIdentifier -eq $warningDialogId) { $this.CloseForm($_) } } } [object] GetControlByName([object] $control, [string] $name) { $result = $control.ContainedControls | Where-Object { $_.Name -eq $name } | Select-Object -First 1 if (-not $result) { $result = $control.ContainedControls | Where-Object { $_.Caption -eq $name } | Select-Object -First 1 } return $result } [object] GetControlByType([object] $control, [Type] $type) { return $control.ContainedControls | Where-Object { $_ -is $type } | Select-Object -First 1 } [object] GetActionByName([object] $control, [string] $name) { $result = $control.ContainedControls | Where-Object { ($_ -is [Microsoft.Dynamics.Framework.UI.Client.ClientActionControl]) -and ($_.Name -eq $name) } | Select-Object -First 1 if (-not $result) { $result = $control.ContainedControls | Where-Object { ($_ -is [Microsoft.Dynamics.Framework.UI.Client.ClientActionControl]) -and ($_.Caption -eq $name) } | Select-Object -First 1 } return $result } [void] SaveValue([object] $control, [object] $newValue) { $this.InvokeInteraction((New-Object Microsoft.Dynamics.Framework.UI.Client.Interactions.SaveValueInteraction -ArgumentList $control, $newValue)) } [void] InvokeAction([object] $action) { $this.InvokeInteraction((New-Object Microsoft.Dynamics.Framework.UI.Client.Interactions.InvokeActionInteraction -ArgumentList $action)) } } |