mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
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:
@@ -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')),
|
||||
),
|
||||
|
||||
@@ -101,7 +101,6 @@ void main() {
|
||||
theme: AppTheme.light(),
|
||||
home: SettingsPage(
|
||||
profileHeader: const SizedBox.shrink(),
|
||||
invitePageBuilder: (_) => const SizedBox.shrink(),
|
||||
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user