mirror of
https://github.com/block/buzz.git
synced 2026-11-14 13:21:02 +00:00
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>
782 lines
27 KiB
Dart
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);
|
|
}
|
|
}
|