Skip to content

Commit 9f97e42

Browse files
committed
Add video tests just to see it work (code duplication)
1 parent 90dd487 commit 9f97e42

File tree

6 files changed

+612
-94
lines changed

6 files changed

+612
-94
lines changed
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
#nullable enable
5+
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Collections.Concurrent;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
using System.Linq;
12+
using Microsoft.AspNetCore.Components.Test.Helpers;
13+
using Microsoft.Extensions.DependencyInjection;
14+
using Microsoft.JSInterop;
15+
using Xunit;
16+
using Microsoft.AspNetCore.Components;
17+
using Microsoft.AspNetCore.Components.RenderTree;
18+
using Microsoft.AspNetCore.Components.Web.Media;
19+
20+
namespace Microsoft.AspNetCore.Components.Web.Media.Tests;
21+
22+
/// <summary>
23+
/// Unit tests for the Media.Video component (non-visual logic and JS interaction semantics).
24+
/// </summary>
25+
public class VideoTest
26+
{
27+
// Arbitrary small byte array (not a real mp4) sufficient for exercising logic.
28+
private static readonly byte[] Mp4Bytes = new byte[] { 0,0,0,24, (byte)'f',(byte)'t',(byte)'y',(byte)'p',(byte)'i',(byte)'s',(byte)'o',(byte)'m',0,0,0,0,(byte)'i',(byte)'s',(byte)'o',(byte)'m',(byte)'i',(byte)'s',(byte)'o',(byte)'2' };
29+
30+
[Fact]
31+
public async Task LoadsVideo_InvokesSetContentAsync_WhenSourceProvided()
32+
{
33+
var js = new FakeMediaJsRuntime(cacheHit: false);
34+
using var renderer = CreateRenderer(js);
35+
var comp = (Video)renderer.InstantiateComponent<Video>();
36+
var id = renderer.AssignRootComponentId(comp);
37+
38+
var source = new MediaSource(Mp4Bytes, "video/mp4", cacheKey: "vid-1");
39+
await renderer.RenderRootComponentAsync(id, ParameterView.FromDictionary(new Dictionary<string, object?>
40+
{
41+
[nameof(Video.Source)] = source,
42+
}));
43+
44+
Assert.Equal(1, js.Count("Blazor._internal.BinaryMedia.setContentAsync"));
45+
}
46+
47+
[Fact]
48+
public async Task SkipsReload_OnSameCacheKey()
49+
{
50+
var js = new FakeMediaJsRuntime(cacheHit: false);
51+
using var renderer = CreateRenderer(js);
52+
var comp = (Video)renderer.InstantiateComponent<Video>();
53+
var id = renderer.AssignRootComponentId(comp);
54+
55+
var s1 = new MediaSource(new byte[10], "video/mp4", cacheKey: "same");
56+
await renderer.RenderRootComponentAsync(id, ParameterView.FromDictionary(new Dictionary<string, object?>
57+
{
58+
[nameof(Video.Source)] = s1,
59+
}));
60+
61+
var s2 = new MediaSource(new byte[20], "video/mp4", cacheKey: "same");
62+
await renderer.RenderRootComponentAsync(id, ParameterView.FromDictionary(new Dictionary<string, object?>
63+
{
64+
[nameof(Video.Source)] = s2,
65+
}));
66+
67+
Assert.Equal(1, js.Count("Blazor._internal.BinaryMedia.setContentAsync"));
68+
}
69+
70+
[Fact]
71+
public async Task NullSource_Throws()
72+
{
73+
var js = new FakeMediaJsRuntime(cacheHit: false);
74+
using var renderer = CreateRenderer(js);
75+
var comp = (Video)renderer.InstantiateComponent<Video>();
76+
var id = renderer.AssignRootComponentId(comp);
77+
78+
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
79+
{
80+
await renderer.RenderRootComponentAsync(id, ParameterView.FromDictionary(new Dictionary<string, object?>
81+
{
82+
[nameof(Video.Source)] = null,
83+
}));
84+
});
85+
86+
Assert.Equal(0, js.TotalInvocationCount);
87+
}
88+
89+
[Fact]
90+
public async Task ParameterChange_DifferentCacheKey_Reloads()
91+
{
92+
var js = new FakeMediaJsRuntime(cacheHit: false);
93+
using var renderer = CreateRenderer(js);
94+
var comp = (Video)renderer.InstantiateComponent<Video>();
95+
var id = renderer.AssignRootComponentId(comp);
96+
97+
var s1 = new MediaSource(new byte[4], "video/mp4", "key-a");
98+
await renderer.RenderRootComponentAsync(id, ParameterView.FromDictionary(new Dictionary<string, object?>
99+
{
100+
[nameof(Video.Source)] = s1,
101+
}));
102+
var s2 = new MediaSource(new byte[6], "video/mp4", "key-b");
103+
await renderer.RenderRootComponentAsync(id, ParameterView.FromDictionary(new Dictionary<string, object?>
104+
{
105+
[nameof(Video.Source)] = s2,
106+
}));
107+
108+
Assert.Equal(2, js.Count("Blazor._internal.BinaryMedia.setContentAsync"));
109+
}
110+
111+
[Fact]
112+
public async Task ChangingSource_CancelsPreviousLoad()
113+
{
114+
var js = new FakeMediaJsRuntime(cacheHit: false) { DelayOnFirstSetCall = true };
115+
using var renderer = CreateRenderer(js);
116+
var comp = (Video)renderer.InstantiateComponent<Video>();
117+
var id = renderer.AssignRootComponentId(comp);
118+
119+
var s1 = new MediaSource(new byte[10], "video/mp4", "k1");
120+
await renderer.RenderRootComponentAsync(id, ParameterView.FromDictionary(new Dictionary<string, object?>
121+
{
122+
[nameof(Video.Source)] = s1,
123+
}));
124+
125+
var s2 = new MediaSource(new byte[10], "video/mp4", "k2");
126+
await renderer.RenderRootComponentAsync(id, ParameterView.FromDictionary(new Dictionary<string, object?>
127+
{
128+
[nameof(Video.Source)] = s2,
129+
}));
130+
131+
for (var i = 0; i < 10 && js.CapturedTokens.Count < 2; i++)
132+
{
133+
await Task.Delay(10);
134+
}
135+
136+
Assert.NotEmpty(js.CapturedTokens);
137+
Assert.True(js.CapturedTokens.First().IsCancellationRequested);
138+
Assert.Equal(2, js.Count("Blazor._internal.BinaryMedia.setContentAsync"));
139+
}
140+
141+
private static TestRenderer CreateRenderer(IJSRuntime js)
142+
{
143+
var services = new ServiceCollection();
144+
services.AddLogging();
145+
services.AddSingleton(js);
146+
return new InteractiveTestRenderer(services.BuildServiceProvider());
147+
}
148+
149+
private sealed class InteractiveTestRenderer : TestRenderer
150+
{
151+
public InteractiveTestRenderer(IServiceProvider serviceProvider) : base(serviceProvider) { }
152+
protected internal override RendererInfo RendererInfo => new RendererInfo("Test", isInteractive: true);
153+
}
154+
155+
private sealed class FakeMediaJsRuntime : IJSRuntime
156+
{
157+
public sealed record Invocation(string Identifier, object?[] Args, CancellationToken Token);
158+
private readonly ConcurrentQueue<Invocation> _invocations = new();
159+
private readonly ConcurrentDictionary<string, bool> _memoryCache = new();
160+
private readonly bool _forceCacheHit;
161+
162+
public FakeMediaJsRuntime(bool cacheHit) { _forceCacheHit = cacheHit; }
163+
164+
public int TotalInvocationCount => _invocations.Count;
165+
public int Count(string id) => _invocations.Count(i => i.Identifier == id);
166+
public IReadOnlyList<CancellationToken> CapturedTokens => _invocations.Select(i => i.Token).ToList();
167+
168+
public bool DelayOnFirstSetCall { get; set; }
169+
public bool ForceFail { get; set; }
170+
public bool FailOnce { get; set; } = true;
171+
public bool FailIfTotalBytesIsZero { get; set; }
172+
private bool _failUsed;
173+
private int _setCalls;
174+
175+
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, object?[]? args)
176+
=> InvokeAsync<TValue>(identifier, CancellationToken.None, args ?? Array.Empty<object?>());
177+
178+
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, object?[]? args)
179+
{
180+
args ??= Array.Empty<object?>();
181+
_invocations.Enqueue(new Invocation(identifier, args, cancellationToken));
182+
183+
if (identifier == "Blazor._internal.BinaryMedia.setContentAsync")
184+
{
185+
_setCalls++;
186+
var cacheKey = args.Length >= 4 ? args[3] as string : null;
187+
var hasStream = args.Length >= 2 && args[1] != null;
188+
long? totalBytes = null;
189+
if (args.Length >= 5 && args[4] != null)
190+
{
191+
try { totalBytes = Convert.ToInt64(args[4], System.Globalization.CultureInfo.InvariantCulture); } catch { totalBytes = null; }
192+
}
193+
194+
if (DelayOnFirstSetCall && _setCalls == 1)
195+
{
196+
var tcs = new TaskCompletionSource<TValue>(TaskCreationOptions.RunContinuationsAsynchronously);
197+
cancellationToken.Register(() => tcs.TrySetException(new OperationCanceledException(cancellationToken)));
198+
return new ValueTask<TValue>(tcs.Task);
199+
}
200+
201+
var shouldFail = (ForceFail && (!_failUsed || !FailOnce))
202+
|| (FailIfTotalBytesIsZero && (totalBytes.HasValue && totalBytes.Value == 0));
203+
if (ForceFail)
204+
{
205+
_failUsed = true;
206+
}
207+
208+
var fromCache = !shouldFail && cacheKey != null && (_forceCacheHit || _memoryCache.ContainsKey(cacheKey));
209+
if (!fromCache && hasStream && !string.IsNullOrEmpty(cacheKey) && !shouldFail)
210+
{
211+
_memoryCache[cacheKey!] = true;
212+
}
213+
214+
var t = typeof(TValue);
215+
object? instance = Activator.CreateInstance(t, nonPublic: true);
216+
if (instance is null)
217+
{
218+
return ValueTask.FromResult(default(TValue)!);
219+
}
220+
221+
void setProp(string name, object? value)
222+
{
223+
var p = t.GetProperty(name, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
224+
p?.SetValue(instance, value);
225+
}
226+
227+
if (shouldFail)
228+
{
229+
setProp("Success", false);
230+
setProp("FromCache", false);
231+
setProp("ObjectUrl", null);
232+
setProp("Error", "simulated-failure");
233+
}
234+
else
235+
{
236+
setProp("Success", hasStream || fromCache);
237+
setProp("FromCache", fromCache);
238+
setProp("ObjectUrl", (hasStream || fromCache) && cacheKey != null ? $"blob:{cacheKey}:{Guid.NewGuid()}" : null);
239+
setProp("Error", null);
240+
}
241+
242+
return ValueTask.FromResult((TValue)instance);
243+
}
244+
245+
return ValueTask.FromResult(default(TValue)!);
246+
}
247+
}
248+
}

src/Components/test/E2ETest/Tests/ImageTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
using System.Globalization;
55
using BasicTestApp;
6-
using BasicTestApp.ImageTest;
6+
using BasicTestApp.MediaTest;
77
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
88
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
99
using Microsoft.AspNetCore.E2ETesting;

0 commit comments

Comments
 (0)