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
1 change: 1 addition & 0 deletions JianpuEditor.Tests/Glue/DocumentTabTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ private static ServiceProvider BuildServiceProvider()
services.AddScoped<MeasureContentViewModel>();
services.AddScoped<ChordEditorViewModel>();
services.AddScoped<OrnamentEditorViewModel>();
services.AddScoped<DynamicsEditorViewModel>();
services.AddScoped<ScoreEditorViewModel>();
services.AddScoped<PlaybackViewModel>();
services.AddScoped<MainViewModel>();
Expand Down
1 change: 1 addition & 0 deletions JianpuEditor.Tests/MainFormMultiTabTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ private static ServiceProvider BuildServiceProvider()
services.AddScoped<MeasureContentViewModel>();
services.AddScoped<ChordEditorViewModel>();
services.AddScoped<OrnamentEditorViewModel>();
services.AddScoped<DynamicsEditorViewModel>();
services.AddScoped<ScoreEditorViewModel>();
services.AddScoped<PlaybackViewModel>();
services.AddScoped<MainViewModel>();
Expand Down
14 changes: 14 additions & 0 deletions JianpuEditor.Tests/Rendering/AccidentalRenderingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,20 @@ public void RenderToBitmap_SegnoAndCoda_DoesNotThrowInCompactOrDefaultLayout()
}
}

[Fact]
public void RenderToBitmap_DynamicMarking_DoesNotThrow()
{
var measure = ScoreTestHelper.Measure(ScoreTestHelper.Note(1), ScoreTestHelper.Note(2));
DynamicMarkingService.TrySetForNote(measure, 0, "mf");

var score = ScoreTestHelper.CreateScore(measure);
using (var renderer = new JianpuRenderer())
{
Assert.NotNull(renderer.RenderToBitmap(score, 1280, ScoreLayoutOptions.Editor));
Assert.NotNull(renderer.RenderToBitmap(score, 1280, ScoreLayoutOptions.Default));
}
}

[Fact]
public void GetAccidentalMark_ReturnsSharpOrFlat()
{
Expand Down
32 changes: 32 additions & 0 deletions JianpuEditor.Tests/Rendering/StaffRowLayoutTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using JianpuEditor.Rendering;
using Xunit;

namespace JianpuEditor.Tests.Rendering
{
/// <summary>Locks in the row-stacking invariant added for the dynamics row: each row's top must
/// sit exactly one RowGap below the row above it's bottom edge, and the sum of every row plus
/// its gaps must equal StaffBlockHeight -- otherwise rows would overlap or StaffBlockHeight
/// (used everywhere: block bounds, hit-testing, PDF pagination content height, block stacking)
/// would silently drift out of sync with what's actually drawn.</summary>
public sealed class StaffRowLayoutTests
{
[Fact]
public void RowTops_StackWithoutOverlapOrGapDrift()
{
var measure = new JianpuRenderer.MeasureLayout { BlockTop = 100, X = 0, Width = 200 };

var melodyBottom = measure.BlockTop + JianpuRenderer.MelodyRowHeight;
var dynamicsTop = JianpuRenderer.GetDynamicsRowTop(measure);
var dynamicsBottom = dynamicsTop + JianpuRenderer.DynamicsRowHeight;
var secondaryTop = JianpuRenderer.GetSecondaryRowTop(measure);
var secondaryBottom = secondaryTop + JianpuRenderer.SecondaryRowHeight;
var lyricTop = JianpuRenderer.GetLyricRowTop(measure);
var lyricBottom = lyricTop + JianpuRenderer.TextRowHeight;

Assert.Equal(melodyBottom + JianpuRenderer.RowGap, dynamicsTop);
Assert.Equal(dynamicsBottom + JianpuRenderer.RowGap, secondaryTop);
Assert.Equal(secondaryBottom + JianpuRenderer.RowGap, lyricTop);
Assert.Equal(measure.BlockTop + JianpuRenderer.StaffBlockHeight, lyricBottom);
}
}
}
37 changes: 37 additions & 0 deletions JianpuEditor.Tests/Services/DynamicMarkingPlaybackServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using JianpuEditor.Services;
using Xunit;

namespace JianpuEditor.Tests.Services
{
public sealed class DynamicMarkingPlaybackServiceTests
{
[Theory]
[InlineData("pp", DynamicMarkingPlaybackService.PianissimoVelocity)]
[InlineData("p", DynamicMarkingPlaybackService.PianoVelocity)]
[InlineData("mp", DynamicMarkingPlaybackService.MezzoPianoVelocity)]
[InlineData("mf", DynamicMarkingPlaybackService.MezzoForteVelocity)]
[InlineData("f", DynamicMarkingPlaybackService.ForteVelocity)]
[InlineData("ff", DynamicMarkingPlaybackService.FortissimoVelocity)]
public void ResolveVelocity_MapsKnownDynamicText(string text, int expected)
{
Assert.Equal(expected, DynamicMarkingPlaybackService.ResolveVelocity(text, 90));
}

[Fact]
public void ResolveVelocity_FallsBackForUnknownText()
{
Assert.Equal(90, DynamicMarkingPlaybackService.ResolveVelocity("cresc.", 90));
Assert.Equal(90, DynamicMarkingPlaybackService.ResolveVelocity(null, 90));
}

[Fact]
public void ResolveVelocity_LevelsIncreaseMonotonically()
{
Assert.True(DynamicMarkingPlaybackService.PianissimoVelocity < DynamicMarkingPlaybackService.PianoVelocity);
Assert.True(DynamicMarkingPlaybackService.PianoVelocity < DynamicMarkingPlaybackService.MezzoPianoVelocity);
Assert.True(DynamicMarkingPlaybackService.MezzoPianoVelocity < DynamicMarkingPlaybackService.MezzoForteVelocity);
Assert.True(DynamicMarkingPlaybackService.MezzoForteVelocity < DynamicMarkingPlaybackService.ForteVelocity);
Assert.True(DynamicMarkingPlaybackService.ForteVelocity < DynamicMarkingPlaybackService.FortissimoVelocity);
}
}
}
92 changes: 92 additions & 0 deletions JianpuEditor.Tests/Services/DynamicMarkingServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using JianpuEditor.Models;
using JianpuEditor.Services;
using JianpuEditor.Tests.Helpers;
using Xunit;

namespace JianpuEditor.Tests.Services
{
public sealed class DynamicMarkingServiceTests
{
[Fact]
public void TrySetForNote_AddsMarking()
{
var measure = ScoreTestHelper.Measure(ScoreTestHelper.Note(1), ScoreTestHelper.Note(2));

var added = DynamicMarkingService.TrySetForNote(measure, 1, "mf");

Assert.True(added);
Assert.Single(measure.Dynamics);
Assert.Equal("mf", measure.Dynamics[0].Text);
Assert.Equal(1, DynamicMarkingService.ResolveNoteIndex(measure, measure.Dynamics[0]));
}

[Fact]
public void TrySetForNote_ReplacesExistingMarkingOnSameNote()
{
var measure = ScoreTestHelper.Measure(ScoreTestHelper.Note(1));
DynamicMarkingService.TrySetForNote(measure, 0, "p");

DynamicMarkingService.TrySetForNote(measure, 0, "ff");

Assert.Single(measure.Dynamics);
Assert.Equal("ff", measure.Dynamics[0].Text);
}

[Fact]
public void TryRemoveForNote_RemovesMarking()
{
var measure = ScoreTestHelper.Measure(ScoreTestHelper.Note(1));
DynamicMarkingService.TrySetForNote(measure, 0, "f");

var removed = DynamicMarkingService.TryRemoveForNote(measure, 0);

Assert.True(removed);
Assert.Empty(measure.Dynamics);
}

[Fact]
public void GetMarkingForNote_ReturnsNullWhenNoneSet()
{
var measure = ScoreTestHelper.Measure(ScoreTestHelper.Note(1));

Assert.Null(DynamicMarkingService.GetMarkingForNote(measure, 0));
}

[Fact]
public void OnNoteRemoved_ShiftsLaterMarkingIndices()
{
var measure = ScoreTestHelper.Measure(
ScoreTestHelper.Note(1),
ScoreTestHelper.Note(2),
ScoreTestHelper.Note(3));
DynamicMarkingService.TrySetForNote(measure, 2, "f");

DynamicMarkingService.OnNoteRemoved(measure, 0);

Assert.Single(measure.Dynamics);
Assert.Equal(1, measure.Dynamics[0].NoteIndex);
}

[Fact]
public void OnNoteRemoved_DropsMarkingOnTheRemovedNote()
{
var measure = ScoreTestHelper.Measure(ScoreTestHelper.Note(1), ScoreTestHelper.Note(2));
DynamicMarkingService.TrySetForNote(measure, 0, "f");

DynamicMarkingService.OnNoteRemoved(measure, 0);

Assert.Empty(measure.Dynamics);
}

[Fact]
public void NormalizeMeasure_DropsMarkingsWithEmptyText()
{
var measure = ScoreTestHelper.Measure(ScoreTestHelper.Note(1));
measure.Dynamics.Add(new DynamicMarking { Text = string.Empty, NoteIndex = 0 });

DynamicMarkingService.NormalizeMeasure(measure);

Assert.Empty(measure.Dynamics);
}
}
}
2 changes: 1 addition & 1 deletion JianpuEditor.Tests/Services/PdfExportServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public void PlanPages_EightLines_SplitsAcrossPages()

[Theory]
[InlineData(1, 1)]
[InlineData(7, 1)]
[InlineData(7, 2)]
[InlineData(8, 2)]
public void PlanPages_LineCount_MapsToExpectedPageCount(int lineCount, int expectedPages)
{
Expand Down
53 changes: 53 additions & 0 deletions JianpuEditor.Tests/Services/ScoreMidiScheduleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,59 @@ public void Build_PickupMeasure_SecondMeasureStartsRightAfterShortFirstMeasure()
Assert.Equal(6, schedule.TotalQuarterLength, 3);
}

[Fact]
public void Build_NoDynamicMarkings_UsesDefaultMelodyVelocityForEveryNote()
{
var score = ScoreTestHelper.CreateScore(
ScoreTestHelper.Measure(ScoreTestHelper.Note(1), ScoreTestHelper.Note(2)));

var schedule = ScoreMidiSchedule.Build(score);
var melodyNotes = schedule.Notes.Where(note => note.Channel == ScoreMidiSchedule.MelodyChannel).ToList();

Assert.All(melodyNotes, note => Assert.Equal(ScoreMidiSchedule.MelodyVelocity, note.Velocity));
}

[Fact]
public void Build_DynamicMarking_AppliesVelocityFromThatNoteOnwardAcrossMeasures()
{
var score = ScoreTestHelper.CreateScore(
ScoreTestHelper.Measure(ScoreTestHelper.Note(1), ScoreTestHelper.Note(2)),
ScoreTestHelper.Measure(ScoreTestHelper.Note(3)));
DynamicMarkingService.TrySetForNote(score.Measures[0], 1, "pp");

var schedule = ScoreMidiSchedule.Build(score);
var melodyNotes = schedule.Notes
.Where(note => note.Channel == ScoreMidiSchedule.MelodyChannel)
.OrderBy(note => note.StartQuarter)
.ToList();

Assert.Equal(ScoreMidiSchedule.MelodyVelocity, melodyNotes[0].Velocity);
Assert.Equal(DynamicMarkingPlaybackService.PianissimoVelocity, melodyNotes[1].Velocity);
Assert.Equal(DynamicMarkingPlaybackService.PianissimoVelocity, melodyNotes[2].Velocity);
}

[Fact]
public void Build_SecondDynamicMarking_OverridesTheFirstFromItsNoteOnward()
{
var score = ScoreTestHelper.CreateScore(
ScoreTestHelper.Measure(
ScoreTestHelper.Note(1),
ScoreTestHelper.Note(2),
ScoreTestHelper.Note(3)));
DynamicMarkingService.TrySetForNote(score.Measures[0], 0, "ff");
DynamicMarkingService.TrySetForNote(score.Measures[0], 2, "pp");

var schedule = ScoreMidiSchedule.Build(score);
var melodyNotes = schedule.Notes
.Where(note => note.Channel == ScoreMidiSchedule.MelodyChannel)
.OrderBy(note => note.StartQuarter)
.ToList();

Assert.Equal(DynamicMarkingPlaybackService.FortissimoVelocity, melodyNotes[0].Velocity);
Assert.Equal(DynamicMarkingPlaybackService.FortissimoVelocity, melodyNotes[1].Velocity);
Assert.Equal(DynamicMarkingPlaybackService.PianissimoVelocity, melodyNotes[2].Velocity);
}

[Fact]
public void Build_SuppressesTieEndNotes()
{
Expand Down
91 changes: 91 additions & 0 deletions JianpuEditor.Tests/ViewModels/DynamicsEditorViewModelTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using JianpuEditor.Models;
using JianpuEditor.Tests.Helpers;
using JianpuEditor.ViewModels;
using Xunit;

namespace JianpuEditor.Tests.ViewModels
{
public sealed class DynamicsEditorViewModelTests
{
[Fact]
public void SetDynamic_AppliesToSelectedNote()
{
var (document, selection, messenger, history) = ViewModelTestHelper.CreateDocumentWithSelection();
document.EnsureMeasures();
document.Score.Measures[0].MelodyNotes.Add(ScoreTestHelper.Note(1));
selection.UpdateFrom(new ScoreSelectionInfo { MeasureIndex = 0, NoteIndex = 0 });
var navigation = ViewModelTestHelper.CreateMeasureNavigation(document, selection, messenger, history);
var editor = ViewModelTestHelper.CreateDynamicsEditor(document, navigation, selection, messenger, history);

var result = editor.SetDynamic("mf");

Assert.True(result.Changed);
Assert.Single(document.Score.Measures[0].Dynamics);
Assert.Equal("mf", document.Score.Measures[0].Dynamics[0].Text);
}

[Fact]
public void SetDynamic_SameLevelTwice_RemovesIt()
{
var (document, selection, messenger, history) = ViewModelTestHelper.CreateDocumentWithSelection();
document.EnsureMeasures();
document.Score.Measures[0].MelodyNotes.Add(ScoreTestHelper.Note(1));
selection.UpdateFrom(new ScoreSelectionInfo { MeasureIndex = 0, NoteIndex = 0 });
var navigation = ViewModelTestHelper.CreateMeasureNavigation(document, selection, messenger, history);
var editor = ViewModelTestHelper.CreateDynamicsEditor(document, navigation, selection, messenger, history);
editor.SetDynamic("f");

var result = editor.SetDynamic("f");

Assert.True(result.Changed);
Assert.Empty(document.Score.Measures[0].Dynamics);
}

[Fact]
public void SetDynamic_DifferentLevel_ReplacesPreviousOne()
{
var (document, selection, messenger, history) = ViewModelTestHelper.CreateDocumentWithSelection();
document.EnsureMeasures();
document.Score.Measures[0].MelodyNotes.Add(ScoreTestHelper.Note(1));
selection.UpdateFrom(new ScoreSelectionInfo { MeasureIndex = 0, NoteIndex = 0 });
var navigation = ViewModelTestHelper.CreateMeasureNavigation(document, selection, messenger, history);
var editor = ViewModelTestHelper.CreateDynamicsEditor(document, navigation, selection, messenger, history);
editor.SetDynamic("p");

var result = editor.SetDynamic("ff");

Assert.True(result.Changed);
Assert.Single(document.Score.Measures[0].Dynamics);
Assert.Equal("ff", document.Score.Measures[0].Dynamics[0].Text);
}

[Fact]
public void SetDynamic_NoSelection_ReturnsUnchanged()
{
var (document, selection, messenger, history) = ViewModelTestHelper.CreateDocumentWithSelection();
document.EnsureMeasures();
var navigation = ViewModelTestHelper.CreateMeasureNavigation(document, selection, messenger, history);
var editor = ViewModelTestHelper.CreateDynamicsEditor(document, navigation, selection, messenger, history);

var result = editor.SetDynamic("mf");

Assert.False(result.Changed);
}

[Fact]
public void SetDynamic_Undo_RemovesAddedMarking()
{
var (document, selection, messenger, history) = ViewModelTestHelper.CreateDocumentWithSelection();
document.EnsureMeasures();
document.Score.Measures[0].MelodyNotes.Add(ScoreTestHelper.Note(1));
selection.UpdateFrom(new ScoreSelectionInfo { MeasureIndex = 0, NoteIndex = 0 });
var navigation = ViewModelTestHelper.CreateMeasureNavigation(document, selection, messenger, history);
var editor = ViewModelTestHelper.CreateDynamicsEditor(document, navigation, selection, messenger, history);
editor.SetDynamic("mf");

history.Undo();

Assert.Empty(document.Score.Measures[0].Dynamics);
}
}
}
Loading