[Web] Allow specifying the strategy on when to use <img> element to display images (#159917)
This PR follows the discussion under https://github.com/flutter/flutter/pull/157755 and adds a flag to determine when `<img>` elements are used. By default the feature is turned off. Instead of just a boolean for on & off, I made it an enum to accept multiple options. Notably, an `always` option can be useful when the developer wants a unified experience regardless of the image origin (such as when displaying an image from arbitrary URLs.) ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
parent
3f981117eb
commit
c16b9c52e4
@ -21,7 +21,12 @@ typedef _SimpleDecoderCallback = Future<ui.Codec> Function(ui.ImmutableBuffer bu
|
||||
class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkImage>
|
||||
implements image_provider.NetworkImage {
|
||||
/// Creates an object that fetches the image at the given URL.
|
||||
const NetworkImage(this.url, {this.scale = 1.0, this.headers});
|
||||
const NetworkImage(
|
||||
this.url, {
|
||||
this.scale = 1.0,
|
||||
this.headers,
|
||||
this.webHtmlElementStrategy = image_provider.WebHtmlElementStrategy.never,
|
||||
});
|
||||
|
||||
@override
|
||||
final String url;
|
||||
@ -32,6 +37,9 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
|
||||
@override
|
||||
final Map<String, String>? headers;
|
||||
|
||||
@override
|
||||
final image_provider.WebHtmlElementStrategy webHtmlElementStrategy;
|
||||
|
||||
@override
|
||||
Future<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) {
|
||||
return SynchronousFuture<NetworkImage>(this);
|
||||
|
@ -18,9 +18,9 @@ import 'image_stream.dart';
|
||||
/// used for testing purposes.
|
||||
typedef HttpRequestFactory = web.XMLHttpRequest Function();
|
||||
|
||||
/// The type for an overridable factory function for creating <img> elements,
|
||||
/// used for testing purposes.
|
||||
typedef ImgElementFactory = web.HTMLImageElement Function();
|
||||
/// The type for an overridable factory function for creating HTML elements to
|
||||
/// display images, used for testing purposes.
|
||||
typedef HtmlElementFactory = web.HTMLImageElement Function();
|
||||
|
||||
// Method signature for _loadAsync decode callbacks.
|
||||
typedef _SimpleDecoderCallback = Future<ui.Codec> Function(ui.ImmutableBuffer buffer);
|
||||
@ -40,17 +40,17 @@ void debugRestoreHttpRequestFactory() {
|
||||
httpRequestFactory = _httpClient;
|
||||
}
|
||||
|
||||
/// The default <img> element factory.
|
||||
/// The default HTML element factory.
|
||||
web.HTMLImageElement _imgElementFactory() {
|
||||
return web.document.createElement('img') as web.HTMLImageElement;
|
||||
}
|
||||
|
||||
/// The factory function that creates <img> elements, can be overridden for
|
||||
/// The factory function that creates HTML elements, can be overridden for
|
||||
/// tests.
|
||||
@visibleForTesting
|
||||
ImgElementFactory imgElementFactory = _imgElementFactory;
|
||||
HtmlElementFactory imgElementFactory = _imgElementFactory;
|
||||
|
||||
/// Restores the default <img> element factory.
|
||||
/// Restores the default HTML element factory.
|
||||
@visibleForTesting
|
||||
void debugRestoreImgElementFactory() {
|
||||
imgElementFactory = _imgElementFactory;
|
||||
@ -63,7 +63,12 @@ void debugRestoreImgElementFactory() {
|
||||
class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkImage>
|
||||
implements image_provider.NetworkImage {
|
||||
/// Creates an object that fetches the image at the given URL.
|
||||
const NetworkImage(this.url, {this.scale = 1.0, this.headers});
|
||||
const NetworkImage(
|
||||
this.url, {
|
||||
this.scale = 1.0,
|
||||
this.headers,
|
||||
this.webHtmlElementStrategy = image_provider.WebHtmlElementStrategy.never,
|
||||
});
|
||||
|
||||
@override
|
||||
final String url;
|
||||
@ -74,6 +79,9 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
|
||||
@override
|
||||
final Map<String, String>? headers;
|
||||
|
||||
@override
|
||||
final image_provider.WebHtmlElementStrategy webHtmlElementStrategy;
|
||||
|
||||
@override
|
||||
Future<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) {
|
||||
return SynchronousFuture<NetworkImage>(this);
|
||||
@ -136,19 +144,7 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
|
||||
) async {
|
||||
assert(key == this);
|
||||
|
||||
final Uri resolved = Uri.base.resolve(key.url);
|
||||
|
||||
final bool containsNetworkImageHeaders = key.headers?.isNotEmpty ?? false;
|
||||
|
||||
// We use a different method when headers are set because the
|
||||
// `ui_web.createImageCodecFromUrl` method is not capable of handling headers.
|
||||
if (containsNetworkImageHeaders) {
|
||||
// It is not possible to load an <img> element and pass the headers with
|
||||
// the request to fetch the image. Since the user has provided headers,
|
||||
// this function should assume the headers are required to resolve to
|
||||
// the correct resource and should not attempt to load the image in an
|
||||
// <img> tag without the headers.
|
||||
|
||||
Future<ImageStreamCompleter> loadViaDecode() async {
|
||||
// Resolve the Codec before passing it to
|
||||
// [MultiFrameImageStreamCompleter] so any errors aren't reported
|
||||
// twice (once from the MultiFrameImageStreamCompleter and again
|
||||
@ -161,34 +157,38 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
|
||||
debugLabel: key.url,
|
||||
informationCollector: _imageStreamInformationCollector(key),
|
||||
);
|
||||
} else if (isSkiaWeb) {
|
||||
try {
|
||||
// Resolve the Codec before passing it to
|
||||
// [MultiFrameImageStreamCompleter] so any errors aren't reported
|
||||
// twice (once from the MultiFrameImageStreamCompleter and again
|
||||
// from the wrapping [ForwardingImageStreamCompleter]).
|
||||
final ui.Codec codec = await _fetchImageBytes(decode);
|
||||
return MultiFrameImageStreamCompleter(
|
||||
chunkEvents: chunkEvents.stream,
|
||||
codec: Future<ui.Codec>.value(codec),
|
||||
scale: key.scale,
|
||||
debugLabel: key.url,
|
||||
informationCollector: _imageStreamInformationCollector(key),
|
||||
);
|
||||
} catch (e) {
|
||||
// If we failed to fetch the bytes, try to load the image in an <img>
|
||||
// element instead.
|
||||
final web.HTMLImageElement imageElement = imgElementFactory();
|
||||
imageElement.src = key.url;
|
||||
// Decode the <img> element before creating the ImageStreamCompleter
|
||||
// to avoid double reporting the error.
|
||||
await imageElement.decode().toDart;
|
||||
return OneFrameImageStreamCompleter(
|
||||
Future<ImageInfo>.value(WebImageInfo(imageElement, debugLabel: key.url)),
|
||||
informationCollector: _imageStreamInformationCollector(key),
|
||||
)..debugLabel = key.url;
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
Future<ImageStreamCompleter> loadViaImgElement() async {
|
||||
// If we failed to fetch the bytes, try to load the image in an <img>
|
||||
// element instead.
|
||||
final web.HTMLImageElement imageElement = imgElementFactory();
|
||||
imageElement.src = key.url;
|
||||
// Decode the <img> element before creating the ImageStreamCompleter
|
||||
// to avoid double reporting the error.
|
||||
await imageElement.decode().toDart;
|
||||
return OneFrameImageStreamCompleter(
|
||||
Future<ImageInfo>.value(WebImageInfo(imageElement, debugLabel: key.url)),
|
||||
informationCollector: _imageStreamInformationCollector(key),
|
||||
)..debugLabel = key.url;
|
||||
}
|
||||
|
||||
final bool containsNetworkImageHeaders = key.headers?.isNotEmpty ?? false;
|
||||
// When headers are set, the image can only be loaded by decoding.
|
||||
//
|
||||
// For the HTML renderer, `ui_web.createImageCodecFromUrl` method is not
|
||||
// capable of handling headers.
|
||||
//
|
||||
// For CanvasKit and Skwasm, it is not possible to load an <img> element and
|
||||
// pass the headers with the request to fetch the image. Since the user has
|
||||
// provided headers, this function should assume the headers are required to
|
||||
// resolve to the correct resource and should not attempt to load the image
|
||||
// in an <img> tag without the headers.
|
||||
if (containsNetworkImageHeaders) {
|
||||
return loadViaDecode();
|
||||
}
|
||||
|
||||
if (!isSkiaWeb) {
|
||||
// This branch is only hit by the HTML renderer, which is deprecated. The
|
||||
// HTML renderer supports loading images with CORS restrictions, so we
|
||||
// don't need to catch errors and try loading the image in an <img> tag
|
||||
@ -198,6 +198,7 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
|
||||
// [MultiFrameImageStreamCompleter] so any errors aren't reported
|
||||
// twice (once from the MultiFrameImageStreamCompleter) and again
|
||||
// from the wrapping [ForwardingImageStreamCompleter].
|
||||
final Uri resolved = Uri.base.resolve(key.url);
|
||||
final ui.Codec codec = await ui_web.createImageCodecFromUrl(
|
||||
resolved,
|
||||
chunkCallback: (int bytes, int total) {
|
||||
@ -212,6 +213,21 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
|
||||
informationCollector: _imageStreamInformationCollector(key),
|
||||
);
|
||||
}
|
||||
|
||||
switch (webHtmlElementStrategy) {
|
||||
case image_provider.WebHtmlElementStrategy.never:
|
||||
return loadViaDecode();
|
||||
case image_provider.WebHtmlElementStrategy.prefer:
|
||||
return loadViaImgElement();
|
||||
case image_provider.WebHtmlElementStrategy.fallback:
|
||||
try {
|
||||
// Await here so that errors occurred during the asynchronous process
|
||||
// of `loadViaDecode` are caught and triggers `loadViaImgElement`.
|
||||
return await loadViaDecode();
|
||||
} catch (e) {
|
||||
return loadViaImgElement();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<ui.Codec> _fetchImageBytes(_SimpleDecoderCallback decode) async {
|
||||
|
@ -7,12 +7,12 @@ import 'dart:ui' as ui;
|
||||
import 'image_stream.dart';
|
||||
|
||||
/// An [ImageInfo] object indicating that the image can only be displayed in
|
||||
/// an <img> element, and no [dart:ui.Image] can be created for it.
|
||||
/// an HTML element, and no [dart:ui.Image] can be created for it.
|
||||
///
|
||||
/// This occurs on the web when the image resource is from a different origin
|
||||
/// and is not configured for CORS. Since the image bytes cannot be directly
|
||||
/// fetched, [ui.Image]s cannot be created from it. However, the image can
|
||||
/// still be displayed if an <img> element is used.
|
||||
/// still be displayed if an HTML element is used.
|
||||
class WebImageInfo implements ImageInfo {
|
||||
@override
|
||||
ImageInfo clone() => _unsupported();
|
||||
|
@ -8,18 +8,18 @@ import '../web.dart' as web;
|
||||
import 'image_stream.dart';
|
||||
|
||||
/// An [ImageInfo] object indicating that the image can only be displayed in
|
||||
/// an <img> element, and no [dart:ui.Image] can be created for it.
|
||||
/// an HTML element, and no [dart:ui.Image] can be created for it.
|
||||
///
|
||||
/// This occurs on the web when the image resource is from a different origin
|
||||
/// and is not configured for CORS. Since the image bytes cannot be directly
|
||||
/// fetched, [Image]s cannot be created from it. However, the image can
|
||||
/// still be displayed if an <img> element is used.
|
||||
/// still be displayed if an HTML element is used.
|
||||
class WebImageInfo implements ImageInfo {
|
||||
/// Creates a new [WebImageInfo] from a given <img> element.
|
||||
/// Creates a new [WebImageInfo] from a given HTML element.
|
||||
WebImageInfo(this.htmlImage, {this.debugLabel});
|
||||
|
||||
/// The <img> element used to display this image. This <img> element has
|
||||
/// already been decoded, so size information can be retrieved from it.
|
||||
/// The HTML element used to display this image. This HTML element has already
|
||||
/// decoded the image, so size information can be retrieved from it.
|
||||
final web.HTMLImageElement htmlImage;
|
||||
|
||||
@override
|
||||
|
@ -6,6 +6,7 @@
|
||||
// late BuildContext context;
|
||||
|
||||
/// @docImport 'package:flutter/widgets.dart';
|
||||
/// @docImport '_web_image_info_io.dart';
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
@ -1467,10 +1468,46 @@ class ResizeImage extends ImageProvider<ResizeImageKey> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The strategy for [Image.network] and [NetworkImage] to decide whether to
|
||||
/// display images in HTML elements contained in a platform view instead of
|
||||
/// fetching bytes.
|
||||
///
|
||||
/// See [Image.network] for more explanation on the impact.
|
||||
///
|
||||
/// This option is only effective on the Web platform. Other platforms always
|
||||
/// display network images by fetching bytes.
|
||||
enum WebHtmlElementStrategy {
|
||||
/// Only show images by fetching bytes, and report errors if the fetch
|
||||
/// encounters errors.
|
||||
never,
|
||||
|
||||
/// Prefer fetching bytes to display images, and fall back to HTML elements
|
||||
/// when fetching bytes is not available.
|
||||
///
|
||||
/// This strategy uses HTML elements only if `headers` is empty and the fetch
|
||||
/// encounters errors. Errors may still be reported if neither approach works.
|
||||
fallback,
|
||||
|
||||
/// Prefer HTML elements to display images, and fall back to fetching bytes
|
||||
/// when HTML elements do not work.
|
||||
///
|
||||
/// This strategy fetches bytes only if `headers` is not empty, since HTML
|
||||
/// elements do not support headers. Errors may still be reported if neither
|
||||
/// approach works.
|
||||
prefer,
|
||||
}
|
||||
|
||||
/// Fetches the given URL from the network, associating it with the given scale.
|
||||
///
|
||||
/// The image will be cached regardless of cache headers from the server.
|
||||
///
|
||||
/// Typically this class resolves to an image stream that ultimately produces
|
||||
/// [dart:ui.Image]s. On the Web platform, the [webHtmlElementStrategy]
|
||||
/// parameter can be used to make the image stream ultimately produce an
|
||||
/// [WebImageInfo] instead, which makes [Image.network] display the image as an
|
||||
/// HTML element in a platform view. The feature is by default turned off
|
||||
/// ([WebHtmlElementStrategy.never]). See [Image.network] for more explanation.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Image.network] for a shorthand of an [Image] widget backed by [NetworkImage].
|
||||
@ -1484,8 +1521,15 @@ abstract class NetworkImage extends ImageProvider<NetworkImage> {
|
||||
///
|
||||
/// The [scale] argument is the linear scale factor for drawing this image at
|
||||
/// its intended size. See [ImageInfo.scale] for more information.
|
||||
const factory NetworkImage(String url, {double scale, Map<String, String>? headers}) =
|
||||
network_image.NetworkImage;
|
||||
///
|
||||
/// The [webHtmlElementStrategy] option is by default
|
||||
/// [WebHtmlElementStrategy.never].
|
||||
const factory NetworkImage(
|
||||
String url, {
|
||||
double scale,
|
||||
Map<String, String>? headers,
|
||||
WebHtmlElementStrategy webHtmlElementStrategy,
|
||||
}) = network_image.NetworkImage;
|
||||
|
||||
/// The URL from which the image will be fetched.
|
||||
String get url;
|
||||
@ -1498,6 +1542,17 @@ abstract class NetworkImage extends ImageProvider<NetworkImage> {
|
||||
/// When running Flutter on the web, headers are not used.
|
||||
Map<String, String>? get headers;
|
||||
|
||||
/// On the Web platform, specifies when the image is loaded as a
|
||||
/// [WebImageInfo], which causes [Image.network] to display the image in an
|
||||
/// HTML element in a platform view.
|
||||
///
|
||||
/// See [Image.network] for more explanation.
|
||||
///
|
||||
/// Defaults to [WebHtmlElementStrategy.never].
|
||||
///
|
||||
/// Has no effect on other platforms, which always fetch bytes.
|
||||
WebHtmlElementStrategy get webHtmlElementStrategy;
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadBuffer(NetworkImage key, DecoderBufferCallback decode);
|
||||
|
||||
|
@ -6,7 +6,7 @@ import '../painting/_web_image_info_io.dart';
|
||||
import 'basic.dart';
|
||||
import 'framework.dart';
|
||||
|
||||
/// A [Widget] that displays an image that is backed by an <img> element.
|
||||
/// A [Widget] that displays an image that is backed by an HTML element.
|
||||
class RawWebImage extends StatelessWidget {
|
||||
/// Creates a [RawWebImage].
|
||||
RawWebImage({
|
||||
@ -22,7 +22,7 @@ class RawWebImage extends StatelessWidget {
|
||||
throw UnsupportedError('Cannot create a $RawWebImage when not running on the web');
|
||||
}
|
||||
|
||||
/// The underlying `<img>` element to be displayed.
|
||||
/// The underlying HTML element to be displayed.
|
||||
final WebImageInfo image;
|
||||
|
||||
/// A debug label explaining the image.
|
||||
@ -34,7 +34,7 @@ class RawWebImage extends StatelessWidget {
|
||||
/// The requested height for this widget.
|
||||
final double? height;
|
||||
|
||||
/// How the `<img>` should be inscribed in the box constraining it.
|
||||
/// How the HTML element should be inscribed in the box constraining it.
|
||||
final BoxFit? fit;
|
||||
|
||||
/// How the image should be aligned in the box constraining it.
|
||||
|
@ -54,7 +54,8 @@ class ImgElementPlatformView extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget which displays and lays out an underlying `<img>` platform view.
|
||||
/// A widget which displays and lays out an underlying HTML element in a
|
||||
/// platform view.
|
||||
class RawWebImage extends SingleChildRenderObjectWidget {
|
||||
/// Creates a [RawWebImage].
|
||||
RawWebImage({
|
||||
@ -68,7 +69,7 @@ class RawWebImage extends SingleChildRenderObjectWidget {
|
||||
this.matchTextDirection = false,
|
||||
}) : super(child: ImgElementPlatformView(image.htmlImage.src));
|
||||
|
||||
/// The underlying `<img>` element to be displayed.
|
||||
/// The underlying HTML element to be displayed.
|
||||
final WebImageInfo image;
|
||||
|
||||
/// A debug label explaining the image.
|
||||
@ -80,7 +81,7 @@ class RawWebImage extends SingleChildRenderObjectWidget {
|
||||
/// The requested height for this widget.
|
||||
final double? height;
|
||||
|
||||
/// How the `<img>` should be inscribed in the box constraining it.
|
||||
/// How the HTML element should be inscribed in the box constraining it.
|
||||
final BoxFit? fit;
|
||||
|
||||
/// How the image should be aligned in the box constraining it.
|
||||
@ -117,7 +118,7 @@ class RawWebImage extends SingleChildRenderObjectWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Lays out and positions the child `<img>` element similarly to [RenderImage].
|
||||
/// Lays out and positions the child HTML element similarly to [RenderImage].
|
||||
class RenderWebImage extends RenderShiftedBox {
|
||||
/// Creates a new [RenderWebImage].
|
||||
RenderWebImage({
|
||||
|
@ -393,6 +393,34 @@ class Image extends StatefulWidget {
|
||||
/// In the case where the network image is on the Web platform, the [cacheWidth]
|
||||
/// and [cacheHeight] parameters are ignored as the web engine delegates
|
||||
/// image decoding to the web which does not support custom decode sizes.
|
||||
///
|
||||
/// ### Same-origin policy on Web
|
||||
///
|
||||
/// Due to browser restriction on Cross-Origin Resource Sharing (CORS),
|
||||
/// Flutter on the Web platform can not fetch images from other origins
|
||||
/// (domain, scheme, or port) than the origin that hosts the app, unless the
|
||||
/// image hosting origin explicitly allows so. CORS errors can be resolved
|
||||
/// by configuring the image hosting server. More information can be
|
||||
/// found at Mozilla's introduction on
|
||||
/// [same-origin policy](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy)
|
||||
/// and
|
||||
/// [CORS errors](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors).
|
||||
///
|
||||
/// If it's not possible to configure the host, such as when images are hosted
|
||||
/// on a CDN or from arbitrary URLs, the app can set the
|
||||
/// `webHtmlElementStrategy` parameter of [Image.network] to display the image
|
||||
/// in an HTML element, which bypasses the same-origin policy.
|
||||
///
|
||||
/// The HTML element is placed in a platform view, and therefore has the
|
||||
/// following drawbacks:
|
||||
///
|
||||
/// * Suboptimal performance.
|
||||
/// * Can't be captured by screenshot widgets.
|
||||
/// * The `headers` argument must be null or empty.
|
||||
/// * Some image options are ignored, including [opacity], [colorBlendMode],
|
||||
/// [repeat], filtering, and blurring.
|
||||
///
|
||||
/// By default, this feature is turned off ([WebHtmlElementStrategy.never]).
|
||||
Image.network(
|
||||
String src, {
|
||||
super.key,
|
||||
@ -418,10 +446,16 @@ class Image extends StatefulWidget {
|
||||
Map<String, String>? headers,
|
||||
int? cacheWidth,
|
||||
int? cacheHeight,
|
||||
WebHtmlElementStrategy webHtmlElementStrategy = WebHtmlElementStrategy.never,
|
||||
}) : image = ResizeImage.resizeIfNeeded(
|
||||
cacheWidth,
|
||||
cacheHeight,
|
||||
NetworkImage(src, scale: scale, headers: headers),
|
||||
NetworkImage(
|
||||
src,
|
||||
scale: scale,
|
||||
headers: headers,
|
||||
webHtmlElementStrategy: webHtmlElementStrategy,
|
||||
),
|
||||
),
|
||||
assert(cacheWidth == null || cacheWidth > 0),
|
||||
assert(cacheHeight == null || cacheHeight > 0);
|
||||
|
@ -97,7 +97,53 @@ void runTests() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('emits a WebImageInfo if the image is cross-origin', (WidgetTester tester) async {
|
||||
testWidgets('When strategy is default, emits an error if the image is cross-origin', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final TestHttpRequest failingRequest =
|
||||
TestHttpRequest()
|
||||
..status = 500
|
||||
..mockEvent = MockEvent('load', web.Event('bytes inaccessible'))
|
||||
..response = (Uint8List.fromList(<int>[])).buffer;
|
||||
|
||||
httpRequestFactory = () {
|
||||
return failingRequest.getMock() as web_shim.XMLHttpRequest;
|
||||
};
|
||||
|
||||
imgElementFactory = () {
|
||||
throw UnimplementedError();
|
||||
};
|
||||
|
||||
const NetworkImage networkImage = NetworkImage('https://www.example.com/images/frame4.png');
|
||||
ImageInfo? imageInfo;
|
||||
Object? recordedError;
|
||||
Completer<void>? imageCompleter;
|
||||
await tester.runAsync(() async {
|
||||
imageCompleter = Completer<void>();
|
||||
final ImageStream stream = networkImage.resolve(ImageConfiguration.empty);
|
||||
stream.addListener(
|
||||
ImageStreamListener(
|
||||
(ImageInfo info, bool isSync) {
|
||||
imageInfo = info;
|
||||
imageCompleter!.complete();
|
||||
},
|
||||
onError: (Object error, StackTrace? stackTrace) {
|
||||
recordedError = error;
|
||||
imageCompleter!.complete();
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
await tester.runAsync(() async {
|
||||
await imageCompleter!.future;
|
||||
});
|
||||
expect(recordedError, isNotNull);
|
||||
expect(imageInfo, isNull);
|
||||
}, skip: !isSkiaWeb);
|
||||
|
||||
testWidgets('When strategy is .fallback, emits a WebImageInfo if the image is cross-origin', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final TestHttpRequest failingRequest =
|
||||
TestHttpRequest()
|
||||
..status = 500
|
||||
@ -113,7 +159,10 @@ void runTests() {
|
||||
return testImg.getMock() as web_shim.HTMLImageElement;
|
||||
};
|
||||
|
||||
const NetworkImage networkImage = NetworkImage('https://www.example.com/images/frame4.png');
|
||||
const NetworkImage networkImage = NetworkImage(
|
||||
'https://www.example.com/images/frame5.png',
|
||||
webHtmlElementStrategy: WebHtmlElementStrategy.fallback,
|
||||
);
|
||||
ImageInfo? imageInfo;
|
||||
Object? recordedError;
|
||||
Completer<void>? imageCompleter;
|
||||
@ -141,28 +190,82 @@ void runTests() {
|
||||
expect(imageInfo, isA<WebImageInfo>());
|
||||
|
||||
final WebImageInfo webImageInfo = imageInfo! as WebImageInfo;
|
||||
expect(webImageInfo.htmlImage.src, equals('https://www.example.com/images/frame4.png'));
|
||||
expect(webImageInfo.htmlImage.src, equals('https://www.example.com/images/frame5.png'));
|
||||
}, skip: !isSkiaWeb);
|
||||
|
||||
testWidgets('emits an error if the image is cross-origin but fails to decode', (
|
||||
testWidgets(
|
||||
'When strategy is .fallback, emits an error if the image is cross-origin but fails to decode',
|
||||
(WidgetTester tester) async {
|
||||
final TestHttpRequest failingRequest =
|
||||
TestHttpRequest()
|
||||
..status = 500
|
||||
..mockEvent = MockEvent('load', web.Event('bytes inaccessible'))
|
||||
..response = (Uint8List.fromList(<int>[])).buffer;
|
||||
final TestImgElement testImg = TestImgElement();
|
||||
|
||||
httpRequestFactory = () {
|
||||
return failingRequest.getMock() as web_shim.XMLHttpRequest;
|
||||
};
|
||||
|
||||
imgElementFactory = () {
|
||||
return testImg.getMock() as web_shim.HTMLImageElement;
|
||||
};
|
||||
|
||||
const NetworkImage networkImage = NetworkImage(
|
||||
'https://www.example.com/images/frame6.png',
|
||||
webHtmlElementStrategy: WebHtmlElementStrategy.fallback,
|
||||
);
|
||||
ImageInfo? imageInfo;
|
||||
Object? recordedError;
|
||||
Completer<void>? imageCompleter;
|
||||
await tester.runAsync(() async {
|
||||
imageCompleter = Completer<void>();
|
||||
final ImageStream stream = networkImage.resolve(ImageConfiguration.empty);
|
||||
stream.addListener(
|
||||
ImageStreamListener(
|
||||
(ImageInfo info, bool isSync) {
|
||||
imageInfo = info;
|
||||
imageCompleter!.complete();
|
||||
},
|
||||
onError: (Object error, StackTrace? stackTrace) {
|
||||
recordedError = error;
|
||||
imageCompleter!.complete();
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
await tester.runAsync(() async {
|
||||
testImg.decodeFailure();
|
||||
await imageCompleter!.future;
|
||||
});
|
||||
expect(recordedError, isNotNull);
|
||||
expect(imageInfo, isNull);
|
||||
},
|
||||
skip: !isSkiaWeb,
|
||||
);
|
||||
|
||||
testWidgets('When strategy is .prefer, emits an WebImageInfo if the image is same-origin', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final TestHttpRequest failingRequest =
|
||||
final TestHttpRequest testHttpRequest =
|
||||
TestHttpRequest()
|
||||
..status = 500
|
||||
..mockEvent = MockEvent('load', web.Event('bytes inaccessible'))
|
||||
..response = (Uint8List.fromList(<int>[])).buffer;
|
||||
..status = 200
|
||||
..mockEvent = MockEvent('load', web.Event('test error'))
|
||||
..response = (Uint8List.fromList(kTransparentImage)).buffer;
|
||||
final TestImgElement testImg = TestImgElement();
|
||||
|
||||
httpRequestFactory = () {
|
||||
return failingRequest.getMock() as web_shim.XMLHttpRequest;
|
||||
return testHttpRequest.getMock() as web_shim.XMLHttpRequest;
|
||||
};
|
||||
|
||||
imgElementFactory = () {
|
||||
return testImg.getMock() as web_shim.HTMLImageElement;
|
||||
};
|
||||
|
||||
const NetworkImage networkImage = NetworkImage('https://www.example.com/images/frame5.png');
|
||||
const NetworkImage networkImage = NetworkImage(
|
||||
'https://www.example.com/images/frame7.png',
|
||||
webHtmlElementStrategy: WebHtmlElementStrategy.prefer,
|
||||
);
|
||||
ImageInfo? imageInfo;
|
||||
Object? recordedError;
|
||||
Completer<void>? imageCompleter;
|
||||
@ -183,11 +286,65 @@ void runTests() {
|
||||
);
|
||||
});
|
||||
await tester.runAsync(() async {
|
||||
testImg.decodeFailure();
|
||||
testImg.decodeSuccess();
|
||||
await imageCompleter!.future;
|
||||
});
|
||||
expect(recordedError, isNotNull);
|
||||
expect(imageInfo, isNull);
|
||||
expect(recordedError, isNull);
|
||||
expect(imageInfo, isA<WebImageInfo>());
|
||||
|
||||
final WebImageInfo webImageInfo = imageInfo! as WebImageInfo;
|
||||
expect(webImageInfo.htmlImage.src, equals('https://www.example.com/images/frame7.png'));
|
||||
}, skip: !isSkiaWeb);
|
||||
|
||||
testWidgets('When strategy is .prefer, emits a normal image if headers is not null', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final TestHttpRequest testHttpRequest =
|
||||
TestHttpRequest()
|
||||
..status = 200
|
||||
..mockEvent = MockEvent('load', web.Event('test error'))
|
||||
..response = (Uint8List.fromList(kTransparentImage)).buffer;
|
||||
final TestImgElement testImg = TestImgElement();
|
||||
|
||||
httpRequestFactory = () {
|
||||
return testHttpRequest.getMock() as web_shim.XMLHttpRequest;
|
||||
};
|
||||
|
||||
imgElementFactory = () {
|
||||
return testImg.getMock() as web_shim.HTMLImageElement;
|
||||
};
|
||||
|
||||
const NetworkImage networkImage = NetworkImage(
|
||||
'https://www.example.com/images/frame8.png',
|
||||
webHtmlElementStrategy: WebHtmlElementStrategy.prefer,
|
||||
headers: <String, String>{'flutter': 'flutter', 'second': 'second'},
|
||||
);
|
||||
ImageInfo? imageInfo;
|
||||
Object? recordedError;
|
||||
Completer<void>? imageCompleter;
|
||||
await tester.runAsync(() async {
|
||||
imageCompleter = Completer<void>();
|
||||
final ImageStream stream = networkImage.resolve(ImageConfiguration.empty);
|
||||
stream.addListener(
|
||||
ImageStreamListener(
|
||||
(ImageInfo info, bool isSync) {
|
||||
imageInfo = info;
|
||||
imageCompleter!.complete();
|
||||
},
|
||||
onError: (Object error, StackTrace? stackTrace) {
|
||||
recordedError = error;
|
||||
imageCompleter!.complete();
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
await tester.runAsync(() async {
|
||||
testImg.decodeSuccess();
|
||||
await imageCompleter!.future;
|
||||
});
|
||||
expect(recordedError, isNull);
|
||||
expect(imageInfo, isNotNull);
|
||||
expect(imageInfo, isNot(isA<WebImageInfo>()));
|
||||
}, skip: !isSkiaWeb);
|
||||
|
||||
testWidgets('Image renders an image using a Platform View if the image info is WebImageInfo', (
|
||||
|
@ -129,10 +129,23 @@ class TestImgElement {
|
||||
int naturalWidth = -1;
|
||||
int naturalHeight = -1;
|
||||
|
||||
late JSFunction _resolveFunc;
|
||||
late JSFunction _rejectFunc;
|
||||
// Either `decode` or `decodeSuccess/Failure` may be called first.
|
||||
// The following fields allow properly handling either case.
|
||||
bool _callbacksAssigned = false;
|
||||
late final JSFunction _resolveFunc;
|
||||
late final JSFunction _rejectFunc;
|
||||
|
||||
bool _resultAssigned = false;
|
||||
late final bool _resultSuccessful;
|
||||
|
||||
JSPromise<JSAny?> decode() {
|
||||
if (_resultAssigned) {
|
||||
return switch (_resultSuccessful) {
|
||||
true => Future<JSAny?>.value().toJS,
|
||||
false => Future<JSAny?>.error(Error()).toJS,
|
||||
};
|
||||
}
|
||||
_callbacksAssigned = true;
|
||||
return JSPromise<JSAny?>(
|
||||
(JSFunction resolveFunc, JSFunction rejectFunc) {
|
||||
_resolveFunc = resolveFunc;
|
||||
@ -142,11 +155,21 @@ class TestImgElement {
|
||||
}
|
||||
|
||||
void decodeSuccess() {
|
||||
_resolveFunc.callAsFunction();
|
||||
if (_callbacksAssigned) {
|
||||
_resolveFunc.callAsFunction();
|
||||
} else {
|
||||
_resultAssigned = true;
|
||||
_resultSuccessful = true;
|
||||
}
|
||||
}
|
||||
|
||||
void decodeFailure() {
|
||||
_rejectFunc.callAsFunction();
|
||||
if (_callbacksAssigned) {
|
||||
_rejectFunc.callAsFunction();
|
||||
} else {
|
||||
_resultAssigned = true;
|
||||
_resultSuccessful = false;
|
||||
}
|
||||
}
|
||||
|
||||
web.HTMLImageElement getMock() => _mock as web.HTMLImageElement;
|
||||
|
Loading…
x
Reference in New Issue
Block a user