mirror of
https://github.com/flutter/packages.git
synced 2025-06-28 13:47:29 +08:00
[rfw] Support web (as JS) (#4650)
Fixes https://github.com/flutter/flutter/issues/129843
This commit is contained in:
@ -1,5 +1,6 @@
|
|||||||
## NEXT
|
## 1.0.12
|
||||||
|
|
||||||
|
* Improves web compatibility.
|
||||||
* Updates minimum supported SDK version to Flutter 3.7/Dart 2.19.
|
* Updates minimum supported SDK version to Flutter 3.7/Dart 2.19.
|
||||||
* Adds more testing to restore coverage to 100%.
|
* Adds more testing to restore coverage to 100%.
|
||||||
* Removes some dead code.
|
* Removes some dead code.
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
# TODO(stuartmorgan): Fix the web failures, and enable. See
|
|
||||||
# https://github.com/flutter/flutter/issues/129843
|
|
||||||
test_on: vm
|
|
@ -245,6 +245,18 @@ Uint8List encodeLibraryBlob(RemoteWidgetLibrary value) {
|
|||||||
/// ([SetStateHandler.stateReference]), followed by the tagged value to which
|
/// ([SetStateHandler.stateReference]), followed by the tagged value to which
|
||||||
/// to set that state entry ([SetStateHandler.value]).
|
/// to set that state entry ([SetStateHandler.value]).
|
||||||
///
|
///
|
||||||
|
/// ## Limitations
|
||||||
|
///
|
||||||
|
/// JavaScript does not have a native integer type; all numbers are stored as
|
||||||
|
/// [double]s. Data loss may therefore occur when handling integers that cannot
|
||||||
|
/// be completely represented as a [binary64] floating point number.
|
||||||
|
///
|
||||||
|
/// Integers are used for two purposes in this format; as a length, for which it
|
||||||
|
/// is extremely unlikely that numbers above 2^53 would be practical anyway, and
|
||||||
|
/// for representing integer literals. Thus, when using RFW with JavaScript
|
||||||
|
/// environments, it is recommended to use [double]s instead of [int]s whenever
|
||||||
|
/// possible, to avoid accidental data loss.
|
||||||
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * [encodeLibraryBlob], which encodes this format.
|
/// * [encodeLibraryBlob], which encodes this format.
|
||||||
@ -264,6 +276,10 @@ RemoteWidgetLibrary decodeLibraryBlob(Uint8List bytes) {
|
|||||||
// endianess used by this format
|
// endianess used by this format
|
||||||
const Endian _blobEndian = Endian.little;
|
const Endian _blobEndian = Endian.little;
|
||||||
|
|
||||||
|
// whether we can use 64 bit APIs on this platform
|
||||||
|
// (on JS, we can only use 32 bit APIs and integers only go up to ~2^53)
|
||||||
|
const bool _has64Bits = 0x1000000000000000 + 1 != 0x1000000000000000; // 2^60
|
||||||
|
|
||||||
// magic signatures
|
// magic signatures
|
||||||
const int _msFalse = 0x00;
|
const int _msFalse = 0x00;
|
||||||
const int _msTrue = 0x01;
|
const int _msTrue = 0x01;
|
||||||
@ -316,8 +332,15 @@ class _BlobDecoder {
|
|||||||
int _readInt64() {
|
int _readInt64() {
|
||||||
final int byteOffset = _cursor;
|
final int byteOffset = _cursor;
|
||||||
_advance('int64', 8);
|
_advance('int64', 8);
|
||||||
|
if (_has64Bits) {
|
||||||
return bytes.getInt64(byteOffset, _blobEndian);
|
return bytes.getInt64(byteOffset, _blobEndian);
|
||||||
}
|
}
|
||||||
|
// We use multiplication rather than bit shifts because << truncates to 32 bits when compiled to JS:
|
||||||
|
// https://dart.dev/guides/language/numbers#bitwise-operations
|
||||||
|
final int a = bytes.getUint32(byteOffset, _blobEndian);
|
||||||
|
final int b = bytes.getInt32(byteOffset + 4, _blobEndian);
|
||||||
|
return a + (b * 0x100000000);
|
||||||
|
}
|
||||||
|
|
||||||
double _readBinary64() {
|
double _readBinary64() {
|
||||||
final int byteOffset = _cursor;
|
final int byteOffset = _cursor;
|
||||||
@ -516,7 +539,19 @@ class _BlobEncoder {
|
|||||||
final BytesBuilder bytes = BytesBuilder(); // copying builder -- we repeatedly add _scratchOut after changing it
|
final BytesBuilder bytes = BytesBuilder(); // copying builder -- we repeatedly add _scratchOut after changing it
|
||||||
|
|
||||||
void _writeInt64(int value) {
|
void _writeInt64(int value) {
|
||||||
|
if (_has64Bits) {
|
||||||
_scratchIn.setInt64(0, value, _blobEndian);
|
_scratchIn.setInt64(0, value, _blobEndian);
|
||||||
|
} else {
|
||||||
|
// We use division rather than bit shifts because >> truncates to 32 bits when compiled to JS:
|
||||||
|
// https://dart.dev/guides/language/numbers#bitwise-operations
|
||||||
|
if (value >= 0) {
|
||||||
|
_scratchIn.setInt32(0, value, _blobEndian);
|
||||||
|
_scratchIn.setInt32(4, value ~/ 0x100000000, _blobEndian);
|
||||||
|
} else {
|
||||||
|
_scratchIn.setInt32(0, value, _blobEndian);
|
||||||
|
_scratchIn.setInt32(4, -((-value) ~/ 0x100000000 + 1), _blobEndian);
|
||||||
|
}
|
||||||
|
}
|
||||||
bytes.add(_scratchOut);
|
bytes.add(_scratchOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -551,7 +586,7 @@ class _BlobEncoder {
|
|||||||
bytes.addByte(_msFalse);
|
bytes.addByte(_msFalse);
|
||||||
} else if (value == true) {
|
} else if (value == true) {
|
||||||
bytes.addByte(_msTrue);
|
bytes.addByte(_msTrue);
|
||||||
} else if (value is double) {
|
} else if (value is double && value is! int) { // When compiled to JS, a Number can be both.
|
||||||
bytes.addByte(_msBinary64);
|
bytes.addByte(_msBinary64);
|
||||||
_scratchIn.setFloat64(0, value, _blobEndian);
|
_scratchIn.setFloat64(0, value, _blobEndian);
|
||||||
bytes.add(_scratchOut);
|
bytes.add(_scratchOut);
|
||||||
|
@ -2,7 +2,7 @@ name: rfw
|
|||||||
description: "Remote Flutter widgets: a library for rendering declarative widget description files at runtime."
|
description: "Remote Flutter widgets: a library for rendering declarative widget description files at runtime."
|
||||||
repository: https://github.com/flutter/packages/tree/main/packages/rfw
|
repository: https://github.com/flutter/packages/tree/main/packages/rfw
|
||||||
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+rfw%22
|
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+rfw%22
|
||||||
version: 1.0.11
|
version: 1.0.12
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.0.0 <4.0.0"
|
sdk: ">=3.0.0 <4.0.0"
|
||||||
|
@ -4,20 +4,15 @@
|
|||||||
|
|
||||||
// This file is hand-formatted.
|
// This file is hand-formatted.
|
||||||
|
|
||||||
import 'dart:io' show Platform;
|
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:rfw/formats.dart' show parseLibraryFile;
|
import 'package:rfw/formats.dart' show parseLibraryFile;
|
||||||
import 'package:rfw/rfw.dart';
|
import 'package:rfw/rfw.dart';
|
||||||
|
|
||||||
final bool masterChannel =
|
import 'utils.dart';
|
||||||
!Platform.environment.containsKey('CHANNEL') ||
|
|
||||||
Platform.environment['CHANNEL'] == 'master';
|
|
||||||
|
|
||||||
// See Contributing section of README.md file.
|
|
||||||
final bool runGoldens = Platform.isLinux && masterChannel;
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('String example', (WidgetTester tester) async {
|
testWidgets('String example', (WidgetTester tester) async {
|
||||||
@ -371,19 +366,23 @@ void main() {
|
|||||||
);
|
);
|
||||||
'''));
|
'''));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
if (!kIsWeb) {
|
||||||
expect(eventLog, hasLength(1));
|
expect(eventLog, hasLength(1));
|
||||||
expect(eventLog.first, startsWith('image-error-event {exception: HTTP request failed, statusCode: 400, x-invalid:'));
|
expect(eventLog.first, startsWith('image-error-event {exception: HTTP request failed, statusCode: 400, x-invalid:'));
|
||||||
eventLog.clear();
|
eventLog.clear();
|
||||||
|
}
|
||||||
await expectLater(
|
await expectLater(
|
||||||
find.byType(RemoteWidget),
|
find.byType(RemoteWidget),
|
||||||
matchesGoldenFile('goldens/argument_decoders_test.containers.png'),
|
matchesGoldenFile('goldens/argument_decoders_test.containers.png'),
|
||||||
skip: !runGoldens,
|
skip: !runGoldens,
|
||||||
);
|
);
|
||||||
expect(find.byType(DecoratedBox), findsNWidgets(6));
|
expect(find.byType(DecoratedBox), findsNWidgets(6));
|
||||||
|
const String matrix = kIsWeb ? '1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1'
|
||||||
|
: '1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0';
|
||||||
expect(
|
expect(
|
||||||
(tester.widgetList<DecoratedBox>(find.byType(DecoratedBox)).toList()[1].decoration as BoxDecoration).image.toString(),
|
(tester.widgetList<DecoratedBox>(find.byType(DecoratedBox)).toList()[1].decoration as BoxDecoration).image.toString(),
|
||||||
'DecorationImage(AssetImage(bundle: null, name: "asset"), ' // this just seemed like the easiest way to check all this...
|
'DecorationImage(AssetImage(bundle: null, name: "asset"), ' // this just seemed like the easiest way to check all this...
|
||||||
'ColorFilter.matrix([1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]), '
|
'ColorFilter.matrix([$matrix]), '
|
||||||
'Alignment.center, centerSlice: Rect.fromLTRB(5.0, 8.0, 105.0, 78.0), scale 1.0, opacity 1.0, FilterQuality.low)',
|
'Alignment.center, centerSlice: Rect.fromLTRB(5.0, 8.0, 105.0, 78.0), scale 1.0, opacity 1.0, FilterQuality.low)',
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
@ -543,5 +542,5 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(eventLog, isEmpty);
|
expect(eventLog, isEmpty);
|
||||||
}, skip: !masterChannel); // https://github.com/flutter/flutter/pull/129851
|
}, skip: kIsWeb || !isMainChannel); // https://github.com/flutter/flutter/pull/129851
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,11 @@ import 'dart:typed_data';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:rfw/formats.dart';
|
import 'package:rfw/formats.dart';
|
||||||
|
|
||||||
|
// This is a number that requires more than 32 bits but less than 53 bits, so
|
||||||
|
// that it works in a JS Number and tests the logic that parses 64 bit ints as
|
||||||
|
// two separate 32 bit ints.
|
||||||
|
const int largeNumber = 9007199254730661;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('String example', (WidgetTester tester) async {
|
testWidgets('String example', (WidgetTester tester) async {
|
||||||
final Uint8List bytes = encodeDataBlob('Hello');
|
final Uint8List bytes = encodeDataBlob('Hello');
|
||||||
@ -18,6 +23,48 @@ void main() {
|
|||||||
expect(value, 'Hello');
|
expect(value, 'Hello');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Big integer example', (WidgetTester tester) async {
|
||||||
|
// This value is intentionally inside the JS Number range but above 2^32.
|
||||||
|
final Uint8List bytes = encodeDataBlob(largeNumber);
|
||||||
|
expect(bytes, <int>[ 0xfe, 0x52, 0x57, 0x44, 0x02, 0xa5, 0xd7, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00, ]);
|
||||||
|
final Object value = decodeDataBlob(bytes);
|
||||||
|
expect(value, isA<int>());
|
||||||
|
expect(value, largeNumber);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Big negative integer example', (WidgetTester tester) async {
|
||||||
|
final Uint8List bytes = encodeDataBlob(-largeNumber);
|
||||||
|
expect(bytes, <int>[ 0xfe, 0x52, 0x57, 0x44, 0x02, 0x5b, 0x28, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xff, ]);
|
||||||
|
final Object value = decodeDataBlob(bytes);
|
||||||
|
expect(value, isA<int>());
|
||||||
|
expect(value, -largeNumber);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Small integer example', (WidgetTester tester) async {
|
||||||
|
final Uint8List bytes = encodeDataBlob(1);
|
||||||
|
expect(bytes, <int>[ 0xfe, 0x52, 0x57, 0x44, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]);
|
||||||
|
final Object value = decodeDataBlob(bytes);
|
||||||
|
expect(value, isA<int>());
|
||||||
|
expect(value, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Small negative integer example', (WidgetTester tester) async {
|
||||||
|
final Uint8List bytes = encodeDataBlob(-1);
|
||||||
|
expect(bytes, <int>[ 0xfe, 0x52, 0x57, 0x44, 0x02, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, ]);
|
||||||
|
final Object value = decodeDataBlob(bytes);
|
||||||
|
expect(value, isA<int>());
|
||||||
|
expect(value, -1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Zero integer example', (WidgetTester tester) async {
|
||||||
|
// This value is intentionally inside the JS Number range but above 2^32.
|
||||||
|
final Uint8List bytes = encodeDataBlob(0);
|
||||||
|
expect(bytes, <int>[ 0xfe, 0x52, 0x57, 0x44, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]);
|
||||||
|
final Object value = decodeDataBlob(bytes);
|
||||||
|
expect(value, isA<int>());
|
||||||
|
expect(value, 0);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Map example', (WidgetTester tester) async {
|
testWidgets('Map example', (WidgetTester tester) async {
|
||||||
final Uint8List bytes = encodeDataBlob(const <String, Object?>{ 'a': 15 });
|
final Uint8List bytes = encodeDataBlob(const <String, Object?>{ 'a': 15 });
|
||||||
expect(bytes, <int>[
|
expect(bytes, <int>[
|
||||||
|
@ -2,17 +2,12 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'dart:io' show Platform;
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:rfw/formats.dart' show parseLibraryFile;
|
import 'package:rfw/formats.dart' show parseLibraryFile;
|
||||||
import 'package:rfw/rfw.dart';
|
import 'package:rfw/rfw.dart';
|
||||||
|
|
||||||
// See Contributing section of README.md file.
|
import 'utils.dart';
|
||||||
final bool runGoldens = Platform.isLinux &&
|
|
||||||
(!Platform.environment.containsKey('CHANNEL') ||
|
|
||||||
Platform.environment['CHANNEL'] == 'master');
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('Material widgets', (WidgetTester tester) async {
|
testWidgets('Material widgets', (WidgetTester tester) async {
|
||||||
|
@ -43,7 +43,7 @@ void main() {
|
|||||||
test('', 'Expected symbol "{" but found <EOF> at line 1 column 0.');
|
test('', 'Expected symbol "{" but found <EOF> at line 1 column 0.');
|
||||||
test('}', 'Expected symbol "{" but found } at line 1 column 1.');
|
test('}', 'Expected symbol "{" but found } at line 1 column 1.');
|
||||||
test('1', 'Expected symbol "{" but found 1 at line 1 column 1.');
|
test('1', 'Expected symbol "{" but found 1 at line 1 column 1.');
|
||||||
test('1.0', 'Expected symbol "{" but found 1.0 at line 1 column 3.');
|
test('1.2', 'Expected symbol "{" but found 1.2 at line 1 column 3.');
|
||||||
test('a', 'Expected symbol "{" but found a at line 1 column 1.');
|
test('a', 'Expected symbol "{" but found a at line 1 column 1.');
|
||||||
test('"a"', 'Expected symbol "{" but found "a" at line 1 column 3.');
|
test('"a"', 'Expected symbol "{" but found "a" at line 1 column 3.');
|
||||||
test('&', 'Unexpected character U+0026 ("&") at line 1 column 1.');
|
test('&', 'Unexpected character U+0026 ("&") at line 1 column 1.');
|
||||||
|
23
packages/rfw/test/utils.dart
Normal file
23
packages/rfw/test/utils.dart
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:io' show Platform;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
// Detects if we're running the tests on the main channel.
|
||||||
|
//
|
||||||
|
// This is useful for _tests_ that depend on _Flutter_ features that have not
|
||||||
|
// yet rolled to stable. Avoid using this to skip tests of _RFW_ features that
|
||||||
|
// aren't compatible with stable. Those should wait until the stable release
|
||||||
|
// channel is updated so that RFW can be compatible with it.
|
||||||
|
bool get isMainChannel {
|
||||||
|
assert(!kIsWeb, 'isMainChannel is not available on web');
|
||||||
|
return !Platform.environment.containsKey('CHANNEL') ||
|
||||||
|
Platform.environment['CHANNEL'] == 'main' ||
|
||||||
|
Platform.environment['CHANNEL'] == 'master';
|
||||||
|
}
|
||||||
|
|
||||||
|
// See Contributing section of README.md file.
|
||||||
|
final bool runGoldens = !kIsWeb && Platform.isLinux && isMainChannel;
|
Reference in New Issue
Block a user