diff --git a/tests/Fluent.UITests/.editorconfig b/tests/Fluent.UITests/.editorconfig new file mode 100644 index 0000000..8293aa2 --- /dev/null +++ b/tests/Fluent.UITests/.editorconfig @@ -0,0 +1,252 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = false + +#### .NET Code Actions #### + +# Type members +dotnet_hide_advanced_members = false +dotnet_member_insertion_location = with_other_members_of_the_same_kind +dotnet_property_generation_behavior = prefer_throwing_properties + +# Symbol search +dotnet_search_reference_assemblies = true + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_prefer_system_hash_code = true +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_collection_expression = when_types_loosely_match +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true + +# Parameter preferences +dotnet_code_quality_unused_parameters = all + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true +csharp_style_expression_bodied_constructors = false +csharp_style_expression_bodied_indexers = true +csharp_style_expression_bodied_lambdas = true +csharp_style_expression_bodied_local_functions = false +csharp_style_expression_bodied_methods = false +csharp_style_expression_bodied_operators = false +csharp_style_expression_bodied_properties = true + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true + +# Modifier preferences +csharp_prefer_static_anonymous_function = true +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async +csharp_style_prefer_readonly_struct = true +csharp_style_prefer_readonly_struct_member = true + +# Code-block preferences +csharp_prefer_braces = true +csharp_prefer_simple_using_statement = true +csharp_prefer_system_threading_lock = true +csharp_style_namespace_declarations = block_scoped +csharp_style_prefer_method_group_conversion = true +csharp_style_prefer_primary_constructors = true +csharp_style_prefer_top_level_statements = true + +# Expression-level preferences +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_local_over_anonymous_function = true +csharp_style_prefer_null_check_over_type_check = true +csharp_style_prefer_range_operator = true +csharp_style_prefer_tuple_swap = true +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true +max_line_length=120 + +csharp_wrap_after_declaration_lpar=true +csharp_wrap_parameters_style=chop_if_long +csharp_max_formal_parameters_on_line=2 + +csharp_wrap_after_invocation_lpar=true +csharp_wrap_arguments_style=chop_if_long +csharp_max_invocation_arguments_on_line=2 +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case diff --git a/tests/Fluent.UITests/ApplicationFixture.cs b/tests/Fluent.UITests/ApplicationFixture.cs new file mode 100644 index 0000000..85de16b --- /dev/null +++ b/tests/Fluent.UITests/ApplicationFixture.cs @@ -0,0 +1,160 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.IO.Packaging; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Threading; + +namespace Fluent.UITests; + + +public class ApplicationFixture : IDisposable +{ + public ApplicationFixture() + { + var waitForApplicationRun = new TaskCompletionSource(); + Thread thread = new Thread(() => + { + Application application = new Application(); + application.Startup += (s, e) => { waitForApplicationRun.SetResult(true); }; + application.Run(); + //Dispatcher.Run(); + }); + + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + waitForApplicationRun.Task.Wait(); + App = Application.Current; + } + + public void ResetApplicationInstance() + { + Execute(() => + { + App.ThemeMode = ThemeMode.None; + App.MainWindow = null; + if (App.Resources.Count > 0 + || App.Resources.MergedDictionaries.Count > 0) + { + App.Resources.Clear(); + } + }); + } + + public void Execute(Action action) + { + Execute(Application.Current.Dispatcher, action); + } + + private void Execute(Dispatcher dispatcher, Action action) + { + Exception exception = null; + if (dispatcher.CheckAccess()) + { + action(); + } + else + { + var workComplete = new AutoResetEvent(false); + dispatcher.InvokeAsync(() => + { + try + { + action(); + } + catch (Exception e) + { + exception = e; + throw; + } + finally // Unblock calling thread even if action() throws + { + workComplete.Set(); + } + }); + + workComplete.WaitOne(); + if (exception != null) + { + throw exception; + } + } + + } + + private T ExecuteFunc(Func func) + { + return ExecuteFunc(Application.Current.Dispatcher, func); + } + + private T? ExecuteFunc(Dispatcher dispatcher, Func func) + { + Exception exception = null; + T? result = default(T); + if (dispatcher.CheckAccess()) + { + result = func(); + } + else + { + var workComplete = new AutoResetEvent(false); + dispatcher.InvokeAsync(() => + { + try + { + result = func(); + } + catch (Exception e) + { + exception = e; + throw; + } + finally // Unblock calling thread even if action() throws + { + workComplete.Set(); + } + }); + + workComplete.WaitOne(); + if (exception != null) + { + throw exception; + } + + } + return result; + } + + public Window GetMainWindow() + { + + Window window = ExecuteFunc(() => + { + return Application.Current.MainWindow; + }); + return window; + } + + public void Dispose() + { + Execute(() => + { + Application.Current.Shutdown(); + }); + } + + public Application App { get; private set; } +} + + +[CollectionDefinition("Application Tests Collection", DisableParallelization = true)] +public class ApplicationTestsCollection : ICollectionFixture +{ + // This class has no code, and is never created. Its purpose is simply + // to be the place to apply [CollectionDefinition] and all the + // ICollectionFixture<> interfaces. +} + diff --git a/tests/Fluent.UITests/ApplicationThemeModeTests.cs b/tests/Fluent.UITests/ApplicationThemeModeTests.cs new file mode 100644 index 0000000..139d48d --- /dev/null +++ b/tests/Fluent.UITests/ApplicationThemeModeTests.cs @@ -0,0 +1,226 @@ +using FluentAssertions; +using System.Windows; +using System.Windows.Media; + +namespace Fluent.UITests; + +[Collection("Application Tests Collection")] +public class ApplicationThemeModeTests : IDisposable +{ + ApplicationFixture _fixture; + + public ApplicationThemeModeTests(ApplicationFixture fixture) + { + _fixture = fixture; + } + + [WpfFact] + public void Application_ThemeMode_Default() + { + _fixture.Execute(() => + { + Window window = new Window(); + _fixture.App.MainWindow = window; + window.ApplyTemplate(); + }); + + _fixture.Execute(() => + { + _fixture.App.ThemeMode.Value.Should().Be("None"); + _fixture.App.Resources.MergedDictionaries.Count.Should().Be(0); + + Window window = _fixture.App.MainWindow; + window.Should().NotBeNull(); + window.Background.Should().BeNull(); + window.Resources.MergedDictionaries.Should().BeEmpty(); + }); + + } + + [WpfTheory] + [MemberData(nameof(ThemeModes))] + public void Application_ThemeMode_Initialization_Tests(ThemeMode themeMode) + { + if (themeMode == ThemeMode.None) return; + + _fixture.Execute(() => + { + Window window = new Window(); + _fixture.App.ThemeMode = themeMode; + _fixture.App.MainWindow = window; + window.Show(); + }); + + _fixture.Execute(() => + { + Verify_ApplicationProperties(_fixture.App, themeMode); + Verify_WindowProperties(_fixture.App.MainWindow, themeMode); + Window window = _fixture.App.MainWindow; + window.Should().NotBeNull(); + window.Background.Should().Be(Brushes.Transparent); + window.Resources.MergedDictionaries.Count.Should().Be(0); + }); + } + + [WpfTheory] + [MemberData(nameof(ThemeModePairs))] + public void Application_ThemeMode_Switch_Tests(ThemeMode themeMode, ThemeMode newThemeMode) + { + if (themeMode == newThemeMode) return; + + _fixture.Execute(() => { + Window window = new Window(); + _fixture.App.ThemeMode = themeMode; + _fixture.App.MainWindow = window; + window.Show(); + }); + + _fixture.Execute(() => + { + Verify_ApplicationProperties(_fixture.App, themeMode); + Verify_WindowResources(_fixture.App.MainWindow, ThemeMode.None); + Verify_WindowProperties(_fixture.App.MainWindow, ThemeMode.None, themeMode); + }); + + _fixture.Execute(() => + { + _fixture.App.ThemeMode = newThemeMode; + }); + + + _fixture.Execute(() => + { + Verify_ApplicationProperties(_fixture.App, newThemeMode); + Verify_WindowResources(_fixture.App.MainWindow, ThemeMode.None); + Verify_WindowProperties(_fixture.App.MainWindow, ThemeMode.None, newThemeMode); + }); + } + + + [WpfTheory] + [MemberData(nameof(ThemeModePairs))] + public void Application_Window_ThemeMode_Initialization(ThemeMode appThemeMode, ThemeMode windowThemeMode) + { + _fixture.Execute(() => + { + Window window = new Window() { ThemeMode = windowThemeMode }; + _fixture.App.ThemeMode = appThemeMode; + _fixture.App.MainWindow = window; + window.Show(); + }); + + _fixture.Execute(() => + { + Verify_ApplicationProperties(_fixture.App, appThemeMode); + Verify_WindowResources(_fixture.App.MainWindow, windowThemeMode); + Verify_WindowProperties(_fixture.App.MainWindow, windowThemeMode, appThemeMode); + }); + } + + #region Helper Methods + + + private void Verify_ApplicationProperties(Application app, ThemeMode t) + { + if(t == ThemeMode.None) + { + app.Resources.MergedDictionaries.Should().BeEmpty(); + app.ThemeMode.Value.Should().Be(t.Value); + return; + } + + app.ThemeMode.Value.Should().Be(t.Value); + app.Resources.MergedDictionaries.Should().HaveCount(1); + Uri source = app.Resources.MergedDictionaries[0].Source; + source.AbsoluteUri.ToString().Should().EndWith(FluentThemeResourceDictionaryMap[t]); + } + + private void Verify_WindowProperties(Window window, ThemeMode windowThemeMode, ThemeMode appThemeMode) + { + if (windowThemeMode == ThemeMode.None && appThemeMode == ThemeMode.None) + { + Verify_WindowProperties(window, windowThemeMode); + } + + ThemeMode t = windowThemeMode; + if (t == ThemeMode.None) { t = appThemeMode; } + Verify_WindowProperties(window, t); + + } + + private void Verify_WindowProperties(Window window, ThemeMode themeMode) + { + if (themeMode == ThemeMode.None) + { + window.Background.ToString().Should().Be(Brushes.White.ToString()); + window.Foreground.ToString().Should().Be(Brushes.Black.ToString()); + window.Resources.MergedDictionaries.Should().HaveCount(0); + return; + } + + window.Background.Should().Be(Brushes.Transparent); + } + + private void Verify_WindowResources(Window window, ThemeMode themeMode) + { + if (themeMode == ThemeMode.None) + { + window.Resources.MergedDictionaries.Should().BeEmpty(); + return; + } + + window.Resources.MergedDictionaries.Should().HaveCount(1); + + Uri source = window.Resources.MergedDictionaries[0].Source; + source.AbsoluteUri.ToString() + .Should().Be(FluentThemeResourceDictionaryMap[themeMode]); + } + + public void Dispose() + { + _fixture.ResetApplicationInstance(); + } + + #endregion + + #region Test Data + + public static IEnumerable ThemeModes => new List + { + new object[] { ThemeMode.None }, + new object[] { ThemeMode.System }, + new object[] { ThemeMode.Light }, + new object[] { ThemeMode.Dark } + }; + + public static IEnumerable ThemeModePairs => new List + { + new object[] { ThemeMode.None, ThemeMode.None }, + new object[] { ThemeMode.None, ThemeMode.Light }, + new object[] { ThemeMode.None, ThemeMode.Dark }, + new object[] { ThemeMode.None, ThemeMode.System }, + new object[] { ThemeMode.Light, ThemeMode.None }, + new object[] { ThemeMode.Light, ThemeMode.Light }, + new object[] { ThemeMode.Light, ThemeMode.Dark }, + new object[] { ThemeMode.Light, ThemeMode.System }, + new object[] { ThemeMode.Dark, ThemeMode.None }, + new object[] { ThemeMode.Dark, ThemeMode.Light }, + new object[] { ThemeMode.Dark, ThemeMode.Dark }, + new object[] { ThemeMode.Dark, ThemeMode.System }, + new object[] { ThemeMode.System, ThemeMode.None }, + new object[] { ThemeMode.System, ThemeMode.Light }, + new object[] { ThemeMode.System, ThemeMode.Dark }, + new object[] { ThemeMode.System, ThemeMode.System } + }; + + private static Dictionary FluentThemeResourceDictionaryMap + = new Dictionary + { + { ThemeMode.None, ""}, + { ThemeMode.System, "pack://application:,,,/PresentationFramework.Fluent;component/Themes/Fluent.xaml"}, + { ThemeMode.Light, "pack://application:,,,/PresentationFramework.Fluent;component/Themes/Fluent.Light.xaml"}, + { ThemeMode.Dark, "pack://application:,,,/PresentationFramework.Fluent;component/Themes/Fluent.Dark.xaml"}, + }; + + #endregion +} \ No newline at end of file diff --git a/tests/Fluent.UITests/AssemblyInfo.cs b/tests/Fluent.UITests/AssemblyInfo.cs new file mode 100644 index 0000000..6e17c1b --- /dev/null +++ b/tests/Fluent.UITests/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Runtime.InteropServices; + +// In SDK-style projects such as this one, several assembly attributes that were historically +// defined in this file are now automatically added during build and populated with +// values defined in project properties. For details of which attributes are included +// and how to customise this process see: https://aka.ms/assembly-info-properties + + +// Setting ComVisible to false makes the types in this assembly not visible to COM +// components. If you need to access a type in this assembly from COM, set the ComVisible +// attribute to true on that type. + +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM. + +[assembly: Guid("be88de12-c360-4632-a9cc-3eb52499262c")] +[assembly: CollectionBehavior(DisableTestParallelization = true)] + diff --git a/tests/Fluent.UITests/Fluent.UITests.csproj b/tests/Fluent.UITests/Fluent.UITests.csproj new file mode 100644 index 0000000..217852b --- /dev/null +++ b/tests/Fluent.UITests/Fluent.UITests.csproj @@ -0,0 +1,35 @@ + + + + net9.0-windows + enable + enable + false + $(NoWarn);WPF0001 + AnyCPU;x64 + x64 + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/tests/Fluent.UITests/Fluent.UITests.sln b/tests/Fluent.UITests/Fluent.UITests.sln new file mode 100644 index 0000000..4fc0f7e --- /dev/null +++ b/tests/Fluent.UITests/Fluent.UITests.sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.12.35410.144 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fluent.UITests", "Fluent.UITests.csproj", "{F861E2E6-C514-4AA1-AD63-E898E9699FC0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F861E2E6-C514-4AA1-AD63-E898E9699FC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F861E2E6-C514-4AA1-AD63-E898E9699FC0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F861E2E6-C514-4AA1-AD63-E898E9699FC0}.Debug|x64.ActiveCfg = Debug|x64 + {F861E2E6-C514-4AA1-AD63-E898E9699FC0}.Debug|x64.Build.0 = Debug|x64 + {F861E2E6-C514-4AA1-AD63-E898E9699FC0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F861E2E6-C514-4AA1-AD63-E898E9699FC0}.Release|Any CPU.Build.0 = Release|Any CPU + {F861E2E6-C514-4AA1-AD63-E898E9699FC0}.Release|x64.ActiveCfg = Release|x64 + {F861E2E6-C514-4AA1-AD63-E898E9699FC0}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {E64946E5-A49E-4812-B1CE-C7F71ABBFF12} + EndGlobalSection +EndGlobal diff --git a/tests/Fluent.UITests/ResourceDictionaryTests.cs b/tests/Fluent.UITests/ResourceDictionaryTests.cs new file mode 100644 index 0000000..58cb2b9 --- /dev/null +++ b/tests/Fluent.UITests/ResourceDictionaryTests.cs @@ -0,0 +1,146 @@ +using FluentAssertions; +using FluentAssertions.Execution; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Security.Policy; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Forms; +using Application = System.Windows.Application; + +namespace Fluent.UITests; +public class ResourceDictionaryTests +{ + [WpfTheory] + [MemberData(nameof(ThemeDictionarySourceList))] + public void Fluent_ResourceDictionary_LoadTests(string source) + { + LoadFluentResourceDictionary(source); + } + + [WpfTheory] + [MemberData(nameof(GetColorDictionary_MatchKeys_TestData))] + public void Fluent_ColorDictionary_MatchKeysTest(string firstSource, string secondSource) + { + ResourceDictionary dictionary1 = LoadFluentResourceDictionary(firstSource); + ResourceDictionary dictionary2 = LoadFluentResourceDictionary(secondSource); + + int colorDictionary1KeysCount = GetResourceKeysFromResourceDictionary(dictionary1, + out List dictionary1StringKeys, out List dictionary1ObjectKeys); + + int colorDictionary2KeysCount = GetResourceKeysFromResourceDictionary(dictionary2, + out List dictionary2StringKeys, out List dictionary2ObjectKeys); + + List dictionary1ExtraStringKeys = dictionary1StringKeys.Except(dictionary2StringKeys).ToList(); + List dictionary2ExtraStringKeys = dictionary2StringKeys.Except(dictionary1StringKeys).ToList(); + + List dictionary1ExtraObjectKeys = dictionary1ObjectKeys.Except(dictionary2ExtraStringKeys).ToList(); + List dictionary2ExtraObjectKeys = dictionary2ObjectKeys.Except(dictionary1ObjectKeys).ToList(); + + Log_ExtraKeys(dictionary1ExtraStringKeys, $"Dictionary 1 : {firstSource} extra keys"); + Log_ExtraKeys(dictionary2ExtraStringKeys, $"Dictionary 2 : {secondSource} extra keys"); + + using (new AssertionScope()) + { + dictionary1ExtraStringKeys.Should().BeEmpty(); + dictionary2ExtraStringKeys.Should().BeEmpty(); + dictionary1ExtraObjectKeys.Should().BeEmpty(); + dictionary2ExtraObjectKeys.Should().BeEmpty(); + } + } + + private void Log_ExtraKeys(List dictionary1ExtraStringKeys, string v) + { + Console.WriteLine(v); + if(dictionary1ExtraStringKeys.Count == 0) + { + Console.WriteLine("None\n"); + return; + } + + foreach(string key in dictionary1ExtraStringKeys) + { + Console.WriteLine(key); + } + Console.WriteLine(); + } + + + #region Helper Methods + + private static ResourceDictionary LoadFluentResourceDictionary(string source) + { + var uri = new Uri(source, UriKind.RelativeOrAbsolute); + ResourceDictionary? resourceDictionary = Application.LoadComponent(uri) as ResourceDictionary; + resourceDictionary.Should().NotBeNull(); + return resourceDictionary; + } + + private static int GetResourceKeysFromResourceDictionary(ResourceDictionary resourceDictionary, + out List stringResourceKeys, + out List objectResourceKeys) + { + ArgumentNullException.ThrowIfNull(resourceDictionary, nameof(resourceDictionary)); + stringResourceKeys = new List(); + objectResourceKeys = new List(); + + int resourceDictionaryKeysCount = resourceDictionary.Count; + + foreach (object key in resourceDictionary.Keys) + { + if (key is string skey) + { + stringResourceKeys.Add(skey); + } + else + { + objectResourceKeys.Add(key); + } + } + + return resourceDictionaryKeysCount; + } + + #endregion + + + #region Test Data + + public static IEnumerable GetColorDictionary_MatchKeys_TestData() + { + int count = ColorDictionarySourceList.Count; + for (int i = 0; i < count; i++) + { + for (int j = i + 1; j < count; j++) + { + yield return new object[] { ColorDictionarySourceList[i], ColorDictionarySourceList[j] }; + } + } + } + + public static IList ThemeDictionarySourceList + = new List + { + new object[] { $"{ThemeDictionaryPath}/Fluent.xaml" }, + new object[] { $"{ThemeDictionaryPath}/Fluent.Light.xaml" }, + new object[] { $"{ThemeDictionaryPath}/Fluent.Dark.xaml" }, + new object[] { $"{ThemeDictionaryPath}/Fluent.HC.xaml" }, + }; + + public static IList ColorDictionarySourceList + = new List + { + $"{ColorDictionaryPath}/Light.xaml", + $"{ColorDictionaryPath}/Dark.xaml", + $"{ColorDictionaryPath}/HC.xaml" + }; + + private const string ThemeDictionaryPath = @"/PresentationFramework.Fluent;component/Themes"; + private const string ColorDictionaryPath = @"/PresentationFramework.Fluent;component/Resources/Theme"; + + #endregion + +} diff --git a/tests/Fluent.UITests/UnitTest1.cs b/tests/Fluent.UITests/UnitTest1.cs new file mode 100644 index 0000000..fd38836 --- /dev/null +++ b/tests/Fluent.UITests/UnitTest1.cs @@ -0,0 +1,41 @@ +using System.Drawing; +using System.Security.Principal; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using Brush = System.Windows.Media.Brush; + +namespace Fluent.UITests +{ + public class UnitTest1 + { + [WpfFact] + public void Test1() + { + Window window = new Window(); + window.ThemeMode = ThemeMode.System; + + var stackPanel = new StackPanel(); + + var rb = new RadioButton(); + rb.IsEnabled = false; + stackPanel.Children.Add(rb); + window.Content = stackPanel; + window.Show(); + + Style style = rb.Style; + + Border? border = rb.Template.FindName("RootBorder", rb) as Border; + Assert.NotNull(border); + Brush br = border.Background as Brush; + if(br is SolidColorBrush scb) + { + Assert.Equal(scb.Color, Colors.Transparent); + } + + var grid = rb.Template.FindName("RootGrid", rb); + Assert.NotNull(grid); + + } + } +} diff --git a/tests/Fluent.UITests/Usings.cs b/tests/Fluent.UITests/Usings.cs new file mode 100644 index 0000000..182a793 --- /dev/null +++ b/tests/Fluent.UITests/Usings.cs @@ -0,0 +1,3 @@ +global using System.Collections.Generic; +global using Xunit; + diff --git a/tests/Fluent.UITests/Utilities/KeyboardController.cs b/tests/Fluent.UITests/Utilities/KeyboardController.cs new file mode 100644 index 0000000..7ed1e34 --- /dev/null +++ b/tests/Fluent.UITests/Utilities/KeyboardController.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Fluent.UITests.TestUtilities +{ + internal class KeyboardController + { + } +} diff --git a/tests/Fluent.UITests/Utilities/MouseController.cs b/tests/Fluent.UITests/Utilities/MouseController.cs new file mode 100644 index 0000000..038825a --- /dev/null +++ b/tests/Fluent.UITests/Utilities/MouseController.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Fluent.UITests.TestUtilities +{ + internal class MouseController + { + } +} diff --git a/tests/Fluent.UITests/Utilities/WindowsColorModeSwitcher.cs b/tests/Fluent.UITests/Utilities/WindowsColorModeSwitcher.cs new file mode 100644 index 0000000..2c0a015 --- /dev/null +++ b/tests/Fluent.UITests/Utilities/WindowsColorModeSwitcher.cs @@ -0,0 +1,127 @@ +using Microsoft.Win32; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Fluent.UITests.TestUtilities +{ + //public static class WindowsSettingManager + //{ + // public static void SetColorMode(bool isLightMode) + // { + // using (var key = Registry.CurrentUser.OpenSubKey(PersonalizeKey, writable: true)) + // { + // if (key == null) + // throw new InvalidOperationException("Could not open registry key for personalization settings."); + + // // Set color mode for both apps and system + // key.SetValue("AppsUseLightTheme", isLightMode ? 1 : 0, RegistryValueKind.DWord); + // key.SetValue("SystemUsesLightTheme", isLightMode ? 1 : 0, RegistryValueKind.DWord); + + // // Notify system about the changes + // NotifySystemColorModeChange(); + // } + // } + + // public static void SetAccentColor(uint argbColor) + // { + // // Update registry for AccentColor + // using (var key = Registry.CurrentUser.OpenSubKey(AccentColorKey, writable: true)) + // { + // if (key == null) + // throw new InvalidOperationException("Could not open registry key for AccentColor."); + + // key.SetValue("AccentColorMenu", argbColor, RegistryValueKind.DWord); + // } + + // // Enable color prevalence + // using (var key = Registry.CurrentUser.OpenSubKey(PersonalizeKey, writable: true)) + // { + // if (key == null) + // throw new InvalidOperationException("Could not open registry key for Personalize settings."); + + // key.SetValue("ColorPrevalence", 1, RegistryValueKind.DWord); + // } + + // // Notify the system of the change + // NotifyAccentColorChange(); + // } + + // public static void SetHighContrastMode(bool enable) + // { + // using (var key = Registry.CurrentUser.OpenSubKey(HighContrastKey, writable: true)) + // { + // if (key == null) + // throw new InvalidOperationException("Could not open registry key for High Contrast settings."); + + // key.SetValue("Flags", enable ? "1" : "0", RegistryValueKind.String); + + // // Notify the system of the change + // NotifySettingsChange(); + // } + // } + + // public static bool IsHighContrastModeEnabled() + // { + // using (var key = Registry.CurrentUser.OpenSubKey(HighContrastKey, writable: false)) + // { + // if (key == null) + // return false; + + // var value = key.GetValue("Flags", "0").ToString(); + // return value == "1"; + // } + // } + + // public static bool IsSystemColorModeLight() + // { + // var useLightTheme = Registry.GetValue(PersonalizeKey, + // "AppsUseLightTheme", null) as int?; + + // if (useLightTheme == null) + // { + // useLightTheme = Registry.GetValue(PersonalizeKey, + // "SystemUsesLightTheme", null) as int?; + // } + + // return useLightTheme != null && useLightTheme != 0; + // } + + // private static void NotifySystemColorModeChange() + // { + // // This sends a broadcast message to update the theme + // const int HWND_BROADCAST = 0xffff; + // const int WM_SETTINGCHANGE = 0x1a; + + // NativeMethods.SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, IntPtr.Zero, "ImmersiveColorSet".ToIntPtr()); + // } + + // private static void NotifyAccentColorChange() + // { + // const int HWND_BROADCAST = 0xffff; + // const int WM_SETTINGCHANGE = 0x1a; + + // NativeMethods.SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, IntPtr.Zero, "ImmersiveColorSet".ToIntPtr()); + // } + + // private static void NotifySettingsChange() + // { + // const int HWND_BROADCAST = 0xffff; + // const int WM_SETTINGCHANGE = 0x1a; + + // NativeMethods.SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, IntPtr.Zero, "Accessibility".ToIntPtr()); + // } + + // private const string PersonalizeKey = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + // private const string AccentColorKey = @"Software\Microsoft\Windows\CurrentVersion\Explorer\Accent"; + // private const string HighContrastKey = @"Control Panel\Accessibility\HighContrast"; + //} + + internal static class NativeMethods + { + [System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto)] + public static extern int SendMessage(int hWnd, int Msg, IntPtr wParam, IntPtr lParam); + } +} diff --git a/tests/Fluent.UITests/WindowThemeModeTests.cs b/tests/Fluent.UITests/WindowThemeModeTests.cs new file mode 100644 index 0000000..c6b335f --- /dev/null +++ b/tests/Fluent.UITests/WindowThemeModeTests.cs @@ -0,0 +1,140 @@ +using FluentAssertions; +using System.Windows; +using System.Windows.Media; + +namespace Fluent.UITests; + +public class WindowThemeModeTests +{ + [WpfTheory] + [MemberData(nameof(ThemeModes))] + public void Window_ThemeMode_Initialization(ThemeMode themeMode) + { + if (themeMode == ThemeMode.None) return; + + Window window = new Window(); + window.ThemeMode = themeMode; + window.ApplyTemplate(); + + Verify_WindowProperties(window, themeMode); + } + + [WpfFact] + public void Window_ThemeMode_Default() + { + Window window = new Window(); + window.ApplyTemplate(); + //window.Show(); + + window.ThemeMode.Value.Should().Be("None"); + window.Background.Should().BeNull(); + window.Resources.MergedDictionaries.Should().HaveCount(0); + } + + [WpfTheory] + [MemberData(nameof(ThemeModePairs))] + public void Window_ThemeMode_Switch(ThemeMode themeMode, ThemeMode newThemeMode) + { + if (themeMode == newThemeMode) return; + + Window window = new Window(); + window.Show(); + window.ThemeMode = themeMode; + //window.ApplyTemplate(); + Verify_WindowProperties(window, themeMode); + Verify_WindowResources(window, themeMode); + + window.ThemeMode = newThemeMode; + //window.ApplyTemplate(); + Verify_WindowProperties(window, newThemeMode); + Verify_WindowResources(window, newThemeMode); + } + + #region Helper Methods + + private void Verify_WindowProperties(Window window, ThemeMode themeMode) + { + if(themeMode == ThemeMode.None) + { + window.Background.ToString().Should().Be(Brushes.White.ToString()); + window.Foreground.ToString().Should().Be(Brushes.Black.ToString()); + window.Resources.MergedDictionaries.Should().HaveCount(0); + return; + } + + window.Background.Should().Be(Brushes.Transparent); + + } + + private void Verify_ApplicationResources(Application application, ThemeMode themeMode) + { + if(themeMode == ThemeMode.None) + { + application.Resources.MergedDictionaries.Should().BeEmpty(); + return; + } + + application.Resources.MergedDictionaries.Should().HaveCount(1); + Uri source = application.Resources.MergedDictionaries[0].Source; + source.AbsoluteUri.ToString() + .Should().EndWith(FluentThemeResourceDictionaryMap[themeMode]); + } + + private void Verify_WindowResources(Window window, ThemeMode themeMode) + { + if (themeMode == ThemeMode.None) + { + window.Resources.MergedDictionaries.Should().BeEmpty(); + return; + } + + window.Resources.MergedDictionaries.Should().HaveCount(1); + + Uri source = window.Resources.MergedDictionaries[0].Source; + source.AbsoluteUri.ToString() + .Should().Be(FluentThemeResourceDictionaryMap[themeMode]); + } + + #endregion + + #region Test Data + + public static IEnumerable ThemeModes => new List + { + new object[] { ThemeMode.None }, + new object[] { ThemeMode.System }, + new object[] { ThemeMode.Light }, + new object[] { ThemeMode.Dark } + }; + + public static IEnumerable ThemeModePairs => new List + { + new object[] { ThemeMode.None, ThemeMode.None }, + new object[] { ThemeMode.None, ThemeMode.Light }, + new object[] { ThemeMode.None, ThemeMode.Dark }, + new object[] { ThemeMode.None, ThemeMode.System }, + new object[] { ThemeMode.Light, ThemeMode.None }, + new object[] { ThemeMode.Light, ThemeMode.Light }, + new object[] { ThemeMode.Light, ThemeMode.Dark }, + new object[] { ThemeMode.Light, ThemeMode.System }, + new object[] { ThemeMode.Dark, ThemeMode.None }, + new object[] { ThemeMode.Dark, ThemeMode.Light }, + new object[] { ThemeMode.Dark, ThemeMode.Dark }, + new object[] { ThemeMode.Dark, ThemeMode.System }, + new object[] { ThemeMode.System, ThemeMode.None }, + new object[] { ThemeMode.System, ThemeMode.Light }, + new object[] { ThemeMode.System, ThemeMode.Dark }, + new object[] { ThemeMode.System, ThemeMode.System } + }; + + private static Dictionary FluentThemeResourceDictionaryMap + = new Dictionary + { + { ThemeMode.None, ""}, + { ThemeMode.System, "pack://application:,,,/PresentationFramework.Fluent;component/Themes/Fluent.xaml"}, + { ThemeMode.Light, "pack://application:,,,/PresentationFramework.Fluent;component/Themes/Fluent.Light.xaml"}, + { ThemeMode.Dark, "pack://application:,,,/PresentationFramework.Fluent;component/Themes/Fluent.Dark.xaml"}, + }; + + #endregion +} \ No newline at end of file