FilePickerVM unit tests, better filter enforcement

This commit is contained in:
Justin Swanson 2019-12-14 14:11:39 -06:00
parent 7c03806e9a
commit 078b457857
18 changed files with 659 additions and 121 deletions

View File

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Wabbajack.Common
{
public class TempFile : IDisposable
{
public FileInfo File { get; private set; }
public bool DeleteAfter = true;
public TempFile(bool deleteAfter = true, bool createFolder = true)
: this(new FileInfo(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())))
{
}
public TempFile(FileInfo file, bool deleteAfter = true, bool createFolder = true)
{
this.File = file;
if (createFolder && !file.Directory.Exists)
{
file.Directory.Create();
}
this.DeleteAfter = deleteAfter;
}
public void Dispose()
{
if (DeleteAfter)
{
this.File.Delete();
}
}
}
}

View File

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Wabbajack.Common
{
public class TempFolder : IDisposable
{
public DirectoryInfo Dir { get; private set; }
public bool DeleteAfter = true;
public TempFolder(bool deleteAfter = true)
{
this.Dir = new DirectoryInfo(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()));
this.Dir.Create();
this.DeleteAfter = deleteAfter;
}
public TempFolder(DirectoryInfo dir, bool deleteAfter = true)
{
this.Dir = dir;
if (!dir.Exists)
{
this.Dir.Create();
}
this.DeleteAfter = deleteAfter;
}
public TempFolder(string addedFolderPath, bool deleteAfter = true)
: this(new DirectoryInfo(Path.Combine(Path.GetTempPath(), addedFolderPath)), deleteAfter: deleteAfter)
{
}
public void Dispose()
{
if (DeleteAfter)
{
Utils.DeleteDirectory(this.Dir.FullName);
}
}
}
}

View File

@ -75,9 +75,6 @@
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<Reference Include="Syroot.KnownFolders">
<HintPath>..\..\..\Users\tbald\.nuget\packages\syroot.windows.io.knownfolders\1.2.1\lib\net452\Syroot.KnownFolders.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
@ -103,6 +100,7 @@
<Compile Include="Enums\ModManager.cs" />
<Compile Include="ExtensionManager.cs" />
<Compile Include="Extensions\DictionaryExt.cs" />
<Compile Include="Extensions\EnumerableExt.cs" />
<Compile Include="Extensions\EnumExt.cs" />
<Compile Include="Extensions\HashHelper.cs" />
<Compile Include="Extensions\RxExt.cs" />
@ -130,6 +128,8 @@
<Compile Include="StatusUpdate.cs" />
<Compile Include="SteamHandler.cs" />
<Compile Include="Utils.cs" />
<Compile Include="Util\TempFile.cs" />
<Compile Include="Util\TempFolder.cs" />
<Compile Include="WorkQueue.cs" />
</ItemGroup>
<ItemGroup>

View File

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ReactiveUI />
</Weavers>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="ReactiveUI" minOccurs="0" maxOccurs="1" type="xs:anyType" />
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

View File

@ -1,4 +1,5 @@
using Microsoft.WindowsAPICodePack.Dialogs;
using DynamicData;
using Microsoft.WindowsAPICodePack.Dialogs;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using System;
@ -9,7 +10,7 @@ using System.Reactive.Linq;
using System.Windows.Input;
using Wabbajack.Lib;
namespace Wabbajack
namespace Wabbajack.Lib
{
public class FilePickerVM : ViewModel
{
@ -21,10 +22,10 @@ namespace Wabbajack
Folder
}
public enum ExistCheckOptions
public enum CheckOptions
{
Off,
IfNotEmpty,
IfPathNotEmpty,
On
}
@ -43,7 +44,10 @@ namespace Wabbajack
public PathTypeOptions PathType { get; set; }
[Reactive]
public ExistCheckOptions ExistCheckOption { get; set; }
public CheckOptions ExistCheckOption { get; set; }
[Reactive]
public CheckOptions FilterCheckOption { get; set; } = CheckOptions.IfPathNotEmpty;
[Reactive]
public IObservable<IErrorResponse> AdditionalError { get; set; }
@ -60,46 +64,49 @@ namespace Wabbajack
private readonly ObservableAsPropertyHelper<string> _errorTooltip;
public string ErrorTooltip => _errorTooltip.Value;
public List<CommonFileDialogFilter> Filters { get; } = new List<CommonFileDialogFilter>();
public SourceList<CommonFileDialogFilter> Filters { get; } = new SourceList<CommonFileDialogFilter>();
public FilePickerVM(object parentVM = null)
{
Parent = parentVM;
SetTargetPathCommand = ConstructTypicalPickerCommand();
// Check that file exists
var existsCheckTuple = Observable.CombineLatest(
this.WhenAny(x => x.ExistCheckOption),
this.WhenAny(x => x.PathType),
this.WhenAny(x => x.TargetPath)
// Dont want to debounce the initial value, because we know it's null
.Skip(1)
.Debounce(TimeSpan.FromMilliseconds(200))
.StartWith(default(string)),
// Dont want to debounce the initial value, because we know it's null
.Skip(1)
.Debounce(TimeSpan.FromMilliseconds(200))
.StartWith(default(string)),
resultSelector: (existsOption, type, path) => (ExistsOption: existsOption, Type: type, Path: path))
.Publish()
.StartWith((ExistsOption: ExistCheckOption, Type: PathType, Path: TargetPath))
.Replay(1)
.RefCount();
var doExistsCheck = existsCheckTuple
.Select(t =>
{
// Don't do exists type if we don't know what path type we're tracking
if (t.Type == PathTypeOptions.Off) return false;
switch (t.ExistsOption)
{
case CheckOptions.Off:
return false;
case CheckOptions.IfPathNotEmpty:
return !string.IsNullOrWhiteSpace(t.Path);
case CheckOptions.On:
return true;
default:
throw new NotImplementedException();
}
})
.Replay(1)
.RefCount();
_exists = Observable.Interval(TimeSpan.FromSeconds(3))
// Only check exists on timer if desired
.FilterSwitch(existsCheckTuple
.Select(t =>
{
// Don't do exists type if we don't know what path type we're tracking
if (t.Type == PathTypeOptions.Off) return false;
switch (t.ExistsOption)
{
case ExistCheckOptions.Off:
return false;
case ExistCheckOptions.IfNotEmpty:
return !string.IsNullOrWhiteSpace(t.Path);
case ExistCheckOptions.On:
return true;
default:
throw new NotImplementedException();
}
}))
.FilterSwitch(doExistsCheck)
.Unit()
// Also check though, when fields change
.Merge(this.WhenAny(x => x.PathType).Unit())
@ -113,14 +120,14 @@ namespace Wabbajack
{
switch (t.ExistsOption)
{
case ExistCheckOptions.IfNotEmpty:
if (string.IsNullOrWhiteSpace(t.Path)) return true;
case CheckOptions.IfPathNotEmpty:
if (string.IsNullOrWhiteSpace(t.Path)) return false;
break;
case ExistCheckOptions.On:
case CheckOptions.On:
break;
case ExistCheckOptions.Off:
case CheckOptions.Off:
default:
return true;
return false;
}
switch (t.Type)
{
@ -130,24 +137,82 @@ namespace Wabbajack
return File.Exists(t.Path);
case PathTypeOptions.Folder:
return Directory.Exists(t.Path);
case PathTypeOptions.Off:
default:
return false;
}
})
.DistinctUntilChanged()
.ObserveOn(RxApp.MainThreadScheduler)
.StartWith(false)
.ToProperty(this, nameof(Exists));
var passesFilters = Observable.CombineLatest(
this.WhenAny(x => x.TargetPath),
this.WhenAny(x => x.PathType),
this.WhenAny(x => x.FilterCheckOption),
Filters.Connect().QueryWhenChanged(),
resultSelector: (target, type, checkOption, query) =>
{
switch (type)
{
case PathTypeOptions.Either:
case PathTypeOptions.File:
break;
default:
return true;
}
if (query.Count == 0) return true;
switch (checkOption)
{
case CheckOptions.Off:
return true;
case CheckOptions.IfPathNotEmpty:
if (string.IsNullOrWhiteSpace(target)) return true;
break;
case CheckOptions.On:
break;
default:
throw new NotImplementedException();
}
try
{
var extension = Path.GetExtension(target);
if (extension == null || !extension.StartsWith(".")) return false;
extension = extension.Substring(1);
if (!query.Any(filter => filter.Extensions.Any(ext => string.Equals(ext, extension)))) return false;
}
catch (ArgumentException)
{
return false;
}
return true;
})
.StartWith(false)
.DistinctUntilChanged()
.ObserveOn(RxApp.MainThreadScheduler)
.ToProperty(this, nameof(Exists));
.StartWith(true)
.Select(passed =>
{
if (passed) return ErrorResponse.Success;
return ErrorResponse.Fail($"Path does not pass designated filters");
})
.Publish()
.RefCount();
_errorState = Observable.CombineLatest(
this.WhenAny(x => x.Exists)
this.WhenAny(x => x.Exists),
Observable.CombineLatest(
this.WhenAny(x => x.Exists),
doExistsCheck,
resultSelector: (exists, doExists) => !doExists || exists)
.Select(exists => ErrorResponse.Create(successful: exists, exists ? default(string) : "Path does not exist")),
passesFilters,
this.WhenAny(x => x.AdditionalError)
.Select(x => x ?? Observable.Return<IErrorResponse>(ErrorResponse.Success))
.Switch(),
resultSelector: (exist, err) =>
resultSelector: (exists, existCheck, filter, err) =>
{
if (exist.Failed) return exist;
if (existCheck.Failed) return existCheck;
if (filter.Failed) return filter;
return ErrorResponse.Convert(err);
})
.ToProperty(this, nameof(ErrorState));
@ -161,12 +226,15 @@ namespace Wabbajack
_errorTooltip = Observable.CombineLatest(
this.WhenAny(x => x.Exists)
.Select(exists => exists ? default(string) : "Path does not exist"),
passesFilters
.Select(x => x.Reason),
this.WhenAny(x => x.AdditionalError)
.Select(x => x ?? Observable.Return<IErrorResponse>(ErrorResponse.Success))
.Switch(),
resultSelector: (exists, err) =>
resultSelector: (exists, filters, err) =>
{
if (!string.IsNullOrWhiteSpace(exists)) return exists;
if (!string.IsNullOrWhiteSpace(filters)) return filters;
return err?.Reason;
})
.ToProperty(this, nameof(ErrorTooltip));
@ -201,7 +269,7 @@ namespace Wabbajack
Multiselect = false,
ShowPlacesList = true,
};
foreach (var filter in Filters)
foreach (var filter in Filters.Items)
{
dlg.Filters.Add(filter);
}

View File

@ -59,7 +59,6 @@
<ItemGroup>
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="Syroot.KnownFolders" />
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Design" />
@ -119,6 +118,7 @@
<Compile Include="Downloaders\GameFileSourceDownloader.cs" />
<Compile Include="Downloaders\LoversLabDownloader.cs" />
<Compile Include="Downloaders\SteamWorkshopDownloader.cs" />
<Compile Include="Extensions\ReactiveUIExt.cs" />
<Compile Include="LibCefHelpers\Init.cs" />
<Compile Include="MO2Compiler.cs" />
<Compile Include="Data.cs" />
@ -145,6 +145,7 @@
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ReportBuilder.cs" />
<Compile Include="StatusMessages\ConfirmUpdateOfExistingInstall.cs" />
<Compile Include="UI\FilePickerVM.cs" />
<Compile Include="UI\UIUtils.cs" />
<Compile Include="Validation\DTOs.cs" />
<Compile Include="Validation\ValidateModlist.cs" />
@ -210,6 +211,9 @@
<PackageReference Include="ReactiveUI">
<Version>11.0.1</Version>
</PackageReference>
<PackageReference Include="ReactiveUI.Fody">
<Version>11.0.1</Version>
</PackageReference>
<PackageReference Include="SharpCompress">
<Version>0.24.0</Version>
</PackageReference>

View File

@ -0,0 +1,344 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using DynamicData;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Wabbajack.Common;
using Wabbajack.Lib;
namespace Wabbajack.Test
{
[TestClass]
public class FilePickerTests
{
public static TempFile CreateSetFile(FilePickerVM vm)
{
var temp = new TempFile();
using (new FileStream(temp.File.FullName, FileMode.CreateNew)) { }
vm.TargetPath = temp.File.FullName;
return temp;
}
public static TempFolder CreateSetFolder(FilePickerVM vm)
{
var temp = new TempFolder();
Directory.CreateDirectory(temp.Dir.FullName);
vm.TargetPath = temp.Dir.FullName;
return temp;
}
[TestMethod]
public async Task Stock()
{
var vm = new FilePickerVM();
Assert.AreEqual(FilePickerVM.PathTypeOptions.Off, vm.PathType);
Assert.AreEqual(FilePickerVM.CheckOptions.Off, vm.ExistCheckOption);
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
}
[TestMethod]
public async Task FileNoExistsCheck_DoesNotExist()
{
var vm = new FilePickerVM();
vm.PathType = FilePickerVM.PathTypeOptions.File;
vm.ExistCheckOption = FilePickerVM.CheckOptions.Off;
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
}
[TestMethod]
public async Task FileNoExistsCheck_Exists()
{
var vm = new FilePickerVM();
using (CreateSetFile(vm))
{
vm.PathType = FilePickerVM.PathTypeOptions.File;
vm.ExistCheckOption = FilePickerVM.CheckOptions.Off;
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
}
}
[TestMethod]
public async Task ExistCheckTypeOff_DoesNotExist()
{
var vm = new FilePickerVM();
vm.PathType = FilePickerVM.PathTypeOptions.Off;
vm.ExistCheckOption = FilePickerVM.CheckOptions.On;
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
}
[TestMethod]
public async Task ExistCheckTypeOff_Exists()
{
var vm = new FilePickerVM();
using (CreateSetFile(vm))
{
vm.PathType = FilePickerVM.PathTypeOptions.Off;
vm.ExistCheckOption = FilePickerVM.CheckOptions.On;
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
}
}
[TestMethod]
public async Task FileIfNotEmptyCheck_DoesNotExist()
{
var vm = new FilePickerVM();
vm.PathType = FilePickerVM.PathTypeOptions.File;
vm.ExistCheckOption = FilePickerVM.CheckOptions.IfPathNotEmpty;
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
}
[TestMethod]
public async Task FileIfNotEmptyCheck_SetPath_DoesNotExist()
{
var vm = new FilePickerVM();
vm.PathType = FilePickerVM.PathTypeOptions.File;
vm.TargetPath = "SomePath.jpg";
vm.ExistCheckOption = FilePickerVM.CheckOptions.IfPathNotEmpty;
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsFalse(vm.ErrorState.Succeeded);
Assert.IsTrue(vm.InError);
}
[TestMethod]
public async Task FileIfNotEmptyCheck_Exists()
{
var vm = new FilePickerVM();
using (CreateSetFile(vm))
{
vm.PathType = FilePickerVM.PathTypeOptions.File;
vm.ExistCheckOption = FilePickerVM.CheckOptions.IfPathNotEmpty;
await Task.Delay(250);
Assert.IsTrue(vm.Exists);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
}
}
[TestMethod]
public async Task FileOnExistsCheck_DoesNotExist()
{
var vm = new FilePickerVM();
vm.PathType = FilePickerVM.PathTypeOptions.File;
vm.ExistCheckOption = FilePickerVM.CheckOptions.On;
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsFalse(vm.ErrorState.Succeeded);
Assert.IsTrue(vm.InError);
}
[TestMethod]
public async Task FileOnExistsCheck_Exists()
{
var vm = new FilePickerVM();
using (CreateSetFile(vm))
{
vm.PathType = FilePickerVM.PathTypeOptions.File;
vm.ExistCheckOption = FilePickerVM.CheckOptions.On;
await Task.Delay(250);
Assert.IsTrue(vm.Exists);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
}
}
[TestMethod]
public async Task FolderIfNotEmptyCheck_DoesNotExist()
{
var vm = new FilePickerVM();
vm.PathType = FilePickerVM.PathTypeOptions.Folder;
vm.ExistCheckOption = FilePickerVM.CheckOptions.IfPathNotEmpty;
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
}
[TestMethod]
public async Task FolderIfNotEmptyCheck_SetPath_DoesNotExist()
{
var vm = new FilePickerVM();
vm.PathType = FilePickerVM.PathTypeOptions.Folder;
vm.TargetPath = "SomePath.jpg";
vm.ExistCheckOption = FilePickerVM.CheckOptions.IfPathNotEmpty;
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsFalse(vm.ErrorState.Succeeded);
Assert.IsTrue(vm.InError);
}
[TestMethod]
public async Task FolderIfNotEmptyCheck_Exists()
{
var vm = new FilePickerVM();
using (CreateSetFolder(vm))
{
vm.PathType = FilePickerVM.PathTypeOptions.Folder;
vm.ExistCheckOption = FilePickerVM.CheckOptions.IfPathNotEmpty;
await Task.Delay(250);
Assert.IsTrue(vm.Exists);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
}
}
[TestMethod]
public async Task FolderOnExistsCheck_DoesNotExist()
{
var vm = new FilePickerVM();
vm.PathType = FilePickerVM.PathTypeOptions.Folder;
vm.ExistCheckOption = FilePickerVM.CheckOptions.On;
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsFalse(vm.ErrorState.Succeeded);
Assert.IsTrue(vm.InError);
}
[TestMethod]
public async Task FolderOnExistsCheck_Exists()
{
var vm = new FilePickerVM();
using (CreateSetFolder(vm))
{
vm.PathType = FilePickerVM.PathTypeOptions.Folder;
vm.ExistCheckOption = FilePickerVM.CheckOptions.On;
await Task.Delay(250);
Assert.IsTrue(vm.Exists);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
}
}
[TestMethod]
public async Task AdditionalError_Success()
{
var vm = new FilePickerVM();
vm.AdditionalError = Observable.Return<IErrorResponse>(ErrorResponse.Succeed());
await Task.Delay(250);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
}
[TestMethod]
public async Task AdditionalError_Fail()
{
var vm = new FilePickerVM();
vm.AdditionalError = Observable.Return<IErrorResponse>(ErrorResponse.Fail());
await Task.Delay(250);
Assert.IsFalse(vm.ErrorState.Succeeded);
Assert.IsTrue(vm.InError);
}
[TestMethod]
public async Task FileExistsButSetToFolder()
{
var vm = new FilePickerVM();
using (CreateSetFile(vm))
{
vm.PathType = FilePickerVM.PathTypeOptions.Folder;
vm.ExistCheckOption = FilePickerVM.CheckOptions.On;
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsFalse(vm.ErrorState.Succeeded);
Assert.IsTrue(vm.InError);
}
}
[TestMethod]
public async Task FolderExistsButSetToFile()
{
var vm = new FilePickerVM();
using (CreateSetFolder(vm))
{
vm.PathType = FilePickerVM.PathTypeOptions.File;
vm.ExistCheckOption = FilePickerVM.CheckOptions.On;
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsFalse(vm.ErrorState.Succeeded);
Assert.IsTrue(vm.InError);
}
}
[TestMethod]
public async Task FileWithFilters_Passes()
{
var vm = new FilePickerVM();
using (CreateSetFile(vm))
{
vm.PathType = FilePickerVM.PathTypeOptions.File;
vm.ExistCheckOption = FilePickerVM.CheckOptions.Off;
vm.Filters.Add(new Microsoft.WindowsAPICodePack.Dialogs.CommonFileDialogFilter("test", $"*.{Path.GetExtension(vm.TargetPath)}"));
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
}
}
[TestMethod]
public async Task FileWithFilters_ExistsButFails()
{
var vm = new FilePickerVM();
using (CreateSetFile(vm))
{
vm.PathType = FilePickerVM.PathTypeOptions.File;
vm.ExistCheckOption = FilePickerVM.CheckOptions.Off;
vm.Filters.Add(new Microsoft.WindowsAPICodePack.Dialogs.CommonFileDialogFilter("test", $"*.{Path.GetExtension(vm.TargetPath)}z"));
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsFalse(vm.ErrorState.Succeeded);
Assert.IsTrue(vm.InError);
}
}
[TestMethod]
public async Task FileWithFilters_PassesButDoesntExist()
{
var vm = new FilePickerVM();
vm.PathType = FilePickerVM.PathTypeOptions.File;
vm.ExistCheckOption = FilePickerVM.CheckOptions.Off;
vm.TargetPath = "SomePath.png";
vm.Filters.Add(new Microsoft.WindowsAPICodePack.Dialogs.CommonFileDialogFilter("test", $"*.{Path.GetExtension(vm.TargetPath)}"));
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
}
[TestMethod]
public async Task FileWithFilters_IfNotEmptyCheck_DoesntExist()
{
var vm = new FilePickerVM();
vm.PathType = FilePickerVM.PathTypeOptions.File;
vm.ExistCheckOption = FilePickerVM.CheckOptions.Off;
vm.FilterCheckOption = FilePickerVM.CheckOptions.IfPathNotEmpty;
vm.Filters.Add(new Microsoft.WindowsAPICodePack.Dialogs.CommonFileDialogFilter("test", $"*.{Path.GetExtension(vm.TargetPath)}"));
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
}
}
}

View File

@ -105,6 +105,7 @@
<Compile Include="DownloaderTests.cs" />
<Compile Include="EndToEndTests.cs" />
<Compile Include="Extensions.cs" />
<Compile Include="FilePickerTests.cs" />
<Compile Include="MiscTests.cs" />
<Compile Include="ModlistMetadataTests.cs" />
<Compile Include="TestUtils.cs" />

View File

@ -66,7 +66,7 @@ namespace Wabbajack
OutputLocation = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.ExistCheckOptions.IfNotEmpty,
ExistCheckOption = FilePickerVM.CheckOptions.IfPathNotEmpty,
PathType = FilePickerVM.PathTypeOptions.Folder,
PromptTitle = "Select the folder to place the resulting modlist.wabbajack file",
};

View File

@ -43,13 +43,13 @@ namespace Wabbajack
Parent = parent;
ModlistLocation = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.ExistCheckOptions.On,
ExistCheckOption = FilePickerVM.CheckOptions.On,
PathType = FilePickerVM.PathTypeOptions.File,
PromptTitle = "Select modlist"
};
DownloadLocation = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.ExistCheckOptions.On,
ExistCheckOption = FilePickerVM.CheckOptions.On,
PathType = FilePickerVM.PathTypeOptions.Folder,
PromptTitle = "Select download location",
};
@ -91,13 +91,44 @@ namespace Wabbajack
return ErrorResponse.Fail($"MO2 Folder could not be located from the given modlist location.{Environment.NewLine}Make sure your modlist is inside a valid MO2 distribution.");
});
// Load custom modlist settings per MO2 profile
_modlistSettings = Observable.CombineLatest(
this.WhenAny(x => x.ModlistLocation.ErrorState),
this.WhenAny(x => x.ModlistLocation.TargetPath),
resultSelector: (state, path) => (State: state, Path: path))
// A short throttle is a quick hack to make the above changes "atomic"
.Throttle(TimeSpan.FromMilliseconds(25))
.Select(u =>
{
if (u.State.Failed) return null;
var modlistSettings = _settings.ModlistSettings.TryCreate(u.Path);
return new ModlistSettingsEditorVM(modlistSettings)
{
ModListName = MOProfile
};
})
// Interject and save old while loading new
.Pairwise()
.Do(pair =>
{
pair.Previous?.Save();
pair.Current?.Init();
})
.Select(x => x.Current)
// Save to property
.ObserveOnGuiThread()
.ToProperty(this, nameof(ModlistSettings));
// Wire start command
BeginCommand = ReactiveCommand.CreateFromTask(
canExecute: Observable.CombineLatest(
this.WhenAny(x => x.ModlistLocation.InError),
this.WhenAny(x => x.DownloadLocation.InError),
parent.WhenAny(x => x.OutputLocation.InError),
resultSelector: (ml, down, output) => !ml && !down && !output)
this.WhenAny(x => x.ModlistSettings)
.Select(x => x?.InError ?? Observable.Return(false))
.Switch(),
resultSelector: (ml, down, output, modlistSettings) => !ml && !down && !output && !modlistSettings)
.ObserveOnGuiThread(),
execute: async () =>
{
@ -161,34 +192,6 @@ namespace Wabbajack
.Subscribe(_ => Unload())
.DisposeWith(CompositeDisposable);
// Load custom modlist settings per MO2 profile
_modlistSettings = Observable.CombineLatest(
this.WhenAny(x => x.ModlistLocation.ErrorState),
this.WhenAny(x => x.ModlistLocation.TargetPath),
resultSelector: (state, path) => (State: state, Path: path))
// A short throttle is a quick hack to make the above changes "atomic"
.Throttle(TimeSpan.FromMilliseconds(25))
.Select(u =>
{
if (u.State.Failed) return null;
var modlistSettings = _settings.ModlistSettings.TryCreate(u.Path);
return new ModlistSettingsEditorVM(modlistSettings)
{
ModListName = MOProfile
};
})
// Interject and save old while loading new
.Pairwise()
.Do(pair =>
{
pair.Previous?.Save();
pair.Current?.Init();
})
.Select(x => x.Current)
// Save to property
.ObserveOnGuiThread()
.ToProperty(this, nameof(ModlistSettings));
// If Mo2 folder changes and download location is empty, set it for convenience
this.WhenAny(x => x.Mo2Folder)
.DelayInitial(TimeSpan.FromMilliseconds(100))

View File

@ -1,4 +1,7 @@
using Microsoft.WindowsAPICodePack.Dialogs;
using System;
using System.Reactive.Linq;
using DynamicData;
using Microsoft.WindowsAPICodePack.Dialogs;
using ReactiveUI.Fody.Helpers;
using Wabbajack.Lib;
@ -24,27 +27,30 @@ namespace Wabbajack
[Reactive]
public string Website { get; set; }
public IObservable<bool> InError { get; }
public ModlistSettingsEditorVM(CompilationModlistSettings settings)
{
this._settings = settings;
ImagePath = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.ExistCheckOptions.IfNotEmpty,
ExistCheckOption = FilePickerVM.CheckOptions.IfPathNotEmpty,
PathType = FilePickerVM.PathTypeOptions.File,
Filters =
{
new CommonFileDialogFilter("Banner image", "*.png")
}
};
ImagePath.Filters.Add(new CommonFileDialogFilter("Banner image", "*.png"));
ReadMeText = new FilePickerVM()
{
PathType = FilePickerVM.PathTypeOptions.File,
ExistCheckOption = FilePickerVM.ExistCheckOptions.IfNotEmpty,
Filters =
{
new CommonFileDialogFilter("Text", "*.txt"),
}
ExistCheckOption = FilePickerVM.CheckOptions.IfPathNotEmpty,
};
ReadMeText.Filters.Add(new CommonFileDialogFilter("Text", "*.txt"));
InError = Observable.CombineLatest(
this.WhenAny(x => x.ImagePath.ErrorState).Select(err => err.Failed),
this.WhenAny(x => x.ReadMeText.ErrorState).Select(err => err.Failed),
resultSelector: (img, readme) => img || readme)
.Publish()
.RefCount();
}
public void Init()

View File

@ -59,30 +59,53 @@ namespace Wabbajack
Parent = parent;
GameLocation = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.ExistCheckOptions.On,
ExistCheckOption = FilePickerVM.CheckOptions.On,
PathType = FilePickerVM.PathTypeOptions.Folder,
PromptTitle = "Select Game Folder Location"
};
DownloadsLocation = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.ExistCheckOptions.On,
ExistCheckOption = FilePickerVM.CheckOptions.On,
PathType = FilePickerVM.PathTypeOptions.Folder,
PromptTitle = "Select Downloads Folder"
};
StagingLocation = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.ExistCheckOptions.On,
ExistCheckOption = FilePickerVM.CheckOptions.On,
PathType = FilePickerVM.PathTypeOptions.Folder,
PromptTitle = "Select Staging Folder"
};
// Load custom ModList settings when game type changes
_modListSettings = this.WhenAny(x => x.SelectedGame)
.Select(game =>
{
var gameSettings = _settings.ModlistSettings.TryCreate(game.Game);
return new ModlistSettingsEditorVM(gameSettings.ModlistSettings);
})
// Interject and save old while loading new
.Pairwise()
.Do(pair =>
{
var (previous, current) = pair;
previous?.Save();
current?.Init();
})
.Select(x => x.Current)
// Save to property
.ObserveOnGuiThread()
.ToProperty(this, nameof(ModlistSettings));
// Wire start command
BeginCommand = ReactiveCommand.CreateFromTask(
canExecute: Observable.CombineLatest(
this.WhenAny(x => x.GameLocation.InError),
this.WhenAny(x => x.DownloadsLocation.InError),
this.WhenAny(x => x.StagingLocation.InError),
(g, d, s) => !g && !d && !s)
this.WhenAny(x => x.ModlistSettings)
.Select(x => x?.InError ?? Observable.Return(false))
.Switch(),
(g, d, s, ml) => !g && !d && !s && !ml)
.ObserveOnGuiThread(),
execute: async () =>
{
@ -178,26 +201,6 @@ namespace Wabbajack
})
.DisposeWith(CompositeDisposable);
// Load custom ModList settings when game type changes
_modListSettings = this.WhenAny(x => x.SelectedGame)
.Select(game =>
{
var gameSettings = _settings.ModlistSettings.TryCreate(game.Game);
return new ModlistSettingsEditorVM(gameSettings.ModlistSettings);
})
// Interject and save old while loading new
.Pairwise()
.Do(pair =>
{
var (previous, current) = pair;
previous?.Save();
current?.Init();
})
.Select(x => x.Current)
// Save to property
.ObserveOnGuiThread()
.ToProperty(this, nameof(ModlistSettings));
// Find game commands
FindGameInSteamCommand = ReactiveCommand.Create(SetGameToSteamLocation);
FindGameInGogCommand = ReactiveCommand.Create(SetGameToGogLocation);

View File

@ -109,7 +109,7 @@ namespace Wabbajack
ModListLocation = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.ExistCheckOptions.On,
ExistCheckOption = FilePickerVM.CheckOptions.On,
PathType = FilePickerVM.PathTypeOptions.File,
PromptTitle = "Select a modlist to install"
};

View File

@ -41,7 +41,7 @@ namespace Wabbajack
Location = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.ExistCheckOptions.Off,
ExistCheckOption = FilePickerVM.CheckOptions.Off,
PathType = FilePickerVM.PathTypeOptions.Folder,
PromptTitle = "Select Installation Directory",
};
@ -49,7 +49,7 @@ namespace Wabbajack
.Select(x => Utils.IsDirectoryPathValid(x));
DownloadLocation = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.ExistCheckOptions.Off,
ExistCheckOption = FilePickerVM.CheckOptions.Off,
PathType = FilePickerVM.PathTypeOptions.Folder,
PromptTitle = "Select a location for MO2 downloads",
};

View File

@ -231,19 +231,16 @@
<Compile Include="Views\Common\DetailImageView.xaml.cs">
<DependentUpon>DetailImageView.xaml</DependentUpon>
</Compile>
<Compile Include="Extensions\EnumerableExt.cs" />
<Compile Include="Views\Common\TopProgressView.xaml.cs">
<DependentUpon>TopProgressView.xaml</DependentUpon>
</Compile>
<Compile Include="Settings.cs" />
<Compile Include="View Models\FilePickerVM.cs" />
<Compile Include="View Models\ModListVM.cs" />
<Compile Include="View Models\ModVM.cs" />
<Compile Include="Views\Compilers\CompilerView.xaml.cs">
<DependentUpon>CompilerView.xaml</DependentUpon>
</Compile>
<Compile Include="Converters\IsNotNullVisibilityConverter.cs" />
<Compile Include="Extensions\ReactiveUIExt.cs" />
<Compile Include="View Models\ModeSelectionVM.cs" />
<Compile Include="Views\Common\FilePicker.xaml.cs">
<DependentUpon>FilePicker.xaml</DependentUpon>