@ -23,6 +23,7 @@ using System.Threading.Tasks;
@@ -23,6 +23,7 @@ using System.Threading.Tasks;
using Avalonia.Controls ;
using Avalonia.Headless.NUnit ;
using Avalonia.Threading ;
using Avalonia.VisualTree ;
using AwesomeAssertions ;
@ -40,10 +41,12 @@ using NUnit.Framework;
@@ -40,10 +41,12 @@ using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests ;
/// <summary>
/// The options dialog edits the font size in points (like the Windows font dialogs and the
/// WPF host's FontSizeConverter), while <see cref="DisplaySettings.SelectedFontSize"/> keeps
/// The options dialog edits the font size in points (the unit Windows font dialogs use),
/// while <see cref="DisplaySettings.SelectedFontSize"/> keeps
/// storing device-independent pixels so persisted settings round-trip with ILSpy 9.x.
/// The pt/px conversion lives in <see cref="DisplaySettingsViewModel.SelectedFontSizePoints"/>.
/// No per-test save/restore of the settings here: ResetAppState rebuilds the container and
/// settings file before every test, so each test starts from pristine defaults.
/// </summary>
[TestFixture]
public class DisplayFontSizeTests
@ -59,36 +62,22 @@ public class DisplayFontSizeTests
@@ -59,36 +62,22 @@ public class DisplayFontSizeTests
[AvaloniaTest]
public void Default_Pixel_Size_Displays_As_10_Points ( )
{
// Deliberately no arrange: this asserts on the genuine built-in default.
var ( page , settings ) = CreatePage ( ) ;
var original = settings . SelectedFontSize ;
try
{
settings . SelectedFontSize = 1 0.0 * 4 / 3 ;
page . SelectedFontSizePoints . Should ( ) . Be ( "10" ,
"the default 13.33 px must be presented as the 10 pt it actually is" ) ;
}
finally
{
settings . SelectedFontSize = original ;
}
settings . SelectedFontSize . Should ( ) . BeApproximately ( 1 0.0 * 4 / 3 , 1e-9 ,
"precondition: the built-in default is 10 pt expressed in pixels" ) ;
page . SelectedFontSizePoints . Should ( ) . Be ( "10" ,
"the default 13.33 px must be presented as the 10 pt it actually is" ) ;
}
[AvaloniaTest]
public void Typed_Point_Size_Is_Stored_As_Pixels ( )
{
var ( page , settings ) = CreatePage ( ) ;
var original = settings . SelectedFontSize ;
try
{
page . SelectedFontSizePoints = "12" ;
settings . SelectedFontSize . Should ( ) . BeApproximately ( 1 2.0 * 4 / 3 , 1e-9 ,
"the stored value stays device-independent pixels for 9.x round-tripping" ) ;
page . SelectedFontSizePoints . Should ( ) . Be ( "12" ) ;
}
finally
{
settings . SelectedFontSize = original ;
}
page . SelectedFontSizePoints = "12" ;
settings . SelectedFontSize . Should ( ) . BeApproximately ( 1 2.0 * 4 / 3 , 1e-9 ,
"the stored value stays device-independent pixels for 9.x round-tripping" ) ;
page . SelectedFontSizePoints . Should ( ) . Be ( "12" ) ;
}
[AvaloniaTest]
@ -97,21 +86,13 @@ public class DisplayFontSizeTests
@@ -97,21 +86,13 @@ public class DisplayFontSizeTests
// Reset-to-defaults and LoadFromXml write SelectedFontSize directly; the dialog text
// must follow via PropertyChanged on SelectedFontSizePoints.
var ( page , settings ) = CreatePage ( ) ;
var original = settings . SelectedFontSize ;
try
{
var notified = new List < string? > ( ) ;
page . PropertyChanged + = ( _ , e ) = > notified . Add ( e . PropertyName ) ;
settings . SelectedFontSize = 2 0 ;
notified . Should ( ) . Contain ( nameof ( DisplaySettingsViewModel . SelectedFontSizePoints ) ) ;
page . SelectedFontSizePoints . Should ( ) . Be ( "15" ) ;
}
finally
{
settings . SelectedFontSize = original ;
}
var notified = new List < string? > ( ) ;
page . PropertyChanged + = ( _ , e ) = > notified . Add ( e . PropertyName ) ;
settings . SelectedFontSize = 2 0 ;
notified . Should ( ) . Contain ( nameof ( DisplaySettingsViewModel . SelectedFontSizePoints ) ) ;
page . SelectedFontSizePoints . Should ( ) . Be ( "15" ) ;
}
[AvaloniaTest]
@ -120,97 +101,139 @@ public class DisplayFontSizeTests
@@ -120,97 +101,139 @@ public class DisplayFontSizeTests
// While the user is typing in the size box, the setter must not raise PropertyChanged
// for SelectedFontSizePoints - the binding would rewrite the box mid-keystroke
// (typing "10." would snap back to "10" before the fraction can be completed).
var ( page , _ ) = CreatePage ( ) ;
var notified = new List < string? > ( ) ;
page . PropertyChanged + = ( _ , e ) = > notified . Add ( e . PropertyName ) ;
page . SelectedFontSizePoints = "14" ;
notified . Should ( ) . NotContain ( nameof ( DisplaySettingsViewModel . SelectedFontSizePoints ) ) ;
}
[AvaloniaTest]
public void NonNumeric_And_Empty_Input_Are_Ignored ( )
{
// The empty-string case is load-bearing, not just typing UX: ComboBox re-publishes its
// still-empty Text when ItemsSource initializes before the Text binding has delivered a
// value. If the setter ever "helpfully" fell back to a default instead, every Options
// page load would silently reset the font size.
var ( page , settings ) = CreatePage ( ) ;
var original = settings . SelectedFontSize ;
try
{
var notified = new List < string? > ( ) ;
page . PropertyChanged + = ( _ , e ) = > notified . Add ( e . PropertyName ) ;
page . SelectedFontSizePoints = "14" ;
notified . Should ( ) . NotContain ( nameof ( DisplaySettingsViewModel . SelectedFontSizePoints ) ) ;
}
finally
{
settings . SelectedFontSize = original ;
}
settings . SelectedFontSize = 1 6 ;
page . SelectedFontSizePoints = "abc" ;
settings . SelectedFontSize . Should ( ) . Be ( 1 6 ,
"transient garbage while typing must not move the stored size" ) ;
page . SelectedFontSizePoints = "" ;
settings . SelectedFontSize . Should ( ) . Be ( 1 6 ,
"the empty Text published during ComboBox ItemsSource initialization must not clobber the setting" ) ;
}
[AvaloniaTest]
public void NonNumeric _Input_Is_Ignored ( )
public void NaN_Input_Is_Ignored ( )
{
// double.TryParse accepts the culture's NaN symbol and NaN falls through Math.Clamp.
// Persisted, it would fail DecompilerTextEditor's SelectedFontSize > 0 guard on every
// run, permanently disabling font-size application until the settings file is repaired.
var ( page , settings ) = CreatePage ( ) ;
var original = settings . SelectedFontSize ;
try
{
settings . SelectedFontSize = 1 6 ;
page . SelectedFontSizePoints = "abc" ;
settings . SelectedFontSize . Should ( ) . Be ( 1 6 ,
"transient garbage while typing must not move the stored size" ) ;
}
finally
{
settings . SelectedFontSize = original ;
}
settings . SelectedFontSize = 1 6 ;
page . SelectedFontSizePoints = "NaN" ;
settings . SelectedFontSize . Should ( ) . Be ( 1 6 ) ;
double . IsFinite ( settings . SelectedFontSize ) . Should ( ) . BeTrue ( ) ;
}
[AvaloniaTest]
public void Typed_Sizes_Are_Clamped_To_The_6_To_72_Point_Range ( )
{
var ( page , settings ) = CreatePage ( ) ;
var original = settings . SelectedFontSize ;
try
{
page . SelectedFontSizePoints = "1" ;
settings . SelectedFontSize . Should ( ) . BeApproximately ( 6.0 * 4 / 3 , 1e-9 ) ;
page . SelectedFontSizePoints = "500" ;
settings . SelectedFontSize . Should ( ) . BeApproximately ( 7 2.0 * 4 / 3 , 1e-9 ) ;
}
finally
{
settings . SelectedFontSize = original ;
}
page . SelectedFontSizePoints = "1" ;
settings . SelectedFontSize . Should ( ) . BeApproximately ( 6.0 * 4 / 3 , 1e-9 ) ;
page . SelectedFontSizePoints = "500" ;
settings . SelectedFontSize . Should ( ) . BeApproximately ( 7 2.0 * 4 / 3 , 1e-9 ) ;
}
[AvaloniaTest]
public void Size_List_Offers_6_Through_24_Points_Like_The_WPF_Host ( )
public void Size_List_Offers_6_Through_24_Points ( )
{
var ( page , _ ) = CreatePage ( ) ;
page . FontSizes . Should ( ) . Equal ( Enumerable . Range ( 6 , 2 4 - 6 + 1 ) ) ;
}
static async Task < ComboBox > OpenDisplayPanelSizeBoxAsync ( MainWindow window )
{
AppComposition . Current . GetExport < MainMenuCommandRegistry > ( )
. GetCommand ( nameof ( Resources . _ Options ) ) . Execute ( null ) ;
var vm = ( MainWindowViewModel ) window . DataContext ! ;
var model = ( OptionsPageModel ) vm . DockWorkspace . Documents ! . VisibleDockables !
. OfType < ContentTabPage > ( ) . First ( t = > t . Content is OptionsPageModel ) . Content ! ;
model . SelectedPage = model . Pages . OfType < DisplaySettingsViewModel > ( ) . Single ( ) ;
TestCapture . Step ( "display-page-selected" ) ;
var panel = await window . WaitForComponent < DisplaySettingsPanel > ( ) ;
var box = panel . FindControl < ComboBox > ( "fontSizeComboBox" ) ;
( ( object? ) box ) . Should ( ) . NotBeNull ( "the size box must be the named editable ComboBox" ) ;
Dispatcher . UIThread . RunJobs ( ) ;
return box ! ;
}
[AvaloniaTest]
public async Task Display_Panel_Size_Box_Is_An_Editable_ComboBox_Showing_Points ( )
{
var window = AppComposition . Current . GetExport < MainWindow > ( ) ;
window . Show ( ) ;
var settings = AppComposition . Current . GetExport < SettingsService > ( ) ;
var original = settings . DisplaySettings . SelectedFontSize ;
try
{
settings . DisplaySettings . SelectedFontSize = 1 0.0 * 4 / 3 ;
AppComposition . Current . GetExport < MainMenuCommandRegistry > ( )
. GetCommand ( nameof ( Resources . _ Options ) ) . Execute ( null ) ;
var vm = ( MainWindowViewModel ) window . DataContext ! ;
var model = ( OptionsPageModel ) vm . DockWorkspace . Documents ! . VisibleDockables !
. OfType < ContentTabPage > ( ) . First ( t = > t . Content is OptionsPageModel ) . Content ! ;
model . SelectedPage = model . Pages . OfType < DisplaySettingsViewModel > ( ) . Single ( ) ;
TestCapture . Step ( "display-page-selected" ) ;
var panel = await window . WaitForComponent < DisplaySettingsPanel > ( ) ;
var box = panel . FindControl < ComboBox > ( "fontSizeComboBox" ) ;
( ( object? ) box ) . Should ( ) . NotBeNull ( "the size box must be the named editable ComboBox" ) ;
Dispatcher . UIThread . RunJobs ( ) ;
box ! . IsEditable . Should ( ) . BeTrue ( "custom sizes must be typeable, like Notepad's font page" ) ;
box . Text . Should ( ) . Be ( "10" , "the box shows points, not device-independent pixels" ) ;
}
finally
{
settings . DisplaySettings . SelectedFontSize = original ;
}
var box = await OpenDisplayPanelSizeBoxAsync ( window ) ;
box . IsEditable . Should ( ) . BeTrue ( "custom sizes must be typeable, like Notepad's font page" ) ;
box . Text . Should ( ) . Be ( "10" , "the box shows points, not device-independent pixels" ) ;
// IsEditable=true is only real if the applied ControlTheme materialized the editable
// text box part; assert the template realized it rather than trusting the property.
box . ApplyTemplate ( ) ;
Dispatcher . UIThread . RunJobs ( ) ;
box . GetVisualDescendants ( ) . OfType < TextBox > ( )
. Should ( ) . Contain ( t = > t . Name = = "PART_EditableTextBox" ,
"the theme must realize the editable text box, or IsEditable is a no-op" ) ;
}
[AvaloniaTest]
public async Task Clamped_Value_Is_Written_Back_Into_The_Box_On_Focus_Loss ( )
{
// Typing "3" stores the clamped 6 pt, but the echo suppression leaves the box showing
// "3". Focus loss must resync the text from the stored value, like the NumericUpDown
// this ComboBox replaced did via CommitInput on LostFocus.
var window = AppComposition . Current . GetExport < MainWindow > ( ) ;
window . Show ( ) ;
var settings = AppComposition . Current . GetExport < SettingsService > ( ) . DisplaySettings ;
var box = await OpenDisplayPanelSizeBoxAsync ( window ) ;
// A real focus change, not a synthetic RaiseEvent: LostFocus is typed
// (FocusChangedEventArgs), so hand-built RoutedEventArgs blow up any typed
// subscriber on the route - and a genuine focus move is what users do anyway.
// The editable ComboBox delegates focus to its template's text box, so focus that
// (ComboBox.Focus() itself returns false when IsEditable).
box . ApplyTemplate ( ) ;
Dispatcher . UIThread . RunJobs ( ) ;
var sizeEditor = box . GetVisualDescendants ( ) . OfType < TextBox > ( )
. First ( t = > t . Name = = "PART_EditableTextBox" ) ;
sizeEditor . Focus ( ) . Should ( ) . BeTrue ( "headless focus must land in the size box" ) ;
Dispatcher . UIThread . RunJobs ( ) ;
box . Text = "3" ;
Dispatcher . UIThread . RunJobs ( ) ;
settings . SelectedFontSize . Should ( ) . BeApproximately ( 6.0 * 4 / 3 , 1e-9 ,
"precondition: the typed 3 is stored clamped to 6 pt" ) ;
TestCapture . Step ( "undersized-value-typed" ) ;
var elsewhere = box . FindAncestorOfType < DisplaySettingsPanel > ( ) !
. GetVisualDescendants ( ) . OfType < CheckBox > ( ) . First ( ) ;
elsewhere . Focus ( ) . Should ( ) . BeTrue ( "headless focus must be able to leave the size box" ) ;
Dispatcher . UIThread . RunJobs ( ) ;
TestCapture . Step ( "focus-left-size-box" ) ;
box . Text . Should ( ) . Be ( "6" , "focus loss must replace the rejected text with the clamped value" ) ;
}
}