Files
Artemis/src/Artemis.UI.Shared.Tests/ProfileEditor/Commands/LayerPropertyCommandTests.cs
T
RobertBeekman 0920cf8917 Profile editor - Fix undoing profile editor commands
- Undoing a change to an existing keyframe's value restores the value instead of deleting the keyframe
- Undoing "remove effect" puts the effect back in its place instead of below the next one
- Undoing "toggle keyframes" restores what it was before
- Undoing a brush change on a layer that had no brush removes the brush again, using the new Layer.RemoveLayerBrush
- Undoing "add element" disables the element again
- Undoing an event path change back to no path goes back to an event start node, like a new condition
- MoveProfileElement says "Move folder" for folders
- UpdateLayerProperty without a time no longer calls itself "Add ... keyframe"
2026-10-20 09:47:43 +02:00

294 lines
10 KiB
C#

using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Artemis.UI.Shared.Tests.Infrastructure;
using RGB.NET.Core;
using SkiaSharp;
namespace Artemis.UI.Shared.Tests.ProfileEditor.Commands;
/// <summary>
/// Changing and resetting property values, data bindings, color gradients and timeline segments
/// </summary>
public class LayerPropertyCommandTests : ProfileEditorTestBase
{
private readonly Layer _layer;
private readonly FloatLayerProperty _opacity;
public LayerPropertyCommandTests()
{
_layer = AddLayer("Layer", null, LedId.Keyboard_A);
_opacity = _layer.Transform.Opacity;
}
private LayerPropertyKeyframe<float> AddKeyframe(float value, double seconds)
{
LayerPropertyKeyframe<float> keyframe = new(value, TimeSpan.FromSeconds(seconds), Easings.Functions.Linear, _opacity);
_opacity.AddKeyframe(keyframe);
return keyframe;
}
[Fact]
public void UpdateLayerProperty_WithoutKeyframes_ChangesTheValue()
{
string before = Snapshot();
UpdateLayerProperty<float> command = new(_opacity, 40, TimeSpan.FromSeconds(1));
command.Execute();
command.DisplayName.ShouldBe("Update Opacity");
_opacity.BaseValue.ShouldBe(40);
_opacity.CurrentValue.ShouldBe(40);
command.Undo();
_opacity.BaseValue.ShouldBe(100);
Snapshot().ShouldBe(before);
}
[Fact]
public void UpdateLayerProperty_WithKeyframes_AddsAKeyframeAtTheTime()
{
AddKeyframe(0, 0);
string before = Snapshot();
UpdateLayerProperty<float> command = new(_opacity, 40, TimeSpan.FromSeconds(1));
command.Execute();
command.DisplayName.ShouldBe("Add Opacity keyframe");
LayerPropertyKeyframe<float> added = _opacity.Keyframes.Last();
added.Position.ShouldBe(TimeSpan.FromSeconds(1));
added.Value.ShouldBe(40);
command.Undo();
_opacity.Keyframes.Count.ShouldBe(1);
Snapshot().ShouldBe(before);
command.Execute();
_opacity.Keyframes.Last().ShouldBe(added);
}
[Fact]
public void UpdateLayerProperty_OnAKeyframe_ChangesItsValue()
{
AddKeyframe(0, 0);
LayerPropertyKeyframe<float> keyframe = AddKeyframe(60, 1);
_layer.OverrideTimelineAndApply(TimeSpan.FromSeconds(1));
string before = Snapshot();
UpdateLayerProperty<float> command = new(_opacity, 40, TimeSpan.FromSeconds(1));
command.Execute();
command.DisplayName.ShouldBe("Update Opacity");
_opacity.Keyframes.Count.ShouldBe(2);
keyframe.Value.ShouldBe(40);
command.Undo();
keyframe.Value.ShouldBe(60);
Snapshot().ShouldBe(before);
}
[Fact]
public void UpdateLayerProperty_OnAKeyframe_UndoesToTheValueAtTheCurrentTime()
{
AddKeyframe(0, 0);
LayerPropertyKeyframe<float> keyframe = AddKeyframe(60, 2);
// The property shows the value of the timeline's position, which is between the two keyframes
_layer.OverrideTimelineAndApply(TimeSpan.FromSeconds(1));
UpdateLayerProperty<float> command = new(_opacity, 40, TimeSpan.FromSeconds(2));
command.Execute();
command.Undo();
// The original value is the value the property had at the time the command was created, not the keyframe's own
// value. The editor always edits keyframes at the current time, where the two are the same
keyframe.Value.ShouldBe(30, 0.01f);
}
[Fact]
public void UpdateLayerProperty_WithOriginalValue_ChangesTheValue()
{
// The property inputs show a preview while dragging and pass the value from before the preview
_opacity.SetCurrentValue(55);
UpdateLayerProperty<float> command = new(_opacity, 40, 100, TimeSpan.FromSeconds(1));
command.Execute();
_opacity.BaseValue.ShouldBe(40);
command.Undo();
_opacity.BaseValue.ShouldBe(100);
}
[Fact]
public void UpdateLayerProperty_WithOriginalValue_OnAKeyframe_RestoresTheKeyframeOnUndo()
{
AddKeyframe(0, 0);
LayerPropertyKeyframe<float> keyframe = AddKeyframe(60, 1);
UpdateLayerProperty<float> command = new(_opacity, 40, 60, TimeSpan.FromSeconds(1));
command.Execute();
keyframe.Value.ShouldBe(40);
command.Undo();
_opacity.Keyframes.ShouldContain(keyframe);
_opacity.Keyframes.Count.ShouldBe(2);
keyframe.Value.ShouldBe(60);
command.DisplayName.ShouldBe("Update Opacity");
}
[Fact]
public void UpdateLayerProperty_WithoutTime_ChangesTheBaseValueEvenWithKeyframes()
{
AddKeyframe(0, 0);
UpdateLayerProperty<float> command = new(_opacity, 40, null);
command.Execute();
_opacity.BaseValue.ShouldBe(40);
_opacity.Keyframes.Count.ShouldBe(1);
command.DisplayName.ShouldBe("Update Opacity");
command.Undo();
_opacity.BaseValue.ShouldBe(0, 0.01f);
}
[Fact]
public void ResetLayerProperty_RestoresTheDefaultAndTurnsOffKeyframes()
{
_opacity.SetCurrentValue(40);
AddKeyframe(10, 0);
string before = Snapshot();
ResetLayerProperty<float> command = new(_opacity);
command.Execute();
command.DisplayName.ShouldBe("Reset layer property");
_opacity.BaseValue.ShouldBe(100);
_opacity.KeyframesEnabled.ShouldBeFalse();
_opacity.Keyframes.Count.ShouldBe(1);
command.Undo();
_opacity.BaseValue.ShouldBe(40);
_opacity.KeyframesEnabled.ShouldBeTrue();
Snapshot().ShouldBe(before);
}
[Fact]
public void ResetLayerProperty_WithoutDefault_Throws()
{
_layer.General.BrushReference.DefaultValue.ShouldBeNull();
Should.Throw<ArtemisSharedUIException>(() => new ResetLayerProperty<LayerBrushReference>(_layer.General.BrushReference));
}
[Fact]
public void ChangeDataBindingEnabled_TogglesAndUndoRestores()
{
string before = Snapshot();
ChangeDataBindingEnabled enable = new(_opacity, true);
enable.Execute();
enable.DisplayName.ShouldBe("Enable data binding");
_opacity.BaseDataBinding.IsEnabled.ShouldBeTrue();
enable.Undo();
_opacity.BaseDataBinding.IsEnabled.ShouldBeFalse();
Snapshot().ShouldBe(before);
new ChangeDataBindingEnabled(_opacity, false).DisplayName.ShouldBe("Disable data binding");
}
[Fact]
public void UpdateColorGradient_ReplacesTheStopsAndUndoRestoresThem()
{
ColorGradient gradient = [new ColorGradientStop(SKColors.Red, 0), new ColorGradientStop(SKColors.Blue, 1)];
UpdateColorGradient command = new(gradient, [new ColorGradientStop(SKColors.Lime, 0.2f), new ColorGradientStop(SKColors.White, 0.5f), new ColorGradientStop(SKColors.Black, 0.9f)], null);
command.Execute();
command.DisplayName.ShouldBe("Update color gradient");
gradient.Select(s => (s.Color, s.Position)).ShouldBe([(SKColors.Lime, 0.2f), (SKColors.White, 0.5f), (SKColors.Black, 0.9f)]);
command.Undo();
gradient.Select(s => (s.Color, s.Position)).ShouldBe([(SKColors.Red, 0f), (SKColors.Blue, 1f)]);
}
[Fact]
public void UpdateColorGradient_UpdatesExistingStopsInPlace()
{
ColorGradientStop first = new(SKColors.Red, 0);
ColorGradientStop second = new(SKColors.Blue, 1);
ColorGradient gradient = [first, second];
UpdateColorGradient command = new(gradient, [new ColorGradientStop(SKColors.Lime, 0.3f)], null);
command.Execute();
gradient.ShouldBe([first]);
first.Color.ShouldBe(SKColors.Lime);
command.Undo();
gradient[0].ShouldBe(first);
gradient.Count.ShouldBe(2);
gradient[1].ShouldNotBeSameAs(second);
}
[Fact]
public void UpdateColorGradient_KeepsItsOwnCopyOfTheStops()
{
ColorGradient gradient = [new ColorGradientStop(SKColors.Red, 0)];
ColorGradientStop newStop = new(SKColors.Lime, 0.5f);
UpdateColorGradient command = new(gradient, [newStop], null);
newStop.Color = SKColors.Black;
command.Execute();
gradient[0].Color.ShouldBe(SKColors.Lime);
}
[Fact]
public void UpdateColorGradient_WithOriginalStops_UndoesToThoseStops()
{
// The gradient editor changes the gradient while editing and passes the stops from before
ColorGradient gradient = [new ColorGradientStop(SKColors.Lime, 0.5f)];
UpdateColorGradient command = new(gradient, [new ColorGradientStop(SKColors.Lime, 0.5f)], [new ColorGradientStop(SKColors.Red, 0), new ColorGradientStop(SKColors.Blue, 1)]);
command.Execute();
command.Undo();
gradient.Select(s => (s.Color, s.Position)).ShouldBe([(SKColors.Red, 0f), (SKColors.Blue, 1f)]);
}
[Theory]
[InlineData(ResizeTimelineSegment.SegmentType.Start, "Resize start segment")]
[InlineData(ResizeTimelineSegment.SegmentType.Main, "Resize main segment")]
[InlineData(ResizeTimelineSegment.SegmentType.End, "Resize end segment")]
public void ResizeTimelineSegment_ResizesAndUndoRestoresTheLength(ResizeTimelineSegment.SegmentType segmentType, string displayName)
{
_layer.Timeline.StartSegmentLength = TimeSpan.FromSeconds(1);
_layer.Timeline.EndSegmentLength = TimeSpan.FromSeconds(2);
string before = Snapshot();
ResizeTimelineSegment command = new(segmentType, _layer, TimeSpan.FromSeconds(7));
command.Execute();
command.DisplayName.ShouldBe(displayName);
SegmentLength(segmentType).ShouldBe(TimeSpan.FromSeconds(7));
command.Undo();
Snapshot().ShouldBe(before);
}
[Fact]
public void ResizeTimelineSegment_WithOriginalLength_UndoesToThatLength()
{
// The timeline resizes segments while dragging and passes the length from before the drag
_layer.Timeline.MainSegmentLength = TimeSpan.FromSeconds(6);
ResizeTimelineSegment command = new(ResizeTimelineSegment.SegmentType.Main, _layer, TimeSpan.FromSeconds(7), TimeSpan.FromSeconds(5));
command.Execute();
command.Undo();
_layer.Timeline.MainSegmentLength.ShouldBe(TimeSpan.FromSeconds(5));
}
private TimeSpan SegmentLength(ResizeTimelineSegment.SegmentType segmentType)
{
return segmentType switch
{
ResizeTimelineSegment.SegmentType.Start => _layer.Timeline.StartSegmentLength,
ResizeTimelineSegment.SegmentType.Main => _layer.Timeline.MainSegmentLength,
_ => _layer.Timeline.EndSegmentLength
};
}
}