
These tweaks came from https://github.com/flutter/packages/pull/4536 - The TwoDimensionalChildBuilderDelegate asserts that maxXIndex and maxYIndex are null or >= 0 - The TwoDimensionalChildDelegate setter in RenderTwoDimensionalViewport has a covariant to allow type safety for subclasses of RenderTwoDimensionalViewport implementing with other subclasses of TwoDimensionalChildDelegate I'd like to cherry pick this so https://github.com/flutter/packages/pull/4536 will not have to wait for it to reach stable.
1922 lines
76 KiB
Dart
1922 lines
76 KiB
Dart
// Copyright 2014 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 'package:flutter/foundation.dart';
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'two_dimensional_utils.dart';
|
|
|
|
void main() {
|
|
group('TwoDimensionalChildDelegate', () {
|
|
group('TwoDimensionalChildBuilderDelegate', () {
|
|
testWidgets('repaintBoundaries', (WidgetTester tester) async {
|
|
// Default - adds repaint boundaries
|
|
await tester.pumpWidget(simpleBuilderTest(
|
|
delegate: TwoDimensionalChildBuilderDelegate(
|
|
// Only build 1 child
|
|
maxXIndex: 0,
|
|
maxYIndex: 0,
|
|
builder: (BuildContext context, ChildVicinity vicinity) {
|
|
return SizedBox(
|
|
height: 200,
|
|
width: 200,
|
|
child: Center(child: Text('C${vicinity.xIndex}:R${vicinity.yIndex}')),
|
|
);
|
|
}
|
|
)
|
|
));
|
|
await tester.pumpAndSettle();
|
|
|
|
switch (defaultTargetPlatform) {
|
|
case TargetPlatform.android:
|
|
case TargetPlatform.fuchsia:
|
|
expect(find.byType(RepaintBoundary), findsNWidgets(7));
|
|
case TargetPlatform.iOS:
|
|
case TargetPlatform.linux:
|
|
case TargetPlatform.macOS:
|
|
case TargetPlatform.windows:
|
|
expect(find.byType(RepaintBoundary), findsNWidgets(3));
|
|
}
|
|
|
|
// None
|
|
await tester.pumpWidget(simpleBuilderTest(
|
|
delegate: TwoDimensionalChildBuilderDelegate(
|
|
// Only build 1 child
|
|
maxXIndex: 0,
|
|
maxYIndex: 0,
|
|
addRepaintBoundaries: false,
|
|
builder: (BuildContext context, ChildVicinity vicinity) {
|
|
return SizedBox(
|
|
height: 200,
|
|
width: 200,
|
|
child: Center(child: Text('C${vicinity.xIndex}:R${vicinity.yIndex}')),
|
|
);
|
|
}
|
|
)
|
|
));
|
|
await tester.pumpAndSettle();
|
|
|
|
switch (defaultTargetPlatform) {
|
|
case TargetPlatform.android:
|
|
case TargetPlatform.fuchsia:
|
|
expect(find.byType(RepaintBoundary), findsNWidgets(6));
|
|
case TargetPlatform.iOS:
|
|
case TargetPlatform.linux:
|
|
case TargetPlatform.macOS:
|
|
case TargetPlatform.windows:
|
|
expect(find.byType(RepaintBoundary), findsNWidgets(2));
|
|
}
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('will return null from build for exceeding maxXIndex and maxYIndex', (WidgetTester tester) async {
|
|
late BuildContext capturedContext;
|
|
final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate(
|
|
// Only build 1 child
|
|
maxXIndex: 0,
|
|
maxYIndex: 0,
|
|
addRepaintBoundaries: false,
|
|
builder: (BuildContext context, ChildVicinity vicinity) {
|
|
capturedContext = context;
|
|
return SizedBox(
|
|
height: 200,
|
|
width: 200,
|
|
child: Center(child: Text('C${vicinity.xIndex}:R${vicinity.yIndex}')),
|
|
);
|
|
}
|
|
);
|
|
await tester.pumpWidget(simpleBuilderTest(
|
|
delegate: delegate,
|
|
));
|
|
await tester.pumpAndSettle();
|
|
// maxXIndex
|
|
expect(
|
|
delegate.build(capturedContext, const ChildVicinity(xIndex: 1, yIndex: 0)),
|
|
isNull,
|
|
);
|
|
|
|
// maxYIndex
|
|
expect(
|
|
delegate.build(capturedContext, const ChildVicinity(xIndex: 0, yIndex: 1)),
|
|
isNull,
|
|
);
|
|
|
|
// Both
|
|
expect(
|
|
delegate.build(capturedContext, const ChildVicinity(xIndex: 1, yIndex: 1)),
|
|
isNull,
|
|
);
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
test('maxXIndex and maxYIndex assertions', () {
|
|
final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate(
|
|
maxXIndex: 0,
|
|
maxYIndex: 0,
|
|
builder: (BuildContext context, ChildVicinity vicinity) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
);
|
|
// Update
|
|
expect(
|
|
() {
|
|
delegate.maxXIndex = -1;
|
|
},
|
|
throwsA(
|
|
isA<AssertionError>().having(
|
|
(AssertionError error) => error.toString(),
|
|
'description',
|
|
contains('value == null || value >= 0'),
|
|
),
|
|
),
|
|
);
|
|
expect(
|
|
() {
|
|
delegate.maxYIndex = -1;
|
|
},
|
|
throwsA(
|
|
isA<AssertionError>().having(
|
|
(AssertionError error) => error.toString(),
|
|
'description',
|
|
contains('value == null || value >= 0'),
|
|
),
|
|
),
|
|
);
|
|
// Constructor
|
|
expect(
|
|
() {
|
|
TwoDimensionalChildBuilderDelegate(
|
|
maxXIndex: -1,
|
|
maxYIndex: 0,
|
|
builder: (BuildContext context, ChildVicinity vicinity) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
);
|
|
},
|
|
throwsA(
|
|
isA<AssertionError>().having(
|
|
(AssertionError error) => error.toString(),
|
|
'description',
|
|
contains('maxXIndex == null || maxXIndex >= 0'),
|
|
),
|
|
),
|
|
);
|
|
expect(
|
|
() {
|
|
TwoDimensionalChildBuilderDelegate(
|
|
maxXIndex: 0,
|
|
maxYIndex: -1,
|
|
builder: (BuildContext context, ChildVicinity vicinity) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
);
|
|
},
|
|
throwsA(
|
|
isA<AssertionError>().having(
|
|
(AssertionError error) => error.toString(),
|
|
'description',
|
|
contains('maxYIndex == null || maxYIndex >= 0'),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('throws an error when builder throws', (WidgetTester tester) async {
|
|
final List<Object> exceptions = <Object>[];
|
|
final FlutterExceptionHandler? oldHandler = FlutterError.onError;
|
|
FlutterError.onError = (FlutterErrorDetails details) {
|
|
exceptions.add(details.exception);
|
|
};
|
|
final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate(
|
|
// Only build 1 child
|
|
maxXIndex: 0,
|
|
maxYIndex: 0,
|
|
addRepaintBoundaries: false,
|
|
builder: (BuildContext context, ChildVicinity vicinity) {
|
|
throw 'Builder error!';
|
|
}
|
|
);
|
|
await tester.pumpWidget(simpleBuilderTest(
|
|
delegate: delegate,
|
|
));
|
|
await tester.pumpAndSettle();
|
|
FlutterError.onError = oldHandler;
|
|
|
|
expect(exceptions.isNotEmpty, isTrue);
|
|
expect(exceptions.length, 1);
|
|
expect(exceptions[0] as String, contains('Builder error!'));
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('shouldRebuild', (WidgetTester tester) async {
|
|
expect(builderDelegate.shouldRebuild(builderDelegate), isTrue);
|
|
}, variant: TargetPlatformVariant.all());
|
|
});
|
|
|
|
group('TwoDimensionalChildListDelegate', () {
|
|
testWidgets('repaintBoundaries', (WidgetTester tester) async {
|
|
final List<List<Widget>> children = <List<Widget>>[];
|
|
children.add(<Widget>[
|
|
const SizedBox(
|
|
height: 200,
|
|
width: 200,
|
|
child: Center(child: Text('R0:C0')),
|
|
)
|
|
]);
|
|
// Default - adds repaint boundaries
|
|
await tester.pumpWidget(simpleListTest(
|
|
delegate: TwoDimensionalChildListDelegate(
|
|
// Only builds 1 child
|
|
children: children,
|
|
)
|
|
));
|
|
await tester.pumpAndSettle();
|
|
|
|
// In the tests below the number of RepaintBoundary widgets depends on:
|
|
// ModalRoute - builds 2
|
|
// GlowingOverscrollIndicator - builds 2
|
|
// TwoDimensionalChildListDelegate - builds 1 unless addRepaintBoundaries is false
|
|
|
|
void expectModalRoute() {
|
|
expect(ModalRoute.of(tester.element(find.byType(SimpleListTableViewport))), isA<MaterialPageRoute<void>>());
|
|
}
|
|
|
|
switch (defaultTargetPlatform) {
|
|
case TargetPlatform.fuchsia:
|
|
expectModalRoute();
|
|
expect(find.byType(GlowingOverscrollIndicator), findsNWidgets(2));
|
|
expect(find.byType(RepaintBoundary), findsNWidgets(7));
|
|
|
|
case TargetPlatform.android:
|
|
case TargetPlatform.iOS:
|
|
case TargetPlatform.linux:
|
|
case TargetPlatform.macOS:
|
|
case TargetPlatform.windows:
|
|
expectModalRoute();
|
|
expect(find.byType(RepaintBoundary), findsNWidgets(3));
|
|
}
|
|
|
|
// None
|
|
await tester.pumpWidget(simpleListTest(
|
|
delegate: TwoDimensionalChildListDelegate(
|
|
// Different children triggers rebuild
|
|
children: <List<Widget>>[<Widget>[Container()]],
|
|
addRepaintBoundaries: false,
|
|
)
|
|
));
|
|
await tester.pumpAndSettle();
|
|
|
|
switch (defaultTargetPlatform) {
|
|
case TargetPlatform.fuchsia:
|
|
expectModalRoute();
|
|
expect(find.byType(GlowingOverscrollIndicator), findsNWidgets(2));
|
|
expect(find.byType(RepaintBoundary), findsNWidgets(6));
|
|
|
|
case TargetPlatform.android:
|
|
case TargetPlatform.iOS:
|
|
case TargetPlatform.linux:
|
|
case TargetPlatform.macOS:
|
|
case TargetPlatform.windows:
|
|
expectModalRoute();
|
|
expect(find.byType(RepaintBoundary), findsNWidgets(2));
|
|
}
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('will return null for a ChildVicinity outside of list bounds', (WidgetTester tester) async {
|
|
final List<List<Widget>> children = <List<Widget>>[];
|
|
children.add(<Widget>[
|
|
const SizedBox(
|
|
height: 200,
|
|
width: 200,
|
|
child: Center(child: Text('R0:C0')),
|
|
)
|
|
]);
|
|
final TwoDimensionalChildListDelegate delegate = TwoDimensionalChildListDelegate(
|
|
// Only builds 1 child
|
|
children: children,
|
|
);
|
|
|
|
// X index
|
|
expect(
|
|
delegate.build(_NullBuildContext(), const ChildVicinity(xIndex: 1, yIndex: 0)),
|
|
isNull,
|
|
);
|
|
// Y index
|
|
expect(
|
|
delegate.build(_NullBuildContext(), const ChildVicinity(xIndex: 0, yIndex: 1)),
|
|
isNull,
|
|
);
|
|
|
|
// Both
|
|
expect(
|
|
delegate.build(_NullBuildContext(), const ChildVicinity(xIndex: 1, yIndex: 1)),
|
|
isNull,
|
|
);
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('shouldRebuild', (WidgetTester tester) async {
|
|
final List<List<Widget>> children = <List<Widget>>[];
|
|
children.add(<Widget>[
|
|
const SizedBox(
|
|
height: 200,
|
|
width: 200,
|
|
child: Center(child: Text('R0:C0')),
|
|
)
|
|
]);
|
|
final TwoDimensionalChildListDelegate delegate = TwoDimensionalChildListDelegate(
|
|
// Only builds 1 child
|
|
children: children,
|
|
);
|
|
expect(delegate.shouldRebuild(delegate), isFalse);
|
|
|
|
final List<List<Widget>> newChildren = <List<Widget>>[];
|
|
final TwoDimensionalChildListDelegate oldDelegate = TwoDimensionalChildListDelegate(
|
|
children: newChildren,
|
|
);
|
|
|
|
expect(delegate.shouldRebuild(oldDelegate), isTrue);
|
|
}, variant: TargetPlatformVariant.all());
|
|
});
|
|
});
|
|
|
|
group('TwoDimensionalScrollable', () {
|
|
testWidgets('.of, .maybeOf', (WidgetTester tester) async {
|
|
late BuildContext capturedContext;
|
|
final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate(
|
|
maxXIndex: 0,
|
|
maxYIndex: 0,
|
|
builder: (BuildContext context, ChildVicinity vicinity) {
|
|
capturedContext = context;
|
|
return const SizedBox.square(dimension: 200);
|
|
}
|
|
);
|
|
await tester.pumpWidget(simpleBuilderTest(
|
|
delegate: delegate,
|
|
));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(TwoDimensionalScrollable.of(capturedContext), isNotNull);
|
|
expect(TwoDimensionalScrollable.maybeOf(capturedContext), isNotNull);
|
|
|
|
await tester.pumpWidget(Builder(
|
|
builder: (BuildContext context) {
|
|
capturedContext = context;
|
|
TwoDimensionalScrollable.of(context);
|
|
return Container();
|
|
}
|
|
));
|
|
await tester.pumpAndSettle();
|
|
final dynamic exception = tester.takeException();
|
|
expect(exception, isFlutterError);
|
|
final FlutterError error = exception as FlutterError;
|
|
expect(error.toString(), contains(
|
|
'TwoDimensionalScrollable.of() was called with a context that does '
|
|
'not contain a TwoDimensionalScrollable widget.'
|
|
));
|
|
|
|
expect(TwoDimensionalScrollable.maybeOf(capturedContext), isNull);
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('horizontal and vertical getters', (WidgetTester tester) async {
|
|
late BuildContext capturedContext;
|
|
final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate(
|
|
maxXIndex: 0,
|
|
maxYIndex: 0,
|
|
builder: (BuildContext context, ChildVicinity vicinity) {
|
|
capturedContext = context;
|
|
return const SizedBox.square(dimension: 200);
|
|
}
|
|
);
|
|
await tester.pumpWidget(simpleBuilderTest(
|
|
delegate: delegate,
|
|
));
|
|
await tester.pumpAndSettle();
|
|
|
|
final TwoDimensionalScrollableState scrollable = TwoDimensionalScrollable.of(capturedContext);
|
|
expect(scrollable.verticalScrollable.position.pixels, 0.0);
|
|
expect(scrollable.horizontalScrollable.position.pixels, 0.0);
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('creates fallback ScrollControllers if not provided by ScrollableDetails', (WidgetTester tester) async {
|
|
late BuildContext capturedContext;
|
|
final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate(
|
|
maxXIndex: 0,
|
|
maxYIndex: 0,
|
|
builder: (BuildContext context, ChildVicinity vicinity) {
|
|
capturedContext = context;
|
|
return const SizedBox.square(dimension: 200);
|
|
}
|
|
);
|
|
await tester.pumpWidget(simpleBuilderTest(
|
|
delegate: delegate,
|
|
));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Vertical
|
|
final ScrollableState vertical = Scrollable.of(capturedContext, axis: Axis.vertical);
|
|
expect(vertical.widget.controller, isNotNull);
|
|
// Horizontal
|
|
final ScrollableState horizontal = Scrollable.of(capturedContext, axis: Axis.horizontal);
|
|
expect(horizontal.widget.controller, isNotNull);
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('asserts the axis directions do not conflict with one another', (WidgetTester tester) async {
|
|
final List<Object> exceptions = <Object>[];
|
|
final FlutterExceptionHandler? oldHandler = FlutterError.onError;
|
|
FlutterError.onError = (FlutterErrorDetails details) {
|
|
exceptions.add(details.exception);
|
|
};
|
|
// Horizontal mismatch
|
|
await tester.pumpWidget(TwoDimensionalScrollable(
|
|
horizontalDetails: const ScrollableDetails.horizontal(),
|
|
verticalDetails: const ScrollableDetails.horizontal(),
|
|
viewportBuilder: (BuildContext context, ViewportOffset verticalPosition, ViewportOffset horizontalPosition) {
|
|
return Container();
|
|
},
|
|
));
|
|
|
|
// Vertical mismatch
|
|
await tester.pumpWidget(TwoDimensionalScrollable(
|
|
horizontalDetails: const ScrollableDetails.vertical(),
|
|
verticalDetails: const ScrollableDetails.vertical(),
|
|
viewportBuilder: (BuildContext context, ViewportOffset verticalPosition, ViewportOffset horizontalPosition) {
|
|
return Container();
|
|
},
|
|
));
|
|
|
|
// Both
|
|
await tester.pumpWidget(TwoDimensionalScrollable(
|
|
horizontalDetails: const ScrollableDetails.vertical(),
|
|
verticalDetails: const ScrollableDetails.horizontal(),
|
|
viewportBuilder: (BuildContext context, ViewportOffset verticalPosition, ViewportOffset horizontalPosition) {
|
|
return Container();
|
|
},
|
|
));
|
|
|
|
expect(exceptions.length, 3);
|
|
for (final Object exception in exceptions) {
|
|
expect(exception, isAssertionError);
|
|
expect((exception as AssertionError).message, contains('are not Axis'));
|
|
}
|
|
FlutterError.onError = oldHandler;
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('correctly sets restorationIds', (WidgetTester tester) async {
|
|
late BuildContext capturedContext;
|
|
// with restorationID set
|
|
await tester.pumpWidget(WidgetsApp(
|
|
color: const Color(0xFFFFFFFF),
|
|
restorationScopeId: 'Test ID',
|
|
builder: (BuildContext context, Widget? child) => TwoDimensionalScrollable(
|
|
restorationId: 'Custom Restoration ID',
|
|
horizontalDetails: const ScrollableDetails.horizontal(),
|
|
verticalDetails: const ScrollableDetails.vertical(),
|
|
viewportBuilder: (BuildContext context, ViewportOffset verticalPosition, ViewportOffset horizontalPosition) {
|
|
return SizedBox.square(
|
|
dimension: 200,
|
|
child: Builder(
|
|
builder: (BuildContext context) {
|
|
capturedContext = context;
|
|
return Container();
|
|
},
|
|
)
|
|
);
|
|
},
|
|
),
|
|
));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
RestorationScope.of(capturedContext).restorationId,
|
|
'Custom Restoration ID',
|
|
);
|
|
expect(
|
|
Scrollable.of(capturedContext, axis: Axis.vertical).widget.restorationId,
|
|
'OuterVerticalTwoDimensionalScrollable',
|
|
);
|
|
expect(
|
|
Scrollable.of(capturedContext, axis: Axis.horizontal).widget.restorationId,
|
|
'InnerHorizontalTwoDimensionalScrollable',
|
|
);
|
|
|
|
// default restorationID
|
|
await tester.pumpWidget(TwoDimensionalScrollable(
|
|
horizontalDetails: const ScrollableDetails.horizontal(),
|
|
verticalDetails: const ScrollableDetails.vertical(),
|
|
viewportBuilder: (BuildContext context, ViewportOffset verticalPosition, ViewportOffset horizontalPosition) {
|
|
return SizedBox.square(
|
|
dimension: 200,
|
|
child: Builder(
|
|
builder: (BuildContext context) {
|
|
capturedContext = context;
|
|
return Container();
|
|
},
|
|
)
|
|
);
|
|
},
|
|
));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
RestorationScope.maybeOf(capturedContext),
|
|
isNull,
|
|
);
|
|
expect(
|
|
Scrollable.of(capturedContext, axis: Axis.vertical).widget.restorationId,
|
|
'OuterVerticalTwoDimensionalScrollable',
|
|
);
|
|
expect(
|
|
Scrollable.of(capturedContext, axis: Axis.horizontal).widget.restorationId,
|
|
'InnerHorizontalTwoDimensionalScrollable',
|
|
);
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('Restoration works', (WidgetTester tester) async {
|
|
await tester.pumpWidget(WidgetsApp(
|
|
color: const Color(0xFFFFFFFF),
|
|
restorationScopeId: 'Test ID',
|
|
builder: (BuildContext context, Widget? child) => TwoDimensionalScrollable(
|
|
restorationId: 'Custom Restoration ID',
|
|
horizontalDetails: const ScrollableDetails.horizontal(),
|
|
verticalDetails: const ScrollableDetails.vertical(),
|
|
viewportBuilder: (BuildContext context, ViewportOffset verticalPosition, ViewportOffset horizontalPosition) {
|
|
return SimpleBuilderTableViewport(
|
|
verticalOffset: verticalPosition,
|
|
verticalAxisDirection: AxisDirection.down,
|
|
horizontalOffset: horizontalPosition,
|
|
horizontalAxisDirection: AxisDirection.right,
|
|
delegate: builderDelegate,
|
|
mainAxis: Axis.vertical,
|
|
);
|
|
},
|
|
),
|
|
));
|
|
await tester.pumpAndSettle();
|
|
|
|
await restoreScrollAndVerify(tester);
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('Inner Scrollables receive the correct details from TwoDimensionalScrollable', (WidgetTester tester) async {
|
|
// Default
|
|
late BuildContext capturedContext;
|
|
await tester.pumpWidget(TwoDimensionalScrollable(
|
|
horizontalDetails: const ScrollableDetails.horizontal(),
|
|
verticalDetails: const ScrollableDetails.vertical(),
|
|
viewportBuilder: (BuildContext context, ViewportOffset verticalPosition, ViewportOffset horizontalPosition) {
|
|
return SizedBox.square(
|
|
dimension: 200,
|
|
child: Builder(
|
|
builder: (BuildContext context) {
|
|
capturedContext = context;
|
|
return Container();
|
|
},
|
|
)
|
|
);
|
|
},
|
|
));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Vertical
|
|
ScrollableState vertical = Scrollable.of(capturedContext, axis: Axis.vertical);
|
|
expect(vertical.widget.key, isNotNull);
|
|
expect(vertical.widget.axisDirection, AxisDirection.down);
|
|
expect(vertical.widget.controller, isNotNull);
|
|
expect(vertical.widget.physics, isNull);
|
|
expect(vertical.widget.clipBehavior, Clip.hardEdge);
|
|
expect(vertical.widget.incrementCalculator, isNull);
|
|
expect(vertical.widget.excludeFromSemantics, isFalse);
|
|
expect(vertical.widget.restorationId, 'OuterVerticalTwoDimensionalScrollable');
|
|
expect(vertical.widget.dragStartBehavior, DragStartBehavior.start);
|
|
|
|
// Horizontal
|
|
ScrollableState horizontal = Scrollable.of(capturedContext, axis: Axis.horizontal);
|
|
expect(horizontal.widget.key, isNotNull);
|
|
expect(horizontal.widget.axisDirection, AxisDirection.right);
|
|
expect(horizontal.widget.controller, isNotNull);
|
|
expect(horizontal.widget.physics, isNull);
|
|
expect(horizontal.widget.clipBehavior, Clip.hardEdge);
|
|
expect(horizontal.widget.incrementCalculator, isNull);
|
|
expect(horizontal.widget.excludeFromSemantics, isFalse);
|
|
expect(horizontal.widget.restorationId, 'InnerHorizontalTwoDimensionalScrollable');
|
|
expect(horizontal.widget.dragStartBehavior, DragStartBehavior.start);
|
|
|
|
// Customized
|
|
final ScrollController horizontalController = ScrollController();
|
|
final ScrollController verticalController = ScrollController();
|
|
double calculator(_) => 0.0;
|
|
await tester.pumpWidget(TwoDimensionalScrollable(
|
|
incrementCalculator: calculator,
|
|
excludeFromSemantics: true,
|
|
dragStartBehavior: DragStartBehavior.down,
|
|
horizontalDetails: ScrollableDetails.horizontal(
|
|
reverse: true,
|
|
controller: horizontalController,
|
|
physics: const ClampingScrollPhysics(),
|
|
decorationClipBehavior: Clip.antiAlias,
|
|
),
|
|
verticalDetails: ScrollableDetails.vertical(
|
|
reverse: true,
|
|
controller: verticalController,
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
decorationClipBehavior: Clip.antiAliasWithSaveLayer,
|
|
),
|
|
viewportBuilder: (BuildContext context, ViewportOffset verticalPosition, ViewportOffset horizontalPosition) {
|
|
return SizedBox.square(
|
|
dimension: 200,
|
|
child: Builder(
|
|
builder: (BuildContext context) {
|
|
capturedContext = context;
|
|
return Container();
|
|
},
|
|
)
|
|
);
|
|
},
|
|
));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Vertical
|
|
vertical = Scrollable.of(capturedContext, axis: Axis.vertical);
|
|
expect(vertical.widget.key, isNotNull);
|
|
expect(vertical.widget.axisDirection, AxisDirection.up);
|
|
expect(vertical.widget.controller, verticalController);
|
|
expect(vertical.widget.physics, const AlwaysScrollableScrollPhysics());
|
|
expect(vertical.widget.clipBehavior, Clip.antiAliasWithSaveLayer);
|
|
expect(
|
|
vertical.widget.incrementCalculator!(ScrollIncrementDetails(
|
|
type: ScrollIncrementType.line,
|
|
metrics: verticalController.position,
|
|
)),
|
|
0.0,
|
|
);
|
|
expect(vertical.widget.excludeFromSemantics, isTrue);
|
|
expect(vertical.widget.restorationId, 'OuterVerticalTwoDimensionalScrollable');
|
|
expect(vertical.widget.dragStartBehavior, DragStartBehavior.down);
|
|
|
|
// Horizontal
|
|
horizontal = Scrollable.of(capturedContext, axis: Axis.horizontal);
|
|
expect(horizontal.widget.key, isNotNull);
|
|
expect(horizontal.widget.axisDirection, AxisDirection.left);
|
|
expect(horizontal.widget.controller, horizontalController);
|
|
expect(horizontal.widget.physics, const ClampingScrollPhysics());
|
|
expect(horizontal.widget.clipBehavior, Clip.antiAlias);
|
|
expect(
|
|
horizontal.widget.incrementCalculator!(ScrollIncrementDetails(
|
|
type: ScrollIncrementType.line,
|
|
metrics: horizontalController.position,
|
|
)),
|
|
0.0,
|
|
);
|
|
expect(horizontal.widget.excludeFromSemantics, isTrue);
|
|
expect(horizontal.widget.restorationId, 'InnerHorizontalTwoDimensionalScrollable');
|
|
expect(horizontal.widget.dragStartBehavior, DragStartBehavior.down);
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
group('DiagonalDragBehavior', () {
|
|
testWidgets('none (default)', (WidgetTester tester) async {
|
|
// Vertical and horizontal axes are locked.
|
|
final ScrollController verticalController = ScrollController();
|
|
final ScrollController horizontalController = ScrollController();
|
|
await tester.pumpWidget(Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: simpleBuilderTest(
|
|
verticalDetails: ScrollableDetails.vertical(controller: verticalController),
|
|
horizontalDetails: ScrollableDetails.horizontal(controller: horizontalController),
|
|
)
|
|
));
|
|
await tester.pumpAndSettle();
|
|
final Finder findScrollable = find.byElementPredicate((Element e) => e.widget is TwoDimensionalScrollable);
|
|
|
|
expect(verticalController.position.pixels, 0.0);
|
|
expect(horizontalController.position.pixels, 0.0);
|
|
await tester.drag(findScrollable, const Offset(0.0, -100.0));
|
|
await tester.pumpAndSettle();
|
|
expect(verticalController.position.pixels, 80.0);
|
|
expect(horizontalController.position.pixels, 0.0);
|
|
await tester.drag(findScrollable, const Offset(-100.0, 0.0));
|
|
await tester.pumpAndSettle();
|
|
expect(verticalController.position.pixels, 80.0);
|
|
expect(horizontalController.position.pixels, 80.0);
|
|
// Drag with and x and y offset, only vertical will accept the gesture
|
|
// since the x is < kTouchSlop
|
|
await tester.drag(findScrollable, const Offset(-10.0, -50.0));
|
|
await tester.pumpAndSettle();
|
|
expect(verticalController.position.pixels, 110.0);
|
|
expect(horizontalController.position.pixels, 80.0);
|
|
// Drag with and x and y offset, only horizontal will accept the gesture
|
|
// since the y is < kTouchSlop
|
|
await tester.drag(findScrollable, const Offset(-50.0, -10.0));
|
|
await tester.pumpAndSettle();
|
|
expect(verticalController.position.pixels, 110.0);
|
|
expect(horizontalController.position.pixels, 110.0);
|
|
// Drag with and x and y offset, only vertical will accept the gesture
|
|
// x is > kTouchSlop, larger offset wins
|
|
await tester.drag(findScrollable, const Offset(-20.0, -50.0));
|
|
await tester.pumpAndSettle();
|
|
expect(verticalController.position.pixels, 140.0);
|
|
expect(horizontalController.position.pixels, 110.0);
|
|
// Drag with and x and y offset, only horizontal will accept the gesture
|
|
// y is > kTouchSlop, larger offset wins
|
|
await tester.drag(findScrollable, const Offset(-50.0, -20.0));
|
|
await tester.pumpAndSettle();
|
|
expect(verticalController.position.pixels, 140.0);
|
|
expect(horizontalController.position.pixels, 140.0);
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('weightedEvent', (WidgetTester tester) async {
|
|
// For weighted event, the winning axis is locked for the duration of
|
|
// the gesture.
|
|
final ScrollController verticalController = ScrollController();
|
|
final ScrollController horizontalController = ScrollController();
|
|
await tester.pumpWidget(Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: simpleBuilderTest(
|
|
diagonalDrag: DiagonalDragBehavior.weightedEvent,
|
|
verticalDetails: ScrollableDetails.vertical(controller: verticalController),
|
|
horizontalDetails: ScrollableDetails.horizontal(controller: horizontalController),
|
|
)
|
|
));
|
|
await tester.pumpAndSettle();
|
|
final Finder findScrollable = find.byElementPredicate((Element e) => e.widget is TwoDimensionalScrollable);
|
|
|
|
// Locks to vertical axis - simple.
|
|
expect(verticalController.position.pixels, 0.0);
|
|
expect(horizontalController.position.pixels, 0.0);
|
|
TestGesture gesture = await tester.startGesture(tester.getCenter(findScrollable));
|
|
// In this case, the vertical axis clearly wins.
|
|
Offset secondLocation = tester.getCenter(findScrollable) + const Offset(0.0, -50.0);
|
|
await gesture.moveTo(secondLocation);
|
|
await tester.pumpAndSettle();
|
|
expect(verticalController.position.pixels, 50.0);
|
|
expect(horizontalController.position.pixels, 0.0);
|
|
// Gesture has not ended yet, move with horizontal diff
|
|
Offset thirdLocation = secondLocation + const Offset(-30, -15);
|
|
await gesture.moveTo(thirdLocation);
|
|
await tester.pumpAndSettle();
|
|
// Only vertical diff applied
|
|
expect(verticalController.position.pixels, 65.0);
|
|
expect(horizontalController.position.pixels, 0.0);
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
// Lock to vertical axis - scrolls diagonally until certain
|
|
verticalController.jumpTo(0.0);
|
|
horizontalController.jumpTo(0.0);
|
|
await tester.pump();
|
|
expect(verticalController.position.pixels, 0.0);
|
|
expect(horizontalController.position.pixels, 0.0);
|
|
gesture = await tester.startGesture(tester.getCenter(findScrollable));
|
|
// In this case, the no one clearly wins, so it moves diagonally.
|
|
secondLocation = tester.getCenter(findScrollable) + const Offset(-50.0, -50.0);
|
|
await gesture.moveTo(secondLocation);
|
|
await tester.pumpAndSettle();
|
|
expect(verticalController.position.pixels, 50.0);
|
|
expect(horizontalController.position.pixels, 50.0);
|
|
// Gesture has not ended yet, move clearly indicating vertical
|
|
thirdLocation = secondLocation + const Offset(-20, -50);
|
|
await gesture.moveTo(thirdLocation);
|
|
await tester.pumpAndSettle();
|
|
// Only vertical diff applied
|
|
expect(verticalController.position.pixels, 100.0);
|
|
expect(horizontalController.position.pixels, 50.0);
|
|
// Gesture has not ended yet, and vertical axis has won for the gesture
|
|
// continue only vertical scrolling.
|
|
Offset fourthLocation = thirdLocation + const Offset(-30, -30);
|
|
await gesture.moveTo(fourthLocation);
|
|
await tester.pumpAndSettle();
|
|
// Only vertical diff applied
|
|
expect(verticalController.position.pixels, 130.0);
|
|
expect(horizontalController.position.pixels, 50.0);
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
// Locks to horizontal axis - simple.
|
|
verticalController.jumpTo(0.0);
|
|
horizontalController.jumpTo(0.0);
|
|
await tester.pump();
|
|
expect(verticalController.position.pixels, 0.0);
|
|
expect(horizontalController.position.pixels, 0.0);
|
|
gesture = await tester.startGesture(tester.getCenter(findScrollable));
|
|
// In this case, the horizontal axis clearly wins.
|
|
secondLocation = tester.getCenter(findScrollable) + const Offset(-50.0, 0.0);
|
|
await gesture.moveTo(secondLocation);
|
|
await tester.pumpAndSettle();
|
|
expect(verticalController.position.pixels, 0.0);
|
|
expect(horizontalController.position.pixels, 50.0);
|
|
// Gesture has not ended yet, move with vertical diff
|
|
thirdLocation = secondLocation + const Offset(-15, -30);
|
|
await gesture.moveTo(thirdLocation);
|
|
await tester.pumpAndSettle();
|
|
// Only vertical diff applied
|
|
expect(verticalController.position.pixels, 0.0);
|
|
expect(horizontalController.position.pixels, 65.0);
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
// Lock to horizontal axis - scrolls diagonally until certain
|
|
verticalController.jumpTo(0.0);
|
|
horizontalController.jumpTo(0.0);
|
|
await tester.pump();
|
|
expect(verticalController.position.pixels, 0.0);
|
|
expect(horizontalController.position.pixels, 0.0);
|
|
gesture = await tester.startGesture(tester.getCenter(findScrollable));
|
|
// In this case, the no one clearly wins, so it moves diagonally.
|
|
secondLocation = tester.getCenter(findScrollable) + const Offset(-50.0, -50.0);
|
|
await gesture.moveTo(secondLocation);
|
|
await tester.pumpAndSettle();
|
|
expect(verticalController.position.pixels, 50.0);
|
|
expect(horizontalController.position.pixels, 50.0);
|
|
// Gesture has not ended yet, move clearly indicating horizontal
|
|
thirdLocation = secondLocation + const Offset(-50, -20);
|
|
await gesture.moveTo(thirdLocation);
|
|
await tester.pumpAndSettle();
|
|
// Only horizontal diff applied
|
|
expect(verticalController.position.pixels, 50.0);
|
|
expect(horizontalController.position.pixels, 100.0);
|
|
// Gesture has not ended yet, and horizontal axis has won for the gesture
|
|
// continue only horizontal scrolling.
|
|
fourthLocation = thirdLocation + const Offset(-30, -30);
|
|
await gesture.moveTo(fourthLocation);
|
|
await tester.pumpAndSettle();
|
|
// Only horizontal diff applied
|
|
expect(verticalController.position.pixels, 50.0);
|
|
expect(horizontalController.position.pixels, 130.0);
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('weightedContinuous', (WidgetTester tester) async {
|
|
// For weighted continuous, the winning axis can change if the axis
|
|
// differential for the gesture exceeds kTouchSlop. So it can lock, and
|
|
// remain locked, if the user maintains a generally straight gesture,
|
|
// otherwise it will unlock and re-evaluate.
|
|
final ScrollController verticalController = ScrollController();
|
|
final ScrollController horizontalController = ScrollController();
|
|
await tester.pumpWidget(Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: simpleBuilderTest(
|
|
diagonalDrag: DiagonalDragBehavior.weightedContinuous,
|
|
verticalDetails: ScrollableDetails.vertical(controller: verticalController),
|
|
horizontalDetails: ScrollableDetails.horizontal(controller: horizontalController),
|
|
)
|
|
));
|
|
await tester.pumpAndSettle();
|
|
final Finder findScrollable = find.byElementPredicate((Element e) => e.widget is TwoDimensionalScrollable);
|
|
|
|
// Locks to vertical, and then unlocks, resets to horizontal, then
|
|
// unlocks and scrolls diagonally.
|
|
expect(verticalController.position.pixels, 0.0);
|
|
expect(horizontalController.position.pixels, 0.0);
|
|
final TestGesture gesture = await tester.startGesture(tester.getCenter(findScrollable));
|
|
// In this case, the vertical axis clearly wins.
|
|
final Offset secondLocation = tester.getCenter(findScrollable) + const Offset(0.0, -50.0);
|
|
await gesture.moveTo(secondLocation);
|
|
await tester.pumpAndSettle();
|
|
expect(verticalController.position.pixels, 50.0);
|
|
expect(horizontalController.position.pixels, 0.0);
|
|
// Gesture has not ended yet, move with horizontal diff, but still
|
|
// dominant vertical
|
|
final Offset thirdLocation = secondLocation + const Offset(-15, -50);
|
|
await gesture.moveTo(thirdLocation);
|
|
await tester.pumpAndSettle();
|
|
// Only vertical diff applied since kTouchSlop was not exceeded in the
|
|
// horizontal axis from one drag event to the next.
|
|
expect(verticalController.position.pixels, 100.0);
|
|
expect(horizontalController.position.pixels, 0.0);
|
|
// Gesture has not ended yet, move with unlocking horizontal diff
|
|
final Offset fourthLocation = thirdLocation + const Offset(-50, -15);
|
|
await gesture.moveTo(fourthLocation);
|
|
await tester.pumpAndSettle();
|
|
// Only horizontal diff applied
|
|
expect(verticalController.position.pixels, 100.0);
|
|
expect(horizontalController.position.pixels, 50.0);
|
|
// Gesture has not ended yet, move with unlocking diff that results in
|
|
// diagonal move since neither wins.
|
|
final Offset fifthLocation = fourthLocation + const Offset(-50, -50);
|
|
await gesture.moveTo(fifthLocation);
|
|
await tester.pumpAndSettle();
|
|
// Only horizontal diff applied
|
|
expect(verticalController.position.pixels, 150.0);
|
|
expect(horizontalController.position.pixels, 100.0);
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('free', (WidgetTester tester) async {
|
|
// For free, anything goes.
|
|
final ScrollController verticalController = ScrollController();
|
|
final ScrollController horizontalController = ScrollController();
|
|
await tester.pumpWidget(Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: simpleBuilderTest(
|
|
diagonalDrag: DiagonalDragBehavior.free,
|
|
verticalDetails: ScrollableDetails.vertical(controller: verticalController),
|
|
horizontalDetails: ScrollableDetails.horizontal(controller: horizontalController),
|
|
)
|
|
));
|
|
await tester.pumpAndSettle();
|
|
final Finder findScrollable = find.byElementPredicate((Element e) => e.widget is TwoDimensionalScrollable);
|
|
|
|
// Nothing locks.
|
|
expect(verticalController.position.pixels, 0.0);
|
|
expect(horizontalController.position.pixels, 0.0);
|
|
final TestGesture gesture = await tester.startGesture(tester.getCenter(findScrollable));
|
|
final Offset secondLocation = tester.getCenter(findScrollable) + const Offset(0.0, -50.0);
|
|
await gesture.moveTo(secondLocation);
|
|
await tester.pumpAndSettle();
|
|
expect(verticalController.position.pixels, 50.0);
|
|
expect(horizontalController.position.pixels, 0.0);
|
|
final Offset thirdLocation = secondLocation + const Offset(-15, -50);
|
|
await gesture.moveTo(thirdLocation);
|
|
await tester.pumpAndSettle();
|
|
expect(verticalController.position.pixels, 100.0);
|
|
expect(horizontalController.position.pixels, 15.0);
|
|
final Offset fourthLocation = thirdLocation + const Offset(-50, -15);
|
|
await gesture.moveTo(fourthLocation);
|
|
await tester.pumpAndSettle();
|
|
expect(verticalController.position.pixels, 115.0);
|
|
expect(horizontalController.position.pixels, 65.0);
|
|
final Offset fifthLocation = fourthLocation + const Offset(-50, -50);
|
|
await gesture.moveTo(fifthLocation);
|
|
await tester.pumpAndSettle();
|
|
expect(verticalController.position.pixels, 165.0);
|
|
expect(horizontalController.position.pixels, 115.0);
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
});
|
|
});
|
|
});
|
|
|
|
testWidgets('TwoDimensionalViewport asserts against axes mismatch', (WidgetTester tester) async {
|
|
// Horizontal mismatch
|
|
expect(
|
|
() {
|
|
SimpleBuilderTableViewport(
|
|
verticalOffset: ViewportOffset.fixed(0.0),
|
|
verticalAxisDirection: AxisDirection.left,
|
|
horizontalOffset: ViewportOffset.fixed(0.0),
|
|
horizontalAxisDirection: AxisDirection.right,
|
|
delegate: builderDelegate,
|
|
mainAxis: Axis.vertical,
|
|
);
|
|
},
|
|
throwsA(
|
|
isA<AssertionError>().having(
|
|
(AssertionError error) => error.toString(),
|
|
'description',
|
|
contains('AxisDirection is not Axis.'),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Vertical mismatch
|
|
expect(
|
|
() {
|
|
SimpleBuilderTableViewport(
|
|
verticalOffset: ViewportOffset.fixed(0.0),
|
|
verticalAxisDirection: AxisDirection.up,
|
|
horizontalOffset: ViewportOffset.fixed(0.0),
|
|
horizontalAxisDirection: AxisDirection.down,
|
|
delegate: builderDelegate,
|
|
mainAxis: Axis.vertical,
|
|
);
|
|
},
|
|
throwsA(
|
|
isA<AssertionError>().having(
|
|
(AssertionError error) => error.toString(),
|
|
'description',
|
|
contains('AxisDirection is not Axis.'),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Both
|
|
expect(
|
|
() {
|
|
SimpleBuilderTableViewport(
|
|
verticalOffset: ViewportOffset.fixed(0.0),
|
|
verticalAxisDirection: AxisDirection.left,
|
|
horizontalOffset: ViewportOffset.fixed(0.0),
|
|
horizontalAxisDirection: AxisDirection.down,
|
|
delegate: builderDelegate,
|
|
mainAxis: Axis.vertical,
|
|
);
|
|
},
|
|
throwsA(
|
|
isA<AssertionError>().having(
|
|
(AssertionError error) => error.toString(),
|
|
'description',
|
|
contains('AxisDirection is not Axis.'),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
|
|
test('TwoDimensionalViewportParentData', () {
|
|
// Default vicinity is invalid
|
|
final TwoDimensionalViewportParentData parentData = TwoDimensionalViewportParentData();
|
|
expect(parentData.vicinity, ChildVicinity.invalid);
|
|
|
|
// toString
|
|
parentData
|
|
..vicinity = const ChildVicinity(xIndex: 10, yIndex: 10)
|
|
..paintOffset = const Offset(20.0, 20.0)
|
|
..layoutOffset = const Offset(20.0, 20.0);
|
|
expect(
|
|
parentData.toString(),
|
|
'vicinity=(xIndex: 10, yIndex: 10); layoutOffset=Offset(20.0, 20.0); '
|
|
'paintOffset=Offset(20.0, 20.0); not visible ',
|
|
);
|
|
});
|
|
|
|
test('ChildVicinity comparable', () {
|
|
const ChildVicinity baseVicinity = ChildVicinity(xIndex: 0, yIndex: 0);
|
|
const ChildVicinity sameXVicinity = ChildVicinity(xIndex: 0, yIndex: 2);
|
|
const ChildVicinity sameYVicinity = ChildVicinity(xIndex: 3, yIndex: 0);
|
|
const ChildVicinity sameNothingVicinity = ChildVicinity(xIndex: 20, yIndex: 30);
|
|
// ==
|
|
expect(baseVicinity == baseVicinity, isTrue);
|
|
expect(baseVicinity == sameXVicinity, isFalse);
|
|
expect(baseVicinity == sameYVicinity, isFalse);
|
|
expect(baseVicinity == sameNothingVicinity, isFalse);
|
|
|
|
// compareTo
|
|
expect(baseVicinity.compareTo(baseVicinity), 0);
|
|
expect(baseVicinity.compareTo(sameXVicinity), -2);
|
|
expect(baseVicinity.compareTo(sameYVicinity), -3);
|
|
expect(baseVicinity.compareTo(sameNothingVicinity), -20);
|
|
|
|
// toString
|
|
expect(baseVicinity.toString(), '(xIndex: 0, yIndex: 0)');
|
|
expect(sameXVicinity.toString(), '(xIndex: 0, yIndex: 2)');
|
|
expect(sameYVicinity.toString(), '(xIndex: 3, yIndex: 0)');
|
|
expect(sameNothingVicinity.toString(), '(xIndex: 20, yIndex: 30)');
|
|
});
|
|
|
|
group('RenderTwoDimensionalViewport', () {
|
|
testWidgets('asserts against axes mismatch', (WidgetTester tester) async {
|
|
// Horizontal mismatch
|
|
expect(
|
|
() {
|
|
RenderSimpleBuilderTableViewport(
|
|
verticalOffset: ViewportOffset.fixed(0.0),
|
|
verticalAxisDirection: AxisDirection.left,
|
|
horizontalOffset: ViewportOffset.fixed(0.0),
|
|
horizontalAxisDirection: AxisDirection.right,
|
|
delegate: builderDelegate,
|
|
mainAxis: Axis.vertical,
|
|
childManager: _NullBuildContext(),
|
|
);
|
|
},
|
|
throwsA(
|
|
isA<AssertionError>().having(
|
|
(AssertionError error) => error.toString(),
|
|
'description',
|
|
contains('AxisDirection is not Axis.'),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Vertical mismatch
|
|
expect(
|
|
() {
|
|
RenderSimpleBuilderTableViewport(
|
|
verticalOffset: ViewportOffset.fixed(0.0),
|
|
verticalAxisDirection: AxisDirection.up,
|
|
horizontalOffset: ViewportOffset.fixed(0.0),
|
|
horizontalAxisDirection: AxisDirection.down,
|
|
delegate: builderDelegate,
|
|
mainAxis: Axis.vertical,
|
|
childManager: _NullBuildContext(),
|
|
);
|
|
},
|
|
throwsA(
|
|
isA<AssertionError>().having(
|
|
(AssertionError error) => error.toString(),
|
|
'description',
|
|
contains('AxisDirection is not Axis.'),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Both
|
|
expect(
|
|
() {
|
|
RenderSimpleBuilderTableViewport(
|
|
verticalOffset: ViewportOffset.fixed(0.0),
|
|
verticalAxisDirection: AxisDirection.left,
|
|
horizontalOffset: ViewportOffset.fixed(0.0),
|
|
horizontalAxisDirection: AxisDirection.down,
|
|
delegate: builderDelegate,
|
|
mainAxis: Axis.vertical,
|
|
childManager: _NullBuildContext(),
|
|
);
|
|
},
|
|
throwsA(
|
|
isA<AssertionError>().having(
|
|
(AssertionError error) => error.toString(),
|
|
'description',
|
|
contains('AxisDirection is not Axis.'),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('getters', (WidgetTester tester) async {
|
|
final UniqueKey childKey = UniqueKey();
|
|
final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate(
|
|
maxXIndex: 0,
|
|
maxYIndex: 0,
|
|
builder: (BuildContext context, ChildVicinity vicinity) {
|
|
return SizedBox.square(key: childKey, dimension: 200);
|
|
}
|
|
);
|
|
final RenderSimpleBuilderTableViewport renderViewport = RenderSimpleBuilderTableViewport(
|
|
verticalOffset: ViewportOffset.fixed(10.0),
|
|
verticalAxisDirection: AxisDirection.down,
|
|
horizontalOffset: ViewportOffset.fixed(20.0),
|
|
horizontalAxisDirection: AxisDirection.right,
|
|
delegate: delegate,
|
|
mainAxis: Axis.vertical,
|
|
childManager: _NullBuildContext(),
|
|
);
|
|
|
|
expect(renderViewport.clipBehavior, Clip.hardEdge);
|
|
expect(renderViewport.cacheExtent, RenderAbstractViewport.defaultCacheExtent);
|
|
expect(renderViewport.isRepaintBoundary, isTrue);
|
|
expect(renderViewport.sizedByParent, isTrue);
|
|
// No size yet, should assert.
|
|
expect(
|
|
() {
|
|
renderViewport.viewportDimension;
|
|
},
|
|
throwsA(
|
|
isA<AssertionError>().having(
|
|
(AssertionError error) => error.toString(),
|
|
'description',
|
|
contains('hasSize'),
|
|
),
|
|
),
|
|
);
|
|
expect(renderViewport.horizontalOffset.pixels, 20.0);
|
|
expect(renderViewport.horizontalAxisDirection, AxisDirection.right);
|
|
expect(renderViewport.verticalOffset.pixels, 10.0);
|
|
expect(renderViewport.verticalAxisDirection, AxisDirection.down);
|
|
expect(renderViewport.delegate, delegate);
|
|
expect(renderViewport.mainAxis, Axis.vertical);
|
|
|
|
// viewportDimension when hasSize
|
|
await tester.pumpWidget(simpleBuilderTest(
|
|
delegate: delegate,
|
|
));
|
|
await tester.pumpAndSettle();
|
|
final RenderTwoDimensionalViewport viewport = getViewport(tester, childKey);
|
|
expect(viewport.viewportDimension, const Size(800.0, 600.0));
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('Children are organized according to mainAxis', (WidgetTester tester) async {
|
|
final Map<ChildVicinity, UniqueKey> childKeys = <ChildVicinity, UniqueKey>{};
|
|
final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate(
|
|
maxXIndex: 5,
|
|
maxYIndex: 5,
|
|
builder: (BuildContext context, ChildVicinity vicinity) {
|
|
childKeys[vicinity] = UniqueKey();
|
|
return SizedBox.square(key: childKeys[vicinity], dimension: 200);
|
|
}
|
|
);
|
|
TwoDimensionalViewportParentData parentDataOf(RenderBox child) {
|
|
return child.parentData! as TwoDimensionalViewportParentData;
|
|
}
|
|
// mainAxis is vertical (default)
|
|
await tester.pumpWidget(simpleBuilderTest(
|
|
delegate: delegate,
|
|
));
|
|
await tester.pumpAndSettle();
|
|
RenderTwoDimensionalViewport viewport = getViewport(
|
|
tester,
|
|
childKeys.values.first,
|
|
);
|
|
expect(viewport.mainAxis, Axis.vertical);
|
|
// first child
|
|
expect(
|
|
parentDataOf(viewport.firstChild!).vicinity,
|
|
const ChildVicinity(xIndex: 0, yIndex: 0),
|
|
);
|
|
expect(
|
|
parentDataOf(viewport.childAfter(viewport.firstChild!)!).vicinity,
|
|
const ChildVicinity(xIndex: 1, yIndex: 0),
|
|
);
|
|
expect(
|
|
viewport.childBefore(viewport.firstChild!),
|
|
isNull,
|
|
);
|
|
// last child
|
|
expect(
|
|
parentDataOf(viewport.lastChild!).vicinity,
|
|
const ChildVicinity(xIndex: 4, yIndex: 3),
|
|
);
|
|
expect(
|
|
viewport.childAfter(viewport.lastChild!),
|
|
isNull,
|
|
);
|
|
expect(
|
|
parentDataOf(viewport.childBefore(viewport.lastChild!)!).vicinity,
|
|
const ChildVicinity(xIndex: 3, yIndex: 3),
|
|
);
|
|
|
|
// mainAxis is horizontal
|
|
await tester.pumpWidget(simpleBuilderTest(
|
|
delegate: delegate,
|
|
mainAxis: Axis.horizontal,
|
|
));
|
|
await tester.pumpAndSettle();
|
|
viewport = getViewport(tester, childKeys.values.first);
|
|
expect(viewport.mainAxis, Axis.horizontal);
|
|
// first child
|
|
expect(
|
|
parentDataOf(viewport.firstChild!).vicinity,
|
|
const ChildVicinity(xIndex: 0, yIndex: 0),
|
|
);
|
|
expect(
|
|
parentDataOf(viewport.childAfter(viewport.firstChild!)!).vicinity,
|
|
const ChildVicinity(xIndex: 0, yIndex: 1),
|
|
);
|
|
expect(
|
|
viewport.childBefore(viewport.firstChild!),
|
|
isNull,
|
|
);
|
|
// last child
|
|
expect(
|
|
parentDataOf(viewport.lastChild!).vicinity,
|
|
const ChildVicinity(xIndex: 4, yIndex: 3),
|
|
);
|
|
expect(
|
|
viewport.childAfter(viewport.lastChild!),
|
|
isNull,
|
|
);
|
|
expect(
|
|
parentDataOf(viewport.childBefore(viewport.lastChild!)!).vicinity,
|
|
const ChildVicinity(xIndex: 4, yIndex: 2),
|
|
);
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('sets up parent data', (WidgetTester tester) async {
|
|
// Also tests computeAbsolutePaintOffsetFor & computeChildPaintExtent
|
|
// Regression test for https://github.com/flutter/flutter/issues/128723
|
|
final Map<ChildVicinity, UniqueKey> childKeys = <ChildVicinity, UniqueKey>{};
|
|
final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate(
|
|
maxXIndex: 5,
|
|
maxYIndex: 5,
|
|
builder: (BuildContext context, ChildVicinity vicinity) {
|
|
childKeys[vicinity] = UniqueKey();
|
|
return SizedBox.square(key: childKeys[vicinity], dimension: 200);
|
|
}
|
|
);
|
|
|
|
// parent data is TwoDimensionalViewportParentData
|
|
TwoDimensionalViewportParentData parentDataOf(RenderBox child) {
|
|
return child.parentData! as TwoDimensionalViewportParentData;
|
|
}
|
|
|
|
await tester.pumpWidget(simpleBuilderTest(
|
|
delegate: delegate,
|
|
useCacheExtent: true,
|
|
));
|
|
await tester.pumpAndSettle();
|
|
|
|
RenderTwoDimensionalViewport viewport = getViewport(
|
|
tester,
|
|
childKeys.values.first,
|
|
);
|
|
|
|
// first child
|
|
// parentData is computed correctly - normal axes
|
|
// - layoutOffset, paintOffset, isVisible, ChildVicinity
|
|
TwoDimensionalViewportParentData childParentData = parentDataOf(viewport.firstChild!);
|
|
expect(childParentData.vicinity, const ChildVicinity(xIndex: 0, yIndex: 0));
|
|
expect(childParentData.isVisible, isTrue);
|
|
expect(childParentData.paintOffset, Offset.zero);
|
|
expect(childParentData.layoutOffset, Offset.zero);
|
|
// The last child is in the cache extent, and should not be visible.
|
|
childParentData = parentDataOf(viewport.lastChild!);
|
|
expect(childParentData.vicinity, const ChildVicinity(xIndex: 5, yIndex: 5));
|
|
expect(childParentData.isVisible, isFalse);
|
|
expect(childParentData.paintOffset, const Offset(1000.0, 1000.0));
|
|
expect(childParentData.layoutOffset, const Offset(1000.0, 1000.0));
|
|
|
|
// parentData is computed correctly - reverse axes
|
|
// - vertical reverse
|
|
await tester.pumpWidget(simpleBuilderTest(
|
|
delegate: delegate,
|
|
verticalDetails: const ScrollableDetails.vertical(reverse: true),
|
|
));
|
|
await tester.pumpAndSettle();
|
|
|
|
viewport = getViewport(tester, childKeys.values.first);
|
|
|
|
childParentData = parentDataOf(viewport.firstChild!);
|
|
expect(childParentData.vicinity, const ChildVicinity(xIndex: 0, yIndex: 0));
|
|
expect(childParentData.isVisible, isTrue);
|
|
expect(childParentData.paintOffset, const Offset(0.0, 400.0));
|
|
expect(childParentData.layoutOffset, Offset.zero);
|
|
// The last child is in the cache extent, and should not be visible.
|
|
childParentData = parentDataOf(viewport.lastChild!);
|
|
expect(childParentData.vicinity, const ChildVicinity(xIndex: 5, yIndex: 5));
|
|
expect(childParentData.isVisible, isFalse);
|
|
expect(childParentData.paintOffset, const Offset(1000.0, -600.0));
|
|
expect(childParentData.layoutOffset, const Offset(1000.0, 1000.0));
|
|
|
|
// - horizontal reverse
|
|
await tester.pumpWidget(simpleBuilderTest(
|
|
delegate: delegate,
|
|
horizontalDetails: const ScrollableDetails.horizontal(reverse: true),
|
|
));
|
|
await tester.pumpAndSettle();
|
|
|
|
viewport = getViewport(tester, childKeys.values.first);
|
|
|
|
childParentData = parentDataOf(viewport.firstChild!);
|
|
expect(childParentData.vicinity, const ChildVicinity(xIndex: 0, yIndex: 0));
|
|
expect(childParentData.isVisible, isTrue);
|
|
expect(childParentData.paintOffset, const Offset(600.0, 0.0));
|
|
expect(childParentData.layoutOffset, Offset.zero);
|
|
// The last child is in the cache extent, and should not be visible.
|
|
childParentData = parentDataOf(viewport.lastChild!);
|
|
expect(childParentData.vicinity, const ChildVicinity(xIndex: 5, yIndex: 5));
|
|
expect(childParentData.isVisible, isFalse);
|
|
expect(childParentData.paintOffset, const Offset(-400.0, 1000.0));
|
|
expect(childParentData.layoutOffset, const Offset(1000.0, 1000.0));
|
|
|
|
// - both reverse
|
|
await tester.pumpWidget(simpleBuilderTest(
|
|
delegate: delegate,
|
|
horizontalDetails: const ScrollableDetails.horizontal(reverse: true),
|
|
verticalDetails: const ScrollableDetails.vertical(reverse: true),
|
|
));
|
|
await tester.pumpAndSettle();
|
|
|
|
viewport = getViewport(tester, childKeys.values.first);
|
|
|
|
childParentData = parentDataOf(viewport.firstChild!);
|
|
expect(childParentData.vicinity, const ChildVicinity(xIndex: 0, yIndex: 0));
|
|
expect(childParentData.isVisible, isTrue);
|
|
expect(childParentData.paintOffset, const Offset(600.0, 400.0));
|
|
expect(childParentData.layoutOffset, Offset.zero);
|
|
// The last child is in the cache extent, and should not be visible.
|
|
childParentData = parentDataOf(viewport.lastChild!);
|
|
expect(childParentData.vicinity, const ChildVicinity(xIndex: 5, yIndex: 5));
|
|
expect(childParentData.isVisible, isFalse);
|
|
expect(childParentData.paintOffset, const Offset(-400.0, -600.0));
|
|
expect(childParentData.layoutOffset, const Offset(1000.0, 1000.0));
|
|
|
|
// Change the scroll positions to test partially visible.
|
|
final ScrollController verticalController = ScrollController();
|
|
final ScrollController horizontalController = ScrollController();
|
|
await tester.pumpWidget(simpleBuilderTest(
|
|
delegate: delegate,
|
|
horizontalDetails: ScrollableDetails.horizontal(controller: horizontalController),
|
|
verticalDetails: ScrollableDetails.vertical(controller: verticalController),
|
|
));
|
|
await tester.pumpAndSettle();
|
|
verticalController.jumpTo(50.0);
|
|
horizontalController.jumpTo(50.0);
|
|
await tester.pump();
|
|
|
|
viewport = getViewport(tester, childKeys.values.first);
|
|
|
|
childParentData = parentDataOf(viewport.firstChild!);
|
|
expect(childParentData.vicinity, const ChildVicinity(xIndex: 0, yIndex: 0));
|
|
expect(childParentData.isVisible, isTrue);
|
|
expect(childParentData.paintOffset, const Offset(-50.0, -50.0));
|
|
expect(childParentData.layoutOffset, const Offset(-50.0, -50.0));
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('debugDescribeChildren', (WidgetTester tester) async {
|
|
final Map<ChildVicinity, UniqueKey> childKeys = <ChildVicinity, UniqueKey>{};
|
|
final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate(
|
|
maxXIndex: 5,
|
|
maxYIndex: 5,
|
|
builder: (BuildContext context, ChildVicinity vicinity) {
|
|
childKeys[vicinity] = UniqueKey();
|
|
return SizedBox.square(key: childKeys[vicinity], dimension: 200);
|
|
}
|
|
);
|
|
|
|
await tester.pumpWidget(simpleBuilderTest(
|
|
delegate: delegate,
|
|
));
|
|
await tester.pumpAndSettle();
|
|
|
|
final RenderTwoDimensionalViewport viewport = getViewport(
|
|
tester,
|
|
childKeys.values.first,
|
|
);
|
|
final List<DiagnosticsNode> result = viewport.debugDescribeChildren();
|
|
expect(result.length, 20);
|
|
expect(
|
|
result.first.toString(),
|
|
equalsIgnoringHashCodes('(xIndex: 0, yIndex: 0): RenderRepaintBoundary#00000'),
|
|
);
|
|
expect(
|
|
result.last.toString(),
|
|
equalsIgnoringHashCodes('(xIndex: 4, yIndex: 3): RenderRepaintBoundary#00000 NEEDS-PAINT'),
|
|
);
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('asserts that both axes are bounded', (WidgetTester tester) async {
|
|
final List<Object> exceptions = <Object>[];
|
|
final FlutterExceptionHandler? oldHandler = FlutterError.onError;
|
|
FlutterError.onError = (FlutterErrorDetails details) {
|
|
exceptions.add(details.exception);
|
|
};
|
|
// Compose unbounded - vertical axis
|
|
await tester.pumpWidget(WidgetsApp(
|
|
color: const Color(0xFFFFFFFF),
|
|
builder: (BuildContext context, Widget? child) => Column(
|
|
children: <Widget>[
|
|
SimpleBuilderTableView(delegate: builderDelegate)
|
|
]
|
|
),
|
|
));
|
|
await tester.pumpAndSettle();
|
|
FlutterError.onError = oldHandler;
|
|
expect(exceptions.isNotEmpty, isTrue);
|
|
expect((exceptions[0] as FlutterError).message, contains('unbounded'));
|
|
|
|
exceptions.clear();
|
|
FlutterError.onError = (FlutterErrorDetails details) {
|
|
exceptions.add(details.exception);
|
|
};
|
|
// Compose unbounded - horizontal axis
|
|
await tester.pumpWidget(WidgetsApp(
|
|
color: const Color(0xFFFFFFFF),
|
|
builder: (BuildContext context, Widget? child) => Row(
|
|
children: <Widget>[
|
|
SimpleBuilderTableView(delegate: builderDelegate)
|
|
]
|
|
),
|
|
));
|
|
await tester.pumpAndSettle();
|
|
FlutterError.onError = oldHandler;
|
|
expect(exceptions.isNotEmpty, isTrue);
|
|
expect((exceptions[0] as FlutterError).message, contains('unbounded'));
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('computeDryLayout asserts axes are bounded', (WidgetTester tester) async {
|
|
final UniqueKey childKey = UniqueKey();
|
|
final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate(
|
|
maxXIndex: 0,
|
|
maxYIndex: 0,
|
|
builder: (BuildContext context, ChildVicinity vicinity) {
|
|
return SizedBox.square(key: childKey, dimension: 200);
|
|
}
|
|
);
|
|
// Call computeDryLayout with unbounded constraints
|
|
await tester.pumpWidget(simpleBuilderTest(delegate: delegate));
|
|
final RenderTwoDimensionalViewport viewport = getViewport(
|
|
tester,
|
|
childKey,
|
|
);
|
|
expect(
|
|
() {
|
|
viewport.computeDryLayout(const BoxConstraints());
|
|
},
|
|
throwsA(
|
|
isA<FlutterError>().having(
|
|
(FlutterError error) => error.message,
|
|
'error.message',
|
|
contains('unbounded'),
|
|
),
|
|
),
|
|
);
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('correctly resizes dimensions', (WidgetTester tester) async {
|
|
final UniqueKey childKey = UniqueKey();
|
|
final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate(
|
|
maxXIndex: 0,
|
|
maxYIndex: 0,
|
|
builder: (BuildContext context, ChildVicinity vicinity) {
|
|
return SizedBox.square(key: childKey, dimension: 200);
|
|
}
|
|
);
|
|
await tester.pumpWidget(simpleBuilderTest(
|
|
delegate: delegate,
|
|
));
|
|
await tester.pumpAndSettle();
|
|
RenderTwoDimensionalViewport viewport = getViewport(
|
|
tester,
|
|
childKey,
|
|
);
|
|
expect(viewport.viewportDimension, const Size(800.0, 600.0));
|
|
tester.view.physicalSize = const Size(300.0, 300.0);
|
|
tester.view.devicePixelRatio = 1;
|
|
await tester.pumpWidget(simpleBuilderTest(
|
|
delegate: delegate,
|
|
));
|
|
await tester.pumpAndSettle();
|
|
viewport = getViewport(tester, childKey);
|
|
expect(viewport.viewportDimension, const Size(300.0, 300.0));
|
|
tester.view.resetPhysicalSize();
|
|
tester.view.resetDevicePixelRatio();
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('Rebuilds when delegate changes', (WidgetTester tester) async {
|
|
final UniqueKey firstChildKey = UniqueKey();
|
|
final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate(
|
|
maxXIndex: 0,
|
|
maxYIndex: 0,
|
|
addRepaintBoundaries: false,
|
|
builder: (BuildContext context, ChildVicinity vicinity) {
|
|
return SizedBox.square(key: firstChildKey, dimension: 200);
|
|
}
|
|
);
|
|
await tester.pumpWidget(simpleBuilderTest(
|
|
delegate: delegate,
|
|
));
|
|
RenderTwoDimensionalViewport viewport = getViewport(tester, firstChildKey);
|
|
expect(viewport.firstChild, tester.renderObject<RenderBox>(find.byKey(firstChildKey)));
|
|
// New delegate
|
|
final UniqueKey newChildKey = UniqueKey();
|
|
final TwoDimensionalChildBuilderDelegate newDelegate = TwoDimensionalChildBuilderDelegate(
|
|
maxXIndex: 0,
|
|
maxYIndex: 0,
|
|
addRepaintBoundaries: false,
|
|
builder: (BuildContext context, ChildVicinity vicinity) {
|
|
return Container(key: newChildKey, height: 300, width: 300, color: const Color(0xFFFFFFFF));
|
|
}
|
|
);
|
|
await tester.pumpWidget(simpleBuilderTest(
|
|
delegate: newDelegate,
|
|
));
|
|
viewport = getViewport(tester, newChildKey);
|
|
expect(firstChildKey, isNot(newChildKey));
|
|
expect(find.byKey(firstChildKey), findsNothing);
|
|
expect(find.byKey(newChildKey), findsOneWidget);
|
|
expect(viewport.firstChild, tester.renderObject<RenderBox>(find.byKey(newChildKey)));
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('hitTestChildren', (WidgetTester tester) async {
|
|
final List<ChildVicinity> taps = <ChildVicinity>[];
|
|
final Map<ChildVicinity, UniqueKey> childKeys = <ChildVicinity, UniqueKey>{};
|
|
final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate(
|
|
maxXIndex: 19,
|
|
maxYIndex: 19,
|
|
builder: (BuildContext context, ChildVicinity vicinity) {
|
|
childKeys[vicinity] = UniqueKey();
|
|
return SizedBox.square(
|
|
dimension: 200,
|
|
child: Center(
|
|
child: FloatingActionButton(
|
|
key: childKeys[vicinity],
|
|
onPressed: () {
|
|
taps.add(vicinity);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
);
|
|
|
|
await tester.pumpWidget(simpleBuilderTest(
|
|
delegate: delegate,
|
|
useCacheExtent: true, // Untappable children are rendered in the cache extent
|
|
));
|
|
await tester.pumpAndSettle();
|
|
// Regular orientation
|
|
// Offset at center of first child
|
|
await tester.tapAt(const Offset(100.0, 100.0));
|
|
await tester.pump();
|
|
expect(taps.contains(const ChildVicinity(xIndex: 0, yIndex: 0)), isTrue);
|
|
// Offset by child location
|
|
await tester.tap(find.byKey(childKeys[const ChildVicinity(xIndex: 2, yIndex: 2)]!));
|
|
await tester.pump();
|
|
expect(taps.contains(const ChildVicinity(xIndex: 2, yIndex: 2)), isTrue);
|
|
// Offset out of bounds
|
|
await tester.tap(
|
|
find.byKey(childKeys[const ChildVicinity(xIndex: 5, yIndex: 5)]!),
|
|
warnIfMissed: false,
|
|
);
|
|
await tester.pump();
|
|
expect(taps.contains(const ChildVicinity(xIndex: 5, yIndex: 5)), isFalse);
|
|
|
|
// Reversed
|
|
await tester.pumpWidget(simpleBuilderTest(
|
|
delegate: delegate,
|
|
verticalDetails: const ScrollableDetails.vertical(reverse: true),
|
|
horizontalDetails: const ScrollableDetails.horizontal(reverse: true),
|
|
useCacheExtent: true, // Untappable children are rendered in the cache extent
|
|
));
|
|
await tester.pumpAndSettle();
|
|
// Offset at center of first child
|
|
await tester.tapAt(const Offset(700.0, 500.0));
|
|
await tester.pump();
|
|
expect(taps.contains(const ChildVicinity(xIndex: 0, yIndex: 0)), isTrue);
|
|
// Offset by child location
|
|
await tester.tap(find.byKey(childKeys[const ChildVicinity(xIndex: 2, yIndex: 2)]!));
|
|
await tester.pump();
|
|
expect(taps.contains(const ChildVicinity(xIndex: 2, yIndex: 2)), isTrue);
|
|
// Offset out of bounds
|
|
await tester.tap(
|
|
find.byKey(childKeys[const ChildVicinity(xIndex: 5, yIndex: 5)]!),
|
|
warnIfMissed: false,
|
|
);
|
|
await tester.pump();
|
|
expect(taps.contains(const ChildVicinity(xIndex: 5, yIndex: 5)), isFalse);
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('getChildFor', (WidgetTester tester) async {
|
|
final Map<ChildVicinity, UniqueKey> childKeys = <ChildVicinity, UniqueKey>{};
|
|
final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate(
|
|
maxXIndex: 5,
|
|
maxYIndex: 5,
|
|
builder: (BuildContext context, ChildVicinity vicinity) {
|
|
childKeys[vicinity] = UniqueKey();
|
|
return SizedBox.square(key: childKeys[vicinity], dimension: 200);
|
|
}
|
|
);
|
|
|
|
await tester.pumpWidget(simpleBuilderTest(
|
|
delegate: delegate,
|
|
));
|
|
await tester.pumpAndSettle();
|
|
|
|
final RenderSimpleBuilderTableViewport viewport = getViewport(
|
|
tester, childKeys.values.first,
|
|
) as RenderSimpleBuilderTableViewport;
|
|
// returns child
|
|
expect(
|
|
viewport.testGetChildFor(const ChildVicinity(xIndex: 0, yIndex: 0)),
|
|
isNotNull,
|
|
);
|
|
expect(
|
|
viewport.testGetChildFor(const ChildVicinity(xIndex: 0, yIndex: 0)),
|
|
viewport.firstChild,
|
|
);
|
|
|
|
// returns null
|
|
expect(
|
|
viewport.testGetChildFor(const ChildVicinity(xIndex: 10, yIndex: 10)),
|
|
isNull,
|
|
);
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('asserts vicinity is valid when children are asked to build', (WidgetTester tester) async {
|
|
final Map<ChildVicinity, UniqueKey> childKeys = <ChildVicinity, UniqueKey>{};
|
|
final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate(
|
|
maxXIndex: 5,
|
|
maxYIndex: 5,
|
|
builder: (BuildContext context, ChildVicinity vicinity) {
|
|
childKeys[vicinity] = UniqueKey();
|
|
return SizedBox.square(key: childKeys[vicinity], dimension: 200);
|
|
}
|
|
);
|
|
|
|
await tester.pumpWidget(simpleBuilderTest(
|
|
delegate: delegate,
|
|
));
|
|
await tester.pumpAndSettle();
|
|
|
|
final RenderTwoDimensionalViewport viewport = getViewport(
|
|
tester,
|
|
childKeys.values.first,
|
|
);
|
|
expect(
|
|
() {
|
|
viewport.buildOrObtainChildFor(ChildVicinity.invalid);
|
|
},
|
|
throwsA(
|
|
isA<AssertionError>().having(
|
|
(AssertionError error) => error.toString(),
|
|
'description',
|
|
contains('ChildVicinity.invalid'),
|
|
),
|
|
),
|
|
);
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('asserts that content dimensions have been applied', (WidgetTester tester) async {
|
|
final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate(
|
|
maxXIndex: 5,
|
|
maxYIndex: 5,
|
|
builder: (BuildContext context, ChildVicinity vicinity) {
|
|
return const SizedBox.square(dimension: 200);
|
|
}
|
|
);
|
|
await tester.pumpWidget(simpleBuilderTest(
|
|
delegate: delegate,
|
|
// Will cause the test implementation to not set dimensions
|
|
applyDimensions: false,
|
|
));
|
|
final FlutterError error = tester.takeException() as FlutterError;
|
|
expect(error.message, contains('was not given content dimensions'));
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('will not rebuild a child if it can be reused', (WidgetTester tester) async {
|
|
final List<ChildVicinity> builtChildren = <ChildVicinity>[];
|
|
final ScrollController controller = ScrollController();
|
|
final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate(
|
|
maxXIndex: 5,
|
|
maxYIndex: 5,
|
|
builder: (BuildContext context, ChildVicinity vicinity) {
|
|
builtChildren.add(vicinity);
|
|
return const SizedBox.square(dimension: 200);
|
|
}
|
|
);
|
|
|
|
await tester.pumpWidget(simpleBuilderTest(
|
|
delegate: delegate,
|
|
verticalDetails: ScrollableDetails.vertical(controller: controller),
|
|
));
|
|
expect(controller.position.pixels, 0.0);
|
|
expect(builtChildren.length, 20);
|
|
expect(builtChildren[0], const ChildVicinity(xIndex: 0, yIndex: 0));
|
|
builtChildren.clear();
|
|
controller.jumpTo(1.0); // Move slightly to trigger another layout
|
|
await tester.pump();
|
|
expect(controller.position.pixels, 1.0);
|
|
expect(builtChildren.length, 5); // Next row of children was built
|
|
// Children from the first layout pass were re-used, not rebuilt.
|
|
expect(
|
|
builtChildren.contains(const ChildVicinity(xIndex: 0, yIndex: 0)),
|
|
isFalse,
|
|
);
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('asserts the layoutOffset has been set by the subclass', (WidgetTester tester) async {
|
|
final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate(
|
|
maxXIndex: 5,
|
|
maxYIndex: 5,
|
|
builder: (BuildContext context, ChildVicinity vicinity) {
|
|
return const SizedBox.square(dimension: 200);
|
|
}
|
|
);
|
|
await tester.pumpWidget(simpleBuilderTest(
|
|
delegate: delegate,
|
|
// Will cause the test implementation to not set the layoutOffset of
|
|
// the parent data
|
|
setLayoutOffset: false,
|
|
));
|
|
final AssertionError error = tester.takeException() as AssertionError;
|
|
expect(error.message, contains('was not provided a layoutOffset'));
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('asserts the children have a size after layoutChildSequence', (WidgetTester tester) async {
|
|
final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate(
|
|
maxXIndex: 5,
|
|
maxYIndex: 5,
|
|
builder: (BuildContext context, ChildVicinity vicinity) {
|
|
return const SizedBox.square(dimension: 200);
|
|
}
|
|
);
|
|
await tester.pumpWidget(simpleBuilderTest(
|
|
delegate: delegate,
|
|
// Will cause the test implementation to not actually layout the
|
|
// children it asked for.
|
|
forgetToLayoutChild: true,
|
|
));
|
|
final AssertionError error = tester.takeException() as AssertionError;
|
|
expect(error.toString(), contains('child.hasSize'));
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('does not support intrinsics', (WidgetTester tester) async {
|
|
final Map<ChildVicinity, UniqueKey> childKeys = <ChildVicinity, UniqueKey>{};
|
|
final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate(
|
|
maxXIndex: 5,
|
|
maxYIndex: 5,
|
|
builder: (BuildContext context, ChildVicinity vicinity) {
|
|
childKeys[vicinity] = UniqueKey();
|
|
return SizedBox.square(key: childKeys[vicinity], dimension: 200);
|
|
}
|
|
);
|
|
|
|
await tester.pumpWidget(simpleBuilderTest(
|
|
delegate: delegate,
|
|
));
|
|
await tester.pumpAndSettle();
|
|
|
|
final RenderTwoDimensionalViewport viewport = getViewport(
|
|
tester,
|
|
childKeys.values.first,
|
|
);
|
|
expect(
|
|
() {
|
|
viewport.computeMinIntrinsicWidth(100);
|
|
},
|
|
throwsA(
|
|
isA<AssertionError>().having(
|
|
(AssertionError error) => error.toString(),
|
|
'description',
|
|
contains('does not support returning intrinsic dimensions'),
|
|
),
|
|
),
|
|
);
|
|
expect(
|
|
() {
|
|
viewport.computeMaxIntrinsicWidth(100);
|
|
},
|
|
throwsA(
|
|
isA<AssertionError>().having(
|
|
(AssertionError error) => error.toString(),
|
|
'description',
|
|
contains('does not support returning intrinsic dimensions'),
|
|
),
|
|
),
|
|
);
|
|
expect(
|
|
() {
|
|
viewport.computeMinIntrinsicHeight(100);
|
|
},
|
|
throwsA(
|
|
isA<AssertionError>().having(
|
|
(AssertionError error) => error.toString(),
|
|
'description',
|
|
contains('does not support returning intrinsic dimensions'),
|
|
),
|
|
),
|
|
);
|
|
expect(
|
|
() {
|
|
viewport.computeMaxIntrinsicHeight(100);
|
|
},
|
|
throwsA(
|
|
isA<AssertionError>().having(
|
|
(AssertionError error) => error.toString(),
|
|
'description',
|
|
contains('does not support returning intrinsic dimensions'),
|
|
),
|
|
),
|
|
);
|
|
}, variant: TargetPlatformVariant.all());
|
|
});
|
|
}
|
|
|
|
RenderTwoDimensionalViewport getViewport(WidgetTester tester, Key childKey) {
|
|
return RenderAbstractViewport.of(
|
|
tester.renderObject(find.byKey(childKey))
|
|
) as RenderSimpleBuilderTableViewport;
|
|
}
|
|
|
|
class _NullBuildContext implements BuildContext, TwoDimensionalChildManager {
|
|
@override
|
|
dynamic noSuchMethod(Invocation invocation) => throw UnimplementedError();
|
|
}
|
|
|
|
Future<void> restoreScrollAndVerify(WidgetTester tester) async {
|
|
final Finder findScrollable = find.byElementPredicate((Element e) => e.widget is TwoDimensionalScrollable);
|
|
|
|
tester.state<TwoDimensionalScrollableState>(findScrollable).horizontalScrollable.position.jumpTo(100);
|
|
tester.state<TwoDimensionalScrollableState>(findScrollable).verticalScrollable.position.jumpTo(100);
|
|
await tester.pump();
|
|
await tester.restartAndRestore();
|
|
|
|
expect(
|
|
tester.state<TwoDimensionalScrollableState>(findScrollable).horizontalScrollable.position.pixels,
|
|
100.0,
|
|
);
|
|
expect(
|
|
tester.state<TwoDimensionalScrollableState>(findScrollable).verticalScrollable.position.pixels,
|
|
100.0,
|
|
);
|
|
|
|
final TestRestorationData data = await tester.getRestorationData();
|
|
tester.state<TwoDimensionalScrollableState>(findScrollable).horizontalScrollable.position.jumpTo(0);
|
|
tester.state<TwoDimensionalScrollableState>(findScrollable).verticalScrollable.position.jumpTo(0);
|
|
await tester.pump();
|
|
await tester.restoreFrom(data);
|
|
|
|
expect(
|
|
tester.state<TwoDimensionalScrollableState>(findScrollable).horizontalScrollable.position.pixels,
|
|
100.0,
|
|
);
|
|
expect(
|
|
tester.state<TwoDimensionalScrollableState>(findScrollable).verticalScrollable.position.pixels,
|
|
100.0,
|
|
);
|
|
}
|
|
|
|
// Validates covariant through analysis.
|
|
mixin _SomeDelegateMixin on TwoDimensionalChildDelegate {}
|
|
|
|
class _SomeRenderTwoDimensionalViewport extends RenderTwoDimensionalViewport { // ignore: unused_element
|
|
_SomeRenderTwoDimensionalViewport({
|
|
required super.horizontalOffset,
|
|
required super.horizontalAxisDirection,
|
|
required super.verticalOffset,
|
|
required super.verticalAxisDirection,
|
|
required _SomeDelegateMixin super.delegate,
|
|
required super.mainAxis,
|
|
required super.childManager,
|
|
});
|
|
|
|
@override
|
|
_SomeDelegateMixin get delegate => super.delegate as _SomeDelegateMixin;
|
|
@override
|
|
set delegate(_SomeDelegateMixin value) { // Analysis would fail without covariant
|
|
super.delegate = value;
|
|
}
|
|
|
|
@override
|
|
void layoutChildSequence() {}
|
|
}
|