From ed88d9f17036a75ce43eb784bef026916056a013 Mon Sep 17 00:00:00 2001 From: Glenn Watson <5834289+glennawatson@users.noreply.github.com> Date: Sun, 2 Aug 2026 01:30:35 +1000 Subject: [PATCH] fix(drawing): report design mode correctly for nested WPF user controls - Read the design-mode default the designer raises for FrameworkElement instead of querying a freshly constructed, unparented DependencyObject. The designer only raises that default, so an orphan object answered false for any control it did not set the attached property on directly, which is what broke a user control nested inside another user control. - Stop discarding the probe results. DetectDesignMode ran every probe and then overwrote the answer with false, so InDesignMode could only ever return false. The overwrite has been there since the class was extracted. - Identify the design host by its process executable, not by the assembly base directory. A directory path yields an empty executable name, and the containment test matched every name that was a fragment of a known host (the empty name included), so the fallback probe reported design mode for every application once the overwrite above was removed. The name now has to match a known design host exactly. - Skip host types the target framework cannot load: the .NET Framework and Windows-specific targets set UseWPF, so they call the designer API directly with no reflection, and the Android and Apple targets compile no designer probe at all. Only the target-framework-neutral builds still reflect, because a Windows desktop application can resolve to them. - Drop the Silverlight probe, whose assembly cannot load on any supported target framework and which carried the same orphan-object defect. - Make the memoized result resettable through the existing detector state hooks so tests can exercise the class, and cover both detector files to 100% of lines and branches. --- .../DefaultPlatformModeDetector.cs | 244 +++++++++--------- src/Splat.Drawing/PlatformModeDetector.cs | 12 +- .../PlatformModeDetectorScope.cs | 8 +- ...efaultPlatformModeDetectorCoverageTests.cs | 1 + .../DesignModeDetectionTests.cs | 139 ++++++++++ .../PlatformModeDetectorCoverageTests.cs | 38 ++- 6 files changed, 305 insertions(+), 137 deletions(-) create mode 100644 src/tests/Splat.Drawing.Tests/DesignModeDetectionTests.cs diff --git a/src/Splat.Drawing/DefaultPlatformModeDetector.cs b/src/Splat.Drawing/DefaultPlatformModeDetector.cs index c527963a6..024dcfe8f 100644 --- a/src/Splat.Drawing/DefaultPlatformModeDetector.cs +++ b/src/Splat.Drawing/DefaultPlatformModeDetector.cs @@ -2,45 +2,47 @@ // ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +#if !MONO using System.Diagnostics.CodeAnalysis; +#endif using System.IO; +#if NETFRAMEWORK using System.Reflection; +#endif namespace Splat; -/// -/// Provides a default implementation for detecting whether the application is running in design mode across supported -/// platforms. -/// -/// This class is typically used to determine if code is executing within a designer environment, such as -/// Visual Studio or Blend, to enable or disable design-time specific logic. It supports multiple platforms and design -/// environments, including WPF, Silverlight, and UWP, by checking for known design mode indicators. The detection -/// result may be cached for performance. Thread safety is not guaranteed. +/// Provides a default implementation for detecting whether the application is running in design mode. +/// +/// This class is typically used to determine if code is executing within a designer environment, such as +/// Visual Studio or Blend, to enable or disable design-time specific logic. The detection result is memoized, so a +/// process that starts outside a designer never re-runs the probes. Thread safety is not guaranteed. +/// Which probes are compiled depends on the target framework. Splat.Drawing sets UseWPF and +/// UseWindowsForms for the .NET Framework and Windows-specific targets, so those builds call the WPF designer +/// API directly instead of reflecting over it. The target-framework-neutral builds keep a reflective probe, because a +/// Windows desktop application resolves to them when its own platform version predates the Windows-specific targets. +/// The Android and Apple targets cannot host a XAML designer at all and compile no designer probe. +/// public class DefaultPlatformModeDetector : IPlatformModeDetector { - /// Assembly-qualified name of the Silverlight/XAML DesignerProperties type probed for design mode. - private const string XamlDesignPropertiesType = "System.ComponentModel.DesignerProperties, System.Windows, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e"; - - /// Assembly-qualified name of the XAML Border control used as the dependency object for the design-mode probe. - private const string XamlControlBorderType = "System.Windows.Controls.Border, System.Windows, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e"; - - /// Name of the XAML DesignerProperties method that reports design mode. - private const string XamlDesignPropertiesDesignModeMethodName = "GetIsInDesignMode"; - +#if !NETFRAMEWORK && !WINDOWS && !MONO /// Assembly-qualified name of the WPF DesignerProperties type probed for design mode. private const string WpfDesignerPropertiesType = "System.ComponentModel.DesignerProperties, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"; /// Name of the WPF DesignerProperties method that reports design mode. private const string WpfDesignerPropertiesDesignModeMethod = "GetIsInDesignMode"; - /// Assembly-qualified name of the WPF DependencyObject type used as the dependency object for the design-mode probe. - private const string WpfDependencyPropertyType = "System.Windows.DependencyObject, WindowsBase, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"; + /// Assembly-qualified name of the WPF element type the designer raises the design-mode default for. + private const string WpfFrameworkElementType = "System.Windows.FrameworkElement, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"; +#endif - /// Assembly-qualified name of the WinRT DesignMode type probed for design mode. - private const string WinFormsDesignerPropertiesType = "Windows.ApplicationModel.DesignMode, Windows, ContentType=WindowsRuntime"; +#if !MONO + /// Assembly-qualified name of the Windows Runtime DesignMode type probed for design mode. + private const string WinRtDesignModeType = "Windows.ApplicationModel.DesignMode, Windows, ContentType=WindowsRuntime"; - /// Name of the WinRT DesignMode property that reports design mode. - private const string WinFormsDesignerPropertiesDesignModeMethod = "DesignModeEnabled"; + /// Name of the Windows Runtime DesignMode property that reports design mode. + private const string WinRtDesignModeEnabledProperty = "DesignModeEnabled"; +#endif /// Executable names of known design-time host processes used as a fallback design-mode signal. private static readonly string[] _designEnvironments = ["BLEND.EXE", "XDESPROC.EXE"]; @@ -48,24 +50,41 @@ public class DefaultPlatformModeDetector : IPlatformModeDetector /// Memoizes the design-mode detection result; until first computed. private static bool? _cachedInDesignModeResult; + /// Gets the path of the executable hosting the current process, or when it is unavailable. + /// This is the executable file, not the directory the assembly was loaded from. A design-time host is + /// recognised by its process name, and a directory path yields no executable name to compare. + internal static string? HostExecutablePath => +#if NETFRAMEWORK + Assembly.GetEntryAssembly()?.Location; +#else + Environment.ProcessPath; +#endif + /// - public bool? InDesignMode() => DetectDesignMode(); + public bool? InDesignMode() => _cachedInDesignModeResult ??= DetectDesignMode(HostExecutablePath); /// Determines whether the supplied host entry-point path names a known design-environment executable. /// The host entry-point path, or when it is unavailable. /// when the entry path names a known design environment; otherwise . + /// The executable name has to match a known design host exactly. A containment test reports a false + /// positive for every name that is a fragment of one, including the empty name a directory path yields. internal static bool IsDesignEnvironmentEntry(string? entry) { - if (entry is null) + if (string.IsNullOrEmpty(entry)) { return false; } - var exeName = new FileInfo(entry).Name; + var executableName = new FileInfo(entry).Name; - foreach (var designEnv in _designEnvironments) + if (executableName.Length == 0) { - if (IsDesignEnvironment(designEnv, exeName)) + return false; + } + + foreach (var knownHost in _designEnvironments) + { + if (string.Equals(knownHost, executableName, StringComparison.OrdinalIgnoreCase)) { return true; } @@ -74,118 +93,85 @@ internal static bool IsDesignEnvironmentEntry(string? entry) return false; } - /// Runs the design-mode probes in priority order and returns (and caches) the result. - /// A value indicating whether the application is running in design mode. - private static bool? DetectDesignMode() - { - if (_cachedInDesignModeResult.HasValue) - { - return _cachedInDesignModeResult.Value; - } - - // Probe each platform in priority order; the first detected platform wins, - // mirroring the original else-if chain. The cached result is then reset to - // false below, preserving the original behaviour. - RunDesignModeProbes(); - - _cachedInDesignModeResult = false; + /// Runs the design-mode probes and reports whether any of them detected a designer. + /// The path of the executable hosting the current process, or . + /// when a designer is hosting the application; otherwise . + internal static bool DetectDesignMode(string? hostExecutablePath) => +#if MONO + IsDesignEnvironmentEntry(hostExecutablePath); +#else + ResolveDesignMode(ProbeWpfDesignMode(), ProbeWinRtDesignMode(), hostExecutablePath); +#endif - return _cachedInDesignModeResult; - } +#if !MONO + /// Combines the probe results into a single answer: any probe reporting a designer wins. + /// Whether the WPF probe reported a designer. + /// Whether the Windows Runtime probe reported a designer. + /// The path of the executable hosting the current process, or . + /// when a designer is hosting the application; otherwise . + internal static bool ResolveDesignMode(bool wpfDesignMode, bool winRtDesignMode, string? hostExecutablePath) => + wpfDesignMode || winRtDesignMode || IsDesignEnvironmentEntry(hostExecutablePath); +#endif - /// Runs the platform design-mode probes in priority order. - /// None of the Silverlight, WPF, or WinRT host types resolve on the supported .NET targets, so the - /// short-circuiting branches that fire when one of those probes reports a platform are unreachable off their - /// native hosts. - [ExcludeFromCodeCoverage] - private static void RunDesignModeProbes() => - _ = ProbeXamlDesignProperties() - || ProbeWpfDesignerProperties() - || ProbeWinRtDesignMode() - || ProbeDesignEnvironmentExecutable(); - - /// Probes for the Silverlight / Windows Phone 8 design-mode indicator. - /// if the platform was detected; otherwise . - [ExcludeFromCodeCoverage] // Off-platform reflection: the Silverlight/XAML type never resolves on supported targets; only the type-null guard runs. - private static bool ProbeXamlDesignProperties() + /// Gets the memoized design-mode result. Used by test scopes. + /// The memoized result, or when the probes have not run yet. + internal static bool? GetState() => _cachedInDesignModeResult; + + /// Restores the memoized design-mode result. Used by test scopes. + /// The memoized result to restore. + internal static void RestoreState(bool? state) => _cachedInDesignModeResult = state; + + /// Discards the memoized design-mode result so the next query re-runs the probes. Used by test scopes. + internal static void ResetState() => _cachedInDesignModeResult = null; + +#if !MONO +#if NETFRAMEWORK || WINDOWS + /// Reads the design-mode default that the WPF designer raises for the elements it loads. + /// when a XAML designer is hosting the application; otherwise . + /// The designer raises the default that FrameworkElement reports for the IsInDesignMode + /// attached property, which is what makes every element it loads answer . Asking + /// DesignerProperties.GetIsInDesignMode about a freshly constructed, unparented DependencyObject + /// reads the unraised base default instead, so it answers for any control the designer did + /// not set the attached property on directly - a user control nested inside another user control, for example. + /// Reading the metadata needs no element at all, so it also avoids creating a dispatcher-affine object. + [ExcludeFromCodeCoverage] // Only a hosting designer raises this default, and the call that would simulate it mutates the property metadata for the whole process. + private static bool ProbeWpfDesignMode() => + System.ComponentModel.DesignerProperties.IsInDesignModeProperty + .GetMetadata(typeof(System.Windows.FrameworkElement)).DefaultValue is true; +#else + /// Reads the design-mode default that the WPF designer raises, when WPF is loaded into the process. + /// when a XAML designer is hosting the application; otherwise . + /// The element handed to GetIsInDesignMode is a FrameworkElement rather than a bare + /// DependencyObject, because the designer raises the attached property's default for + /// FrameworkElement. A bare DependencyObject reads the unraised base default and therefore answers + /// for any control the designer did not set the attached property on directly - a user + /// control nested inside another user control, for example. + [ExcludeFromCodeCoverage] // Off-platform reflection: PresentationFramework only resolves inside a WPF host. + private static bool ProbeWpfDesignMode() { - var type = Type.GetType(XamlDesignPropertiesType, false); - if (type is null) - { - return false; - } + var designerProperties = Type.GetType(WpfDesignerPropertiesType, false); - var methodInfo = type.GetMethod(XamlDesignPropertiesDesignModeMethodName); - var dependencyObject = Type.GetType(XamlControlBorderType, false); - - if (methodInfo is null || dependencyObject is null) - { - return true; - } - - _cachedInDesignModeResult = (bool)(methodInfo.Invoke(null, [Activator.CreateInstance(dependencyObject)]) ?? false); - return true; - } - - /// Probes for the WPF designer design-mode indicator. - /// if the platform was detected; otherwise . - [ExcludeFromCodeCoverage] // Off-platform reflection: the WPF type never resolves on supported targets; only the type-null guard runs. - private static bool ProbeWpfDesignerProperties() - { - var type = Type.GetType(WpfDesignerPropertiesType, false); - if (type is null) + if (designerProperties is null) { return false; } - var methodInfo = type.GetMethod(WpfDesignerPropertiesDesignModeMethod); - var dependencyObject = Type.GetType(WpfDependencyPropertyType, false); - if (methodInfo is null || dependencyObject is null) - { - return true; - } - - _cachedInDesignModeResult = (bool)(methodInfo.Invoke(null, [Activator.CreateInstance(dependencyObject)]) ?? false); - return true; - } - - /// Probes for the WinRT design-mode indicator. - /// if the platform was detected; otherwise . - [ExcludeFromCodeCoverage] // Off-platform reflection: the WinRT type never resolves on supported targets; only the type-null guard runs. - private static bool ProbeWinRtDesignMode() - { - var type = Type.GetType(WinFormsDesignerPropertiesType, false); - if (type is null) - { - return false; - } + var designModeMethod = designerProperties.GetMethod(WpfDesignerPropertiesDesignModeMethod); + var frameworkElement = Type.GetType(WpfFrameworkElementType, false); - _cachedInDesignModeResult = (bool)(type.GetProperty(WinFormsDesignerPropertiesDesignModeMethod)?.GetMethod?.Invoke(null, null) ?? false); - return true; + return designModeMethod is not null + && frameworkElement is not null + && designModeMethod.Invoke(null, [Activator.CreateInstance(frameworkElement)]) is true; } - - /// Probes for a known design-environment host executable as the fallback indicator. - /// Always , as this is the terminal fallback probe. - private static bool ProbeDesignEnvironmentExecutable() - { -#if NETFRAMEWORK - var entry = Assembly.GetEntryAssembly()?.Location; -#else - var entry = AppContext.BaseDirectory; #endif - _cachedInDesignModeResult = IsDesignEnvironmentEntry(entry); - - return true; - } - /// Determines whether the host executable name corresponds to a known design environment. - /// The known design-environment executable name. - /// The current host executable name. - /// when the host is the supplied design environment. - private static bool IsDesignEnvironment(string designEnvironment, string exeName) => -#if NETFRAMEWORK - designEnvironment.IndexOf(exeName, StringComparison.InvariantCultureIgnoreCase) != -1; -#else - designEnvironment.Contains(exeName, StringComparison.InvariantCultureIgnoreCase); + /// Reads the Windows Runtime design-mode indicator, when the Windows Runtime is available. + /// when a Windows Runtime designer is hosting the application; otherwise . + [ExcludeFromCodeCoverage] // Off-platform reflection: the Windows Runtime projection only resolves inside a Windows Runtime host. + private static bool ProbeWinRtDesignMode() => + Type.GetType(WinRtDesignModeType, false)? + .GetProperty(WinRtDesignModeEnabledProperty)? + .GetMethod? + .Invoke(null, null) is true; #endif } diff --git a/src/Splat.Drawing/PlatformModeDetector.cs b/src/Splat.Drawing/PlatformModeDetector.cs index 6200d395a..3b75a923b 100644 --- a/src/Splat.Drawing/PlatformModeDetector.cs +++ b/src/Splat.Drawing/PlatformModeDetector.cs @@ -55,16 +55,19 @@ public static bool InDesignMode() } /// Gets the current state for test isolation. Used by test scopes. - /// A tuple containing the current detector and cached result. - internal static (IPlatformModeDetector detector, bool? cachedResult) GetState() => - (Current, _cachedInDesignModeResult); + /// A tuple containing the current detector, the cached result, and the default detector's own cached result. + /// memoizes separately, so its cache has to travel with this + /// state; otherwise a test that leaves a design-mode value behind there leaks into every later test. + internal static (IPlatformModeDetector detector, bool? cachedResult, bool? defaultDetectorCachedResult) GetState() => + (Current, _cachedInDesignModeResult, DefaultPlatformModeDetector.GetState()); /// Restores the state for test isolation. Used by test scopes. /// The state to restore. - internal static void RestoreState((IPlatformModeDetector detector, bool? cachedResult) state) + internal static void RestoreState((IPlatformModeDetector detector, bool? cachedResult, bool? defaultDetectorCachedResult) state) { Current = state.detector; _cachedInDesignModeResult = state.cachedResult; + DefaultPlatformModeDetector.RestoreState(state.defaultDetectorCachedResult); } /// Resets the state to default for test isolation. Used by test scopes. @@ -72,5 +75,6 @@ internal static void ResetState() { Current = new DefaultPlatformModeDetector(); _cachedInDesignModeResult = null; + DefaultPlatformModeDetector.ResetState(); } } diff --git a/src/tests/Splat.Common.Test/PlatformModeDetectorScope.cs b/src/tests/Splat.Common.Test/PlatformModeDetectorScope.cs index f81e5b4fe..875085fb8 100644 --- a/src/tests/Splat.Common.Test/PlatformModeDetectorScope.cs +++ b/src/tests/Splat.Common.Test/PlatformModeDetectorScope.cs @@ -26,13 +26,17 @@ public sealed class PlatformModeDetectorScope : IDisposable /// The cached platform-mode-detection result captured on construction. private readonly bool? _savedCachedResult; + /// The default detector's own cached design-mode result captured on construction. + private readonly bool? _savedDefaultDetectorCachedResult; + /// Initializes a new instance of the class. Saves the current PlatformModeDetector state and resets it to default. public PlatformModeDetectorScope() { - (_savedDetector, _savedCachedResult) = PlatformModeDetector.GetState(); + (_savedDetector, _savedCachedResult, _savedDefaultDetectorCachedResult) = PlatformModeDetector.GetState(); PlatformModeDetector.ResetState(); } /// Restores the PlatformModeDetector to its previous state. - public void Dispose() => PlatformModeDetector.RestoreState((_savedDetector, _savedCachedResult)); + public void Dispose() => + PlatformModeDetector.RestoreState((_savedDetector, _savedCachedResult, _savedDefaultDetectorCachedResult)); } diff --git a/src/tests/Splat.Drawing.Tests/DefaultPlatformModeDetectorCoverageTests.cs b/src/tests/Splat.Drawing.Tests/DefaultPlatformModeDetectorCoverageTests.cs index 2a1ab6a0b..ced69a774 100644 --- a/src/tests/Splat.Drawing.Tests/DefaultPlatformModeDetectorCoverageTests.cs +++ b/src/tests/Splat.Drawing.Tests/DefaultPlatformModeDetectorCoverageTests.cs @@ -5,6 +5,7 @@ namespace Splat.Drawing.Tests; /// Unit tests covering . +[NotInParallel] // Reads the memoized design-mode result, which is static state. public sealed class DefaultPlatformModeDetectorCoverageTests { /// An entry-point path whose executable name matches a known design-environment host. diff --git a/src/tests/Splat.Drawing.Tests/DesignModeDetectionTests.cs b/src/tests/Splat.Drawing.Tests/DesignModeDetectionTests.cs new file mode 100644 index 000000000..3884f86ff --- /dev/null +++ b/src/tests/Splat.Drawing.Tests/DesignModeDetectionTests.cs @@ -0,0 +1,139 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.IO; + +namespace Splat.Drawing.Tests; + +/// Unit tests covering how decides that a designer is hosting the application. +[NotInParallel] // Mutates the memoized design-mode result, which is static state. +public sealed class DesignModeDetectionTests +{ + /// An entry-point path whose executable name matches a known design-environment host. + private const string DesignHostPath = "/apps/design/BLEND.EXE"; + + /// An entry-point path whose executable name does not match any known design environment. + private const string ApplicationHostPath = "/apps/myapp/MyApp.dll"; + + /// Verifies that a probe reporting a design environment is what the detector reports, rather than being discarded. + /// A representing the asynchronous operation. + [Test] + public async Task DetectDesignMode_WithDesignHostExecutable_ReportsDesignMode() => + await Assert.That(DefaultPlatformModeDetector.DetectDesignMode(DesignHostPath)).IsTrue(); + + /// Verifies that an ordinary application host is not reported as a designer. + /// A representing the asynchronous operation. + [Test] + public async Task DetectDesignMode_WithApplicationExecutable_ReportsRuntime() => + await Assert.That(DefaultPlatformModeDetector.DetectDesignMode(ApplicationHostPath)).IsFalse(); + + /// Verifies that an unavailable host path is not reported as a designer. + /// A representing the asynchronous operation. + [Test] + public async Task DetectDesignMode_WithUnavailableHostPath_ReportsRuntime() => + await Assert.That(DefaultPlatformModeDetector.DetectDesignMode(null)).IsFalse(); + + /// Verifies that a WPF designer signal is reported even when nothing else indicates design mode. + /// A representing the asynchronous operation. + [Test] + public async Task ResolveDesignMode_WhenWpfProbeReportsDesigner_ReportsDesignMode() => + await Assert.That(DefaultPlatformModeDetector.ResolveDesignMode(true, false, ApplicationHostPath)).IsTrue(); + + /// Verifies that a Windows Runtime designer signal is reported even when nothing else indicates design mode. + /// A representing the asynchronous operation. + [Test] + public async Task ResolveDesignMode_WhenWinRtProbeReportsDesigner_ReportsDesignMode() => + await Assert.That(DefaultPlatformModeDetector.ResolveDesignMode(false, true, ApplicationHostPath)).IsTrue(); + + /// Verifies that the host executable decides when neither designer probe reports anything. + /// A representing the asynchronous operation. + [Test] + public async Task ResolveDesignMode_WhenNoProbeReportsDesigner_FallsBackToHostExecutable() => + await Assert.That(DefaultPlatformModeDetector.ResolveDesignMode(false, false, DesignHostPath)).IsTrue(); + + /// Verifies that nothing indicating design mode reports a running application. + /// A representing the asynchronous operation. + [Test] + public async Task ResolveDesignMode_WhenNothingReportsDesigner_ReportsRuntime() => + await Assert.That(DefaultPlatformModeDetector.ResolveDesignMode(false, false, ApplicationHostPath)).IsFalse(); + + /// Verifies that the design-host match ignores case, as Windows executable names do. + /// A representing the asynchronous operation. + [Test] + public async Task IsDesignEnvironmentEntry_WithDifferentlyCasedDesignHost_IsTrue() => + await Assert.That(DefaultPlatformModeDetector.IsDesignEnvironmentEntry("/apps/design/Blend.exe")).IsTrue(); + + /// Verifies that a directory path, which has no executable name, is not treated as a design environment. + /// A representing the asynchronous operation. + [Test] + public async Task IsDesignEnvironmentEntry_WithDirectoryPath_IsFalse() => + await Assert.That(DefaultPlatformModeDetector.IsDesignEnvironmentEntry("/apps/myapp/bin/")).IsFalse(); + + /// Verifies that an empty entry path is not treated as a design environment. + /// A representing the asynchronous operation. + [Test] + public async Task IsDesignEnvironmentEntry_WithEmptyEntry_IsFalse() => + await Assert.That(DefaultPlatformModeDetector.IsDesignEnvironmentEntry(string.Empty)).IsFalse(); + + /// Verifies that an executable whose name is merely a fragment of a design host name is not a match. + /// A representing the asynchronous operation. + [Test] + public async Task IsDesignEnvironmentEntry_WithNameFragmentOfDesignHost_IsFalse() => + await Assert.That(DefaultPlatformModeDetector.IsDesignEnvironmentEntry("/apps/myapp/END.EXE")).IsFalse(); + + /// Verifies that the host path names an executable file rather than the directory it lives in. + /// A representing the asynchronous operation. + [Test] + public async Task HostExecutablePath_NamesAnExecutableFile() + { + var hostExecutablePath = DefaultPlatformModeDetector.HostExecutablePath; + + await Assert.That(hostExecutablePath).IsNotNullOrEmpty(); + await Assert.That(new FileInfo(hostExecutablePath!).Name).IsNotEmpty(); + } + + /// Verifies that the running test host is not mistaken for a design environment. + /// A representing the asynchronous operation. + [Test] + public async Task HostExecutablePath_IsNotADesignEnvironment() => + await Assert.That(DefaultPlatformModeDetector.IsDesignEnvironmentEntry(DefaultPlatformModeDetector.HostExecutablePath)).IsFalse(); + + /// Verifies that the detector reports the memoized result rather than re-probing. + /// A representing the asynchronous operation. + [Test] + public async Task InDesignMode_ReportsMemoizedResult() + { + var saved = DefaultPlatformModeDetector.GetState(); + try + { + DefaultPlatformModeDetector.RestoreState(true); + + await Assert.That(new DefaultPlatformModeDetector().InDesignMode()).IsTrue(); + } + finally + { + DefaultPlatformModeDetector.RestoreState(saved); + } + } + + /// Verifies that discarding the memoized result makes the detector probe again. + /// A representing the asynchronous operation. + [Test] + public async Task InDesignMode_AfterStateReset_ProbesAgain() + { + var saved = DefaultPlatformModeDetector.GetState(); + try + { + DefaultPlatformModeDetector.RestoreState(true); + DefaultPlatformModeDetector.ResetState(); + + await Assert.That(DefaultPlatformModeDetector.GetState()).IsNull(); + await Assert.That(new DefaultPlatformModeDetector().InDesignMode()).IsFalse(); + } + finally + { + DefaultPlatformModeDetector.RestoreState(saved); + } + } +} diff --git a/src/tests/Splat.Drawing.Tests/PlatformModeDetectorCoverageTests.cs b/src/tests/Splat.Drawing.Tests/PlatformModeDetectorCoverageTests.cs index 55f332ea1..04a5a1cf7 100644 --- a/src/tests/Splat.Drawing.Tests/PlatformModeDetectorCoverageTests.cs +++ b/src/tests/Splat.Drawing.Tests/PlatformModeDetectorCoverageTests.cs @@ -44,6 +44,33 @@ public async Task OverrideModeDetector_FalseDetector_ReturnsFalse() } } + /// Verifies that a repeat query reports the memoized result instead of asking the detector again. + /// A representing the asynchronous operation. + [Test] + public async Task InDesignMode_OnRepeatQuery_ReportsMemoizedResult() + { + var saved = PlatformModeDetector.GetState(); + try + { + var detector = new StubModeDetector(true); + PlatformModeDetector.OverrideModeDetector(detector); + + var first = PlatformModeDetector.InDesignMode(); + var second = PlatformModeDetector.InDesignMode(); + + using (Assert.Multiple()) + { + await Assert.That(first).IsTrue(); + await Assert.That(second).IsTrue(); + await Assert.That(detector.QueryCount).IsEqualTo(1); + } + } + finally + { + PlatformModeDetector.RestoreState(saved); + } + } + /// Verifies that a null design-mode result falls back to false. /// A representing the asynchronous operation. [Test] @@ -81,11 +108,18 @@ public async Task ResetState_RestoresDefaultDetector() } } - /// A stub mode detector returning a fixed design-mode value. + /// A stub mode detector returning a fixed design-mode value and counting how often it was asked. /// The value to return from . private sealed class StubModeDetector(bool? result) : IPlatformModeDetector { + /// Gets the number of times the detector was asked for the design-mode value. + public int QueryCount { get; private set; } + /// - public bool? InDesignMode() => result; + public bool? InDesignMode() + { + QueryCount++; + return result; + } } }