- Updated `IMediaSourceCatalog` to support user-added media sources, allowing dynamic editing and management of sources. - Removed the `UrlListSource` class as its functionality is now integrated into the new catalog structure. - Enhanced `CollectOptions` to default `RequireProxy` to true, ensuring stricter handling of proxy requirements. - Improved error handling in `ParseError` to include a `Subject` field for better context on failures. - Adjusted dependency injection to reflect changes in media source management, removing old source registrations. - Introduced background proxy checks to ensure a more robust proxy pool management during collection processes. These changes streamline the media collection process and improve the overall user experience by providing clearer error reporting and more flexible source management.
188 lines
6.2 KiB
C#
188 lines
6.2 KiB
C#
using AvParser.Core.Collecting;
|
|
using AvParser.UI.Tests.Fakes;
|
|
using AvParser.UI.ViewModels;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using ReactiveUI.Primitives;
|
|
using ReactiveUI.Primitives.Concurrency;
|
|
|
|
namespace AvParser.UI.Tests;
|
|
|
|
public class GalleryViewModelTests
|
|
{
|
|
private static StoredMedia Media(string url, MediaKind kind = MediaKind.Png, bool animated = false) =>
|
|
new(
|
|
Guid.NewGuid().ToString("N") + Guid.NewGuid().ToString("N"),
|
|
kind,
|
|
MediaKinds.ExtensionFor(kind),
|
|
4096,
|
|
"url-list",
|
|
url,
|
|
DateTimeOffset.UtcNow
|
|
)
|
|
{
|
|
Width = 800,
|
|
Height = 600,
|
|
IsAnimated = animated,
|
|
};
|
|
|
|
private static (GalleryViewModel Page, FakeMediaStore Store, FakeThumbnailCache Thumbnails) Build(
|
|
params StoredMedia[] items
|
|
)
|
|
{
|
|
var store = new FakeMediaStore();
|
|
store.Browse.AddRange(items);
|
|
store.SourceIds.Add("url-list");
|
|
|
|
var thumbnails = new FakeThumbnailCache();
|
|
var page = new GalleryViewModel(
|
|
store,
|
|
thumbnails,
|
|
NullLogger<GalleryViewModel>.Instance,
|
|
ImmediateSequencer.Instance
|
|
);
|
|
|
|
return (page, store, thumbnails);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task What_the_store_holds_becomes_tiles()
|
|
{
|
|
var (page, _, _) = Build(Media("https://a.test/1.png"), Media("https://a.test/2.gif", MediaKind.Gif));
|
|
|
|
await page.LoadAsync(0, TestContext.Current.CancellationToken);
|
|
|
|
page.Items.Count.ShouldBe(2);
|
|
page.TotalCount.ShouldBe(2);
|
|
page.StatusMessage.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Every_tile_asks_for_a_thumbnail()
|
|
{
|
|
var (page, _, thumbnails) = Build(Media("https://a.test/1.png"), Media("https://a.test/2.png"));
|
|
|
|
await page.LoadAsync(0, TestContext.Current.CancellationToken);
|
|
|
|
// By distinct hash rather than call count: the page also loads once on construction, and
|
|
// what matters is that every tile got asked for, at the tile's own width.
|
|
thumbnails.Requested.Select(r => r.Sha256).Distinct().Count().ShouldBe(2);
|
|
thumbnails.Requested.ShouldAllBe(r => r.Width == GalleryItemViewModel.ThumbnailWidth);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Content_nothing_can_decode_is_flagged_rather_than_left_blank()
|
|
{
|
|
// The fake decodes nothing, which is exactly what the real cache does for a video.
|
|
var (page, _, _) = Build(Media("https://a.test/clip.mp4", MediaKind.Mp4));
|
|
|
|
await page.LoadAsync(0, TestContext.Current.CancellationToken);
|
|
|
|
var tile = page.Items.ShouldHaveSingleItem();
|
|
tile.Thumbnail.ShouldBeNull();
|
|
tile.HasNoPreview.ShouldBeTrue();
|
|
tile.KindText.ShouldBe("MP4");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task An_empty_store_says_so_instead_of_showing_a_blank_page()
|
|
{
|
|
var (page, _, _) = Build();
|
|
|
|
await page.LoadAsync(0, TestContext.Current.CancellationToken);
|
|
|
|
page.Items.ShouldBeEmpty();
|
|
page.StatusMessage.ShouldNotBeNull().ShouldContain("Nothing here yet");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task The_filters_reach_the_query()
|
|
{
|
|
var (page, store, _) = Build(Media("https://a.test/1.png"));
|
|
|
|
page.SelectedSourceId = "url-list";
|
|
page.AnimatedOnly = true;
|
|
page.SearchText = "kitten";
|
|
page.SelectedKinds = page.KindFilters.Single(option => option.Value == MediaKindFilter.Images);
|
|
|
|
await page.LoadAsync(0, TestContext.Current.CancellationToken);
|
|
|
|
var query = store.LastBrowse.ShouldNotBeNull();
|
|
query.SourceId.ShouldBe("url-list");
|
|
query.AnimatedOnly.ShouldBeTrue();
|
|
query.Search.ShouldBe("kitten");
|
|
query.Kinds.ShouldBe(MediaKindFilter.Images);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task An_unset_source_filter_means_all_of_them()
|
|
{
|
|
var (page, store, _) = Build(Media("https://a.test/1.png"));
|
|
|
|
page.SelectedSourceId = " ";
|
|
await page.LoadAsync(0, TestContext.Current.CancellationToken);
|
|
|
|
store.LastBrowse.ShouldNotBeNull().SourceId.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Paging_moves_the_offset_and_stops_at_the_ends()
|
|
{
|
|
var (page, store, _) = Build(Media("https://a.test/1.png"));
|
|
store.TotalOverride = 250;
|
|
|
|
await page.LoadAsync(0, TestContext.Current.CancellationToken);
|
|
page.PageText.ShouldContain("1");
|
|
|
|
await page.LoadAsync(1, TestContext.Current.CancellationToken);
|
|
store.LastBrowse!.Skip.ShouldBe(120);
|
|
|
|
// A negative page is a clamp, not a crash.
|
|
await page.LoadAsync(-3, TestContext.Current.CancellationToken);
|
|
page.PageIndex.ShouldBe(0);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Opening_a_tile_opens_the_viewer_and_closing_it_clears_the_selection()
|
|
{
|
|
var (page, _, _) = Build(Media("https://a.test/1.png"));
|
|
await page.LoadAsync(0, TestContext.Current.CancellationToken);
|
|
|
|
page.IsViewerOpen.ShouldBeFalse();
|
|
|
|
page.Selected = page.Items[0];
|
|
page.IsViewerOpen.ShouldBeTrue();
|
|
|
|
await page.CloseViewerCommand.Execute().ToTask(TestContext.Current.CancellationToken);
|
|
|
|
page.Selected.ShouldBeNull();
|
|
page.IsViewerOpen.ShouldBeFalse();
|
|
page.Preview.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_store_that_throws_reports_it_rather_than_taking_the_page_down()
|
|
{
|
|
var (page, store, _) = Build(Media("https://a.test/1.png"));
|
|
store.BrowseThrows = true;
|
|
|
|
await page.LoadAsync(0, TestContext.Current.CancellationToken);
|
|
|
|
page.StatusMessage.ShouldNotBeNull().ShouldContain("Could not read the store");
|
|
page.IsLoading.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Disposing_twice_is_safe()
|
|
{
|
|
// Not hypothetical: every page is registered under its own type and under PageViewModel, so
|
|
// the container disposes it once per registration. The second call used to cancel an
|
|
// already-disposed token source and bring the process down on every exit.
|
|
var (page, _, _) = Build(Media("https://a.test/1.png"));
|
|
await page.LoadAsync(0, TestContext.Current.CancellationToken);
|
|
|
|
page.Dispose();
|
|
|
|
Should.NotThrow(page.Dispose);
|
|
}
|
|
}
|