├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ └── dotnet-core.yml ├── .gitignore ├── Directory.Build.props ├── Directory.Build.targets ├── Directory.Packages.props ├── Images ├── Icon.ico ├── Icon.pdn └── Icon.png ├── LICENSE ├── NuGet.config ├── README.md ├── XAMLTest.Generator ├── ElementGenerator.cs ├── IsExternalInit.cs └── XAMLTest.Generator.csproj ├── XAMLTest.TestApp ├── App.xaml ├── App.xaml.cs ├── AssemblyInfo.cs ├── MainWindow.xaml ├── MainWindow.xaml.cs └── XAMLTest.TestApp.csproj ├── XAMLTest.Tests ├── AppTests.cs ├── AssemblyInfo.cs ├── ColorMixinsTests.cs ├── Generated │ ├── ContextMenuGeneratedExtensionsTests.cs │ ├── TooltipGeneratedExtensionsTests.cs │ └── WindowGeneratedExtensionsTests.cs ├── GeneratedTests.cs ├── GeneratorTests.cs ├── GetCoordinatesTests.cs ├── GetEffectiveBackgroundTests.cs ├── GetElementTests.cs ├── GetResourceTests.cs ├── HighlightTests.cs ├── MouseInputTests.cs ├── PositionTests.cs ├── SendKeyboardInputTests.cs ├── SendMouseInputTests.cs ├── SerializerTests.cs ├── Simulators │ ├── App.cs │ └── Image.cs ├── TestControls │ ├── MouseClickPositions.xaml │ ├── MouseClickPositions.xaml.cs │ ├── TestWindow.xaml │ ├── TestWindow.xaml.cs │ ├── TextBlock_AttachedProperty.xaml │ ├── TextBlock_AttachedProperty.xaml.cs │ ├── TextBox_ValidationError.xaml │ └── TextBox_ValidationError.xaml.cs ├── TestRecorderTests.cs ├── ValidationTests.cs ├── VisualElementTests.cs ├── WaitTests.cs └── XAMLTest.Tests.csproj ├── XAMLTest.UnitTestGenerator ├── UnitTestGenerator.cs └── XAMLTest.UnitTestGenerator.csproj ├── XAMLTest.sln ├── XAMLTest ├── App.cs ├── AppMixins.RemoteExecute.cs ├── AppMixins.cs ├── AppOptions.cs ├── Build │ └── XAMLTest.targets ├── ColorMixins.cs ├── DependencyPropertyHelper.cs ├── ElementQuery.cs ├── Event │ └── EventRegistrar.cs ├── GenerateHelpersAttribute.cs ├── HighlightConfig.cs ├── Host │ ├── VisualTreeService.Elements.cs │ ├── VisualTreeService.Events.cs │ ├── VisualTreeService.Highlight.cs │ ├── VisualTreeService.Input.cs │ ├── VisualTreeService.Invocation.cs │ ├── VisualTreeService.cs │ └── XamlTestSpec.proto ├── IApp.cs ├── IEventInvocation.cs ├── IEventRegistration.cs ├── IImage.cs ├── IProperty.cs ├── IQuery.cs ├── IResource.cs ├── ISerializer.cs ├── IValidation.cs ├── IValue.cs ├── IVersion.cs ├── IVisualElement.cs ├── IVisualElementConverter.cs ├── IWindow.cs ├── Input │ ├── DelayInput.cs │ ├── IInput.cs │ ├── KeyboardInput.cs │ ├── KeysInput.cs │ ├── ModifiersInput.cs │ ├── MouseInput.cs │ └── TextInput.cs ├── Internal │ ├── App.cs │ ├── AppContext.cs │ ├── BaseValue.cs │ ├── BitmapImage.cs │ ├── DependencyObjectTracker.cs │ ├── EventRegistration.cs │ ├── IElementId.cs │ ├── Property.cs │ ├── ProtocolClientMixins.cs │ ├── Resource.cs │ ├── Screen.cs │ ├── SelectionAdorner.cs │ ├── Serializer.cs │ ├── Service.cs │ ├── Validation.cs │ ├── Validation{T}.cs │ ├── Version.cs │ ├── VisualElement.cs │ └── Window.cs ├── KeyboardInput.cs ├── Logger.cs ├── MouseInput.cs ├── NativeMethods.txt ├── Position.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Query │ └── StringBuilderQuery.cs ├── QueryMixins.cs ├── RectMixins.cs ├── Retry.cs ├── Server.cs ├── TestRecorder.cs ├── Transport │ ├── BrushSerializer.cs │ ├── CharSerializer.cs │ ├── DefaultSerializer.cs │ ├── DependencyPropertyConverter.cs │ ├── DpiScaleSerializer.cs │ ├── GridSerializer.cs │ ├── JsonSerializer.cs │ ├── SecureStringSerializer.cs │ └── XamlSegmentSerializer.cs ├── Utility │ └── AppDomainMixins.cs ├── VTMixins.cs ├── VisualElementMixins.Highlight.cs ├── VisualElementMixins.Input.cs ├── VisualElementMixins.Query.cs ├── VisualElementMixins.RemoteExecute.cs ├── VisualElementMixins.Window.cs ├── VisualElementMixins.cs ├── VisualStudioAttacher.cs ├── Wait.cs ├── WindowMessage.cs ├── WindowMixins.cs ├── XAMLTest.csproj ├── XAMLTestException.cs ├── XamlSegment.cs ├── XmlNamespace.cs └── XmlNamespaceMixins.cs └── global.json /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # IDE0022: Use block body for methods 4 | csharp_style_expression_bodied_methods = when_on_single_line:silent 5 | csharp_indent_labels = one_less_than_current 6 | csharp_using_directive_placement = outside_namespace:silent 7 | csharp_prefer_simple_using_statement = true:suggestion 8 | csharp_prefer_braces = true:silent 9 | csharp_style_namespace_declarations = block_scoped:silent 10 | csharp_style_prefer_method_group_conversion = true:silent 11 | csharp_style_prefer_top_level_statements = true:silent 12 | csharp_style_prefer_primary_constructors = true:suggestion 13 | csharp_style_expression_bodied_constructors = false:silent 14 | csharp_style_expression_bodied_operators = false:silent 15 | csharp_style_expression_bodied_properties = true:silent 16 | csharp_style_expression_bodied_indexers = true:silent 17 | csharp_style_expression_bodied_accessors = true:silent 18 | csharp_style_expression_bodied_lambdas = true:silent 19 | csharp_style_expression_bodied_local_functions = false:silent 20 | 21 | [*.{cs,vb}] 22 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 23 | tab_width = 4 24 | indent_size = 4 25 | end_of_line = crlf 26 | dotnet_style_coalesce_expression = true:suggestion 27 | dotnet_style_null_propagation = true:suggestion 28 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 29 | dotnet_style_prefer_auto_properties = true:silent 30 | [*.{cs,vb}] 31 | #### Naming styles #### 32 | 33 | # Naming rules 34 | 35 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion 36 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface 37 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i 38 | 39 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion 40 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types 41 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case 42 | 43 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion 44 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members 45 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case 46 | 47 | # Symbol specifications 48 | 49 | dotnet_naming_symbols.interface.applicable_kinds = interface 50 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 51 | dotnet_naming_symbols.interface.required_modifiers = 52 | 53 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum 54 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 55 | dotnet_naming_symbols.types.required_modifiers = 56 | 57 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 58 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 59 | dotnet_naming_symbols.non_field_members.required_modifiers = 60 | 61 | # Naming styles 62 | 63 | dotnet_naming_style.begins_with_i.required_prefix = I 64 | dotnet_naming_style.begins_with_i.required_suffix = 65 | dotnet_naming_style.begins_with_i.word_separator = 66 | dotnet_naming_style.begins_with_i.capitalization = pascal_case 67 | 68 | dotnet_naming_style.pascal_case.required_prefix = 69 | dotnet_naming_style.pascal_case.required_suffix = 70 | dotnet_naming_style.pascal_case.word_separator = 71 | dotnet_naming_style.pascal_case.capitalization = pascal_case 72 | 73 | dotnet_naming_style.pascal_case.required_prefix = 74 | dotnet_naming_style.pascal_case.required_suffix = 75 | dotnet_naming_style.pascal_case.word_separator = 76 | dotnet_naming_style.pascal_case.capitalization = pascal_case 77 | dotnet_style_object_initializer = true:suggestion 78 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "nuget" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | ignore: 13 | - dependency-name: "Microsoft.CodeAnalysis.Common" 14 | - dependency-name: "Microsoft.CodeAnalysis.CSharp" 15 | 16 | - package-ecosystem: "github-actions" # See documentation for possible values 17 | directory: "/" # Location of package manifests 18 | schedule: 19 | interval: "weekly" 20 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-core.yml: -------------------------------------------------------------------------------- 1 | name: .NET Core 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | # Sequence of patterns matched against refs/tags 7 | tags: 8 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 9 | paths-ignore: 10 | - 'README.md' 11 | pull_request: 12 | branches: [ master ] 13 | workflow_dispatch: 14 | 15 | defaults: 16 | run: 17 | shell: pwsh 18 | 19 | env: 20 | configuration: Release 21 | baseVersion: 1.3.0 22 | preRelease: true 23 | 24 | jobs: 25 | build: 26 | runs-on: windows-latest 27 | 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | 32 | - name: Set Version 33 | run: | 34 | if ("${{ github.ref }}".startsWith("refs/tags/v")) { 35 | $tagVersion = "${{ github.ref }}".substring(11) 36 | echo "buildVersion=$tagVersion.${{ github.run_number }}" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append 37 | echo "nugetVersion=$tagVersion" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append 38 | echo "preRelease=false" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append 39 | } else { 40 | echo "buildVersion=${{ env.baseVersion }}.${{ github.run_number }}" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append 41 | echo "nugetVersion=${{ env.baseVersion }}-ci${{ github.run_number }}" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append 42 | } 43 | 44 | - name: Setup .NET 45 | uses: actions/setup-dotnet@v4 46 | with: 47 | dotnet-version: | 48 | 8.x 49 | 9.x 50 | env: 51 | NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 52 | 53 | - name: Install dependencies 54 | run: dotnet restore 55 | 56 | - name: Build 57 | run: dotnet build -p:Version=${{ env.buildVersion }} -p:ContinuousIntegrationBuild=True --configuration ${{ env.configuration }} --no-restore 58 | 59 | - name: Minimize all windows 60 | run: (New-Object -ComObject "Shell.Application").minimizeall() 61 | 62 | - name: Test net8.0 63 | run: dotnet test --framework net8.0-windows --no-build --verbosity normal --configuration ${{ env.configuration }} --blame-crash --blame-crash-collect-always --blame-hang --blame-hang-timeout 5m 64 | 65 | - name: Test net9.0 66 | run: dotnet test --framework net9.0-windows --no-build --verbosity normal --configuration ${{ env.configuration }} --blame-crash --blame-crash-collect-always --blame-hang --blame-hang-timeout 5m 67 | 68 | - name: Test Logs 69 | if: ${{ always() }} 70 | uses: actions/upload-artifact@v4 71 | with: 72 | name: Test Logs 73 | path: ${{ github.workspace }}\XAMLTest.Tests\bin\${{ env.configuration }}\**\*.log 74 | 75 | - name: Upload Crash Dumps 76 | if: ${{ always() }} 77 | uses: actions/upload-artifact@v4 78 | with: 79 | name: Crash Dumps 80 | path: ${{ github.workspace }}\XAMLTest.Tests\TestResults\**\*.dmp 81 | 82 | - name: Upload Screenshots 83 | if: ${{ always() }} 84 | uses: actions/upload-artifact@v4 85 | with: 86 | name: Test Images 87 | path: ${{ github.workspace }}\XAMLTest.Tests\bin\${{ env.configuration }}\**\Screenshots 88 | 89 | - name: Pack 90 | run: dotnet pack -p:PackageVersion=${{ env.nugetVersion }} --configuration ${{ env.configuration }} --no-build 91 | 92 | - name: Upload Artifacts 93 | if: ${{ github.event_name != 'pull_request' }} 94 | uses: actions/upload-artifact@v4 95 | with: 96 | name: NuGet 97 | path: ${{ github.workspace }}\XAMLTest\bin\${{ env.configuration }}\XAMLTest.${{ env.nugetVersion }}*nupkg 98 | 99 | - name: Push NuGet 100 | if: ${{ github.event_name != 'pull_request' }} 101 | run: dotnet nuget push ${{ github.workspace }}\XAMLTest\bin\${{ env.configuration }}\XAMLTest.${{ env.nugetVersion }}.nupkg --source https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_API_KEY }} --skip-duplicate 102 | 103 | automerge: 104 | needs: build 105 | runs-on: ubuntu-latest 106 | 107 | permissions: 108 | pull-requests: write 109 | contents: write 110 | 111 | steps: 112 | - uses: fastify/github-action-merge-dependabot@v3.11.0 113 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | enable 4 | true 5 | 5 6 | 12.0 7 | 8 | 9 | 10 | true 11 | 12 | 13 | 14 | XAML UI Testing Library 15 | A library that allows unit tests to be written against WPF XAML. 16 | Copyright 2020 17 | MIT 18 | icon.png 19 | https://github.com/Keboo/XamlTest 20 | Keboo 21 | 22 | 23 | 24 | 25 | true 26 | true 27 | true 28 | true 29 | snupkg 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Images/Icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Keboo/XAMLTest/e72acf6709402d7987df622ae1a038d31b663c95/Images/Icon.ico -------------------------------------------------------------------------------- /Images/Icon.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Keboo/XAMLTest/e72acf6709402d7987df622ae1a038d31b663c95/Images/Icon.pdn -------------------------------------------------------------------------------- /Images/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Keboo/XAMLTest/e72acf6709402d7987df622ae1a038d31b663c95/Images/Icon.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kevin B 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XAMLTest 2 | [![Build](https://github.com/Keboo/XAMLTest/workflows/.NET%20Core/badge.svg)](https://github.com/Keboo/XAMLTest/actions/workflows/dotnet-core.yml) 3 | [![NuGet](https://img.shields.io/nuget/v/XAMLTest.svg?label=NuGet)](https://www.nuget.org/packages/XAMLTest/) 4 | 5 | ## Description 6 | XAMLTest is a testing framework designed to allow WPF developers a way to directly "unit test" their XAML. In many ways this library is similar to a UI testing library, with some key differences. Rather than leveraging accessibility or automation APIs, this library is designed to load up a small piece of XAML and provide a simple API to make assertions about the run-time state of the UI. This library is NOT DESIGNED to replace UI testing of WPF apps. Instead, it serves as a helpful tool for WPF library and theme developers to have a mechanism to effectively write tests for their XAML. 7 | 8 | ## Versioning 9 | After the 1.0.0 release, this library will follow [Semantic Versioning](https://semver.org/). However, in the interim while developing the initial release all 0.x.x versions should be considered alpha releases and all APIs are subject to change without notice. 10 | 11 | ## Samples? 12 | See the tests (XAMLTests.Tests) for samples of how tests can be setup. [Material Design In XAML Toolkit](https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit) is also leveraging this library for the purpose of testing its various styles and templates. You can see samples of its tests [here](https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit/tree/master/MaterialDesignThemes.UITests). 13 | -------------------------------------------------------------------------------- /XAMLTest.Generator/IsExternalInit.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace System.Runtime.CompilerServices; 4 | 5 | /// 6 | /// Reserved to be used by the compiler for tracking metadata. 7 | /// This class should not be used by developers in source code. 8 | /// 9 | [EditorBrowsable(EditorBrowsableState.Never)] 10 | public static class IsExternalInit 11 | { } 12 | -------------------------------------------------------------------------------- /XAMLTest.Generator/XAMLTest.Generator.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | false 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /XAMLTest.TestApp/App.xaml: -------------------------------------------------------------------------------- 1 |  8 | 9 | 10 | 11 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /XAMLTest.TestApp/App.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace XAMLTest.TestApp; 2 | 3 | /// 4 | /// Interaction logic for App.xaml 5 | /// 6 | public partial class App : Application 7 | { 8 | } 9 | -------------------------------------------------------------------------------- /XAMLTest.TestApp/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo( 4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 5 | //(used if a resource is not found in the page, 6 | // or application resource dictionaries) 7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 8 | //(used if a resource is not found in the page, 9 | // app, or any theme specific resource dictionaries) 10 | )] 11 | -------------------------------------------------------------------------------- /XAMLTest.TestApp/MainWindow.xaml: -------------------------------------------------------------------------------- 1 |  12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /XAMLTest.TestApp/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace XAMLTest.TestApp; 2 | 3 | /// 4 | /// Interaction logic for MainWindow.xaml 5 | /// 6 | public partial class MainWindow : Window 7 | { 8 | public MainWindow() 9 | { 10 | InitializeComponent(); 11 | Tag = Environment.CommandLine; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /XAMLTest.TestApp/XAMLTest.TestApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WinExe 5 | net8.0-windows;net9.0-windows 6 | true 7 | false 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /XAMLTest.Tests/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | [assembly: DoNotParallelize] 2 | -------------------------------------------------------------------------------- /XAMLTest.Tests/ColorMixinsTests.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Media; 2 | 3 | namespace XamlTest.Tests; 4 | 5 | [TestClass] 6 | public class ColorMixinsTests 7 | { 8 | [TestMethod] 9 | public void ContrastRatio() 10 | { 11 | float ratio = Colors.Black.ContrastRatio(Colors.White); 12 | 13 | //Actual value should be 21, allowing for floating point rounding errors 14 | Assert.IsTrue(ratio >= 20.9); 15 | } 16 | 17 | [TestMethod] 18 | public void FlattenOnto_ReturnsForegroundWhenItIsOpaque() 19 | { 20 | Color foreground = Colors.Red; 21 | Color background = Colors.Blue; 22 | 23 | Color flattened = foreground.FlattenOnto(background); 24 | 25 | Assert.AreEqual(Colors.Red, flattened); 26 | } 27 | 28 | [TestMethod] 29 | public void FlattenOnto_ReturnsMergedColorWhenForegroundIsTransparent() 30 | { 31 | Color foreground = Color.FromArgb(0x88, 0, 0, 0); 32 | Color background = Colors.White; 33 | 34 | Color flattened = foreground.FlattenOnto(background); 35 | 36 | Color expected = Color.FromRgb(0x76, 0x76, 0x76); 37 | Assert.AreEqual(expected, flattened); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /XAMLTest.Tests/Generated/ContextMenuGeneratedExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest.Tests.Generated; 2 | 3 | partial class ContextMenuGeneratedExtensionsTests 4 | { 5 | static partial void OnClassInitialize() 6 | { 7 | GetWindowContent = x => 8 | { 9 | return @$" 10 | 11 | {x} 12 | "; 13 | }; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /XAMLTest.Tests/Generated/TooltipGeneratedExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest.Tests.Generated; 2 | 3 | partial class ToolTipGeneratedExtensionsTests 4 | { 5 | static partial void OnClassInitialize() 6 | { 7 | GetWindowContent = x => 8 | { 9 | return @$" 10 | 11 | {x} 12 | "; 13 | }; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /XAMLTest.Tests/Generated/WindowGeneratedExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using System.Windows; 3 | 4 | namespace XamlTest.Tests.Generated; 5 | 6 | partial class WindowGeneratedExtensionsTests 7 | { 8 | static partial void OnClassInitialize() 9 | { 10 | GetWindowContent = x => ""; 11 | GetElement = _ => Task.FromResult>(Window); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /XAMLTest.Tests/GeneratorTests.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Controls; 2 | using XamlTest; 3 | using XamlTest.Tests; 4 | 5 | [assembly: GenerateHelpers(typeof(IntButton))] 6 | [assembly: GenerateHelpers(typeof(DecimalButton))] 7 | 8 | namespace XamlTest.Tests; 9 | 10 | public class GenericBase : Button 11 | { 12 | public T SomeValue 13 | { 14 | get => (T)GetValue(SomeValueProperty); 15 | set => SetValue(SomeValueProperty, value); 16 | } 17 | 18 | // Using a DependencyProperty as the backing store for Value. This enables animation, styling, binding, etc... 19 | public static readonly DependencyProperty SomeValueProperty = 20 | DependencyProperty.Register("SomeValue", typeof(T), typeof(GenericBase<>), new PropertyMetadata(default(T))); 21 | } 22 | 23 | public class IntButton : GenericBase; 24 | 25 | public class DecimalButton : GenericBase; 26 | 27 | [TestClass] 28 | public class GeneratorTests 29 | { 30 | [TestMethod] 31 | [Ignore("This test is used to verify that the generator is working correctly; so we only need to compile")] 32 | public void CanAccessGeneratedGenericBaseClassExtensions() 33 | { 34 | IVisualElement intButton = default!; 35 | IVisualElement decimalButton = default!; 36 | 37 | _ = intButton.GetSomeValue(); 38 | _ = decimalButton.GetSomeValue(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /XAMLTest.Tests/GetCoordinatesTests.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Controls; 2 | 3 | namespace XamlTest.Tests; 4 | 5 | [TestClass] 6 | public class GetCoordinatesTests 7 | { 8 | [NotNull] 9 | private static IApp? App { get; set; } 10 | 11 | [NotNull] 12 | private static IWindow? Window { get; set; } 13 | 14 | [ClassInitialize] 15 | public static async Task ClassInitialize(TestContext context) 16 | { 17 | App = await XamlTest.App.StartRemote(logMessage: context.WriteLine); 18 | 19 | await App.InitializeWithDefaults(Assembly.GetExecutingAssembly().Location); 20 | 21 | Window = await App.CreateWindowWithContent(@""); 22 | } 23 | 24 | [ClassCleanup(ClassCleanupBehavior.EndOfClass)] 25 | public static async Task ClassCleanup() 26 | { 27 | if (App is { } app) 28 | { 29 | await app.DisposeAsync(); 30 | App = null; 31 | } 32 | } 33 | 34 | [TestMethod] 35 | public async Task OnGetCoordinate_ReturnsScreenCoordinatesOfElement() 36 | { 37 | IVisualElement element = await Window.SetXamlContent(@""); 39 | 40 | Rect initialCoordinates = await element.GetCoordinates(); 41 | await element.SetWidth(90); 42 | await element.SetHeight(80); 43 | await element.SetMargin(new Thickness(30)); 44 | 45 | Rect newCoordinates = await element.GetCoordinates(); 46 | Assert.AreEqual(3.0, Math.Round(newCoordinates.Width / initialCoordinates.Width)); 47 | Assert.AreEqual(2.0, Math.Round(newCoordinates.Height / initialCoordinates.Height)); 48 | Assert.AreEqual(initialCoordinates.Width, newCoordinates.Left - initialCoordinates.Left); 49 | Assert.AreEqual(initialCoordinates.Width, newCoordinates.Top - initialCoordinates.Top); 50 | } 51 | 52 | [TestMethod] 53 | public async Task OnGetCoordinate_ReturnsFractionalCoordinatesOfElement() 54 | { 55 | DpiScale scale = await Window.GetScale(); 56 | IVisualElement element = await Window.SetXamlContent(@""); 58 | 59 | //38.375 60 | Rect initialCoordinates = await element.GetCoordinates(); 61 | await element.SetWidth(await element.GetWidth() + 0.7); 62 | await element.SetHeight(await element.GetHeight() + 0.3); 63 | await element.SetMargin(new Thickness(0.1)); 64 | 65 | Rect newCoordinates = await element.GetCoordinates(); 66 | Assert.AreEqual(initialCoordinates.Width + (0.7 * scale.DpiScaleX), newCoordinates.Width, 0.00001); 67 | Assert.AreEqual(initialCoordinates.Height + (0.3 * scale.DpiScaleY), newCoordinates.Height, 0.00001); 68 | Assert.AreEqual(0.1 * scale.DpiScaleX, Math.Round(newCoordinates.Left - initialCoordinates.Left, 5), 0.00001); 69 | Assert.AreEqual(0.1 * scale.DpiScaleY, Math.Round(newCoordinates.Top - initialCoordinates.Top, 5), 0.00001); 70 | } 71 | 72 | [TestMethod] 73 | public async Task OnGetCoordinate_ReturnsRotatedElementLocation() 74 | { 75 | DpiScale scale = await Window.GetScale(); 76 | 77 | IVisualElement element = await Window.SetXamlContent(@" 78 | 79 | 80 | 81 | 82 | 83 | "); 84 | App.LogMessage("Before"); 85 | Rect coordinates = await element.GetCoordinates(); 86 | App.LogMessage("After"); 87 | 88 | Assert.AreEqual(40 * scale.DpiScaleX, coordinates.Width, 0.00001); 89 | App.LogMessage("Assert1"); 90 | Assert.AreEqual(30 * scale.DpiScaleY, coordinates.Height, 0.00001); 91 | App.LogMessage("Assert2"); 92 | } 93 | } -------------------------------------------------------------------------------- /XAMLTest.Tests/GetEffectiveBackgroundTests.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Media; 2 | 3 | namespace XamlTest.Tests; 4 | 5 | [TestClass] 6 | public class GetEffectiveBackgroundTests 7 | { 8 | [NotNull] 9 | private static IApp? App { get; set; } 10 | 11 | [NotNull] 12 | private static IWindow? Window { get; set; } 13 | 14 | [ClassInitialize] 15 | public static async Task ClassInitialize(TestContext context) 16 | { 17 | App = await XamlTest.App.StartRemote(logMessage: context.WriteLine); 18 | 19 | await App.InitializeWithDefaults(Assembly.GetExecutingAssembly().Location); 20 | 21 | Window = await App.CreateWindowWithContent(@""); 22 | } 23 | 24 | [ClassCleanup(ClassCleanupBehavior.EndOfClass)] 25 | public static async Task TestCleanup() 26 | { 27 | if (App is { } app) 28 | { 29 | await app.DisposeAsync(); 30 | App = null; 31 | } 32 | } 33 | 34 | [TestMethod] 35 | public async Task OnGetEffectiveBackground_ReturnsFirstOpaqueColor() 36 | { 37 | IVisualElement element = await Window.SetXamlContent(@""); 38 | await Window.SetBackgroundColor(Colors.Red); 39 | 40 | Color background = await element.GetEffectiveBackground(); 41 | 42 | Assert.AreEqual(Colors.Red, background); 43 | } 44 | 45 | [TestMethod] 46 | public async Task OnGetEffectiveBackground_ReturnsMergingOfTransparentColors() 47 | { 48 | var backgroundParent = Colors.Blue; 49 | var backgroundChild = Color.FromArgb(0xDD, 0, 0, 0); 50 | await Window.SetXamlContent($@" 51 | 52 | 53 | "); 54 | await Window.SetBackgroundColor(Colors.Red); 55 | 56 | IVisualElement element = await Window.GetElement("MyBorder"); 57 | 58 | Color background = await element.GetEffectiveBackground(); 59 | 60 | var expected = backgroundChild.FlattenOnto(backgroundParent); 61 | Assert.AreEqual(expected, background); 62 | } 63 | 64 | [TestMethod] 65 | public async Task OnGetEffectiveBackground_ReturnsOpaquePanelColor() 66 | { 67 | await Window.SetXamlContent(@" 68 | 69 | 70 | 71 | "); 72 | await Window.SetBackgroundColor(Colors.Blue); 73 | 74 | IVisualElement element = await Window.GetElement("/TextBlock"); 75 | 76 | Color background = await element.GetEffectiveBackground(); 77 | 78 | Assert.AreEqual(Colors.Red, background); 79 | } 80 | 81 | [TestMethod] 82 | public async Task OnGetEffectiveBackground_StopsProcessingAtDefinedParent() 83 | { 84 | await Window.SetXamlContent(@" 85 | 86 | 87 | 88 | "); 89 | await Window.SetBackgroundColor(Colors.Blue); 90 | 91 | IVisualElement child = await Window.GetElement("/TextBlock"); 92 | IVisualElement parent = await Window.GetElement("/Grid"); 93 | 94 | Color background = await child.GetEffectiveBackground(parent); 95 | 96 | Assert.AreEqual(Color.FromArgb(0xDD, 0xFF, 0x00, 0x00), background); 97 | } 98 | 99 | [TestMethod] 100 | public async Task OnGetEffectiveBackground_AppliesOpacityFromParents() 101 | { 102 | await Window.SetXamlContent(@" 103 | 104 | 105 | 106 | 107 | 108 | "); 109 | await Window.SetBackgroundColor(Colors.Lime); 110 | 111 | IVisualElement child = await Window.GetElement("/TextBlock"); 112 | IVisualElement parent = await Window.GetElement("BlueGrid"); 113 | 114 | Color background = await child.GetEffectiveBackground(parent); 115 | 116 | Assert.AreEqual(Color.FromArgb(127, 0x00, 0x00, 0xFF), background); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /XAMLTest.Tests/GetResourceTests.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Controls; 2 | using System.Windows.Media; 3 | 4 | namespace XamlTest.Tests; 5 | 6 | [TestClass] 7 | public class GetResourceTests 8 | { 9 | [NotNull] 10 | private static IApp? App { get; set; } 11 | 12 | [NotNull] 13 | private static IWindow? Window { get; set; } 14 | 15 | [NotNull] 16 | private static IVisualElement? Grid { get; set; } 17 | 18 | [ClassInitialize] 19 | public static async Task ClassInitialize(TestContext context) 20 | { 21 | App = await XamlTest.App.StartRemote(logMessage: context.WriteLine); 22 | 23 | await App.InitializeWithResources(@" 24 | Red 25 | ", 26 | Assembly.GetExecutingAssembly().Location); 27 | 28 | Window = await App.CreateWindowWithContent(@" 29 | 30 | Red 31 | 32 | "); 33 | 34 | Grid = await Window.GetElement("MyGrid"); 35 | } 36 | 37 | [ClassCleanup(ClassCleanupBehavior.EndOfClass)] 38 | public static async Task TestCleanup() 39 | { 40 | if (App is { } app) 41 | { 42 | await app.DisposeAsync(); 43 | App = null; 44 | } 45 | } 46 | 47 | [TestMethod] 48 | [ExpectedException(typeof(XamlTestException))] 49 | public async Task OnAppGetResource_ThrowsExceptionWhenNotFound() 50 | { 51 | await App.GetResource("NotFound"); 52 | } 53 | 54 | [TestMethod] 55 | public async Task OnAppGetResource_ReturnsFoundResource() 56 | { 57 | IResource resource = await App.GetResource("TestColor"); 58 | 59 | Assert.AreEqual("TestColor", resource.Key); 60 | Assert.AreEqual(Colors.Red, resource.GetAs()); 61 | Assert.AreEqual(typeof(Color).AssemblyQualifiedName, resource.ValueType); 62 | } 63 | 64 | [TestMethod] 65 | public async Task OnAppGetResource_ReturnsColorForBrushResource() 66 | { 67 | IResource resource = await App.GetResource("TestBrush"); 68 | 69 | Assert.AreEqual("TestBrush", resource.Key); 70 | Color? color = resource.GetAs(); 71 | Assert.AreEqual(Colors.Red, color); 72 | } 73 | 74 | [TestMethod] 75 | [ExpectedException(typeof(XamlTestException))] 76 | public async Task OnVisualElementGetResource_ThrowsExceptionWhenNotFound() 77 | { 78 | await Grid.GetResource("NotFound"); 79 | } 80 | 81 | [TestMethod] 82 | public async Task OnVisualElementGetResource_ReturnsFoundResource() 83 | { 84 | IResource resource = await Grid.GetResource("GridColorResource"); 85 | 86 | Assert.AreEqual("GridColorResource", resource.Key); 87 | Assert.AreEqual(Colors.Red, resource.GetAs()); 88 | Assert.AreEqual(typeof(Color).AssemblyQualifiedName, resource.ValueType); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /XAMLTest.Tests/HighlightTests.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Controls; 2 | using System.Windows.Documents; 3 | using System.Windows.Media; 4 | 5 | namespace XamlTest.Tests; 6 | 7 | [TestClass] 8 | public class HighlightTests 9 | { 10 | [NotNull] 11 | private static IApp? App { get; set; } 12 | 13 | [NotNull] 14 | private static IWindow? Window { get; set; } 15 | 16 | [ClassInitialize] 17 | public static async Task ClassInitialize(TestContext context) 18 | { 19 | App = await XamlTest.App.StartRemote(logMessage: context.WriteLine); 20 | 21 | await App.InitializeWithDefaults(Assembly.GetExecutingAssembly().Location); 22 | 23 | Window = await App.CreateWindowWithContent(@""); 24 | } 25 | 26 | [ClassCleanup(ClassCleanupBehavior.EndOfClass)] 27 | public static async Task TestCleanup() 28 | { 29 | if (App is { } app) 30 | { 31 | await app.DisposeAsync(); 32 | App = null; 33 | } 34 | } 35 | 36 | [TestMethod] 37 | public async Task OnHighlight_WithDefaults_AddsHighlightAdorner() 38 | { 39 | await Window.SetXamlContent(@""); 40 | 41 | IVisualElement grid = await Window.GetElement("MyGrid"); 42 | 43 | await grid.Highlight(); 44 | 45 | IVisualElement adorner = await grid.GetElement("/SelectionAdorner"); 46 | 47 | Assert.IsNotNull(adorner); 48 | Assert.AreEqual(HighlightConfig.DefaultBorderColor, await adorner.GetProperty("BorderBrush")); 49 | Assert.AreEqual(HighlightConfig.DefaultBorderWidth, await adorner.GetProperty("BorderThickness")); 50 | Assert.AreEqual(HighlightConfig.DefaultOverlayColor, await adorner.GetProperty("OverlayBrush")); 51 | } 52 | 53 | [TestMethod] 54 | public async Task OnHighlight_WithCustomValues_AddsHighlightAdorner() 55 | { 56 | await Window.SetXamlContent(@""); 57 | 58 | IVisualElement grid = await Window.GetElement("MyGrid"); 59 | 60 | await grid.Highlight(new HighlightConfig() 61 | { 62 | BorderBrush = new SolidColorBrush(Colors.Blue), 63 | BorderThickness = 3, 64 | OverlayBrush = new SolidColorBrush(Colors.Green) 65 | }); 66 | 67 | IVisualElement adorner = await grid.GetElement("/SelectionAdorner"); 68 | 69 | Assert.IsNotNull(adorner); 70 | Assert.AreEqual(Colors.Blue, await adorner.GetProperty("BorderBrush")); 71 | Assert.AreEqual(3.0, await adorner.GetProperty("BorderThickness")); 72 | Assert.AreEqual(Colors.Green, await adorner.GetProperty("OverlayBrush")); 73 | } 74 | 75 | [TestMethod] 76 | public async Task OnHighlight_WithExistingHighlight_UpdatesHighlight() 77 | { 78 | await Window.SetXamlContent(@""); 79 | 80 | IVisualElement grid = await Window.GetElement("MyGrid"); 81 | 82 | await grid.Highlight(); 83 | await grid.Highlight(new HighlightConfig() 84 | { 85 | BorderBrush = new SolidColorBrush(Colors.Blue), 86 | BorderThickness = 3, 87 | OverlayBrush = new SolidColorBrush(Colors.Green) 88 | }); 89 | 90 | IVisualElement adorner = await grid.GetElement("/SelectionAdorner"); 91 | 92 | Assert.IsNotNull(adorner); 93 | Assert.AreEqual(Colors.Blue, await adorner.GetProperty("BorderBrush")); 94 | Assert.AreEqual(3.0, await adorner.GetProperty("BorderThickness")); 95 | Assert.AreEqual(Colors.Green, await adorner.GetProperty("OverlayBrush")); 96 | } 97 | 98 | [TestMethod] 99 | public async Task OnClearHighlight_WithHighlight_ClearsAdorner() 100 | { 101 | await Window.SetXamlContent(@""); 102 | 103 | IVisualElement grid = await Window.GetElement("MyGrid"); 104 | 105 | await grid.Highlight(); 106 | await grid.ClearHighlight(); 107 | 108 | var ex = await Assert.ThrowsExceptionAsync(() => grid.GetElement("/SelectionAdorner")); 109 | 110 | Assert.IsTrue(ex.Message.Contains("Failed to find child element of type 'SelectionAdorner'")); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /XAMLTest.Tests/MouseInputTests.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest.Tests; 2 | 3 | [TestClass] 4 | public class MouseInputTests 5 | { 6 | [TestMethod] 7 | public void CanRetrieveMouseDoubleClickTime() 8 | { 9 | Assert.IsTrue(MouseInput.GetDoubleClickTime > TimeSpan.Zero); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /XAMLTest.Tests/PositionTests.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Controls; 2 | using XamlTest.Tests.TestControls; 3 | 4 | namespace XamlTest.Tests; 5 | 6 | [TestClass] 7 | public class PositionTests 8 | { 9 | [NotNull] 10 | private static IApp? App { get; set; } 11 | 12 | [NotNull] 13 | public static IVisualElement? UserControl { get; set; } 14 | 15 | [NotNull] 16 | public static IVisualElement? PositionTextElement { get; set; } 17 | 18 | [ClassInitialize] 19 | public static async Task ClassInitialize(TestContext context) 20 | { 21 | App = await XamlTest.App.StartRemote(logMessage: context.WriteLine); 22 | 23 | await App.InitializeWithDefaults(Assembly.GetExecutingAssembly().Location); 24 | 25 | var window = await App.CreateWindowWithUserControl(windowSize: new(400, 300)); 26 | UserControl = await window.GetElement("/MouseClickPositions"); 27 | PositionTextElement = await UserControl.GetElement("ClickLocation"); 28 | } 29 | 30 | [ClassCleanup(ClassCleanupBehavior.EndOfClass)] 31 | public static async Task TestCleanup() 32 | { 33 | if (App is { } app) 34 | { 35 | await app.DisposeAsync(); 36 | App = null; 37 | } 38 | } 39 | 40 | [TestMethod] 41 | public async Task CanClick_Center() 42 | { 43 | Rect coordinates = await UserControl.GetCoordinates(); 44 | Point clickPosition = await UserControl.LeftClick(Position.Center); 45 | 46 | Assert.AreEqual(coordinates.Left + coordinates.Width / 2.0, clickPosition.X, 2.0); 47 | Assert.AreEqual(coordinates.Top + coordinates.Height / 2.0, clickPosition.Y, 2.0); 48 | } 49 | 50 | [TestMethod] 51 | public async Task CanClick_TopLeft() 52 | { 53 | Rect coordinates = await UserControl.GetCoordinates(); 54 | Point clickPosition = await UserControl.LeftClick(Position.TopLeft); 55 | 56 | Assert.AreEqual(coordinates.Left, clickPosition.X, 2.0); 57 | Assert.AreEqual(coordinates.Top, clickPosition.Y, 2.0); 58 | } 59 | 60 | [TestMethod] 61 | public async Task CanClick_TopCenter() 62 | { 63 | Rect coordinates = await UserControl.GetCoordinates(); 64 | Point clickPosition = await UserControl.LeftClick(Position.TopCenter); 65 | 66 | Assert.AreEqual(coordinates.Left + coordinates.Width / 2.0, clickPosition.X, 2.0); 67 | Assert.AreEqual(coordinates.Top, clickPosition.Y, 2.0); 68 | } 69 | 70 | [TestMethod] 71 | public async Task CanClick_TopRight() 72 | { 73 | Rect coordinates = await UserControl.GetCoordinates(); 74 | Point clickPosition = await UserControl.LeftClick(Position.TopRight); 75 | 76 | Assert.AreEqual(coordinates.Right, clickPosition.X, 2.0); 77 | Assert.AreEqual(coordinates.Top, clickPosition.Y, 2.0); 78 | } 79 | 80 | [TestMethod] 81 | public async Task CanClick_RightCenter() 82 | { 83 | Rect coordinates = await UserControl.GetCoordinates(); 84 | Point clickPosition = await UserControl.LeftClick(Position.RightCenter); 85 | 86 | Assert.AreEqual(coordinates.Right, clickPosition.X, 2.0); 87 | Assert.AreEqual(coordinates.Top + coordinates.Height / 2.0, clickPosition.Y, 2.0); 88 | } 89 | 90 | [TestMethod] 91 | public async Task CanClick_BottomRight() 92 | { 93 | Rect coordinates = await UserControl.GetCoordinates(); 94 | Point clickPosition = await UserControl.LeftClick(Position.BottomRight); 95 | 96 | Assert.AreEqual(coordinates.Right, clickPosition.X, 2.0); 97 | Assert.AreEqual(coordinates.Bottom, clickPosition.Y, 2.0); 98 | } 99 | 100 | [TestMethod] 101 | public async Task CanClick_BottomCenter() 102 | { 103 | Rect coordinates = await UserControl.GetCoordinates(); 104 | Point clickPosition = await UserControl.LeftClick(Position.BottomCenter); 105 | 106 | Assert.AreEqual(coordinates.Left + coordinates.Width / 2.0, clickPosition.X, 2.0); 107 | Assert.AreEqual(coordinates.Bottom, clickPosition.Y, 2.0); 108 | } 109 | 110 | [TestMethod] 111 | public async Task CanClick_BottomLeft() 112 | { 113 | Rect coordinates = await UserControl.GetCoordinates(); 114 | Point clickPosition = await UserControl.LeftClick(Position.BottomLeft); 115 | 116 | Assert.AreEqual(coordinates.Left, clickPosition.X, 2.0); 117 | Assert.AreEqual(coordinates.Bottom, clickPosition.Y, 2.0); 118 | } 119 | 120 | [TestMethod] 121 | public async Task CanClick_LeftCenter() 122 | { 123 | Rect coordinates = await UserControl.GetCoordinates(); 124 | Point clickPosition = await UserControl.LeftClick(Position.LeftCenter); 125 | 126 | Assert.AreEqual(coordinates.Left, clickPosition.X, 2.0); 127 | Assert.AreEqual(coordinates.Top + coordinates.Height / 2.0, clickPosition.Y, 2.0); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /XAMLTest.Tests/SerializerTests.cs: -------------------------------------------------------------------------------- 1 | using XamlTest.Transport; 2 | 3 | namespace XamlTest.Tests; 4 | 5 | [TestClass] 6 | public class SerializerTests 7 | { 8 | [NotNull] 9 | private static IApp? App { get; set; } 10 | 11 | [NotNull] 12 | private static IWindow? Window { get; set; } 13 | 14 | [ClassInitialize] 15 | public static async Task ClassInitialize(TestContext context) 16 | { 17 | App = await XamlTest.App.StartRemote(logMessage: context.WriteLine); 18 | 19 | await App.InitializeWithDefaults(Assembly.GetExecutingAssembly().Location); 20 | 21 | Window = await App.CreateWindowWithContent("", title: "Test Window Title"); 22 | } 23 | 24 | [ClassCleanup(ClassCleanupBehavior.EndOfClass)] 25 | public static void TestCleanup() 26 | { 27 | App.Dispose(); 28 | } 29 | 30 | [TestMethod] 31 | public async Task OnRegisterSerializer_RegistersCustomSerializer() 32 | { 33 | await using var recorder = new TestRecorder(App); 34 | 35 | await App.RegisterSerializer(); 36 | 37 | Assert.AreEqual("In-Test Window Title-Out", await Window.GetTitle()); 38 | 39 | recorder.Success(); 40 | } 41 | 42 | [TestMethod] 43 | public async Task OnGetSerializers_ReturnsDefaultSerializers() 44 | { 45 | var serializers = (await App.GetSerializers()).ToList(); 46 | 47 | int brushSerializerIndex = serializers.FindIndex(x => x is BrushSerializer); 48 | int charSerializerIndex = serializers.FindIndex(x => x is CharSerializer); 49 | int gridSerializerIndex = serializers.FindIndex(x => x is GridSerializer); 50 | int secureStringSerializerIndex = serializers.FindIndex(x => x is SecureStringSerializer); 51 | int defaultSerializerIndex = serializers.FindIndex(x => x is DefaultSerializer); 52 | 53 | Assert.IsTrue(brushSerializerIndex < charSerializerIndex); 54 | Assert.IsTrue(charSerializerIndex < gridSerializerIndex); 55 | Assert.IsTrue(gridSerializerIndex < secureStringSerializerIndex); 56 | Assert.IsTrue(secureStringSerializerIndex < defaultSerializerIndex); 57 | Assert.AreEqual(serializers.Count - 1, defaultSerializerIndex); 58 | } 59 | 60 | [TestMethod] 61 | public async Task OnGetSerializers_IncludesCustomSerializers() 62 | { 63 | var initialSerializersCount = (await App.GetSerializers()).Count; 64 | 65 | await App.RegisterSerializer(1); 66 | 67 | var serializers = await App.GetSerializers(); 68 | 69 | Assert.AreEqual(initialSerializersCount + 1, serializers.Count); 70 | Assert.IsInstanceOfType(serializers[1], typeof(CustomSerializer)); 71 | } 72 | 73 | private class CustomSerializer : ISerializer 74 | { 75 | public bool CanSerialize(Type type, ISerializer rootSerializer) => type == typeof(string); 76 | 77 | public object? Deserialize(Type type, string value, ISerializer rootSerializer) 78 | { 79 | return $"{value}-Out"; 80 | } 81 | 82 | public string Serialize(Type type, object? value, ISerializer rootSerializer) 83 | { 84 | return $"In-{value}"; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /XAMLTest.Tests/Simulators/App.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest.Tests.Simulators; 2 | 3 | public class App : IApp 4 | { 5 | public Task CreateWindow(string xaml) 6 | { 7 | throw new NotImplementedException(); 8 | } 9 | 10 | public Task CreateWindow() where TWindow : Window 11 | { 12 | throw new NotImplementedException(); 13 | } 14 | 15 | public void Dispose() 16 | { } 17 | 18 | private static ValueTask Completed { get; } = new(); 19 | 20 | public IList DefaultXmlNamespaces => throw new NotImplementedException(); 21 | 22 | public ValueTask DisposeAsync() => Completed; 23 | 24 | public Task GetMainWindow() 25 | { 26 | throw new NotImplementedException(); 27 | } 28 | 29 | public Task GetResource(string key) 30 | { 31 | throw new NotImplementedException(); 32 | } 33 | 34 | public Task GetScreenshot() 35 | => Task.FromResult(new Image()); 36 | public Task> GetSerializers() => throw new NotImplementedException(); 37 | 38 | public Task> GetWindows() 39 | { 40 | throw new NotImplementedException(); 41 | } 42 | 43 | public Task Initialize(string applicationResourceXaml, params string[] assemblies) 44 | { 45 | throw new NotImplementedException(); 46 | } 47 | 48 | public Task RegisterSerializer(int insertIndex = 0) where T : ISerializer, new() => throw new NotImplementedException(); 49 | public void LogMessage(string message) => throw new NotImplementedException(); 50 | public Task RemoteExecute(Delegate @delegate, object?[] parameters) => throw new NotImplementedException(); 51 | } 52 | -------------------------------------------------------------------------------- /XAMLTest.Tests/Simulators/Image.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | 4 | namespace XamlTest.Tests.Simulators; 5 | 6 | public class Image : IImage 7 | { 8 | public Task Save(Stream stream) => Task.CompletedTask; 9 | } 10 | -------------------------------------------------------------------------------- /XAMLTest.Tests/TestControls/MouseClickPositions.xaml: -------------------------------------------------------------------------------- 1 |  11 | 12 | 13 | -------------------------------------------------------------------------------- /XAMLTest.Tests/TestControls/MouseClickPositions.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Input; 3 | 4 | namespace XamlTest.Tests.TestControls; 5 | 6 | /// 7 | /// Interaction logic for MouseClickPositions.xaml 8 | /// 9 | public partial class MouseClickPositions 10 | { 11 | public MouseClickPositions() 12 | { 13 | InitializeComponent(); 14 | } 15 | 16 | private void UserControl_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) 17 | { 18 | Point p = e.GetPosition(this); 19 | ClickLocation.Text = $"{(int)p.X}x{(int)p.Y}"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /XAMLTest.Tests/TestControls/TestWindow.xaml: -------------------------------------------------------------------------------- 1 |  9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /XAMLTest.Tests/TestControls/TestWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace XamlTest.Tests.TestControls; 4 | 5 | /// 6 | /// Interaction logic for TestWindow.xaml 7 | /// 8 | public partial class TestWindow : Window 9 | { 10 | public TestWindow() 11 | { 12 | InitializeComponent(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /XAMLTest.Tests/TestControls/TextBlock_AttachedProperty.xaml: -------------------------------------------------------------------------------- 1 |  9 | 10 | 11 | -------------------------------------------------------------------------------- /XAMLTest.Tests/TestControls/TextBlock_AttachedProperty.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | 4 | namespace XamlTest.Tests.TestControls; 5 | 6 | /// 7 | /// Interaction logic for TextBlock_AttachedProperty.xaml 8 | /// 9 | public partial class TextBlock_AttachedProperty : UserControl 10 | { 11 | public static string GetMyCustomProperty(DependencyObject obj) 12 | { 13 | return (string)obj.GetValue(MyCustomPropertyProperty); 14 | } 15 | 16 | public static void SetMyCustomProperty(DependencyObject obj, string value) 17 | { 18 | obj.SetValue(MyCustomPropertyProperty, value); 19 | } 20 | 21 | public static readonly DependencyProperty MyCustomPropertyProperty = 22 | DependencyProperty.RegisterAttached("MyCustomProperty", typeof(string), 23 | typeof(TextBlock_AttachedProperty), new PropertyMetadata("Foo")); 24 | 25 | public TextBlock_AttachedProperty() 26 | { 27 | InitializeComponent(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /XAMLTest.Tests/TestControls/TextBox_ValidationError.xaml: -------------------------------------------------------------------------------- 1 |  9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /XAMLTest.Tests/TestControls/TextBox_ValidationError.xaml.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | using System.Windows.Controls; 3 | 4 | namespace XamlTest.Tests.TestControls; 5 | 6 | /// 7 | /// Interaction logic for TextBox_ValidationError.xaml 8 | /// 9 | public partial class TextBox_ValidationError : UserControl 10 | { 11 | public TextBox_ValidationError() 12 | { 13 | InitializeComponent(); 14 | DataContext = new ViewModel(); 15 | } 16 | 17 | public class ViewModel : ObservableObject 18 | { 19 | private string? _name; 20 | public string? Name 21 | { 22 | get => _name; 23 | set => SetProperty(ref _name, value); 24 | } 25 | } 26 | } 27 | 28 | public class NotEmptyValidationRule : ValidationRule 29 | { 30 | public override ValidationResult Validate(object value, CultureInfo cultureInfo) 31 | { 32 | return string.IsNullOrWhiteSpace((value ?? "").ToString()) 33 | ? new ValidationResult(false, "Field is required.") 34 | : ValidationResult.ValidResult; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /XAMLTest.Tests/TestRecorderTests.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace XamlTest.Tests; 4 | 5 | [TestClass] 6 | public class TestRecorderTests 7 | { 8 | [NotNull] 9 | public TestContext? TestContext { get; set; } 10 | 11 | [ClassInitialize] 12 | public static void ClassInit(TestContext _) 13 | { 14 | foreach (var file in new TestRecorder(new Simulators.App()).EnumerateScreenshots()) 15 | { 16 | File.Delete(file); 17 | } 18 | } 19 | 20 | [TestCleanup] 21 | public void ClassCleanup() 22 | { 23 | foreach (var file in new TestRecorder(new Simulators.App()).EnumerateScreenshots()) 24 | { 25 | File.Delete(file); 26 | } 27 | } 28 | 29 | [TestMethod] 30 | public async Task SaveScreenshot_SavesImage() 31 | { 32 | await using IApp app = new Simulators.App(); 33 | await using TestRecorder testRecorder = new(app); 34 | 35 | Assert.IsNotNull(await testRecorder.SaveScreenshot()); 36 | 37 | string? file = testRecorder.EnumerateScreenshots() 38 | .Where(x => Path.GetFileName(Path.GetDirectoryName(x)) == nameof(TestRecorderTests) && 39 | Path.GetFileName(x).StartsWith(nameof(SaveScreenshot_SavesImage))) 40 | .Single(); 41 | 42 | string? fileName = Path.GetFileName(file); 43 | Assert.AreEqual(nameof(TestRecorderTests), Path.GetFileName(Path.GetDirectoryName(file))); 44 | Assert.AreEqual($"{nameof(SaveScreenshot_SavesImage)}{GetLineNumber(-9)}-1.jpg", fileName); 45 | testRecorder.Success(skipInputStateCheck:true); 46 | } 47 | 48 | [TestMethod] 49 | public async Task SaveScreenshot_WithSuffix_SavesImage() 50 | { 51 | await using var app = new Simulators.App(); 52 | await using TestRecorder testRecorder = new(app); 53 | 54 | Assert.IsNotNull(await testRecorder.SaveScreenshot("MySuffix")); 55 | 56 | var file = testRecorder.EnumerateScreenshots() 57 | .Where(x => Path.GetFileName(Path.GetDirectoryName(x)) == nameof(TestRecorderTests) && 58 | Path.GetFileName(x).StartsWith(nameof(SaveScreenshot_WithSuffix_SavesImage))) 59 | .Single(); 60 | 61 | var fileName = Path.GetFileName(file); 62 | Assert.AreEqual(nameof(TestRecorderTests), Path.GetFileName(Path.GetDirectoryName(file))); 63 | Assert.AreEqual($"{nameof(SaveScreenshot_WithSuffix_SavesImage)}MySuffix{GetLineNumber(-9)}-1.jpg", fileName); 64 | testRecorder.Success(true); 65 | } 66 | 67 | [TestMethod] 68 | public async Task TestRecorder_WhenExceptionThrown_DoesNotRethrow() 69 | { 70 | await using var app = new Simulators.App(); 71 | await using TestRecorder testRecorder = new(app); 72 | await Assert.ThrowsExactlyAsync(async () => await app.InitializeWithDefaults(null!)); 73 | } 74 | 75 | [TestMethod] 76 | public async Task TestRecorder_WithInvalidXAML_DoesNotRethrow() 77 | { 78 | await using var app = await App.StartRemote(); 79 | await using TestRecorder testRecorder = new(app); 80 | await app.InitializeWithDefaults(); 81 | await Assert.ThrowsExactlyAsync(async () => await app.CreateWindowWithContent("")); 82 | } 83 | 84 | [TestMethod] 85 | public async Task TestRecorder_WithCtorSuffix_AppendsToAllFileNames() 86 | { 87 | await using var app = new Simulators.App(); 88 | await using TestRecorder testRecorder = new(app, "CtorSuffix"); 89 | 90 | Assert.IsNotNull(await testRecorder.SaveScreenshot("OtherSuffix1")); 91 | Assert.IsNotNull(await testRecorder.SaveScreenshot("OtherSuffix2")); 92 | 93 | var files = testRecorder.EnumerateScreenshots() 94 | .Where(x => Path.GetFileName(Path.GetDirectoryName(x)) == nameof(TestRecorderTests) && 95 | Path.GetFileName(x).StartsWith(nameof(TestRecorder_WithCtorSuffix_AppendsToAllFileNames))) 96 | .ToList(); 97 | 98 | Assert.AreEqual(2, files.Count); 99 | var file1Name = Path.GetFileName(files[0]); 100 | var file2Name = Path.GetFileName(files[1]); 101 | Assert.AreEqual($"{nameof(TestRecorder_WithCtorSuffix_AppendsToAllFileNames)}CtorSuffixOtherSuffix1{GetLineNumber(-11)}-1.jpg", file1Name); 102 | Assert.AreEqual($"{nameof(TestRecorder_WithCtorSuffix_AppendsToAllFileNames)}CtorSuffixOtherSuffix2{GetLineNumber(-11)}-2.jpg", file2Name); 103 | testRecorder.Success(skipInputStateCheck:true); 104 | } 105 | 106 | private static int GetLineNumber(int offset = 0, [CallerLineNumber] int lineNumber = 0) 107 | => lineNumber + offset; 108 | } 109 | -------------------------------------------------------------------------------- /XAMLTest.Tests/WaitTests.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest.Tests; 2 | 3 | [TestClass] 4 | public class WaitTests 5 | { 6 | private Task Timeout() 7 | { 8 | return Task.FromResult(false); 9 | } 10 | 11 | private Task Success() 12 | { 13 | return Task.FromResult(true); 14 | } 15 | 16 | [TestMethod] 17 | public async Task ShouldTimeout() 18 | { 19 | await Assert.ThrowsExceptionAsync(async () => await Wait.For(Timeout)); 20 | } 21 | 22 | [TestMethod] 23 | public async Task ShouldNotTimeout() 24 | { 25 | await Wait.For(Success); 26 | } 27 | 28 | [TestMethod] 29 | public async Task ShouldTimeoutWithMessage() 30 | { 31 | var timeoutMessage = "We're expecting a timeout"; 32 | var ex = await Assert.ThrowsExceptionAsync(async () => await Wait.For(Timeout, message: timeoutMessage)); 33 | Assert.IsTrue(ex.Message.StartsWith(timeoutMessage), $"Expected exception message to start with: '{timeoutMessage}'"); 34 | 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /XAMLTest.Tests/XAMLTest.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0-windows;net9.0-windows 5 | false 6 | true 7 | XamlTest.Tests 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /XAMLTest.UnitTestGenerator/XAMLTest.UnitTestGenerator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | false 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /XAMLTest.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 17 3 | VisualStudioVersion = 17.1.32104.313 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XAMLTest", "XAMLTest\XAMLTest.csproj", "{B4E881C8-2384-4515-8124-5D06949E754F}" 6 | EndProject 7 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0A942BB1-02B4-4E84-AA48-D87BC727E18D}" 8 | ProjectSection(SolutionItems) = preProject 9 | .editorconfig = .editorconfig 10 | Directory.Build.props = Directory.Build.props 11 | Directory.Build.targets = Directory.Build.targets 12 | Directory.Packages.props = Directory.Packages.props 13 | global.json = global.json 14 | NuGet.config = NuGet.config 15 | README.md = README.md 16 | EndProjectSection 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XAMLTest.Tests", "XAMLTest.Tests\XAMLTest.Tests.csproj", "{910AD9D6-3683-47EC-8831-5FE63D235E60}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XAMLTest.TestApp", "XAMLTest.TestApp\XAMLTest.TestApp.csproj", "{FA295C70-BF89-4727-8B39-AA1F2CB8E5DE}" 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XAMLTest.Generator", "XAMLTest.Generator\XAMLTest.Generator.csproj", "{CBBE847F-498F-4E17-A437-9B02E6DB7398}" 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XAMLTest.UnitTestGenerator", "XAMLTest.UnitTestGenerator\XAMLTest.UnitTestGenerator.csproj", "{B320FF6B-1691-4BFB-821E-2DD89817AD98}" 25 | EndProject 26 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{F8747EDE-0BE7-4414-A6B2-A4FF5A572E50}" 27 | ProjectSection(SolutionItems) = preProject 28 | .github\dependabot.yml = .github\dependabot.yml 29 | .github\workflows\dotnet-core.yml = .github\workflows\dotnet-core.yml 30 | EndProjectSection 31 | EndProject 32 | Global 33 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 34 | Debug|Any CPU = Debug|Any CPU 35 | Release|Any CPU = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 38 | {B4E881C8-2384-4515-8124-5D06949E754F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {B4E881C8-2384-4515-8124-5D06949E754F}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {B4E881C8-2384-4515-8124-5D06949E754F}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {B4E881C8-2384-4515-8124-5D06949E754F}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {910AD9D6-3683-47EC-8831-5FE63D235E60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {910AD9D6-3683-47EC-8831-5FE63D235E60}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {910AD9D6-3683-47EC-8831-5FE63D235E60}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {910AD9D6-3683-47EC-8831-5FE63D235E60}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {FA295C70-BF89-4727-8B39-AA1F2CB8E5DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {FA295C70-BF89-4727-8B39-AA1F2CB8E5DE}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {FA295C70-BF89-4727-8B39-AA1F2CB8E5DE}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {FA295C70-BF89-4727-8B39-AA1F2CB8E5DE}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {CBBE847F-498F-4E17-A437-9B02E6DB7398}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {CBBE847F-498F-4E17-A437-9B02E6DB7398}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {CBBE847F-498F-4E17-A437-9B02E6DB7398}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {CBBE847F-498F-4E17-A437-9B02E6DB7398}.Release|Any CPU.Build.0 = Release|Any CPU 54 | {B320FF6B-1691-4BFB-821E-2DD89817AD98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 55 | {B320FF6B-1691-4BFB-821E-2DD89817AD98}.Debug|Any CPU.Build.0 = Debug|Any CPU 56 | {B320FF6B-1691-4BFB-821E-2DD89817AD98}.Release|Any CPU.ActiveCfg = Release|Any CPU 57 | {B320FF6B-1691-4BFB-821E-2DD89817AD98}.Release|Any CPU.Build.0 = Release|Any CPU 58 | EndGlobalSection 59 | GlobalSection(SolutionProperties) = preSolution 60 | HideSolutionNode = FALSE 61 | EndGlobalSection 62 | GlobalSection(NestedProjects) = preSolution 63 | {F8747EDE-0BE7-4414-A6B2-A4FF5A572E50} = {0A942BB1-02B4-4E84-AA48-D87BC727E18D} 64 | EndGlobalSection 65 | GlobalSection(ExtensibilityGlobals) = postSolution 66 | SolutionGuid = {91636EEB-81BE-4223-B0D5-A5E2C983890A} 67 | EndGlobalSection 68 | EndGlobal 69 | -------------------------------------------------------------------------------- /XAMLTest/App.cs: -------------------------------------------------------------------------------- 1 | using GrpcDotNetNamedPipes; 2 | using XamlTest.Host; 3 | 4 | namespace XamlTest; 5 | 6 | public static class App 7 | { 8 | private static SemaphoreSlim SingletonAppLock { get; } = new(1, 1); 9 | 10 | public static Task StartRemote(Action? logMessage = null) 11 | { 12 | AppOptions options = new() 13 | { 14 | LogMessage = logMessage 15 | }; 16 | options.WithRemoteApp(); 17 | return StartRemote(options); 18 | } 19 | 20 | public static Task StartRemote(Action? logMessage = null) 21 | { 22 | AppOptions options = new() 23 | { 24 | LogMessage = logMessage 25 | }; 26 | return StartRemote(options); 27 | } 28 | 29 | public static async Task StartRemote(AppOptions options) 30 | { 31 | if (!File.Exists(options.XamlTestPath)) 32 | { 33 | throw new XamlTestException($"Could not find test app '{options.XamlTestPath}'"); 34 | } 35 | 36 | if (options.MinimizeOtherWindows) 37 | { 38 | MinimizeOtherWindows(); 39 | } 40 | 41 | ProcessStartInfo startInfo = new(options.XamlTestPath) 42 | { 43 | WorkingDirectory = Path.GetDirectoryName(options.XamlTestPath) + Path.DirectorySeparatorChar, 44 | UseShellExecute = true 45 | }; 46 | startInfo.ArgumentList.Add($"{Environment.ProcessId}"); 47 | if (!string.IsNullOrWhiteSpace(options.RemoteAppPath)) 48 | { 49 | startInfo.ArgumentList.Add("--application-path"); 50 | startInfo.ArgumentList.Add(options.RemoteAppPath); 51 | } 52 | if (!string.IsNullOrWhiteSpace(options.ApplicationType)) 53 | { 54 | startInfo.ArgumentList.Add("--application-type"); 55 | startInfo.ArgumentList.Add(options.ApplicationType); 56 | } 57 | 58 | bool useDebugger = options.AllowVisualStudioDebuggerAttach && Debugger.IsAttached; 59 | if (useDebugger) 60 | { 61 | startInfo.ArgumentList.Add($"--debug"); 62 | } 63 | if (options.RemoteProcessLogFile is { } logFile) 64 | { 65 | startInfo.ArgumentList.Add($"--log-file"); 66 | startInfo.ArgumentList.Add(logFile.FullName); 67 | } 68 | 69 | var logMessage = options.LogMessage; 70 | 71 | if (logMessage is not null) 72 | { 73 | string args = string.Join(' ', startInfo.ArgumentList.Select(x => x.StartsWith("--", StringComparison.Ordinal) ? x : $"\"{x}\"")); 74 | logMessage($"Starting XAML Test: {startInfo.FileName} {args}"); 75 | } 76 | 77 | await SingletonAppLock.WaitAsync(); 78 | if (Process.Start(startInfo) is Process process) 79 | { 80 | NamedPipeChannel channel = new(".", Server.PipePrefix + process.Id, new NamedPipeChannelOptions 81 | { 82 | ConnectionTimeout = (int)options.ConnectionTimeout.TotalMilliseconds 83 | }); 84 | Protocol.ProtocolClient client = new(channel); 85 | if (useDebugger) 86 | { 87 | await VisualStudioAttacher.AttachVisualStudioToProcess(process); 88 | } 89 | 90 | var app = new Internal.App(process, client, options, SingletonAppLock); 91 | 92 | IVersion version; 93 | try 94 | { 95 | version = await Wait.For(app.GetVersion); 96 | } 97 | catch(TimeoutException) 98 | { 99 | if (logMessage is not null) 100 | { 101 | process.Refresh(); 102 | if (process.HasExited) 103 | { 104 | logMessage($"Remote process not running"); 105 | } 106 | } 107 | await app.DisposeAsync(); 108 | throw; 109 | } 110 | if (logMessage is not null) 111 | { 112 | logMessage($"XAML Test v{version.XamlTestVersion}, App Version v{version.AppVersion}"); 113 | } 114 | return app; 115 | } 116 | throw new XamlTestException("Failed to start remote app"); 117 | } 118 | 119 | private static void MinimizeOtherWindows() 120 | { 121 | Process currentProcess = Process.GetCurrentProcess(); 122 | Process[] processes = Process.GetProcesses(); 123 | 124 | foreach (Process process in processes) 125 | { 126 | if (process.Id != currentProcess.Id && process.MainWindowHandle != IntPtr.Zero) 127 | { 128 | Windows.Win32.PInvoke.ShowWindow( 129 | new Windows.Win32.Foundation.HWND(process.MainWindowHandle), 130 | Windows.Win32.UI.WindowsAndMessaging.SHOW_WINDOW_CMD.SW_MINIMIZE); 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /XAMLTest/AppMixins.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Controls; 2 | 3 | namespace XamlTest; 4 | 5 | public static partial class AppMixins 6 | { 7 | public static async Task InitializeWithDefaults( 8 | this IApp app, 9 | params string[] assemblies) 10 | { 11 | await InitializeWithResources(app, "", assemblies); 12 | } 13 | 14 | public static async Task InitializeWithResources(this IApp app, string resourceDictionaryContents, params string[] assemblies) 15 | { 16 | if (app is null) 17 | { 18 | throw new ArgumentNullException(nameof(app)); 19 | } 20 | 21 | await app.Initialize(@$" 23 | {resourceDictionaryContents} 24 | ", assemblies); 25 | } 26 | 27 | public static async Task CreateWindowWithContent( 28 | this IApp app, 29 | string xamlContent, 30 | Size? windowSize = null, 31 | string title = "Test Window", 32 | string background = "White", 33 | WindowStartupLocation startupLocation = WindowStartupLocation.CenterScreen, 34 | string windowAttributes = "", 35 | params string[] additionalXmlNamespaces) 36 | { 37 | if (app is null) 38 | { 39 | throw new ArgumentNullException(nameof(app)); 40 | } 41 | 42 | string xaml = @$" 49 | { 50 | return x.StartsWith("xmlns") ? x : $"xmlns:{x}"; 51 | }))} 52 | mc:Ignorable=""d"" 53 | Height=""{windowSize?.Height ?? 800}"" 54 | Width=""{windowSize?.Width ?? 1100}"" 55 | Title=""{title}"" 56 | Background=""{background}"" 57 | WindowStartupLocation=""{startupLocation}"" 58 | {windowAttributes}> 59 | {xamlContent} 60 | "; 61 | 62 | return await app.CreateWindow(xaml); 63 | } 64 | 65 | public static async Task CreateWindowWithUserControl( 66 | this IApp app, 67 | Size? windowSize = null, 68 | string title = "Test Window", 69 | string background = "White", 70 | WindowStartupLocation startupLocation = WindowStartupLocation.CenterScreen) 71 | where TUserControl : UserControl 72 | { 73 | if (app is null) 74 | { 75 | throw new ArgumentNullException(nameof(app)); 76 | } 77 | 78 | return await app.CreateWindowWithContent($"", 79 | windowSize, title, background, startupLocation, additionalXmlNamespaces: @$"local=""clr-namespace:{typeof(TUserControl).Namespace};assembly={typeof(TUserControl).Assembly.GetName().Name}"""); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /XAMLTest/AppOptions.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest; 2 | 3 | public class AppOptions 4 | { 5 | private string? _xamlTestPath; 6 | 7 | public bool MinimizeOtherWindows { get; set; } 8 | public string? ApplicationType { get; set; } 9 | public string? RemoteAppPath { get; set; } 10 | public string XamlTestPath 11 | { 12 | get 13 | { 14 | if (_xamlTestPath is { } path) 15 | { 16 | return path; 17 | } 18 | var xamlTestPath = Path.ChangeExtension(Assembly.GetExecutingAssembly().Location, ".exe"); 19 | return Path.GetFullPath(xamlTestPath); 20 | } 21 | set => _xamlTestPath = value; 22 | } 23 | public Action? LogMessage { get; set; } 24 | public FileInfo? RemoteProcessLogFile { get; } 25 | public bool AllowVisualStudioDebuggerAttach { get; set; } = true; 26 | public TimeSpan ConnectionTimeout { get; set; } = TimeSpan.FromSeconds(30); 27 | 28 | public AppOptions() 29 | { 30 | var directory = Path.GetDirectoryName(XamlTestPath); 31 | var file = Path.ChangeExtension(Path.GetRandomFileName(), ".xamltest.log"); 32 | RemoteProcessLogFile = new FileInfo(Path.Combine(directory!, file)); 33 | } 34 | 35 | public void WithRemoteApp() 36 | { 37 | RemoteAppPath = typeof(TApp).Assembly.Location; 38 | } 39 | } 40 | 41 | public class AppOptions : AppOptions 42 | { 43 | public AppOptions() 44 | { 45 | WithRemoteApp(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /XAMLTest/Build/XAMLTest.targets: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /XAMLTest/ColorMixins.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Media; 3 | 4 | namespace XamlTest; 5 | 6 | public static class ColorMixins 7 | { 8 | /// 9 | /// The relative brightness of any point in a colorspace, normalized to 0 for darkest black and 1 for lightest white 10 | /// For the sRGB colorspace, the relative luminance of a color is defined as L = 0.2126 * R + 0.7152 * G + 0.0722 * B where R, G and B are defined as: 11 | /// if RsRGB <= 0.03928 then R = RsRGB / 12.92 else R = ((RsRGB+0.055)/1.055) ^ 2.4 12 | /// if GsRGB <= 0.03928 then G = GsRGB / 12.92 else G = ((GsRGB+0.055)/1.055) ^ 2.4 13 | /// if BsRGB <= 0.03928 then B = BsRGB / 12.92 else B = ((BsRGB+0.055)/1.055) ^ 2.4 14 | /// and RsRGB, GsRGB, and BsRGB are defined as: 15 | /// RsRGB = R8bit/255 16 | /// GsRGB = G8bit/255 17 | /// BsRGB = B8bit/255 18 | /// Based on https://www.w3.org/TR/WCAG21/#dfn-relative-luminance 19 | /// 20 | /// 21 | /// 22 | public static float RelativeLuninance(this Color color) 23 | { 24 | return 25 | 0.2126f * Calc(color.R / 255f) + 26 | 0.7152f * Calc(color.G / 255f) + 27 | 0.0722f * Calc(color.B / 255f); 28 | 29 | static float Calc(float colorValue) 30 | => colorValue <= 0.03928f ? colorValue / 12.92f : (float)Math.Pow((colorValue + 0.055f) / 1.055f, 2.4); 31 | } 32 | 33 | /// 34 | /// The contrast ratio is calculated as (L1 + 0.05) / (L2 + 0.05), where 35 | /// L1 is the: relative luminance of the lighter of the colors, and 36 | /// L2 is the relative luminance of the darker of the colors. 37 | /// Based on https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast%20ratio 38 | /// 39 | /// 40 | /// 41 | /// 42 | public static float ContrastRatio(this Color color, Color color2) 43 | { 44 | float l1 = color.RelativeLuninance(); 45 | float l2 = color2.RelativeLuninance(); 46 | if (l2 > l1) 47 | { 48 | float temp = l1; 49 | l1 = l2; 50 | l2 = temp; 51 | } 52 | return (l1 + 0.05f) / (l2 + 0.05f); 53 | } 54 | 55 | public static Color FlattenOnto(this Color foreground, Color background) 56 | { 57 | if (background.A == 0) return foreground; 58 | 59 | float alpha = foreground.ScA; 60 | float alphaReverse = 1 - alpha; 61 | 62 | float newAlpha = foreground.ScA + background.ScA * alphaReverse; 63 | return Color.FromArgb((byte)(newAlpha * byte.MaxValue), 64 | (byte)(alpha * foreground.R + alphaReverse * background.R), 65 | (byte)(alpha * foreground.G + alphaReverse * background.G), 66 | (byte)(alpha * foreground.B + alphaReverse * background.B) 67 | ); 68 | } 69 | 70 | public static Color WithOpacity(this Color color, double opacity) 71 | => Color.FromArgb((byte)(color.A * opacity), color.R, color.G, color.B); 72 | } 73 | -------------------------------------------------------------------------------- /XAMLTest/DependencyPropertyHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Reflection; 4 | using System.Windows; 5 | using Expression = System.Linq.Expressions.Expression; 6 | 7 | namespace XamlTest; 8 | 9 | internal static class DependencyPropertyHelper 10 | { 11 | private static Func FromName { get; } 12 | 13 | static DependencyPropertyHelper() 14 | { 15 | MethodInfo fromNameMethod = typeof(DependencyProperty).GetMethod("FromName", BindingFlags.Static | BindingFlags.NonPublic) 16 | ?? throw new InvalidOperationException($"Failed to find FromName method"); 17 | 18 | var nameParameter = Expression.Parameter(typeof(string)); 19 | var ownerTypeParameter = Expression.Parameter(typeof(Type)); 20 | var call = Expression.Call(fromNameMethod, nameParameter, ownerTypeParameter); 21 | 22 | var fromName = Expression.Lambda>(call, nameParameter, ownerTypeParameter); 23 | FromName = fromName.Compile(); 24 | } 25 | 26 | private static DependencyProperty? Find(string name, Type ownerType) 27 | => FromName(name, ownerType); 28 | 29 | public static bool TryGetDependencyProperty(string name, string ownerType, 30 | [NotNullWhen(true)]out DependencyProperty? dependencyProperty) 31 | { 32 | Type? type = Type.GetType(ownerType); 33 | if (type is null) 34 | { 35 | dependencyProperty = null; 36 | return false; 37 | } 38 | 39 | dependencyProperty = Find(name, type); 40 | return dependencyProperty != null; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /XAMLTest/ElementQuery.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using System.Reflection; 4 | using XamlTest.Query; 5 | 6 | namespace XamlTest; 7 | 8 | public static class ElementQuery 9 | { 10 | public static IQuery OfType() 11 | => new StringBuilderQuery(TypeQueryString()); 12 | 13 | public static IQuery WithName(string name) 14 | => new StringBuilderQuery(NameQuery(name)); 15 | 16 | public static IQuery Property(string propertyName) 17 | => new StringBuilderQuery(PropertyQuery(propertyName)); 18 | 19 | public static IQuery Property(Expression> propertyExpression) 20 | => Property(GetPropertyName(propertyExpression)); 21 | 22 | public static IQuery PropertyExpression(string propertyName, object value) 23 | => new StringBuilderQuery(PropertyExpressionQuery(propertyName, value)); 24 | 25 | public static IQuery PropertyExpression(Expression> propertyExpression, object value) 26 | => PropertyExpression(GetPropertyName(propertyExpression), value); 27 | 28 | 29 | internal static string TypeQueryString() => $"/{typeof(T).Name}"; 30 | internal static string NameQuery(string name) => $"~{name}"; 31 | internal static string PropertyQuery(string propertyName) => $".{propertyName}"; 32 | internal static string PropertyExpressionQuery(string propertyName, object value) => $"[{propertyName}={value}]"; 33 | internal static string IndexQuery(int index) => $"[{index}]"; 34 | 35 | internal static string GetPropertyName( 36 | Expression> propertyExpression) 37 | { 38 | MemberExpression? member = propertyExpression.Body as MemberExpression; 39 | if (member is null) 40 | throw new ArgumentException($"Expression '{propertyExpression}' refers to a method, not a property.", nameof(propertyExpression)); 41 | 42 | PropertyInfo? propInfo = member.Member as PropertyInfo; 43 | if (propInfo is null) 44 | throw new ArgumentException($"Expression '{propertyExpression}' does not refer to a property.", nameof(propertyExpression)); 45 | 46 | return propInfo.Name; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /XAMLTest/Event/EventRegistrar.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection.Emit; 2 | 3 | namespace XamlTest.Event; 4 | 5 | internal static class EventRegistrar 6 | { 7 | private class EventDetails 8 | { 9 | public List Invocations { get; } = []; 10 | public Delegate Delegate { get; } 11 | public EventInfo Event { get; } 12 | public object? Source { get; } 13 | 14 | public EventDetails(EventInfo eventInfo, Delegate @delegate, object? source) 15 | { 16 | Event = eventInfo; 17 | Delegate = @delegate; 18 | Source = source; 19 | } 20 | } 21 | 22 | private static object SyncObject { get; } = new(); 23 | 24 | private static Dictionary RegisteredEvents { get; } = []; 25 | 26 | private static Dictionary> EventInvocations { get; } = []; 27 | private static Dictionary EventDelegates { get; } = []; 28 | 29 | public static void AddInvocation(string eventId, object[] parameters) 30 | { 31 | lock(SyncObject) 32 | { 33 | if (RegisteredEvents.TryGetValue(eventId, out EventDetails? eventDetails)) 34 | { 35 | eventDetails.Invocations.Add(parameters); 36 | } 37 | } 38 | } 39 | 40 | public static bool Unregister(string eventId) 41 | { 42 | lock (SyncObject) 43 | { 44 | if (RegisteredEvents.TryGetValue(eventId, out EventDetails? eventDetails)) 45 | { 46 | MethodInfo? removeMethod = eventDetails.Event.GetRemoveMethod(); 47 | removeMethod?.Invoke(eventDetails.Source, [eventDetails.Delegate]); 48 | return removeMethod != null; 49 | } 50 | } 51 | return false; 52 | } 53 | 54 | internal static IReadOnlyList? GetInvocations(string eventId) 55 | { 56 | lock (SyncObject) 57 | { 58 | if (RegisteredEvents.TryGetValue(eventId, out EventDetails? eventDetails)) 59 | { 60 | return eventDetails.Invocations; 61 | } 62 | } 63 | return null; 64 | } 65 | 66 | public static void Regsiter(string eventId, EventInfo eventInfo, object? source) 67 | { 68 | if (eventId is null) 69 | { 70 | throw new ArgumentNullException(nameof(eventId)); 71 | } 72 | 73 | if (eventInfo is null) 74 | { 75 | throw new ArgumentNullException(nameof(eventInfo)); 76 | } 77 | 78 | Type delegateType = eventInfo.EventHandlerType ?? 79 | throw new InvalidOperationException($"Could not determine Event Handler Type for event '{eventInfo.Name}'"); 80 | 81 | Type returnType = GetDelegateReturnType(delegateType); 82 | if (returnType != typeof(void)) 83 | throw new XamlTestException("Event delegate must return void."); 84 | 85 | var delegateParameterTypes = GetDelegateParameterTypes(delegateType); 86 | 87 | DynamicMethod handler = new ("", null, delegateParameterTypes, typeof(EventRegistrar)); 88 | 89 | ILGenerator ilgen = handler.GetILGenerator(); 90 | MethodInfo addInvocationMethod = typeof(EventRegistrar) 91 | .GetMethod(nameof(AddInvocation)) 92 | ?? throw new InvalidOperationException("Failed to find method"); 93 | int foo = 0; 94 | string bar = ""; 95 | object[] array = [foo, bar]; 96 | 97 | ilgen.Emit(OpCodes.Ldstr, eventId); 98 | ilgen.Emit(OpCodes.Ldc_I4, delegateParameterTypes.Length); 99 | ilgen.Emit(OpCodes.Newarr, typeof(object)); 100 | 101 | for(int i = 0; i < delegateParameterTypes.Length; i++) 102 | { 103 | ilgen.Emit(OpCodes.Dup); 104 | ilgen.Emit(OpCodes.Ldc_I4, i); 105 | ilgen.Emit(OpCodes.Ldarg, i); 106 | if (!delegateParameterTypes[i].IsClass) 107 | { 108 | ilgen.Emit(OpCodes.Box, delegateParameterTypes[i]); 109 | } 110 | ilgen.Emit(OpCodes.Stelem_Ref); 111 | } 112 | ilgen.Emit(OpCodes.Call, addInvocationMethod); 113 | ilgen.Emit(OpCodes.Ret); 114 | 115 | MethodInfo addHandler = eventInfo.GetAddMethod() ?? 116 | throw new InvalidOperationException($"Could not find add method for event '{eventInfo.Name}'"); 117 | Delegate dEmitted = handler.CreateDelegate(delegateType); 118 | addHandler.Invoke(source, [dEmitted]); 119 | 120 | lock(RegisteredEvents) 121 | { 122 | RegisteredEvents.Add(eventId, new EventDetails(eventInfo, dEmitted, source)); 123 | } 124 | } 125 | 126 | private static Type[] GetDelegateParameterTypes(Type delegateType) 127 | { 128 | if (delegateType.BaseType != typeof(MulticastDelegate)) 129 | { 130 | throw new XamlTestException($"'{delegateType.FullName}' is not a delegate type."); 131 | } 132 | 133 | MethodInfo invoke = delegateType.GetMethod(nameof(Action.Invoke)) 134 | ?? throw new MissingMethodException($"Could not find {nameof(Action.Invoke)} method on delegate {delegateType.FullName}"); 135 | 136 | ParameterInfo[] parameters = invoke.GetParameters(); 137 | Type[] typeParameters = new Type[parameters.Length]; 138 | for (int i = 0; i < parameters.Length; i++) 139 | { 140 | typeParameters[i] = parameters[i].ParameterType; 141 | } 142 | return typeParameters; 143 | } 144 | 145 | private static Type GetDelegateReturnType(Type delegateType) 146 | { 147 | if (delegateType.BaseType != typeof(MulticastDelegate)) 148 | { 149 | throw new XamlTestException($"'{delegateType.FullName}' is not a delegate type."); 150 | } 151 | 152 | MethodInfo? invoke = delegateType.GetMethod(nameof(Action.Invoke)); 153 | return invoke is null 154 | ? throw new MissingMethodException($"Could not find {nameof(Action.Invoke)} method on delegate {delegateType.FullName}") 155 | : invoke.ReturnType; 156 | } 157 | 158 | } 159 | -------------------------------------------------------------------------------- /XAMLTest/HighlightConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Media; 2 | 3 | namespace XamlTest; 4 | 5 | public sealed class HighlightConfig 6 | { 7 | public static Color DefaultBorderColor { get; } = Color.FromArgb(0x75, 0xFF, 0x0, 0x0); 8 | public const double DefaultBorderWidth = 5.0; 9 | public static Color DefaultOverlayColor { get; } = Color.FromArgb(0x30, 0xFF, 0x0, 0x0); 10 | 11 | public bool IsVisible { get; set; } = true; 12 | 13 | public Brush? BorderBrush { get; set; } 14 | public double BorderThickness { get; set; } 15 | public Brush? OverlayBrush { get; set; } 16 | 17 | static HighlightConfig() 18 | { 19 | Brush borderBrush = new SolidColorBrush(DefaultBorderColor); 20 | borderBrush.Freeze(); 21 | Brush overlayBrush = new SolidColorBrush(DefaultOverlayColor); 22 | overlayBrush.Freeze(); 23 | 24 | Default = new() 25 | { 26 | IsVisible = true, 27 | BorderBrush = borderBrush, 28 | BorderThickness = DefaultBorderWidth, 29 | OverlayBrush = overlayBrush 30 | }; 31 | } 32 | 33 | public static HighlightConfig Default { get; } 34 | 35 | public static HighlightConfig None { get; } = new() 36 | { 37 | IsVisible = false 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /XAMLTest/Host/VisualTreeService.Events.cs: -------------------------------------------------------------------------------- 1 | using Grpc.Core; 2 | using XamlTest.Event; 3 | 4 | namespace XamlTest.Host; 5 | 6 | partial class VisualTreeService 7 | { 8 | public override async Task RegisterForEvent(EventRegistrationRequest request, ServerCallContext context) 9 | { 10 | EventRegistrationResult reply = new() 11 | { 12 | EventId = Guid.NewGuid().ToString() 13 | }; 14 | await Application.Dispatcher.InvokeAsync(() => 15 | { 16 | DependencyObject? element = GetCachedElement(request.ElementId); 17 | if (element is null) 18 | { 19 | reply.ErrorMessages.Add("Could not find element"); 20 | return; 21 | } 22 | 23 | Type elementType = element.GetType(); 24 | if (elementType.GetEvent(request.EventName) is { } eventInfo) 25 | { 26 | EventRegistrar.Regsiter(reply.EventId, eventInfo, element); 27 | } 28 | else 29 | { 30 | reply.ErrorMessages.Add($"Could not find event '{request.EventName}' on {elementType.FullName}"); 31 | } 32 | }); 33 | return reply; 34 | } 35 | 36 | public override Task UnregisterForEvent(EventUnregisterRequest request, ServerCallContext context) 37 | { 38 | EventUnregisterResult reply = new(); 39 | if (!EventRegistrar.Unregister(request.EventId)) 40 | { 41 | reply.ErrorMessages.Add("Failed to unregister event"); 42 | } 43 | return Task.FromResult(reply); 44 | } 45 | 46 | public override Task GetEventInvocations(EventInvocationsQuery request, ServerCallContext context) 47 | { 48 | EventInvocationsResult reply = new() 49 | { 50 | EventId = request.EventId, 51 | }; 52 | var invocations = EventRegistrar.GetInvocations(request.EventId); 53 | if (invocations is null) 54 | { 55 | reply.ErrorMessages.Add("Event was not registered"); 56 | } 57 | else 58 | { 59 | reply.EventInvocations.AddRange( 60 | invocations.Select( 61 | array => 62 | { 63 | EventInvocation rv = new(); 64 | rv.Parameters.AddRange(array.Select(item => GetItemString(item))); 65 | return rv; 66 | })); 67 | } 68 | 69 | 70 | return Task.FromResult(reply); 71 | 72 | string GetItemString(object item) 73 | { 74 | return Serializer.Serialize(item.GetType(), item) 75 | ?? item?.ToString() 76 | ?? item?.GetType().FullName 77 | ?? ""; 78 | } 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /XAMLTest/Host/VisualTreeService.Highlight.cs: -------------------------------------------------------------------------------- 1 | using Grpc.Core; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using System.Windows; 5 | using System.Windows.Documents; 6 | using System.Windows.Media; 7 | using XamlTest.Internal; 8 | 9 | namespace XamlTest.Host; 10 | 11 | partial class VisualTreeService 12 | { 13 | public override Task HighlightElement(HighlightRequest request, ServerCallContext context) 14 | { 15 | HighlightResult reply = new(); 16 | Application.Dispatcher.Invoke(() => 17 | { 18 | DependencyObject? dependencyObject = GetCachedElement(request.ElementId); 19 | if (dependencyObject is null) 20 | { 21 | reply.ErrorMessages.Add("Could not find element"); 22 | return; 23 | } 24 | 25 | if (dependencyObject is not UIElement uiElement) 26 | { 27 | reply.ErrorMessages.Add($"Element {dependencyObject.GetType().FullName} is not a {typeof(UIElement).FullName}"); 28 | return; 29 | } 30 | 31 | var adornerLayer = AdornerLayer.GetAdornerLayer(uiElement); 32 | 33 | if (adornerLayer is null) 34 | { 35 | reply.ErrorMessages.Add("Could not find adnorner layer"); 36 | return; 37 | } 38 | 39 | foreach(var adorner in adornerLayer.GetAdorners(uiElement)?.OfType().ToList() ?? Enumerable.Empty()) 40 | { 41 | adornerLayer.Remove(adorner); 42 | } 43 | 44 | if (request.IsVisible) 45 | { 46 | Brush? borderBrush = Serializer.Deserialize(request.BorderBrush); 47 | Brush? overlayBrush = Serializer.Deserialize(request.OverlayBrush); 48 | 49 | var selectionAdorner = new SelectionAdorner(uiElement) 50 | { 51 | AdornerLayer = adornerLayer, 52 | BorderBrush = borderBrush, 53 | BorderThickness = request.BorderThickness, 54 | OverlayBrush = overlayBrush 55 | }; 56 | 57 | adornerLayer.Add(selectionAdorner); 58 | } 59 | }); 60 | return Task.FromResult(reply); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /XAMLTest/Host/VisualTreeService.Invocation.cs: -------------------------------------------------------------------------------- 1 | using Grpc.Core; 2 | 3 | namespace XamlTest.Host; 4 | 5 | partial class VisualTreeService 6 | { 7 | public override async Task RemoteInvocation(RemoteInvocationRequest request, ServerCallContext context) 8 | { 9 | RemoteInvocationResult reply = new(); 10 | await Application.Dispatcher.InvokeAsync(() => 11 | { 12 | try 13 | { 14 | object? element = null; 15 | if (request.UseAppAsElement) 16 | { 17 | element = Application; 18 | } 19 | else 20 | { 21 | element = GetCachedElement(request.ElementId); 22 | } 23 | if (element is null) 24 | { 25 | reply.ErrorMessages.Add("Failed to find element to execute remote code"); 26 | } 27 | Assembly? assembly = LoadedAssemblies.FirstOrDefault(x => x.GetName().FullName == request.Assembly); 28 | if (assembly is null) 29 | { 30 | reply.ErrorMessages.Add($"Failed to find assembly '{request.Assembly}' for remote code"); 31 | } 32 | else 33 | { 34 | if (Type.GetType(request.MethodContainerType) is { } containingType) 35 | { 36 | if (containingType.GetMethod(request.MethodName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy) is { } method) 37 | { 38 | var parameters = new object?[request.Parameters.Count + 1]; 39 | parameters[0] = element; 40 | var methodParameters = method.GetParameters(); 41 | if (methodParameters.Length == parameters.Length) 42 | { 43 | for (int i = 0; i < request.Parameters.Count; i++) 44 | { 45 | Type parameterType = methodParameters[i + 1].ParameterType; 46 | parameters[i + 1] = Serializer.Deserialize(parameterType, request.Parameters[i]); 47 | } 48 | 49 | if (request.MethodGenericTypes.Any()) 50 | { 51 | Type[] genericTypes = request.MethodGenericTypes.Select(x => Type.GetType(x, true)!).ToArray(); 52 | method = method.MakeGenericMethod(genericTypes); 53 | } 54 | 55 | object? response = method.Invoke(null, parameters); 56 | reply.ValueType = method.ReturnType.AssemblyQualifiedName; 57 | if (method.ReturnType != typeof(void)) 58 | { 59 | reply.Value = Serializer.Serialize(method.ReturnType, response); 60 | } 61 | } 62 | else 63 | { 64 | reply.ErrorMessages.Add($"{request.MethodContainerType}.{request.MethodName} contains {methodParameters.Length} does not match the number of passed parameters {parameters.Length}"); 65 | } 66 | } 67 | else 68 | { 69 | reply.ErrorMessages.Add($"Could not find method '{request.MethodName}' on {containingType.FullName}"); 70 | } 71 | } 72 | else 73 | { 74 | reply.ErrorMessages.Add($"Could not find method containing type '{request.MethodContainerType}'"); 75 | } 76 | } 77 | } 78 | catch (Exception e) 79 | { 80 | reply.ErrorMessages.Add(e.ToString()); 81 | } 82 | }); 83 | return reply; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /XAMLTest/IApp.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest; 2 | 3 | /// 4 | /// Represents an application interface for XAML testing, providing methods for initialization, window management, remote execution, resource access, and logging. 5 | /// 6 | public interface IApp : IAsyncDisposable, IDisposable 7 | { 8 | /// 9 | /// Initializes the application with the specified App.xaml content and referenced assemblies. 10 | /// 11 | /// The XAML content for App.xaml. 12 | /// Assemblies to reference during initialization. 13 | /// A task representing the asynchronous operation. 14 | Task Initialize(string applicationResourceXaml, params string[] assemblies); 15 | 16 | /// 17 | /// Creates a new window using the provided XAML. 18 | /// 19 | /// The XAML markup for the window. 20 | /// A task that returns the created . 21 | Task CreateWindow(string xaml); 22 | 23 | /// 24 | /// Creates a new window of the specified type. 25 | /// 26 | /// The type of the window to create. 27 | /// A task that returns the created . 28 | Task CreateWindow() where TWindow : Window; 29 | 30 | /// 31 | /// Executes a delegate remotely in the application context and returns the result. 32 | /// 33 | /// The return type of the delegate. 34 | /// The delegate to execute. 35 | /// Parameters to pass to the delegate. 36 | /// A task that returns the result of the delegate execution. 37 | Task RemoteExecute(Delegate @delegate, object?[] parameters); 38 | 39 | /// 40 | /// Gets the main window of the application, if available. 41 | /// 42 | /// A task that returns the main , or null if not found. 43 | Task GetMainWindow(); 44 | 45 | /// 46 | /// Gets all windows currently open in the application. 47 | /// 48 | /// A task that returns a read-only list of instances. 49 | Task> GetWindows(); 50 | 51 | /// 52 | /// Retrieves a resource by its key. 53 | /// 54 | /// The key of the resource to retrieve. 55 | /// A task that returns the requested . 56 | Task GetResource(string key); 57 | 58 | /// 59 | /// Captures a screenshot of the application. 60 | /// 61 | /// A task that returns an representing the screenshot. 62 | Task GetScreenshot(); 63 | 64 | /// 65 | /// Registers a serializer at the specified index. 66 | /// 67 | /// The type of serializer to register. 68 | /// The index at which to insert the serializer. 69 | /// A task representing the asynchronous operation. 70 | Task RegisterSerializer(int insertIndex = 0) 71 | where T : ISerializer, new(); 72 | 73 | /// 74 | /// Gets the list of registered serializers. 75 | /// 76 | /// A task that returns a read-only list of instances. 77 | Task> GetSerializers(); 78 | 79 | /// 80 | /// Logs a message to the application log. 81 | /// 82 | /// The message to log. 83 | void LogMessage(string message); 84 | 85 | /// 86 | /// Gets the default XML namespaces used by the application. 87 | /// 88 | IList DefaultXmlNamespaces { get; } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /XAMLTest/IEventInvocation.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace XamlTest; 4 | 5 | public interface IEventInvocation 6 | { 7 | IReadOnlyList Parameters { get; } 8 | } 9 | -------------------------------------------------------------------------------- /XAMLTest/IEventRegistration.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest; 2 | 3 | public interface IEventRegistration : IAsyncDisposable 4 | { 5 | Task> GetInvocations(); 6 | } 7 | -------------------------------------------------------------------------------- /XAMLTest/IImage.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | 4 | namespace XamlTest; 5 | 6 | public interface IImage 7 | { 8 | Task Save(Stream stream); 9 | } 10 | -------------------------------------------------------------------------------- /XAMLTest/IProperty.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest; 2 | 3 | public interface IProperty : IValue 4 | { 5 | string PropertyType { get; } 6 | } 7 | -------------------------------------------------------------------------------- /XAMLTest/IQuery.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest; 2 | 3 | public interface IQuery 4 | { 5 | string QueryString { get; } 6 | } 7 | 8 | public interface IQuery : IQuery 9 | { 10 | void Add(string queryPart); 11 | } -------------------------------------------------------------------------------- /XAMLTest/IResource.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest; 2 | 3 | public interface IResource : IValue 4 | { 5 | string Key { get; } 6 | } 7 | -------------------------------------------------------------------------------- /XAMLTest/ISerializer.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest; 2 | 3 | public interface ISerializer 4 | { 5 | bool CanSerialize(Type type, ISerializer rootSerializer); 6 | string Serialize(Type type, object? value, ISerializer rootSerializer); 7 | object? Deserialize(Type type, string value, ISerializer rootSerializer); 8 | } 9 | -------------------------------------------------------------------------------- /XAMLTest/IValidation.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using System.Windows; 3 | using System.Windows.Controls; 4 | 5 | namespace XamlTest; 6 | 7 | public interface IValidation 8 | where T : DependencyObject 9 | { 10 | Task GetHasError(); 11 | Task SetValidationError(DependencyProperty property, object errorContent); 12 | Task SetValidationRule(DependencyProperty property) 13 | where TRule : ValidationRule, new(); 14 | Task GetValidationError(DependencyProperty dependencyProperty); 15 | Task ClearValidationError(DependencyProperty property); 16 | } 17 | -------------------------------------------------------------------------------- /XAMLTest/IValue.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace XamlTest; 4 | 5 | public interface IValue 6 | { 7 | object? Value { get; } 8 | string? ValueType { get; } 9 | 10 | [return: MaybeNull] 11 | T GetAs(); 12 | } 13 | -------------------------------------------------------------------------------- /XAMLTest/IVersion.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest; 2 | 3 | public interface IVersion 4 | { 5 | public string AppVersion { get; } 6 | public string XamlTestVersion { get; } 7 | } 8 | -------------------------------------------------------------------------------- /XAMLTest/IVisualElementConverter.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest; 2 | 3 | public interface IVisualElementConverter 4 | { 5 | TVisualElement Convert(); 6 | } 7 | -------------------------------------------------------------------------------- /XAMLTest/IWindow.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest; 2 | 3 | public interface IWindow : IVisualElement, IEquatable 4 | { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /XAMLTest/Input/DelayInput.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest.Input; 2 | 3 | internal class DelayInput : IInput 4 | { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /XAMLTest/Input/IInput.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest.Input; 2 | 3 | internal interface IInput 4 | { } 5 | -------------------------------------------------------------------------------- /XAMLTest/Input/KeyboardInput.cs: -------------------------------------------------------------------------------- 1 | using PInvoke; 2 | using System.Windows.Input; 3 | using static PInvoke.User32; 4 | 5 | namespace XamlTest.Input; 6 | 7 | internal static class KeyboardInput 8 | { 9 | public static void SendModifiers(IntPtr windowHandle, params ModifierKeys[] modifiers) 10 | { 11 | IEnumerable inputs = modifiers.SelectMany(GetKeyPress); 12 | SendInput(windowHandle, inputs); 13 | } 14 | 15 | public static void SendKeys(IntPtr windowHandle, params Key[] keys) 16 | { 17 | IEnumerable inputs = keys.SelectMany(GetKeyPress); 18 | SendInput(windowHandle, inputs); 19 | } 20 | 21 | public static void SendKeysForText(IntPtr windowHandle, string textInput) 22 | { 23 | IEnumerable inputs = textInput.SelectMany(GetKeyPress); 24 | SendInput(windowHandle, inputs); 25 | } 26 | 27 | private static void SendInput(IntPtr windowHandle, IEnumerable messages) 28 | { 29 | foreach (WindowMessage message in messages) 30 | { 31 | User32.SendMessage(windowHandle, message.Message, message.WParam, message.LParam); 32 | } 33 | } 34 | 35 | private static void SendInput(IntPtr windowHandle, IEnumerable inputs) 36 | { 37 | int sizeOfInputStruct; 38 | unsafe 39 | { 40 | // NOTE: There is a potential x86/x64 size issue here 41 | sizeOfInputStruct = sizeof(INPUT); 42 | } 43 | 44 | foreach (WindowInput input in inputs) 45 | { 46 | User32.SendInput(1, new[] { input.Input }, sizeOfInputStruct); 47 | //https://source.dot.net/#System.Windows.Forms/System/Windows/Forms/SendKeys.cs,720 48 | Thread.Sleep(1); 49 | } 50 | } 51 | 52 | private static IEnumerable GetKeyPress(char character) 53 | { 54 | IntPtr wParam = new(character); 55 | IntPtr lParam = new(0x0000_0000); 56 | yield return new WindowMessage(User32.WindowMessage.WM_CHAR, wParam, lParam); 57 | } 58 | 59 | private static IEnumerable GetKeyPress(Key key) 60 | { 61 | IntPtr wParam = new(KeyInterop.VirtualKeyFromKey(key)); 62 | IntPtr lParam = new(0x0000_0000); 63 | yield return new WindowMessage(User32.WindowMessage.WM_KEYDOWN, wParam, lParam); 64 | yield return new WindowMessage(User32.WindowMessage.WM_KEYUP, wParam, lParam); 65 | } 66 | 67 | private static IEnumerable GetKeyPress(ModifierKeys modifiers) 68 | { 69 | if (modifiers == ModifierKeys.None) 70 | { 71 | // Special case to remove any modifiers previously set, so we send KEYUP for all modifiers 72 | yield return new WindowInput(CreateInput(VirtualKey.VK_MENU, true)); 73 | yield return new WindowInput(CreateInput(VirtualKey.VK_CONTROL, true)); 74 | yield return new WindowInput(CreateInput(VirtualKey.VK_SHIFT, true)); 75 | yield return new WindowInput(CreateInput(VirtualKey.VK_LWIN, true)); 76 | } 77 | else 78 | { 79 | yield return new WindowInput(CreateInput(VirtualKey.VK_MENU, !modifiers.HasFlag(ModifierKeys.Alt))); 80 | yield return new WindowInput(CreateInput(VirtualKey.VK_CONTROL, !modifiers.HasFlag(ModifierKeys.Control))); 81 | yield return new WindowInput(CreateInput(VirtualKey.VK_SHIFT, !modifiers.HasFlag(ModifierKeys.Shift))); 82 | yield return new WindowInput(CreateInput(VirtualKey.VK_LWIN, !modifiers.HasFlag(ModifierKeys.Windows))); 83 | } 84 | } 85 | 86 | private static INPUT CreateInput(VirtualKey modifierKey, bool keyUp) 87 | { 88 | INPUT input = new() 89 | { 90 | type = User32.InputType.INPUT_KEYBOARD 91 | }; 92 | input.Inputs.ki.wVk = modifierKey; 93 | if (keyUp) 94 | { 95 | input.Inputs.ki.dwFlags = KEYEVENTF.KEYEVENTF_KEYUP; 96 | } 97 | return input; 98 | } 99 | 100 | private class WindowMessage 101 | { 102 | public WindowMessage(User32.WindowMessage message, IntPtr wParam, IntPtr lParam) 103 | { 104 | Message = message; 105 | WParam = wParam; 106 | LParam = lParam; 107 | } 108 | 109 | public User32.WindowMessage Message { get; } 110 | public IntPtr WParam { get; } 111 | public IntPtr LParam { get; } 112 | } 113 | 114 | private class WindowInput 115 | { 116 | public WindowInput(INPUT input) 117 | { 118 | Input = input; 119 | } 120 | 121 | public INPUT Input { get; } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /XAMLTest/Input/KeysInput.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Input; 2 | 3 | namespace XamlTest.Input; 4 | 5 | internal class KeysInput : IInput 6 | { 7 | public IReadOnlyList Keys { get; } 8 | 9 | public KeysInput(IEnumerable keys) 10 | { 11 | Keys = keys.ToArray(); 12 | } 13 | 14 | public KeysInput(params Key[] keys) 15 | { 16 | Keys = keys; 17 | } 18 | 19 | public override string ToString() 20 | => $"Keys:{string.Join(",", Keys)}"; 21 | } 22 | -------------------------------------------------------------------------------- /XAMLTest/Input/ModifiersInput.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Input; 2 | 3 | namespace XamlTest.Input; 4 | 5 | internal class ModifiersInput : IInput 6 | { 7 | public ModifierKeys Modifiers { get; } 8 | 9 | public ModifiersInput(ModifierKeys modifiers) 10 | { 11 | Modifiers = modifiers; 12 | } 13 | 14 | public override string ToString() 15 | => $"Modifiers:{Modifiers}"; 16 | } -------------------------------------------------------------------------------- /XAMLTest/Input/MouseInput.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using static PInvoke.User32; 3 | 4 | namespace XamlTest.Input; 5 | 6 | internal static class MouseInput 7 | { 8 | public static void LeftClick() 9 | { 10 | LeftDown(); 11 | LeftUp(); 12 | } 13 | 14 | public static void RightClick() 15 | { 16 | RightDown(); 17 | RightUp(); 18 | } 19 | 20 | public static void MiddleClick() 21 | { 22 | MiddleDown(); 23 | MiddleUp(); 24 | } 25 | 26 | public static void LeftDown() 27 | => MouseEvent(mouse_eventFlags.MOUSEEVENTF_LEFTDOWN); 28 | 29 | public static void LeftUp() 30 | => MouseEvent(mouse_eventFlags.MOUSEEVENTF_LEFTUP); 31 | 32 | public static void RightDown() 33 | => MouseEvent(mouse_eventFlags.MOUSEEVENTF_RIGHTDOWN); 34 | 35 | public static void RightUp() 36 | => MouseEvent(mouse_eventFlags.MOUSEEVENTF_RIGHTUP); 37 | 38 | public static void MiddleDown() 39 | => MouseEvent(mouse_eventFlags.MOUSEEVENTF_MIDDLEDOWN); 40 | 41 | public static void MiddleUp() 42 | => MouseEvent(mouse_eventFlags.MOUSEEVENTF_MIDDLEUP); 43 | 44 | public static void MoveCursor(Point screenLocation) 45 | => SetCursorPos((int)screenLocation.X, (int)screenLocation.Y); 46 | 47 | public static Point GetCursorPosition() 48 | { 49 | PInvoke.POINT pos = GetCursorPos(); 50 | return new Point(pos.x, pos.y); 51 | } 52 | 53 | private static unsafe void MouseEvent(mouse_eventFlags flags) 54 | { 55 | mouse_event(flags, 0, 0, 0, null); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /XAMLTest/Input/TextInput.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace XamlTest.Input; 4 | 5 | internal class TextInput : IInput 6 | { 7 | public string Text { get; } 8 | 9 | public TextInput(string text) 10 | { 11 | Text = text ?? throw new ArgumentNullException(nameof(text)); 12 | } 13 | 14 | public override string ToString() 15 | => $"Text:{Text}"; 16 | } 17 | -------------------------------------------------------------------------------- /XAMLTest/Internal/AppContext.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest.Internal; 2 | 3 | internal class AppContext 4 | { 5 | public Serializer Serializer { get; } = new(); 6 | 7 | public List DefaultNamespaces { get; } 8 | 9 | public AppContext() 10 | { 11 | DefaultNamespaces = new() 12 | { 13 | new XmlNamespace("", "http://schemas.microsoft.com/winfx/2006/xaml/presentation"), 14 | new XmlNamespace("x", "http://schemas.microsoft.com/winfx/2006/xaml"), 15 | new XmlNamespace("d", "http://schemas.microsoft.com/expression/blend/2008"), 16 | new XmlNamespace("mc", "http://schemas.openxmlformats.org/markup-compatibility/2006"), 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /XAMLTest/Internal/BaseValue.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace XamlTest.Internal; 5 | 6 | internal abstract class BaseValue : IValue 7 | { 8 | protected AppContext Context { get; } 9 | protected Serializer Serializer => Context.Serializer; 10 | 11 | public object? Value { get; } 12 | public string? ValueType { get; } 13 | 14 | protected BaseValue(string? valueType, object? value, AppContext context) 15 | { 16 | ValueType = valueType; 17 | Value = value; 18 | Context = context ?? throw new ArgumentNullException(nameof(context)); 19 | } 20 | 21 | [return: MaybeNull] 22 | public virtual T GetAs() 23 | { 24 | if (ValueType is null) 25 | { 26 | return default; 27 | } 28 | 29 | if (Value is T converted && typeof(T) != typeof(string)) 30 | { 31 | return converted; 32 | } 33 | 34 | return (T)Serializer.Deserialize(typeof(T), Value?.ToString() ?? "")!; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /XAMLTest/Internal/BitmapImage.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | using Google.Protobuf; 4 | 5 | namespace XamlTest.Internal; 6 | 7 | internal class BitmapImage : IImage 8 | { 9 | private ByteString Data { get; } 10 | 11 | public BitmapImage(ByteString data) => Data = data ?? throw new System.ArgumentNullException(nameof(data)); 12 | 13 | public Task Save(Stream stream) 14 | { 15 | Data.WriteTo(stream); 16 | return Task.CompletedTask; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /XAMLTest/Internal/DependencyObjectTracker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Windows; 4 | 5 | namespace XamlTest.Internal; 6 | 7 | internal static class DependencyObjectTracker 8 | { 9 | public static string GetId(DependencyObject obj) => (string)obj.GetValue(IdProperty); 10 | 11 | public static void SetId(DependencyObject obj, string value) => obj.SetValue(IdProperty, value); 12 | 13 | // Using a DependencyProperty as the backing store for Id. This enables animation, styling, binding, etc... 14 | public static readonly DependencyProperty IdProperty = 15 | DependencyProperty.RegisterAttached("Id", typeof(string), typeof(DependencyObjectTracker), new PropertyMetadata("")); 16 | 17 | internal static string GetOrSetId(DependencyObject obj, IDictionary> cache) 18 | { 19 | string id = GetId(obj); 20 | if (string.IsNullOrWhiteSpace(id)) 21 | { 22 | SetId(obj, id = Guid.NewGuid().ToString()); 23 | } 24 | lock (cache) 25 | { 26 | cache[id] = new WeakReference(obj); 27 | } 28 | return id; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /XAMLTest/Internal/EventRegistration.cs: -------------------------------------------------------------------------------- 1 | using XamlTest.Host; 2 | 3 | namespace XamlTest.Internal; 4 | 5 | internal class EventRegistration : IEventRegistration 6 | { 7 | public Protocol.ProtocolClient Client { get; } 8 | public string EventId { get; } 9 | public string EventName { get; } 10 | public Serializer Serializer { get; } 11 | public Action? LogMessage { get; } 12 | 13 | public EventRegistration(Protocol.ProtocolClient client, 14 | string eventId, string eventName, 15 | Serializer serializer, Action? logMessage) 16 | { 17 | Client = client ?? throw new ArgumentNullException(nameof(client)); 18 | EventId = eventId ?? throw new ArgumentNullException(nameof(eventId)); 19 | EventName = eventName ?? throw new ArgumentNullException(nameof(eventName)); 20 | Serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); 21 | LogMessage = logMessage; 22 | } 23 | 24 | public override string ToString() => $"{nameof(IEventRegistration)}: {EventName}"; 25 | 26 | public async Task> GetInvocations() 27 | { 28 | EventInvocationsQuery eventInvocationQuery = new() 29 | { 30 | EventId = EventId 31 | }; 32 | LogMessage?.Invoke($"{nameof(GetInvocations)}({EventName})"); 33 | if (await Client.GetEventInvocationsAsync(eventInvocationQuery) is { } reply) 34 | { 35 | if (reply.ErrorMessages.Any()) 36 | { 37 | throw new XamlTestException(string.Join(Environment.NewLine, reply.ErrorMessages)); 38 | } 39 | return reply.EventInvocations 40 | .Select(x => 41 | { 42 | return (IEventInvocation)new EventInvocation(x.Parameters.Cast().ToArray()); 43 | }) 44 | .ToList(); 45 | } 46 | throw new XamlTestException("Failed to receive a reply"); 47 | } 48 | 49 | public async ValueTask DisposeAsync() 50 | { 51 | EventUnregisterRequest eventInvocationQuery = new() 52 | { 53 | EventId = EventId 54 | }; 55 | LogMessage?.Invoke($"Unregister {nameof(GetInvocations)}({EventName})"); 56 | if (await Client.UnregisterForEventAsync(eventInvocationQuery) is { } reply) 57 | { 58 | if (reply.ErrorMessages.Any()) 59 | { 60 | throw new XamlTestException(string.Join(Environment.NewLine, reply.ErrorMessages)); 61 | } 62 | return; 63 | } 64 | throw new XamlTestException("Failed to receive a reply"); 65 | } 66 | } 67 | 68 | internal class EventInvocation : IEventInvocation 69 | { 70 | public IReadOnlyList Parameters { get; } 71 | 72 | public EventInvocation(IReadOnlyList parameters) 73 | { 74 | Parameters = parameters; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /XAMLTest/Internal/IElementId.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest.Internal; 2 | 3 | internal interface IElementId 4 | { 5 | string Id { get; } 6 | } 7 | -------------------------------------------------------------------------------- /XAMLTest/Internal/Property.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace XamlTest.Internal; 5 | 6 | internal class Property : BaseValue, IProperty 7 | { 8 | public string PropertyType { get; } 9 | 10 | public IVisualElement? Element { get; } 11 | 12 | public Property(string propertyType, string valueType, object? value, IVisualElement? element, 13 | AppContext context) 14 | : base(valueType, value, context) 15 | { 16 | PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType)); 17 | Element = element; 18 | } 19 | 20 | [return: MaybeNull] 21 | public override T GetAs() 22 | { 23 | if (Element is T typedElement) 24 | { 25 | return typedElement; 26 | } 27 | Type desiredType = typeof(T); 28 | if (string.IsNullOrEmpty(Value?.ToString()) && 29 | (desiredType == typeof(IVisualElement) || typeof(IVisualElement).IsAssignableFrom(desiredType))) 30 | { 31 | return default; 32 | } 33 | return base.GetAs(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /XAMLTest/Internal/ProtocolClientMixins.cs: -------------------------------------------------------------------------------- 1 | using XamlTest.Host; 2 | 3 | namespace XamlTest.Internal; 4 | 5 | internal static class ProtocolClientMixins 6 | { 7 | public static async Task RemoteExecute(this Protocol.ProtocolClient client, 8 | Serializer serializer, 9 | Action? logMessage, 10 | Action updateRequest, 11 | Delegate @delegate, 12 | object?[] parameters) 13 | { 14 | if (@delegate.Target is not null) 15 | { 16 | throw new ArgumentException("Cannot execute a non-static delegate remotely"); 17 | } 18 | if (@delegate.Method.DeclaringType is null) 19 | { 20 | throw new ArgumentException("Could not find containing type for delegate"); 21 | } 22 | 23 | var request = new RemoteInvocationRequest() 24 | { 25 | MethodName = @delegate.Method.Name, 26 | MethodContainerType = @delegate.Method.DeclaringType!.AssemblyQualifiedName, 27 | Assembly = @delegate.Method.DeclaringType.Assembly.FullName, 28 | }; 29 | foreach (var parameter in parameters) 30 | { 31 | request.Parameters.Add(serializer.Serialize(parameter?.GetType() ?? typeof(object), parameter)); 32 | } 33 | if (@delegate.Method.IsGenericMethod) 34 | { 35 | foreach (var genericArguments in @delegate.Method.GetGenericArguments()) 36 | { 37 | request.MethodGenericTypes.Add(genericArguments.AssemblyQualifiedName); 38 | } 39 | } 40 | updateRequest(request); 41 | logMessage?.Invoke($"{nameof(RemoteExecute)}({request})"); 42 | if (await client.RemoteInvocationAsync(request) is { } reply) 43 | { 44 | if (reply.ErrorMessages.Any()) 45 | { 46 | throw new XamlTestException(string.Join(Environment.NewLine, reply.ErrorMessages)); 47 | } 48 | 49 | if (reply.ValueType is null) 50 | { 51 | return default; 52 | } 53 | 54 | if (reply.Value is TReturn converted && typeof(TReturn) != typeof(string)) 55 | { 56 | return converted; 57 | } 58 | 59 | return (TReturn)serializer.Deserialize(typeof(TReturn), reply.Value ?? "")!; 60 | } 61 | return default; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /XAMLTest/Internal/Resource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace XamlTest.Internal; 4 | 5 | internal class Resource : BaseValue, IResource 6 | { 7 | public string Key { get; } 8 | 9 | public Resource(string key, string valueType, object? value, AppContext context) 10 | : base(valueType, value, context) 11 | { 12 | Key = key ?? throw new ArgumentNullException(nameof(key)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /XAMLTest/Internal/SelectionAdorner.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows; 3 | using System.Windows.Documents; 4 | using System.Windows.Media; 5 | 6 | namespace XamlTest.Internal; 7 | 8 | internal class SelectionAdorner : Adorner, IDisposable 9 | { 10 | static SelectionAdorner() 11 | { 12 | IsHitTestVisibleProperty.OverrideMetadata(typeof(SelectionAdorner), new UIPropertyMetadata(false)); 13 | UseLayoutRoundingProperty.OverrideMetadata(typeof(SelectionAdorner), new FrameworkPropertyMetadata(true)); 14 | } 15 | 16 | public SelectionAdorner(UIElement adornedElement) 17 | : base(adornedElement) 18 | { } 19 | 20 | public Brush? BorderBrush { get; set; } 21 | public double? BorderThickness { get; set; } 22 | public Brush? OverlayBrush { get; set; } 23 | 24 | public AdornerLayer? AdornerLayer { get; set; } 25 | 26 | protected override void OnRender(DrawingContext drawingContext) 27 | { 28 | if (ActualWidth <= 0 || ActualHeight <= 0) 29 | { 30 | return; 31 | } 32 | Pen? pen = null; 33 | if (BorderBrush is { } borderBrush && 34 | BorderThickness is { } borderThickness) 35 | { 36 | pen = new Pen(borderBrush, borderThickness); 37 | } 38 | 39 | drawingContext.DrawRectangle(OverlayBrush, pen, new Rect(0, 0, ActualWidth, ActualHeight)); 40 | } 41 | 42 | public void Dispose() 43 | { 44 | AdornerLayer?.Remove(this); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /XAMLTest/Internal/Serializer.cs: -------------------------------------------------------------------------------- 1 | using XamlTest.Transport; 2 | 3 | namespace XamlTest.Internal; 4 | 5 | internal class Serializer : ISerializer 6 | { 7 | public List Serializers { get; } = new List(); 8 | 9 | public Serializer() 10 | { 11 | //NB: Order matters here. Items earlier in the list take precedence 12 | Serializers.Add(new XamlSegmentSerializer()); 13 | Serializers.Add(new BrushSerializer()); 14 | Serializers.Add(new DpiScaleSerializer()); 15 | Serializers.Add(new CharSerializer()); 16 | Serializers.Add(new GridSerializer()); 17 | Serializers.Add(new DependencyPropertyConverter()); 18 | Serializers.Add(new SecureStringSerializer()); 19 | Serializers.Add(new DefaultSerializer()); 20 | } 21 | 22 | public void AddSerializer(ISerializer serializer, int index) 23 | => Serializers.Insert(index, serializer); 24 | 25 | public string Serialize(Type type, object? value) 26 | => ((ISerializer)this).Serialize(type, value, this); 27 | 28 | string ISerializer.Serialize(Type type, object? value, ISerializer rootSerializer) 29 | { 30 | if (Serializers.FirstOrDefault(x => x.CanSerialize(type, rootSerializer)) is { } serializer) 31 | { 32 | return serializer.Serialize(type, value, rootSerializer); 33 | } 34 | return ""; 35 | } 36 | 37 | public object? Deserialize(Type type, string value) 38 | => ((ISerializer)this).Deserialize(type, value, this); 39 | 40 | object? ISerializer.Deserialize(Type type, string value, ISerializer rootSerializer) 41 | { 42 | if (Serializers.FirstOrDefault(x => x.CanSerialize(type, rootSerializer)) is { } serializer) 43 | { 44 | return serializer.Deserialize(type, value, rootSerializer); 45 | } 46 | return null; 47 | } 48 | 49 | public T? Deserialize(string? value) 50 | { 51 | if (value is not null) 52 | { 53 | return (T?)((ISerializer)this).Deserialize(typeof(T), value, this); 54 | } 55 | return default; 56 | } 57 | 58 | bool ISerializer.CanSerialize(Type type, ISerializer rootSerializer) 59 | => Serializers.Any(x => x.CanSerialize(type, rootSerializer)); 60 | } 61 | -------------------------------------------------------------------------------- /XAMLTest/Internal/Service.cs: -------------------------------------------------------------------------------- 1 | using GrpcDotNetNamedPipes; 2 | using XamlTest.Host; 3 | 4 | namespace XamlTest.Internal; 5 | 6 | internal class Service : IDisposable 7 | { 8 | private NamedPipeServer Server { get; } 9 | private bool IsDisposed { get; set; } 10 | 11 | public Service(string id, Application application) 12 | { 13 | if (application is null) 14 | { 15 | throw new ArgumentNullException(nameof(application)); 16 | } 17 | 18 | Server = new NamedPipeServer(XamlTest.Server.PipePrefix + id); 19 | Protocol.BindService(Server.ServiceBinder, new VisualTreeService(application)); 20 | Server.Start(); 21 | } 22 | 23 | protected virtual void Dispose(bool disposing) 24 | { 25 | if (!IsDisposed) 26 | { 27 | if (disposing) 28 | { 29 | Server.Dispose(); 30 | } 31 | 32 | IsDisposed = true; 33 | } 34 | } 35 | 36 | public void Dispose() => Dispose(true); 37 | } 38 | -------------------------------------------------------------------------------- /XAMLTest/Internal/Validation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Windows; 4 | using System.Windows.Controls; 5 | 6 | namespace XamlTest.Internal; 7 | 8 | internal static class Validation 9 | { 10 | internal static readonly DependencyProperty ErrorProperty = DependencyProperty.RegisterAttached( 11 | $"Error-{Guid.NewGuid()}", typeof(object), typeof(Validation), new PropertyMetadata(default(object?))); 12 | 13 | internal class Rule : ValidationRule 14 | { 15 | private object ErrorContent { get; } 16 | 17 | public Rule(object errorContent) 18 | { 19 | ErrorContent = errorContent; 20 | } 21 | 22 | public override ValidationResult Validate(object value, CultureInfo cultureInfo) 23 | { 24 | return new ValidationResult(false, ErrorContent); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /XAMLTest/Internal/Validation{T}.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Windows; 4 | using System.Windows.Controls; 5 | using System.Windows.Data; 6 | 7 | namespace XamlTest.Internal; 8 | 9 | internal class Validation : IValidation 10 | where T : DependencyObject 11 | { 12 | private IVisualElement Element { get; } 13 | 14 | public Validation(IVisualElement element) 15 | { 16 | Element = element ?? throw new ArgumentNullException(nameof(element)); 17 | } 18 | 19 | public Task SetValidationError(DependencyProperty property, object errorContent) 20 | { 21 | return Element.RemoteExecute(SetError, property, errorContent); 22 | 23 | static void SetError(T element, DependencyProperty property, object errorContent) 24 | { 25 | BindingExpressionBase? bindingExpression = BindingOperations.GetBindingExpression(element, property); 26 | if (bindingExpression is null) 27 | { 28 | Binding binding = new() 29 | { 30 | Path = new PropertyPath(Validation.ErrorProperty), 31 | RelativeSource = new RelativeSource(RelativeSourceMode.Self) 32 | }; 33 | bindingExpression = BindingOperations.SetBinding(element, property, binding); 34 | } 35 | ValidationError validationError = new(new Validation.Rule(errorContent), bindingExpression) 36 | { 37 | ErrorContent = errorContent 38 | }; 39 | System.Windows.Controls.Validation.MarkInvalid(bindingExpression, validationError); 40 | } 41 | } 42 | 43 | public Task SetValidationRule(DependencyProperty property) 44 | where TRule : ValidationRule, new() 45 | { 46 | return Element.RemoteExecute(SetRule, property); 47 | 48 | static void SetRule(T element, DependencyProperty property) 49 | { 50 | TRule rule = new() 51 | { 52 | ValidatesOnTargetUpdated = true 53 | }; 54 | Binding binding = BindingOperations.GetBinding(element, property); 55 | if (binding is null) 56 | { 57 | binding = new() 58 | { 59 | Path = new PropertyPath(Validation.ErrorProperty), 60 | RelativeSource = new RelativeSource(RelativeSourceMode.Self) 61 | }; 62 | binding.ValidationRules.Add(rule); 63 | BindingOperations.SetBinding(element, property, binding); 64 | } 65 | else 66 | { 67 | binding.ValidationRules.Add(rule); 68 | } 69 | } 70 | } 71 | 72 | public Task ClearValidationError(DependencyProperty property) 73 | { 74 | return Element.RemoteExecute(ClearInvalid, property); 75 | 76 | static void ClearInvalid(T element, DependencyProperty property) 77 | { 78 | BindingExpressionBase? bindingExpression = BindingOperations.GetBindingExpression(element, property); 79 | if (bindingExpression != null) 80 | { 81 | // Clear the invalidation 82 | System.Windows.Controls.Validation.ClearInvalid(bindingExpression); 83 | } 84 | } 85 | } 86 | 87 | public Task GetValidationError(DependencyProperty dependencyProperty) 88 | { 89 | return Element.RemoteExecute(GetValidationErrorContent, dependencyProperty); 90 | 91 | static TErrorContext? GetValidationErrorContent(T element, DependencyProperty property) 92 | { 93 | var errors = System.Windows.Controls.Validation.GetErrors(element); 94 | foreach (var error in errors) 95 | { 96 | if (error.BindingInError is BindingExpressionBase bindingExpressionBase && 97 | bindingExpressionBase.TargetProperty == property) 98 | { 99 | if (error.ErrorContent is TErrorContext converted) 100 | { 101 | return converted; 102 | } 103 | } 104 | } 105 | return default; 106 | } 107 | } 108 | 109 | public Task GetHasError() 110 | => Element.GetProperty(System.Windows.Controls.Validation.HasErrorProperty); 111 | } 112 | -------------------------------------------------------------------------------- /XAMLTest/Internal/Version.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest.Internal; 2 | 3 | internal class Version : IVersion 4 | { 5 | public Version(string appVersion, string xamlTestVersion) 6 | { 7 | AppVersion = appVersion; 8 | XamlTestVersion = xamlTestVersion; 9 | } 10 | 11 | public string AppVersion { get; } 12 | 13 | public string XamlTestVersion { get; } 14 | } 15 | -------------------------------------------------------------------------------- /XAMLTest/Internal/Window.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using XamlTest.Host; 4 | 5 | namespace XamlTest.Internal; 6 | 7 | internal class Window : VisualElement, IWindow 8 | { 9 | public Window(Protocol.ProtocolClient client, string id, 10 | AppContext context, Action? logMessage) 11 | : base(client, id, typeof(System.Windows.Window), context, logMessage) 12 | { } 13 | 14 | public bool Equals([AllowNull] IWindow other) 15 | => base.Equals(other); 16 | protected override Host.ElementQuery GetFindElementQuery(string query) 17 | => new Host.ElementQuery 18 | { 19 | WindowId = Id, 20 | Query = query 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /XAMLTest/KeyboardInput.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Input; 2 | using XamlTest.Input; 3 | 4 | namespace XamlTest; 5 | 6 | public sealed class KeyboardInput 7 | { 8 | internal IReadOnlyList Inputs { get; } 9 | 10 | internal KeyboardInput(params IInput[] inputs) 11 | { 12 | Inputs = inputs; 13 | } 14 | 15 | public KeyboardInput(string text) 16 | : this(new TextInput(text)) 17 | { } 18 | 19 | public KeyboardInput(params Key[] keys) 20 | : this(new KeysInput(keys)) 21 | { } 22 | 23 | public override string ToString() => $"{{{string.Join(";", Inputs)}}}"; 24 | } 25 | -------------------------------------------------------------------------------- /XAMLTest/Logger.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest; 2 | 3 | internal static class Logger 4 | { 5 | private static List LogMessages { get; } = new(); 6 | private static List Writers { get; } = new(); 7 | 8 | static Logger() 9 | { 10 | AddLogOutput(File.Open($"XAMLTest.{Process.GetCurrentProcess().Id}.log", FileMode.Create, FileAccess.Write, FileShare.Read)); 11 | } 12 | 13 | public static void AddLogOutput(Stream stream) 14 | { 15 | StreamWriter writer = new(stream) { AutoFlush = true }; 16 | lock(LogMessages) 17 | { 18 | Writers.Add(writer); 19 | } 20 | } 21 | 22 | public static void CloseLogger() 23 | { 24 | lock (LogMessages) 25 | { 26 | Log("Closing logger"); 27 | foreach (var writer in Writers) 28 | { 29 | writer.Flush(); 30 | writer.Dispose(); 31 | } 32 | Writers.Clear(); 33 | } 34 | } 35 | 36 | public static IReadOnlyList GetMessages() 37 | { 38 | lock (LogMessages) 39 | { 40 | return LogMessages.AsReadOnly(); 41 | } 42 | } 43 | 44 | public static void Log(string message) 45 | { 46 | message = $"{DateTime.Now} - {message}"; 47 | lock (LogMessages) 48 | { 49 | LogMessages.Add(message); 50 | foreach(var writer in Writers) 51 | { 52 | writer.WriteLine(message); 53 | writer.Flush(); 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /XAMLTest/MouseInput.cs: -------------------------------------------------------------------------------- 1 | using XamlTest.Host; 2 | using XamlTest.Input; 3 | 4 | namespace XamlTest; 5 | 6 | public sealed class MouseInput : IInput 7 | { 8 | public static TimeSpan GetDoubleClickTime 9 | => TimeSpan.FromMilliseconds(Windows.Win32.PInvoke.GetDoubleClickTime()); 10 | 11 | internal class MouseInputData : IInput 12 | { 13 | public MouseData.Types.MouseEvent Event { get; set; } 14 | public string? Value { get; set; } 15 | 16 | public override string ToString() 17 | { 18 | if (!string.IsNullOrWhiteSpace(Value)) 19 | { 20 | return $"{Event}({Value})"; 21 | } 22 | return $"{Event}"; 23 | } 24 | } 25 | 26 | internal IInput[] Inputs { get; } 27 | 28 | internal MouseInput(params IInput[] inputs) 29 | { 30 | Inputs = inputs ?? throw new ArgumentNullException(nameof(inputs)); 31 | } 32 | 33 | public override string ToString() 34 | => $"{string.Join(";", Inputs.Select(x => x.ToString()))}"; 35 | 36 | public static MouseInput Delay(TimeSpan timespan) 37 | { 38 | return new MouseInput(new MouseInputData 39 | { 40 | Event = MouseData.Types.MouseEvent.Delay, 41 | Value = timespan.TotalMilliseconds.ToString() 42 | }); 43 | } 44 | 45 | public static MouseInput MoveToElement(Position position = Position.Center) 46 | { 47 | return new MouseInput(new MouseInputData 48 | { 49 | Event = MouseData.Types.MouseEvent.MoveToElement, 50 | Value = position.ToString() 51 | }); 52 | } 53 | 54 | public static MouseInput LeftDown() 55 | { 56 | return new MouseInput(new MouseInputData 57 | { 58 | Event = MouseData.Types.MouseEvent.LeftDown 59 | }); 60 | } 61 | 62 | public static MouseInput LeftUp() 63 | { 64 | return new MouseInput(new MouseInputData 65 | { 66 | Event = MouseData.Types.MouseEvent.LeftUp 67 | }); 68 | } 69 | 70 | public static MouseInput RightDown() 71 | { 72 | return new MouseInput(new MouseInputData 73 | { 74 | Event = MouseData.Types.MouseEvent.RightDown 75 | }); 76 | } 77 | 78 | public static MouseInput RightUp() 79 | { 80 | return new MouseInput(new MouseInputData 81 | { 82 | Event = MouseData.Types.MouseEvent.RightUp 83 | }); 84 | } 85 | 86 | public static MouseInput MiddleDown() 87 | { 88 | return new MouseInput(new MouseInputData 89 | { 90 | Event = MouseData.Types.MouseEvent.MiddleDown 91 | }); 92 | } 93 | 94 | public static MouseInput MiddleUp() 95 | { 96 | return new MouseInput(new MouseInputData 97 | { 98 | Event = MouseData.Types.MouseEvent.MiddleUp 99 | }); 100 | } 101 | 102 | public static MouseInput MoveRelative(int xOffset = 0, int yOffset = 0) 103 | { 104 | return new MouseInput(new MouseInputData 105 | { 106 | Event = MouseData.Types.MouseEvent.MoveRelative, 107 | Value = $"{xOffset};{yOffset}" 108 | }); 109 | } 110 | 111 | public static MouseInput MoveAbsolute(int screenXPosition = 0, int screenYPosition = 0) 112 | { 113 | return new MouseInput(new MouseInputData 114 | { 115 | Event = MouseData.Types.MouseEvent.MoveAbsolute, 116 | Value = $"{screenXPosition};{screenYPosition}" 117 | }); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /XAMLTest/NativeMethods.txt: -------------------------------------------------------------------------------- 1 | ShowWindow 2 | GetDoubleClickTime -------------------------------------------------------------------------------- /XAMLTest/Position.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest; 2 | 3 | public enum Position 4 | { 5 | None, 6 | TopLeft, 7 | TopCenter, 8 | TopRight, 9 | RightCenter, 10 | BottomRight, 11 | BottomCenter, 12 | BottomLeft, 13 | LeftCenter, 14 | Center 15 | } 16 | -------------------------------------------------------------------------------- /XAMLTest/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "XAMLTest": { 4 | "commandName": "Project", 5 | "commandLineArgs": "\"298564\" --application-path \"D:\\Dev\\FVS\\iNPUT\\src\\INPUT.UI.Tests\\bin\\Debug\\net6.0-windows\\win-x64\\-iNPUT.dll\" --application-type \"INPUT.App, -iNPUT, Version=2.1.3.0, Culture=neutral, PublicKeyToken=null\" --remote-method-name \"g__Factory|4_0\" --remote-method-container-type \"INPUT.UI.Tests.Tests.SmokeTest.SmokeTests\" --remote-method-assembly \"D:\\Dev\\FVS\\iNPUT\\src\\INPUT.UI.Tests\\bin\\Debug\\net6.0-windows\\win-x64\\INPUT.UI.Tests.dll\" --log-file \"D:\\Dev\\FVS\\iNPUT\\src\\INPUT.UI.Tests\\bin\\Debug\\net6.0-windows\\win-x64\\ldjzmtih.xamltest.log\"" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /XAMLTest/Query/StringBuilderQuery.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace XamlTest.Query; 4 | 5 | internal class StringBuilderQuery : IQuery 6 | { 7 | private StringBuilder Builder { get; } = new(); 8 | public string QueryString => Builder.ToString(); 9 | public void Add(string queryPart) => Builder.Append(queryPart); 10 | 11 | public StringBuilderQuery() 12 | { } 13 | 14 | public StringBuilderQuery(string query) 15 | => Builder.Append(query); 16 | } -------------------------------------------------------------------------------- /XAMLTest/QueryMixins.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using XamlTest.Query; 4 | 5 | namespace XamlTest; 6 | 7 | public static class QueryMixins 8 | { 9 | public static IQuery ChildOfType(this IQuery query) 10 | => AppendQuery(query, ElementQuery.TypeQueryString()); 11 | 12 | public static IQuery ChildWithName(this IQuery query, string name) 13 | => AppendQuery(query, ElementQuery.NameQuery(name)); 14 | 15 | public static IQuery Property(this IQuery query, string propertyName) 16 | => AppendQuery(query, ElementQuery.PropertyQuery(propertyName)); 17 | 18 | public static IQuery Property(this IQuery query, Expression> propertyExpression) 19 | => Property(query, ElementQuery.GetPropertyName(propertyExpression)); 20 | 21 | public static IQuery PropertyExpression(this IQuery query, string propertyName, object value) 22 | => AppendQuery(query, ElementQuery.PropertyExpressionQuery(propertyName, value)); 23 | 24 | public static IQuery PropertyExpression(this IQuery query, Expression> propertyExpression, object value) 25 | => PropertyExpression(query, ElementQuery.GetPropertyName(propertyExpression), value); 26 | 27 | public static IQuery AtIndex(this IQuery query, int index) 28 | => AppendQuery(query, ElementQuery.IndexQuery(index)); 29 | 30 | 31 | private static IQuery AppendQuery(IQuery query, string newQueryPart) 32 | { 33 | if (query is not IQuery typedQuery) 34 | { 35 | typedQuery = new StringBuilderQuery(query.QueryString); 36 | } 37 | typedQuery.Add(newQueryPart); 38 | return typedQuery; 39 | } 40 | } -------------------------------------------------------------------------------- /XAMLTest/RectMixins.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace XamlTest; 4 | 5 | internal static class RectMixins 6 | { 7 | public static Point Center(this Rect rect) 8 | => new(rect.Left + rect.Width / 2, rect.Top + rect.Height / 2); 9 | } 10 | -------------------------------------------------------------------------------- /XAMLTest/Retry.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest; 2 | 3 | public class Retry 4 | { 5 | public int? MinAttempts { get; } 6 | public TimeSpan Timeout { get; } 7 | 8 | public Retry(int minAttempts, TimeSpan timeout) 9 | { 10 | MinAttempts = minAttempts; 11 | Timeout = timeout; 12 | } 13 | 14 | public override string ToString() 15 | => $"{(MinAttempts != null ? $"Attempts: {MinAttempts}, " : "")}Timeout: {Timeout}"; 16 | 17 | public static Retry Default { get; } = new Retry(5, TimeSpan.FromSeconds(2)); 18 | } 19 | -------------------------------------------------------------------------------- /XAMLTest/Server.cs: -------------------------------------------------------------------------------- 1 | using XamlTest.Internal; 2 | 3 | namespace XamlTest; 4 | 5 | public static class Server 6 | { 7 | internal const string PipePrefix = nameof(XamlTest) + ".CommunicationPipe."; 8 | 9 | internal static IDisposable Start(Application? app = null) 10 | { 11 | var process = Process.GetCurrentProcess(); 12 | Service service = new(process.Id.ToString(), app ?? Application.Current); 13 | return service; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /XAMLTest/TestRecorder.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using static PInvoke.User32; 3 | 4 | namespace XamlTest; 5 | 6 | public sealed class TestRecorder : IAsyncDisposable 7 | { 8 | private record class InputStates 9 | { 10 | public bool IsLeftButtonDown { get; init; } 11 | public bool IsRightButtonDown { get; init; } 12 | public bool IsMiddleButtonDown { get; init; } 13 | 14 | public bool IsShiftDown { get; init; } 15 | public bool IsControlDown { get; init; } 16 | public bool IsAltDown { get; init; } 17 | 18 | public static InputStates Empty { get; } = new(); 19 | 20 | public static InputStates GetCurrentState() 21 | { 22 | return new InputStates 23 | { 24 | IsLeftButtonDown = GetCurrentState(VirtualKey.VK_LBUTTON), 25 | IsRightButtonDown = GetCurrentState(VirtualKey.VK_RBUTTON), 26 | IsMiddleButtonDown = GetCurrentState(VirtualKey.VK_MBUTTON), 27 | IsShiftDown = GetCurrentState(VirtualKey.VK_SHIFT), 28 | IsControlDown = GetCurrentState(VirtualKey.VK_CONTROL), 29 | IsAltDown = GetCurrentState(VirtualKey.VK_MENU) 30 | }; 31 | } 32 | 33 | private static bool GetCurrentState(VirtualKey key) 34 | { 35 | var ctrl = GetAsyncKeyState((int)key); 36 | return ((ushort)ctrl >> 15) == 1; 37 | } 38 | } 39 | 40 | public IApp App { get; } 41 | public string BaseFileName { get; } 42 | 43 | private InputStates Inputs { get; } = InputStates.GetCurrentState(); 44 | 45 | private bool IsDisposed { get; set; } 46 | public bool IsSuccess { get; private set; } 47 | 48 | private string Directory { get; } 49 | 50 | private string TestSuffix { get; } 51 | 52 | private int _imageIndex = 0; 53 | 54 | public TestRecorder(IApp app, 55 | string? suffix = null, 56 | [CallerFilePath] string callerFilePath = "", 57 | [CallerMemberName] string unitTestMethod = "") 58 | { 59 | App = app ?? throw new ArgumentNullException(nameof(app)); 60 | TestSuffix = suffix ?? ""; 61 | 62 | var callingAssembly = Assembly.GetCallingAssembly(); 63 | var assemblyName = callingAssembly.GetName().Name!; 64 | int assemblyNameIndex = callerFilePath.IndexOf(assemblyName); 65 | string directory; 66 | if (assemblyNameIndex >= 0) 67 | { 68 | directory = callerFilePath[(assemblyNameIndex + assemblyName.Length + 1)..]; 69 | } 70 | else 71 | { 72 | directory = Path.GetFileName(callerFilePath); 73 | } 74 | directory = Path.ChangeExtension(directory, "").TrimEnd('.'); 75 | var rootDirectory = Path.GetDirectoryName(callingAssembly.Location) ?? Path.GetFullPath("."); 76 | Directory = Path.Combine(rootDirectory, "Screenshots", directory); 77 | 78 | BaseFileName = unitTestMethod; 79 | foreach (char invalidChar in Path.GetInvalidFileNameChars()) 80 | { 81 | BaseFileName = BaseFileName.Replace($"{invalidChar}", ""); 82 | } 83 | if (Inputs != InputStates.Empty) 84 | { 85 | App.LogMessage($"WARNING: Test started with initial input states: {Inputs}"); 86 | } 87 | } 88 | 89 | /// 90 | /// Calling this method indicates that the test completed successfully and no additional recording is needed. 91 | /// 92 | public void Success(bool skipInputStateCheck = false) 93 | { 94 | if (!skipInputStateCheck) 95 | { 96 | var endingState = InputStates.GetCurrentState(); 97 | if (endingState != Inputs) 98 | { 99 | StringBuilder sb = new(); 100 | sb.AppendLine("Input states were not reset at the end of the test:"); 101 | if (endingState.IsLeftButtonDown != Inputs.IsLeftButtonDown) 102 | sb.AppendLine($" Left button down: {endingState.IsLeftButtonDown} (was {Inputs.IsLeftButtonDown})"); 103 | if (endingState.IsRightButtonDown != Inputs.IsRightButtonDown) 104 | sb.AppendLine($" Right button down: {endingState.IsRightButtonDown} (was {Inputs.IsRightButtonDown})"); 105 | if (endingState.IsMiddleButtonDown != Inputs.IsMiddleButtonDown) 106 | sb.AppendLine($" Middle button down: {endingState.IsMiddleButtonDown} (was {Inputs.IsMiddleButtonDown})"); 107 | if (endingState.IsShiftDown != Inputs.IsShiftDown) 108 | sb.AppendLine($" Shift key down: {endingState.IsShiftDown} (was {Inputs.IsShiftDown})"); 109 | if (endingState.IsControlDown != Inputs.IsControlDown) 110 | sb.AppendLine($" Control key down: {endingState.IsControlDown} (was {Inputs.IsControlDown})"); 111 | if (endingState.IsAltDown != Inputs.IsAltDown) 112 | sb.AppendLine($" Alt key down: {endingState.IsAltDown} (was {Inputs.IsAltDown})"); 113 | throw new XamlTestException(sb.ToString()); 114 | } 115 | App.LogMessage("Input states matched starting state."); 116 | } 117 | IsSuccess = true; 118 | } 119 | 120 | /// 121 | /// Enumerate all screenshots 122 | /// 123 | /// 124 | public IEnumerable EnumerateScreenshots() 125 | { 126 | if (!System.IO.Directory.Exists(Directory)) 127 | { 128 | return []; 129 | } 130 | return System.IO.Directory.EnumerateFiles(Directory, "*.jpg", SearchOption.AllDirectories); 131 | } 132 | 133 | public async Task SaveScreenshot([CallerLineNumber] int? lineNumber = null) 134 | => await SaveScreenshot(lineNumber?.ToString() ?? ""); 135 | 136 | public async Task SaveScreenshot(string suffix, [CallerLineNumber] int? lineNumber = null) 137 | => await SaveScreenshot($"{suffix}{lineNumber?.ToString() ?? ""}"); 138 | 139 | private async Task SaveScreenshot(string suffix) 140 | { 141 | string fileName = $"{BaseFileName}{TestSuffix}{suffix}-{Interlocked.Increment(ref _imageIndex)}.jpg"; 142 | System.IO.Directory.CreateDirectory(Directory); 143 | string fullPath = Path.Combine(Directory, fileName); 144 | File.Delete(fullPath); 145 | 146 | try 147 | { 148 | if (await App.GetScreenshot() is IImage screenshot) 149 | { 150 | await screenshot.Save(fullPath); 151 | return fullPath; 152 | } 153 | } 154 | catch (XamlTestException) { } 155 | return null; 156 | } 157 | 158 | #region IDisposable Support 159 | private async ValueTask DisposeAsync(bool disposing) 160 | { 161 | if (!IsDisposed) 162 | { 163 | if (disposing) 164 | { 165 | if (!IsSuccess) 166 | { 167 | await SaveScreenshot(""); 168 | } 169 | } 170 | IsDisposed = true; 171 | } 172 | } 173 | 174 | public ValueTask DisposeAsync() => DisposeAsync(true); 175 | #endregion 176 | 177 | } 178 | -------------------------------------------------------------------------------- /XAMLTest/Transport/CharSerializer.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest.Transport; 2 | 3 | public class CharSerializer : ISerializer 4 | { 5 | public bool CanSerialize(Type type, ISerializer rootSerializer) 6 | => type == typeof(char) || 7 | type == typeof(char?); 8 | 9 | public object? Deserialize(Type type, string value, ISerializer rootSerializer) 10 | { 11 | if (type == typeof(char)) 12 | { 13 | if (value?.Length == 1) 14 | { 15 | return value[0]; 16 | } 17 | } 18 | else if (type == typeof(char?)) 19 | { 20 | if (string.IsNullOrEmpty(value) || 21 | value.Length != 1) 22 | { 23 | return null; 24 | } 25 | return value[0]; 26 | } 27 | return '\0'; 28 | } 29 | 30 | public string Serialize(Type type, object? value, ISerializer rootSerializer) 31 | { 32 | return value switch 33 | { 34 | char c => c.ToString(), 35 | _ => "" 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /XAMLTest/Transport/DefaultSerializer.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace XamlTest.Transport; 4 | 5 | public class DefaultSerializer : ISerializer 6 | { 7 | public virtual bool CanSerialize(Type _, ISerializer rootSerializer) => true; 8 | 9 | public virtual string Serialize(Type type, object? value, ISerializer rootSerializer) 10 | { 11 | if (value is null) return ""; 12 | var converter = TypeDescriptor.GetConverter(type); 13 | return converter.ConvertToInvariantString(value) ?? ""; 14 | } 15 | 16 | public virtual object? Deserialize(Type type, string value, ISerializer rootSerializer) 17 | { 18 | var converter = TypeDescriptor.GetConverter(type); 19 | if (converter.CanConvertFrom(typeof(string))) 20 | { 21 | if (string.IsNullOrEmpty(value)) 22 | { 23 | return null; 24 | } 25 | return converter.ConvertFromInvariantString(value); 26 | } 27 | return value; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /XAMLTest/Transport/DependencyPropertyConverter.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest.Transport; 2 | 3 | public class DependencyPropertyConverter : ISerializer 4 | { 5 | public bool CanSerialize(Type type, ISerializer rootSerializer) => type == typeof(DependencyProperty); 6 | 7 | public object? Deserialize(Type type, string value, ISerializer rootSerializer) 8 | { 9 | if (type == typeof(DependencyProperty) && !string.IsNullOrEmpty(value)) 10 | { 11 | if (System.Text.Json.JsonSerializer.Deserialize(value) is { } data && 12 | DependencyPropertyHelper.TryGetDependencyProperty(data.Name!, data.OwnerType!, 13 | out DependencyProperty? dependencyProperty)) 14 | { 15 | return dependencyProperty; 16 | } 17 | } 18 | return null; 19 | } 20 | 21 | public string Serialize(Type type, object? value, ISerializer rootSerializer) 22 | { 23 | if (value is DependencyProperty dp) 24 | { 25 | return System.Text.Json.JsonSerializer.Serialize(new DependencyPropertyData 26 | { 27 | OwnerType = dp.OwnerType.AssemblyQualifiedName, 28 | Name = dp.Name 29 | }); 30 | } 31 | return ""; 32 | } 33 | 34 | private class DependencyPropertyData 35 | { 36 | public string? OwnerType { get; set; } 37 | public string? Name { get; set; } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /XAMLTest/Transport/DpiScaleSerializer.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace XamlTest.Transport; 5 | 6 | public class DpiScaleSerializer : JsonSerializer 7 | { 8 | private static JsonSerializerOptions? StaticOptions { get; } 9 | 10 | 11 | public override JsonSerializerOptions? Options => StaticOptions; 12 | 13 | static DpiScaleSerializer() 14 | { 15 | StaticOptions = new() 16 | { 17 | PropertyNameCaseInsensitive = true 18 | }; 19 | StaticOptions.Converters.Add(new DpiScaleJsonConverter()); 20 | } 21 | 22 | private class DpiScaleJsonConverter : JsonConverter 23 | { 24 | public override DpiScale Read( 25 | ref Utf8JsonReader reader, 26 | Type typeToConvert, 27 | JsonSerializerOptions options) 28 | { 29 | reader.Read(); //Start object 30 | 31 | double dpiX = 0.0; 32 | double dpiY = 0.0; 33 | 34 | ReadDoubleProperty(ref reader, out string? property1, out double value1); 35 | ReadDoubleProperty(ref reader, out string? property2, out double value2); 36 | 37 | switch (property1) 38 | { 39 | case nameof(DpiScale.DpiScaleX): 40 | dpiX = value1; 41 | break; 42 | case nameof(DpiScale.DpiScaleY): 43 | dpiY = value1; 44 | break; 45 | } 46 | switch (property2) 47 | { 48 | case nameof(DpiScale.DpiScaleX): 49 | dpiX = value2; 50 | break; 51 | case nameof(DpiScale.DpiScaleY): 52 | dpiY = value2; 53 | break; 54 | } 55 | 56 | reader.Read(); //End object 57 | return new(dpiX, dpiY); 58 | } 59 | 60 | public override void Write( 61 | Utf8JsonWriter writer, 62 | DpiScale dpiScale, 63 | JsonSerializerOptions options) 64 | { 65 | writer.WriteStartObject(); 66 | writer.WriteNumber(nameof(DpiScale.DpiScaleX), dpiScale.DpiScaleX); 67 | writer.WriteNumber(nameof(DpiScale.DpiScaleY), dpiScale.DpiScaleY); 68 | writer.WriteEndObject(); 69 | } 70 | 71 | private static void ReadDoubleProperty( 72 | ref Utf8JsonReader reader, 73 | out string? propertyName, 74 | out double value) 75 | { 76 | if (reader.TokenType != JsonTokenType.PropertyName) 77 | { 78 | throw new InvalidOperationException($"Expected property token but was '{reader.TokenType}'"); 79 | } 80 | propertyName = reader.GetString(); 81 | reader.Read(); //Read property name 82 | if (reader.TokenType != JsonTokenType.Number) 83 | { 84 | throw new InvalidOperationException($"Expected number token but was '{reader.TokenType}'"); 85 | } 86 | value = reader.GetDouble(); 87 | reader.Read(); 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /XAMLTest/Transport/GridSerializer.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Windows.Controls; 3 | 4 | namespace XamlTest.Transport; 5 | 6 | public class GridSerializer : ISerializer 7 | { 8 | public bool CanSerialize(Type type, ISerializer rootSerializer) 9 | => typeof(IEnumerable).IsAssignableFrom(type) || 10 | typeof(IEnumerable).IsAssignableFrom(type) || 11 | type == typeof(ColumnDefinition) || 12 | type == typeof(RowDefinition); 13 | 14 | public object? Deserialize(Type type, string value, ISerializer rootSerializer) 15 | { 16 | if (typeof(IEnumerable).IsAssignableFrom(type)) 17 | { 18 | List rv = new(); 19 | foreach (var data in JsonSerializer.Deserialize>(value) ?? Enumerable.Empty()) 20 | { 21 | rv.Add(ConvertFrom(data)); 22 | } 23 | return rv; 24 | } 25 | if (typeof(IEnumerable).IsAssignableFrom(type)) 26 | { 27 | List rv = new(); 28 | foreach (var data in JsonSerializer.Deserialize>(value) ?? Enumerable.Empty()) 29 | { 30 | rv.Add(ConvertFrom(data)); 31 | } 32 | return rv; 33 | } 34 | if (type == typeof(ColumnDefinition)) 35 | { 36 | var data = JsonSerializer.Deserialize(value); 37 | return data is null ? null : ConvertFrom(data); 38 | } 39 | if (type == typeof(RowDefinition)) 40 | { 41 | var data = JsonSerializer.Deserialize(value); 42 | return data is null ? null : ConvertFrom(data); 43 | } 44 | return null; 45 | } 46 | 47 | public string Serialize(Type type, object? value, ISerializer rootSerializer) 48 | { 49 | if (typeof(IEnumerable).IsAssignableFrom(type) && 50 | value is IEnumerable columnDefinitions) 51 | { 52 | return JsonSerializer.Serialize(ConvertTo(columnDefinitions)); 53 | } 54 | if (typeof(IEnumerable).IsAssignableFrom(type) && 55 | value is IEnumerable rowDefinitions) 56 | { 57 | return JsonSerializer.Serialize(ConvertTo(rowDefinitions)); 58 | } 59 | if (type == typeof(ColumnDefinition) && 60 | value is ColumnDefinition column) 61 | { 62 | return JsonSerializer.Serialize(ConvertTo(column)); 63 | } 64 | if (type == typeof(ColumnDefinition) && 65 | value is RowDefinition row) 66 | { 67 | return JsonSerializer.Serialize(ConvertTo(row)); 68 | } 69 | return ""; 70 | } 71 | 72 | private static GridLength ConvertFrom(GridLengthData? value) 73 | => value is null ? default : new(value.Value, value.GridUnitType); 74 | 75 | private static GridLengthData? ConvertTo(GridLength? value) 76 | { 77 | if (value is null) return null; 78 | return new() 79 | { 80 | GridUnitType = value.Value.GridUnitType, 81 | Value = value.Value.Value 82 | }; 83 | } 84 | 85 | private static ColumnDefinition ConvertFrom(ColumnDefinitionData value) 86 | => new() 87 | { 88 | MinWidth = value.MinWidth, 89 | MaxWidth = value.MaxWidth, 90 | Width = ConvertFrom(value?.Width) 91 | }; 92 | 93 | private static ColumnDefinitionData? ConvertTo(ColumnDefinition? value) 94 | => value is null ? default : new() 95 | { 96 | MinWidth = value.MinWidth, 97 | MaxWidth = value.MaxWidth, 98 | Width = ConvertTo(value.Width) 99 | }; 100 | 101 | private static List ConvertTo(IEnumerable value) 102 | => value.Select(x => ConvertTo(x)).OfType().ToList(); 103 | 104 | private static RowDefinition ConvertFrom(RowDefinitionData value) 105 | => new() 106 | { 107 | MinHeight = value.MinHeight, 108 | MaxHeight = value.MaxHeight, 109 | Height = ConvertFrom(value?.Height) 110 | }; 111 | 112 | private static RowDefinitionData? ConvertTo(RowDefinition? value) 113 | => value is null ? default : new() 114 | { 115 | MinHeight = value.MinHeight, 116 | MaxHeight = value.MaxHeight, 117 | Height = ConvertTo(value.Height) 118 | }; 119 | 120 | private static List ConvertTo(IEnumerable value) 121 | => value.Select(x => ConvertTo(x)).OfType().ToList(); 122 | } 123 | 124 | internal class ColumnDefinitionData 125 | { 126 | public GridLengthData? Width { get; set; } 127 | public double MinWidth { get; set; } 128 | public double MaxWidth { get; set; } 129 | } 130 | 131 | internal class RowDefinitionData 132 | { 133 | public GridLengthData? Height { get; set; } 134 | public double MinHeight { get; set; } 135 | public double MaxHeight { get; set; } 136 | } 137 | 138 | internal class GridLengthData 139 | { 140 | public double Value { get; set; } 141 | public GridUnitType GridUnitType { get; set; } 142 | } 143 | -------------------------------------------------------------------------------- /XAMLTest/Transport/JsonSerializer.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | namespace XamlTest.Transport; 4 | 5 | public abstract class JsonSerializer : ISerializer 6 | { 7 | public bool CanSerialize(Type type, ISerializer rootSerializer) 8 | => type == typeof(T); 9 | 10 | public virtual JsonSerializerOptions? Options { get; } 11 | 12 | public object? Deserialize(Type type, string value, ISerializer rootSerializer) 13 | { 14 | if (!string.IsNullOrEmpty(value)) 15 | { 16 | var rv = JsonSerializer.Deserialize(value, Options); 17 | return rv; 18 | } 19 | return default(T); 20 | } 21 | 22 | public string Serialize(Type type, object? value, ISerializer rootSerializer) 23 | { 24 | if (value is not null) 25 | { 26 | var rv = JsonSerializer.Serialize(value, Options); 27 | return rv; 28 | } 29 | return ""; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /XAMLTest/Transport/SecureStringSerializer.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using System.Security; 3 | 4 | namespace XamlTest.Transport; 5 | 6 | public class SecureStringSerializer : ISerializer 7 | { 8 | public bool CanSerialize(Type type, ISerializer rootSerializer) 9 | => type == typeof(SecureString); 10 | 11 | public object? Deserialize(Type type, string value, ISerializer rootSerializer) 12 | { 13 | var rv = new SecureString(); 14 | foreach(var c in value) 15 | { 16 | rv.AppendChar(c); 17 | } 18 | return rv; 19 | } 20 | 21 | public string Serialize(Type type, object? value, ISerializer rootSerializer) 22 | { 23 | if (value is SecureString secureString) 24 | { 25 | IntPtr valuePtr = IntPtr.Zero; 26 | try 27 | { 28 | valuePtr = Marshal.SecureStringToGlobalAllocUnicode(secureString); 29 | return Marshal.PtrToStringUni(valuePtr) ?? ""; 30 | } 31 | finally 32 | { 33 | Marshal.ZeroFreeGlobalAllocUnicode(valuePtr); 34 | } 35 | } 36 | return ""; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /XAMLTest/Transport/XamlSegmentSerializer.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | namespace XamlTest.Transport; 4 | 5 | public class XamlSegmentSerializer : ISerializer 6 | { 7 | public bool CanSerialize(Type type, ISerializer rootSerializer) 8 | => type == typeof(XamlSegment); 9 | 10 | public object? Deserialize(Type type, string value, ISerializer rootSerializer) 11 | { 12 | if (type == typeof(XamlSegment)) 13 | { 14 | return JsonSerializer.Deserialize(value); 15 | } 16 | return null; 17 | } 18 | 19 | public string Serialize(Type type, object? value, ISerializer rootSerializer) 20 | { 21 | return value switch 22 | { 23 | XamlSegment segment => JsonSerializer.Serialize(segment), 24 | _ => "" 25 | }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /XAMLTest/Utility/AppDomainMixins.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest.Utility; 2 | 3 | internal static class AppDomainMixins 4 | { 5 | public static void IncludeAssembliesIn(this AppDomain appDomain, string directory) 6 | { 7 | AssemblyResolver resolver = new(); 8 | resolver.IncludeFiles(Directory.EnumerateFiles(directory) 9 | .Where(file => Path.GetExtension(file).ToLowerInvariant() is ".dll" or ".exe")); 10 | 11 | appDomain.AssemblyResolve += AppDomainAssemblyResolve; 12 | 13 | Assembly? AppDomainAssemblyResolve(object? sender, ResolveEventArgs e) 14 | { 15 | if (e.Name is { } name && resolver.Resolve(name) is { } assembly) 16 | { 17 | return assembly; 18 | } 19 | return null; 20 | } 21 | } 22 | 23 | private class AssemblyResolver 24 | { 25 | private List<(AssemblyName Name, Lazy Assembly)> Assemblies { get; } 26 | = new List<(AssemblyName Name, Lazy Assembly)>(); 27 | 28 | public void IncludeFiles(IEnumerable assemblyFilePaths) 29 | { 30 | if (assemblyFilePaths is null) 31 | { 32 | throw new ArgumentNullException(nameof(assemblyFilePaths)); 33 | } 34 | 35 | foreach (var file in assemblyFilePaths) 36 | { 37 | try 38 | { 39 | Assemblies.Add((AssemblyName.GetAssemblyName(file), new Lazy(() => Assembly.LoadFrom(file)))); 40 | } 41 | catch(FileLoadException) 42 | { 43 | continue; 44 | } 45 | catch (BadImageFormatException) 46 | { 47 | continue; 48 | } 49 | } 50 | } 51 | 52 | public Assembly? Resolve(string assemblyName) 53 | { 54 | AssemblyName name = new(assemblyName); 55 | 56 | var possible = Assemblies.Where(x => x.Name.Name == name.Name); 57 | if (name.Version != null) 58 | { 59 | possible = possible.Where(x => x.Name.Version == name.Version); 60 | } 61 | //NB: AssemblyName.KeyPair throws PlatformNotSupportedException on .NET6 62 | //https://docs.microsoft.com/en-us/dotnet/api/system.reflection.assemblyname.keypair?view=net-6.0#system-reflection-assemblyname-keypair 63 | #if !NET6_0_OR_GREATER 64 | if (name.KeyPair != null) 65 | { 66 | possible = possible.Where(x => name.KeyPair.PublicKey.SequenceEqual(x.Name.KeyPair?.PublicKey ?? Array.Empty())); 67 | } 68 | #endif 69 | var found = possible.ToList(); 70 | if (found.Count == 1) 71 | { 72 | return found[0].Assembly.Value; 73 | } 74 | //TODO Handle 0 and multiple errors cases 75 | return null; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /XAMLTest/VTMixins.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | 4 | namespace XamlTest; 5 | 6 | public static class VTMixins 7 | { 8 | public static async Task Save(this IImage image, string filePath) 9 | { 10 | await using var file = File.OpenWrite(filePath); 11 | await image.Save(file); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /XAMLTest/VisualElementMixins.Highlight.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace XamlTest; 4 | 5 | public static partial class VisualElementMixins 6 | { 7 | /// 8 | /// Clears any exisitng highlight from a control. 9 | /// 10 | /// 11 | public static Task ClearHighlight(this IVisualElement element) 12 | => element.Highlight(HighlightConfig.None); 13 | 14 | /// 15 | /// Applys a highlight with the default configuration. 16 | /// 17 | /// The element to highlight 18 | /// 19 | public static Task Highlight(this IVisualElement element) 20 | => element.Highlight(HighlightConfig.Default); 21 | } 22 | -------------------------------------------------------------------------------- /XAMLTest/VisualElementMixins.Input.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Input; 2 | using XamlTest.Input; 3 | 4 | namespace XamlTest; 5 | 6 | public static partial class VisualElementMixins 7 | { 8 | private const int SingleClick = 1; 9 | private const int DoubleClick = 2; 10 | 11 | public static async Task MoveCursorTo(this IVisualElement element, 12 | Position position = Position.Center, 13 | int xOffset = 0, 14 | int yOffset = 0) 15 | { 16 | List inputs = [MouseInput.MoveToElement(position)]; 17 | if (xOffset != 0 || yOffset != 0) 18 | { 19 | inputs.Add(MouseInput.MoveRelative(xOffset, yOffset)); 20 | } 21 | return await element.SendInput(new MouseInput(inputs.ToArray())); 22 | } 23 | 24 | public static async Task LeftClick(this IVisualElement element, 25 | Position position = Position.Center, 26 | int xOffset = 0, int yOffset = 0, 27 | TimeSpan? clickTime = null) 28 | { 29 | return await SendClick(element, 30 | MouseInput.LeftDown(), 31 | MouseInput.LeftUp(), 32 | position, 33 | xOffset, 34 | yOffset, 35 | clickTime); 36 | } 37 | 38 | public static async Task LeftDoubleClick(this IVisualElement element, 39 | Position position = Position.Center, 40 | int xOffset = 0, int yOffset = 0, 41 | TimeSpan? clickTime = null) 42 | { 43 | return await SendDoubleClick(element, 44 | MouseInput.LeftDown(), 45 | MouseInput.LeftUp(), 46 | position, 47 | xOffset, 48 | yOffset, 49 | clickTime); 50 | } 51 | 52 | public static async Task RightClick(this IVisualElement element, 53 | Position position = Position.Center, 54 | int xOffset = 0, int yOffset = 0, 55 | TimeSpan? clickTime = null) 56 | { 57 | return await SendClick(element, 58 | MouseInput.RightDown(), 59 | MouseInput.RightUp(), 60 | position, 61 | xOffset, 62 | yOffset, 63 | clickTime); 64 | } 65 | 66 | public static async Task SendClick(IVisualElement element, 67 | MouseInput down, 68 | MouseInput up, 69 | Position position, 70 | int xOffset, 71 | int yOffset, 72 | TimeSpan? clickTime) 73 | { 74 | return await SendClick(element, down, up, position, xOffset, yOffset, clickTime, SingleClick); 75 | } 76 | 77 | public static async Task SendDoubleClick(IVisualElement element, 78 | MouseInput down, 79 | MouseInput up, 80 | Position position, 81 | int xOffset, 82 | int yOffset, 83 | TimeSpan? clickTime) 84 | { 85 | return await SendClick(element, down, up, position, xOffset, yOffset, clickTime, DoubleClick); 86 | } 87 | 88 | private static async Task SendClick(IVisualElement element, 89 | MouseInput down, 90 | MouseInput up, 91 | Position position, 92 | int xOffset, 93 | int yOffset, 94 | TimeSpan? clickTime, 95 | int clickCount) 96 | { 97 | List inputs = [MouseInput.MoveToElement(position)]; 98 | if (xOffset != 0 || yOffset != 0) 99 | { 100 | inputs.Add(MouseInput.MoveRelative(xOffset, yOffset)); 101 | } 102 | for (int i = 0; i < clickCount; i++) 103 | { 104 | inputs.Add(down); 105 | if (clickTime != null) 106 | { 107 | inputs.Add(MouseInput.Delay(clickTime.Value)); 108 | } 109 | inputs.Add(up); 110 | } 111 | 112 | return await element.SendInput(new MouseInput([.. inputs])); 113 | } 114 | 115 | public static async Task SendKeyboardInput(this IVisualElement element, FormattableString input) 116 | { 117 | var placeholder = Guid.NewGuid().ToString("N"); 118 | string formatted = string.Format(input.Format, Enumerable.Repeat(placeholder, input.ArgumentCount).Cast().ToArray()); 119 | string[] textParts = formatted.Split(placeholder); 120 | 121 | var inputs = new List(); 122 | int argumentIndex = 0; 123 | foreach (string? part in textParts) 124 | { 125 | if (!string.IsNullOrEmpty(part)) 126 | { 127 | inputs.Add(new TextInput(part)); 128 | } 129 | if (argumentIndex < input.ArgumentCount) 130 | { 131 | object? argument = input.GetArgument(argumentIndex++); 132 | switch (argument) 133 | { 134 | case ModifierKeys modifiers: 135 | inputs.Add(new ModifiersInput(modifiers)); 136 | break; 137 | case Key key: 138 | inputs.Add(new KeysInput(key)); 139 | break; 140 | case IEnumerable keys: 141 | inputs.Add(new KeysInput(keys)); 142 | break; 143 | default: 144 | string? stringArgument = argument?.ToString(); 145 | if (!string.IsNullOrEmpty(stringArgument)) 146 | { 147 | inputs.Add(new TextInput(stringArgument)); 148 | } 149 | break; 150 | } 151 | } 152 | } 153 | await element.SendInput(new KeyboardInput(inputs.ToArray())); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /XAMLTest/VisualElementMixins.Query.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest; 2 | 3 | static partial class VisualElementMixins 4 | { 5 | public static Task> GetElement( 6 | this IVisualElement element, 7 | IQuery query) 8 | => element.GetElement(query.QueryString); 9 | 10 | public static Task> GetElement( 11 | this IVisualElement element) 12 | => element.GetElement(ElementQuery.OfType()); 13 | 14 | public static Task?> FindElement( 15 | this IVisualElement element, 16 | IQuery query) 17 | => element.FindElement(query.QueryString); 18 | 19 | public static Task?> FindElement( 20 | this IVisualElement element) 21 | => element.FindElement(ElementQuery.OfType()); 22 | } 23 | -------------------------------------------------------------------------------- /XAMLTest/VisualElementMixins.RemoteExecute.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest; 2 | 3 | public static partial class VisualElementMixins 4 | { 5 | public static Task RemoteExecute(this IVisualElement element, 6 | Func action) 7 | { 8 | return element.RemoteExecute(action, []); 9 | } 10 | 11 | public static Task RemoteExecute(this IVisualElement element, 12 | Func action, T1 param1) 13 | { 14 | return element.RemoteExecute(action, [param1]); 15 | } 16 | 17 | public static Task RemoteExecute(this IVisualElement element, 18 | Func action, T1 param1, T2 param2) 19 | { 20 | return element.RemoteExecute(action, [param1, param2]); 21 | } 22 | 23 | public static Task RemoteExecute(this IVisualElement element, 24 | Func action, T1 param1, T2 param2, T3 param3) 25 | { 26 | return element.RemoteExecute(action, [param1, param2, param3]); 27 | } 28 | 29 | public static Task RemoteExecute(this IVisualElement element, 30 | Func action, T1 param1, T2 param2, T3 param3, T4 param4) 31 | { 32 | return element.RemoteExecute(action, [param1, param2, param3, param4]); 33 | } 34 | 35 | public static Task RemoteExecute(this IVisualElement element, 36 | Func action, T1 param1, T2 param2, T3 param3, T4 param4, T5 param5) 37 | { 38 | return element.RemoteExecute(action, [param1, param2, param3, param4, param5]); 39 | } 40 | 41 | public static Task RemoteExecute(this IVisualElement element, 42 | Func action, 43 | T1 param1, T2 param2, T3 param3, T4 param4, T5 param5, T6 param6) 44 | { 45 | return element.RemoteExecute(action, [param1, param2, param3, param4, param5, param6]); 46 | } 47 | 48 | public static Task RemoteExecute(this IVisualElement element, 49 | Func action, 50 | T1 param1, T2 param2, T3 param3, T4 param4, T5 param5, T6 param6, T7 param7) 51 | { 52 | return element.RemoteExecute(action, [param1, param2, param3, param4, param5, param6, param7]); 53 | } 54 | 55 | public static Task RemoteExecute(this IVisualElement element, 56 | Func action, 57 | T1 param1, T2 param2, T3 param3, T4 param4, T5 param5, T6 param6, T7 param7, T8 param8) 58 | { 59 | return element.RemoteExecute(action, [param1, param2, param3, param4, param5, param6, param7, param8]); 60 | } 61 | 62 | public static Task RemoteExecute(this IVisualElement element, Action action) 63 | { 64 | return element.RemoteExecute(action, []); 65 | } 66 | 67 | public static Task RemoteExecute(this IVisualElement element, 68 | Action action, T1 param1) 69 | { 70 | return element.RemoteExecute(action, [param1]); 71 | } 72 | 73 | public static Task RemoteExecute(this IVisualElement element, 74 | Action action, T1 param1, T2 param2) 75 | { 76 | return element.RemoteExecute(action, [param1, param2]); 77 | } 78 | 79 | public static Task RemoteExecute(this IVisualElement element, 80 | Action action, T1 param1, T2 param2, T3 param3) 81 | { 82 | return element.RemoteExecute(action, [param1, param2, param3]); 83 | } 84 | 85 | public static Task RemoteExecute(this IVisualElement element, 86 | Action action, T1 param1, T2 param2, T3 param3, T4 param4) 87 | { 88 | return element.RemoteExecute(action, [param1, param2, param3, param4]); 89 | } 90 | 91 | public static Task RemoteExecute(this IVisualElement element, 92 | Action action, T1 param1, T2 param2, T3 param3, T4 param4, T5 param5) 93 | { 94 | return element.RemoteExecute(action, [param1, param2, param3, param4, param5]); 95 | } 96 | 97 | public static Task RemoteExecute(this IVisualElement element, 98 | Action action, T1 param1, T2 param2, T3 param3, T4 param4, T5 param5, T6 param6) 99 | { 100 | return element.RemoteExecute(action, [param1, param2, param3, param4, param5, param6]); 101 | } 102 | 103 | public static Task RemoteExecute(this IVisualElement element, 104 | Action action, 105 | T1 param1, T2 param2, T3 param3, T4 param4, T5 param5, T6 param6, T7 param7) 106 | { 107 | return element.RemoteExecute(action, [param1, param2, param3, param4, param5, param6, param7]); 108 | } 109 | 110 | public static Task RemoteExecute(this IVisualElement element, 111 | Action action, 112 | T1 param1, T2 param2, T3 param3, T4 param4, T5 param5, T6 param6, T7 param7, T8 param8) 113 | { 114 | return element.RemoteExecute(action, [param1, param2, param3, param4, param5, param6, param7, param8]); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /XAMLTest/VisualElementMixins.Window.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Controls; 2 | 3 | namespace XamlTest; 4 | 5 | public static partial class VisualElementMixins 6 | { 7 | public static async Task SetXamlContent(this IWindow window, XamlSegment xaml) 8 | { 9 | if (window is null) 10 | { 11 | throw new ArgumentNullException(nameof(window)); 12 | } 13 | 14 | if (xaml is null) 15 | { 16 | throw new ArgumentNullException(nameof(xaml)); 17 | } 18 | await using var layout = await window.RegisterForEvent(nameof(Window.ContentRendered)); 19 | IVisualElement element = await window.SetXamlProperty(nameof(Window.Content), xaml); 20 | await Wait.For(async () => (await layout.GetInvocations()).Any()); 21 | return element; 22 | } 23 | 24 | public static async Task> SetXamlContent(this IWindow window, XamlSegment xaml) 25 | { 26 | if (window is null) 27 | { 28 | throw new ArgumentNullException(nameof(window)); 29 | } 30 | 31 | if (xaml is null) 32 | { 33 | throw new ArgumentNullException(nameof(xaml)); 34 | } 35 | await using var layout = await window.RegisterForEvent(nameof(Window.ContentRendered)); 36 | IVisualElement element = await window.SetXamlProperty(nameof(Window.Content), xaml); 37 | await Wait.For(async () => (await layout.GetInvocations()).Any()); 38 | return element; 39 | } 40 | 41 | public static async Task> SetXamlContentFromUserControl(this IWindow window) 42 | where TUserControl : UserControl 43 | { 44 | if (window is null) 45 | { 46 | throw new ArgumentNullException(nameof(window)); 47 | } 48 | 49 | XamlSegment segment = new($"", 50 | new XmlNamespace("local", $"clr-namespace:{typeof(TUserControl).Namespace};assembly={typeof(TUserControl).Assembly.GetName().Name}")); 51 | return await window.SetXamlContent(segment); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /XAMLTest/VisualElementMixins.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Threading.Tasks; 4 | using System.Windows; 5 | using System.Windows.Media; 6 | using XamlTest.Host; 7 | using XamlTest.Internal; 8 | 9 | namespace XamlTest; 10 | 11 | public static partial class VisualElementMixins 12 | { 13 | public static async Task GetEffectiveBackground(this IVisualElement element) 14 | => await element.GetEffectiveBackground(null); 15 | 16 | public static async Task GetProperty(this IVisualElement element, string name) 17 | { 18 | if (element is null) 19 | { 20 | throw new ArgumentNullException(nameof(element)); 21 | } 22 | 23 | return await element.GetProperty(name, null); 24 | } 25 | 26 | public static async Task GetProperty(this IVisualElement element, string propertyName) 27 | { 28 | if (element is null) 29 | { 30 | throw new ArgumentNullException(nameof(element)); 31 | } 32 | 33 | IValue value = await element.GetProperty(propertyName); 34 | 35 | return value.GetAs(); 36 | } 37 | 38 | public static async Task GetProperty(this IVisualElement element, DependencyProperty dependencyProperty) 39 | { 40 | if (element is null) 41 | { 42 | throw new ArgumentNullException(nameof(element)); 43 | } 44 | 45 | if (dependencyProperty is null) 46 | { 47 | throw new ArgumentNullException(nameof(dependencyProperty)); 48 | } 49 | 50 | IValue value = await element.GetProperty(dependencyProperty.Name, dependencyProperty.OwnerType.AssemblyQualifiedName); 51 | return value.GetAs(); 52 | } 53 | 54 | public static async Task SetProperty(this IVisualElement element, 55 | string name, string value, string? valueType = null) 56 | { 57 | return await element.SetProperty(name, value, valueType, null); 58 | } 59 | 60 | public static async Task SetProperty(this IVisualElement element, DependencyProperty dependencyProperty, T value) 61 | { 62 | if (element is null) 63 | { 64 | throw new ArgumentNullException(nameof(element)); 65 | } 66 | 67 | if (dependencyProperty is null) 68 | { 69 | throw new ArgumentNullException(nameof(dependencyProperty)); 70 | } 71 | 72 | return await SetProperty(element, dependencyProperty.Name, value, dependencyProperty.OwnerType.AssemblyQualifiedName); 73 | } 74 | 75 | public static async Task SetProperty(this IVisualElement element, string propertyName, T value) 76 | { 77 | if (element is null) 78 | { 79 | throw new ArgumentNullException(nameof(element)); 80 | } 81 | 82 | if (string.IsNullOrEmpty(propertyName)) 83 | { 84 | throw new ArgumentException($"'{nameof(propertyName)}' cannot be null or empty", nameof(propertyName)); 85 | } 86 | 87 | return await SetProperty(element, propertyName, value, null); 88 | } 89 | 90 | private static async Task SetProperty(IVisualElement element, string propertyName, T value, string? ownerType) 91 | { 92 | IValue newValue = await element.SetProperty(propertyName, (value != null ? Convert.ToString(value, CultureInfo.InvariantCulture) : "") ?? "", typeof(T).AssemblyQualifiedName, ownerType); 93 | if (newValue is { }) 94 | { 95 | return newValue.GetAs(); 96 | } 97 | return default; 98 | } 99 | 100 | public static IValidation Validation(this IVisualElement element) 101 | where T : DependencyObject 102 | { 103 | return new Validation(element); 104 | } 105 | 106 | public static Task GetScale(this IVisualElement element) 107 | where T : Visual 108 | { 109 | return element.RemoteExecute(GetVisualScale); 110 | } 111 | 112 | internal static DpiScale GetVisualScale(Visual visual) 113 | { 114 | PresentationSource source = PresentationSource.FromVisual(visual); 115 | 116 | if (source is not null) 117 | { 118 | double scaleX = source.CompositionTarget.TransformToDevice.M11; 119 | double scaleY = source.CompositionTarget.TransformToDevice.M22; 120 | return new(scaleX, scaleY); 121 | } 122 | return new(1.0, 1.0); 123 | } 124 | } 125 | 126 | -------------------------------------------------------------------------------- /XAMLTest/VisualStudioAttacher.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using System.Runtime.InteropServices.ComTypes; 3 | using EnvDTE; 4 | using DTEProcess = EnvDTE.Process; 5 | using Process = System.Diagnostics.Process; 6 | 7 | namespace XamlTest; 8 | 9 | public class VisualStudioAttacher 10 | { 11 | [DllImport("ole32.dll")] 12 | private static extern int CreateBindCtx(int reserved, out IBindCtx ppbc); 13 | 14 | [DllImport("ole32.dll")] 15 | private static extern int GetRunningObjectTable(int reserved, out IRunningObjectTable prot); 16 | 17 | public static string? GetSolutionForVisualStudio(Process visualStudioProcess) 18 | { 19 | 20 | if (TryGetVsInstance(visualStudioProcess.Id, out _DTE? visualStudioInstance)) 21 | { 22 | try 23 | { 24 | return visualStudioInstance.Solution.FullName; 25 | } 26 | catch (Exception) 27 | { 28 | } 29 | } 30 | return null; 31 | } 32 | 33 | public static async Task AttachVisualStudioToProcess(Process applicationProcess) 34 | { 35 | await Wait.For(() => Task.FromResult(AttachVisualStudioToProcessImplementation(applicationProcess)), 36 | retry: new Retry(5, TimeSpan.FromSeconds(15)), 37 | message: "Failed to attach Visual Studio to the XAMLTest host process"); 38 | 39 | static bool AttachVisualStudioToProcessImplementation(Process applicationProcess) 40 | { 41 | if (GetAttachedVisualStudio(Process.GetCurrentProcess()) is { } vsProcess) 42 | { 43 | DTEProcess? processToAttachTo = vsProcess 44 | .Parent 45 | .LocalProcesses 46 | .Cast() 47 | .FirstOrDefault(process => { 48 | try 49 | { 50 | return process.ProcessID == applicationProcess.Id; 51 | } 52 | catch(COMException) 53 | { 54 | return false; 55 | } 56 | }); 57 | 58 | if (processToAttachTo != null) 59 | { 60 | processToAttachTo.Attach(); 61 | return true; 62 | } 63 | } 64 | return false; 65 | } 66 | } 67 | 68 | private static DTEProcess? GetAttachedVisualStudio(Process applicationProcess) 69 | { 70 | IEnumerable visualStudios = GetVisualStudioProcesses(); 71 | 72 | foreach (Process visualStudio in visualStudios) 73 | { 74 | if (TryGetVsInstance(visualStudio.Id, out _DTE? visualStudioInstance)) 75 | { 76 | try 77 | { 78 | foreach (DTEProcess? debuggedProcess in visualStudioInstance.Debugger.DebuggedProcesses) 79 | { 80 | if (debuggedProcess?.ProcessID == applicationProcess.Id) 81 | { 82 | return debuggedProcess; 83 | } 84 | } 85 | } 86 | catch (Exception) 87 | { } 88 | } 89 | } 90 | return null; 91 | } 92 | 93 | private static IEnumerable GetVisualStudioProcesses() 94 | => Process.GetProcesses().Where(o => o.ProcessName.Contains("devenv")); 95 | 96 | private static bool TryGetVsInstance(int processId, [NotNullWhen(true)] out _DTE? instance) 97 | { 98 | IntPtr numFetched = IntPtr.Zero; 99 | IMoniker[] monikers = new IMoniker[1]; 100 | 101 | _ = GetRunningObjectTable(0, out IRunningObjectTable runningObjectTable); 102 | runningObjectTable.EnumRunning(out IEnumMoniker monikerEnumerator); 103 | monikerEnumerator.Reset(); 104 | 105 | while (monikerEnumerator.Next(1, monikers, numFetched) == 0) 106 | { 107 | _ = CreateBindCtx(0, out IBindCtx ctx); 108 | 109 | monikers[0].GetDisplayName(ctx, null, out string runningObjectName); 110 | 111 | runningObjectTable.GetObject(monikers[0], out object runningObjectVal); 112 | 113 | if (runningObjectVal is _DTE dte && runningObjectName.StartsWith("!VisualStudio") && 114 | runningObjectName.Split(':') is { } parts && 115 | parts.Length >= 2 && 116 | int.TryParse(parts[1], out int currentProcessId) && 117 | currentProcessId == processId) 118 | { 119 | instance = dte; 120 | return true; 121 | } 122 | } 123 | 124 | instance = null; 125 | return false; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /XAMLTest/Wait.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest; 2 | 3 | [DebuggerStepThrough] 4 | public static class Wait 5 | { 6 | public static async Task For(Func> action, Retry? retry = null, string? message = null) 7 | { 8 | if (action is null) 9 | { 10 | throw new ArgumentNullException(nameof(action)); 11 | } 12 | 13 | retry ??= Retry.Default; 14 | 15 | int delay = (int)(retry.Timeout.TotalMilliseconds / 10); 16 | if (delay < 15) 17 | { 18 | delay = 0; 19 | } 20 | 21 | int numAttempts = 0; 22 | var sw = Stopwatch.StartNew(); 23 | Exception? thrownException = null; 24 | do 25 | { 26 | numAttempts++; 27 | try 28 | { 29 | if (await action()) 30 | { 31 | //Success 32 | return; 33 | } 34 | } 35 | catch (Exception ex) 36 | { 37 | thrownException = ex; 38 | } 39 | if (delay > 0) 40 | { 41 | await Task.Delay(delay); 42 | } 43 | } 44 | while (ShouldRetry()); 45 | var prefix = message == null ? string.Empty : $"{message}. "; 46 | throw new TimeoutException($"{prefix}Timeout of '{retry}' exceeded", thrownException); 47 | 48 | bool ShouldRetry() => 49 | sw.Elapsed <= retry.Timeout || 50 | numAttempts < retry.MinAttempts; 51 | } 52 | 53 | public static async Task For(Func action, Retry? retry = null, string? message = null) 54 | { 55 | await For(async () => 56 | { 57 | await action(); 58 | return true; 59 | }, retry, message); 60 | } 61 | 62 | public static async Task For(Func> action, Retry? retry = null, string? message = null) 63 | where T : class 64 | { 65 | T? rv = default; 66 | await For(async () => 67 | { 68 | rv = await action(); 69 | return true; 70 | }, retry, message); 71 | 72 | return rv ?? throw new XamlTestException("Return value is null"); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /XAMLTest/WindowMixins.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using System.Windows; 3 | 4 | namespace XamlTest; 5 | 6 | public static class WindowMixins 7 | { 8 | public static Task WaitForLoaded(this IWindow window) 9 | => Wait.For(async () => await window.GetIsLoaded()); 10 | 11 | public static async Task GetIsLoaded(this IWindow window) 12 | => await window.GetProperty(nameof(Window.IsLoaded)); 13 | } 14 | -------------------------------------------------------------------------------- /XAMLTest/XAMLTest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WinExe 5 | net8.0-windows7;net9.0-windows7 6 | true 7 | true 8 | XamlTest 9 | true 10 | ..\Images\Icon.ico 11 | true 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | $(TargetsForTfmSpecificContentInPackage);IncludeExeInPackage 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | build\$(TargetFramework) 62 | 63 | 64 | build\$(TargetFramework) 65 | 66 | 67 | 68 | build\$(TargetFramework) 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /XAMLTest/XAMLTestException.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest; 2 | 3 | public class XamlTestException : Exception 4 | { 5 | public XamlTestException() 6 | { } 7 | 8 | public XamlTestException(string? message) 9 | : base(message) 10 | { } 11 | 12 | public XamlTestException(string? message, Exception? innerException) 13 | : base(message, innerException) 14 | { } 15 | } 16 | -------------------------------------------------------------------------------- /XAMLTest/XamlSegment.cs: -------------------------------------------------------------------------------- 1 | namespace XamlTest; 2 | 3 | public sealed class XamlSegment 4 | { 5 | public string Xaml { get; } 6 | public IReadOnlyList Namespaces { get; } 7 | 8 | public XamlSegment(string xaml, params XmlNamespace[] namespaces) 9 | { 10 | Xaml = xaml; 11 | Namespaces = namespaces; 12 | } 13 | 14 | public static implicit operator XamlSegment(string xamlString) => new(xamlString); 15 | 16 | public override string ToString() 17 | { 18 | StringBuilder sb = new(); 19 | foreach(var @namespace in Namespaces) 20 | { 21 | sb.AppendLine(@namespace.ToString()); 22 | } 23 | sb.Append(Xaml); 24 | return sb.ToString(); 25 | } 26 | } -------------------------------------------------------------------------------- /XAMLTest/XmlNamespace.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | 5 | namespace XamlTest; 6 | 7 | public sealed class XmlNamespace : IEquatable 8 | { 9 | public string? Prefix { get; } 10 | public string Uri { get; } 11 | 12 | public XmlNamespace(string? prefix, string uri) 13 | { 14 | if (string.IsNullOrEmpty(uri)) 15 | { 16 | throw new ArgumentException($"'{nameof(uri)}' cannot be null or empty.", nameof(uri)); 17 | } 18 | 19 | Prefix = prefix; 20 | Uri = uri; 21 | } 22 | 23 | public override string ToString() 24 | { 25 | if (string.IsNullOrWhiteSpace(Prefix)) 26 | { 27 | return $"xmlns=\"{Uri}\""; 28 | } 29 | return $"xmlns:{Prefix}=\"{Uri}\""; 30 | } 31 | 32 | public override int GetHashCode() => HashCode.Combine(Prefix, Uri); 33 | 34 | public static bool operator ==(XmlNamespace? left, XmlNamespace? right) 35 | { 36 | return EqualityComparer.Default.Equals(left, right); 37 | } 38 | 39 | public static bool operator !=(XmlNamespace? left, XmlNamespace? right) 40 | { 41 | return !(left == right); 42 | } 43 | 44 | public override bool Equals(object? obj) 45 | { 46 | return Equals(obj as XmlNamespace); 47 | } 48 | 49 | public bool Equals([AllowNull] XmlNamespace other) 50 | { 51 | if (other is null) return false; 52 | return other.Prefix == Prefix && other.Uri == Uri; 53 | } 54 | } -------------------------------------------------------------------------------- /XAMLTest/XmlNamespaceMixins.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace XamlTest; 4 | 5 | public static class XmlNamespaceMixins 6 | { 7 | public static void Add(this IList list, string? prefix, string uri) 8 | => list.Add(new XmlNamespace(prefix, uri)); 9 | } 10 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "allowPrerelease": true 4 | } 5 | } --------------------------------------------------------------------------------