Files
buzz/mobile/test/shared/widgets/modal_presentation_test.dart
klopez4212 cb69ad9e4f Adapt iPhone Duo layouts, rail navigation, and native sheets (#8568)
Duo keeps conversations and Settings details open across closed, book
and flat postures. Book mode splits at the fold; flattening restores the
sidebar. Native rail controls, full-width navigation backdrops and
sheet-local safe areas keep content aligned. Ordinary iPhones remain
single-pane in portrait and landscape; tablet and Duo split layouts
remain enabled.

Includes the recovered preview's matching pane widths, Buzz empty-pane
mark, stable Settings chevrons, Bestie pane routing and Duo-only upward
New menu attachment. Current main's Activity redesign, Search content
fade and unread-navigation button are retained. Menu selection checks
sit beside their labels.

Shared mobile changes remain in scope: the native iOS status editor,
Appearance preview layout, avatar crop preservation, onboarding footer
layout, profile/invite sheet geometry and nested conversation
navigation. This PR is not exclusively Duo-gated. Nested navigator
observers preserve recording cancellation and navigation callbacks; crop
resizing preserves the selected image region. iOS 27.1 APIs remain
compile-time and runtime gated.

Validation on the reconciled source: Flutter analysis, Activity/Home
journeys, the full mobile test suite, file-size checks and Swift package
tests. The iPhone single-pane fix was also exercised on an iPhone 18 Pro
simulator before reconciliation. Fresh hosted CI and physical-device
confirmation of the reconciled build are separate and remain pending.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Tom Brow <tomb@squareup.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Tom Brow <tomb@squareup.com>
2026-11-12 19:00:58 +00:00

1034 lines
35 KiB
Dart

import 'package:buzz/shared/widgets/ios_glass_navigation_button.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:buzz/shared/widgets/ios_navigation_bar.dart';
import 'package:buzz/shared/widgets/concentric_sheet_surface.dart';
import 'package:buzz/shared/theme/theme.dart';
import 'package:buzz/shared/widgets/modal_presentation.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('open sheet follows folding without losing its draft or Close', (
tester,
) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
addTearDown(() => debugDefaultTargetPlatformOverride = null);
for (final startWide in [true, false]) {
void posture(bool wide) {
tester.view.physicalSize = wide
? const Size(951, 669)
: const Size(390, 844);
tester.view.padding = wide
? const FakeViewPadding(right: 76)
: const FakeViewPadding(bottom: 34);
tester.view.viewPadding = tester.view.padding;
}
posture(startWide);
await tester.pumpWidget(
ProviderScope(
child: MaterialApp(
theme: AppTheme.light(),
home: Builder(
builder: (context) => Scaffold(
body: TextButton(
onPressed: () => showBuzzModalBottomSheet<void>(
context: context,
title: 'Draft',
useSafeArea: true,
isDismissible: false,
enableDrag: false,
builder: (_) => const SizedBox(
height: 200,
child: TextField(key: ValueKey('fold-draft')),
),
),
child: const Text('Open'),
),
),
),
),
),
);
await tester.tap(find.text('Open'));
await tester.pumpAndSettle();
await tester.enterText(
find.byKey(const ValueKey('fold-draft')),
'Keep this edit',
);
for (final wide in [!startWide, startWide]) {
posture(wide);
await tester.pumpAndSettle();
expect(find.text('Keep this edit'), findsOneWidget);
expect(
find.byKey(const ValueKey('duo-toolbar-Close Draft')),
wide ? findsOneWidget : findsNothing,
);
expect(
find.byKey(const ValueKey('buzz-sheet-ios-glass-close')),
wide ? findsNothing : findsOneWidget,
);
if (wide) {
expect(
tester.getSize(find.byKey(const ValueKey('fold-draft'))).width,
greaterThan(800),
);
}
}
FocusManager.instance.primaryFocus?.unfocus();
await tester.pumpAndSettle();
if (startWide) {
tester
.widget<IosNavigationBar>(
find.descendant(
of: find.byKey(const ValueKey('duo-toolbar-Close Draft')),
matching: find.byType(IosNavigationBar),
),
)
.leading!
.onPressed!();
} else {
tester
.widget<IosGlassNavigationButton>(
find.byKey(const ValueKey('buzz-sheet-ios-glass-close')),
)
.onPressed!();
}
await tester.pumpAndSettle();
expect(
find.byKey(const ValueKey('fold-draft')),
findsNothing,
reason: 'startWide=$startWide',
);
await tester.pumpWidget(const SizedBox());
}
debugDefaultTargetPlatformOverride = null;
});
for (final platform in [TargetPlatform.android, TargetPlatform.iOS]) {
for (final titled in [false, true]) {
for (final ownSafeArea in [false, true]) {
testWidgets(
'fixed bottom clearance $platform titled=$titled ownSafeArea=$ownSafeArea',
(tester) async {
debugDefaultTargetPlatformOverride = platform;
tester.view.physicalSize = const Size(390, 844);
tester.view.devicePixelRatio = 1;
final systemInset = platform == TargetPlatform.iOS ? 34.0 : 24.0;
tester.view.padding = FakeViewPadding(bottom: systemInset);
tester.view.viewPadding = FakeViewPadding(bottom: systemInset);
addTearDown(() {
tester.view.reset();
debugDefaultTargetPlatformOverride = null;
});
try {
double? contentInset;
await tester.pumpWidget(
MaterialApp(
theme: AppTheme.light(),
home: Builder(
builder: (context) => Scaffold(
body: TextButton(
onPressed: () => showBuzzModalBottomSheet<void>(
context: context,
title: titled ? 'People' : null,
showCloseButton: titled,
isScrollControlled: true,
builder: (context) {
contentInset = MediaQuery.paddingOf(context).bottom;
final list = SizedBox(
height: 300,
child: ListView.builder(
key: const ValueKey('clearance-scroll'),
padding: EdgeInsets.zero,
itemCount: 30,
itemBuilder: (_, index) => SizedBox(
height: 48,
child: Text('Person $index'),
),
),
);
return ownSafeArea
? SafeArea(top: false, child: list)
: list;
},
),
child: const Text('Open'),
),
),
),
),
);
await tester.tap(find.text('Open'));
await tester.pumpAndSettle();
final list = find.byKey(const ValueKey('clearance-scroll'));
final viewportBottom = tester.getRect(list).bottom;
// This is outside the scrolling viewport, not trailing list space.
expect(
viewportBottom,
lessThanOrEqualTo(844 - systemInset - Grid.half),
);
expect(contentInset, 0);
await tester.drag(list, const Offset(0, -2000));
await tester.pumpAndSettle();
expect(find.text('Person 29'), findsOneWidget);
expect(tester.getRect(list).bottom, viewportBottom);
expect(
tester.getRect(find.text('Person 29')).bottom,
lessThanOrEqualTo(viewportBottom),
);
expect(tester.takeException(), isNull);
} finally {
debugDefaultTargetPlatformOverride = null;
}
},
);
}
}
}
for (final left in [false, true]) {
for (final width in [480.0, 900.0]) {
testWidgets(
'bounded sheet localizes rail inset left=$left width=$width',
(tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
tester.view.physicalSize = const Size(951, 669);
tester.view.devicePixelRatio = 1;
final padding = FakeViewPadding(
left: left ? 76 : 0,
right: left ? 0 : 76,
bottom: 21,
);
tester.view.padding = padding;
tester.view.viewPadding = padding;
addTearDown(() {
tester.view.reset();
debugDefaultTargetPlatformOverride = null;
});
EdgeInsets? local;
await tester.pumpWidget(
MaterialApp(
theme: AppTheme.light(),
home: Builder(
builder: (context) => Scaffold(
body: TextButton(
onPressed: () => showBuzzModalBottomSheet<void>(
context: context,
useDuoFullWidth: false,
constraints: BoxConstraints(maxWidth: width),
showCloseButton: false,
builder: (context) {
local = MediaQuery.paddingOf(context);
return const SizedBox(
height: 100,
width: double.infinity,
);
},
),
child: const Text('Open'),
),
),
),
),
);
await tester.tap(find.text('Open'));
await tester.pumpAndSettle();
final overlap = (76 - (951 - width) / 2).clamp(0.0, double.infinity);
expect(local!.left, left ? overlap : 0);
expect(local!.right, left ? 0 : overlap);
expect(tester.takeException(), isNull);
debugDefaultTargetPlatformOverride = null;
},
);
}
}
Border sheetHeaderBorder(WidgetTester tester) {
final decoration =
tester
.widget<DecoratedBox>(
find.byKey(const ValueKey('buzz-sheet-scroll-divider')),
)
.decoration
as BoxDecoration;
return decoration.border! as Border;
}
testWidgets(
'keeps an opaque Flutter surface when iOS native support is unavailable',
(tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
try {
await tester.pumpWidget(
MaterialApp(
theme: AppTheme.light(),
home: const ConcentricSheetSurface(
enabled: true,
color: Colors.red,
child: SizedBox(height: 80, child: Text('Sheet body')),
),
),
);
await tester.pumpAndSettle();
expect(
find.byWidgetPredicate(
(widget) => widget is Material && widget.color == Colors.red,
),
findsOneWidget,
);
final contentClip = tester.widget<ClipRSuperellipse>(
find.byKey(const ValueKey('concentric-sheet-content-clip')),
);
expect(contentClip.borderRadius, BorderRadius.circular(Radii.dialog));
} finally {
debugDefaultTargetPlatformOverride = null;
}
},
);
testWidgets(
'replaces the Flutter fallback when native support is available',
(tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
const surfaceChannel = MethodChannel('buzz/concentric_sheet_surface');
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
surfaceChannel,
(call) async => call.method == 'isSupported' ? true : null,
);
try {
await tester.pumpWidget(
MaterialApp(
theme: AppTheme.light(),
home: const ConcentricSheetSurface(
enabled: true,
color: Colors.red,
child: SizedBox(height: 80, child: Text('Sheet body')),
),
),
);
await tester.pump();
expect(find.byType(UiKitView), findsOneWidget);
final nativeView = tester.widget<UiKitView>(find.byType(UiKitView));
expect(
nativeView.creationParams,
containsPair('independentCorners', true),
);
const viewChannel = MethodChannel('buzz/concentric_sheet_surface/73');
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
viewChannel,
(call) async => call.method == 'getCornerRadii'
? {
'topLeft': 24.0,
'topRight': 24.0,
'bottomLeft': 62.0,
'bottomRight': 48.0,
}
: null,
);
nativeView.onPlatformViewCreated!(73);
await tester.pumpAndSettle();
BorderRadius clipRadius() =>
tester
.widget<ClipRSuperellipse>(
find.byKey(
const ValueKey('concentric-sheet-content-clip'),
),
)
.borderRadius
as BorderRadius;
expect(clipRadius().bottomLeft, const Radius.circular(62));
expect(clipRadius().bottomRight, const Radius.circular(48));
// Rotation/folding can change each corner independently.
await tester.binding.defaultBinaryMessenger.handlePlatformMessage(
viewChannel.name,
const StandardMethodCodec().encodeMethodCall(
const MethodCall('cornerRadiiChanged', {
'topLeft': 48.0,
'topRight': 62.0,
'bottomLeft': 24.0,
'bottomRight': 24.0,
}),
),
(_) {},
);
await tester.pump();
expect(clipRadius().topRight, const Radius.circular(62));
expect(clipRadius().bottomLeft, const Radius.circular(24));
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
viewChannel,
null,
);
expect(
tester.widget<UiKitView>(find.byType(UiKitView)).hitTestBehavior,
PlatformViewHitTestBehavior.translucent,
);
expect(
find.ancestor(
of: find.byType(UiKitView),
matching: find.byWidgetPredicate(
(widget) => widget is IgnorePointer && widget.ignoring,
),
),
findsNothing,
);
expect(
find.byWidgetPredicate(
(widget) => widget is Material && widget.color == Colors.red,
),
findsNothing,
);
} finally {
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
surfaceChannel,
null,
);
debugDefaultTargetPlatformOverride = null;
}
},
);
testWidgets(
'native surfaces can limit concentric clipping to bottom corners',
(tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
const surfaceChannel = MethodChannel('buzz/concentric_sheet_surface');
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
surfaceChannel,
(call) async => call.method == 'isSupported' ? true : null,
);
try {
await tester.pumpWidget(
MaterialApp(
theme: AppTheme.light(),
home: const ConcentricSheetSurface(
enabled: true,
color: Colors.red,
backdropColor: Colors.black,
corners: ConcentricSurfaceCorners.bottom,
padding: EdgeInsets.zero,
providesSheetSurface: false,
child: SizedBox(height: 80, child: Text('App surface')),
),
),
);
await tester.pump();
final nativeSurface = tester.widget<UiKitView>(find.byType(UiKitView));
expect(nativeSurface.creationParams, containsPair('corners', 'bottom'));
expect(
nativeSurface.creationParams,
containsPair('backdropColor', Colors.black.toARGB32()),
);
final contentClip = tester.widget<ClipRSuperellipse>(
find.byKey(const ValueKey('concentric-sheet-content-clip')),
);
expect(
contentClip.borderRadius,
BorderRadius.vertical(bottom: Radius.circular(Radii.dialog * 2)),
);
} finally {
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
surfaceChannel,
null,
);
debugDefaultTargetPlatformOverride = null;
}
},
);
testWidgets('native surface colors follow live theme changes', (
tester,
) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
const supportChannel = MethodChannel('buzz/concentric_sheet_surface');
const viewChannel = MethodChannel('buzz/concentric_sheet_surface/42');
final colorUpdates = <MethodCall>[];
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
supportChannel,
(call) async => call.method == 'isSupported' ? true : null,
);
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
viewChannel,
(call) async {
colorUpdates.add(call);
return null;
},
);
Widget themedSurface(ThemeData theme) => MaterialApp(
theme: theme,
home: Builder(
builder: (context) => ConcentricSheetSurface(
enabled: true,
color: context.colors.surface,
backdropColor: context.appColors.huddleDrawerSurface,
corners: ConcentricSurfaceCorners.bottom,
padding: EdgeInsets.zero,
providesSheetSurface: false,
child: const SizedBox(height: 80, child: Text('App surface')),
),
),
);
try {
final darkTheme = AppTheme.dark();
await tester.pumpWidget(themedSurface(darkTheme));
await tester.pumpAndSettle();
tester.widget<UiKitView>(find.byType(UiKitView)).onPlatformViewCreated!(
42,
);
await tester.pump();
final initialColorUpdates = colorUpdates
.where((call) => call.method == 'updateColors')
.toList();
expect(initialColorUpdates, hasLength(1));
expect(
initialColorUpdates.single.arguments,
containsPair('color', darkTheme.colorScheme.surface.toARGB32()),
);
expect(
initialColorUpdates.single.arguments,
containsPair(
'backdropColor',
darkTheme.extension<AppColors>()!.huddleDrawerSurface.toARGB32(),
),
);
final lightTheme = AppTheme.light();
await tester.pumpWidget(themedSurface(lightTheme));
await tester.pumpAndSettle();
final updatedColorCalls = colorUpdates
.where((call) => call.method == 'updateColors')
.toList();
expect(updatedColorCalls.length, greaterThanOrEqualTo(2));
expect(
updatedColorCalls.last.arguments,
containsPair('color', lightTheme.colorScheme.surface.toARGB32()),
);
expect(
updatedColorCalls.last.arguments,
containsPair(
'backdropColor',
lightTheme.extension<AppColors>()!.huddleDrawerSurface.toARGB32(),
),
);
} finally {
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
supportChannel,
null,
);
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
viewChannel,
null,
);
debugDefaultTargetPlatformOverride = null;
}
});
testWidgets('native glass surfaces receive concentric composer geometry', (
tester,
) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
const supportChannel = MethodChannel('buzz/concentric_sheet_surface');
const viewChannel = MethodChannel('buzz/concentric_sheet_surface/84');
final updates = <MethodCall>[];
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
supportChannel,
(call) async => call.method == 'isSupported' ? true : null,
);
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
viewChannel,
(call) async {
updates.add(call);
return null;
},
);
try {
await tester.pumpWidget(
MaterialApp(
theme: AppTheme.light(),
home: const ConcentricSheetSurface(
enabled: true,
usesGlass: true,
minimumRadius: 26,
contentClipRadius: 18,
padding: EdgeInsets.zero,
providesSheetSurface: false,
child: SizedBox(height: 80, child: Text('Composer')),
),
),
);
await tester.pump();
final nativeSurface = tester.widget<UiKitView>(find.byType(UiKitView));
expect(nativeSurface.creationParams, containsPair('usesGlass', true));
expect(nativeSurface.creationParams, containsPair('minimumRadius', 26));
final contentClip = tester.widget<ClipRSuperellipse>(
find.byKey(const ValueKey('concentric-sheet-content-clip')),
);
expect(contentClip.borderRadius, BorderRadius.circular(18));
nativeSurface.onPlatformViewCreated!(84);
await tester.pump();
final geometryUpdate = updates.singleWhere(
(call) => call.method == 'updateGeometry',
);
expect(geometryUpdate.arguments, containsPair('minimumRadius', 26.0));
expect(geometryUpdate.arguments, containsPair('brightness', 'light'));
} finally {
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
supportChannel,
null,
);
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
viewChannel,
null,
);
debugDefaultTargetPlatformOverride = null;
}
});
testWidgets('native titled sheets leave the concentric surface unobscured', (
tester,
) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
const surfaceChannel = MethodChannel('buzz/concentric_sheet_surface');
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
surfaceChannel,
(call) async => call.method == 'isSupported' ? true : null,
);
try {
await tester.pumpWidget(
MaterialApp(
theme: AppTheme.light(),
home: Scaffold(
body: Builder(
builder: (context) => FilledButton(
onPressed: () => showBuzzModalBottomSheet<void>(
context: context,
title: 'Members',
builder: (sheetContext) => ColoredBox(
key: const ValueKey('sheet-container-surface'),
color: Theme.of(
sheetContext,
).colorScheme.surfaceContainerHighest,
child: const Text('Sheet body'),
),
),
child: const Text('Open sheet'),
),
),
),
),
);
await tester.tap(find.text('Open sheet'));
await tester.pumpAndSettle();
final nativeSurface = tester.widget<UiKitView>(
find.byWidgetPredicate(
(widget) =>
widget is UiKitView &&
widget.viewType == 'buzz/concentric_sheet_surface',
),
);
expect(
nativeSurface.creationParams,
containsPair(
'color',
lightColorScheme.surfaceContainerHighest.toARGB32(),
),
);
expect(
tester
.widget<ColoredBox>(
find.byKey(const ValueKey('sheet-container-surface')),
)
.color,
lightColorScheme.surface,
);
expect(nativeSurface.creationParams, isNot(contains('headerGradient')));
expect(
find.byKey(const ValueKey('buzz-sheet-header-gradient')),
findsNothing,
);
expect(
find.byKey(const ValueKey('buzz-sheet-surface-clip')),
findsNothing,
);
final contentClip = tester.widget<ClipRSuperellipse>(
find.byKey(const ValueKey('concentric-sheet-content-clip')),
);
expect(
contentClip.borderRadius,
const BorderRadius.vertical(
top: Radius.circular(ConcentricSheetSurface.sheetTopRadius),
bottom: Radius.circular(Radii.dialog * 2),
),
);
expect((nativeSurface.creationParams as Map)['topRadius'], 40.0);
expect(contentClip.clipBehavior, Clip.antiAlias);
expect(find.text('Members'), findsOneWidget);
final nativeClose = tester.widget<UiKitView>(
find.byWidgetPredicate(
(widget) =>
widget is UiKitView && widget.viewType == 'buzz/navigation_glass',
),
);
expect(nativeClose.creationParams, containsPair('icon', 'close'));
} finally {
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
surfaceChannel,
null,
);
debugDefaultTargetPlatformOverride = null;
}
});
testWidgets(
'non-iOS sheets use Flutter drag handle and shared close control',
(tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
try {
await tester.pumpWidget(
MaterialApp(
theme: AppTheme.light(),
home: Scaffold(
body: Builder(
builder: (context) => FilledButton(
onPressed: () => showBuzzModalBottomSheet<void>(
context: context,
title: 'Sheet title',
showDragHandle: true,
builder: (_) => const Text('Sheet body'),
),
child: const Text('Open sheet'),
),
),
),
),
);
await tester.tap(find.text('Open sheet'));
await tester.pumpAndSettle();
final closeButton = find.byTooltip('Close sheet');
final title = find.byKey(const ValueKey('buzz-sheet-title'));
expect(closeButton, findsOneWidget);
expect(title, findsOneWidget);
expect(
find.byKey(const ValueKey('buzz-sheet-surface-clip')),
findsOneWidget,
);
expect(tester.getSize(closeButton), const Size.square(44));
expect(tester.widget<Text>(title).style?.fontSize, 16);
expect(
find.byKey(const ValueKey('buzz-sheet-header-gradient')),
findsNothing,
);
expect(
tester
.widget<ColoredBox>(
find.byKey(const ValueKey('buzz-sheet-surface')),
)
.color,
lightColorScheme.surfaceContainerHighest,
);
expect(
tester.getTopLeft(find.text('Sheet body')).dy -
tester
.getTopLeft(find.byKey(const ValueKey('buzz-sheet-surface')))
.dy,
80,
);
expect(find.byType(BackdropFilter), findsNothing);
expect(
tester.widget<BottomSheet>(find.byType(BottomSheet)).backgroundColor,
Colors.transparent,
);
expect(
tester.getCenter(title).dx,
closeTo(tester.getCenter(find.byType(BottomSheet)).dx, 0.01),
);
expect(
tester.getCenter(title).dy,
closeTo(tester.getCenter(closeButton).dy, 0.01),
);
expect(
tester.getCenter(closeButton).dx,
greaterThan(tester.getCenter(title).dx),
);
final closeGutter = find.ancestor(
of: closeButton,
matching: find.byWidgetPredicate(
(widget) =>
widget is Padding &&
widget.padding ==
const EdgeInsets.only(
top: Grid.xxs,
left: Grid.gutter,
right: Grid.gutter,
bottom: Grid.xs,
),
),
);
expect(closeGutter, findsOneWidget);
final gutterRect = tester.getRect(closeGutter);
final closeRect = tester.getRect(closeButton);
expect(closeRect.top - gutterRect.top, Grid.gutter);
expect(gutterRect.right - closeRect.right, Grid.gutter);
expect(
tester.widget<BottomSheet>(find.byType(BottomSheet)).showDragHandle,
isFalse,
);
expect(
find.byKey(const ValueKey('buzz-sheet-drag-handle')),
findsOneWidget,
);
expect(
tester.getTopLeft(closeButton).dy -
tester.getTopLeft(find.byType(BottomSheet)).dy,
Grid.gutter,
);
final dismissHandle = find.bySemanticsLabel('Dismiss').first;
expect(dismissHandle, findsOneWidget);
final semantics = tester.getSemantics(dismissHandle);
expect(semantics.flagsCollection.isButton, isTrue);
expect(
semantics.getSemanticsData().hasAction(SemanticsAction.tap),
isTrue,
);
tester.binding.performSemanticsAction(
SemanticsActionEvent(
type: SemanticsAction.tap,
viewId: tester.view.viewId,
nodeId: semantics.id,
),
);
await tester.pumpAndSettle();
expect(find.text('Sheet body'), findsNothing);
} finally {
debugDefaultTargetPlatformOverride = null;
}
},
);
testWidgets('untitled Android sheets paint the utility route surface', (
tester,
) async {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
try {
await tester.pumpWidget(
MaterialApp(
theme: AppTheme.light(),
home: Scaffold(
body: Builder(
builder: (context) => FilledButton(
onPressed: () => showBuzzModalBottomSheet<void>(
context: context,
builder: (sheetContext) => ColoredBox(
key: const ValueKey('untitled-sheet-container'),
color: Theme.of(
sheetContext,
).colorScheme.surfaceContainerHighest,
child: const SizedBox(
height: 80,
child: Text('Sheet body'),
),
),
),
child: const Text('Open sheet'),
),
),
),
),
);
await tester.tap(find.text('Open sheet'));
await tester.pumpAndSettle();
expect(
tester.widget<BottomSheet>(find.byType(BottomSheet)).backgroundColor,
lightColorScheme.surfaceContainerHighest,
);
expect(
tester
.widget<ColoredBox>(
find.byKey(const ValueKey('untitled-sheet-container')),
)
.color,
lightColorScheme.surface,
);
} finally {
debugDefaultTargetPlatformOverride = null;
}
});
testWidgets('sheet divider appears only when content scrolls under header', (
tester,
) async {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
try {
await tester.pumpWidget(
MaterialApp(
theme: AppTheme.light(),
home: Scaffold(
body: Builder(
builder: (context) => FilledButton(
onPressed: () => showBuzzModalBottomSheet<void>(
context: context,
title: 'Theme',
isScrollControlled: true,
builder: (_) => SizedBox(
height: 360,
child: ListView.builder(
key: const ValueKey('sheet-scroll-view'),
itemCount: 30,
itemBuilder: (_, index) =>
SizedBox(height: 44, child: Text('Option $index')),
),
),
),
child: const Text('Open sheet'),
),
),
),
),
);
await tester.tap(find.text('Open sheet'));
await tester.pumpAndSettle();
expect(sheetHeaderBorder(tester).bottom.color.a, 0);
await tester.drag(
find.byKey(const ValueKey('sheet-scroll-view')),
const Offset(0, -100),
);
await tester.pumpAndSettle();
expect(
tester
.state<ScrollableState>(
find.descendant(
of: find.byKey(const ValueKey('sheet-scroll-view')),
matching: find.byType(Scrollable),
),
)
.position
.pixels,
greaterThan(0),
);
expect(sheetHeaderBorder(tester).bottom.color.a, greaterThan(0));
await tester.drag(
find.byKey(const ValueKey('sheet-scroll-view')),
const Offset(0, 500),
);
await tester.pumpAndSettle();
expect(sheetHeaderBorder(tester).bottom.color.a, 0);
} finally {
debugDefaultTargetPlatformOverride = null;
}
});
testWidgets('iOS paints the drag handle inside the concentric surface', (
tester,
) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
try {
await tester.pumpWidget(
MaterialApp(
theme: AppTheme.light(),
home: Scaffold(
body: Builder(
builder: (context) => FilledButton(
onPressed: () => showBuzzModalBottomSheet<void>(
context: context,
showDragHandle: true,
builder: (_) => const Text('Sheet body'),
),
child: const Text('Open sheet'),
),
),
),
),
);
await tester.tap(find.text('Open sheet'));
await tester.pumpAndSettle();
expect(
tester.widget<BottomSheet>(find.byType(BottomSheet)).showDragHandle,
isFalse,
);
final internalHandle = find.byKey(
const ValueKey('buzz-sheet-drag-handle'),
);
expect(internalHandle, findsOneWidget);
expect(tester.getSize(internalHandle), const Size(32, 4));
final dismissHandle = find.bySemanticsLabel('Dismiss');
expect(dismissHandle, findsOneWidget);
final semantics = tester.getSemantics(dismissHandle);
expect(semantics.flagsCollection.isButton, isTrue);
expect(
semantics.getSemanticsData().hasAction(SemanticsAction.tap),
isTrue,
);
expect(find.byTooltip('Close sheet'), findsOneWidget);
expect(find.text('Sheet body'), findsOneWidget);
} finally {
debugDefaultTargetPlatformOverride = null;
}
});
testWidgets('iOS compact sheets can omit X but retain the inside handle', (
tester,
) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
try {
await tester.pumpWidget(
MaterialApp(
theme: AppTheme.light(),
home: Scaffold(
body: Builder(
builder: (context) => FilledButton(
onPressed: () => showBuzzModalBottomSheet<void>(
context: context,
showDragHandle: true,
showCloseButton: false,
builder: (_) => const Text('Compact sheet body'),
),
child: const Text('Open compact sheet'),
),
),
),
),
);
await tester.tap(find.text('Open compact sheet'));
await tester.pumpAndSettle();
expect(
find.byKey(const ValueKey('buzz-sheet-drag-handle')),
findsOneWidget,
);
expect(find.byTooltip('Close sheet'), findsNothing);
expect(find.text('Compact sheet body'), findsOneWidget);
} finally {
debugDefaultTargetPlatformOverride = null;
}
});
}