7c9627fffb
Changelog: Optional two-tier Liquid Glass and slide-to-pick menus
2069 lines
61 KiB
Dart
2069 lines
61 KiB
Dart
import 'dart:typed_data';
|
|
|
|
import 'package:fl_clash/ui/icons/icons.dart';
|
|
import 'package:fl_clash/ui/liquid_glass.dart';
|
|
import 'package:fl_clash/ui/navigation_dock.dart';
|
|
import 'package:fl_clash/ui/popup.dart';
|
|
import 'package:fl_clash/ui/surface.dart';
|
|
import 'package:flutter/gestures.dart' show PointerDeviceKind, kPressTimeout;
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:material_ui/material_ui.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import '../helpers/glyph_finders.dart';
|
|
import '../helpers/test_app.dart';
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
final surfaces = find.byType(PopupMenuCard);
|
|
|
|
PopupMenuCard cardOf(WidgetTester tester, Finder surface) =>
|
|
tester.widget<PopupMenuCard>(surface);
|
|
|
|
double shadowOf(WidgetTester tester, Finder surface) => cardOf(
|
|
tester,
|
|
surface,
|
|
).shadows.fold(0.0, (sum, shadow) => sum + shadow.color.a);
|
|
|
|
Rect shrinkingContent(WidgetTester tester) {
|
|
final content = tester.layers.whereType<ImageFilterLayer>().last;
|
|
final transform = content.firstChild! as TransformLayer;
|
|
final popup = tester.getRect(find.byKey(const Key('popup')));
|
|
return MatrixUtils.transformRect(
|
|
transform.transform!,
|
|
Offset.zero & popup.size,
|
|
).shift(popup.topLeft);
|
|
}
|
|
|
|
Rect painted(WidgetTester tester, Finder press) {
|
|
final resting = tester.getRect(press);
|
|
Matrix4? transform;
|
|
expect(
|
|
tester.renderObject<RenderProxyBox>(press).child,
|
|
paints..everything((method, arguments) {
|
|
if (method == #transform) {
|
|
transform ??= Matrix4.fromFloat64List(
|
|
arguments.single as Float64List,
|
|
);
|
|
}
|
|
return true;
|
|
}),
|
|
);
|
|
final local = Offset.zero & resting.size;
|
|
return (transform == null
|
|
? local
|
|
: MatrixUtils.transformRect(transform!, local))
|
|
.shift(resting.topLeft);
|
|
}
|
|
|
|
Future<PopupOpen> pumpBox(WidgetTester tester) async {
|
|
late PopupOpen open;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: CommonPopupBox(
|
|
targetBuilder: (value) {
|
|
open = value;
|
|
return const SizedBox(width: 40, height: 40);
|
|
},
|
|
popupBuilder: (_) =>
|
|
const SizedBox(width: 80, height: 80, key: Key('popup')),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
return open;
|
|
}
|
|
|
|
Future<PopupOpen> pumpMenu(
|
|
WidgetTester tester,
|
|
List<CommonPopupMenuItem> items, {
|
|
int? maxVisibleItems,
|
|
}) async {
|
|
late PopupOpen open;
|
|
await tester.pumpWidget(
|
|
TestApp(
|
|
homeBuilder: (child) => Scaffold(body: Center(child: child)),
|
|
child: CommonPopupBox(
|
|
targetBuilder: (value) {
|
|
open = value;
|
|
return const SizedBox(width: 40, height: 40);
|
|
},
|
|
popupBuilder: (_) =>
|
|
CommonPopupMenu(items: items, maxVisibleItems: maxVisibleItems),
|
|
),
|
|
),
|
|
);
|
|
return open;
|
|
}
|
|
|
|
testWidgets('opening pushes the popup above the target', (tester) async {
|
|
final open = await pumpBox(tester);
|
|
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('popup')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('a menu over a button at the top covers the bar it stands in', (
|
|
tester,
|
|
) async {
|
|
late PopupOpen open;
|
|
await tester.pumpWidget(
|
|
TestApp(
|
|
homeBuilder: (child) => Scaffold(body: child),
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(top: 14),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const SizedBox(width: 40, height: 40, key: Key('neighbour')),
|
|
CommonPopupBox(
|
|
targetBuilder: (value) {
|
|
open = value;
|
|
return const SizedBox(width: 40, height: 40);
|
|
},
|
|
popupBuilder: (_) => CommonPopupMenu(
|
|
items: [CommonPopupMenuItem(label: 'one', onPressed: () {})],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
tester.getRect(find.byType(PopupMenuCard)).top,
|
|
lessThan(tester.getRect(find.byKey(const Key('neighbour'))).top),
|
|
);
|
|
});
|
|
|
|
testWidgets('a popup fills from the elevated surface it opened on', (
|
|
tester,
|
|
) async {
|
|
const lifted = Color(0xFF505050);
|
|
late PopupOpen open;
|
|
await tester.pumpWidget(
|
|
TestApp(
|
|
theme: ThemeData(
|
|
extensions: const [
|
|
ElevatedColorScheme(ColorScheme.dark(surfaceContainerHigh: lifted)),
|
|
],
|
|
),
|
|
homeBuilder: (child) => Scaffold(body: Center(child: child)),
|
|
child: ElevatedSurfaceTheme(
|
|
child: CommonPopupBox(
|
|
targetBuilder: (value) {
|
|
open = value;
|
|
return const SizedBox(width: 40, height: 40);
|
|
},
|
|
popupBuilder: (_) => CommonPopupMenu(
|
|
items: [CommonPopupMenuItem(label: 'one', onPressed: () {})],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(cardOf(tester, find.byType(PopupMenuCard)).fill, lifted);
|
|
});
|
|
|
|
testWidgets('the popup is only built once it opens', (tester) async {
|
|
var builds = 0;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: CommonPopupBox(
|
|
targetBuilder: (_) => const SizedBox(width: 40, height: 40),
|
|
popupBuilder: (_) {
|
|
builds++;
|
|
return const SizedBox.shrink();
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(builds, 0);
|
|
});
|
|
|
|
testWidgets('the box can be removed while its popup is still up', (
|
|
tester,
|
|
) async {
|
|
// The box goes away under an open popup while the navigator carrying that
|
|
// popup stays; the route then holds an anchor it can no longer measure.
|
|
final showBox = ValueNotifier<bool>(true);
|
|
addTearDown(showBox.dispose);
|
|
late PopupOpen open;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: ValueListenableBuilder<bool>(
|
|
valueListenable: showBox,
|
|
builder: (_, visible, _) {
|
|
if (!visible) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
return CommonPopupBox(
|
|
targetBuilder: (value) {
|
|
open = value;
|
|
return const SizedBox(width: 40, height: 40);
|
|
},
|
|
popupBuilder: (_) =>
|
|
const SizedBox(width: 80, height: 80, key: Key('popup')),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
|
|
showBox.value = false;
|
|
await tester.pumpAndSettle();
|
|
expect(find.byKey(const Key('popup')), findsOneWidget);
|
|
|
|
await tester.tapAt(const Offset(10, 10));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('popup')), findsNothing);
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets('a point placement opens the popup from that point', (
|
|
tester,
|
|
) async {
|
|
late BuildContext context;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Builder(
|
|
builder: (value) {
|
|
context = value;
|
|
return const SizedBox.expand();
|
|
},
|
|
),
|
|
),
|
|
);
|
|
|
|
Navigator.of(context).push(
|
|
CommonPopupRoute<void>(
|
|
barrierLabel: 'Dismiss',
|
|
placement: PopupPlacement.belowPoint,
|
|
anchorOf: () => const Offset(100, 120) & Size.zero,
|
|
builder: (_) =>
|
|
const SizedBox(width: 80, height: 80, key: Key('popup')),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
tester.getTopLeft(find.byKey(const Key('popup'))),
|
|
const Offset(100, 120),
|
|
);
|
|
});
|
|
|
|
for (final (name, point, avoid, top) in [
|
|
('flips above a point too close to the bottom', 560.0, null, 480.0),
|
|
(
|
|
'sits above the rect it avoids',
|
|
250.0,
|
|
const Rect.fromLTRB(0, 200, 800, 300),
|
|
112.0,
|
|
),
|
|
(
|
|
'drops below the rect it avoids when above is full',
|
|
250.0,
|
|
const Rect.fromLTRB(0, 40, 800, 300),
|
|
308.0,
|
|
),
|
|
]) {
|
|
testWidgets('a point placement $name', (tester) async {
|
|
late BuildContext context;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Builder(
|
|
builder: (value) {
|
|
context = value;
|
|
return const SizedBox.expand();
|
|
},
|
|
),
|
|
),
|
|
);
|
|
|
|
Navigator.of(context).push(
|
|
CommonPopupRoute<void>(
|
|
barrierLabel: 'Dismiss',
|
|
placement: PopupPlacement.belowPoint,
|
|
anchorOf: () => Offset(100, point) & Size.zero,
|
|
avoid: avoid,
|
|
builder: (_) =>
|
|
const SizedBox(width: 80, height: 80, key: Key('popup')),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(tester.getTopLeft(find.byKey(const Key('popup'))).dy, top);
|
|
});
|
|
}
|
|
|
|
testWidgets(
|
|
'a point placement flips above a point the keyboard would cover',
|
|
(tester) async {
|
|
tester.view.viewInsets = const FakeViewPadding(bottom: 900);
|
|
addTearDown(tester.view.resetViewInsets);
|
|
late BuildContext context;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Builder(
|
|
builder: (value) {
|
|
context = value;
|
|
return const SizedBox.expand();
|
|
},
|
|
),
|
|
),
|
|
);
|
|
|
|
Navigator.of(context).push(
|
|
CommonPopupRoute<void>(
|
|
barrierLabel: 'Dismiss',
|
|
placement: PopupPlacement.belowPoint,
|
|
anchorOf: () => const Offset(100, 250) & Size.zero,
|
|
builder: (_) =>
|
|
const SizedBox(width: 80, height: 80, key: Key('popup')),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(tester.getBottomLeft(find.byKey(const Key('popup'))).dy, 250);
|
|
},
|
|
);
|
|
|
|
testWidgets('a non-modal popup lets touches and focus stay on the page', (
|
|
tester,
|
|
) async {
|
|
var taps = 0;
|
|
final focusNode = FocusNode();
|
|
addTearDown(focusNode.dispose);
|
|
late BuildContext context;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Builder(
|
|
builder: (value) {
|
|
context = value;
|
|
return Focus(
|
|
focusNode: focusNode,
|
|
autofocus: true,
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () => taps++,
|
|
child: const SizedBox.expand(),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
expect(focusNode.hasFocus, isTrue);
|
|
|
|
Navigator.of(context).push(
|
|
CommonPopupRoute<void>(
|
|
barrierLabel: 'Dismiss',
|
|
placement: PopupPlacement.belowPoint,
|
|
modal: false,
|
|
anchorOf: () => const Offset(100, 120) & Size.zero,
|
|
builder: (_) =>
|
|
const SizedBox(width: 80, height: 80, key: Key('popup')),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(focusNode.hasFocus, isTrue);
|
|
|
|
await tester.tapAt(const Offset(10, 10));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(taps, 1);
|
|
expect(find.byKey(const Key('popup')), findsNothing);
|
|
expect(focusNode.hasFocus, isTrue);
|
|
});
|
|
|
|
testWidgets('only a checked item carries a check', (tester) async {
|
|
final open = await pumpMenu(tester, [
|
|
const CommonPopupMenuItem(label: 'on', checked: true),
|
|
const CommonPopupMenuItem(label: 'off', checked: false),
|
|
const CommonPopupMenuItem(label: 'plain'),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
find.descendant(
|
|
of: find.ancestor(of: find.text('on'), matching: find.byType(Row)),
|
|
matching: find.byGlyph(AppGlyphs.check),
|
|
),
|
|
findsOneWidget,
|
|
);
|
|
expect(find.byGlyph(AppGlyphs.check), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('the popup grows out of its anchor and closes back into it', (
|
|
tester,
|
|
) async {
|
|
final open = await pumpBox(tester);
|
|
final anchor = tester.getRect(find.byType(CommonPopupBox));
|
|
double sourceOpacity() => tester
|
|
.widget<Opacity>(
|
|
find.descendant(
|
|
of: find.byType(CommonPopupBox),
|
|
matching: find.byType(Opacity),
|
|
),
|
|
)
|
|
.opacity;
|
|
Rect? platter() {
|
|
final clips = tester.layers.whereType<ClipPathLayer>();
|
|
return clips.isEmpty ? null : clips.last.clipPath!.getBounds();
|
|
}
|
|
|
|
open();
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
|
|
final opening = platter()!;
|
|
final popup = tester.getRect(find.byKey(const Key('popup')));
|
|
expect(opening.width, lessThan(popup.width));
|
|
expect(opening.height, lessThan(popup.height));
|
|
expect(opening.overlaps(anchor), isTrue);
|
|
|
|
await tester.pumpAndSettle();
|
|
expect(platter(), isNull);
|
|
expect(sourceOpacity(), 0);
|
|
|
|
Navigator.of(tester.element(find.byKey(const Key('popup')))).pop();
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 60));
|
|
|
|
final closing = shrinkingContent(tester);
|
|
expect(closing.width, lessThan(popup.width));
|
|
expect(closing.overlaps(anchor), isTrue);
|
|
|
|
await tester.pumpAndSettle();
|
|
expect(find.byKey(const Key('popup')), findsNothing);
|
|
expect(sourceOpacity(), 1);
|
|
});
|
|
|
|
testWidgets('the platter flies to the popup center on an arc', (
|
|
tester,
|
|
) async {
|
|
final open = await pumpBox(tester);
|
|
final anchor = tester.getRect(find.byType(CommonPopupBox)).center;
|
|
Offset platter() => tester.layers
|
|
.whereType<ClipPathLayer>()
|
|
.last
|
|
.clipPath!
|
|
.getBounds()
|
|
.center;
|
|
|
|
open();
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 60));
|
|
|
|
final opening = platter();
|
|
final popup = tester.getRect(find.byKey(const Key('popup'))).center;
|
|
final across = (opening.dx - anchor.dx) / (popup.dx - anchor.dx);
|
|
final down = (opening.dy - anchor.dy) / (popup.dy - anchor.dy);
|
|
expect(across, greaterThan(0));
|
|
expect(down, greaterThan(across + 0.1));
|
|
|
|
await tester.pumpAndSettle();
|
|
Navigator.of(tester.element(find.byKey(const Key('popup')))).pop();
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
|
|
final closing = shrinkingContent(tester).center;
|
|
final back = (popup.dx - closing.dx) / (popup.dx - anchor.dx);
|
|
final up = (popup.dy - closing.dy) / (popup.dy - anchor.dy);
|
|
expect(back, greaterThan(up + 0.1));
|
|
await tester.pumpAndSettle();
|
|
});
|
|
|
|
testWidgets('a popup from a point shrinks toward it as it fades', (
|
|
tester,
|
|
) async {
|
|
late BuildContext context;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Builder(
|
|
builder: (value) {
|
|
context = value;
|
|
return const SizedBox.expand();
|
|
},
|
|
),
|
|
),
|
|
);
|
|
const point = Offset(100, 120);
|
|
Navigator.of(context).push(
|
|
CommonPopupRoute<void>(
|
|
barrierLabel: 'Dismiss',
|
|
placement: PopupPlacement.belowPoint,
|
|
anchorOf: () => point & Size.zero,
|
|
builder: (_) =>
|
|
const SizedBox(width: 80, height: 80, key: Key('popup')),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
Navigator.of(context).pop();
|
|
await tester.pump();
|
|
var alpha = 255;
|
|
var faded = false;
|
|
while (find.byKey(const Key('popup')).evaluate().isNotEmpty) {
|
|
await tester.pump(const Duration(milliseconds: 16));
|
|
final opacity = tester.layers
|
|
.whereType<OpacityLayer>()
|
|
.where((layer) => layer.parent is TransformLayer)
|
|
.lastOrNull;
|
|
if (opacity == null) {
|
|
continue;
|
|
}
|
|
final transform = (opacity.parent! as TransformLayer).transform!;
|
|
final scale = transform.entry(0, 0);
|
|
expect(scale, greaterThanOrEqualTo(0.8));
|
|
expect(
|
|
MatrixUtils.transformPoint(transform, point),
|
|
offsetMoreOrLessEquals(point),
|
|
);
|
|
expect(
|
|
1 - opacity.alpha! / 255,
|
|
moreOrLessEquals((1 - scale) / 0.2, epsilon: 0.01),
|
|
);
|
|
expect(tester.layers.whereType<ImageFilterLayer>(), isEmpty);
|
|
expect(opacity.alpha!, lessThanOrEqualTo(alpha));
|
|
alpha = opacity.alpha!;
|
|
faded = faded || alpha < 64;
|
|
}
|
|
expect(faded, isTrue);
|
|
});
|
|
|
|
testWidgets('dismissing the barrier closes the popup', (tester) async {
|
|
final open = await pumpBox(tester);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tapAt(const Offset(10, 10));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('popup')), findsNothing);
|
|
});
|
|
|
|
testWidgets('a sub menu opens above the level that owns it', (tester) async {
|
|
final open = await pumpMenu(tester, [
|
|
const CommonPopupMenuItem(
|
|
label: 'parent',
|
|
subItems: [CommonPopupMenuItem(label: 'child')],
|
|
),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byGlyph(AppGlyphs.chevronForward), findsOneWidget);
|
|
|
|
await tester.tap(find.text('parent'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('child'), findsOneWidget);
|
|
expect(surfaces, findsNWidgets(2));
|
|
expect(find.text('parent'), findsNWidgets(2));
|
|
});
|
|
|
|
testWidgets('a sub menu opened mid-flight is not held in the platter', (
|
|
tester,
|
|
) async {
|
|
final open = await pumpMenu(tester, [
|
|
const CommonPopupMenuItem(
|
|
label: 'parent',
|
|
subItems: [CommonPopupMenuItem(label: 'child')],
|
|
),
|
|
]);
|
|
bool inPlatter() => tester.layers.whereType<ImageFilterLayer>().isNotEmpty;
|
|
|
|
open();
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
expect(inPlatter(), isTrue);
|
|
|
|
await tester.tap(find.text('parent'), warnIfMissed: false);
|
|
await tester.pump();
|
|
|
|
expect(inPlatter(), isFalse);
|
|
await tester.pumpAndSettle();
|
|
});
|
|
|
|
testWidgets('the sub menu header lands exactly on the opening item', (
|
|
tester,
|
|
) async {
|
|
final open = await pumpMenu(tester, [
|
|
const CommonPopupMenuItem(label: 'first'),
|
|
const CommonPopupMenuItem(
|
|
label: 'parent',
|
|
subItems: [CommonPopupMenuItem(label: 'child')],
|
|
),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
|
|
final itemTop = tester.getTopLeft(find.text('parent')).dy;
|
|
|
|
await tester.tap(find.text('parent'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(tester.getTopLeft(find.text('parent').last).dy, itemTop);
|
|
});
|
|
|
|
testWidgets('the menu stays between its min and max width', (tester) async {
|
|
Future<double> widthOf(String label) async {
|
|
final open = await pumpMenu(tester, [CommonPopupMenuItem(label: label)]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
final width = tester.getSize(surfaces).width;
|
|
await tester.tapAt(const Offset(10, 10));
|
|
await tester.pumpAndSettle();
|
|
return width;
|
|
}
|
|
|
|
expect(await widthOf('a'), 160);
|
|
expect(
|
|
await widthOf('a label far too long to ever fit inside a menu'),
|
|
280,
|
|
);
|
|
});
|
|
|
|
testWidgets('only a sub menu carries a divider, under its header', (
|
|
tester,
|
|
) async {
|
|
final open = await pumpMenu(tester, [
|
|
const CommonPopupMenuItem(label: 'first'),
|
|
const CommonPopupMenuItem(
|
|
label: 'parent',
|
|
subItems: [CommonPopupMenuItem(label: 'child')],
|
|
),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
expect(find.byType(Divider), findsNothing);
|
|
|
|
await tester.tap(find.text('parent'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byType(Divider), findsOneWidget);
|
|
final header = find.ancestor(
|
|
of: find.text('parent').last,
|
|
matching: find.byType(InkWell),
|
|
);
|
|
final divider = find.byType(Divider);
|
|
expect(tester.getTopLeft(divider).dy, tester.getBottomLeft(header).dy);
|
|
expect(tester.getSize(divider).height, 17);
|
|
expect(
|
|
tester.getTopLeft(find.text('child')).dy -
|
|
tester.getBottomLeft(divider).dy,
|
|
greaterThanOrEqualTo(12),
|
|
);
|
|
});
|
|
|
|
testWidgets('the header folds the sub menu back into its item', (
|
|
tester,
|
|
) async {
|
|
final open = await pumpMenu(tester, [
|
|
const CommonPopupMenuItem(
|
|
label: 'parent',
|
|
subItems: [CommonPopupMenuItem(label: 'child')],
|
|
),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('parent'));
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.text('parent').last);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('child'), findsNothing);
|
|
expect(find.text('parent'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('a sub menu can be opened again after it was folded back', (
|
|
tester,
|
|
) async {
|
|
final open = await pumpMenu(tester, [
|
|
const CommonPopupMenuItem(
|
|
label: 'parent',
|
|
subItems: [CommonPopupMenuItem(label: 'child')],
|
|
),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('parent'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('parent').last);
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.text('parent'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('child'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('back walks one level up and leaves the popup open', (
|
|
tester,
|
|
) async {
|
|
final open = await pumpMenu(tester, [
|
|
const CommonPopupMenuItem(
|
|
label: 'parent',
|
|
subItems: [CommonPopupMenuItem(label: 'child')],
|
|
),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('parent'));
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.binding.handlePopRoute();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('child'), findsNothing);
|
|
expect(find.text('parent'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('a tap outside closes every level at once', (tester) async {
|
|
final open = await pumpMenu(tester, [
|
|
const CommonPopupMenuItem(
|
|
label: 'parent',
|
|
subItems: [CommonPopupMenuItem(label: 'child')],
|
|
),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('parent'));
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tapAt(const Offset(10, 10));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('child'), findsNothing);
|
|
expect(find.text('parent'), findsNothing);
|
|
});
|
|
|
|
testWidgets('picking an item runs it and closes the popup', (tester) async {
|
|
var pressed = 0;
|
|
final open = await pumpMenu(tester, [
|
|
CommonPopupMenuItem(label: 'run', onPressed: () => pressed++),
|
|
const CommonPopupMenuItem(label: 'disabled'),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.text('disabled'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('run'), findsOneWidget);
|
|
|
|
await tester.tap(find.text('run'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(pressed, 1);
|
|
expect(find.text('run'), findsNothing);
|
|
});
|
|
|
|
testWidgets('a menu swells under a press and stretches only past its edges', (
|
|
tester,
|
|
) async {
|
|
var pressed = 0;
|
|
final open = await pumpMenu(tester, [
|
|
CommonPopupMenuItem(label: 'one', onPressed: () => pressed++),
|
|
CommonPopupMenuItem(label: 'two', onPressed: () => pressed++),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
final press = find.descendant(
|
|
of: find.byType(CommonPopupMenu),
|
|
matching: find.byType(ElasticPress),
|
|
);
|
|
final resting = tester.getRect(press);
|
|
|
|
final gesture = await tester.startGesture(
|
|
tester.getCenter(find.text('one')),
|
|
);
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 400));
|
|
final swollen = painted(tester, press);
|
|
expect(swollen.width, greaterThan(resting.width + 4));
|
|
expect(swollen.center.dx, moreOrLessEquals(resting.center.dx));
|
|
|
|
await gesture.moveTo(tester.getCenter(find.text('two')));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 400));
|
|
expect(
|
|
painted(tester, press).center.dx,
|
|
moreOrLessEquals(swollen.center.dx),
|
|
);
|
|
expect(
|
|
painted(tester, press).center.dy,
|
|
moreOrLessEquals(swollen.center.dy),
|
|
);
|
|
|
|
await gesture.moveTo(Offset(resting.left - 80, resting.center.dy));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 400));
|
|
final pulled = painted(tester, press);
|
|
expect(pulled.center.dx, lessThan(resting.center.dx - 4));
|
|
expect(
|
|
pulled.width / pulled.height,
|
|
greaterThan(resting.width / resting.height),
|
|
);
|
|
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(painted(tester, press), resting);
|
|
expect(pressed, 0);
|
|
expect(find.text('one'), findsOneWidget);
|
|
});
|
|
|
|
bool pressedRow(WidgetTester tester, String label) {
|
|
final box = tester.widget<DecoratedBox>(
|
|
find
|
|
.ancestor(of: find.text(label), matching: find.byType(DecoratedBox))
|
|
.first,
|
|
);
|
|
return (box.decoration as ShapeDecoration).color != null;
|
|
}
|
|
|
|
List<Object?> recordHaptics(WidgetTester tester) {
|
|
final haptics = <Object?>[];
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.platform,
|
|
(call) async {
|
|
if (call.method == 'HapticFeedback.vibrate') {
|
|
haptics.add(call.arguments);
|
|
}
|
|
return null;
|
|
},
|
|
);
|
|
addTearDown(
|
|
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
SystemChannels.platform,
|
|
null,
|
|
),
|
|
);
|
|
return haptics;
|
|
}
|
|
|
|
testWidgets('a press follows the finger and picks the item let go on', (
|
|
tester,
|
|
) async {
|
|
final clicks = recordHaptics(tester);
|
|
final picked = <String>[];
|
|
final open = await pumpMenu(tester, [
|
|
for (final label in ['one', 'two', 'three'])
|
|
CommonPopupMenuItem(label: label, onPressed: () => picked.add(label)),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
|
|
final gesture = await tester.startGesture(
|
|
tester.getCenter(find.text('one')),
|
|
);
|
|
await tester.pump();
|
|
expect(pressedRow(tester, 'one'), isTrue);
|
|
|
|
await gesture.moveTo(tester.getCenter(find.text('three')));
|
|
await tester.pump();
|
|
expect(pressedRow(tester, 'one'), isFalse);
|
|
expect(pressedRow(tester, 'three'), isTrue);
|
|
expect(clicks, ['HapticFeedbackType.selectionClick']);
|
|
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(picked, ['three']);
|
|
expect(find.text('one'), findsNothing);
|
|
});
|
|
|
|
testWidgets('a press let go off the items picks nothing', (tester) async {
|
|
final picked = <String>[];
|
|
final open = await pumpMenu(tester, [
|
|
for (final label in ['one', 'two'])
|
|
CommonPopupMenuItem(label: label, onPressed: () => picked.add(label)),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
final menu = tester.getRect(find.byType(CommonPopupMenu));
|
|
|
|
final gesture = await tester.startGesture(
|
|
tester.getCenter(find.text('one')),
|
|
);
|
|
await tester.pump();
|
|
await gesture.moveTo(Offset(menu.left - 60, menu.center.dy));
|
|
await tester.pump();
|
|
expect(pressedRow(tester, 'one'), isFalse);
|
|
expect(pressedRow(tester, 'two'), isFalse);
|
|
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(picked, isEmpty);
|
|
expect(find.text('one'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('a drag that scrolls a long menu lets go of the press', (
|
|
tester,
|
|
) async {
|
|
final picked = <String>[];
|
|
final open = await pumpMenu(tester, [
|
|
for (var index = 0; index < 30; index++)
|
|
CommonPopupMenuItem(
|
|
label: 'item $index',
|
|
onPressed: () => picked.add('item $index'),
|
|
),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
|
|
final gesture = await tester.startGesture(
|
|
tester.getCenter(find.text('item 3')),
|
|
);
|
|
await tester.pump();
|
|
for (var step = 0; step < 5; step++) {
|
|
await gesture.moveBy(const Offset(0, -20));
|
|
await tester.pump();
|
|
}
|
|
expect(pressedRow(tester, 'item 3'), isFalse);
|
|
expect(
|
|
tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels,
|
|
greaterThan(0),
|
|
);
|
|
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(picked, isEmpty);
|
|
expect(find.text('item 3'), findsOneWidget);
|
|
});
|
|
|
|
Future<List<String>> pumpButtonMenu(
|
|
WidgetTester tester, {
|
|
bool enabled = true,
|
|
}) async {
|
|
final picked = <String>[];
|
|
await tester.pumpWidget(
|
|
TestApp(
|
|
homeBuilder: (child) => Scaffold(body: Center(child: child)),
|
|
child: CommonPopupBox(
|
|
targetBuilder: (open) => TextButton(
|
|
onPressed: enabled ? open : null,
|
|
child: const Text('menu'),
|
|
),
|
|
popupBuilder: (_) => CommonPopupMenu(
|
|
items: [
|
|
for (final label in ['one', 'two', 'three'])
|
|
CommonPopupMenuItem(
|
|
label: label,
|
|
onPressed: () => picked.add(label),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
return picked;
|
|
}
|
|
|
|
testWidgets('a press on the button opens its menu to slide on into', (
|
|
tester,
|
|
) async {
|
|
final picked = await pumpButtonMenu(tester);
|
|
|
|
final gesture = await tester.startGesture(
|
|
tester.getCenter(find.text('menu')),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(find.byType(CommonPopupMenu), findsOneWidget);
|
|
|
|
await gesture.moveTo(tester.getCenter(find.text('two')));
|
|
await tester.pump();
|
|
expect(pressedRow(tester, 'two'), isTrue);
|
|
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(picked, ['two']);
|
|
expect(find.byType(CommonPopupMenu), findsNothing);
|
|
});
|
|
|
|
testWidgets('a press let go where it went down leaves the menu open', (
|
|
tester,
|
|
) async {
|
|
final picked = await pumpButtonMenu(tester);
|
|
|
|
await tester.tap(find.text('menu'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byType(CommonPopupMenu), findsOneWidget);
|
|
expect(picked, isEmpty);
|
|
});
|
|
|
|
testWidgets('a pick runs once the popup has returned into its button', (
|
|
tester,
|
|
) async {
|
|
final picked = await pumpButtonMenu(tester);
|
|
await tester.tap(find.text('menu'));
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.text('two'));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 200));
|
|
expect(find.byType(CommonPopupMenu), findsOneWidget);
|
|
expect(picked, isEmpty);
|
|
|
|
await tester.pumpAndSettle();
|
|
expect(picked, ['two']);
|
|
});
|
|
|
|
testWidgets('a pick from a popup at a point runs at once', (tester) async {
|
|
late BuildContext context;
|
|
final picked = <String>[];
|
|
await tester.pumpWidget(
|
|
TestApp(
|
|
child: Builder(
|
|
builder: (value) {
|
|
context = value;
|
|
return const SizedBox.expand();
|
|
},
|
|
),
|
|
),
|
|
);
|
|
Navigator.of(context).push(
|
|
CommonPopupRoute<void>(
|
|
barrierLabel: 'Dismiss',
|
|
placement: PopupPlacement.belowPoint,
|
|
anchorOf: () => const Offset(100, 120) & Size.zero,
|
|
builder: (_) => CommonPopupMenu(
|
|
items: [
|
|
CommonPopupMenuItem(
|
|
label: 'one',
|
|
onPressed: () => picked.add('one'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.text('one'));
|
|
await tester.pump();
|
|
expect(picked, ['one']);
|
|
await tester.pumpAndSettle();
|
|
});
|
|
|
|
testWidgets('a popup value fades across as its width eases over', (
|
|
tester,
|
|
) async {
|
|
Widget value(String text) => Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: PopupValueText(text, alignment: AlignmentDirectional.centerEnd),
|
|
),
|
|
);
|
|
await tester.pumpWidget(value('short'));
|
|
final from = tester.getSize(find.byType(PopupValueText)).width;
|
|
|
|
await tester.pumpWidget(value('a much longer value'));
|
|
final to = tester
|
|
.getSize(find.text('a much longer value', skipOffstage: false))
|
|
.width;
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
final easing = tester.getSize(find.byType(PopupValueText)).width;
|
|
expect(easing, greaterThan(from));
|
|
expect(easing, lessThan(to));
|
|
expect(find.text('short'), findsOneWidget);
|
|
expect(
|
|
tester
|
|
.widgetList<ImageFiltered>(find.byType(ImageFiltered))
|
|
.where((filter) => filter.enabled),
|
|
hasLength(2),
|
|
);
|
|
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('short'), findsNothing);
|
|
expect(tester.getSize(find.byType(PopupValueText)).width, to);
|
|
expect(
|
|
tester
|
|
.widgetList<ImageFiltered>(find.byType(ImageFiltered))
|
|
.where((filter) => filter.enabled),
|
|
isEmpty,
|
|
);
|
|
});
|
|
|
|
testWidgets('a disabled button opens nothing under a press', (tester) async {
|
|
await pumpButtonMenu(tester, enabled: false);
|
|
|
|
final gesture = await tester.startGesture(
|
|
tester.getCenter(find.text('menu')),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(find.byType(CommonPopupMenu), findsNothing);
|
|
await gesture.up();
|
|
});
|
|
|
|
testWidgets('a press resting on an item opens its sub menu to slide into', (
|
|
tester,
|
|
) async {
|
|
final clicks = recordHaptics(tester);
|
|
final picked = <String>[];
|
|
final open = await pumpMenu(tester, [
|
|
CommonPopupMenuItem(label: 'other', onPressed: () => picked.add('other')),
|
|
CommonPopupMenuItem(
|
|
label: 'parent',
|
|
subItems: [
|
|
CommonPopupMenuItem(
|
|
label: 'child',
|
|
onPressed: () => picked.add('child'),
|
|
),
|
|
],
|
|
),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
|
|
final gesture = await tester.startGesture(
|
|
tester.getCenter(find.text('other')),
|
|
);
|
|
await tester.pump();
|
|
await gesture.moveTo(tester.getCenter(find.text('parent')));
|
|
await tester.pump(const Duration(milliseconds: 200));
|
|
expect(find.text('child'), findsNothing);
|
|
expect(clicks, hasLength(1));
|
|
|
|
await tester.pump(const Duration(milliseconds: 300));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('child'), findsOneWidget);
|
|
expect(clicks, hasLength(2));
|
|
|
|
await gesture.moveTo(tester.getCenter(find.text('child')));
|
|
await tester.pump();
|
|
expect(clicks, hasLength(3));
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(picked, ['child']);
|
|
});
|
|
|
|
testWidgets('a press resting on the header folds its sub menu back', (
|
|
tester,
|
|
) async {
|
|
final clicks = recordHaptics(tester);
|
|
final picked = <String>[];
|
|
final open = await pumpMenu(tester, [
|
|
CommonPopupMenuItem(
|
|
label: 'parent',
|
|
subItems: [
|
|
CommonPopupMenuItem(
|
|
label: 'child',
|
|
onPressed: () => picked.add('child'),
|
|
),
|
|
],
|
|
),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
|
|
final header = tester.getCenter(find.text('parent'));
|
|
final gesture = await tester.startGesture(header);
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
await tester.pumpAndSettle();
|
|
await gesture.moveTo(tester.getCenter(find.text('child')));
|
|
await tester.pump();
|
|
await gesture.moveTo(header);
|
|
await tester.pump();
|
|
expect(clicks, hasLength(3));
|
|
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('child'), findsNothing);
|
|
expect(clicks, hasLength(4));
|
|
expect(pressedRow(tester, 'parent'), isTrue);
|
|
|
|
await tester.pump(const Duration(seconds: 1));
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('child'), findsNothing);
|
|
expect(find.text('parent'), findsOneWidget);
|
|
expect(picked, isEmpty);
|
|
});
|
|
|
|
testWidgets('a tap that folds a sub menu back gives no click', (
|
|
tester,
|
|
) async {
|
|
final clicks = recordHaptics(tester);
|
|
final open = await pumpMenu(tester, [
|
|
const CommonPopupMenuItem(
|
|
label: 'parent',
|
|
subItems: [CommonPopupMenuItem(label: 'child')],
|
|
),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.text('parent'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('parent').last);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('child'), findsNothing);
|
|
expect(clicks, isEmpty);
|
|
});
|
|
|
|
testWidgets('a press let go where its sub menu opened keeps it open', (
|
|
tester,
|
|
) async {
|
|
final open = await pumpMenu(tester, [
|
|
const CommonPopupMenuItem(
|
|
label: 'parent',
|
|
subItems: [CommonPopupMenuItem(label: 'child')],
|
|
),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
|
|
final gesture = await tester.startGesture(
|
|
tester.getCenter(find.text('parent')),
|
|
);
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('child'), findsOneWidget);
|
|
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('child'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('a press held on the button keeps its tooltip down', (
|
|
tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
TestApp(
|
|
homeBuilder: (child) => Scaffold(body: Center(child: child)),
|
|
child: CommonPopupBox(
|
|
targetBuilder: (open) => IconButton(
|
|
tooltip: 'tip',
|
|
onPressed: open,
|
|
icon: const GlyphIcon(AppGlyphs.more),
|
|
),
|
|
popupBuilder: (_) => CommonPopupMenu(
|
|
items: [CommonPopupMenuItem(label: 'one', onPressed: () {})],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final gesture = await tester.startGesture(
|
|
tester.getCenter(find.byType(IconButton)),
|
|
);
|
|
await tester.pump(const Duration(seconds: 1));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('one'), findsOneWidget);
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
expect(find.text('tip'), findsNothing);
|
|
});
|
|
|
|
Future<List<String>> pumpScrollingButtonMenu(WidgetTester tester) async {
|
|
final picked = <String>[];
|
|
await tester.pumpWidget(
|
|
TestApp(
|
|
homeBuilder: (child) => Scaffold(body: child),
|
|
child: ListView(
|
|
children: [
|
|
const SizedBox(height: 200),
|
|
Center(
|
|
child: CommonPopupBox(
|
|
targetBuilder: (open) =>
|
|
TextButton(onPressed: open, child: const Text('menu')),
|
|
popupBuilder: (_) => CommonPopupMenu(
|
|
items: [
|
|
for (final label in ['one', 'two', 'three'])
|
|
CommonPopupMenuItem(
|
|
label: label,
|
|
onPressed: () => picked.add(label),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 2000),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
return picked;
|
|
}
|
|
|
|
testWidgets('in a scroll view a drag from the button scrolls instead', (
|
|
tester,
|
|
) async {
|
|
await pumpScrollingButtonMenu(tester);
|
|
final scrollable = tester.state<ScrollableState>(find.byType(Scrollable));
|
|
|
|
final gesture = await tester.startGesture(
|
|
tester.getCenter(find.text('menu')),
|
|
);
|
|
for (var step = 0; step < 4; step++) {
|
|
await gesture.moveBy(const Offset(0, -20));
|
|
await tester.pump(const Duration(milliseconds: 16));
|
|
}
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(scrollable.position.pixels, greaterThan(0));
|
|
expect(find.byType(CommonPopupMenu), findsNothing);
|
|
});
|
|
|
|
testWidgets('in a scroll view a press held on the button opens its menu', (
|
|
tester,
|
|
) async {
|
|
await pumpScrollingButtonMenu(tester);
|
|
|
|
final gesture = await tester.startGesture(
|
|
tester.getCenter(find.text('menu')),
|
|
);
|
|
await tester.pump(kPressTimeout ~/ 2);
|
|
expect(find.byType(CommonPopupMenu), findsNothing);
|
|
await tester.pump(kPressTimeout);
|
|
await tester.pumpAndSettle();
|
|
expect(find.byType(CommonPopupMenu), findsOneWidget);
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(find.byType(CommonPopupMenu), findsOneWidget);
|
|
});
|
|
|
|
for (final kind in [PointerDeviceKind.touch, PointerDeviceKind.mouse]) {
|
|
testWidgets('in a scroll view a ${kind.name} press held on the button '
|
|
'slides on into its menu', (tester) async {
|
|
final picked = await pumpScrollingButtonMenu(tester);
|
|
|
|
final gesture = await tester.startGesture(
|
|
tester.getCenter(find.text('menu')),
|
|
kind: kind,
|
|
);
|
|
await tester.pump(kPressTimeout);
|
|
await tester.pumpAndSettle();
|
|
await gesture.moveTo(tester.getCenter(find.text('two')));
|
|
await tester.pump();
|
|
expect(pressedRow(tester, 'two'), isTrue);
|
|
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(picked, ['two']);
|
|
expect(find.byType(CommonPopupMenu), findsNothing);
|
|
});
|
|
}
|
|
|
|
testWidgets('in a scroll view a tap on the button opens its menu', (
|
|
tester,
|
|
) async {
|
|
await pumpScrollingButtonMenu(tester);
|
|
|
|
await tester.tap(find.text('menu'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byType(CommonPopupMenu), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('a press carried in from the button stretches the menu', (
|
|
tester,
|
|
) async {
|
|
await pumpButtonMenu(tester);
|
|
final gesture = await tester.startGesture(
|
|
tester.getCenter(find.text('menu')),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
final press = find.descendant(
|
|
of: find.byType(CommonPopupMenu),
|
|
matching: find.byType(ElasticPress),
|
|
);
|
|
final resting = tester.getRect(press);
|
|
|
|
await gesture.moveTo(Offset(resting.left - 80, resting.center.dy));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 400));
|
|
final pulled = painted(tester, press);
|
|
expect(pulled.center.dx, lessThan(resting.center.dx - 4));
|
|
expect(pulled.width, greaterThan(resting.width));
|
|
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(painted(tester, press), resting);
|
|
});
|
|
|
|
testWidgets('a press carried in while the menu opens stretches it only '
|
|
'once the platter steps aside', (tester) async {
|
|
await pumpButtonMenu(tester);
|
|
final gesture = await tester.startGesture(
|
|
tester.getCenter(find.text('menu')),
|
|
);
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 50));
|
|
final press = find.descendant(
|
|
of: find.byType(CommonPopupMenu),
|
|
matching: find.byType(ElasticPress),
|
|
);
|
|
final resting = tester.getRect(press);
|
|
|
|
await gesture.moveTo(Offset(resting.left - 80, resting.center.dy));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
expect(painted(tester, press), resting);
|
|
|
|
await tester.pumpAndSettle();
|
|
await gesture.moveBy(const Offset(-1, 0));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 400));
|
|
expect(painted(tester, press).center.dx, lessThan(resting.center.dx - 4));
|
|
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
});
|
|
|
|
testWidgets('a capped menu opens on its checked item and cuts a row at '
|
|
'its foot', (tester) async {
|
|
tester.view.physicalSize = const Size(800, 1600);
|
|
tester.view.devicePixelRatio = 1;
|
|
addTearDown(tester.view.reset);
|
|
final open = await pumpMenu(tester, [
|
|
for (var i = 0; i < 30; i++)
|
|
CommonPopupMenuItem(
|
|
label: 'Item $i',
|
|
checked: i == 20,
|
|
onPressed: () {},
|
|
),
|
|
], maxVisibleItems: 5);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
|
|
final menu = tester.getRect(find.byType(PopupMenuCard));
|
|
final checked = tester.getRect(find.text('Item 20'));
|
|
final cut = tester.getRect(find.text('Item 23'));
|
|
expect(checked.top, greaterThan(menu.top));
|
|
expect(checked.bottom, lessThan(menu.bottom));
|
|
expect(
|
|
tester.getRect(find.text('Item 17', skipOffstage: false)).bottom,
|
|
lessThan(menu.top),
|
|
);
|
|
expect(cut.top, lessThan(menu.bottom));
|
|
expect(cut.bottom, greaterThan(menu.bottom));
|
|
});
|
|
|
|
testWidgets('a long menu caps itself well short of a tall window', (
|
|
tester,
|
|
) async {
|
|
tester.view.physicalSize = const Size(800, 1600);
|
|
tester.view.devicePixelRatio = 1;
|
|
addTearDown(tester.view.reset);
|
|
final open = await pumpMenu(tester, [
|
|
for (var i = 0; i < 30; i++)
|
|
CommonPopupMenuItem(label: 'Item $i', onPressed: () {}),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
|
|
final menu = tester.getRect(find.byType(PopupMenuCard));
|
|
final cut = tester.getRect(find.text('Item 7'));
|
|
expect(menu.height, lessThan(800));
|
|
expect(cut.top, lessThan(menu.bottom));
|
|
expect(cut.bottom, greaterThan(menu.bottom));
|
|
});
|
|
|
|
testWidgets('a capped menu is as wide as its rows laid out in full', (
|
|
tester,
|
|
) async {
|
|
final items = [
|
|
CommonPopupMenuItem(label: '🇭🇰 HK', onPressed: () {}),
|
|
CommonPopupMenuItem(label: 'Fallback', checked: true, onPressed: () {}),
|
|
for (var i = 0; i < 12; i++)
|
|
CommonPopupMenuItem(label: 'Group $i', onPressed: () {}),
|
|
];
|
|
Future<double> widthOf(int? maxVisibleItems) async {
|
|
final open = await pumpMenu(
|
|
tester,
|
|
items,
|
|
maxVisibleItems: maxVisibleItems,
|
|
);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
final width = tester.getSize(find.byType(PopupMenuCard)).width;
|
|
await tester.tapAt(Offset.zero);
|
|
await tester.pumpAndSettle();
|
|
return width;
|
|
}
|
|
|
|
final full = await widthOf(null);
|
|
expect(full, inExclusiveRange(160, 280));
|
|
expect(await widthOf(5), moreOrLessEquals(full, epsilon: 0.5));
|
|
});
|
|
|
|
testWidgets('a danger item keeps the error color off its background', (
|
|
tester,
|
|
) async {
|
|
final open = await pumpMenu(tester, [
|
|
CommonPopupMenuItem(label: 'plain', onPressed: () {}),
|
|
CommonPopupMenuItem(
|
|
glyph: AppGlyphs.delete,
|
|
label: 'delete',
|
|
danger: true,
|
|
onPressed: () {},
|
|
),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
|
|
final colorScheme = Theme.of(
|
|
tester.element(find.text('delete')),
|
|
).colorScheme;
|
|
|
|
expect(
|
|
tester.widget<Text>(find.text('delete')).style?.color,
|
|
colorScheme.error,
|
|
);
|
|
expect(
|
|
tester.widget<GlyphIcon>(find.byGlyph(AppGlyphs.delete)).color,
|
|
colorScheme.error,
|
|
);
|
|
expect(
|
|
find.ancestor(of: find.text('delete'), matching: find.byType(Ink)),
|
|
findsNothing,
|
|
);
|
|
expect(
|
|
tester
|
|
.widget<InkWell>(
|
|
find.ancestor(
|
|
of: find.text('delete'),
|
|
matching: find.byType(InkWell),
|
|
),
|
|
)
|
|
.hoverColor,
|
|
colorScheme.error.withValues(alpha: 0.1),
|
|
);
|
|
expect(
|
|
tester.widget<Text>(find.text('plain')).style?.color,
|
|
colorScheme.onSurface,
|
|
);
|
|
expect(
|
|
tester
|
|
.widget<InkWell>(
|
|
find.ancestor(
|
|
of: find.text('plain'),
|
|
matching: find.byType(InkWell),
|
|
),
|
|
)
|
|
.hoverColor,
|
|
isNull,
|
|
);
|
|
});
|
|
|
|
testWidgets('a sub menu is at least as wide as its parent', (tester) async {
|
|
final open = await pumpMenu(tester, [
|
|
const CommonPopupMenuItem(
|
|
label: 'a label long enough to widen the parent menu',
|
|
),
|
|
const CommonPopupMenuItem(
|
|
label: 'parent',
|
|
subItems: [CommonPopupMenuItem(label: 'x')],
|
|
),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('parent'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(surfaces, findsNWidgets(2));
|
|
final parentWidth = tester.getSize(surfaces.first).width;
|
|
final childWidth = tester.getSize(surfaces.last).width;
|
|
expect(childWidth, greaterThanOrEqualTo(parentWidth));
|
|
});
|
|
|
|
testWidgets('the open sub menu casts the stronger shadow', (tester) async {
|
|
final open = await pumpMenu(tester, [
|
|
const CommonPopupMenuItem(
|
|
label: 'parent',
|
|
subItems: [CommonPopupMenuItem(label: 'child')],
|
|
),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('parent'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(surfaces, findsNWidgets(2));
|
|
expect(
|
|
shadowOf(tester, surfaces.first),
|
|
lessThan(shadowOf(tester, surfaces.last)),
|
|
);
|
|
});
|
|
|
|
testWidgets('a sub menu settles wider than the parent it opened from', (
|
|
tester,
|
|
) async {
|
|
final open = await pumpMenu(tester, [
|
|
const CommonPopupMenuItem(
|
|
label: 'parent',
|
|
subItems: [CommonPopupMenuItem(label: 'child')],
|
|
),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
final parentWidth = tester.getSize(surfaces).width;
|
|
|
|
await tester.tap(find.text('parent'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
tester.getSize(surfaces.last).width,
|
|
closeTo(parentWidth * 1.12, 0.01),
|
|
);
|
|
});
|
|
|
|
testWidgets('a sub menu grows out of its item instead of snapping open', (
|
|
tester,
|
|
) async {
|
|
final open = await pumpMenu(tester, [
|
|
const CommonPopupMenuItem(
|
|
label: 'parent',
|
|
subItems: [CommonPopupMenuItem(label: 'child')],
|
|
),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
final parentWidth = tester.getSize(surfaces).width;
|
|
|
|
await tester.tap(find.text('parent'));
|
|
await tester.pump();
|
|
|
|
final active = surfaces.last;
|
|
expect(tester.getSize(active).width, parentWidth);
|
|
expect(shadowOf(tester, active), 0);
|
|
|
|
await tester.pump(const Duration(milliseconds: 40));
|
|
final midWidth = tester.getSize(active).width;
|
|
expect(midWidth, greaterThan(parentWidth));
|
|
expect(midWidth, lessThan(parentWidth * 1.12));
|
|
|
|
await tester.pumpAndSettle();
|
|
expect(tester.getSize(active).width, closeTo(parentWidth * 1.12, 0.01));
|
|
});
|
|
|
|
testWidgets('a sub menu settles its width before it finishes unfolding', (
|
|
tester,
|
|
) async {
|
|
final open = await pumpMenu(tester, [
|
|
const CommonPopupMenuItem(
|
|
label: 'parent',
|
|
subItems: [
|
|
CommonPopupMenuItem(label: 'first child'),
|
|
CommonPopupMenuItem(label: 'second child'),
|
|
],
|
|
),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
final parentWidth = tester.getSize(surfaces).width;
|
|
|
|
await tester.tap(find.text('parent'));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 150));
|
|
|
|
final active = surfaces.last;
|
|
final midWidth = tester.getSize(active).width;
|
|
expect(midWidth, greaterThanOrEqualTo(parentWidth * 1.12));
|
|
expect(cardOf(tester, active).grow, lessThan(1));
|
|
|
|
await tester.pumpAndSettle();
|
|
expect(tester.getSize(active).width, midWidth);
|
|
expect(cardOf(tester, active).grow, 1);
|
|
});
|
|
|
|
testWidgets('a sub menu widening as it unfolds keeps its rows unclipped', (
|
|
tester,
|
|
) async {
|
|
final open = await pumpMenu(tester, [
|
|
const CommonPopupMenuItem(
|
|
label: 'a label long enough to widen the parent menu',
|
|
),
|
|
const CommonPopupMenuItem(
|
|
label: 'parent',
|
|
subItems: [CommonPopupMenuItem(label: 'child')],
|
|
),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
|
|
Rect clipOf(Finder surface) {
|
|
Rect? clip;
|
|
expect(
|
|
tester.renderObject(surface),
|
|
paints..everything((method, arguments) {
|
|
if (method == #clipPath) {
|
|
clip ??= (arguments.first as Path).getBounds();
|
|
}
|
|
return true;
|
|
}),
|
|
);
|
|
return clip!.shift(tester.getTopLeft(surface));
|
|
}
|
|
|
|
Rect rowOf(Finder label) => tester.getRect(
|
|
find.ancestor(of: label, matching: find.byType(InkWell)).first,
|
|
);
|
|
|
|
await tester.tap(find.text('parent'));
|
|
await tester.pump();
|
|
final active = surfaces.last;
|
|
for (var frame = 0; frame < 20; frame++) {
|
|
await tester.pump(const Duration(milliseconds: 16));
|
|
final left = clipOf(active).left;
|
|
expect(left, lessThanOrEqualTo(rowOf(find.text('parent').last).left));
|
|
expect(left, lessThanOrEqualTo(rowOf(find.text('child')).left));
|
|
}
|
|
await tester.pumpAndSettle();
|
|
});
|
|
|
|
testWidgets('folding back lands on the item without a jump at the end', (
|
|
tester,
|
|
) async {
|
|
final open = await pumpMenu(tester, [
|
|
const CommonPopupMenuItem(label: 'plain item'),
|
|
const CommonPopupMenuItem(
|
|
label: 'parent',
|
|
subItems: [
|
|
CommonPopupMenuItem(label: 'first child'),
|
|
CommonPopupMenuItem(label: 'second child'),
|
|
],
|
|
),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.text('parent'));
|
|
await tester.pump();
|
|
final foldedWidth = tester.getSize(surfaces.last).width;
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.text('parent').last);
|
|
late PopupMenuCard last;
|
|
late double lastWidth;
|
|
var frames = 0;
|
|
while (surfaces.evaluate().length > 1) {
|
|
last = cardOf(tester, surfaces.last);
|
|
lastWidth = tester.getSize(surfaces.last).width;
|
|
await tester.pump(const Duration(milliseconds: 16));
|
|
frames++;
|
|
expect(frames, lessThan(60));
|
|
}
|
|
|
|
expect(lastWidth, closeTo(foldedWidth, 1));
|
|
expect(last.grow, closeTo(0, 0.01));
|
|
expect(shadowOf(tester, surfaces), greaterThan(0));
|
|
await tester.pumpAndSettle();
|
|
});
|
|
|
|
testWidgets('a fold caught mid-unfold turns back from where it stands', (
|
|
tester,
|
|
) async {
|
|
final open = await pumpMenu(tester, [
|
|
const CommonPopupMenuItem(
|
|
label: 'parent',
|
|
subItems: [
|
|
CommonPopupMenuItem(label: 'first child'),
|
|
CommonPopupMenuItem(label: 'second child'),
|
|
],
|
|
),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('parent'));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 80));
|
|
final caught = cardOf(tester, surfaces.last).grow;
|
|
expect(caught, inExclusiveRange(0, 1));
|
|
|
|
await tester.tap(find.text('parent').last, warnIfMissed: false);
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 16));
|
|
expect(cardOf(tester, surfaces.last).grow, closeTo(caught, 0.15));
|
|
|
|
await tester.pumpAndSettle();
|
|
expect(surfaces, findsOneWidget);
|
|
});
|
|
|
|
testWidgets('the item tapped again while its sub menu folds opens it back', (
|
|
tester,
|
|
) async {
|
|
final open = await pumpMenu(tester, [
|
|
const CommonPopupMenuItem(
|
|
label: 'parent',
|
|
subItems: [
|
|
CommonPopupMenuItem(label: 'first child'),
|
|
CommonPopupMenuItem(label: 'second child'),
|
|
],
|
|
),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('parent'));
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.text('parent').last);
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 60));
|
|
await tester.tap(find.text('parent').last);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('first child'), findsOneWidget);
|
|
expect(surfaces, findsNWidgets(2));
|
|
expect(cardOf(tester, surfaces.last).grow, 1);
|
|
});
|
|
|
|
testWidgets('a tap on the menu behind a sub menu folds it back', (
|
|
tester,
|
|
) async {
|
|
final open = await pumpMenu(tester, [
|
|
const CommonPopupMenuItem(label: 'plain item'),
|
|
const CommonPopupMenuItem(
|
|
label: 'parent',
|
|
subItems: [CommonPopupMenuItem(label: 'child')],
|
|
),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('parent'));
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.text('plain item'), warnIfMissed: false);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('child'), findsNothing);
|
|
expect(find.text('plain item'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('a tap on a card between its items keeps the menu open', (
|
|
tester,
|
|
) async {
|
|
final open = await pumpMenu(tester, [
|
|
CommonPopupMenuItem(label: 'item', onPressed: () {}),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tapAt(tester.getTopLeft(surfaces) + const Offset(4, 4));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('item'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('the item a sub menu stands on hides under its header', (
|
|
tester,
|
|
) async {
|
|
final open = await pumpMenu(tester, [
|
|
const CommonPopupMenuItem(
|
|
label: 'parent',
|
|
subItems: [CommonPopupMenuItem(label: 'child')],
|
|
),
|
|
]);
|
|
double opacityOf(Finder label) => tester
|
|
.widget<Opacity>(
|
|
find.ancestor(of: label, matching: find.byType(Opacity)).first,
|
|
)
|
|
.opacity;
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
expect(opacityOf(find.text('parent')), 1);
|
|
|
|
await tester.tap(find.text('parent'));
|
|
await tester.pumpAndSettle();
|
|
expect(opacityOf(find.text('parent').first), 0);
|
|
|
|
await tester.tap(find.text('parent').last);
|
|
await tester.pumpAndSettle();
|
|
expect(opacityOf(find.text('parent')), 1);
|
|
});
|
|
|
|
testWidgets('a sub menu unfolds without moving the menu it opens from', (
|
|
tester,
|
|
) async {
|
|
late BuildContext context;
|
|
await tester.pumpWidget(
|
|
TestApp(
|
|
child: Builder(
|
|
builder: (value) {
|
|
context = value;
|
|
return const SizedBox.expand();
|
|
},
|
|
),
|
|
),
|
|
);
|
|
|
|
Navigator.of(context).push(
|
|
CommonPopupRoute<void>(
|
|
barrierLabel: 'Dismiss',
|
|
placement: PopupPlacement.belowPoint,
|
|
anchorOf: () => const Offset(100, 250) & Size.zero,
|
|
avoid: const Rect.fromLTRB(0, 200, 800, 300),
|
|
builder: (_) => const CommonPopupMenu(
|
|
items: [
|
|
CommonPopupMenuItem(label: 'plain item'),
|
|
CommonPopupMenuItem(
|
|
label: 'parent',
|
|
subItems: [
|
|
CommonPopupMenuItem(label: 'first child'),
|
|
CommonPopupMenuItem(label: 'second child'),
|
|
CommonPopupMenuItem(label: 'third child'),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
final menu = find.byType(CommonPopupMenu);
|
|
final corner = tester.getTopRight(menu);
|
|
final foldedHeight = tester.getSize(menu).height;
|
|
expect(tester.getBottomRight(menu).dy, lessThan(200));
|
|
|
|
await tester.tap(find.text('parent'));
|
|
for (var frame = 0; frame < 30; frame++) {
|
|
await tester.pump(const Duration(milliseconds: 16));
|
|
expect(tester.getTopRight(menu), offsetMoreOrLessEquals(corner));
|
|
}
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(tester.getTopRight(menu), offsetMoreOrLessEquals(corner));
|
|
expect(tester.getSize(menu).height, greaterThan(foldedHeight));
|
|
});
|
|
|
|
testWidgets('the parent hands its shadow over without a step', (
|
|
tester,
|
|
) async {
|
|
final open = await pumpMenu(tester, [
|
|
const CommonPopupMenuItem(
|
|
label: 'parent',
|
|
subItems: [CommonPopupMenuItem(label: 'child')],
|
|
),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
final restingShadow = shadowOf(tester, surfaces);
|
|
|
|
await tester.tap(find.text('parent'));
|
|
await tester.pump();
|
|
expect(shadowOf(tester, surfaces.first), restingShadow);
|
|
expect(shadowOf(tester, surfaces.last), 0);
|
|
|
|
await tester.pumpAndSettle();
|
|
expect(shadowOf(tester, surfaces.first), lessThan(restingShadow));
|
|
expect(shadowOf(tester, surfaces.last), restingShadow);
|
|
|
|
await tester.tap(find.text('parent').last);
|
|
await tester.pumpAndSettle();
|
|
expect(shadowOf(tester, surfaces), restingShadow);
|
|
});
|
|
|
|
testWidgets('a parent keeps its scroll offset while a sub menu is open', (
|
|
tester,
|
|
) async {
|
|
final open = await pumpMenu(tester, [
|
|
for (var index = 0; index < 4; index++)
|
|
CommonPopupMenuItem(label: 'item $index'),
|
|
const CommonPopupMenuItem(
|
|
label: 'parent',
|
|
subItems: [CommonPopupMenuItem(label: 'child')],
|
|
),
|
|
for (var index = 4; index < 30; index++)
|
|
CommonPopupMenuItem(label: 'item $index'),
|
|
]);
|
|
open();
|
|
await tester.pumpAndSettle();
|
|
|
|
final scrollable = tester.state<ScrollableState>(find.byType(Scrollable));
|
|
scrollable.position.jumpTo(100);
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.text('parent'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('child'), findsOneWidget);
|
|
final parentScrollable = tester.state<ScrollableState>(
|
|
find.byType(Scrollable).first,
|
|
);
|
|
expect(identical(parentScrollable, scrollable), isTrue);
|
|
expect(parentScrollable.position.pixels, 100);
|
|
|
|
await tester.tap(find.text('parent').last);
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
identical(
|
|
tester.state<ScrollableState>(find.byType(Scrollable)),
|
|
scrollable,
|
|
),
|
|
isTrue,
|
|
);
|
|
expect(scrollable.position.pixels, 100);
|
|
});
|
|
|
|
testWidgets('on glass a card stands bare while the platter carries it', (
|
|
tester,
|
|
) async {
|
|
late PopupOpen open;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
builder: (_, child) => LiquidGlassScope(enabled: true, child: child!),
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: CommonPopupBox(
|
|
targetBuilder: (value) {
|
|
open = value;
|
|
return const SizedBox(width: 40, height: 40);
|
|
},
|
|
popupBuilder: (_) => const CommonPopupMenu(
|
|
items: [CommonPopupMenuItem(label: 'item')],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
bool bare() => cardOf(tester, surfaces).bare;
|
|
|
|
open();
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
expect(bare(), isTrue);
|
|
|
|
await tester.pumpAndSettle();
|
|
expect(bare(), isFalse);
|
|
|
|
await tester.tapAt(Offset.zero);
|
|
await tester.pump();
|
|
expect(bare(), isTrue);
|
|
});
|
|
}
|