mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-17 05:31:01 +08:00
170 lines
6.3 KiB
C#
170 lines
6.3 KiB
C#
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 =
|
|
"<svg xmlns='http://www.w3.org/2000/svg' width='48' height='32' viewBox='0 0 48 32'>" +
|
|
"<rect width='48' height='32' fill='#1677ff'/></svg>";
|
|
|
|
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("<svg"u8.ToArray(), "invalid-svg");
|
|
var request = ImageCacheKey.Normalize(new ImageLoadRequest(source), options, forceReload: false);
|
|
var content = new ImageEncodedContent(
|
|
"<svg"u8.ToArray(),
|
|
"image/svg+xml",
|
|
ImageLoadOrigin.Local,
|
|
DateTimeOffset.UtcNow).MarkValidated();
|
|
var metadata = new SvgContentMetadata(
|
|
1,
|
|
0,
|
|
1,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
16,
|
|
16,
|
|
16,
|
|
16,
|
|
256);
|
|
var probe = new ImageProbeResult(
|
|
ImageContentFormat.Svg,
|
|
"image/svg+xml",
|
|
16,
|
|
16,
|
|
false,
|
|
metadata);
|
|
|
|
var exception = await Should.ThrowAsync<ImageLoadFailureException>(() =>
|
|
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<HttpResponseMessage> 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);
|
|
}
|
|
}
|
|
}
|