Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions Pointframe.Tests/Services/AnnotationCanvasRendererTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,22 @@ public void SamplePixelColor_UsesFloorNotRound_ForStableMapping()
// 4-pixel wide bitmap: pixel 0=Red, pixel 1=Blue (1px wide each at DPI=1)
var pixels = new byte[4 * 1 * 4];
// pixel 0 = Red
pixels[0] = Colors.Red.B; pixels[1] = Colors.Red.G; pixels[2] = Colors.Red.R; pixels[3] = 255;
pixels[0] = Colors.Red.B;
pixels[1] = Colors.Red.G;
pixels[2] = Colors.Red.R;
pixels[3] = 255;
// pixel 1 = Blue
pixels[4] = Colors.Blue.B; pixels[5] = Colors.Blue.G; pixels[6] = Colors.Blue.R; pixels[7] = 255;
pixels[4] = Colors.Blue.B;
pixels[5] = Colors.Blue.G;
pixels[6] = Colors.Blue.R;
pixels[7] = 255;
// pixel 2,3 = Green
for (var i = 8; i < 16; i += 4)
{
pixels[i] = Colors.Green.B; pixels[i + 1] = Colors.Green.G; pixels[i + 2] = Colors.Green.R; pixels[i + 3] = 255;
pixels[i] = Colors.Green.B;
pixels[i + 1] = Colors.Green.G;
pixels[i + 2] = Colors.Green.R;
pixels[i + 3] = 255;
}

var bitmap = BitmapSource.Create(4, 1, 96, 96, PixelFormats.Bgra32, null, pixels, 4 * 4);
Expand Down Expand Up @@ -145,7 +154,6 @@ public void CropLoupeRegion_AtTopLeftCorner_MaintainsFullWindowSize()
});
}


[Fact]
public void BeginUpdateCommit_WithRectangle_TracksCommittedElement()
{
Expand Down
1 change: 0 additions & 1 deletion Pointframe.Tests/SettingsWindowTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using System.Windows.Interop;
using System.Windows.Media;
using Moq;
using Pointframe;
using Pointframe.Models;
using Pointframe.Services;
using Pointframe.Tests.Services.Handlers;
Expand Down
225 changes: 218 additions & 7 deletions Pointframe.Tests/ViewModels/AnnotationViewModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -685,13 +685,224 @@ public void TrackElement_BeforeBeginGroup_IsIgnored()
}

// Concrete subclass so we can instantiate the abstract-like partial base
private sealed partial class TestAnnotationViewModel(AnnotationGeometryService geom, IEventAggregator? eventAggregator = null)
: AnnotationViewModel(
geom,
NullLogger<AnnotationViewModel>.Instance,
Mock.Of<IUserSettingsService>(s => s.Current == new UserSettings()),
eventAggregator ?? new DefaultEventAggregator(NullLogger<DefaultEventAggregator>.Instance))
{ }
private sealed partial class TestAnnotationViewModel : AnnotationViewModel
{
public TestAnnotationViewModel(
AnnotationGeometryService geom,
IEventAggregator? eventAggregator = null,
IUserSettingsService? settingsService = null)
: base(
geom,
NullLogger<AnnotationViewModel>.Instance,
settingsService ?? Mock.Of<IUserSettingsService>(s => s.Current == new UserSettings()),
eventAggregator ?? new DefaultEventAggregator(NullLogger<DefaultEventAggregator>.Instance))
{ }
}

// -----------------------------------------------------------------------
// Style Preset tests
// -----------------------------------------------------------------------

private static Mock<IUserSettingsService> MakeSettingsMockWithPresets(UserSettings? settings = null)
{
var s = settings ?? new UserSettings
{
StylePresets =
[
new() { Name = "Red Bold", Color = "#FFFF0000", StrokeThickness = 4.0 },
new() { Name = "Blue", Color = "#FF1E90FF", StrokeThickness = 2.5 },
],
};
var mock = new Mock<IUserSettingsService>();
mock.SetupGet(x => x.Current).Returns(s);
mock.Setup(x => x.Update(It.IsAny<Action<UserSettings>>()))
.Callback<Action<UserSettings>>(a => a(s));
return mock;
}

[Fact]
public void ApplyPreset_ValidIndex_SetsActiveColor()
{
// Arrange
var settingsMock = MakeSettingsMockWithPresets();
var vm = new TestAnnotationViewModel(Geom(), settingsService: settingsMock.Object);

// Act
vm.ApplyPresetCommand.Execute(vm.StylePresets[0]);

// Assert
Assert.Equal(Colors.Red, vm.ActiveColor);
}

[Fact]
public void ApplyPreset_ValidIndex_SetsStrokeThickness()
{
// Arrange
var settingsMock = MakeSettingsMockWithPresets();
var vm = new TestAnnotationViewModel(Geom(), settingsService: settingsMock.Object);

// Act
vm.ApplyPresetCommand.Execute(vm.StylePresets[0]);

// Assert
Assert.Equal(4.0, vm.StrokeThickness);
}

[Fact]
public void ApplyPreset_ValidIndex_SetsActivePresetIndex()
{
// Arrange
var settingsMock = MakeSettingsMockWithPresets();
var vm = new TestAnnotationViewModel(Geom(), settingsService: settingsMock.Object);

// Act
vm.ApplyPresetCommand.Execute(vm.StylePresets[1]);

// Assert
Assert.Equal(1, vm.ActivePresetIndex);
}

[Fact]
public void ApplyPreset_ValidIndex_CallsSettingsUpdate()
{
// Arrange
var settingsMock = MakeSettingsMockWithPresets();
var vm = new TestAnnotationViewModel(Geom(), settingsService: settingsMock.Object);

// Act
vm.ApplyPresetCommand.Execute(vm.StylePresets[0]);

// Assert
settingsMock.Verify(x => x.Update(It.IsAny<Action<UserSettings>>()), Times.Once);
}

[Fact]
public void ApplyPreset_ValidIndex_SavesPresetColorAsDefault()
{
// Arrange
var settings = new UserSettings
{
StylePresets = [new() { Name = "Green", Color = "#FF22A422", StrokeThickness = 3.0 }],
};
var settingsMock = MakeSettingsMockWithPresets(settings);
var vm = new TestAnnotationViewModel(Geom(), settingsService: settingsMock.Object);

// Act
vm.ApplyPresetCommand.Execute(vm.StylePresets[0]);

// Assert — Update callback mutated the settings object
Assert.Equal("#FF22A422", settings.DefaultAnnotationColor);
Assert.Equal(3.0, settings.DefaultStrokeThickness);
}

[Fact]
public void SetColorFromTag_ClearsActivePresetIndex()
{
// Arrange
var settingsMock = MakeSettingsMockWithPresets();
var vm = new TestAnnotationViewModel(Geom(), settingsService: settingsMock.Object);
vm.ApplyPresetCommand.Execute(vm.StylePresets[0]);
Assert.NotNull(vm.ActivePresetIndex);

// Act
vm.SetColorFromTag("Blue");

// Assert
Assert.Null(vm.ActivePresetIndex);
}

[Fact]
public void SetStrokeThicknessFromText_ClearsActivePresetIndex()
{
// Arrange
var settingsMock = MakeSettingsMockWithPresets();
var vm = new TestAnnotationViewModel(Geom(), settingsService: settingsMock.Object);
vm.ApplyPresetCommand.Execute(vm.StylePresets[0]);
Assert.NotNull(vm.ActivePresetIndex);

// Act
vm.SetStrokeThicknessFromText("3.0");

// Assert
Assert.Null(vm.ActivePresetIndex);
}

[Fact]
public void StylePresets_Count_MatchesSettingsPresets()
{
// Arrange
var settingsMock = MakeSettingsMockWithPresets();

// Act
var vm = new TestAnnotationViewModel(Geom(), settingsService: settingsMock.Object);

// Assert
Assert.Equal(2, vm.StylePresets.Count);
Assert.True(vm.HasStylePresets);
}

[Fact]
public void HasStylePresets_ReturnsFalse_WhenNoPresets()
{
// Arrange
var settings = new UserSettings { StylePresets = [] };
var mock = new Mock<IUserSettingsService>();
mock.SetupGet(x => x.Current).Returns(settings);
var vm = new TestAnnotationViewModel(Geom(), settingsService: mock.Object);

// Assert
Assert.False(vm.HasStylePresets);
}

[Fact]
public void ApplyPreset_PropertyChanged_FiresForActivePresetIndex()
{
// Arrange
var settingsMock = MakeSettingsMockWithPresets();
var vm = new TestAnnotationViewModel(Geom(), settingsService: settingsMock.Object);
vm.ActiveColor = Colors.Green; // ensure it differs from preset 0 (red) so PropertyChanged fires
var raised = new List<string?>();
vm.PropertyChanged += (_, e) => raised.Add(e.PropertyName);

// Act
vm.ApplyPresetCommand.Execute(vm.StylePresets[0]);

// Assert
Assert.Contains(nameof(vm.ActivePresetIndex), raised);
Assert.Contains(nameof(vm.ActiveColor), raised);
Assert.Contains(nameof(vm.StrokeThickness), raised);
}

[Fact]
public void ToggleColorMenu_TogglesIsColorMenuOpen()
{
// Arrange
var vm = new TestAnnotationViewModel(Geom());
Assert.False(vm.IsColorMenuOpen);

// Act / Assert
vm.ToggleColorMenuCommand.Execute(null);
Assert.True(vm.IsColorMenuOpen);

vm.ToggleColorMenuCommand.Execute(null);
Assert.False(vm.IsColorMenuOpen);
}

[Fact]
public void ApplyPreset_ClosesColorMenu()
{
// Arrange
var settingsMock = MakeSettingsMockWithPresets();
var vm = new TestAnnotationViewModel(Geom(), settingsService: settingsMock.Object);
vm.ToggleColorMenuCommand.Execute(null);
Assert.True(vm.IsColorMenuOpen);

// Act
vm.ApplyPresetCommand.Execute(vm.StylePresets[0]);

// Assert
Assert.False(vm.IsColorMenuOpen);
}

private sealed class GroupMessageRecorder
{
Expand Down
11 changes: 11 additions & 0 deletions Pointframe/Models/AnnotationStylePreset.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Pointframe.Models;

public sealed class AnnotationStylePreset
{
public const int MaxNameLength = 24;
public const int MaxCount = 5;

public string Name { get; set; } = "Preset";
public string Color { get; set; } = "#FFFF0000";
public double StrokeThickness { get; set; } = 2.5;
}
7 changes: 7 additions & 0 deletions Pointframe/Models/UserSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,11 @@ public sealed class UserSettings
public DateTime? LastAutoUpdateCheckUtc { get; set; } = null;

public AppTheme Theme { get; set; } = AppTheme.System;

public List<AnnotationStylePreset> StylePresets { get; set; } =
[
new() { Name = "Red", Color = "#FFFF0000", StrokeThickness = 2.5 },
new() { Name = "Blue", Color = "#FF1E90FF", StrokeThickness = 2.5 },
new() { Name = "Black", Color = "#FF1A1A1A", StrokeThickness = 3.5 },
];
}
Loading
Loading