Files
SongTidy/SongTidy.Tests/ViewModels/MainWindowViewModelTests.cs

310 lines
10 KiB
C#

using System.IO;
using System.Linq;
using SongTidy.Models;
using SongTidy.Services;
using SongTidy.Tests.TestUtilities;
using SongTidy.ViewModels;
namespace SongTidy.Tests.ViewModels;
/// <summary>
/// Contains tests for <see cref="MainWindowViewModel"/>.
/// </summary>
[TestClass]
public sealed class MainWindowViewModelTests {
private readonly TempDirectory _tempDirectory = new();
/// <summary>
/// Removes the temporary directory created for the current test.
/// </summary>
[TestCleanup]
public void Cleanup() {
_tempDirectory.Dispose();
}
#region Input and merge rules
/// <summary>
/// Verifies that added files are parsed and appended.
/// </summary>
[TestMethod]
public void AddFilesParsesAndAppendsEntries() {
MainWindowViewModel viewModel = new();
string sourcePath = Path.Combine(_tempDirectory.FullPath, "A - B (Live).mp3");
viewModel.AddFiles(new[] { sourcePath });
Assert.HasCount(1, viewModel.Entries);
Assert.AreEqual(1, viewModel.EntryCount);
Assert.IsTrue(viewModel.IsDirty);
SongEntryViewModel entry = viewModel.Entries[0];
Assert.AreEqual(sourcePath, entry.FilePath);
Assert.AreEqual("A", entry.Artist);
Assert.AreEqual("B", entry.Title);
Assert.AreEqual("Live", entry.Comment);
Assert.AreEqual("A - B (Live).mp3", entry.OriginalFileName);
Assert.AreEqual("A - B (Live).mp3", entry.TargetFileName);
}
/// <summary>
/// Verifies that duplicate paths keep the existing entry and its edits.
/// </summary>
[TestMethod]
public void AddFilesSkipsDuplicatesAndPreservesEdits() {
MainWindowViewModel viewModel = new();
string sourcePath = Path.Combine(_tempDirectory.FullPath, "A - B.mp3");
viewModel.AddFiles(new[] { sourcePath });
viewModel.Entries[0].Artist = "Edited";
viewModel.AddFiles(new[] { sourcePath });
Assert.HasCount(1, viewModel.Entries);
Assert.AreEqual("Edited", viewModel.Entries[0].Artist);
}
/// <summary>
/// Verifies that duplicate detection compares full path strings case-sensitively.
/// </summary>
[TestMethod]
public void AddFilesTreatsPathsAsCaseSensitive() {
MainWindowViewModel viewModel = new();
viewModel.AddFiles(new[] {
Path.Combine(_tempDirectory.FullPath, "a.mp3"),
Path.Combine(_tempDirectory.FullPath, "A.mp3"),
});
Assert.HasCount(2, viewModel.Entries);
}
/// <summary>
/// Verifies that adding only duplicates does not change the dirty state.
/// </summary>
[TestMethod]
public void AddFilesWithOnlyDuplicatesDoesNotDirty() {
MainWindowViewModel viewModel = new();
string sourcePath = Path.Combine(_tempDirectory.FullPath, "A.mp3");
viewModel.AddFiles(new[] { sourcePath });
viewModel.SavePlan(Path.Combine(_tempDirectory.FullPath, "plan.xml"));
viewModel.AddFiles(new[] { sourcePath });
Assert.IsFalse(viewModel.IsDirty);
}
/// <summary>
/// Verifies that folder input uses the audio extension filter.
/// </summary>
[TestMethod]
public void AddFilesFromFolderAddsOnlyAudioFiles() {
MainWindowViewModel viewModel = new();
CreateFile("A.mp3");
CreateFile("B.FLAC");
CreateFile("C.txt");
CreateFile(Path.Combine("sub", "D.wav"));
viewModel.AddFilesFromFolder(_tempDirectory.FullPath);
Assert.HasCount(2, viewModel.Entries);
}
#endregion
#region Dirty tracking
/// <summary>
/// Verifies that field edits mark the document dirty and reverting does not clear it.
/// </summary>
[TestMethod]
public void EntryEditMarksDirtyAndRevertingDoesNotClear() {
MainWindowViewModel viewModel = new();
string sourcePath = Path.Combine(_tempDirectory.FullPath, "A.mp3");
viewModel.AddFiles(new[] { sourcePath });
viewModel.SavePlan(Path.Combine(_tempDirectory.FullPath, "plan.xml"));
SongEntryViewModel entry = viewModel.Entries[0];
string originalArtist = entry.Artist;
entry.Artist = "Changed";
Assert.IsTrue(viewModel.IsDirty);
entry.Artist = originalArtist;
Assert.IsTrue(viewModel.IsDirty);
}
/// <summary>
/// Verifies that changing the format updates previews and marks the document dirty.
/// </summary>
[TestMethod]
public void FormatChangeUpdatesPreviewsAndMarksDirty() {
MainWindowViewModel viewModel = new();
string sourcePath = Path.Combine(_tempDirectory.FullPath, "A - B (Live).mp3");
viewModel.AddFiles(new[] { sourcePath });
viewModel.SavePlan(Path.Combine(_tempDirectory.FullPath, "plan.xml"));
viewModel.SelectedFormat = RenameFormat.TitleFirst;
Assert.IsTrue(viewModel.IsDirty);
Assert.AreEqual("B (Live) - A.mp3", viewModel.Entries[0].TargetFileName);
}
/// <summary>
/// Verifies that selecting the current format does not mark the document dirty.
/// </summary>
[TestMethod]
public void SettingSameFormatDoesNotDirty() {
MainWindowViewModel viewModel = new();
string sourcePath = Path.Combine(_tempDirectory.FullPath, "A.mp3");
viewModel.AddFiles(new[] { sourcePath });
viewModel.SavePlan(Path.Combine(_tempDirectory.FullPath, "plan.xml"));
viewModel.SelectedFormat = RenameFormat.ArtistFirst;
Assert.IsFalse(viewModel.IsDirty);
}
#endregion
#region Plan operations
/// <summary>
/// Verifies that starting a new plan clears entries, path, and dirty state.
/// </summary>
[TestMethod]
public void NewPlanClearsDocument() {
MainWindowViewModel viewModel = new();
string sourcePath = Path.Combine(_tempDirectory.FullPath, "A.mp3");
viewModel.AddFiles(new[] { sourcePath });
viewModel.SavePlan(Path.Combine(_tempDirectory.FullPath, "plan.xml"));
viewModel.NewPlan();
Assert.HasCount(0, viewModel.Entries);
Assert.AreEqual(0, viewModel.EntryCount);
Assert.IsNull(viewModel.PlanPath);
Assert.IsFalse(viewModel.IsDirty);
}
/// <summary>
/// Verifies that closing a plan clears entries, path, and dirty state.
/// </summary>
[TestMethod]
public void ClosePlanClearsDocument() {
MainWindowViewModel viewModel = new();
string sourcePath = Path.Combine(_tempDirectory.FullPath, "A.mp3");
viewModel.AddFiles(new[] { sourcePath });
viewModel.SavePlan(Path.Combine(_tempDirectory.FullPath, "plan.xml"));
viewModel.ClosePlan();
Assert.HasCount(0, viewModel.Entries);
Assert.AreEqual(0, viewModel.EntryCount);
Assert.IsNull(viewModel.PlanPath);
Assert.IsFalse(viewModel.IsDirty);
}
/// <summary>
/// Verifies that loading a plan restores its format, entries, and path.
/// </summary>
[TestMethod]
public void LoadPlanRestoresDocument() {
string planPath = Path.Combine(_tempDirectory.FullPath, "plan.xml");
SongPlan plan = new() {
Format = RenameFormat.TitleFirst,
Items = new List<PlanItem> {
new PlanItem {
Path = Path.Combine(_tempDirectory.FullPath, "A - B.mp3"),
Artist = "A",
Title = "B",
},
new PlanItem {
Path = Path.Combine(_tempDirectory.FullPath, "C.mp3"),
Title = "C",
Comment = "Live",
},
},
};
PlanXmlIO.Save(plan, planPath);
MainWindowViewModel viewModel = new();
viewModel.LoadPlan(planPath);
Assert.AreEqual(RenameFormat.TitleFirst, viewModel.SelectedFormat);
Assert.HasCount(2, viewModel.Entries);
Assert.AreEqual(2, viewModel.EntryCount);
Assert.AreEqual(planPath, viewModel.PlanPath);
Assert.IsFalse(viewModel.IsDirty);
Assert.AreEqual("B - A.mp3", viewModel.Entries[0].TargetFileName);
Assert.AreEqual("C (Live).mp3", viewModel.Entries[1].TargetFileName);
}
/// <summary>
/// Verifies saving to a path and then using the existing-path save overload.
/// </summary>
[TestMethod]
public void SavePlanWritesDocumentAndClearsDirty() {
MainWindowViewModel viewModel = new();
string sourcePath = Path.Combine(_tempDirectory.FullPath, "A - B (Live).mp3");
viewModel.AddFiles(new[] { sourcePath });
viewModel.SelectedFormat = RenameFormat.TitleFirst;
string planPath = Path.Combine(_tempDirectory.FullPath, "saved.xml");
viewModel.SavePlan(planPath);
Assert.AreEqual(planPath, viewModel.PlanPath);
Assert.IsFalse(viewModel.IsDirty);
SongPlan saved = PlanXmlIO.Load(planPath);
Assert.AreEqual(RenameFormat.TitleFirst, saved.Format);
Assert.HasCount(1, saved.Items);
Assert.AreEqual("A", saved.Items[0].Artist);
Assert.AreEqual("B", saved.Items[0].Title);
Assert.AreEqual("Live", saved.Items[0].Comment);
viewModel.Entries[0].Comment = "Changed";
Assert.IsTrue(viewModel.IsDirty);
Assert.IsTrue(viewModel.SavePlan());
Assert.IsFalse(viewModel.IsDirty);
SongPlan reloaded = PlanXmlIO.Load(planPath);
Assert.AreEqual("Changed", reloaded.Items[0].Comment);
}
#endregion
#region Validation
/// <summary>
/// Verifies that validation uses the current entries.
/// </summary>
[TestMethod]
public void ValidateRenameUsesCurrentDocument() {
string sourcePath = CreateFile("A.mp3");
MainWindowViewModel viewModel = new();
viewModel.AddFiles(new[] { sourcePath });
viewModel.Entries[0].Title = string.Empty;
RenameValidationResult result = viewModel.ValidateRename();
Assert.IsTrue(result.Issues.Any(issue => issue.Problem == RenameValidationProblem.EmptyTitle));
}
#endregion
/// <summary>
/// Creates an empty file inside the test directory.
/// </summary>
/// <param name="fileName">The relative file name, which may include a subdirectory.</param>
/// <returns>The full path of the created file.</returns>
private string CreateFile(string fileName) {
string path = Path.Combine(_tempDirectory.FullPath, fileName);
string directory = Path.GetDirectoryName(path) ?? _tempDirectory.FullPath;
Directory.CreateDirectory(directory);
File.WriteAllText(path, string.Empty);
return path;
}
}