From a12e78c74057d43b9586a8029454be70183f65f6 Mon Sep 17 00:00:00 2001 From: Sathish-087 Date: Wed, 12 Aug 2026 09:44:46 +0530 Subject: [PATCH] Added unit test cases for Issue 13442 --- .../System/Windows/Forms/HtmlHistoryTests.cs | 249 ++++++++++++++++++ .../Forms/RadioButtonFlatAdapterTests.cs | 201 ++++++++++++++ .../Forms/RadioButtonPopupAdapterTests.cs | 238 +++++++++++++++++ ...oolStrip.RestoreFocusMessageFilterTests.cs | 181 +++++++++++++ 4 files changed, 869 insertions(+) create mode 100644 src/test/unit/System.Windows.Forms/System/Windows/Forms/HtmlHistoryTests.cs create mode 100644 src/test/unit/System.Windows.Forms/System/Windows/Forms/RadioButtonFlatAdapterTests.cs create mode 100644 src/test/unit/System.Windows.Forms/System/Windows/Forms/RadioButtonPopupAdapterTests.cs create mode 100644 src/test/unit/System.Windows.Forms/System/Windows/Forms/ToolStrip.RestoreFocusMessageFilterTests.cs diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/HtmlHistoryTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/HtmlHistoryTests.cs new file mode 100644 index 00000000000..f21e84f20f3 --- /dev/null +++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/HtmlHistoryTests.cs @@ -0,0 +1,249 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System.Text; +using Windows.Win32.Web.MsHtml; + +namespace System.Windows.Forms.Tests; + +[Collection("Sequential")] // workaround for WebBrowser control corrupting memory when run on multiple UI threads +public class HtmlHistoryTests +{ + private const string HtmlPage1 = "page1"; + private const string HtmlPage2 = "page2"; + + [WinFormsFact] + public async Task HtmlHistory_Length_Get_ReturnsExpected() + { + using Control parent = new(); + using WebBrowser control = new() + { + Parent = parent + }; + + HtmlDocument document = await GetDocument(control, HtmlPage1); + document.Window.Should().NotBeNull(); + using HtmlHistory? history = document.Window.History; + + history.Should().NotBeNull(); + // IE/WebBrowser reports engine-specific history lengths (often 0 for a single load). + // Exercise the getter and only require a non-negative value. + history!.Length.Should().BeGreaterThanOrEqualTo(0); + } + + [WinFormsFact] + public async Task HtmlHistory_DomHistory_Get_ReturnsExpected() + { + using Control parent = new(); + using WebBrowser control = new() + { + Parent = parent + }; + + HtmlDocument document = await GetDocument(control, HtmlPage1); + document.Window.Should().NotBeNull(); + using HtmlHistory? history = document.Window.History; + + history.Should().NotBeNull(); + object domHistory = history!.DomHistory; + + domHistory.Should().NotBeNull(); + domHistory.Should().BeSameAs(history.DomHistory); + domHistory.GetType().IsCOMObject.Should().BeTrue(); + domHistory.Should().BeAssignableTo(); + } + + [WinFormsFact] + public async Task HtmlHistory_Back_Negative_ThrowsArgumentOutOfRangeException() + { + using Control parent = new(); + using WebBrowser control = new() + { + Parent = parent + }; + + HtmlDocument document = await GetDocument(control, HtmlPage1); + document.Window.Should().NotBeNull(); + using HtmlHistory? history = document.Window.History; + + history.Should().NotBeNull(); + Action action = () => history!.Back(-1); + action.Should().Throw() + .And.ParamName.Should().Be("numberBack"); + } + + [WinFormsFact] + public async Task HtmlHistory_Forward_Negative_ThrowsArgumentOutOfRangeException() + { + using Control parent = new(); + using WebBrowser control = new() + { + Parent = parent + }; + + HtmlDocument document = await GetDocument(control, HtmlPage1); + document.Window.Should().NotBeNull(); + using HtmlHistory? history = document.Window.History; + + history.Should().NotBeNull(); + Action action = () => history!.Forward(-1); + action.Should().Throw() + .And.ParamName.Should().Be("numberForward"); + } + + [WinFormsFact] + public async Task HtmlHistory_Back_And_Forward_Zero_DoNotThrow() + { + using Control parent = new(); + using WebBrowser control = new() + { + Parent = parent + }; + + HtmlDocument document = await GetDocument(control, HtmlPage1); + document.Window.Should().NotBeNull(); + using HtmlHistory? history = document.Window.History; + + history.Should().NotBeNull(); + + // Zero is a no-op (guarded by number > 0) and must not call into COM go(). + Action back = () => history!.Back(0); + Action forward = () => history!.Forward(0); + + back.Should().NotThrow(); + forward.Should().NotThrow(); + } + + [WinFormsFact] + public async Task HtmlHistory_Back_And_Forward_Positive_InvokeGo() + { + using Control parent = new(); + using WebBrowser control = new() + { + Parent = parent + }; + + // Keep both temp files alive for the life of the test. Disposing them after + // Navigate can leave history entries pointing at deleted paths and collapse the stack. + using TempFile file1 = CreateTempFile(HtmlPage1); + using TempFile file2 = CreateTempFile(HtmlPage2); + + await NavigateToPathAsync(control, file1.Path); + await NavigateToPathAsync(control, file2.Path); + + // Positive Back/Forward must enter the number > 0 branch that calls IOmHistory.go. + // Do not assert Length — IE reports engine-specific values (often 1 after two loads). + // Do not wait for DocumentCompleted — go() may not raise it when the stack is thin. + control.Document.Should().NotBeNull(); + control.Document!.Window.Should().NotBeNull(); + + using (HtmlHistory? history = control.Document.Window!.History) + { + history.Should().NotBeNull(); + Action back = () => history!.Back(1); + back.Should().NotThrow(); + } + + // History wrapper is per-get; obtain a fresh instance after Back. + control.Document.Window.Should().NotBeNull(); + using (HtmlHistory? history = control.Document.Window!.History) + { + history.Should().NotBeNull(); + Action forward = () => history!.Forward(1); + forward.Should().NotThrow(); + } + } + + [WinFormsFact] + public async Task HtmlHistory_Go_Overloads_DoNotThrow() + { + using Control parent = new(); + using WebBrowser control = new() + { + Parent = parent + }; + + HtmlDocument document = await GetDocument(control, HtmlPage1); + document.Window.Should().NotBeNull(); + using HtmlHistory? history = document.Window.History; + + history.Should().NotBeNull(); + + // Relative position always forwards to COM go(); 0 is the safe relative position. + Action goRelative = () => history!.Go(0); + // String overload intentionally accepts values that may not be fully qualified Uris. + Action goString = () => history!.Go("about:blank"); + // Uri overload is a thin wrapper over the string overload. + Action goUri = () => history!.Go(new Uri("about:blank")); + + goRelative.Should().NotThrow(); + goString.Should().NotThrow(); + goUri.Should().NotThrow(); + } + + [WinFormsFact] + public async Task HtmlHistory_Dispose_IsIdempotentAndMembersThrow() + { + using Control parent = new(); + using WebBrowser control = new() + { + Parent = parent + }; + + HtmlDocument document = await GetDocument(control, HtmlPage1); + document.Window.Should().NotBeNull(); + HtmlHistory? history = document.Window.History; + + history.Should().NotBeNull(); + history!.Dispose(); + + // Dispose is safe to call more than once. + Action disposeAgain = history.Dispose; + disposeAgain.Should().NotThrow(); + + // Members route through NativeOmHistory, which throws once disposed. + // Back/Forward(1) also cover the disposed path past the negative check. + Action getLength = () => _ = history.Length; + Action getDomHistory = () => _ = history.DomHistory; + Action back = () => history.Back(1); + Action forward = () => history.Forward(1); + Action go = () => history.Go(0); + + getLength.Should().Throw(); + getDomHistory.Should().Throw(); + back.Should().Throw(); + forward.Should().Throw(); + go.Should().Throw(); + } + + private static async Task GetDocument(WebBrowser control, string html) + { + using TempFile file = CreateTempFile(html); + await NavigateToPathAsync(control, file.Path); + return control.Document!; + } + + private static async Task NavigateToPathAsync(WebBrowser control, string path) + { + TaskCompletionSource source = new(); + void Handler(object? sender, WebBrowserDocumentCompletedEventArgs e) => source.TrySetResult(true); + control.DocumentCompleted += Handler; + try + { + await Task.Run(() => control.Navigate(path)); + Assert.True(await source.Task); + } + finally + { + control.DocumentCompleted -= Handler; + } + } + + private static TempFile CreateTempFile(string html) + { + byte[] data = Encoding.UTF8.GetBytes(html); + return TempFile.Create(data); + } +} diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/RadioButtonFlatAdapterTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/RadioButtonFlatAdapterTests.cs new file mode 100644 index 00000000000..c71fb00d2d4 --- /dev/null +++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/RadioButtonFlatAdapterTests.cs @@ -0,0 +1,201 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System.Drawing; +using System.Windows.Forms.ButtonInternal; +using static System.Windows.Forms.ButtonInternal.ButtonBaseAdapter; + +namespace System.Windows.Forms.Tests; + +public class RadioButtonFlatAdapterTests : IDisposable +{ + private RadioButton? _radioButton; + + private (RadioButtonFlatAdapter Adapter, RadioButton Control) CreateAdapter( + Appearance appearance = Appearance.Normal, + bool enabled = true, + bool @checked = false) + { + _radioButton?.Dispose(); + _radioButton = new RadioButton + { + Appearance = appearance, + Enabled = enabled, + Checked = @checked, + Size = new Size(100, 30), + Text = "Radio" + }; + + return (new RadioButtonFlatAdapter(_radioButton), _radioButton); + } + + public void Dispose() => _radioButton?.Dispose(); + + [WinFormsTheory] + [InlineData(Appearance.Button, true, true)] + [InlineData(Appearance.Button, true, false)] + [InlineData(Appearance.Button, false, true)] + [InlineData(Appearance.Button, false, false)] + [InlineData(Appearance.Normal, true, true)] + [InlineData(Appearance.Normal, true, false)] + [InlineData(Appearance.Normal, false, true)] + [InlineData(Appearance.Normal, false, false)] + public void PaintDown_DoesNotThrow(Appearance appearance, bool enabled, bool @checked) + { + (RadioButtonFlatAdapter adapter, RadioButton control) = CreateAdapter(appearance, enabled, @checked); + using Bitmap bitmap = new(control.Width, control.Height); + using Graphics graphics = Graphics.FromImage(bitmap); + using PaintEventArgs e = new(graphics, control.ClientRectangle); + + Action action = () => adapter.PaintDown(e, control.Checked ? CheckState.Checked : CheckState.Unchecked); + + action.Should().NotThrow(); + } + + [WinFormsTheory] + [InlineData(Appearance.Button, true, true)] + [InlineData(Appearance.Button, true, false)] + [InlineData(Appearance.Button, false, true)] + [InlineData(Appearance.Button, false, false)] + [InlineData(Appearance.Normal, true, true)] + [InlineData(Appearance.Normal, true, false)] + [InlineData(Appearance.Normal, false, true)] + [InlineData(Appearance.Normal, false, false)] + public void PaintOver_DoesNotThrow(Appearance appearance, bool enabled, bool @checked) + { + (RadioButtonFlatAdapter adapter, RadioButton control) = CreateAdapter(appearance, enabled, @checked); + using Bitmap bitmap = new(control.Width, control.Height); + using Graphics graphics = Graphics.FromImage(bitmap); + using PaintEventArgs e = new(graphics, control.ClientRectangle); + + Action action = () => adapter.PaintOver(e, control.Checked ? CheckState.Checked : CheckState.Unchecked); + + action.Should().NotThrow(); + } + + [WinFormsTheory] + [InlineData(Appearance.Button, true, true)] + [InlineData(Appearance.Button, true, false)] + [InlineData(Appearance.Button, false, true)] + [InlineData(Appearance.Button, false, false)] + [InlineData(Appearance.Normal, true, true)] + [InlineData(Appearance.Normal, true, false)] + [InlineData(Appearance.Normal, false, true)] + [InlineData(Appearance.Normal, false, false)] + public void PaintUp_DoesNotThrow(Appearance appearance, bool enabled, bool @checked) + { + (RadioButtonFlatAdapter adapter, RadioButton control) = CreateAdapter(appearance, enabled, @checked); + using Bitmap bitmap = new(control.Width, control.Height); + using Graphics graphics = Graphics.FromImage(bitmap); + using PaintEventArgs e = new(graphics, control.ClientRectangle); + + Action action = () => adapter.PaintUp(e, control.Checked ? CheckState.Checked : CheckState.Unchecked); + + action.Should().NotThrow(); + } + + [WinFormsFact] + public void CreateButtonAdapter_ReturnsButtonFlatAdapter() + { + (RadioButtonFlatAdapter adapter, _) = CreateAdapter(); + + ButtonBaseAdapter result = adapter.TestAccessor.Dynamic.CreateButtonAdapter(); + + result.Should().NotBeNull(); + result.Should().BeOfType(); + } + + [WinFormsFact] + public void Layout_SetsCheckSizeAndDisablesShadowedText() + { + (RadioButtonFlatAdapter adapter, RadioButton control) = CreateAdapter(); + using Bitmap bitmap = new(control.Width, control.Height); + using Graphics graphics = Graphics.FromImage(bitmap); + using PaintEventArgs e = new(graphics, control.ClientRectangle); + + LayoutOptions layout = adapter.TestAccessor.Dynamic.Layout(e); + + layout.Should().NotBeNull(); + layout.CheckSize.Should().BeGreaterThan(0); + layout.ShadowedText.Should().BeFalse(); + } + + [WinFormsFact] + public void Layout_CheckSize_MatchesFlatCheckSizeScaledByDpi() + { + (RadioButtonFlatAdapter adapter, RadioButton control) = CreateAdapter(); + using Bitmap bitmap = new(control.Width, control.Height); + using Graphics graphics = Graphics.FromImage(bitmap); + using PaintEventArgs e = new(graphics, control.ClientRectangle); + + LayoutOptions layout = adapter.TestAccessor.Dynamic.Layout(e); + double dpiScale = adapter.TestAccessor.Dynamic.GetDpiScaleRatio(); + int expectedCheckSize = (int)(12 * dpiScale); + + layout.CheckSize.Should().Be(expectedCheckSize); + } + + [WinFormsTheory] + [InlineData(true)] + [InlineData(false)] + public void PaintUp_NormalAppearance_WithTextAndImage_DoesNotThrow(bool enabled) + { + (RadioButtonFlatAdapter adapter, RadioButton control) = CreateAdapter(Appearance.Normal, enabled, @checked: true); + control.Text = "Option"; + using Bitmap image = new(16, 16); + using (Graphics g = Graphics.FromImage(image)) + { + g.Clear(Color.Red); + } + + control.Image = image; + + using Bitmap bitmap = new(control.Width, control.Height); + using Graphics graphics = Graphics.FromImage(bitmap); + using PaintEventArgs e = new(graphics, control.ClientRectangle); + + Action action = () => adapter.PaintUp(e, CheckState.Checked); + + action.Should().NotThrow(); + } + + [WinFormsTheory] + [InlineData(CheckState.Unchecked)] + [InlineData(CheckState.Checked)] + public void PaintDown_NormalAppearance_CheckedStates_DoesNotThrow(CheckState state) + { + (RadioButtonFlatAdapter adapter, RadioButton control) = CreateAdapter( + Appearance.Normal, + enabled: true, + @checked: state == CheckState.Checked); + + using Bitmap bitmap = new(control.Width, control.Height); + using Graphics graphics = Graphics.FromImage(bitmap); + using PaintEventArgs e = new(graphics, control.ClientRectangle); + + Action action = () => adapter.PaintDown(e, state); + + action.Should().NotThrow(); + } + + [WinFormsTheory] + [InlineData(CheckState.Unchecked)] + [InlineData(CheckState.Checked)] + public void PaintOver_NormalAppearance_CheckedStates_DoesNotThrow(CheckState state) + { + (RadioButtonFlatAdapter adapter, RadioButton control) = CreateAdapter( + Appearance.Normal, + enabled: true, + @checked: state == CheckState.Checked); + + using Bitmap bitmap = new(control.Width, control.Height); + using Graphics graphics = Graphics.FromImage(bitmap); + using PaintEventArgs e = new(graphics, control.ClientRectangle); + + Action action = () => adapter.PaintOver(e, state); + + action.Should().NotThrow(); + } +} diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/RadioButtonPopupAdapterTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/RadioButtonPopupAdapterTests.cs new file mode 100644 index 00000000000..fa20e03a40c --- /dev/null +++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/RadioButtonPopupAdapterTests.cs @@ -0,0 +1,238 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System.Drawing; +using System.Windows.Forms.ButtonInternal; +using static System.Windows.Forms.ButtonInternal.ButtonBaseAdapter; + +namespace System.Windows.Forms.Tests; + +public class RadioButtonPopupAdapterTests : IDisposable +{ + private class TestRadioButton : RadioButton + { + public void InvokeOnMouseEnter(EventArgs e) => base.OnMouseEnter(e); + + public void InvokeOnMouseDown(MouseEventArgs e) => base.OnMouseDown(e); + } + + private TestRadioButton? _radioButton; + + private (RadioButtonPopupAdapter Adapter, TestRadioButton Control) CreateAdapter( + Appearance appearance = Appearance.Normal, + bool enabled = true, + bool @checked = false) + { + _radioButton?.Dispose(); + _radioButton = new TestRadioButton + { + Appearance = appearance, + Enabled = enabled, + Checked = @checked, + Size = new Size(100, 30), + Text = "Radio" + }; + + return (new RadioButtonPopupAdapter(_radioButton), _radioButton); + } + + public void Dispose() => _radioButton?.Dispose(); + + [WinFormsTheory] + [InlineData(Appearance.Button, true, true)] + [InlineData(Appearance.Button, true, false)] + [InlineData(Appearance.Button, false, true)] + [InlineData(Appearance.Button, false, false)] + [InlineData(Appearance.Normal, true, true)] + [InlineData(Appearance.Normal, true, false)] + [InlineData(Appearance.Normal, false, true)] + [InlineData(Appearance.Normal, false, false)] + public void PaintDown_DoesNotThrow(Appearance appearance, bool enabled, bool @checked) + { + (RadioButtonPopupAdapter adapter, RadioButton control) = CreateAdapter(appearance, enabled, @checked); + using Bitmap bitmap = new(control.Width, control.Height); + using Graphics graphics = Graphics.FromImage(bitmap); + using PaintEventArgs e = new(graphics, control.ClientRectangle); + + Action action = () => adapter.PaintDown(e, control.Checked ? CheckState.Checked : CheckState.Unchecked); + + action.Should().NotThrow(); + } + + [WinFormsTheory] + [InlineData(Appearance.Button, true, true)] + [InlineData(Appearance.Button, true, false)] + [InlineData(Appearance.Button, false, true)] + [InlineData(Appearance.Button, false, false)] + [InlineData(Appearance.Normal, true, true)] + [InlineData(Appearance.Normal, true, false)] + [InlineData(Appearance.Normal, false, true)] + [InlineData(Appearance.Normal, false, false)] + public void PaintOver_DoesNotThrow(Appearance appearance, bool enabled, bool @checked) + { + (RadioButtonPopupAdapter adapter, RadioButton control) = CreateAdapter(appearance, enabled, @checked); + using Bitmap bitmap = new(control.Width, control.Height); + using Graphics graphics = Graphics.FromImage(bitmap); + using PaintEventArgs e = new(graphics, control.ClientRectangle); + + Action action = () => adapter.PaintOver(e, control.Checked ? CheckState.Checked : CheckState.Unchecked); + + action.Should().NotThrow(); + } + + [WinFormsTheory] + [InlineData(Appearance.Button, true, true)] + [InlineData(Appearance.Button, true, false)] + [InlineData(Appearance.Button, false, true)] + [InlineData(Appearance.Button, false, false)] + [InlineData(Appearance.Normal, true, true)] + [InlineData(Appearance.Normal, true, false)] + [InlineData(Appearance.Normal, false, true)] + [InlineData(Appearance.Normal, false, false)] + public void PaintUp_DoesNotThrow(Appearance appearance, bool enabled, bool @checked) + { + (RadioButtonPopupAdapter adapter, RadioButton control) = CreateAdapter(appearance, enabled, @checked); + using Bitmap bitmap = new(control.Width, control.Height); + using Graphics graphics = Graphics.FromImage(bitmap); + using PaintEventArgs e = new(graphics, control.ClientRectangle); + + Action action = () => adapter.PaintUp(e, control.Checked ? CheckState.Checked : CheckState.Unchecked); + + action.Should().NotThrow(); + } + + [WinFormsFact] + public void CreateButtonAdapter_ReturnsButtonPopupAdapter() + { + (RadioButtonPopupAdapter adapter, _) = CreateAdapter(); + + ButtonBaseAdapter result = adapter.TestAccessor.Dynamic.CreateButtonAdapter(); + + result.Should().NotBeNull(); + result.Should().BeOfType(); + } + + [WinFormsFact] + public void Layout_WhenMouseNotOverOrDown_EnablesShadowedText() + { + (RadioButtonPopupAdapter adapter, RadioButton control) = CreateAdapter(); + using Bitmap bitmap = new(control.Width, control.Height); + using Graphics graphics = Graphics.FromImage(bitmap); + using PaintEventArgs e = new(graphics, control.ClientRectangle); + + LayoutOptions layout = adapter.TestAccessor.Dynamic.Layout(e); + + layout.Should().NotBeNull(); + layout.CheckSize.Should().BeGreaterThan(0); + layout.ShadowedText.Should().BeTrue(); + } + + [WinFormsFact] + public void Layout_WhenMouseOver_DisablesShadowedText() + { + (RadioButtonPopupAdapter adapter, TestRadioButton control) = CreateAdapter(); + control.InvokeOnMouseEnter(EventArgs.Empty); + + using Bitmap bitmap = new(control.Width, control.Height); + using Graphics graphics = Graphics.FromImage(bitmap); + using PaintEventArgs e = new(graphics, control.ClientRectangle); + + LayoutOptions layout = adapter.TestAccessor.Dynamic.Layout(e); + + layout.ShadowedText.Should().BeFalse(); + } + + [WinFormsFact] + public void Layout_WhenMouseDown_DisablesShadowedText() + { + (RadioButtonPopupAdapter adapter, TestRadioButton control) = CreateAdapter(); + control.InvokeOnMouseDown(new MouseEventArgs(MouseButtons.Left, 1, 0, 0, 0)); + + using Bitmap bitmap = new(control.Width, control.Height); + using Graphics graphics = Graphics.FromImage(bitmap); + using PaintEventArgs e = new(graphics, control.ClientRectangle); + + LayoutOptions layout = adapter.TestAccessor.Dynamic.Layout(e); + + layout.ShadowedText.Should().BeFalse(); + } + + [WinFormsFact] + public void Layout_CheckSize_MatchesFlatCheckSizeScaledByDpi() + { + (RadioButtonPopupAdapter adapter, RadioButton control) = CreateAdapter(); + using Bitmap bitmap = new(control.Width, control.Height); + using Graphics graphics = Graphics.FromImage(bitmap); + using PaintEventArgs e = new(graphics, control.ClientRectangle); + + LayoutOptions layout = adapter.TestAccessor.Dynamic.Layout(e); + double dpiScale = adapter.TestAccessor.Dynamic.GetDpiScaleRatio(); + int expectedCheckSize = (int)(12 * dpiScale); + + layout.CheckSize.Should().Be(expectedCheckSize); + } + + [WinFormsTheory] + [InlineData(true)] + [InlineData(false)] + public void PaintUp_NormalAppearance_WithTextAndImage_DoesNotThrow(bool enabled) + { + (RadioButtonPopupAdapter adapter, RadioButton control) = CreateAdapter(Appearance.Normal, enabled, @checked: true); + control.Text = "Option"; + using Bitmap image = new(16, 16); + using (Graphics g = Graphics.FromImage(image)) + { + g.Clear(Color.Red); + } + + control.Image = image; + + using Bitmap bitmap = new(control.Width, control.Height); + using Graphics graphics = Graphics.FromImage(bitmap); + using PaintEventArgs e = new(graphics, control.ClientRectangle); + + Action action = () => adapter.PaintUp(e, CheckState.Checked); + + action.Should().NotThrow(); + } + + [WinFormsTheory] + [InlineData(CheckState.Unchecked)] + [InlineData(CheckState.Checked)] + public void PaintDown_NormalAppearance_CheckedStates_DoesNotThrow(CheckState state) + { + (RadioButtonPopupAdapter adapter, RadioButton control) = CreateAdapter( + Appearance.Normal, + enabled: true, + @checked: state == CheckState.Checked); + + using Bitmap bitmap = new(control.Width, control.Height); + using Graphics graphics = Graphics.FromImage(bitmap); + using PaintEventArgs e = new(graphics, control.ClientRectangle); + + Action action = () => adapter.PaintDown(e, state); + + action.Should().NotThrow(); + } + + [WinFormsTheory] + [InlineData(CheckState.Unchecked)] + [InlineData(CheckState.Checked)] + public void PaintOver_NormalAppearance_CheckedStates_DoesNotThrow(CheckState state) + { + (RadioButtonPopupAdapter adapter, RadioButton control) = CreateAdapter( + Appearance.Normal, + enabled: true, + @checked: state == CheckState.Checked); + + using Bitmap bitmap = new(control.Width, control.Height); + using Graphics graphics = Graphics.FromImage(bitmap); + using PaintEventArgs e = new(graphics, control.ClientRectangle); + + Action action = () => adapter.PaintOver(e, state); + + action.Should().NotThrow(); + } +} diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/ToolStrip.RestoreFocusMessageFilterTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/ToolStrip.RestoreFocusMessageFilterTests.cs new file mode 100644 index 00000000000..7be11523a6a --- /dev/null +++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/ToolStrip.RestoreFocusMessageFilterTests.cs @@ -0,0 +1,181 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System.Drawing; + +namespace System.Windows.Forms.Tests; + +public class ToolStrip_RestoreFocusMessageFilterTests +{ + public static TheoryData MouseDownMessages => new() + { + (int)PInvokeCore.WM_LBUTTONDOWN, + (int)PInvokeCore.WM_RBUTTONDOWN, + (int)PInvokeCore.WM_MBUTTONDOWN, + (int)PInvokeCore.WM_NCLBUTTONDOWN, + (int)PInvokeCore.WM_NCRBUTTONDOWN, + (int)PInvokeCore.WM_NCMBUTTONDOWN, + }; + + [WinFormsFact] + public void Ctor_StoresOwnerToolStrip() + { + using ToolStrip toolStrip = new(); + ToolStrip.RestoreFocusMessageFilter filter = new(toolStrip); + + ToolStrip owner = filter.TestAccessor.Dynamic._ownerToolStrip; + owner.Should().BeSameAs(toolStrip); + } + + [WinFormsFact] + public void RestoreFocusFilter_ReturnsSameInstance() + { + using ToolStrip toolStrip = new(); + + ToolStrip.RestoreFocusMessageFilter first = toolStrip.RestoreFocusFilter; + ToolStrip.RestoreFocusMessageFilter second = toolStrip.RestoreFocusFilter; + + first.Should().BeSameAs(second); + } + + [WinFormsFact] + public void PreFilterMessage_NonMouseMessage_ReturnsFalse() + { + using ToolStrip toolStrip = new(); + ToolStrip.RestoreFocusMessageFilter filter = new(toolStrip); + // Use the public Create(IntPtr, int, IntPtr, IntPtr) overload to avoid MessageId accessibility + // and uint/MessageId internal overload ambiguity. + Message message = Message.Create(IntPtr.Zero, (int)PInvokeCore.WM_MOUSEMOVE, IntPtr.Zero, IntPtr.Zero); + + filter.PreFilterMessage(ref message).Should().BeFalse(); + } + + [WinFormsTheory] + [MemberData(nameof(MouseDownMessages))] + public void PreFilterMessage_WhenOwnerDisposed_ReturnsFalse(int msg) + { + ToolStrip toolStrip = new(); + ToolStrip.RestoreFocusMessageFilter filter = new(toolStrip); + toolStrip.Dispose(); + + Message message = Message.Create(IntPtr.Zero, msg, IntPtr.Zero, IntPtr.Zero); + + filter.PreFilterMessage(ref message).Should().BeFalse(); + } + + [WinFormsTheory] + [MemberData(nameof(MouseDownMessages))] + public void PreFilterMessage_WhenOwnerIsDropDown_ReturnsFalse(int msg) + { + using ToolStripDropDown dropDown = new(); + ToolStrip.RestoreFocusMessageFilter filter = new(dropDown); + dropDown.IsDropDown.Should().BeTrue(); + + Message message = Message.Create(IntPtr.Zero, msg, IntPtr.Zero, IntPtr.Zero); + + filter.PreFilterMessage(ref message).Should().BeFalse(); + } + + [WinFormsTheory] + [MemberData(nameof(MouseDownMessages))] + public void PreFilterMessage_WhenToolStripDoesNotContainFocus_ReturnsFalse(int msg) + { + using Form form = new() { ShowInTaskbar = false }; + using ToolStrip toolStrip = new(); + using Button sibling = new() { Text = "Sibling" }; + form.Controls.Add(toolStrip); + form.Controls.Add(sibling); + form.Show(); + + toolStrip.ContainsFocus.Should().BeFalse(); + + ToolStrip.RestoreFocusMessageFilter filter = new(toolStrip); + Message message = Message.Create(sibling.Handle, msg, IntPtr.Zero, IntPtr.Zero); + + filter.PreFilterMessage(ref message).Should().BeFalse(); + } + + [WinFormsTheory] + [MemberData(nameof(MouseDownMessages))] + public void PreFilterMessage_WhenClickIsOnToolStripChild_DoesNotRestoreFocus(int msg) + { + using Form form = new() { ShowInTaskbar = false }; + using TrackingToolStrip toolStrip = new(); + using TextBox hostedTextBox = new() { Width = 80 }; + toolStrip.Items.Add(new ToolStripControlHost(hostedTextBox)); + form.Controls.Add(toolStrip); + form.Show(); + + hostedTextBox.Focus(); + toolStrip.ContainsFocus.Should().BeTrue(); + + ToolStrip.RestoreFocusMessageFilter filter = new(toolStrip); + Application.AddMessageFilter(filter); + + try + { + Message message = Message.Create(hostedTextBox.Handle, msg, IntPtr.Zero, IntPtr.Zero); + + filter.PreFilterMessage(ref message).Should().BeFalse(); + + // Click is on a child of the toolstrip — restore must not be scheduled. + Application.DoEvents(); + toolStrip.RestoreFocusCallCount.Should().Be(0); + } + finally + { + Application.RemoveMessageFilter(filter); + } + } + + [WinFormsTheory] + [MemberData(nameof(MouseDownMessages))] + public void PreFilterMessage_WhenClickIsOutsideToolStripOnSameRoot_RestoresFocus(int msg) + { + using Form form = new() { ShowInTaskbar = false }; + using TrackingToolStrip toolStrip = new(); + using TextBox hostedTextBox = new() { Width = 80 }; + using Button sibling = new() { Text = "Sibling", Location = new Point(0, 40) }; + toolStrip.Items.Add(new ToolStripControlHost(hostedTextBox)); + form.Controls.Add(toolStrip); + form.Controls.Add(sibling); + form.Show(); + + hostedTextBox.Focus(); + toolStrip.ContainsFocus.Should().BeTrue(); + toolStrip.TabStop.Should().BeFalse(); + + ToolStrip.RestoreFocusMessageFilter filter = toolStrip.RestoreFocusFilter; + Application.AddMessageFilter(filter); + + try + { + Message message = Message.Create(sibling.Handle, msg, IntPtr.Zero, IntPtr.Zero); + + // Filter never consumes the message. + filter.PreFilterMessage(ref message).Should().BeFalse(); + + // Restore is posted via BeginInvoke; pump so it runs. + Application.DoEvents(); + toolStrip.RestoreFocusCallCount.Should().Be(1); + } + finally + { + Application.RemoveMessageFilter(filter); + ToolStripManager.ModalMenuFilter.ExitMenuMode(); + } + } + + private sealed class TrackingToolStrip : ToolStrip + { + public int RestoreFocusCallCount { get; private set; } + + protected override void RestoreFocus() + { + RestoreFocusCallCount++; + base.RestoreFocus(); + } + } +}