From b9e114107ebbd1789ced3967ed0d5150dcd1e727 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Thu, 20 Jul 2023 19:04:02 -0700 Subject: [PATCH] Support web (as JS) --- packages/rfw/CHANGELOG.md | 3 +- packages/rfw/dart_test.yaml | 3 -- packages/rfw/lib/src/dart/binary.dart | 41 ++++++++++++++-- packages/rfw/pubspec.yaml | 2 +- packages/rfw/test/argument_decoders_test.dart | 23 +++++---- packages/rfw/test/binary_test.dart | 47 +++++++++++++++++++ packages/rfw/test/material_widgets_test.dart | 7 +-- packages/rfw/test/text_test.dart | 2 +- packages/rfw/test/utils.dart | 23 +++++++++ 9 files changed, 124 insertions(+), 27 deletions(-) delete mode 100644 packages/rfw/dart_test.yaml create mode 100644 packages/rfw/test/utils.dart diff --git a/packages/rfw/CHANGELOG.md b/packages/rfw/CHANGELOG.md index 7fbd71cdeb1e..cbea927ff62d 100644 --- a/packages/rfw/CHANGELOG.md +++ b/packages/rfw/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 1.0.12 +* Improves web compatibility. * Updates minimum supported SDK version to Flutter 3.7/Dart 2.19. * Adds more testing to restore coverage to 100%. * Removes some dead code. diff --git a/packages/rfw/dart_test.yaml b/packages/rfw/dart_test.yaml deleted file mode 100644 index ae7e45135259..000000000000 --- a/packages/rfw/dart_test.yaml +++ /dev/null @@ -1,3 +0,0 @@ -# TODO(stuartmorgan): Fix the web failures, and enable. See -# https://github.com/flutter/flutter/issues/129843 -test_on: vm diff --git a/packages/rfw/lib/src/dart/binary.dart b/packages/rfw/lib/src/dart/binary.dart index e7242ef18e6f..a062bdd16469 100644 --- a/packages/rfw/lib/src/dart/binary.dart +++ b/packages/rfw/lib/src/dart/binary.dart @@ -245,6 +245,18 @@ Uint8List encodeLibraryBlob(RemoteWidgetLibrary value) { /// ([SetStateHandler.stateReference]), followed by the tagged value to which /// 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: /// /// * [encodeLibraryBlob], which encodes this format. @@ -264,6 +276,10 @@ RemoteWidgetLibrary decodeLibraryBlob(Uint8List bytes) { // endianess used by this format 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 const int _msFalse = 0x00; const int _msTrue = 0x01; @@ -316,7 +332,14 @@ class _BlobDecoder { int _readInt64() { final int byteOffset = _cursor; _advance('int64', 8); - return bytes.getInt64(byteOffset, _blobEndian); + if (_has64Bits) { + 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() { @@ -516,7 +539,19 @@ class _BlobEncoder { final BytesBuilder bytes = BytesBuilder(); // copying builder -- we repeatedly add _scratchOut after changing it void _writeInt64(int value) { - _scratchIn.setInt64(0, value, _blobEndian); + if (_has64Bits) { + _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); } @@ -551,7 +586,7 @@ class _BlobEncoder { bytes.addByte(_msFalse); } else if (value == true) { 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); _scratchIn.setFloat64(0, value, _blobEndian); bytes.add(_scratchOut); diff --git a/packages/rfw/pubspec.yaml b/packages/rfw/pubspec.yaml index 9b3c26153160..019bf5c6eb2a 100644 --- a/packages/rfw/pubspec.yaml +++ b/packages/rfw/pubspec.yaml @@ -2,7 +2,7 @@ name: rfw description: "Remote Flutter widgets: a library for rendering declarative widget description files at runtime." 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 -version: 1.0.11 +version: 1.0.12 environment: sdk: ">=3.0.0 <4.0.0" diff --git a/packages/rfw/test/argument_decoders_test.dart b/packages/rfw/test/argument_decoders_test.dart index 41f60aec83f1..9906cfa142bb 100644 --- a/packages/rfw/test/argument_decoders_test.dart +++ b/packages/rfw/test/argument_decoders_test.dart @@ -4,20 +4,15 @@ // This file is hand-formatted. -import 'dart:io' show Platform; import 'dart:ui' as ui; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:rfw/formats.dart' show parseLibraryFile; import 'package:rfw/rfw.dart'; -final bool masterChannel = - !Platform.environment.containsKey('CHANNEL') || - Platform.environment['CHANNEL'] == 'master'; - -// See Contributing section of README.md file. -final bool runGoldens = Platform.isLinux && masterChannel; +import 'utils.dart'; void main() { testWidgets('String example', (WidgetTester tester) async { @@ -371,19 +366,23 @@ void main() { ); ''')); await tester.pump(); - expect(eventLog, hasLength(1)); - expect(eventLog.first, startsWith('image-error-event {exception: HTTP request failed, statusCode: 400, x-invalid:')); - eventLog.clear(); + if (!kIsWeb) { + expect(eventLog, hasLength(1)); + expect(eventLog.first, startsWith('image-error-event {exception: HTTP request failed, statusCode: 400, x-invalid:')); + eventLog.clear(); + } await expectLater( find.byType(RemoteWidget), matchesGoldenFile('goldens/argument_decoders_test.containers.png'), skip: !runGoldens, ); 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( (tester.widgetList(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... - '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)', ); expect( @@ -543,5 +542,5 @@ void main() { ); expect(eventLog, isEmpty); - }, skip: !masterChannel); // https://github.com/flutter/flutter/pull/129851 + }, skip: kIsWeb || !isMainChannel); // https://github.com/flutter/flutter/pull/129851 } diff --git a/packages/rfw/test/binary_test.dart b/packages/rfw/test/binary_test.dart index 207f7060f487..0eca7888bbe9 100644 --- a/packages/rfw/test/binary_test.dart +++ b/packages/rfw/test/binary_test.dart @@ -9,6 +9,11 @@ import 'dart:typed_data'; import 'package:flutter_test/flutter_test.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() { testWidgets('String example', (WidgetTester tester) async { final Uint8List bytes = encodeDataBlob('Hello'); @@ -18,6 +23,48 @@ void main() { 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, [ 0xfe, 0x52, 0x57, 0x44, 0x02, 0xa5, 0xd7, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00, ]); + final Object value = decodeDataBlob(bytes); + expect(value, isA()); + expect(value, largeNumber); + }); + + testWidgets('Big negative integer example', (WidgetTester tester) async { + final Uint8List bytes = encodeDataBlob(-largeNumber); + expect(bytes, [ 0xfe, 0x52, 0x57, 0x44, 0x02, 0x5b, 0x28, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xff, ]); + final Object value = decodeDataBlob(bytes); + expect(value, isA()); + expect(value, -largeNumber); + }); + + testWidgets('Small integer example', (WidgetTester tester) async { + final Uint8List bytes = encodeDataBlob(1); + expect(bytes, [ 0xfe, 0x52, 0x57, 0x44, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]); + final Object value = decodeDataBlob(bytes); + expect(value, isA()); + expect(value, 1); + }); + + testWidgets('Small negative integer example', (WidgetTester tester) async { + final Uint8List bytes = encodeDataBlob(-1); + expect(bytes, [ 0xfe, 0x52, 0x57, 0x44, 0x02, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, ]); + final Object value = decodeDataBlob(bytes); + expect(value, isA()); + 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, [ 0xfe, 0x52, 0x57, 0x44, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]); + final Object value = decodeDataBlob(bytes); + expect(value, isA()); + expect(value, 0); + }); + testWidgets('Map example', (WidgetTester tester) async { final Uint8List bytes = encodeDataBlob(const { 'a': 15 }); expect(bytes, [ diff --git a/packages/rfw/test/material_widgets_test.dart b/packages/rfw/test/material_widgets_test.dart index 6aecc3c00a13..01aefa9ede04 100644 --- a/packages/rfw/test/material_widgets_test.dart +++ b/packages/rfw/test/material_widgets_test.dart @@ -2,17 +2,12 @@ // 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/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:rfw/formats.dart' show parseLibraryFile; import 'package:rfw/rfw.dart'; -// See Contributing section of README.md file. -final bool runGoldens = Platform.isLinux && - (!Platform.environment.containsKey('CHANNEL') || - Platform.environment['CHANNEL'] == 'master'); +import 'utils.dart'; void main() { testWidgets('Material widgets', (WidgetTester tester) async { diff --git a/packages/rfw/test/text_test.dart b/packages/rfw/test/text_test.dart index bee5f8bd65fc..64ec047491d0 100644 --- a/packages/rfw/test/text_test.dart +++ b/packages/rfw/test/text_test.dart @@ -43,7 +43,7 @@ void main() { test('', 'Expected symbol "{" but found at line 1 column 0.'); test('}', 'Expected symbol "{" but found } 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 3.'); test('&', 'Unexpected character U+0026 ("&") at line 1 column 1.'); diff --git a/packages/rfw/test/utils.dart b/packages/rfw/test/utils.dart new file mode 100644 index 000000000000..a27455cf3e39 --- /dev/null +++ b/packages/rfw/test/utils.dart @@ -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;