OSDN Git Service

Merge pull request #307 from opentween/detect-rate-limits
[opentween/open-tween.git] / OpenTween.Tests / TweetThumbnailTest.cs
index 4654203..635f8e8 100644 (file)
 
 using System;
 using System.Collections.Generic;
-using System.IO;
 using System.Linq;
 using System.Net.Http;
-using System.Reflection;
-using System.Runtime.InteropServices;
-using System.Text.RegularExpressions;
+using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
-using System.Windows.Forms;
 using Moq;
 using OpenTween.Models;
 using OpenTween.Thumbnail;
 using OpenTween.Thumbnail.Services;
 using Xunit;
-using Xunit.Extensions;
 
 namespace OpenTween
 {
     public class TweetThumbnailTest
     {
-        class TestThumbnailService : IThumbnailService
+        private ThumbnailGenerator CreateThumbnailGenerator()
         {
-            private readonly Regex regex;
-            private readonly string replaceUrl;
-            private readonly string replaceTooltip;
+            var imgAzyobuziNet = new ImgAzyobuziNet(autoupdate: false);
+            var thumbGenerator = new ThumbnailGenerator(imgAzyobuziNet);
+            thumbGenerator.Services.Clear();
+            return thumbGenerator;
+        }
 
-            public TestThumbnailService(string pattern, string replaceUrl, string replaceTooltip)
-            {
-                this.regex = new Regex(pattern);
-                this.replaceUrl = replaceUrl;
-                this.replaceTooltip = replaceTooltip;
-            }
+        private IThumbnailService CreateThumbnailService()
+        {
+            var thumbnailServiceMock = new Mock<IThumbnailService>();
+            thumbnailServiceMock
+                .Setup(
+                    x => x.GetThumbnailInfoAsync("http://example.com/abcd", It.IsAny<PostClass>(), It.IsAny<CancellationToken>())
+                )
+                .ReturnsAsync(new MockThumbnailInfo
+                {
+                    MediaPageUrl = "http://example.com/abcd",
+                    ThumbnailImageUrl = "http://img.example.com/abcd.png",
+                });
+            thumbnailServiceMock
+                .Setup(
+                    x => x.GetThumbnailInfoAsync("http://example.com/efgh", It.IsAny<PostClass>(), It.IsAny<CancellationToken>())
+                )
+                .ReturnsAsync(new MockThumbnailInfo
+                {
+                    MediaPageUrl = "http://example.com/efgh",
+                    ThumbnailImageUrl = "http://img.example.com/efgh.png",
+                });
+            return thumbnailServiceMock.Object;
+        }
 
-            public override async Task<ThumbnailInfo> GetThumbnailInfoAsync(string url, PostClass post, CancellationToken token)
-            {
-                var match = this.regex.Match(url);
+        [Fact]
+        public async Task PrepareThumbnails_Test()
+        {
+            var thumbnailGenerator = this.CreateThumbnailGenerator();
+            thumbnailGenerator.Services.Add(this.CreateThumbnailService());
 
-                if (!match.Success) return null;
+            var tweetThumbnail = new TweetThumbnail();
+            tweetThumbnail.Initialize(thumbnailGenerator);
 
-                if (url.StartsWith("http://slow.example.com/", StringComparison.Ordinal))
-                    await Task.Delay(1000, token).ConfigureAwait(false);
+            var post = new PostClass
+            {
+                StatusId = new TwitterStatusId("100"),
+                Media = new() { new("http://example.com/abcd") },
+            };
 
-                return new MockThumbnailInfo
-                {
-                    MediaPageUrl = url,
-                    ThumbnailImageUrl = match.Result(this.replaceUrl),
-                    TooltipText = this.replaceTooltip != null ? match.Result(this.replaceTooltip) : null,
-                };
-            }
+            await tweetThumbnail.PrepareThumbnails(post, CancellationToken.None);
 
-            class MockThumbnailInfo : ThumbnailInfo
-            {
-                public override Task<MemoryImage> LoadThumbnailImageAsync(HttpClient http, CancellationToken cancellationToken)
-                {
-                    return Task.FromResult(TestUtils.CreateDummyImage());
-                }
-            }
+            Assert.True(tweetThumbnail.ThumbnailAvailable);
+            Assert.Single(tweetThumbnail.Thumbnails);
+            Assert.Equal(0, tweetThumbnail.SelectedIndex);
+            Assert.Equal("http://example.com/abcd", tweetThumbnail.CurrentThumbnail.MediaPageUrl);
+            Assert.Equal("http://img.example.com/abcd.png", tweetThumbnail.CurrentThumbnail.ThumbnailImageUrl);
         }
 
-        public TweetThumbnailTest()
+        [Fact]
+        public async Task PrepareThumbnails_NoThumbnailTest()
         {
-            this.ThumbnailGeneratorSetup();
-            this.MyCommonSetup();
-        }
+            var thumbnailGenerator = this.CreateThumbnailGenerator();
+            thumbnailGenerator.Services.Add(this.CreateThumbnailService());
 
-        private void ThumbnailGeneratorSetup()
-        {
-            ThumbnailGenerator.Services.Clear();
-            ThumbnailGenerator.Services.AddRange(new[]
+            var tweetThumbnail = new TweetThumbnail();
+            tweetThumbnail.Initialize(thumbnailGenerator);
+
+            var post = new PostClass
             {
-                new TestThumbnailService(@"^https?://foo.example.com/(.+)$", @"http://img.example.com/${1}.png", null),
-                new TestThumbnailService(@"^https?://bar.example.com/(.+)$", @"http://img.example.com/${1}.png", @"${1}"),
-                new TestThumbnailService(@"^https?://slow.example.com/(.+)$", @"http://img.example.com/${1}.png", null),
-            });
-        }
+                StatusId = new TwitterStatusId("100"),
+                Media = new() { new("http://hoge.example.com/") },
+            };
 
-        private void MyCommonSetup()
-        {
-            var mockAssembly = new Mock<_Assembly>();
-            mockAssembly.Setup(m => m.GetName()).Returns(new AssemblyName("OpenTween"));
+            await tweetThumbnail.PrepareThumbnails(post, CancellationToken.None);
 
-            MyCommon.EntryAssembly = mockAssembly.Object;
+            Assert.False(tweetThumbnail.ThumbnailAvailable);
+            Assert.Throws<InvalidOperationException>(() => tweetThumbnail.Thumbnails);
         }
 
         [Fact]
-        public void CreatePictureBoxTest()
+        public async Task PrepareThumbnails_CancelTest()
         {
-            using (var thumbBox = new TweetThumbnail())
+            var thumbnailServiceMock = new Mock<IThumbnailService>();
+            thumbnailServiceMock
+                .Setup(
+                    x => x.GetThumbnailInfoAsync("http://slow.example.com/abcd", It.IsAny<PostClass>(), It.IsAny<CancellationToken>())
+                )
+                .Returns(async () =>
+                {
+                    await Task.Delay(200);
+                    return new MockThumbnailInfo();
+                });
+
+            var thumbnailGenerator = this.CreateThumbnailGenerator();
+            thumbnailGenerator.Services.Add(thumbnailServiceMock.Object);
+
+            var tweetThumbnail = new TweetThumbnail();
+            tweetThumbnail.Initialize(thumbnailGenerator);
+
+            var post = new PostClass
             {
-                var method = typeof(TweetThumbnail).GetMethod("CreatePictureBox", BindingFlags.Instance | BindingFlags.NonPublic);
-                var picbox = method.Invoke(thumbBox, new[] { "pictureBox1" }) as PictureBox;
+                StatusId = new TwitterStatusId("100"),
+                Media = new() { new("http://slow.example.com/abcd") },
+            };
 
-                Assert.NotNull(picbox);
-                Assert.Equal("pictureBox1", picbox.Name);
-                Assert.Equal(PictureBoxSizeMode.Zoom, picbox.SizeMode);
-                Assert.False(picbox.WaitOnLoad);
-                Assert.Equal(DockStyle.Fill, picbox.Dock);
+            using var tokenSource = new CancellationTokenSource();
+            var task = tweetThumbnail.PrepareThumbnails(post, tokenSource.Token);
+            tokenSource.Cancel();
 
-                picbox.Dispose();
-            }
+            await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await task);
+            Assert.True(task.IsCanceled);
         }
 
         [Fact]
-        public async Task CancelAsyncTest()
+        public async Task LoadSelectedThumbnail_Test()
         {
+            using var image = TestUtils.CreateDummyImage();
+            var thumbnailInfoMock = new Mock<ThumbnailInfo>() { CallBase = true };
+            thumbnailInfoMock
+                .Setup(
+                    x => x.LoadThumbnailImageAsync(It.IsAny<HttpClient>(), It.IsAny<CancellationToken>())
+                )
+                .ReturnsAsync(image);
+
+            var thumbnailServiceMock = new Mock<IThumbnailService>();
+            thumbnailServiceMock
+                .Setup(
+                    x => x.GetThumbnailInfoAsync("http://example.com/abcd", It.IsAny<PostClass>(), It.IsAny<CancellationToken>())
+                )
+                .ReturnsAsync(thumbnailInfoMock.Object);
+
+            var thumbnailGenerator = this.CreateThumbnailGenerator();
+            thumbnailGenerator.Services.Add(thumbnailServiceMock.Object);
+
+            var tweetThumbnail = new TweetThumbnail();
+            tweetThumbnail.Initialize(thumbnailGenerator);
+
             var post = new PostClass
             {
-                TextFromApi = "てすと http://slow.example.com/abcd",
-                Media = new List<MediaInfo>
-                {
-                    new MediaInfo("http://slow.example.com/abcd"),
-                },
+                StatusId = new TwitterStatusId("100"),
+                Media = new() { new("http://example.com/abcd") },
             };
 
-            using (var thumbbox = new TweetThumbnail())
-            using (var tokenSource = new CancellationTokenSource())
-            {
-                SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
-                var task = thumbbox.ShowThumbnailAsync(post, tokenSource.Token);
-
-                tokenSource.Cancel();
+            await tweetThumbnail.PrepareThumbnails(post, CancellationToken.None);
 
-                await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await task);
-                Assert.True(task.IsCanceled);
-            }
+            var loadedImage = await tweetThumbnail.LoadSelectedThumbnail();
+            Assert.Same(image, loadedImage);
         }
 
-        [Theory]
-        [InlineData(0)]
-        [InlineData(1)]
-        [InlineData(2)]
-        public void SetThumbnailCountTest(int count)
+        [Fact]
+        public async Task LoadSelectedThumbnail_RequestCollapsingTest()
         {
-            using (var thumbbox = new TweetThumbnail())
+            var tsc = new TaskCompletionSource<MemoryImage>();
+            var thumbnailInfoMock = new Mock<ThumbnailInfo>() { CallBase = true };
+            thumbnailInfoMock
+                .Setup(
+                    x => x.LoadThumbnailImageAsync(It.IsAny<HttpClient>(), It.IsAny<CancellationToken>())
+                )
+                .Returns(tsc.Task);
+
+            var thumbnailServiceMock = new Mock<IThumbnailService>();
+            thumbnailServiceMock
+                .Setup(
+                    x => x.GetThumbnailInfoAsync("http://example.com/abcd", It.IsAny<PostClass>(), It.IsAny<CancellationToken>())
+                )
+                .ReturnsAsync(thumbnailInfoMock.Object);
+
+            var thumbnailGenerator = this.CreateThumbnailGenerator();
+            thumbnailGenerator.Services.Add(thumbnailServiceMock.Object);
+
+            var tweetThumbnail = new TweetThumbnail();
+            tweetThumbnail.Initialize(thumbnailGenerator);
+
+            var post = new PostClass
             {
-                var method = typeof(TweetThumbnail).GetMethod("SetThumbnailCount", BindingFlags.Instance | BindingFlags.NonPublic);
-                method.Invoke(thumbbox, new[] { (object)count });
+                StatusId = new TwitterStatusId("100"),
+                Media = new() { new("http://example.com/abcd") },
+            };
 
-                Assert.Equal(count, thumbbox.pictureBox.Count);
+            await tweetThumbnail.PrepareThumbnails(post, CancellationToken.None);
 
-                var num = 0;
-                foreach (var picbox in thumbbox.pictureBox)
-                {
-                    Assert.Equal("pictureBox" + num, picbox.Name);
-                    num++;
-                }
+            var loadTask1 = tweetThumbnail.LoadSelectedThumbnail();
+            await Task.Delay(50);
 
-                Assert.Equal(thumbbox.pictureBox, thumbbox.panelPictureBox.Controls.Cast<OTPictureBox>());
+            // 画像のロードが完了しない間に再度 LoadSelectedThumbnail が呼ばれた場合は同一の Task を返す
+            // (複数回呼ばれても画像のリクエストは一本にまとめられる)
+            var loadTask2 = tweetThumbnail.LoadSelectedThumbnail();
+            Assert.Same(loadTask1, loadTask2);
 
-                Assert.Equal(0, thumbbox.scrollBar.Minimum);
+            using var image = TestUtils.CreateDummyImage();
+            tsc.SetResult(image);
 
-                if (count == 0)
-                    Assert.Equal(0, thumbbox.scrollBar.Maximum);
-                else
-                    Assert.Equal(count - 1, thumbbox.scrollBar.Maximum);
-            }
+            Assert.Same(image, await loadTask1);
         }
 
         [Fact]
-        public async Task ShowThumbnailAsyncTest()
+        public async Task SelectedIndex_Test()
         {
+            var thumbnailGenerator = this.CreateThumbnailGenerator();
+            thumbnailGenerator.Services.Add(this.CreateThumbnailService());
+
+            var tweetThumbnail = new TweetThumbnail();
+            tweetThumbnail.Initialize(thumbnailGenerator);
+
             var post = new PostClass
             {
-                TextFromApi = "てすと http://foo.example.com/abcd",
-                Media = new List<MediaInfo>
-                {
-                    new MediaInfo("http://foo.example.com/abcd"),
-                },
+                StatusId = new TwitterStatusId("100"),
+                Media = new() { new("http://example.com/abcd"), new("http://example.com/efgh") },
             };
 
-            using (var thumbbox = new TweetThumbnail())
-            {
-                SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
-                await thumbbox.ShowThumbnailAsync(post);
+            await tweetThumbnail.PrepareThumbnails(post, CancellationToken.None);
 
-                Assert.Equal(0, thumbbox.scrollBar.Maximum);
-                Assert.False(thumbbox.scrollBar.Enabled);
+            Assert.Equal(2, tweetThumbnail.Thumbnails.Length);
+            Assert.Equal(0, tweetThumbnail.SelectedIndex);
+            Assert.Equal("http://example.com/abcd", tweetThumbnail.CurrentThumbnail.MediaPageUrl);
 
-                Assert.Single(thumbbox.pictureBox);
-                Assert.NotNull(thumbbox.pictureBox[0].Image);
+            tweetThumbnail.SelectedIndex = 1;
+            Assert.Equal(1, tweetThumbnail.SelectedIndex);
+            Assert.Equal("http://example.com/efgh", tweetThumbnail.CurrentThumbnail.MediaPageUrl);
 
-                Assert.IsAssignableFrom<ThumbnailInfo>(thumbbox.pictureBox[0].Tag);
-                var thumbinfo = (ThumbnailInfo)thumbbox.pictureBox[0].Tag;
+            Assert.Throws<ArgumentOutOfRangeException>(() => tweetThumbnail.SelectedIndex = -1);
+            Assert.Throws<ArgumentOutOfRangeException>(() => tweetThumbnail.SelectedIndex = 2);
+        }
+
+        [Fact]
+        public void SelectedIndex_NoThumbnailTest()
+        {
+            var thumbnailGenerator = this.CreateThumbnailGenerator();
+            var tweetThumbnail = new TweetThumbnail();
+            tweetThumbnail.Initialize(thumbnailGenerator);
 
-                Assert.Equal("http://foo.example.com/abcd", thumbinfo.MediaPageUrl);
-                Assert.Equal("http://img.example.com/abcd.png", thumbinfo.ThumbnailImageUrl);
+            Assert.False(tweetThumbnail.ThumbnailAvailable);
 
-                Assert.Equal("", thumbbox.toolTip.GetToolTip(thumbbox.pictureBox[0]));
-            }
+            // サムネイルが無い場合に 0 以外の値をセットすると例外を発生させる
+            tweetThumbnail.SelectedIndex = 0;
+            Assert.Throws<ArgumentOutOfRangeException>(() => tweetThumbnail.SelectedIndex = -1);
+            Assert.Throws<ArgumentOutOfRangeException>(() => tweetThumbnail.SelectedIndex = 1);
         }
 
         [Fact]
-        public async Task ShowThumbnailAsyncTest2()
+        public async Task GetImageSearchUriGoogle_Test()
         {
+            var thumbnailGenerator = this.CreateThumbnailGenerator();
+            thumbnailGenerator.Services.Add(this.CreateThumbnailService());
+
+            var tweetThumbnail = new TweetThumbnail();
+            tweetThumbnail.Initialize(thumbnailGenerator);
+
             var post = new PostClass
             {
-                TextFromApi = "てすと http://foo.example.com/abcd http://bar.example.com/efgh",
-                Media = new List<MediaInfo>
-                {
-                    new MediaInfo("http://foo.example.com/abcd"),
-                    new MediaInfo("http://bar.example.com/efgh"),
-                },
+                StatusId = new TwitterStatusId("100"),
+                Media = new() { new("http://example.com/abcd") },
             };
 
-            using (var thumbbox = new TweetThumbnail())
-            {
-                SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
-                await thumbbox.ShowThumbnailAsync(post);
-
-                Assert.Equal(1, thumbbox.scrollBar.Maximum);
-                Assert.True(thumbbox.scrollBar.Enabled);
+            await tweetThumbnail.PrepareThumbnails(post, CancellationToken.None);
 
-                Assert.Equal(2, thumbbox.pictureBox.Count);
-                Assert.NotNull(thumbbox.pictureBox[0].Image);
-                Assert.NotNull(thumbbox.pictureBox[1].Image);
+            Assert.Equal("http://img.example.com/abcd.png", tweetThumbnail.CurrentThumbnail.ThumbnailImageUrl);
+            Assert.Equal(
+                new(@"https://lens.google.com/uploadbyurl?url=http%3A%2F%2Fimg.example.com%2Fabcd.png"),
+                tweetThumbnail.GetImageSearchUriGoogle()
+            );
+        }
 
-                Assert.IsAssignableFrom<ThumbnailInfo>(thumbbox.pictureBox[0].Tag);
-                var thumbinfo = (ThumbnailInfo)thumbbox.pictureBox[0].Tag;
+        [Fact]
+        public async Task GetImageSearchUriSauceNao_Test()
+        {
+            var thumbnailGenerator = this.CreateThumbnailGenerator();
+            thumbnailGenerator.Services.Add(this.CreateThumbnailService());
 
-                Assert.Equal("http://foo.example.com/abcd", thumbinfo.MediaPageUrl);
-                Assert.Equal("http://img.example.com/abcd.png", thumbinfo.ThumbnailImageUrl);
+            var tweetThumbnail = new TweetThumbnail();
+            tweetThumbnail.Initialize(thumbnailGenerator);
 
-                Assert.IsAssignableFrom<ThumbnailInfo>(thumbbox.pictureBox[1].Tag);
-                thumbinfo = (ThumbnailInfo)thumbbox.pictureBox[1].Tag;
+            var post = new PostClass
+            {
+                StatusId = new TwitterStatusId("100"),
+                Media = new() { new("http://example.com/abcd") },
+            };
 
-                Assert.Equal("http://bar.example.com/efgh", thumbinfo.MediaPageUrl);
-                Assert.Equal("http://img.example.com/efgh.png", thumbinfo.ThumbnailImageUrl);
+            await tweetThumbnail.PrepareThumbnails(post, CancellationToken.None);
 
-                Assert.Equal("", thumbbox.toolTip.GetToolTip(thumbbox.pictureBox[0]));
-                Assert.Equal("efgh", thumbbox.toolTip.GetToolTip(thumbbox.pictureBox[1]));
-            }
+            Assert.Equal("http://img.example.com/abcd.png", tweetThumbnail.CurrentThumbnail.ThumbnailImageUrl);
+            Assert.Equal(
+                new(@"https://saucenao.com/search.php?url=http%3A%2F%2Fimg.example.com%2Fabcd.png"),
+                tweetThumbnail.GetImageSearchUriSauceNao()
+            );
         }
 
         [Fact]
-        public async Task ThumbnailLoadingEventTest()
+        public async Task Scroll_Test()
         {
-            using (var thumbbox = new TweetThumbnail())
-            {
-                SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
+            var thumbnailGenerator = this.CreateThumbnailGenerator();
+            thumbnailGenerator.Services.Add(this.CreateThumbnailService());
 
-                var post = new PostClass
-                {
-                    TextFromApi = "てすと",
-                    Media = new List<MediaInfo>
-                    {
-                    },
-                };
-                await TestUtils.NotRaisesAsync<EventArgs>(
-                    x => thumbbox.ThumbnailLoading += x,
-                    x => thumbbox.ThumbnailLoading -= x,
-                    () => thumbbox.ShowThumbnailAsync(post)
-                );
-
-                SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
-
-                var post2 = new PostClass
-                {
-                    TextFromApi = "てすと http://foo.example.com/abcd",
-                    Media = new List<MediaInfo>
-                    {
-                        new MediaInfo("http://foo.example.com/abcd"),
-                    },
-                };
-
-                await Assert.RaisesAsync<EventArgs>(
-                    x => thumbbox.ThumbnailLoading += x,
-                    x => thumbbox.ThumbnailLoading -= x,
-                    () => thumbbox.ShowThumbnailAsync(post2)
-                );
-            }
-        }
+            var tweetThumbnail = new TweetThumbnail();
+            tweetThumbnail.Initialize(thumbnailGenerator);
 
-        [Fact]
-        public async Task ScrollTest()
-        {
             var post = new PostClass
             {
-                TextFromApi = "てすと http://foo.example.com/abcd http://foo.example.com/efgh",
-                Media = new List<MediaInfo>
-                {
-                    new MediaInfo("http://foo.example.com/abcd"),
-                    new MediaInfo("http://foo.example.com/efgh"),
-                },
+                StatusId = new TwitterStatusId("100"),
+                Media = new() { new("http://example.com/abcd"), new("http://example.com/efgh") },
             };
 
-            using (var thumbbox = new TweetThumbnail())
-            {
-                SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
-                await thumbbox.ShowThumbnailAsync(post);
-
-                Assert.Equal(0, thumbbox.scrollBar.Minimum);
-                Assert.Equal(1, thumbbox.scrollBar.Maximum);
-
-                thumbbox.scrollBar.Value = 0;
-
-                thumbbox.ScrollDown();
-                Assert.Equal(1, thumbbox.scrollBar.Value);
-                Assert.False(thumbbox.pictureBox[0].Visible);
-                Assert.True(thumbbox.pictureBox[1].Visible);
-
-                thumbbox.ScrollDown();
-                Assert.Equal(1, thumbbox.scrollBar.Value);
-                Assert.False(thumbbox.pictureBox[0].Visible);
-                Assert.True(thumbbox.pictureBox[1].Visible);
-
-                thumbbox.ScrollUp();
-                Assert.Equal(0, thumbbox.scrollBar.Value);
-                Assert.True(thumbbox.pictureBox[0].Visible);
-                Assert.False(thumbbox.pictureBox[1].Visible);
-
-                thumbbox.ScrollUp();
-                Assert.Equal(0, thumbbox.scrollBar.Value);
-                Assert.True(thumbbox.pictureBox[0].Visible);
-                Assert.False(thumbbox.pictureBox[1].Visible);
-            }
+            await tweetThumbnail.PrepareThumbnails(post, CancellationToken.None);
+
+            Assert.Equal(2, tweetThumbnail.Thumbnails.Length);
+            Assert.Equal(0, tweetThumbnail.SelectedIndex);
+
+            tweetThumbnail.ScrollDown();
+            Assert.Equal(1, tweetThumbnail.SelectedIndex);
+
+            tweetThumbnail.ScrollDown();
+            Assert.Equal(1, tweetThumbnail.SelectedIndex);
+
+            tweetThumbnail.ScrollUp();
+            Assert.Equal(0, tweetThumbnail.SelectedIndex);
+
+            tweetThumbnail.ScrollUp();
+            Assert.Equal(0, tweetThumbnail.SelectedIndex);
+        }
+
+        private class MockThumbnailInfo : ThumbnailInfo
+        {
+            public override Task<MemoryImage> LoadThumbnailImageAsync(HttpClient http, CancellationToken cancellationToken)
+                => Task.FromResult(TestUtils.CreateDummyImage());
         }
     }
 }