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