diff --git a/dev/bots/test.dart b/dev/bots/test.dart index 37ff25b3c9..9a9ad5f549 100644 --- a/dev/bots/test.dart +++ b/dev/bots/test.dart @@ -59,13 +59,13 @@ const int kWebShardCount = 8; const List kWebTestFileBlacklist = [ // This test doesn't compile because it depends on code outside the flutter package. 'test/examples/sector_layout_test.dart', + // This test relies on widget tracking capability in the VM. + 'test/widgets/widget_inspector_test.dart', + 'test/widgets/selectable_text_test.dart', 'test/widgets/color_filter_test.dart', 'test/widgets/editable_text_cursor_test.dart', - 'test/widgets/raw_keyboard_listener_test.dart', 'test/widgets/editable_text_test.dart', - 'test/widgets/widget_inspector_test.dart', - 'test/widgets/shortcuts_test.dart', 'test/material/animated_icons_private_test.dart', 'test/material/text_form_field_test.dart', 'test/material/data_table_test.dart', diff --git a/packages/flutter/lib/src/widgets/shortcuts.dart b/packages/flutter/lib/src/widgets/shortcuts.dart index 104ffe23b8..df92ebf350 100644 --- a/packages/flutter/lib/src/widgets/shortcuts.dart +++ b/packages/flutter/lib/src/widgets/shortcuts.dart @@ -79,9 +79,6 @@ class KeySet { /// Returns an unmodifiable view of the [KeyboardKey]s in this [KeySet]. Set get keys => UnmodifiableSetView(_keys); - // This needs to be a hash set to be sure that the hashCode accessor returns - // consistent results. LinkedHashSet (the default Set implementation) depends - // upon insertion order, and HashSet does not. final HashSet _keys; @override @@ -93,9 +90,58 @@ class KeySet { && setEquals(other._keys, _keys); } + // Arrays used to temporarily store hash codes for sorting. + static final List _tempHashStore3 = [0, 0, 0]; // used to sort exactly 3 keys + static final List _tempHashStore4 = [0, 0, 0, 0]; // used to sort exactly 4 keys + + // Cached hash code value. Improves [hashCode] performance by 27%-900%, + // depending on key set size and read/write ratio. + int _hashCode; + @override int get hashCode { - return hashList(_keys); + // Return cached hash code if available. + if (_hashCode != null) { + return _hashCode; + } + + // Compute order-independent hash and cache it. + final int length = _keys.length; + final Iterator iterator = _keys.iterator; + + // There's always at least one key. Just extract it. + iterator.moveNext(); + final int h1 = iterator.current.hashCode; + + if (length == 1) { + // Don't do anything fancy if there's exactly one key. + return _hashCode = h1; + } + + iterator.moveNext(); + final int h2 = iterator.current.hashCode; + if (length == 2) { + // No need to sort if there's two keys, just compare them. + return _hashCode = h1 < h2 + ? hashValues(h1, h2) + : hashValues(h2, h1); + } + + // Sort key hash codes and feed to hashList to ensure the aggregate + // hash code does not depend on the key order. + final List sortedHashes = length == 3 + ? _tempHashStore3 + : _tempHashStore4; + sortedHashes[0] = h1; + sortedHashes[1] = h2; + iterator.moveNext(); + sortedHashes[2] = iterator.current.hashCode; + if (length == 4) { + iterator.moveNext(); + sortedHashes[3] = iterator.current.hashCode; + } + sortedHashes.sort(); + return _hashCode = hashList(sortedHashes); } } diff --git a/packages/flutter/test/material/input_decorator_test.dart b/packages/flutter/test/material/input_decorator_test.dart index c8cf1c072a..7c84b50cf0 100644 --- a/packages/flutter/test/material/input_decorator_test.dart +++ b/packages/flutter/test/material/input_decorator_test.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@TestOn('!chrome') // needs substantial triage. import 'dart:async'; import 'package:flutter/material.dart'; @@ -2627,7 +2626,7 @@ void main() { expect(tester.getTopLeft(find.text('text')).dy, 16.0); expect(getBorderBottom(tester), kMinInteractiveDimension); // 40 bumped up to minimum. expect(getBorderWeight(tester), 1.0); - }, skip: isBrowser); + }); testWidgets('InputDecorator.collapsed', (WidgetTester tester) async { await tester.pumpWidget( @@ -2667,7 +2666,7 @@ void main() { expect(tester.getSize(find.text('hint')).height, 16.0); expect(tester.getTopLeft(find.text('hint')).dy, 16.0); expect(getBorderWeight(tester), 0.0); - }, skip: isBrowser); + }); testWidgets('InputDecorator with baseStyle', (WidgetTester tester) async { // Setting the baseStyle of the InputDecoration and the style of the input @@ -2708,7 +2707,7 @@ void main() { expect(tester.getTopLeft(find.text('hint')).dy, 24.75); expect(tester.getTopLeft(find.text('label')).dy, 19.0); expect(tester.getTopLeft(find.text('text')).dy, 24.75); - }, skip: isBrowser); + }); testWidgets('InputDecorator with empty style overrides', (WidgetTester tester) async { // Same as not specifying any style overrides @@ -2750,7 +2749,7 @@ void main() { expect(getBorderWeight(tester), 1.0); expect(tester.getTopLeft(find.text('helper')), const Offset(12.0, 64.0)); expect(tester.getTopRight(find.text('counter')), const Offset(788.0, 64.0)); - }, skip: isBrowser); + }); testWidgets('InputDecoration outline shape with no border and no floating placeholder', (WidgetTester tester) async { await tester.pumpWidget( @@ -2804,7 +2803,7 @@ void main() { // The label should not be seen. expect(getOpacity(tester, 'label'), 0.0); - }, skip: isBrowser); + }); test('InputDecorationTheme copyWith, ==, hashCode basics', () { expect(const InputDecorationTheme(), const InputDecorationTheme().copyWith()); @@ -2850,7 +2849,7 @@ void main() { expect(tester.getBottomLeft(find.text('label')).dy, 36.0); expect(getBorderBottom(tester), 56.0); expect(getBorderWeight(tester), 1.0); - }, skip: isBrowser); + }); testWidgets('InputDecorationTheme outline border, dense layout', (WidgetTester tester) async { await tester.pumpWidget( @@ -3670,7 +3669,7 @@ void main() { ) ..restore(), ); - }); + }, skip: isBrowser); testWidgets('OutlineInputBorder radius carries over when lerping', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/23982 @@ -3880,6 +3879,6 @@ void main() { expect(tester.getTopLeft(find.text('hint')).dy, 28.75); // Ideographic (incorrect) value is 50.299999713897705 - expect(tester.getBottomLeft(find.text('hint')).dy, 47.75); + expect(tester.getBottomLeft(find.text('hint')).dy, isBrowser ? 45.75 : 47.75); }); } diff --git a/packages/flutter/test/widgets/app_title_test.dart b/packages/flutter/test/widgets/app_title_test.dart index 837b9208d4..8548a6cf31 100644 --- a/packages/flutter/test/widgets/app_title_test.dart +++ b/packages/flutter/test/widgets/app_title_test.dart @@ -55,6 +55,6 @@ void main() { await tester.pump(); expect(tester.widget(find.byType(Title)).title, 'en_US'); expect(tester.widget<Title>(find.byType(Title)).color, kTitleColor); - }, skip: isBrowser); + }); } diff --git a/packages/flutter/test/widgets/list_view_test.dart b/packages/flutter/test/widgets/list_view_test.dart index 22abadf0f7..7696761a8d 100644 --- a/packages/flutter/test/widgets/list_view_test.dart +++ b/packages/flutter/test/widgets/list_view_test.dart @@ -212,7 +212,7 @@ void main() { await tester.pumpWidget(_StatefulListView((int i) => i % 3 == 0)); await checkAndScroll('0:true'); - }, skip: isBrowser); + }); testWidgets('ListView can build out of underflow', (WidgetTester tester) async { await tester.pumpWidget( diff --git a/packages/flutter/test/widgets/opacity_test.dart b/packages/flutter/test/widgets/opacity_test.dart index 46e3ca4d7e..044a51fd99 100644 --- a/packages/flutter/test/widgets/opacity_test.dart +++ b/packages/flutter/test/widgets/opacity_test.dart @@ -191,5 +191,5 @@ void main() { // empty opacity layer is sent. final OffsetLayer offsetLayer = element.renderObject.debugLayer as OffsetLayer; await offsetLayer.toImage(const Rect.fromLTRB(0.0, 0.0, 1.0, 1.0)); - }, skip: isBrowser); + }, skip: isBrowser); // https://github.com/flutter/flutter/issues/52856 } diff --git a/packages/flutter/test/widgets/physical_model_test.dart b/packages/flutter/test/widgets/physical_model_test.dart index 13fc56113a..0991e36da2 100644 --- a/packages/flutter/test/widgets/physical_model_test.dart +++ b/packages/flutter/test/widgets/physical_model_test.dart @@ -277,7 +277,7 @@ void main() { await _testStackChildren(tester, children, expectedErrorCount: 0); expect(find.byType(Material), findsNWidgets(2)); - }, skip: isBrowser); + }, skip: isBrowser); // https://github.com/flutter/flutter/issues/52855 // Tests: // @@ -484,7 +484,7 @@ void main() { await _testStackChildren(tester, children, expectedErrorCount: 0); expect(find.byType(Material), findsNWidgets(2)); - }, skip: isBrowser); + }, skip: isBrowser); // https://github.com/flutter/flutter/issues/52855 // Tests: // diff --git a/packages/flutter/test/widgets/scrollable_fling_test.dart b/packages/flutter/test/widgets/scrollable_fling_test.dart index 4a3744bd77..87515f36a3 100644 --- a/packages/flutter/test/widgets/scrollable_fling_test.dart +++ b/packages/flutter/test/widgets/scrollable_fling_test.dart @@ -119,7 +119,7 @@ void main() { await tester.tap(find.byType(Scrollable)); await tester.pump(const Duration(milliseconds: 50)); expect(log, equals(<String>['tap 21', 'tap 35'])); - }, skip: isBrowser); + }); testWidgets('fling and wait and tap', (WidgetTester tester) async { final List<String> log = <String>[]; @@ -148,5 +148,5 @@ void main() { await tester.tap(find.byType(Scrollable)); await tester.pump(const Duration(milliseconds: 50)); expect(log, equals(<String>['tap 21', 'tap 48'])); - }, skip: isBrowser); + }); } diff --git a/packages/flutter/test/widgets/scrollable_semantics_test.dart b/packages/flutter/test/widgets/scrollable_semantics_test.dart index 45b3fc4441..d7f992f4c7 100644 --- a/packages/flutter/test/widgets/scrollable_semantics_test.dart +++ b/packages/flutter/test/widgets/scrollable_semantics_test.dart @@ -253,7 +253,7 @@ void main() { )); semantics.dispose(); - }, skip: isBrowser); + }); testWidgets('correct scrollProgress for unbound', (WidgetTester tester) async { semantics = SemanticsTester(tester); diff --git a/packages/flutter/test/widgets/semantics_clipping_test.dart b/packages/flutter/test/widgets/semantics_clipping_test.dart index da6145662e..c1811b10f8 100644 --- a/packages/flutter/test/widgets/semantics_clipping_test.dart +++ b/packages/flutter/test/widgets/semantics_clipping_test.dart @@ -63,7 +63,7 @@ void main() { )); semantics.dispose(); - }, skip: isBrowser); + }); testWidgets('SemanticsNode is not removed if out of bounds and merged into something within bounds', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); @@ -124,5 +124,5 @@ void main() { )); semantics.dispose(); - }, skip: isBrowser); + }); } diff --git a/packages/flutter/test/widgets/semantics_test.dart b/packages/flutter/test/widgets/semantics_test.dart index 2803695d0d..bc31ebed05 100644 --- a/packages/flutter/test/widgets/semantics_test.dart +++ b/packages/flutter/test/widgets/semantics_test.dart @@ -540,7 +540,7 @@ void main() { expect(semantics, hasSemantics(expectedSemantics, ignoreId: true)); semantics.dispose(); - }, skip: isBrowser); + }); testWidgets('Actions can be replaced without triggering semantics update', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); diff --git a/packages/flutter/test/widgets/shape_decoration_test.dart b/packages/flutter/test/widgets/shape_decoration_test.dart index 87a93258e9..ab48e48517 100644 --- a/packages/flutter/test/widgets/shape_decoration_test.dart +++ b/packages/flutter/test/widgets/shape_decoration_test.dart @@ -59,7 +59,7 @@ Future<void> main() async { ..rect(color: Colors.black) ..rect(color: Colors.white), ); - }, skip: isBrowser); + }); test('ShapeDecoration with BorderDirectional', () { const ShapeDecoration decoration = ShapeDecoration( diff --git a/packages/flutter/test/widgets/shortcuts_test.dart b/packages/flutter/test/widgets/shortcuts_test.dart index c974cf0151..f5231bec0a 100644 --- a/packages/flutter/test/widgets/shortcuts_test.dart +++ b/packages/flutter/test/widgets/shortcuts_test.dart @@ -132,8 +132,8 @@ void main() { final Map<LogicalKeySet, String> map = <LogicalKeySet, String>{set1: 'one'}; expect(set2 == set3, isTrue); expect(set2 == set4, isTrue); - expect(set2.hashCode == set3.hashCode, isTrue); - expect(set2.hashCode == set4.hashCode, isTrue); + expect(set2.hashCode, set3.hashCode); + expect(set2.hashCode, set4.hashCode); expect(map.containsKey(set1), isTrue); expect(map.containsKey(LogicalKeySet(LogicalKeyboardKey.keyA)), isTrue); expect( @@ -146,6 +146,40 @@ void main() { })), ); }); + + test('LogicalKeySet.hashCode is stable', () { + final LogicalKeySet set1 = LogicalKeySet(LogicalKeyboardKey.keyA); + expect(set1.hashCode, set1.hashCode); + + final LogicalKeySet set2 = LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB); + expect(set2.hashCode, set2.hashCode); + + final LogicalKeySet set3 = LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC); + expect(set3.hashCode, set3.hashCode); + + final LogicalKeySet set4 = LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyD); + expect(set4.hashCode, set4.hashCode); + }); + + test('LogicalKeySet.hashCode is order-independent', () { + expect( + LogicalKeySet(LogicalKeyboardKey.keyA).hashCode, + LogicalKeySet(LogicalKeyboardKey.keyA).hashCode, + ); + expect( + LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB).hashCode, + LogicalKeySet(LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyA).hashCode, + ); + expect( + LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC).hashCode, + LogicalKeySet(LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyA).hashCode, + ); + expect( + LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyD).hashCode, + LogicalKeySet(LogicalKeyboardKey.keyD, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyA).hashCode, + ); + }); + test('LogicalKeySet diagnostics work.', () { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); diff --git a/packages/flutter/test/widgets/simple_semantics_test.dart b/packages/flutter/test/widgets/simple_semantics_test.dart index 715f43c990..394812d0da 100644 --- a/packages/flutter/test/widgets/simple_semantics_test.dart +++ b/packages/flutter/test/widgets/simple_semantics_test.dart @@ -34,7 +34,7 @@ void main() { ))); semantics.dispose(); - }, skip: isBrowser); + }); testWidgets('Simple tree is simple - material', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); @@ -78,5 +78,5 @@ void main() { ))); semantics.dispose(); - }, skip: isBrowser); + }); } diff --git a/packages/flutter/test/widgets/sliver_visibility_test.dart b/packages/flutter/test/widgets/sliver_visibility_test.dart index 7409fd2fdb..10a3699826 100644 --- a/packages/flutter/test/widgets/sliver_visibility_test.dart +++ b/packages/flutter/test/widgets/sliver_visibility_test.dart @@ -486,5 +486,5 @@ void main() { log.clear(); semantics.dispose(); - }, skip: isBrowser); + }); } diff --git a/packages/flutter/test/widgets/slivers_appbar_floating_pinned_test.dart b/packages/flutter/test/widgets/slivers_appbar_floating_pinned_test.dart index 82b014a9bc..19e56b9e20 100644 --- a/packages/flutter/test/widgets/slivers_appbar_floating_pinned_test.dart +++ b/packages/flutter/test/widgets/slivers_appbar_floating_pinned_test.dart @@ -298,7 +298,7 @@ void main() { // initial position of E was 200 + 56 + cSize.height + cSize.height + 500 // we've scrolled that up by 600.0, meaning it's at that minus 600 now: expect(tester.getTopLeft(find.text('E')), Offset(0.0, 200.0 + 56.0 + cSize.height * 2.0 + 500.0 - 600.0)); - }, skip: isBrowser); + }); testWidgets('Does not crash when there is less than minExtent remainingPaintExtent', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/21887. diff --git a/packages/flutter/test/widgets/transformed_scrollable_test.dart b/packages/flutter/test/widgets/transformed_scrollable_test.dart index b386301290..5f78398d9b 100644 --- a/packages/flutter/test/widgets/transformed_scrollable_test.dart +++ b/packages/flutter/test/widgets/transformed_scrollable_test.dart @@ -214,5 +214,5 @@ void main() { // coordinate space of the screen, the scroll view actually moved far more // pixels in its local coordinate system due to the perspective transform. expect(controller.offset, greaterThan(100)); - }, skip: isBrowser); + }); } diff --git a/packages/flutter/test/widgets/visibility_test.dart b/packages/flutter/test/widgets/visibility_test.dart index 3edd8c3558..3e5a9ade5a 100644 --- a/packages/flutter/test/widgets/visibility_test.dart +++ b/packages/flutter/test/widgets/visibility_test.dart @@ -429,5 +429,5 @@ void main() { log.clear(); semantics.dispose(); - }, skip: isBrowser); + }); } diff --git a/packages/flutter/test/widgets/wrap_test.dart b/packages/flutter/test/widgets/wrap_test.dart index ff098056a2..46b7b8339f 100644 --- a/packages/flutter/test/widgets/wrap_test.dart +++ b/packages/flutter/test/widgets/wrap_test.dart @@ -827,7 +827,7 @@ void main() { expect(tester.renderObject<RenderBox>(find.text('X')).size, const Size(100.0, 100.0)); expect(tester.renderObject<RenderBox>(find.byType(Baseline)).size, within<Size>(from: const Size(100.0, 200.0), distance: 0.001)); - }, skip: isBrowser); + }); testWidgets('Spacing with slight overflow', (WidgetTester tester) async { await tester.pumpWidget(Wrap( diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart index ec79f32982..7bf83aa25d 100644 --- a/packages/flutter_test/lib/src/binding.dart +++ b/packages/flutter_test/lib/src/binding.dart @@ -351,9 +351,6 @@ abstract class TestWidgetsFlutterBinding extends BindingBase return TestAsyncUtils.guard<void>(() async { assert(inTest); final Locale locale = Locale(languageCode, countryCode == '' ? null : countryCode); - if (isBrowser) { - return; - } dispatchLocalesChanged(<Locale>[locale]); }); }