Files
buzz/mobile/test/features/settings/connection_section_test.dart
klopez4212 cfcaef4e9f Update mobile toast animations (#8855)
Updates mobile toast animations and styling.



| Sign in | Retry | Copied |
| --- | --- | --- |
| ![Reconnect to
Builderlab](https://github.com/user-attachments/assets/5298b507-df5b-49da-a021-03f337e997fa)
| ![Save error with
Retry](https://github.com/user-attachments/assets/492c8929-4f91-4342-943b-a72b96547992)
| ![Copied
confirmation](https://github.com/user-attachments/assets/864fd629-7414-415e-81ef-5f7d3d5333a4)
|

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-31 07:50:00 -07:00

420 lines
14 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'package:buzz/features/pairing/pairing_provider.dart';
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';
import 'package:buzz/shared/theme/buzz_icons.dart';
import 'package:nostr/nostr.dart' as nostr;
import 'package:shared_preferences/shared_preferences.dart';
import '../../helpers/widget_helpers.dart';
void main() {
testWidgets('shows a compact copyable identity row', (tester) async {
var hapticCount = 0;
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
const MethodChannel('buzz/haptics'),
(call) async {
expect(call.method, 'success');
hapticCount++;
return null;
},
);
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;
if (call.method == 'HapticFeedback.vibrate') {
fail('Success must use native notification feedback on iOS');
}
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)),
),
savedPrefsProvider.overrideWithValue(prefs),
],
child: SettingsPage(
profileHeader: const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
),
);
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';
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,
);
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(BuzzIcons.copy));
final chevron = tester.getRect(find.byIcon(BuzzIcons.chevronRight).first);
expect(copy.center.dx, closeTo(chevron.center.dx, 0.5));
await tester.tap(find.text('Copy public key (npub)'));
await tester.pump();
expect(hapticCount, 1);
expect(clipboardCall?.method, 'Clipboard.setData');
expect(clipboardCall?.arguments, {'text': expectedNpub});
expect(find.text('Public key copied'), findsOneWidget);
}, variant: TargetPlatformVariant.only(TargetPlatform.iOS));
testWidgets('an out-of-range identity shows as unavailable', (tester) async {
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
await tester.pumpWidget(
WidgetHelpers.testable(
overrides: [
relayConfigProvider.overrideWith(_OutOfRangeRelayConfigNotifier.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(),
),
),
);
await tester.pump();
await tester.ensureVisible(find.text('Copy public key (npub)'));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
expect(
find.ancestor(
of: find.text('Copy public key (npub)'),
matching: find.byWidgetPredicate(
(widget) =>
widget is Semantics &&
widget.properties.value == 'Identity unavailable',
),
),
findsOneWidget,
);
});
testWidgets('waits for a resumed frame before navigating after auth', (
tester,
) async {
final authorization = Completer<bool>();
final pairing = _PairingNotifier(authorization.future);
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
await tester.pumpWidget(
WidgetHelpers.testable(
overrides: [
relayConfigProvider.overrideWith(_RelayConfigNotifier.new),
authProvider.overrideWith(_AuthNotifier.new),
pairingProvider.overrideWith(() => pairing),
savedPrefsProvider.overrideWithValue(prefs),
],
child: SettingsPage(
profileHeader: const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) =>
const Scaffold(body: Text('Identity recovery')),
),
),
);
await tester.pump();
await tester.tap(find.text('Send identity to desktop'));
await tester.pump();
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive);
authorization.complete(true);
await tester.pump();
expect(find.text('Identity recovery'), findsNothing);
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed);
await tester.pump();
await tester.pump();
await tester.pumpAndSettle();
expect(find.text('Identity recovery'), findsOneWidget);
});
testWidgets('resume timeout keeps identity recovery closed', (tester) async {
final pairing = _PairingNotifier(Future<bool>.value(true));
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
await tester.pumpWidget(
WidgetHelpers.testable(
overrides: [
relayConfigProvider.overrideWith(_RelayConfigNotifier.new),
authProvider.overrideWith(_AuthNotifier.new),
pairingProvider.overrideWith(() => pairing),
savedPrefsProvider.overrideWithValue(prefs),
],
child: SettingsPage(
profileHeader: const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) =>
const Scaffold(body: Text('Identity recovery')),
),
),
);
await tester.pump();
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive);
await tester.tap(find.text('Send identity to desktop'));
await tester.pump();
await tester.pump(const Duration(seconds: 5));
await tester.pump();
expect(pairing.resetCalls, 1);
expect(find.text('Identity recovery'), findsNothing);
expect(find.text('Couldn’t resume Buzz. Try again.'), findsOneWidget);
});
testWidgets('clears authorization when disposed during authentication', (
tester,
) async {
final authorization = Completer<bool>();
final pairing = _PairingNotifier(authorization.future);
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
await tester.pumpWidget(
WidgetHelpers.testable(
overrides: [
relayConfigProvider.overrideWith(_RelayConfigNotifier.new),
authProvider.overrideWith(_AuthNotifier.new),
pairingProvider.overrideWith(() => pairing),
savedPrefsProvider.overrideWithValue(prefs),
],
child: SettingsPage(
profileHeader: const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) =>
const Scaffold(body: Text('Identity recovery')),
),
),
);
await tester.pump();
final settingsContext = tester.element(
find.text('Send identity to desktop'),
);
await tester.tap(find.text('Send identity to desktop'));
await tester.pump();
unawaited(
Navigator.of(settingsContext).pushReplacement(
MaterialPageRoute<void>(builder: (_) => const SizedBox.shrink()),
),
);
await tester.pumpAndSettle();
authorization.complete(true);
await tester.pump();
expect(pairing.resetCalls, 1);
expect(find.text('Identity recovery'), findsNothing);
});
testWidgets('clears authorization when disposed during resume wait', (
tester,
) async {
final pairing = _PairingNotifier(Future<bool>.value(true));
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
await tester.pumpWidget(
WidgetHelpers.testable(
overrides: [
relayConfigProvider.overrideWith(_RelayConfigNotifier.new),
authProvider.overrideWith(_AuthNotifier.new),
pairingProvider.overrideWith(() => pairing),
savedPrefsProvider.overrideWithValue(prefs),
],
child: SettingsPage(
profileHeader: const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) =>
const Scaffold(body: Text('Identity recovery')),
),
),
);
await tester.pump();
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive);
final settingsContext = tester.element(
find.text('Send identity to desktop'),
);
await tester.tap(find.text('Send identity to desktop'));
await tester.pump();
unawaited(
Navigator.of(settingsContext).pushReplacement(
MaterialPageRoute<void>(builder: (_) => const SizedBox.shrink()),
),
);
await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 5));
await tester.pump();
expect(pairing.resetCalls, 1);
expect(find.text('Identity recovery'), findsNothing);
});
testWidgets('denied authentication does not open identity recovery', (
tester,
) async {
final pairing = _PairingNotifier(Future<bool>.value(false));
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
await tester.pumpWidget(
WidgetHelpers.testable(
overrides: [
relayConfigProvider.overrideWith(_RelayConfigNotifier.new),
authProvider.overrideWith(_AuthNotifier.new),
pairingProvider.overrideWith(() => pairing),
savedPrefsProvider.overrideWithValue(prefs),
],
child: SettingsPage(
profileHeader: const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) =>
const Scaffold(body: Text('Identity recovery')),
),
),
);
await tester.pump();
await tester.tap(find.text('Send identity to desktop'));
await tester.pumpAndSettle();
expect(pairing.authorizationCalls, 1);
expect(find.text('Identity recovery'), findsNothing);
});
}
class _AuthNotifier extends AuthNotifier {
@override
Future<AuthState> build() async => AuthState(
status: AuthStatus.authenticated,
community: Community(
id: 'community',
name: 'Test',
relayUrl: 'https://relay.test',
nsec: _RelayConfigNotifier.nsec,
addedAt: DateTime.utc(2026),
),
);
}
class _RelayConfigNotifier extends RelayConfigNotifier {
static final nsec = nostr.Keys(
'1111111111111111111111111111111111111111111111111111111111111111',
).nsec;
@override
RelayConfig build() => RelayConfig(baseUrl: 'https://relay.test', nsec: nsec);
}
class _OutOfRangeRelayConfigNotifier extends RelayConfigNotifier {
// Valid bech32 and 64 hex chars, but not below the secp256k1 order.
static final nsec = nostr.Nip19.encode(
prefix: nostr.Nip19Prefix.nsec,
data: 'f' * 64,
);
@override
RelayConfig build() => RelayConfig(baseUrl: 'https://relay.test', nsec: nsec);
}
class _PairingNotifier extends PairingNotifier {
_PairingNotifier(this.authorization);
final Future<bool> authorization;
int authorizationCalls = 0;
int resetCalls = 0;
@override
PairingState build() => const PairingState();
@override
Future<bool> authorizeIdentityExport({required Community community}) {
authorizationCalls++;
return authorization;
}
@override
void reset() {
resetCalls++;
}
}