using System.Net; using System.Net.Http.Headers; using System.Text; using Avalonia; using Avalonia.Threading; using Shouldly; using Svg; using Xunit; namespace AtomUI.Controls.Tests.ImageLoading; public class SvgImageCodecTests { private const string ValidSvg = "" + ""; static SvgImageCodecTests() { AvaloniaTestApp.EnsureInitialized(); } [Fact] public void Codec_Uses_The_Approved_Secure_Static_Contract() { var codec = new SvgImageCodec(); var parameters = SvgImageCodec.CreateParameters(); codec.Id.ShouldBe("atomui.svg"); codec.Version.ShouldBe(2); codec.IsDecodeSizeDependent.ShouldBeFalse(); parameters.Entities.ShouldBeNull(); parameters.Css.ShouldBeNull(); parameters.CurrentColor.ShouldBeNull(); var loadOptions = parameters.LoadOptions.ShouldNotBeNull(); loadOptions.ProcessingMode.ShouldBe(SvgProcessingMode.SecureStatic); loadOptions.ExternalResources.ShouldBe(SvgExternalResourcePolicy.SameDocumentAndDataOnly); loadOptions.PreserveUnknownElements.ShouldBeFalse(); loadOptions.PreferSvg2Href.ShouldBeTrue(); } [Fact] public async Task Http_Svg_Loads_Through_The_Unified_Pipeline_And_Reuses_One_Vector_Image() { var handler = new SvgResponseHandler(ValidSvg); var options = new ImageLoadingOptionsBuilder().Build("svg-codec-tests"); using var loader = new ImageLoader(options, [new SvgImageCodec()], handler); var source = ImageSource.Parse("https://example.com/avatar.svg"); var firstTask = Task.Run(async () => await loader.LoadAsync( new ImageLoadRequest(source) { DecodePixelWidth = 32, DecodePixelHeight = 32 }, TestContext.Current.CancellationToken)); ImageControlTestHost.WaitUntil(() => firstTask.IsCompleted, "first HTTP SVG load"); using var first = await firstTask; var secondTask = Task.Run(async () => await loader.LoadAsync( new ImageLoadRequest(source) { DecodePixelWidth = 512, DecodePixelHeight = 512 }, TestContext.Current.CancellationToken)); ImageControlTestHost.WaitUntil(() => secondTask.IsCompleted, "second HTTP SVG load"); using var second = await secondTask; first.IsSuccess.ShouldBeTrue(); second.IsSuccess.ShouldBeTrue(); second.Origin.ShouldBe(ImageLoadOrigin.DecodedMemory); second.Image.ShouldBeSameAs(first.Image); first.Image.ShouldNotBeNull().Size.ShouldBe(new Size(48, 32)); var backgroundSize = await Task.Run( () => first.Image.ShouldNotBeNull().Size, TestContext.Current.CancellationToken); backgroundSize.ShouldBe(new Size(48, 32)); handler.RequestCount.ShouldBe(1); } [Fact] public async Task Background_Last_Lease_Release_Does_Not_Access_SvgImage_Off_Thread() { var handler = new SvgResponseHandler(ValidSvg); var options = new ImageLoadingOptionsBuilder().Build("svg-release-tests"); using var loader = new ImageLoader(options, [new SvgImageCodec()], handler); var loadTask = Task.Run(async () => await loader.LoadAsync( new ImageLoadRequest(ImageSource.Parse("https://example.com/release.svg")), TestContext.Current.CancellationToken)); ImageControlTestHost.WaitUntil(() => loadTask.IsCompleted, "SVG release test load"); var result = await loadTask; result.IsSuccess.ShouldBeTrue(); var clearTask = Task.Run(async () => { result.Dispose(); await loader.ClearCacheAsync( new ImageCacheClearRequest(), TestContext.Current.CancellationToken); }, TestContext.Current.CancellationToken); ImageControlTestHost.WaitUntil(() => clearTask.IsCompleted, "background SVG release"); await clearTask; Should.NotThrow(() => Dispatcher.UIThread.RunJobs()); } [Fact] public async Task Invalid_Model_Is_Reported_As_DecodeFailed() { var options = new ImageLoadingOptionsBuilder().Build("svg-invalid-model-tests"); var source = new BytesImageSource("(() => new SvgImageCodec().DecodeAsync( content, probe, request, TestContext.Current.CancellationToken)); exception.Error.Code.ShouldBe(ImageLoadErrorCode.DecodeFailed); } private sealed class SvgResponseHandler(string svg) : HttpMessageHandler { private readonly byte[] _bytes = Encoding.UTF8.GetBytes(svg); private int _requestCount; internal int RequestCount => Volatile.Read(ref _requestCount); protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); Interlocked.Increment(ref _requestCount); var response = new HttpResponseMessage(HttpStatusCode.OK) { RequestMessage = request, Content = new ByteArrayContent(_bytes) }; response.Content.Headers.ContentType = new MediaTypeHeaderValue("image/svg+xml"); response.Headers.CacheControl = new CacheControlHeaderValue { MaxAge = TimeSpan.FromMinutes(5) }; return Task.FromResult(response); } } }