├── .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 | [](https://github.com/Keboo/XAMLTest/actions/workflows/dotnet-core.yml)
3 | [](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