Skip to content

Commit 5bdc2fb

Browse files
committed
Get the funtionality up to date with merged Image component
1 parent 627eb7b commit 5bdc2fb

File tree

9 files changed

+112
-60
lines changed

9 files changed

+112
-60
lines changed

src/Components/Web.JS/src/Rendering/BinaryMedia.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ export class BinaryMedia {
8080

8181
this.observer.observe(document.body, {
8282
childList: true,
83-
subtree: true,
8483
attributes: true,
8584
attributeFilter: ['src', 'href'],
8685
});
@@ -295,15 +294,14 @@ export class BinaryMedia {
295294
BinaryMedia.loadingElements.delete(element);
296295
element.style.removeProperty('--blazor-media-progress');
297296
}
298-
element.removeEventListener('error', onError);
299297
};
300298

301299
const onError = (_e: Event) => {
302300
if (!cacheKey || BinaryMedia.activeCacheKey.get(element) === cacheKey) {
303301
BinaryMedia.loadingElements.delete(element);
304302
element.style.removeProperty('--blazor-media-progress');
303+
element.setAttribute('data-state', 'error');
305304
}
306-
element.removeEventListener('load', onLoad);
307305
};
308306

309307
element.addEventListener('load', onLoad, { once: true });

src/Components/Web/src/Media/FileDownload.cs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,19 @@
66

77
namespace Microsoft.AspNetCore.Components.Web.Media;
88

9+
/* This is equivalent to a .razor file containing:
10+
*
11+
* <a data-blazor-file-download
12+
* href="javascript:void(0)"
13+
* data-state=@(IsLoading ? "loading" : _hasError ? "error" : null)
14+
* @attributes="AdditionalAttributes"
15+
* @ref="Element"
16+
* @onclick="OnClickAsync">@(Text ?? "Download")</a>
17+
*
18+
*/
919
/// <summary>
10-
/// A component that provides a link which, when activated (clicked), downloads the provided media source
11-
/// either via a save-as dialog or directly, using the same BinaryMedia pipeline as <see cref="Image"/> and <see cref="Video"/>.
20+
/// A component that provides an anchor element to download the provided media source.
1221
/// </summary>
13-
/// <remarks>
14-
/// Unlike <see cref="Image"/> and <see cref="Video"/>, this component does not automatically load the media.
15-
/// It defers loading until the user activates the link. The stream is then materialized (no caching is performed for downloads)
16-
/// and a client-side download is triggered. Developers can style the link as a button via CSS (e.g., with a framework class).
17-
/// </remarks>
1822
public sealed class FileDownload : MediaComponentBase
1923
{
2024
/// <summary>

src/Components/Web/src/Media/Image.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ namespace Microsoft.AspNetCore.Components.Web.Media;
66
/* This is equivalent to a .razor file containing:
77
*
88
* <img data-blazor-image
9+
* src="@(_currentObjectUrl)"
910
* data-state=@(IsLoading ? "loading" : _hasError ? "error" : null)
10-
* @ref="Element"
11-
* @attributes="AdditionalAttributes" />
11+
* @attributes="AdditionalAttributes"
12+
* @ref="Element"></img>
1213
*
1314
*/
1415
/// <summary>

src/Components/Web/src/Media/Video.cs

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,18 @@
33

44
namespace Microsoft.AspNetCore.Components.Web.Media;
55

6+
/* This is equivalent to a .razor file containing:
7+
*
8+
* <video data-blazor-video
9+
* src="@(_currentObjectUrl)"
10+
* data-state=@(IsLoading ? "loading" : _hasError ? "error" : null)
11+
* @attributes="AdditionalAttributes"
12+
* @ref="Element"></video>
13+
*
14+
*/
615
/// <summary>
7-
/// A component that renders video content from non-HTTP sources (byte arrays or streams)
8-
/// by materializing an in-memory blob and assigning its object URL to the underlying
9-
/// <c>&lt;video&gt;</c> element.
16+
/// A component that efficiently renders video content from non-HTTP sources like byte arrays.
1017
/// </summary>
11-
/// <remarks>
12-
/// This component uses the same client-side pipeline as <see cref="Image"/> and therefore
13-
/// reads the full content into memory to create a blob URL. It is not suitable for large or
14-
/// truly streaming video scenarios. Use browser-native streaming approaches if you require
15-
/// progressive playback.
16-
///
17-
/// To configure common video attributes like <c>controls</c>, <c>autoplay</c>, <c>muted</c>,
18-
/// or <c>loop</c>, pass them through <see cref="MediaComponentBase.AdditionalAttributes"/>.
19-
/// </remarks>
2018
public sealed class Video : MediaComponentBase
2119
{
2220
/// <inheritdoc/>

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ public void ProvidedHref_IsRemoved_AndInertHrefUsed()
130130
[Fact]
131131
public void RapidClicks_CancelsFirstAndStartsSecond()
132132
{
133-
// Instrument with delay on first call for cancellation scenario
133+
// Instrument with controllable delay on first call for cancellation scenario (no setTimeout)
134134
var success = ((IJavaScriptExecutor)Browser).ExecuteAsyncScript(@"
135135
var callback = arguments[arguments.length - 1];
136136
(function(){
@@ -140,10 +140,19 @@ function patch(){
140140
if (!window.__origDownloadAsyncDelay){
141141
window.__origDownloadAsyncDelay = root.downloadAsync;
142142
window.__downloadCalls = 0;
143+
window.__downloadDelayResolvers = null;
143144
root.downloadAsync = async function(...a){
144145
window.__downloadCalls++;
145146
if (window.__downloadCalls === 1){
146-
await new Promise(r => setTimeout(r, 500));
147+
const getResolvers = () => {
148+
if (Promise.fromResolvers) return Promise.fromResolvers();
149+
let resolve, reject; const p = new Promise((r,j)=>{ resolve=r; reject=j; });
150+
return { promise: p, resolve, reject };
151+
};
152+
if (!window.__downloadDelayResolvers){
153+
window.__downloadDelayResolvers = getResolvers();
154+
}
155+
await window.__downloadDelayResolvers.promise;
147156
}
148157
return window.__origDownloadAsyncDelay.apply(this, a);
149158
};
@@ -160,7 +169,17 @@ function patch(){
160169
link.Click(); // first (delayed)
161170
link.Click(); // second should cancel first
162171

172+
((IJavaScriptExecutor)Browser).ExecuteScript("if (window.__downloadDelayResolvers) { window.__downloadDelayResolvers.resolve(); }");
173+
163174
Browser.True(() => Convert.ToInt32(((IJavaScriptExecutor)Browser).ExecuteScript("return window.__downloadCalls || 0;"), CultureInfo.InvariantCulture) >= 2);
164175
Browser.True(() => string.IsNullOrEmpty(link.GetAttribute("data-state")) || link.GetAttribute("data-state") == null);
176+
177+
// Cleanup instrumentation
178+
((IJavaScriptExecutor)Browser).ExecuteScript(@"
179+
(function(){
180+
const root = Blazor && Blazor._internal && Blazor._internal.BinaryMedia;
181+
if (root && window.__origDownloadAsyncDelay){ root.downloadAsync = window.__origDownloadAsyncDelay; delete window.__origDownloadAsyncDelay; }
182+
delete window.__downloadDelayResolvers;
183+
})();");
165184
}
166185
}

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

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -166,15 +166,24 @@ public void ImageRenders_WithCorrectDimensions()
166166
[Fact]
167167
public void Image_CompletesLoad_AfterArtificialDelay()
168168
{
169-
// Patch setContentAsync to introduce a delay before delegating to original
169+
// Instrument setContentAsync to pause before fulfilling first image load until explicitly resolved.
170170
((IJavaScriptExecutor)Browser).ExecuteScript(@"
171171
(function(){
172172
const root = Blazor && Blazor._internal && Blazor._internal.BinaryMedia;
173173
if (!root) return;
174174
if (!window.__origSetContentAsync) {
175175
window.__origSetContentAsync = root.setContentAsync;
176176
root.setContentAsync = async function(...args){
177-
await new Promise(r => setTimeout(r, 500));
177+
const getResolvers = () => {
178+
if (Promise.fromResolvers) return Promise.fromResolvers();
179+
let resolve, reject; const promise = new Promise((r,j)=>{ resolve=r; reject=j; });
180+
return { promise, resolve, reject };
181+
};
182+
if (!window.__imageContentDelay){
183+
const resolvers = getResolvers();
184+
window.__imageContentDelay = resolvers; // first invocation delayed
185+
await resolvers.promise;
186+
}
178187
return window.__origSetContentAsync.apply(this, args);
179188
};
180189
}
@@ -183,21 +192,26 @@ public void Image_CompletesLoad_AfterArtificialDelay()
183192
Browser.FindElement(By.Id("load-png")).Click();
184193

185194
var imageElement = Browser.FindElement(By.Id("png-basic"));
186-
Browser.True(() =>
187-
{
195+
Assert.NotNull(imageElement);
196+
197+
// Release the delayed promise so load can complete.
198+
((IJavaScriptExecutor)Browser).ExecuteScript("if (window.__imageContentDelay) { window.__imageContentDelay.resolve(); }");
199+
200+
Browser.True(() => {
188201
var src = imageElement.GetAttribute("src");
189202
return !string.IsNullOrEmpty(src) && src.StartsWith("blob:", StringComparison.Ordinal);
190203
});
191204
Browser.Equal("PNG basic loaded", () => Browser.FindElement(By.Id("current-status")).Text);
192205

193-
// Restore
206+
// Restore original function and clean up instrumentation
194207
((IJavaScriptExecutor)Browser).ExecuteScript(@"
195208
(function(){
196209
const root = Blazor && Blazor._internal && Blazor._internal.BinaryMedia;
197210
if (root && window.__origSetContentAsync) {
198211
root.setContentAsync = window.__origSetContentAsync;
199212
delete window.__origSetContentAsync;
200213
}
214+
delete window.__imageContentDelay;
201215
})();");
202216
}
203217

@@ -341,4 +355,19 @@ public void UrlRevoked_WhenImageRemovedFromDom()
341355
}
342356
});
343357
}
358+
359+
[Fact]
360+
public void InvalidMimeImage_SetsErrorState()
361+
{
362+
Browser.FindElement(By.Id("load-invalid-mime")).Click();
363+
Browser.Equal("Invalid mime image loaded", () => Browser.FindElement(By.Id("current-status")).Text);
364+
365+
var img = Browser.FindElement(By.Id("invalid-mime-image"));
366+
Assert.NotNull(img);
367+
368+
Browser.Equal("error", () => img.GetAttribute("data-state"));
369+
370+
var src = img.GetAttribute("src");
371+
Assert.True(string.IsNullOrEmpty(src) || src.StartsWith("blob:", StringComparison.Ordinal));
372+
}
344373
}

src/Components/test/testassets/BasicTestApp/MediaTest/FileDownloadTestComponent.razor

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,37 +47,33 @@
4747
private static readonly byte[] TestPngData = Convert.FromBase64String("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdjqK6u/g8ABVcCcYoGhmwAAAAASUVORK5CYII=");
4848
private static readonly byte[] TestTxtData = System.Text.Encoding.UTF8.GetBytes("Hello file download");
4949

50-
private async Task ShowDownload()
50+
private void ShowDownload()
5151
{
5252
_downloadSource = new MediaSource(TestPngData, "image/png", "download-png-basic");
5353
_downloadFileName = "test.png";
5454
_showDownload = true;
55-
await Task.Delay(10);
5655
}
5756

58-
private async Task ShowErrorDownload()
57+
private void ShowErrorDownload()
5958
{
6059
var ms = new MemoryStream(TestTxtData);
6160
ms.Seek(ms.Length, SeekOrigin.Begin); // Force immediate EOF / error in JS pipeline when reading
6261
_errorSource = new MediaSource(ms, "text/plain", "error-file-key");
6362
_errorFileName = "error.txt";
6463
_showError = true;
65-
await Task.Delay(10);
6664
}
6765

68-
private async Task ShowBlankFilename()
66+
private void ShowBlankFilename()
6967
{
7068
_blankSource = new MediaSource(TestTxtData, "text/plain", "blank-file-key");
7169
_blankFileName = "";
7270
_showBlank = true;
73-
await Task.Delay(10);
7471
}
7572

76-
private async Task ShowCustomHref()
73+
private void ShowCustomHref()
7774
{
7875
_customHrefSource = new MediaSource(TestTxtData, "text/plain", "custom-href-file-key");
7976
_customHrefFileName = "custom.bin";
8077
_showCustomHref = true;
81-
await Task.Delay(10);
8278
}
8379
}

0 commit comments

Comments
 (0)