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; + } } }