Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions lib/web_ui/dev/test_platform.dart
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ class BrowserPlatform extends PlatformPlugin {
// This handler goes last, after all more specific handlers failed to handle the request.
.add(_createAbsolutePackageUrlHandler())
.add(_screenshotHandler)

// Generates and serves a test payload of given length, split into chunks
// of given size. Reponds to requests to /long_test_payload.
.add(_testPayloadGenerator)

// If none of the handlers above handled the request, return 404.
.add(_fileNotFoundCatcher);

server.mount(cascade.handler);
Expand Down Expand Up @@ -320,6 +326,46 @@ class BrowserPlatform extends PlatformPlugin {
};
}

Future<shelf.Response> _testPayloadGenerator(shelf.Request request) async {
if (!request.requestedUri.path.endsWith('/long_test_payload')) {
return shelf.Response.notFound(
'This request is not handled by the test payload generator');
}

final int payloadLength = int.parse(request.requestedUri.queryParameters['length']!);
final int chunkLength = int.parse(request.requestedUri.queryParameters['chunk']!);

final StreamController<List<int>> controller = StreamController<List<int>>();

Future<void> fillPayload() async {
int remainingByteCount = payloadLength;
int byteCounter = 0;
while (remainingByteCount > 0) {
final int currentChunkLength = min(chunkLength, remainingByteCount);
final List<int> chunk = List<int>.generate(
currentChunkLength,
(int i) => (byteCounter + i) & 0xFF,
);
byteCounter = (byteCounter + currentChunkLength) & 0xFF;
remainingByteCount -= currentChunkLength;
controller.add(chunk);
await Future<void>.delayed(const Duration(milliseconds: 100));
}
await controller.close();
}

// Kick off payload filling function but don't block on it. The stream should
// be returned immediately, and the client should receive data in chunks.
unawaited(fillPayload());
return shelf.Response.ok(
controller.stream,
headers: <String, String>{
'Content-Type': 'application/octet-stream',
'Content-Length': '$payloadLength',
},
);
}

Future<shelf.Response> _screenshotHandler(shelf.Request request) async {
if (!request.requestedUri.path.endsWith('/screenshot')) {
return shelf.Response.notFound(
Expand Down
82 changes: 40 additions & 42 deletions lib/web_ui/lib/src/engine/assets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,52 +63,23 @@ class AssetManager {
return Uri.encodeFull('${_baseUrl ?? ''}$assetsDir/$asset');
}

/// Loads an asset and returns the server response.
Future<HttpFetchResponse> loadAsset(String asset) {
return httpFetch(getAssetUrl(asset));
}

/// Loads an asset using an [DomXMLHttpRequest] and returns data as [ByteData].
Future<ByteData> load(String asset) async {
final String url = getAssetUrl(asset);
try {
final DomXMLHttpRequest request =
await domHttpRequest(url, responseType: 'arraybuffer');

final ByteBuffer response = request.response as ByteBuffer;
return response.asByteData();
} catch (e) {
if (!domInstanceOfString(e, 'ProgressEvent')){
rethrow;
}
final DomProgressEvent p = e as DomProgressEvent;
final DomEventTarget? target = p.target;
if (domInstanceOfString(target,'XMLHttpRequest')) {
final DomXMLHttpRequest request = target! as DomXMLHttpRequest;
if (request.status == 404 && asset == 'AssetManifest.json') {
printWarning('Asset manifest does not exist at `$url` – ignoring.');
return Uint8List.fromList(utf8.encode('{}')).buffer.asByteData();
}
throw AssetManagerException(url, request.status!.toInt());
}

final String? constructorName = target == null ? 'null' :
domGetConstructorName(target);
printWarning('Caught ProgressEvent with unknown target: '
'$constructorName');
rethrow;
}
}
}
final HttpFetchResponse response = await httpFetch(url);

/// Thrown to indicate http failure during asset loading.
class AssetManagerException implements Exception {
/// Initializes exception with request url and http status.
AssetManagerException(this.url, this.httpStatus);

/// Http request url for asset.
final String url;

/// Http status of response.
final int httpStatus;
if (response.status == 404 && asset == 'AssetManifest.json') {
printWarning('Asset manifest does not exist at `$url` - ignoring.');
return Uint8List.fromList(utf8.encode('{}')).buffer.asByteData();
}

@override
String toString() => 'Failed to load asset at "$url" ($httpStatus)';
return (await response.payload.asByteBuffer()).asByteData();
}
}

/// An asset manager that gives fake empty responses for assets.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the WebOnlyMockAssetManager class (and in general all the new Mock classes) be moved somewhere under test?

Expand Down Expand Up @@ -141,6 +112,33 @@ class WebOnlyMockAssetManager implements AssetManager {
@override
String getAssetUrl(String asset) => asset;

@override
Future<HttpFetchResponse> loadAsset(String asset) async {
if (asset == getAssetUrl('AssetManifest.json')) {
return MockHttpFetchResponse(
url: asset,
status: 200,
payload: MockHttpFetchPayload(
byteBuffer: _toByteData(utf8.encode(defaultAssetManifest)).buffer,
)
);
}
if (asset == getAssetUrl('FontManifest.json')) {
return MockHttpFetchResponse(
url: asset,
status: 200,
payload: MockHttpFetchPayload(
byteBuffer: _toByteData(utf8.encode(defaultFontManifest)).buffer,
)
);
}

return MockHttpFetchResponse(
url: asset,
status: 404,
);
}

@override
Future<ByteData> load(String asset) {
if (asset == getAssetUrl('AssetManifest.json')) {
Expand All @@ -151,7 +149,7 @@ class WebOnlyMockAssetManager implements AssetManager {
return Future<ByteData>.value(
_toByteData(utf8.encode(defaultFontManifest)));
}
throw AssetManagerException(asset, 404);
throw HttpFetchNoPayloadError(asset, status: 404);
}

ByteData _toByteData(List<int> bytes) {
Expand Down
24 changes: 10 additions & 14 deletions lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -488,37 +488,33 @@ class NotoDownloader {
/// Downloads the [url] and returns it as a [ByteBuffer].
///
/// Override this for testing.
Future<ByteBuffer> downloadAsBytes(String url, {String? debugDescription}) {
Future<ByteBuffer> downloadAsBytes(String url, {String? debugDescription}) async {
if (assertionsEnabled) {
_debugActiveDownloadCount += 1;
}
final Future<ByteBuffer> result = httpFetch(url).then(
(DomResponse fetchResult) => fetchResult
.arrayBuffer()
.then<ByteBuffer>((dynamic x) => x as ByteBuffer));
final Future<ByteBuffer> data = httpFetchByteBuffer(url);
if (assertionsEnabled) {
result.whenComplete(() {
unawaited(data.whenComplete(() {
_debugActiveDownloadCount -= 1;
});
}));
}
return result;
return data;
}

/// Downloads the [url] and returns is as a [String].
///
/// Override this for testing.
Future<String> downloadAsString(String url, {String? debugDescription}) {
Future<String> downloadAsString(String url, {String? debugDescription}) async {
if (assertionsEnabled) {
_debugActiveDownloadCount += 1;
}
final Future<String> result = httpFetch(url).then((DomResponse response) =>
response.text().then<String>((dynamic x) => x as String));
final Future<String> data = httpFetchText(url);
if (assertionsEnabled) {
result.whenComplete(() {
unawaited(data.whenComplete(() {
_debugActiveDownloadCount -= 1;
});
}));
}
return result;
return data;
}
}

Expand Down
31 changes: 10 additions & 21 deletions lib/web_ui/lib/src/engine/canvaskit/fonts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,21 +93,15 @@ class SkiaFontCollection implements FontCollection {
/// Loads fonts from `FontManifest.json`.
@override
Future<void> downloadAssetFonts(AssetManager assetManager) async {
ByteData byteData;
final HttpFetchResponse response = await assetManager.loadAsset('FontManifest.json');

try {
byteData = await assetManager.load('FontManifest.json');
} on AssetManagerException catch (e) {
if (e.httpStatus == 404) {
printWarning('Font manifest does not exist at `${e.url}` – ignoring.');
return;
} else {
rethrow;
}
if (!response.hasPayload) {
printWarning('Font manifest does not exist at `${response.url}` - ignoring.');
return;
}

final List<dynamic>? fontManifest =
json.decode(utf8.decode(byteData.buffer.asUint8List())) as List<dynamic>?;
final Uint8List data = await response.asUint8List();
final List<dynamic>? fontManifest = json.decode(utf8.decode(data)) as List<dynamic>?;
if (fontManifest == null) {
throw AssertionError(
'There was a problem trying to load FontManifest.json');
Expand Down Expand Up @@ -206,10 +200,11 @@ class SkiaFontCollection implements FontCollection {
String family
) {
Future<UnregisteredFont?> downloadFont() async {
ByteBuffer buffer;
// Try to get the font leniently. Do not crash the app when failing to
// fetch the font in the spirit of "gradual degradation of functionality".
try {
buffer = await httpFetch(url).then(_getArrayBuffer);
return UnregisteredFont(buffer, url, family);
final ByteBuffer data = await httpFetchByteBuffer(url);
return UnregisteredFont(data, url, family);
} catch (e) {
printWarning('Failed to load font $family at $url');
printWarning(e.toString());
Expand All @@ -230,12 +225,6 @@ class SkiaFontCollection implements FontCollection {
return actualFamily;
}

Future<ByteBuffer> _getArrayBuffer(DomResponse fetchResult) {
return fetchResult
.arrayBuffer()
.then<ByteBuffer>((dynamic x) => x as ByteBuffer);
}

TypefaceFontProvider? fontProvider;

@override
Expand Down
81 changes: 37 additions & 44 deletions lib/web_ui/lib/src/engine/canvaskit/image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -167,12 +167,6 @@ class ImageCodecException implements Exception {

const String _kNetworkImageMessage = 'Failed to load network image.';

typedef HttpRequestFactory = DomXMLHttpRequest Function();
HttpRequestFactory httpRequestFactory = () => createDomXMLHttpRequest();
void debugRestoreHttpRequestFactory() {
httpRequestFactory = () => createDomXMLHttpRequest();
}

/// Instantiates a [ui.Codec] backed by an `SkAnimatedImage` from Skia after
/// requesting from URI.
Future<ui.Codec> skiaInstantiateWebImageCodec(
Expand All @@ -186,49 +180,48 @@ Future<ui.Codec> skiaInstantiateWebImageCodec(
}

/// Sends a request to fetch image data.
Future<Uint8List> fetchImage(
String url, WebOnlyImageCodecChunkCallback? chunkCallback) {
final Completer<Uint8List> completer = Completer<Uint8List>();

final DomXMLHttpRequest request = httpRequestFactory();
request.open('GET', url, true);
request.responseType = 'arraybuffer';
if (chunkCallback != null) {
request.addEventListener('progress', allowInterop((DomEvent event) {
event = event as DomProgressEvent;
chunkCallback.call(event.loaded!.toInt(), event.total!.toInt());
}));
}

request.addEventListener('error', allowInterop((DomEvent event) {
completer.completeError(ImageCodecException('$_kNetworkImageMessage\n'
Future<Uint8List> fetchImage(String url, WebOnlyImageCodecChunkCallback? chunkCallback) async {
try {
final HttpFetchResponse response = await httpFetch(url);
final int? contentLength = response.contentLength;

if (!response.hasPayload) {
throw ImageCodecException(
'$_kNetworkImageMessage\n'
'Image URL: $url\n'
'Trying to load an image from another domain? Find answers at:\n'
'https://flutter.dev/docs/development/platform-integration/web-images'));
}));

request.addEventListener('load', allowInterop((DomEvent event) {
final int status = request.status!.toInt();
final bool accepted = status >= 200 && status < 300;
final bool fileUri = status == 0; // file:// URIs have status of 0.
final bool notModified = status == 304;
final bool unknownRedirect = status > 307 && status < 400;
final bool success = accepted || fileUri || notModified || unknownRedirect;

if (!success) {
completer.completeError(
ImageCodecException('$_kNetworkImageMessage\n'
'Image URL: $url\n'
'Server response code: $status'),
'Server response code: ${response.status}',
);
return;
}

completer.complete(Uint8List.view(request.response as ByteBuffer));
}));
if (chunkCallback != null && contentLength != null) {
return readChunked(response.payload, contentLength, chunkCallback);
} else {
return await response.asUint8List();
}
} on HttpFetchError catch (_) {
throw ImageCodecException(
'$_kNetworkImageMessage\n'
'Image URL: $url\n'
'Trying to load an image from another domain? Find answers at:\n'
'https://flutter.dev/docs/development/platform-integration/web-images',
);
}
}

request.send();
return completer.future;
/// Reads the [payload] in chunks using the browser's Streams API
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API
Future<Uint8List> readChunked(HttpFetchPayload payload, int contentLength, WebOnlyImageCodecChunkCallback chunkCallback) async {
final Uint8List result = Uint8List(contentLength);
int position = 0;
int cumulativeBytesLoaded = 0;
await payload.read<Uint8List>((Uint8List chunk) {
cumulativeBytesLoaded += chunk.lengthInBytes;
chunkCallback(cumulativeBytesLoaded, contentLength);
result.setAll(position, chunk);
position += chunk.lengthInBytes;
});
return result;
}

/// A [ui.Image] backed by an `SkImage` from Skia.
Expand Down
Loading