Files
buzz/mobile/test/features/settings/settings_page_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

782 lines
27 KiB
Dart

import 'package:buzz/shared/widgets/frosted_app_bar.dart';
import 'package:buzz/shared/widgets/frosted_scaffold.dart';
import 'package:buzz/shared/widgets/ios_navigation_bar.dart';
import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:buzz/features/settings/settings_page.dart';
import 'package:buzz/shared/widgets/adaptive_conversation_pane.dart';
import 'package:buzz/shared/community/community_membership_provider.dart';
import 'package:buzz/shared/community/community.dart';
import 'package:buzz/shared/community/community_provider.dart';
import 'package:buzz/shared/theme/theme.dart';
import 'package:buzz/shared/theme/buzz_icons.dart';
import 'package:buzz/shared/push/dev_push_lease.dart';
import 'package:buzz/shared/push/push_relay_capability_provider.dart';
import 'package:buzz/shared/relay/app_lifecycle_provider.dart';
import 'package:buzz/shared/widgets/app_list.dart';
import 'package:buzz/shared/widgets/app_list_card.dart';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
setUp(() {
PackageInfo.setMockInitialValues(
appName: 'Buzz',
packageName: 'xyz.block.buzz',
version: '0.16.0',
buildNumber: '432',
buildSignature: '',
);
});
testWidgets('wide Settings opens categories and editors in the detail pane', (
tester,
) async {
tester.view.devicePixelRatio = 1;
tester.view.physicalSize = const Size(1000, 800);
addTearDown(tester.view.reset);
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
await tester.pumpWidget(
ProviderScope(
overrides: [savedPrefsProvider.overrideWithValue(prefs)],
child: MaterialApp(
theme: AppTheme.light(),
home: SettingsPage(
profileHeader: const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
profileEditPageBuilder: (context) => FrostedScaffold(
body: const Text('Photo editor'),
appBar: FrostedAppBar(
nativeTitle: 'Photo editor',
leading: IconButton(
tooltip: 'Back to profile',
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.arrow_back),
),
),
),
),
),
),
);
await tester.pumpAndSettle();
expect(find.text('Select a setting'), findsNothing);
expect(find.text('Set status'), findsOneWidget);
expect(
Navigator.of(tester.element(find.text('Set status'))).canPop(),
isFalse,
);
final workspaceTheme = Theme.of(
tester.element(find.byType(AdaptiveConversationDestination)),
);
expect(
workspaceTheme.colorScheme.surface,
utilitySurfaceThemeData(AppTheme.light()).colorScheme.surface,
);
expect(
workspaceTheme.scaffoldBackgroundColor,
workspaceTheme.colorScheme.surface,
);
final categoryRows = find.byWidgetPredicate(
(w) =>
w is AppListRow &&
[
'Status',
'Profile',
'Notifications',
'Connection',
].contains(w.title),
);
final categoryBounds = <String, Rect>{
for (final element in categoryRows.evaluate())
(element.widget as AppListRow).title: tester.getRect(
find.byWidget(element.widget),
),
};
expect(categoryBounds.length, greaterThanOrEqualTo(3));
for (final element in categoryRows.evaluate()) {
expect(
find.descendant(
of: find.byWidget(element.widget),
matching: find.byIcon(BuzzIcons.chevronRight),
),
findsOneWidget,
);
}
expect(find.byIcon(Icons.check), findsNothing);
await tester.tap(find.text('Profile'));
await tester.pumpAndSettle();
for (final entry in categoryBounds.entries) {
final row = find.byWidgetPredicate(
(w) => w is AppListRow && w.title == entry.key,
);
expect(tester.getRect(row), entry.value);
}
expect(find.byIcon(Icons.check), findsNothing);
await tester.tap(find.text('Status'));
await tester.pumpAndSettle();
expect(find.text('Set status'), findsOneWidget);
expect(
Navigator.of(tester.element(find.text('Set status'))).canPop(),
isFalse,
);
await tester.tap(find.text('Profile'));
await tester.pumpAndSettle();
expect(
tester.getCenter(find.text('Edit photo')).dx,
greaterThan(tester.getCenter(find.text('Status')).dx),
);
expect(
Navigator.of(tester.element(find.text('Edit photo'))).canPop(),
isFalse,
);
await tester.tap(find.text('Edit photo'));
await tester.pumpAndSettle();
expect(find.text('Photo editor'), findsOneWidget);
await tester.binding.handlePopRoute();
await tester.pumpAndSettle();
expect(find.text('Edit photo'), findsOneWidget);
await tester.tap(find.text('Edit photo'));
await tester.pumpAndSettle();
expect(find.text('Status'), findsOneWidget);
tester.view.physicalSize = const Size(390, 800);
await tester.pumpAndSettle();
expect(find.text('Photo editor'), findsOneWidget);
await tester.binding.handlePopRoute();
await tester.pumpAndSettle();
expect(find.text('Edit photo'), findsOneWidget);
await tester.binding.handlePopRoute();
await tester.pumpAndSettle();
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
tester.view.physicalSize = const Size(678, 466);
tester.view.viewPadding = const FakeViewPadding(right: 84);
tester.view.padding = const FakeViewPadding(right: 84);
await tester.pumpAndSettle();
expect(find.text('Profile'), findsNothing);
expect(find.text('Display name'), findsOneWidget);
expect(
find.byWidgetPredicate(
(widget) => widget is FrostedAppBar && widget.railOnly,
),
findsOneWidget,
);
expect(find.text('Settings'), findsNothing);
// Book-mode Duo keeps Profile underneath the pushed photo editor.
tester.view.physicalSize = const Size(874, 402);
tester.view.viewPadding = const FakeViewPadding(right: 76);
tester.view.padding = tester.view.viewPadding;
await tester.pumpAndSettle();
await tester.tap(find.text('Profile'));
await tester.pumpAndSettle();
await tester.tap(find.text('Edit photo'));
await tester.pumpAndSettle();
expect(find.text('Photo editor'), findsOneWidget);
expect(
find.byKey(const ValueKey('duo-toolbar-Close settings')),
findsOneWidget,
);
final closeSettings = find.byKey(
const ValueKey('duo-toolbar-Close settings'),
);
final backToProfile = find.byKey(
const ValueKey('duo-toolbar-Back to profile'),
);
expect(backToProfile, findsOneWidget);
expect(
tester.getRect(closeSettings).overlaps(tester.getRect(backToProfile)),
isFalse,
);
tester
.widget<IosNavigationBar>(
find.descendant(
of: backToProfile,
matching: find.byType(IosNavigationBar),
),
)
.leading!
.onPressed!();
await tester.pumpAndSettle();
expect(find.text('Edit photo'), findsOneWidget);
expect(find.text('Set status'), findsNothing);
await tester.pumpWidget(const SizedBox.shrink());
debugDefaultTargetPlatformOverride = null;
});
for (final buildNumber in ['432', '', '2147483647']) {
testWidgets('shows version with build number "$buildNumber"', (
tester,
) async {
tester.view.physicalSize = const Size(320, 700);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
PackageInfo.setMockInitialValues(
appName: 'Buzz',
packageName: 'xyz.block.buzz',
version: '0.16.0',
buildNumber: buildNumber,
buildSignature: '',
);
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
_usePhoneViewport(tester);
await tester.pumpWidget(
ProviderScope(
overrides: [
savedPrefsProvider.overrideWithValue(prefs),
currentCommunityRoleProvider.overrideWithValue(
const AsyncData<CommunityMemberRole?>(CommunityMemberRole.admin),
),
],
child: MaterialApp(
theme: AppTheme.light(),
builder: (context, child) => MediaQuery(
data: MediaQuery.of(
context,
).copyWith(textScaler: TextScaler.linear(2)),
child: child!,
),
home: SettingsPage(
profileHeader: const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
),
),
);
expect(find.text('Invite to community'), findsNothing);
await tester.pumpAndSettle();
final label = buildNumber.isEmpty ? 'v0.16.0' : 'v0.16.0 ($buildNumber)';
final version = find.text(label);
await tester.scrollUntilVisible(
version,
200,
scrollable: find.byType(Scrollable).first,
);
await tester.pumpAndSettle();
expect(version.hitTestable(), findsOneWidget);
expect(
find.ancestor(of: version, matching: find.byType(ListView)),
findsOneWidget,
);
final beforeScroll = tester.getTopLeft(version).dy;
await tester.drag(find.byType(ListView), const Offset(0, 100));
await tester.pumpAndSettle();
// Dragging down can push the footer just below the viewport.
expect(
tester.getTopLeft(find.text(label, skipOffstage: false)).dy,
greaterThan(beforeScroll),
);
expect(tester.takeException(), isNull);
});
}
for (final viaKeyboard in [false, true]) {
testWidgets(
'${viaKeyboard ? 'keyboard activation of' : 'tapping'} the version copies the whole version string',
(tester) async {
String? copied;
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
(call) async {
if (call.method == 'Clipboard.setData') {
copied = (call.arguments as Map)['text'] as String?;
}
return null;
},
);
addTearDown(
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
null,
),
);
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
await tester.pumpWidget(
ProviderScope(
overrides: [savedPrefsProvider.overrideWithValue(prefs)],
child: MaterialApp(
theme: AppTheme.light(),
home: SettingsPage(
profileHeader: const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
),
),
);
await tester.pumpAndSettle();
final version = find.text('v0.16.0 (432)');
await tester.scrollUntilVisible(
version,
200,
scrollable: find.byType(Scrollable).first,
);
await tester.pumpAndSettle();
final target = find.ancestor(
of: version,
matching: find.byType(InkWell),
);
expect(
tester.getSize(target).height,
greaterThanOrEqualTo(kMinInteractiveDimension),
);
if (viaKeyboard) {
final versionFocus = Focus.of(tester.element(version));
var tabs = 0;
while (!versionFocus.hasPrimaryFocus) {
expect(
tabs++,
lessThan(50),
reason: 'version is not reachable by Tab',
);
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pump();
}
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
} else {
await tester.tap(version);
}
await tester.pump();
expect(copied, 'v0.16.0 (432)');
expect(find.text('Version copied'), findsOneWidget);
await tester.pump(const Duration(seconds: 5));
await tester.pumpAndSettle();
},
);
}
for (final outcome in ['absent', 'error']) {
testWidgets('hides notifications while capability loads and is $outcome', (
tester,
) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
final capability = Completer<BuzzPushLeaseDescriptor?>();
_usePhoneViewport(tester);
await tester.pumpWidget(
ProviderScope(
overrides: [
savedPrefsProvider.overrideWithValue(prefs),
activeCommunityProvider.overrideWith(
(ref) async => Community.create(
name: 'No push',
relayUrl: 'wss://relay.example',
).copyWith(pushNotificationsEnabled: false),
),
currentRelayPushDescriptorProvider.overrideWith(
(ref) => capability.future,
),
],
child: MaterialApp(
theme: AppTheme.light(),
home: SettingsPage(
profileHeader: const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
),
),
);
await tester.pumpAndSettle();
expect(find.text('Notifications'), findsNothing);
expect(find.byType(Switch), findsNothing);
if (outcome == 'absent') {
capability.complete(null);
} else {
capability.completeError(StateError('discovery failed'));
}
await tester.pumpAndSettle();
expect(find.text('Notifications'), findsNothing);
expect(find.text('Push notifications'), findsNothing);
expect(find.byType(Switch), findsNothing);
expect(tester.takeException(), isNull);
debugDefaultTargetPlatformOverride = null;
});
}
testWidgets('shows the persisted per-community push opt-in on iOS', (
tester,
) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
final community = Community.create(
name: 'Team',
relayUrl: 'wss://relay.example',
).copyWith(pushNotificationsEnabled: true);
final capability = Future<BuzzPushLeaseDescriptor?>.value(_pushDescriptor);
_usePhoneViewport(tester);
await tester.pumpWidget(
ProviderScope(
overrides: [
savedPrefsProvider.overrideWithValue(prefs),
activeCommunityProvider.overrideWith((ref) async => community),
currentRelayPushDescriptorProvider.overrideWith((ref) => capability),
appLifecycleProvider.overrideWith(_SettingsLifecycleNotifier.new),
],
child: MaterialApp(
theme: AppTheme.light(),
home: SettingsPage(
profileHeader: const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
),
),
);
await tester.pumpAndSettle();
expect(
find.byKey(const ValueKey('push-notifications-enabled')),
findsOneWidget,
);
expect(tester.widget<Switch>(find.byType(Switch)).value, isTrue);
debugDefaultTargetPlatformOverride = null;
});
for (final brightness in Brightness.values) {
testWidgets(
'shows status above profile editing and routes photo directly in ${brightness.name}',
(tester) async {
if (Platform.environment.containsKey('PROFILE_SCREENSHOTS')) {
await tester.runAsync(() async {
for (final font in {
'Inter': 'assets/fonts/InterVariable.ttf',
'BuzzTabler': 'assets/fonts/TablerIcons.ttf',
}.entries) {
await (FontLoader(
font.key,
)..addFont(rootBundle.load(font.value))).load();
}
});
}
tester.view.physicalSize = const Size(390, 844);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
_usePhoneViewport(tester);
await tester.pumpWidget(
ProviderScope(
overrides: [savedPrefsProvider.overrideWithValue(prefs)],
child: MaterialApp(
theme: brightness == Brightness.dark
? AppTheme.dark()
: AppTheme.light(),
home: SettingsPage(
profileHeader: const SizedBox.square(dimension: 128),
profileEditPageBuilder: (_) =>
const Scaffold(body: Text('Profile editor destination')),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
),
),
);
await tester.pumpAndSettle();
expect(find.text('Profile'), findsNothing);
expect(find.text('Edit profile'), findsNothing);
expect(find.text('Display name'), findsOneWidget);
expect(find.text('Profile description'), findsOneWidget);
expect(find.text('Edit photo'), findsOneWidget);
final optionCard = find.descendant(
of: find.byKey(const ValueKey('edit-profile-options')),
matching: find.byType(Material),
);
expect(tester.getSize(optionCard.first).height, greaterThan(150));
for (final key in const [
'edit-profile-display-name',
'edit-profile-description',
'edit-profile-photo',
]) {
expect(
tester
.widget<AppListRow>(find.byKey(ValueKey(key)))
.verticalPadding,
Grid.xs,
);
}
expect(
find.byKey(const ValueKey('settings-edit-profile')),
findsNothing,
);
expect(find.byType(BottomSheet), findsNothing);
expect(
tester.widget<AppListCard>(find.byType(AppListCard).first).key,
const ValueKey('status-identity-options'),
);
if (Platform.environment['PROFILE_SCREENSHOTS'] case final directory?) {
final boundary = tester
.element(find.byType(SettingsPage))
.findAncestorRenderObjectOfType<RenderRepaintBoundary>()!;
await tester.runAsync(() async {
final image = await boundary.toImage(pixelRatio: 2);
final bytes = await image.toByteData(
format: ui.ImageByteFormat.png,
);
await Directory(directory).create(recursive: true);
await File(
'$directory/profile-${brightness.name}.png',
).writeAsBytes(bytes!.buffer.asUint8List());
image.dispose();
});
}
await tester.tap(find.byKey(const ValueKey('edit-profile-photo')));
await tester.pumpAndSettle();
expect(find.text('Profile editor destination'), findsOneWidget);
},
);
}
testWidgets('opens profile text editors directly from settings', (
tester,
) async {
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
final opened = <String>[];
_usePhoneViewport(tester);
await tester.pumpWidget(
ProviderScope(
overrides: [savedPrefsProvider.overrideWithValue(prefs)],
child: MaterialApp(
theme: AppTheme.light(),
home: SettingsPage(
profileHeader: const SizedBox.shrink(),
onSetStatus: (_) => opened.add('status'),
onEditDisplayName: (_) async => opened.add('name'),
onEditProfileDescription: (_) async => opened.add('description'),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
),
),
);
await tester.pumpAndSettle();
expect(
tester.getTopLeft(find.text('Set status')).dy,
lessThan(tester.getTopLeft(find.text('Display name')).dy),
);
await tester.tap(find.text('Set status'));
expect(opened, ['status']);
await tester.tap(find.byKey(const ValueKey('edit-profile-display-name')));
await tester.pumpAndSettle();
expect(opened, ['status', 'name']);
expect(find.text('Edit profile'), findsNothing);
await tester.tap(find.byKey(const ValueKey('edit-profile-description')));
await tester.pumpAndSettle();
expect(opened, ['status', 'name', 'description']);
});
testWidgets('uses native navigation with Close and no Edit action on iOS', (
tester,
) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
_usePhoneViewport(tester);
await tester.pumpWidget(
ProviderScope(
overrides: [savedPrefsProvider.overrideWithValue(prefs)],
child: MaterialApp(
theme: AppTheme.light(),
home: SettingsPage(
profileHeader: const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
),
),
);
await tester.pumpAndSettle();
final nativeBar = tester.widget<UiKitView>(find.byType(UiKitView));
expect(nativeBar.viewType, 'buzz/ios_navigation_bar');
final params = nativeBar.creationParams! as Map<String, Object?>;
expect(params['title'], 'Settings');
expect(params['largeTitle'], isFalse);
expect(params['leading'], containsPair('symbol', 'xmark'));
expect(params['leading'], containsPair('label', 'Close settings'));
expect(params['actions'], isEmpty);
debugDefaultTargetPlatformOverride = null;
});
testWidgets('keeps community controls out of personal settings', (
tester,
) async {
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
_usePhoneViewport(tester);
await tester.pumpWidget(
ProviderScope(
overrides: [
savedPrefsProvider.overrideWithValue(prefs),
currentCommunityRoleProvider.overrideWithValue(
const AsyncData<CommunityMemberRole?>(CommunityMemberRole.admin),
),
],
child: MaterialApp(
theme: AppTheme.light(),
home: SettingsPage(
profileHeader: const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
),
),
);
await tester.pumpAndSettle();
expect(find.text('Invite to community'), findsNothing);
expect(
find.text('Add people directly or share an invite link'),
findsNothing,
);
});
testWidgets('keeps personal settings independent of community role errors', (
tester,
) async {
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
_usePhoneViewport(tester);
await tester.pumpWidget(
ProviderScope(
overrides: [
savedPrefsProvider.overrideWithValue(prefs),
currentCommunityRoleProvider.overrideWithValue(
AsyncError<CommunityMemberRole?>(
Exception('membership query failed'),
StackTrace.empty,
),
),
],
child: MaterialApp(
theme: AppTheme.light(),
home: SettingsPage(
profileHeader: const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
),
),
);
await tester.pumpAndSettle();
expect(find.text('Invite to community'), findsNothing);
});
testWidgets('hides community invite navigation from plain members', (
tester,
) async {
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
_usePhoneViewport(tester);
await tester.pumpWidget(
ProviderScope(
overrides: [
savedPrefsProvider.overrideWithValue(prefs),
currentCommunityRoleProvider.overrideWithValue(
const AsyncData<CommunityMemberRole?>(CommunityMemberRole.member),
),
],
child: MaterialApp(
theme: AppTheme.light(),
home: SettingsPage(
profileHeader: const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
),
),
);
await tester.pumpAndSettle();
expect(find.text('Invite to community'), findsNothing);
});
testWidgets('uses a 24dp rhythm between profile settings groups', (
tester,
) async {
tester.view.physicalSize = const Size(390, 1600);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
_usePhoneViewport(tester);
await tester.pumpWidget(
ProviderScope(
overrides: [
savedPrefsProvider.overrideWithValue(prefs),
currentCommunityRoleProvider.overrideWithValue(
const AsyncData<CommunityMemberRole?>(CommunityMemberRole.admin),
),
],
child: MaterialApp(
theme: AppTheme.light(),
home: SettingsPage(
profileHeader: const SizedBox(height: 100),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
),
),
);
await tester.pumpAndSettle();
final sections = tester.widgetList<AppListCard>(find.byType(AppListCard));
expect(sections, isNotEmpty);
expect(
sections.every((section) => section.verticalPadding == Grid.twelve),
isTrue,
);
});
}
class _SettingsLifecycleNotifier extends AppLifecycleNotifier {
@override
AppLifecycleState build() => AppLifecycleState.resumed;
}
const _pushDescriptor = BuzzPushLeaseDescriptor(
origin: 'wss://relay.example',
executorKeyId: 'key',
executorPubkey: 'pubkey',
transport: 'apns',
maxLeaseTtlSeconds: 3600,
maxContentLength: 4096,
maxPlaintextLength: 4096,
maxEndpointLength: 2048,
maxStringLength: 512,
);
void _usePhoneViewport(WidgetTester tester) {
if (tester.view.physicalSize.width / tester.view.devicePixelRatio >= 640) {
tester.view.devicePixelRatio = 1;
tester.view.physicalSize = const Size(390, 1200);
addTearDown(tester.view.reset);
}
}