feat(mobile): organize community and profile settings (#8056)

## Summary

Move Invite, Appearance, and Remove community into a **Community
settings** section in the community-avatar sheet, above the existing
switcher. Keep the sheet untitled and use circle-minus for removal.

Personal Settings groups **Set status** and **Copy public key (npub)**
above an untitled card containing **Display name**, **Profile
description**, and **Edit photo**. Remove the leading icons from status,
public key, and Send identity to desktop; keep the latter in its own
untitled card. Remove the avatar status badge and top Edit action, and
rename the Theme page to **Appearance**. Opening the presence chip
triggers selection haptics; copying plays native iOS success feedback
and confirms with “Public key (npub) copied.”

### Testing

- Mobile formatting, analysis, and file-size checks passed.
- 111 affected widget tests passed; all 48 settings tests passed again
after the final labels and icon changes.
- Final release build installed and launched on a physical iPhone in the
separate Buzz Community preview, retaining its existing test account.
- Full repository `just ci` was blocked by local disk exhaustion during
Rust compilation. The full mobile suite passed (2,609 passed, four
skipped), plus three push-unconfigured tests. The presence and copy
feedback passed focused widget tests and analysis, including native iOS
success-channel coverage.

### Snapshots

Light and dark Flutter widget renders with fixture data. Images are
GitHub attachments, not committed to the repository. The
personal-settings snapshots precede the final “(npub)” label
clarification.

| Surface | Light | Dark |
| --- | --- | --- |
| Community sheet | <img
src="https://github.com/user-attachments/assets/d52a192e-bd40-4263-ac2d-05d589c7ebf2"
width="280" alt="Community sheet, light"> | <img
src="https://github.com/user-attachments/assets/8756ebe9-b6d4-4920-8fb7-1cb7ee8ef778"
width="280" alt="Community sheet, dark"> |
| Personal settings | <img
src="https://github.com/user-attachments/assets/962b98bd-e4cf-4cd1-8948-69b349390ceb"
width="280" alt="Personal settings, light"> | <img
src="https://github.com/user-attachments/assets/223bb0a8-e10e-43b1-83ea-e0955af4627a"
width="280" alt="Personal settings, dark"> |
| Appearance | <img
src="https://github.com/user-attachments/assets/334ba797-55cb-47ce-b677-0befafb7659b"
width="280" alt="Appearance, light"> | <img
src="https://github.com/user-attachments/assets/79b62503-d476-4dbc-8c75-8cee0ee37e98"
width="280" alt="Appearance, dark"> |

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
klopez4212
2026-10-03 10:06:05 +01:00
committed by GitHub
parent 8af2d91f37
commit 33f54de2dd
20 changed files with 1002 additions and 756 deletions

View File

@@ -5,6 +5,7 @@ import 'package:buzz/features/settings/settings_page.dart';
import 'package:buzz/shared/auth/auth.dart';
import 'package:buzz/shared/relay/relay.dart';
import 'package:buzz/shared/theme/theme.dart';
import 'package:buzz/shared/widgets/app_list_card.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -15,75 +16,124 @@ import 'package:shared_preferences/shared_preferences.dart';
import '../../helpers/widget_helpers.dart';
void main() {
testWidgets('shows a compact copyable identity row', (tester) async {
MethodCall? clipboardCall;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.platform, (call) async {
if (call.method == 'Clipboard.setData') clipboardCall = call;
testWidgets(
'shows a compact copyable identity row',
(tester) async {
var successFeedback = false;
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
const MethodChannel('buzz/haptics'),
(call) async {
expect(call.method, 'success');
successFeedback = true;
return null;
});
addTearDown(
() => TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.platform, null),
);
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
},
);
addTearDown(() {
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
const MethodChannel('buzz/haptics'),
null,
);
});
MethodCall? clipboardCall;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.platform, (call) async {
if (call.method == 'Clipboard.setData') clipboardCall = call;
return null;
});
addTearDown(
() => TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.platform, null),
);
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
await tester.pumpWidget(
WidgetHelpers.testable(
overrides: [
relayConfigProvider.overrideWith(_RelayConfigNotifier.new),
authProvider.overrideWith(_AuthNotifier.new),
pairingProvider.overrideWith(
() => _PairingNotifier(Future<bool>.value(true)),
await tester.pumpWidget(
WidgetHelpers.testable(
overrides: [
relayConfigProvider.overrideWith(_RelayConfigNotifier.new),
authProvider.overrideWith(_AuthNotifier.new),
pairingProvider.overrideWith(
() => _PairingNotifier(Future<bool>.value(true)),
),
savedPrefsProvider.overrideWithValue(prefs),
],
child: SettingsPage(
profileHeader: const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
savedPrefsProvider.overrideWithValue(prefs),
],
child: SettingsPage(
profileHeader: const SizedBox.shrink(),
invitePageBuilder: (_) => const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
),
);
await tester.pump();
await tester.ensureVisible(find.text('Identity (pubkey)'));
await tester.pumpAndSettle();
);
await tester.pump();
await tester.ensureVisible(find.text('Copy public key (npub)'));
await tester.pumpAndSettle();
final expectedPubkey = nostr.Keys(
'1111111111111111111111111111111111111111111111111111111111111111',
).public;
// Keys('1'×64).public ↔ this npub — the NIP-19 canonical vector for the
// identity row, hardcoded so the codec itself stays under test.
const expectedNpub =
'npub1fu64hh9hes90w2808n8tjc2ajp5yhddjef0ctx4s7zmsgp6cwx4qgy4eg9';
expect(find.text('Connected to'), findsNothing);
expect(find.text('https://relay.test'), findsNothing);
// Neither the raw hex key nor the full npub is rendered visually — the
// full npub is exposed through a11y and the clipboard only.
expect(find.text(expectedPubkey), findsNothing);
expect(find.text(expectedNpub), findsNothing);
// The identity row's a11y value carries the full npub (not raw hex).
expect(
find.ancestor(
of: find.text('Identity (pubkey)'),
matching: find.byWidgetPredicate(
(widget) =>
widget is Semantics && widget.properties.value == expectedNpub,
final expectedPubkey = nostr.Keys(
'1111111111111111111111111111111111111111111111111111111111111111',
).public;
// Keys('1'×64).public ↔ this npub — the NIP-19 canonical vector for the
// identity row, hardcoded so the codec itself stays under test.
const expectedNpub =
'npub1fu64hh9hes90w2808n8tjc2ajp5yhddjef0ctx4s7zmsgp6cwx4qgy4eg9';
final statusCard = find.byKey(const ValueKey('status-identity-options'));
expect(
find.descendant(of: statusCard, matching: find.text('Set status')),
findsOneWidget,
);
expect(
find.descendant(
of: statusCard,
matching: find.text('Copy public key (npub)'),
),
),
findsOneWidget,
);
final copy = tester.getRect(find.byIcon(LucideIcons.copy));
final chevron = tester.getRect(find.byIcon(LucideIcons.chevronRight).first);
expect(copy.center.dx, closeTo(chevron.center.dx, 0.5));
findsOneWidget,
);
expect(
tester.getTopLeft(find.text('Copy public key (npub)')).dy,
lessThan(tester.getTopLeft(find.text('Display name')).dy),
);
expect(find.text('Connection'), findsNothing);
final sendCard = find.ancestor(
of: find.text('Send identity to desktop'),
matching: find.byType(AppListCard),
);
expect(
find.descendant(
of: sendCard,
matching: find.text('Copy public key (npub)'),
),
findsNothing,
);
expect(find.text('Connected to'), findsNothing);
expect(find.text('https://relay.test'), findsNothing);
// Neither the raw hex key nor the full npub is rendered visually — the
// full npub is exposed through a11y and the clipboard only.
expect(find.text(expectedPubkey), findsNothing);
expect(find.text(expectedNpub), findsNothing);
// The identity row's a11y value carries the full npub (not raw hex).
expect(
find.ancestor(
of: find.text('Copy public key (npub)'),
matching: find.byWidgetPredicate(
(widget) =>
widget is Semantics && widget.properties.value == expectedNpub,
),
),
findsOneWidget,
);
final copy = tester.getRect(find.byIcon(LucideIcons.copy));
final chevron = tester.getRect(
find.byIcon(LucideIcons.chevronRight).first,
);
expect(copy.center.dx, closeTo(chevron.center.dx, 0.5));
await tester.tap(find.text('Identity (pubkey)'));
await tester.pump();
expect(clipboardCall?.method, 'Clipboard.setData');
expect(clipboardCall?.arguments, {'text': expectedNpub});
expect(find.text('Pubkey copied'), findsOneWidget);
});
await tester.tap(find.text('Copy public key (npub)'));
await tester.pump();
expect(successFeedback, isTrue);
expect(clipboardCall?.method, 'Clipboard.setData');
expect(clipboardCall?.arguments, {'text': expectedNpub});
expect(find.text('Public key (npub) copied'), findsOneWidget);
},
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
);
testWidgets('waits for a resumed frame before navigating after auth', (
tester,
@@ -103,7 +153,6 @@ void main() {
],
child: SettingsPage(
profileHeader: const SizedBox.shrink(),
invitePageBuilder: (_) => const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) =>
const Scaffold(body: Text('Identity recovery')),
),
@@ -141,7 +190,6 @@ void main() {
],
child: SettingsPage(
profileHeader: const SizedBox.shrink(),
invitePageBuilder: (_) => const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) =>
const Scaffold(body: Text('Identity recovery')),
),
@@ -181,7 +229,6 @@ void main() {
],
child: SettingsPage(
profileHeader: const SizedBox.shrink(),
invitePageBuilder: (_) => const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) =>
const Scaffold(body: Text('Identity recovery')),
),
@@ -224,7 +271,6 @@ void main() {
],
child: SettingsPage(
profileHeader: const SizedBox.shrink(),
invitePageBuilder: (_) => const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) =>
const Scaffold(body: Text('Identity recovery')),
),
@@ -268,7 +314,6 @@ void main() {
],
child: SettingsPage(
profileHeader: const SizedBox.shrink(),
invitePageBuilder: (_) => const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) =>
const Scaffold(body: Text('Identity recovery')),
),

View File

@@ -101,7 +101,6 @@ void main() {
theme: AppTheme.light(),
home: SettingsPage(
profileHeader: const SizedBox.shrink(),
invitePageBuilder: (_) => const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
),

View File

@@ -45,13 +45,12 @@ void main() {
),
home: SettingsPage(
profileHeader: const SizedBox.shrink(),
invitePageBuilder: (_) => const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
),
),
);
expect(find.text('Invite to community'), findsOneWidget);
expect(find.text('Invite to community'), findsNothing);
expect(find.text('v0.16.0 (432)'), findsNothing);
expect(find.byTooltip('Close settings'), findsOneWidget);

View File

@@ -1,4 +1,8 @@
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/community/community_membership_provider.dart';
@@ -64,13 +68,12 @@ void main() {
),
home: SettingsPage(
profileHeader: const SizedBox.shrink(),
invitePageBuilder: (_) => const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
),
),
);
expect(find.text('Invite to community'), findsOneWidget);
expect(find.text('Invite to community'), findsNothing);
await tester.pumpAndSettle();
expect(
find.text(buildNumber.isEmpty ? 'v0.16.0' : 'v0.16.0 ($buildNumber)'),
@@ -114,7 +117,6 @@ void main() {
theme: AppTheme.light(),
home: SettingsPage(
profileHeader: const SizedBox.shrink(),
invitePageBuilder: (_) => const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
),
@@ -167,7 +169,6 @@ void main() {
theme: AppTheme.light(),
home: SettingsPage(
profileHeader: const SizedBox.shrink(),
invitePageBuilder: (_) => const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
),
@@ -219,7 +220,6 @@ void main() {
theme: AppTheme.light(),
home: SettingsPage(
profileHeader: const SizedBox.shrink(),
invitePageBuilder: (_) => const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
),
@@ -269,7 +269,6 @@ void main() {
theme: AppTheme.light(),
home: SettingsPage(
profileHeader: const SizedBox.shrink(),
invitePageBuilder: (_) => const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
),
@@ -288,75 +287,102 @@ void main() {
debugDefaultTargetPlatformOverride = null;
});
testWidgets('opens profile edit choices and routes photo directly', (
tester,
) async {
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
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',
'packages/lucide_icons_flutter/Lucide':
'packages/lucide_icons_flutter/assets/lucide.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();
await tester.pumpWidget(
ProviderScope(
overrides: [savedPrefsProvider.overrideWithValue(prefs)],
child: MaterialApp(
theme: AppTheme.light(),
home: SettingsPage(
profileHeader: const SizedBox.square(dimension: 128),
profileEditPageBuilder: (_) =>
const Scaffold(body: Text('Profile editor destination')),
invitePageBuilder: (_) => const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
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();
);
await tester.pumpAndSettle();
expect(find.text('Profile'), findsNothing);
await tester.tap(find.byKey(const ValueKey('settings-edit-profile')));
await tester.pumpAndSettle();
expect(find.text('Edit profile'), findsOneWidget);
expect(find.text('Display name'), findsOneWidget);
expect(find.text('Profile description'), findsOneWidget);
expect(find.text('Photo'), findsOneWidget);
final optionCard = find.descendant(
of: find.byKey(const ValueKey('edit-profile-options')),
matching: find.byType(Material),
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);
},
);
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,
);
}
final sheetContent = tester.widget<Padding>(
find.byKey(const ValueKey('edit-profile-sheet-content')),
);
expect(sheetContent.padding, const EdgeInsets.only(bottom: Grid.xs));
final sheetSafeArea = tester.widget<SafeArea>(
find.ancestor(
of: find.byKey(const ValueKey('edit-profile-sheet-content')),
matching: find.byType(SafeArea),
),
);
expect(sheetSafeArea.top, isFalse);
expect(sheetSafeArea.bottom, isTrue);
final sheetRect = tester.getRect(find.byType(BottomSheet));
final optionRect = tester.getRect(
find.byKey(const ValueKey('edit-profile-options')),
);
expect(sheetRect.height, lessThan(340));
expect(sheetRect.bottom - optionRect.bottom, Grid.xs);
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 after dismissing the choices', (
testWidgets('opens profile text editors directly from settings', (
tester,
) async {
SharedPreferences.setMockInitialValues({});
@@ -370,9 +396,9 @@ void main() {
theme: AppTheme.light(),
home: SettingsPage(
profileHeader: const SizedBox.shrink(),
onSetStatus: (_) => opened.add('status'),
onEditDisplayName: (_) async => opened.add('name'),
onEditProfileDescription: (_) async => opened.add('description'),
invitePageBuilder: (_) => const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
),
@@ -380,56 +406,20 @@ void main() {
);
await tester.pumpAndSettle();
await tester.tap(find.byKey(const ValueKey('settings-edit-profile')));
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, ['name']);
expect(opened, ['status', 'name']);
expect(find.text('Edit profile'), findsNothing);
await tester.tap(find.byKey(const ValueKey('settings-edit-profile')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const ValueKey('edit-profile-description')));
await tester.pumpAndSettle();
expect(opened, ['name', 'description']);
});
testWidgets('places Theme second in the Community section', (tester) async {
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
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(),
invitePageBuilder: (_) => const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
),
),
);
await tester.pumpAndSettle();
expect(find.text('Theme'), findsOneWidget);
expect(
tester.getTopLeft(find.text('Theme')).dy,
greaterThan(tester.getTopLeft(find.text('Invite to community')).dy),
);
expect(find.byKey(const ValueKey('community-theme-row')), findsOneWidget);
expect(find.text('Appearance'), findsNothing);
expect(find.text('Style · This community'), findsNothing);
await tester.tap(find.byKey(const ValueKey('community-theme-row')));
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey('theme-preview-pages')), findsOneWidget);
expect(opened, ['status', 'name', 'description']);
});
testWidgets('uses the native glass close control on iOS', (tester) async {
@@ -445,7 +435,6 @@ void main() {
theme: AppTheme.light(),
home: SettingsPage(
profileHeader: const SizedBox.shrink(),
invitePageBuilder: (_) => const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
),
@@ -458,19 +447,20 @@ void main() {
(view) =>
(view.creationParams as Map<Object?, Object?>?)?['icon'] == 'close',
);
final nativeEdit = nativeViews.singleWhere(
(view) =>
(view.creationParams as Map<Object?, Object?>?)?['label'] == 'Edit',
);
expect(nativeClose.viewType, 'buzz/navigation_glass');
expect(nativeClose.creationParams, containsPair('icon', 'close'));
expect(nativeEdit.viewType, 'buzz/navigation_glass');
expect(nativeEdit.creationParams, containsPair('controlWidth', 56.0));
expect(
nativeViews.where(
(view) =>
(view.creationParams as Map<Object?, Object?>?)?['label'] == 'Edit',
),
isEmpty,
);
expect(find.byTooltip('Close settings'), findsOneWidget);
debugDefaultTargetPlatformOverride = null;
});
testWidgets('shows community invite navigation to owners and admins', (
testWidgets('keeps community controls out of personal settings', (
tester,
) async {
SharedPreferences.setMockInitialValues({});
@@ -488,7 +478,6 @@ void main() {
theme: AppTheme.light(),
home: SettingsPage(
profileHeader: const SizedBox.shrink(),
invitePageBuilder: (_) => const Text('Invite destination'),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
),
@@ -496,17 +485,14 @@ void main() {
);
await tester.pumpAndSettle();
expect(find.text('Invite to community'), findsOneWidget);
expect(find.text('Invite to community'), findsNothing);
expect(
find.text('Add people directly or share an invite link'),
findsNothing,
);
await tester.tap(find.text('Invite to community'));
await tester.pumpAndSettle();
expect(find.text('Invite destination'), findsOneWidget);
});
testWidgets('keeps invite navigation available when role lookup fails', (
testWidgets('keeps personal settings independent of community role errors', (
tester,
) async {
SharedPreferences.setMockInitialValues({});
@@ -527,7 +513,6 @@ void main() {
theme: AppTheme.light(),
home: SettingsPage(
profileHeader: const SizedBox.shrink(),
invitePageBuilder: (_) => const Text('Invite destination'),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
),
@@ -535,10 +520,7 @@ void main() {
);
await tester.pumpAndSettle();
expect(find.text('Invite to community'), findsOneWidget);
await tester.tap(find.text('Invite to community'));
await tester.pumpAndSettle();
expect(find.text('Invite destination'), findsOneWidget);
expect(find.text('Invite to community'), findsNothing);
});
testWidgets('hides community invite navigation from plain members', (
@@ -559,7 +541,6 @@ void main() {
theme: AppTheme.light(),
home: SettingsPage(
profileHeader: const SizedBox.shrink(),
invitePageBuilder: (_) => const Text('Invite destination'),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
),
@@ -591,7 +572,6 @@ void main() {
theme: AppTheme.light(),
home: SettingsPage(
profileHeader: const SizedBox(height: 100),
invitePageBuilder: (_) => const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
),