using Amphora; using System; using System.Collections.Generic; using UnitTestSharp; namespace Glass.UnitTests { public class FileDialogModelTests : TestFixture { /// /// Counts and collects what a model announces. /// public class Listener { public int Changes; public int Cancellations; public readonly List Accepted = new List(); public Listener(FileDialogModel model) { model.OnChanged += () => ++Changes; model.OnCancelled += () => ++Cancellations; model.OnAccepted += path => Accepted.Add(path); } } /// /// An InMemoryFileSystem that can be told to fail, or to have no roots. /// public class FaultyFileSystem : IFileSystem { private readonly InMemoryFileSystem inner; public bool ListFails; public bool CreateDirectoryFails; public bool NoRoots; public FaultyFileSystem(InMemoryFileSystem inner) { this.inner = inner; } public IReadOnlyList Roots => NoRoots ? Array.Empty() : inner.Roots; public string GetParent(string path) => inner.GetParent(path); public string Combine(string directory, string name) => inner.Combine(directory, name); public string GetName(string path) => inner.GetName(path); public Status ValidateFileName(string name) => inner.ValidateFileName(name); public FileKind GetKind(string path) => inner.GetKind(path); public StatusOr> List(string directory) { if (ListFails) { return AccessDenied.Fail(); } return inner.List(directory); } public Status CreateDirectory(string path) { if (CreateDirectoryFails) { return DiskFull.Fail(); } return inner.CreateDirectory(path); } } public static readonly MessageFailureKind AccessDenied = new MessageFailureKind("Glass.UnitTests.AccessDenied", "Access denied."); public static readonly MessageFailureKind DiskFull = new MessageFailureKind("Glass.UnitTests.DiskFull", "Disk full."); private static readonly FileFilter TextFiles = new FileFilter("Text files", new[] { ".txt" }); private static readonly FileFilter MarkdownFiles = new FileFilter("Markdown files", new[] { ".md" }); /// /// A root holding a.txt, b.md and a directory called dir. /// private static InMemoryFileSystem Mixed() { var returnMe = new InMemoryFileSystem(); returnMe.AddFile("/a.txt"); returnMe.AddFile("/b.md"); returnMe.AddDirectory("/dir"); return returnMe; } private static FileDialogModel OpenModel(string startDirectory = null) { return FileDialogModel.Create( InMemoryFileSystemTests.Sample(), FileDialogMode.Open, startDirectory: startDirectory).Value; } private static FileDialogModel SaveModel( IReadOnlyList filters = null, string startDirectory = null) { return FileDialogModel.Create( InMemoryFileSystemTests.Sample(), FileDialogMode.Save, filters, startDirectory).Value; } public class CreateTests : TestFixture { public void Defaults_StartInTheFirstRootWithNothingSelected() { FileDialogModel model = OpenModel(); CheckEqual("/", model.Directory); CheckEqual(3, model.Entries.Count); CheckNull(model.SelectedIndex); CheckEqual("", model.FileName); CheckFalse(model.AwaitingOverwriteConfirmation); } public void Always_KeepsTheMode() { CheckEqual(FileDialogMode.Save, SaveModel().Mode); } public void StartDirectory_IsListed() { FileDialogModel model = OpenModel("/docs"); CheckEqual("/docs", model.Directory); CheckEqual(2, model.Entries.Count); } public void MissingStartDirectory_ReturnsWhy() { StatusOr created = FileDialogModel.Create( InMemoryFileSystemTests.Sample(), FileDialogMode.Open, startDirectory: "/nowhere"); CheckEqual(FileSystemFailures.NotFound.Fail("/nowhere"), created.Status); } public void StartDirectoryThatIsAFile_ReturnsWhy() { StatusOr created = FileDialogModel.Create( InMemoryFileSystemTests.Sample(), FileDialogMode.Open, startDirectory: "/top.txt"); CheckEqual(FileSystemFailures.NotADirectory.Fail("/top.txt"), created.Status); } public void RootThatCantBeRead_ReturnsWhy() { var fileSystem = new FaultyFileSystem(InMemoryFileSystemTests.Sample()) { ListFails = true }; CheckEqual(AccessDenied.Fail(), FileDialogModel.Create(fileSystem, FileDialogMode.Open).Status); } public void FileSystemWithNoRoots_IsFineGivenAStartDirectory() { var fileSystem = new FaultyFileSystem(InMemoryFileSystemTests.Sample()) { NoRoots = true }; StatusOr created = FileDialogModel.Create(fileSystem, FileDialogMode.Open, startDirectory: "/docs"); CheckEqual("/docs", created.Value.Directory); } public void NoFilters_MeansAllFiles() { FileDialogModel model = OpenModel(); CheckEqual(1, model.Filters.Count); CheckEqual(FileFilter.AllFiles, model.Filters[0]); CheckEqual(0, model.SelectedFilterIndex); } public void EmptyFilters_MeansAllFiles() { FileDialogModel model = SaveModel(new List()); CheckEqual(FileFilter.AllFiles, model.Filters[0]); } public void GivenFilters_StartOnTheFirst() { FileDialogModel model = FileDialogModel.Create(Mixed(), FileDialogMode.Open, new[] { MarkdownFiles, TextFiles }).Value; CheckEqual(0, model.SelectedFilterIndex); CheckEqual(2, model.Entries.Count); CheckEqual("b.md", model.Entries[1].Name); } public void NullFileSystem_Throws() { CheckThrow(typeof(ArgumentNullException)); FileDialogModel.Create(null, FileDialogMode.Open); } public void FileSystemWithNoRoots_Throws() { var fileSystem = new FaultyFileSystem(InMemoryFileSystemTests.Sample()) { NoRoots = true }; CheckThrow(typeof(ArgumentException)); FileDialogModel.Create(fileSystem, FileDialogMode.Open); } } public class EntriesTests : TestFixture { public void Directories_ComeBeforeFiles() { var fileSystem = new InMemoryFileSystem(); fileSystem.AddFile("/a.txt"); fileSystem.AddDirectory("/z"); FileDialogModel model = FileDialogModel.Create(fileSystem, FileDialogMode.Open).Value; CheckEqual("z", model.Entries[0].Name); CheckEqual("a.txt", model.Entries[1].Name); } public void Names_AreOrderedIgnoringCase() { var fileSystem = new InMemoryFileSystem(); fileSystem.AddFile("/b.txt"); fileSystem.AddFile("/A.txt"); fileSystem.AddFile("/c.txt"); FileDialogModel model = FileDialogModel.Create(fileSystem, FileDialogMode.Open).Value; CheckEqual("A.txt", model.Entries[0].Name); CheckEqual("b.txt", model.Entries[1].Name); CheckEqual("c.txt", model.Entries[2].Name); } public void NamesDifferingOnlyInCase_AreOrderedOrdinally() { var fileSystem = new InMemoryFileSystem(); fileSystem.AddFile("/a.txt"); fileSystem.AddFile("/A.txt"); FileDialogModel model = FileDialogModel.Create(fileSystem, FileDialogMode.Open).Value; CheckEqual("A.txt", model.Entries[0].Name); CheckEqual("a.txt", model.Entries[1].Name); } public void Filter_HidesOtherFilesButNotDirectories() { FileDialogModel model = FileDialogModel.Create(Mixed(), FileDialogMode.Open, new[] { TextFiles }).Value; CheckEqual(2, model.Entries.Count); CheckEqual("dir", model.Entries[0].Name); CheckEqual("a.txt", model.Entries[1].Name); } } public class NavigateToTests : TestFixture { public void Always_ShowsTheDirectory() { FileDialogModel model = OpenModel(); model.NavigateTo("/docs/notes"); CheckEqual("/docs/notes", model.Directory); CheckEqual(1, model.Entries.Count); CheckEqual("todo.txt", model.Entries[0].Name); } public void Always_NotifiesOnce() { FileDialogModel model = OpenModel(); var listener = new Listener(model); model.NavigateTo("/docs"); CheckEqual(1, listener.Changes); } public void Always_ClearsTheSelection() { FileDialogModel model = OpenModel(); model.Select(0); model.NavigateTo("/docs"); CheckNull(model.SelectedIndex); } public void InOpenMode_ClearsTheFileName() { FileDialogModel model = OpenModel(); model.Select(2); model.NavigateTo("/docs"); CheckEqual("", model.FileName); } public void InSaveMode_KeepsTheFileName() { FileDialogModel model = SaveModel(); model.SetFileName("mine.txt"); model.NavigateTo("/docs"); CheckEqual("mine.txt", model.FileName); } public void ADirectoryThatCantBeRead_LeavesThingsAsTheyWereAndReturnsWhy() { FileDialogModel model = OpenModel(); var listener = new Listener(model); Status navigated = model.NavigateTo("/top.txt"); CheckEqual(FileSystemFailures.NotADirectory.Fail("/top.txt"), navigated); CheckEqual("/", model.Directory); CheckEqual(3, model.Entries.Count); CheckEqual(1, listener.Changes); } public void Success_ReturnsOk() { CheckEqual(Status.Ok, OpenModel().NavigateTo("/docs")); } public void Always_DropsAWaitingOverwriteQuestion() { FileDialogModel model = SaveModel(); model.SetFileName("top.txt"); model.Accept(); model.NavigateTo("/docs"); CheckFalse(model.AwaitingOverwriteConfirmation); } public void MalformedPath_Throws() { FileDialogModel model = OpenModel(); CheckThrow(typeof(ArgumentException)); model.NavigateTo("docs"); } } public class UpTests : TestFixture { public void InADirectory_ShowsItsParentAndReturnsOk() { FileDialogModel model = OpenModel("/docs/notes"); CheckEqual(Status.Ok, model.Up()); CheckEqual("/docs", model.Directory); } public void AtARoot_DoesNothingAndReturnsOk() { FileDialogModel model = OpenModel(); var listener = new Listener(model); CheckEqual(Status.Ok, model.Up()); CheckEqual("/", model.Directory); CheckEqual(0, listener.Changes); } public void ParentThatCantBeRead_ReturnsWhy() { var fileSystem = new FaultyFileSystem(InMemoryFileSystemTests.Sample()); FileDialogModel model = FileDialogModel.Create(fileSystem, FileDialogMode.Open, startDirectory: "/docs").Value; fileSystem.ListFails = true; CheckEqual(AccessDenied.Fail(), model.Up()); CheckEqual("/docs", model.Directory); } } public class SelectTests : TestFixture { public void AFile_SelectsItAndFillsInTheFileName() { FileDialogModel model = OpenModel(); model.Select(2); CheckEqual(2, model.SelectedIndex); CheckEqual("top.txt", model.FileName); } public void ADirectory_SelectsItAndLeavesTheFileName() { FileDialogModel model = SaveModel(); model.SetFileName("mine.txt"); model.Select(0); CheckEqual(0, model.SelectedIndex); CheckEqual("mine.txt", model.FileName); } public void Null_ClearsTheSelectionAndLeavesTheFileName() { FileDialogModel model = OpenModel(); model.Select(2); model.Select(null); CheckNull(model.SelectedIndex); CheckEqual("top.txt", model.FileName); } public void Always_NotifiesOnce() { FileDialogModel model = OpenModel(); var listener = new Listener(model); model.Select(0); CheckEqual(1, listener.Changes); } public void IndexPastTheEnd_Throws() { FileDialogModel model = OpenModel(); CheckThrow(typeof(ArgumentOutOfRangeException)); model.Select(3); } public void NegativeIndex_Throws() { FileDialogModel model = OpenModel(); CheckThrow(typeof(ArgumentOutOfRangeException)); model.Select(-1); } } public class OpenTests : TestFixture { public void ADirectory_GoesIntoItAndReturnsOk() { FileDialogModel model = OpenModel(); var listener = new Listener(model); CheckEqual(Status.Ok, model.Open(0)); CheckEqual("/docs", model.Directory); CheckEqual(1, listener.Changes); CheckEqual(0, listener.Accepted.Count); } public void ADirectoryThatCantBeRead_ReturnsWhy() { var fileSystem = new FaultyFileSystem(InMemoryFileSystemTests.Sample()); FileDialogModel model = FileDialogModel.Create(fileSystem, FileDialogMode.Open).Value; fileSystem.ListFails = true; CheckEqual(AccessDenied.Fail(), model.Open(0)); CheckEqual("/", model.Directory); } public void AFileWhenOpening_AcceptsItAndReturnsOk() { FileDialogModel model = OpenModel(); var listener = new Listener(model); CheckEqual(Status.Ok, model.Open(2)); CheckEqual(new List { "/top.txt" }, listener.Accepted); CheckEqual("top.txt", model.FileName); CheckEqual(2, model.SelectedIndex); CheckEqual(1, listener.Changes); } public void AFileWhenSaving_AsksBeforeReplacingItAndReturnsOk() { FileDialogModel model = SaveModel(); var listener = new Listener(model); CheckEqual(Status.Ok, model.Open(2)); CheckEqual(0, listener.Accepted.Count); CheckTrue(model.AwaitingOverwriteConfirmation); } public void IndexPastTheEnd_Throws() { FileDialogModel model = OpenModel(); CheckThrow(typeof(ArgumentOutOfRangeException)); model.Open(3); } public void NegativeIndex_Throws() { FileDialogModel model = OpenModel(); CheckThrow(typeof(ArgumentOutOfRangeException)); model.Open(-1); } } public class SetFileNameTests : TestFixture { public void Always_ChangesTheNameAndClearsTheSelection() { FileDialogModel model = OpenModel(); model.Select(2); model.SetFileName("other.txt"); CheckEqual("other.txt", model.FileName); CheckNull(model.SelectedIndex); } public void Always_NotifiesOnce() { FileDialogModel model = SaveModel(); var listener = new Listener(model); model.SetFileName("mine.txt"); CheckEqual(1, listener.Changes); } public void Null_Throws() { FileDialogModel model = SaveModel(); CheckThrow(typeof(ArgumentNullException)); model.SetFileName(null); } } public class SelectFilterTests : TestFixture { [IgnoreTest] private static FileDialogModel NewModel() { return FileDialogModel.Create(Mixed(), FileDialogMode.Open, new[] { FileFilter.AllFiles, TextFiles, MarkdownFiles }).Value; } public void Always_NarrowsTheEntries() { FileDialogModel model = NewModel(); model.SelectFilter(2); CheckEqual(2, model.SelectedFilterIndex); CheckEqual(2, model.Entries.Count); CheckEqual("b.md", model.Entries[1].Name); } public void ASelectedFileThatStillShows_StaysSelected() { FileDialogModel model = NewModel(); model.Select(1); model.SelectFilter(1); CheckEqual(1, model.SelectedIndex); CheckEqual("a.txt", model.Entries[1].Name); } public void ASelectedFileThatIsHidden_IsDeselected() { FileDialogModel model = NewModel(); model.Select(1); model.SelectFilter(2); CheckNull(model.SelectedIndex); } public void ASelectedDirectory_StaysSelected() { FileDialogModel model = NewModel(); model.Select(0); model.SelectFilter(1); CheckEqual(0, model.SelectedIndex); } public void NothingSelected_StaysThatWay() { FileDialogModel model = NewModel(); model.SelectFilter(1); CheckNull(model.SelectedIndex); } public void Always_NotifiesOnce() { FileDialogModel model = NewModel(); var listener = new Listener(model); model.SelectFilter(1); CheckEqual(1, listener.Changes); } public void IndexPastTheEnd_Throws() { FileDialogModel model = NewModel(); CheckThrow(typeof(ArgumentOutOfRangeException)); model.SelectFilter(3); } public void NegativeIndex_Throws() { FileDialogModel model = NewModel(); CheckThrow(typeof(ArgumentOutOfRangeException)); model.SelectFilter(-1); } } public class NewFolderTests : TestFixture { public void Always_MakesAFolderSelectsItAndReturnsOk() { FileDialogModel model = OpenModel(); CheckEqual(Status.Ok, model.NewFolder()); CheckNotNull(model.SelectedIndex); CheckEqual("New folder", model.Entries[model.SelectedIndex.Value].Name); CheckTrue(model.Entries[model.SelectedIndex.Value].IsDirectory); } public void NameTaken_UsesTheNextNumber() { FileDialogModel model = OpenModel(); model.NewFolder(); model.NewFolder(); CheckEqual("New folder (2)", model.Entries[model.SelectedIndex.Value].Name); } public void SeveralNamesTaken_UsesTheFirstFreeNumber() { var fileSystem = new InMemoryFileSystem(); fileSystem.AddDirectory("/New folder"); fileSystem.AddDirectory("/New folder (2)"); FileDialogModel model = FileDialogModel.Create(fileSystem, FileDialogMode.Open).Value; model.NewFolder(); CheckEqual("New folder (3)", model.Entries[model.SelectedIndex.Value].Name); } public void Always_NotifiesOnce() { FileDialogModel model = OpenModel(); var listener = new Listener(model); model.NewFolder(); CheckEqual(1, listener.Changes); } public void WhenTheDirectoryCantBeReadBack_ReturnsWhy() { var fileSystem = new FaultyFileSystem(InMemoryFileSystemTests.Sample()); FileDialogModel model = FileDialogModel.Create(fileSystem, FileDialogMode.Open).Value; fileSystem.ListFails = true; CheckEqual(AccessDenied.Fail(), model.NewFolder()); } public void WhenTheFileSystemRefuses_ReturnsWhyAndLeavesTheEntries() { var fileSystem = new FaultyFileSystem(InMemoryFileSystemTests.Sample()) { CreateDirectoryFails = true }; FileDialogModel model = FileDialogModel.Create(fileSystem, FileDialogMode.Open).Value; var listener = new Listener(model); CheckEqual(DiskFull.Fail(), model.NewFolder()); CheckEqual(3, model.Entries.Count); CheckEqual(1, listener.Changes); } } public class AcceptTests : TestFixture { public void Open_WithNoFileName_SaysToPickAFile() { FileDialogModel model = OpenModel(); var listener = new Listener(model); CheckEqual(FileDialogFailures.NothingPicked.Fail(FileDialogMode.Open), model.Accept()); CheckEqual(0, listener.Accepted.Count); } public void Open_ExistingFile_RaisesAcceptedWithItsPath() { FileDialogModel model = OpenModel(); var listener = new Listener(model); model.SetFileName("top.txt"); CheckEqual(Status.Ok, model.Accept()); CheckEqual(new List { "/top.txt" }, listener.Accepted); } public void Open_FileInAnotherDirectory_RaisesAcceptedWithItsPath() { FileDialogModel model = OpenModel("/docs"); var listener = new Listener(model); model.SetFileName("readme.txt"); model.Accept(); CheckEqual(new List { "/docs/readme.txt" }, listener.Accepted); } public void Open_MissingFile_SaysItDoesntExist() { FileDialogModel model = OpenModel(); var listener = new Listener(model); model.SetFileName("missing.txt"); CheckEqual(FileSystemFailures.NotFound.Fail("/missing.txt"), model.Accept()); CheckEqual(0, listener.Accepted.Count); } public void Open_NameOfADirectory_SaysItIsADirectory() { FileDialogModel model = OpenModel(); model.SetFileName("docs"); CheckEqual(FileSystemFailures.IsADirectory.Fail("/docs"), model.Accept()); } public void Open_InvalidName_GivesTheFileSystemsReason() { FileDialogModel model = OpenModel(); model.SetFileName("a/b"); CheckEqual(new InMemoryFileSystem().ValidateFileName("a/b"), model.Accept()); } public void Save_NewName_RaisesAcceptedWithItsPath() { FileDialogModel model = SaveModel(); var listener = new Listener(model); model.SetFileName("mine.txt"); model.Accept(); CheckEqual(new List { "/mine.txt" }, listener.Accepted); CheckFalse(model.AwaitingOverwriteConfirmation); } public void Save_WithNoFileName_SaysToPickAName() { FileDialogModel model = SaveModel(); CheckEqual(FileDialogFailures.NothingPicked.Fail(FileDialogMode.Save), model.Accept()); } public void Save_InvalidName_GivesTheFileSystemsReason() { FileDialogModel model = SaveModel(); model.SetFileName("a/b"); CheckEqual(new InMemoryFileSystem().ValidateFileName("a/b"), model.Accept()); } public void Save_NameOfADirectory_SaysItIsADirectory() { FileDialogModel model = SaveModel(); model.SetFileName("docs"); CheckEqual(FileSystemFailures.IsADirectory.Fail("/docs"), model.Accept()); } public void Save_ExistingFile_AsksBeforeReplacingIt() { FileDialogModel model = SaveModel(); var listener = new Listener(model); model.SetFileName("top.txt"); CheckEqual(Status.Ok, model.Accept()); CheckTrue(model.AwaitingOverwriteConfirmation); CheckEqual(0, listener.Accepted.Count); } public void Save_NameWithoutAnExtension_GetsTheSelectedFiltersFirstExtension() { FileDialogModel model = SaveModel(new[] { TextFiles }); var listener = new Listener(model); model.SetFileName("notes"); model.Accept(); CheckEqual("notes.txt", model.FileName); CheckEqual(new List { "/notes.txt" }, listener.Accepted); } public void Save_NameWithoutAnExtension_AsksAboutTheFileItBecomes() { FileDialogModel model = SaveModel(new[] { TextFiles }); model.SetFileName("top"); model.Accept(); CheckEqual("top.txt", model.FileName); CheckTrue(model.AwaitingOverwriteConfirmation); } public void Save_NameWithAnExtension_KeepsIt() { FileDialogModel model = SaveModel(new[] { TextFiles }); model.SetFileName("notes.md"); model.Accept(); CheckEqual("notes.md", model.FileName); } public void Save_NameEndingInADot_KeepsIt() { FileDialogModel model = SaveModel(new[] { TextFiles }); model.SetFileName("notes."); model.Accept(); CheckEqual("notes.", model.FileName); } public void Save_NameStartingWithADotAndNoOther_GetsTheExtension() { FileDialogModel model = SaveModel(new[] { TextFiles }); model.SetFileName(".hidden"); model.Accept(); CheckEqual(".hidden.txt", model.FileName); } public void Save_FilterWithNoExtensions_KeepsTheNameAsItIs() { FileDialogModel model = SaveModel(); model.SetFileName("notes"); model.Accept(); CheckEqual("notes", model.FileName); } public void Save_DefaultFilter_KeepsTheNameAsItIs() { FileDialogModel model = SaveModel(new[] { default(FileFilter) }); model.SetFileName("notes"); model.Accept(); CheckEqual("notes", model.FileName); } public void Always_NotifiesOnce() { FileDialogModel model = OpenModel(); var listener = new Listener(model); model.Accept(); CheckEqual(1, listener.Changes); } } public class ConfirmOverwriteTests : TestFixture { [IgnoreTest] private static FileDialogModel AwaitingModel() { FileDialogModel model = SaveModel(new[] { TextFiles }); model.SetFileName("top"); model.Accept(); return model; } public void Always_RaisesAcceptedWithThePath() { FileDialogModel model = AwaitingModel(); var listener = new Listener(model); model.ConfirmOverwrite(); CheckEqual(new List { "/top.txt" }, listener.Accepted); } public void Always_StopsAwaitingAndNotifiesOnce() { FileDialogModel model = AwaitingModel(); var listener = new Listener(model); model.ConfirmOverwrite(); CheckFalse(model.AwaitingOverwriteConfirmation); CheckEqual(1, listener.Changes); } public void NothingWaiting_Throws() { FileDialogModel model = SaveModel(); CheckThrow(typeof(InvalidOperationException)); model.ConfirmOverwrite(); } } public class CancelOverwriteTests : TestFixture { [IgnoreTest] private static FileDialogModel AwaitingModel() { FileDialogModel model = SaveModel(); model.SetFileName("top.txt"); model.Accept(); return model; } public void Waiting_StopsAwaitingWithoutAcceptingAndNotifiesOnce() { FileDialogModel model = AwaitingModel(); var listener = new Listener(model); model.CancelOverwrite(); CheckFalse(model.AwaitingOverwriteConfirmation); CheckEqual(0, listener.Accepted.Count); CheckEqual(1, listener.Changes); } public void NothingWaiting_DoesNothing() { FileDialogModel model = SaveModel(); var listener = new Listener(model); model.CancelOverwrite(); CheckEqual(0, listener.Changes); } } public class CancelTests : TestFixture { public void Always_RaisesCancelled() { FileDialogModel model = OpenModel(); var listener = new Listener(model); model.Cancel(); CheckEqual(1, listener.Cancellations); CheckEqual(0, listener.Accepted.Count); } public void Always_NotifiesOnceAndDropsAWaitingOverwriteQuestion() { FileDialogModel model = SaveModel(); model.SetFileName("top.txt"); model.Accept(); var listener = new Listener(model); model.Cancel(); CheckFalse(model.AwaitingOverwriteConfirmation); CheckEqual(1, listener.Changes); } } } }