feat: implement main window view models
This commit is contained in:
1 parent
34a588864f
commit
996c0a9515
7 files changed
+1121
-6
No files matched your search
@@ -0,0 +1,317 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
using SongTidy.Models;
|
||||
using SongTidy.Services;
|
||||
using SongTidy.ViewModels;
|
||||
|
||||
namespace SongTidy.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Contains tests for <see cref="MainWindowViewModel"/>.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public sealed class MainWindowViewModelTests {
|
||||
private string _tempDirectory = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a unique temporary directory for the current test.
|
||||
/// </summary>
|
||||
[TestInitialize]
|
||||
public void Initialize() {
|
||||
_tempDirectory = Path.Combine(Path.GetTempPath(), $"SongTidy ViewModel {Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_tempDirectory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the temporary directory created for the current test.
|
||||
/// </summary>
|
||||
[TestCleanup]
|
||||
public void Cleanup() {
|
||||
Directory.Delete(_tempDirectory, recursive: true);
|
||||
}
|
||||
|
||||
#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, "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, "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, "a.mp3"),
|
||||
Path.Combine(_tempDirectory, "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, "A.mp3");
|
||||
viewModel.AddFiles(new[] { sourcePath });
|
||||
viewModel.SavePlan(Path.Combine(_tempDirectory, "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);
|
||||
|
||||
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, "A.mp3");
|
||||
viewModel.AddFiles(new[] { sourcePath });
|
||||
viewModel.SavePlan(Path.Combine(_tempDirectory, "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, "A - B (Live).mp3");
|
||||
viewModel.AddFiles(new[] { sourcePath });
|
||||
viewModel.SavePlan(Path.Combine(_tempDirectory, "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, "A.mp3");
|
||||
viewModel.AddFiles(new[] { sourcePath });
|
||||
viewModel.SavePlan(Path.Combine(_tempDirectory, "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, "A.mp3");
|
||||
viewModel.AddFiles(new[] { sourcePath });
|
||||
viewModel.SavePlan(Path.Combine(_tempDirectory, "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, "A.mp3");
|
||||
viewModel.AddFiles(new[] { sourcePath });
|
||||
viewModel.SavePlan(Path.Combine(_tempDirectory, "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, "plan.xml");
|
||||
SongPlan plan = new() {
|
||||
Format = RenameFormat.TitleFirst,
|
||||
Items = new List<PlanItem> {
|
||||
new PlanItem {
|
||||
Path = Path.Combine(_tempDirectory, "A - B.mp3"),
|
||||
Artist = "A",
|
||||
Title = "B",
|
||||
},
|
||||
new PlanItem {
|
||||
Path = Path.Combine(_tempDirectory, "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, "A - B (Live).mp3");
|
||||
viewModel.AddFiles(new[] { sourcePath });
|
||||
viewModel.SelectedFormat = RenameFormat.TitleFirst;
|
||||
string planPath = Path.Combine(_tempDirectory, "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, fileName);
|
||||
string directory = Path.GetDirectoryName(path) ?? _tempDirectory;
|
||||
|
||||
Directory.CreateDirectory(directory);
|
||||
File.WriteAllText(path, string.Empty);
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user