mirror of
https://github.com/block/buzz.git
synced 2026-11-03 16:01:35 +00:00
Polish mobile onboarding and profile setup (#8438)
Originally stacked on #8375, now merged. Rebased onto main. Polishes the onboarding landing page, community-link entry, and recovery sheets. Profile setup uses a consistent sheet with a larger avatar, inline display name, and draft photo/emoji/animated editing; iOS uses native controls and coordinated avatar/sheet transitions. Validation: mobile analysis, full tests, and normal pre-push checks passed after rebase. Prior iOS build and device walkthrough passed; native validation was not repeated after this rebase. Snapshots omitted as requested. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> Co-authored-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
@@ -2863,7 +2863,8 @@ void main() {
|
||||
expect(find.byType(PairingPage), findsOneWidget);
|
||||
expect(find.byType(FallbackPairingQrScanner), findsNothing);
|
||||
expect(find.text('Scan a QR code'), findsOneWidget);
|
||||
expect(find.text('Use pairing code'), findsOneWidget);
|
||||
expect(find.text('Use pairing code'), findsNothing);
|
||||
expect(find.byKey(const Key('developer-pairing-trigger')), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
await tester.pump();
|
||||
|
||||
@@ -8,6 +8,8 @@ import 'package:buzz/shared/hosted/hosted_flow.dart';
|
||||
import 'package:buzz/shared/hosted/builderlab_account.dart';
|
||||
import 'package:buzz/shared/hosted/hosted_community_connection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@@ -18,6 +20,155 @@ void main() {
|
||||
testWidgets(
|
||||
'cancel sign-in and resolve batch conflicts without replacing saved identities',
|
||||
(tester) async {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
||||
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
||||
// Keep secure-storage operations in the same test zone.
|
||||
for (final reduced in [false, true]) {
|
||||
tester.view.devicePixelRatio = 1;
|
||||
tester.view.physicalSize = const Size(320, 640);
|
||||
|
||||
FlutterSecureStorage.setMockInitialValues({});
|
||||
PendingDeepLinkNotifier.debugUriStreamOverride =
|
||||
const Stream<Uri>.empty();
|
||||
final f = HostedCommunitiesFixture();
|
||||
await f.initialize(widget: true);
|
||||
f.expired = true;
|
||||
|
||||
await tester.pumpWidget(
|
||||
WidgetHelpers.testable(
|
||||
disableAnimations: reduced,
|
||||
child: const HostedAppGate(
|
||||
authenticated: false,
|
||||
child: OnboardingAppEntry(),
|
||||
),
|
||||
overrides: [hostedFlowProvider.overrideWithValue(f.flow)],
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Other options'));
|
||||
await tester.pumpAndSettle();
|
||||
tester.view.physicalSize = const Size(420, 900);
|
||||
await tester.pumpAndSettle();
|
||||
final linkCard = find.ancestor(
|
||||
of: find.text('Enter a community link').hitTestable(),
|
||||
matching: find.byType(InkWell),
|
||||
);
|
||||
expect(tester.getBottomRight(linkCard).dy, closeTo(876, 1));
|
||||
final scanCard = find.ancestor(
|
||||
of: find.text('Scan a QR code').hitTestable(),
|
||||
matching: find.byType(InkWell),
|
||||
);
|
||||
final subtitle = find
|
||||
.text('Scan a QR code or enter a community link to continue.')
|
||||
.hitTestable();
|
||||
expect(
|
||||
tester.getTopLeft(scanCard).dy - tester.getBottomLeft(subtitle).dy,
|
||||
closeTo(24, 1),
|
||||
);
|
||||
tester.view.physicalSize = const Size(320, 640);
|
||||
await tester.pumpAndSettle();
|
||||
final optionTop = tester
|
||||
.getTopLeft(find.text('Enter a community link').hitTestable())
|
||||
.dy;
|
||||
final scanTop = tester.getTopLeft(scanCard).dy;
|
||||
await tester.tap(find.text('Enter a community link').hitTestable());
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 30));
|
||||
if (!reduced) {
|
||||
// Keyboard setup must finish before any heading/surface movement.
|
||||
expect(
|
||||
tester
|
||||
.getTopLeft(find.byKey(const Key('connection-options-motion')))
|
||||
.dy,
|
||||
scanTop,
|
||||
);
|
||||
await tester.binding.defaultBinaryMessenger.handlePlatformMessage(
|
||||
'buzz/community_keyboard',
|
||||
const StandardMethodCodec().encodeMethodCall(
|
||||
const MethodCall('willShow'),
|
||||
),
|
||||
(_) {},
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 30));
|
||||
final scanLayer = find.byKey(const Key('connection-options-motion'));
|
||||
expect(tester.getTopLeft(scanLayer).dy, greaterThan(scanTop));
|
||||
final formOpacity = tester.widget<Opacity>(
|
||||
find
|
||||
.descendant(
|
||||
of: find.byKey(const Key('community-form-motion')),
|
||||
matching: find.byType(Opacity),
|
||||
)
|
||||
.first,
|
||||
);
|
||||
expect(formOpacity.opacity, greaterThan(0));
|
||||
final surface = find.byWidgetPredicate(
|
||||
(widget) => widget is LayoutId && widget.id == 'surface',
|
||||
);
|
||||
final action = find.byWidgetPredicate(
|
||||
(widget) => widget is LayoutId && widget.id == 'action',
|
||||
);
|
||||
expect(tester.getRect(surface), tester.getRect(action));
|
||||
}
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
await tester.binding.handlePopRoute();
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.testTextInput.isVisible, isFalse);
|
||||
expect(find.text('Scan a QR code').hitTestable(), findsOneWidget);
|
||||
await tester.tap(find.text('Enter a community link').hitTestable());
|
||||
await tester.pump();
|
||||
// A hardware keyboard produces no software-keyboard notification.
|
||||
await tester.pump(const Duration(milliseconds: 350));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
tester
|
||||
.getTopLeft(find.text('Enter a community link').hitTestable())
|
||||
.dy,
|
||||
lessThan(optionTop - 100),
|
||||
);
|
||||
expect(tester.testTextInput.isVisible, isTrue);
|
||||
tester.view.viewInsets = const FakeViewPadding(bottom: 300);
|
||||
await tester.pumpAndSettle();
|
||||
final field = find.byKey(const Key('community-link-input'));
|
||||
await tester.ensureVisible(field);
|
||||
expect(field.hitTestable(), findsOneWidget);
|
||||
expect(
|
||||
tester
|
||||
.widget<EditableText>(
|
||||
find.descendant(of: field, matching: find.byType(EditableText)),
|
||||
)
|
||||
.focusNode
|
||||
.hasFocus,
|
||||
isTrue,
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
tester.view.viewInsets = const FakeViewPadding();
|
||||
await tester.binding.handlePopRoute();
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.testTextInput.isVisible, isFalse);
|
||||
expect(find.text('Use pairing code'), findsNothing);
|
||||
final developerTrigger = find
|
||||
.byKey(const Key('developer-pairing-trigger'))
|
||||
.hitTestable();
|
||||
await tester.tap(developerTrigger);
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
await tester.tap(developerTrigger);
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find
|
||||
.text('Paste the pairing code from your desktop app.')
|
||||
.hitTestable(),
|
||||
findsOneWidget,
|
||||
);
|
||||
await tester.tap(find.text('Back').hitTestable());
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Scan a QR code').hitTestable(), findsOneWidget);
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
await tester.pumpAndSettle();
|
||||
await f.dispose();
|
||||
tester.view.reset();
|
||||
}
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
await tester.binding.setSurfaceSize(const Size(440, 956));
|
||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||
// Keep the process-wide secure-store queue in one widget-test zone.
|
||||
@@ -28,6 +179,7 @@ void main() {
|
||||
final f = HostedCommunitiesFixture();
|
||||
await f.initialize(widget: true);
|
||||
f.rows = [f.row('one'), f.row('two')];
|
||||
f.expired = true;
|
||||
addTearDown(() async {
|
||||
PendingDeepLinkNotifier.debugUriStreamOverride = null;
|
||||
await f.dispose();
|
||||
@@ -43,17 +195,61 @@ void main() {
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.text('Sign in with builderlab').hitTestable(),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('Enter a community link').hitTestable(), findsNothing);
|
||||
expect(find.text('Scan a QR code'), findsNothing);
|
||||
expect(find.text('Use pairing code'), findsNothing);
|
||||
await tester.tap(find.text('Other options'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.text('Enter a community link').hitTestable(),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('Scan a QR code').hitTestable(), findsOneWidget);
|
||||
expect(find.text('Use pairing code').hitTestable(), findsNothing);
|
||||
await tester.tap(find.byType(BackButton));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Enter a community link').hitTestable(), findsNothing);
|
||||
await tester.tap(find.text('Other options'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.binding.handlePopRoute();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Other options').hitTestable(), findsOneWidget);
|
||||
expect(find.text('Enter a community link').hitTestable(), findsNothing);
|
||||
await tester.tap(find.text('Sign in with builderlab'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
expect(f.browser.startedIntent, {'type': 'account'});
|
||||
expect(
|
||||
find.bySemanticsLabel('Cancel sign-in').hitTestable(),
|
||||
findsOneWidget,
|
||||
);
|
||||
await tester.tap(find.bySemanticsLabel('Cancel sign-in').hitTestable());
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.text('Sign in with builderlab').hitTestable(),
|
||||
findsOneWidget,
|
||||
);
|
||||
f.expired = false;
|
||||
await f.flow.signInAccount(openBrowser: false);
|
||||
await tester.pumpAndSettle();
|
||||
f.expired = true;
|
||||
await tester.tap(find.text('Continue with Builderlab'));
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
expect(f.flow.stage, HostedFlowStage.signIn);
|
||||
expect(f.browser.startedIntent, {'type': 'account'});
|
||||
expect(find.byType(HostedConnectPage), findsNothing);
|
||||
expect(find.text('Cancel sign-in').hitTestable(), findsOneWidget);
|
||||
expect(find.text('Enter a community link'), findsOneWidget);
|
||||
expect(
|
||||
find.bySemanticsLabel('Cancel sign-in').hitTestable(),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('Enter a community link').hitTestable(), findsNothing);
|
||||
f.expired = false;
|
||||
await tester.tap(find.text('Cancel sign-in').hitTestable());
|
||||
await tester.tap(find.bySemanticsLabel('Cancel sign-in').hitTestable());
|
||||
await tester.pumpAndSettle();
|
||||
expect(f.flow.stage, HostedFlowStage.idle);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@@ -4,7 +4,12 @@ import 'package:buzz/shared/deeplink/pending_deep_link_provider.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:buzz/features/onboarding/hosted_profile_setup.dart';
|
||||
import 'package:buzz/features/profile/profile_setup_page.dart';
|
||||
import 'package:buzz/features/profile/profile_setup_avatar_editor.dart';
|
||||
import 'package:buzz/features/profile/profile_avatar_draft.dart';
|
||||
import 'package:buzz/shared/emoji/emoji_avatar.dart';
|
||||
import 'package:buzz/shared/widgets/playing_avatar_image.dart';
|
||||
import 'package:buzz/features/pairing/pairing_page.dart';
|
||||
import 'package:buzz/onboarding_app_entry.dart';
|
||||
import 'package:buzz/shared/auth/auth.dart';
|
||||
import 'package:buzz/shared/hosted/builderlab_account.dart';
|
||||
@@ -52,6 +57,73 @@ void main() {
|
||||
);
|
||||
}
|
||||
|
||||
journey('system Back leaves community creation', (tester, f) async {
|
||||
await f.initialize();
|
||||
await f.mount(tester);
|
||||
await f.tap(tester, 'Create a community');
|
||||
expect(
|
||||
tester.widget<PairingPage>(find.byType(PairingPage)).creatingCommunity,
|
||||
isTrue,
|
||||
);
|
||||
await tester.binding.handlePopRoute();
|
||||
await f.settle(tester);
|
||||
expect(
|
||||
tester.widget<PairingPage>(find.byType(PairingPage)).creatingCommunity,
|
||||
isFalse,
|
||||
);
|
||||
expect(find.text('Create a community').hitTestable(), findsOneWidget);
|
||||
});
|
||||
|
||||
journey('retiring profile setup removes both profile and avatar routes', (
|
||||
tester,
|
||||
f,
|
||||
) async {
|
||||
await f.initialize();
|
||||
f.rows.add(f.row('one'));
|
||||
await f.mount(tester);
|
||||
await f.tap(tester, 'Continue with Builderlab');
|
||||
await tester.tap(find.byTooltip('Add profile photo'));
|
||||
await f.settle(tester);
|
||||
expect(find.byType(ProfileSetupAvatarEditor), findsOneWidget);
|
||||
await f.link(tester, 'two');
|
||||
expect(
|
||||
find.byType(ProfileSetupAvatarEditor, skipOffstage: false),
|
||||
findsNothing,
|
||||
);
|
||||
expect(find.text('Set up your profile', skipOffstage: false), findsNothing);
|
||||
expect(f.profileWrites, isEmpty);
|
||||
});
|
||||
|
||||
journey('fallback profile previews the accepted draft before saving', (
|
||||
tester,
|
||||
f,
|
||||
) async {
|
||||
await f.initialize();
|
||||
f.rows.add(f.row('one'));
|
||||
await f.mount(tester);
|
||||
await f.tap(tester, 'Continue with Builderlab');
|
||||
await tester.tap(find.byTooltip('Add profile photo'));
|
||||
await f.settle(tester);
|
||||
final url = emojiAvatarDataUrl('🐝', 0xFFFFE75C);
|
||||
Navigator.of(
|
||||
tester.element(find.byType(ProfileSetupAvatarEditor)),
|
||||
).pop(ProfileUrlAvatarDraft(url));
|
||||
await f.settle(tester);
|
||||
expect(
|
||||
tester
|
||||
.widget<PlayingAvatarImage>(find.byType(PlayingAvatarImage))
|
||||
.imageUrl,
|
||||
url,
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byType(TextField).hitTestable(),
|
||||
'Draft Person',
|
||||
);
|
||||
await f.tap(tester, 'Continue');
|
||||
expect(f.profileWrites, ['Draft Person']);
|
||||
expect(f.profileAvatars, [url]);
|
||||
});
|
||||
|
||||
journey(
|
||||
'QR credentials enter without an account when domain is not required',
|
||||
(tester, f) async {
|
||||
@@ -71,7 +143,7 @@ void main() {
|
||||
expect(find.text('Create a community'), findsNothing);
|
||||
expect(find.text('Enter a community link'), findsOneWidget);
|
||||
expect(find.text('Scan a QR code'), findsOneWidget);
|
||||
expect(find.text('Use pairing code'), findsOneWidget);
|
||||
expect(find.text('Use pairing code'), findsNothing);
|
||||
await f.tap(tester, 'Enter a community link');
|
||||
expect(
|
||||
find.byKey(const Key('community-link-input')).hitTestable(),
|
||||
@@ -384,7 +456,17 @@ void main() {
|
||||
expect((await f.storage.loadAll()).single.starterSetupIncomplete, isTrue);
|
||||
expect(find.text('Try again').hitTestable(), findsOneWidget);
|
||||
await f.tap(tester, 'Try again');
|
||||
expect(find.byType(HostedProfileSetup), findsOneWidget);
|
||||
expect(
|
||||
find.byType(ProfileSetupPage, skipOffstage: false),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.ancestor(
|
||||
of: find.byType(TextField).hitTestable(),
|
||||
matching: find.byType(BottomSheet),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(f.sent('/claim'), hasLength(1));
|
||||
expect(f.recoveryScopes, hasLength(3));
|
||||
expect(
|
||||
@@ -477,17 +559,20 @@ void main() {
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('Scan a QR code'), findsOneWidget);
|
||||
expect(find.text('Use pairing code'), findsOneWidget);
|
||||
expect(find.text('Use pairing code'), findsNothing);
|
||||
f.rows.addAll([f.row('one'), f.row('two')]);
|
||||
await f.tap(tester, 'Continue with Builderlab');
|
||||
// Joined communities are added automatically after choosing the identity.
|
||||
expect(find.byType(CheckboxListTile).hitTestable(), findsNothing);
|
||||
expect(find.byType(HostedProfileSetup), findsOneWidget);
|
||||
expect(
|
||||
find.byType(ProfileSetupPage, skipOffstage: false),
|
||||
findsOneWidget,
|
||||
);
|
||||
final firstSaved = (await f.storage.loadAll()).firstWhere(
|
||||
(c) => c.name == 'one',
|
||||
);
|
||||
Navigator.of(
|
||||
tester.element(find.byType(HostedProfileSetup)),
|
||||
tester.element(find.byType(ProfileSetupPage, skipOffstage: false)),
|
||||
rootNavigator: true,
|
||||
).pop();
|
||||
await f.settle(tester);
|
||||
@@ -528,7 +613,10 @@ void main() {
|
||||
await f.tap(tester, 'Continue with Builderlab');
|
||||
// One discovered community is admitted automatically.
|
||||
expect(f.profileWrites, isEmpty);
|
||||
expect(find.byType(HostedProfileSetup), findsNothing);
|
||||
expect(
|
||||
find.byType(ProfileSetupPage, skipOffstage: false),
|
||||
findsNothing,
|
||||
);
|
||||
final saved = (await f.storage.loadAll()).single;
|
||||
expect(saved.nsec, f.keys.nsec);
|
||||
expect(saved.pubkey, f.keys.public);
|
||||
@@ -728,7 +816,10 @@ void main() {
|
||||
expect(await f.storage.loadAll(), isEmpty);
|
||||
await f.restart(tester);
|
||||
await f.tap(tester, 'Start login again');
|
||||
expect(find.byType(HostedProfileSetup), findsOneWidget);
|
||||
expect(
|
||||
find.byType(ProfileSetupPage, skipOffstage: false),
|
||||
findsOneWidget,
|
||||
);
|
||||
await tester.enterText(find.byType(TextField).hitTestable(), 'Creator');
|
||||
await f.tap(tester, 'Continue');
|
||||
final saved = (await f.storage.loadAll()).single;
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import 'package:buzz/features/onboarding/self_hosted_entry_sheet.dart';
|
||||
import 'package:buzz/shared/hosted/device_identity_source.dart';
|
||||
import 'package:buzz/shared/hosted/self_hosted_entry.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../helpers/widget_helpers.dart';
|
||||
|
||||
class _AvailableEntry extends SelfHostedEntry {
|
||||
@override
|
||||
Future<({List<DeviceIdentityChoice> choices, bool complete})>
|
||||
choices() async => (choices: const <DeviceIdentityChoice>[], complete: true);
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets(
|
||||
'native presentation failure opens the usable Flutter join sheet',
|
||||
(tester) async {
|
||||
FlutterSecureStorage.setMockInitialValues({});
|
||||
const channel = MethodChannel('buzz/community_entry');
|
||||
final messenger = tester.binding.defaultBinaryMessenger;
|
||||
var presentations = 0;
|
||||
messenger.setMockMethodCallHandler(channel, (_) async {
|
||||
presentations++;
|
||||
throw PlatformException(code: 'presentation_failed');
|
||||
});
|
||||
addTearDown(() => messenger.setMockMethodCallHandler(channel, null));
|
||||
await tester.pumpWidget(
|
||||
WidgetHelpers.testable(
|
||||
overrides: [
|
||||
selfHostedEntryProvider.overrideWithValue(_AvailableEntry()),
|
||||
],
|
||||
child: Builder(
|
||||
builder: (context) => TextButton(
|
||||
onPressed: () => showSelfHostedEntry(
|
||||
context,
|
||||
relay: 'wss://example.com',
|
||||
name: 'Example',
|
||||
),
|
||||
child: const Text('Open join'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.text('Open join'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(presentations, 1);
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(find.byType(SelfHostedEntrySheet), findsOneWidget);
|
||||
expect(find.text('Import a Nostr key').hitTestable(), findsOneWidget);
|
||||
Navigator.of(tester.element(find.byType(SelfHostedEntrySheet))).pop();
|
||||
await tester.pumpAndSettle();
|
||||
},
|
||||
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import 'package:buzz/hosted_app_gate.dart';
|
||||
import 'package:buzz/shared/hosted/hosted_flow_provider.dart';
|
||||
import 'package:buzz/onboarding_app_entry.dart';
|
||||
import 'package:buzz/shared/auth/auth.dart';
|
||||
import 'package:buzz/shared/identity_transfer/nsec_import_sheet.dart';
|
||||
import 'package:buzz/shared/deeplink/pending_deep_link_provider.dart';
|
||||
import 'package:buzz/shared/hosted/builderlab_account.dart';
|
||||
import 'package:buzz/shared/hosted/hosted_flow.dart';
|
||||
@@ -46,6 +47,7 @@ void main() {
|
||||
});
|
||||
var probes = 0;
|
||||
final inventoryStorage = _EnumerationStorage();
|
||||
Completer<void>? pendingProbe;
|
||||
await tester.pumpWidget(
|
||||
WidgetHelpers.testable(
|
||||
disableAnimations: true,
|
||||
@@ -57,6 +59,7 @@ void main() {
|
||||
accountStore: AccountIdentityStore(storage: inventoryStorage),
|
||||
probe: (_) async {
|
||||
probes++;
|
||||
await pendingProbe?.future;
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -92,7 +95,12 @@ void main() {
|
||||
}
|
||||
|
||||
Future<void> pair() async {
|
||||
await tester.tap(find.text('Use pairing code').hitTestable());
|
||||
final trigger = find
|
||||
.byKey(const Key('developer-pairing-trigger'))
|
||||
.hitTestable();
|
||||
await tester.tap(trigger);
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
await tester.tap(trigger);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(
|
||||
find.byType(TextField).hitTestable(),
|
||||
@@ -225,9 +233,68 @@ void main() {
|
||||
await tester.tap(find.text('Continue').hitTestable());
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Import a Nostr key').hitTestable(), findsOneWidget);
|
||||
await tester.tap(find.text('Previous device identity').hitTestable());
|
||||
final joinRoute = ModalRoute.of(
|
||||
tester.element(find.byType(SelfHostedEntrySheet)),
|
||||
);
|
||||
final joinHeight = tester.getSize(find.byType(SelfHostedEntrySheet)).height;
|
||||
await tester.tap(find.text('Import a Nostr key').hitTestable());
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(BuzzSheetHeader), findsOneWidget);
|
||||
expect(find.text('Import identity'), findsOneWidget);
|
||||
expect(
|
||||
ModalRoute.of(tester.element(find.byType(NsecImportSheet))),
|
||||
same(joinRoute),
|
||||
);
|
||||
expect(
|
||||
tester.getSize(find.byType(SelfHostedEntrySheet)).height,
|
||||
greaterThan(joinHeight),
|
||||
);
|
||||
await tester.tap(find.byTooltip('Close sheet'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(SelfHostedEntrySheet), findsOneWidget);
|
||||
expect(find.byType(NsecImportSheet), findsNothing);
|
||||
expect(find.text('Join community'), findsOneWidget);
|
||||
expect(
|
||||
tester.getSize(find.byType(SelfHostedEntrySheet)).height,
|
||||
joinHeight,
|
||||
);
|
||||
await tester.tap(find.text('Import a Nostr key').hitTestable());
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(
|
||||
find.descendant(
|
||||
of: find.byType(NsecImportSheet),
|
||||
matching: find.byType(TextField),
|
||||
),
|
||||
f.keys.nsec,
|
||||
);
|
||||
await tester.tap(find.text('Preview identity'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(probes, 0);
|
||||
pendingProbe = Completer<void>();
|
||||
await tester.tap(find.text('Import this identity'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(probes, 1);
|
||||
await tester.tap(find.byTooltip('Close sheet'));
|
||||
await tester.pumpAndSettle();
|
||||
pendingProbe.complete();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Join community'), findsOneWidget);
|
||||
expect(await container.read(communityStorageProvider).loadAll(), isEmpty);
|
||||
pendingProbe = null;
|
||||
await tester.tap(find.text('Import a Nostr key').hitTestable());
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(
|
||||
find.descendant(
|
||||
of: find.byType(NsecImportSheet),
|
||||
matching: find.byType(TextField),
|
||||
),
|
||||
f.keys.nsec,
|
||||
);
|
||||
await tester.tap(find.text('Preview identity'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Import this identity'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(probes, 2);
|
||||
expect(find.byType(SelfHostedEntrySheet), findsNothing);
|
||||
expect(find.byType(OnboardingAppEntry), findsNothing);
|
||||
expect(find.text('Add community').hitTestable(), findsOneWidget);
|
||||
@@ -251,7 +318,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(OnboardingAppEntry), findsNothing);
|
||||
expect(find.text('Add community').hitTestable(), findsOneWidget);
|
||||
expect(probes, 1);
|
||||
expect(probes, 2);
|
||||
expect(
|
||||
await container.read(communityStorageProvider).loadActiveId(),
|
||||
saved.single.id,
|
||||
|
||||
@@ -689,8 +689,8 @@ void main() {
|
||||
expect(confirmTopLeft.dx, Grid.sm);
|
||||
expect(tester.getSize(confirmFinder).width, scaffoldWidth - Grid.sm * 2);
|
||||
expect(tester.getSize(cancelFinder).width, scaffoldWidth - Grid.sm * 2);
|
||||
expect(tester.getSize(confirmFinder).height, 48);
|
||||
expect(tester.getSize(cancelFinder).height, 48);
|
||||
expect(tester.getSize(confirmFinder).height, 52);
|
||||
expect(tester.getSize(cancelFinder).height, 52);
|
||||
expect(
|
||||
find.textContaining(
|
||||
'Only continue if you started this pairing from your desktop.',
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import 'package:buzz/features/profile/profile_setup_avatar_editor.dart';
|
||||
import 'package:buzz/features/profile/profile_avatar_draft.dart';
|
||||
import 'package:buzz/shared/emoji/emoji_avatar.dart';
|
||||
import 'dart:typed_data';
|
||||
import 'package:buzz/features/profile/profile_avatar_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets(
|
||||
'accepted animated draft can reopen and finish without recapture',
|
||||
(tester) async {
|
||||
final draft = ProfileAnimatedAvatarDraft(
|
||||
animation: Uint8List(0),
|
||||
poster: Uint8List(0),
|
||||
);
|
||||
ProfileAvatarDraft? accepted;
|
||||
Rect? nativePreview;
|
||||
Future<void> open(ProfileAvatarDraft? initial) async {
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
child: MaterialApp(
|
||||
home: ProfileSetupAvatarEditor(
|
||||
key: UniqueKey(),
|
||||
initialDraft: initial,
|
||||
onPreviewReady: (rect) async {
|
||||
nativePreview = rect;
|
||||
},
|
||||
onFinished: (value) => accepted = value,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
await open(null);
|
||||
tester
|
||||
.widget<ProfileAvatarEditor>(find.byType(ProfileAvatarEditor))
|
||||
.onModeChanged(ProfileAvatarMode.animated);
|
||||
await tester.pump();
|
||||
tester
|
||||
.widget<ProfileAvatarEditor>(find.byType(ProfileAvatarEditor))
|
||||
.onAnimatedPrepareChanged
|
||||
.call(() async => draft);
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Done'));
|
||||
await tester.pump();
|
||||
expect(accepted, same(draft));
|
||||
nativePreview = null;
|
||||
await open(accepted);
|
||||
expect(nativePreview, isNotNull);
|
||||
expect(nativePreview!.width, greaterThan(0));
|
||||
accepted = null;
|
||||
expect(find.byTooltip('Back to profile'), findsOneWidget);
|
||||
await tester.tap(find.text('Done'));
|
||||
await tester.pump();
|
||||
expect(accepted, same(draft));
|
||||
tester
|
||||
.widget<ProfileAvatarEditor>(find.byType(ProfileAvatarEditor))
|
||||
.onModeChanged(ProfileAvatarMode.image);
|
||||
await tester.pump();
|
||||
expect(
|
||||
tester
|
||||
.widget<TextButton>(find.widgetWithText(TextButton, 'Done'))
|
||||
.onPressed,
|
||||
isNull,
|
||||
);
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('avatar draft editor lays out within a full height sheet', (
|
||||
tester,
|
||||
) async {
|
||||
Rect? previewRect;
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
child: MaterialApp(
|
||||
home: ProfileSetupAvatarEditor(
|
||||
onPreviewReady: (rect) async {
|
||||
previewRect = rect;
|
||||
},
|
||||
initialDraft: ProfileUrlAvatarDraft(
|
||||
emojiAvatarDataUrl('🐝', 0xFFFFE75C),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
expect(find.text('Done'), findsOneWidget);
|
||||
expect(previewRect, isNotNull);
|
||||
expect(previewRect!.width, greaterThan(0));
|
||||
expect(previewRect!.height, greaterThan(0));
|
||||
expect(tester.takeException(), isNull);
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:buzz/features/profile/profile_setup_page.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../helpers/hosted_communities_fixture.dart';
|
||||
import '../../helpers/widget_helpers.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('native profile saves 100 graphemes and rejects 101', (
|
||||
tester,
|
||||
) async {
|
||||
FlutterSecureStorage.setMockInitialValues({});
|
||||
final fixture = HostedCommunitiesFixture();
|
||||
await fixture.initialize(widget: true);
|
||||
addTearDown(fixture.dispose);
|
||||
for (final grapheme in ['😀', 'e\u0301', '👨👩👧👦']) {
|
||||
for (final count in [100, 101]) {
|
||||
const channel = MethodChannel('buzz/profile_setup');
|
||||
final messenger = tester.binding.defaultBinaryMessenger;
|
||||
final presented = Completer<Map<dynamic, dynamic>>();
|
||||
final dismissed = Completer<String>();
|
||||
messenger.setMockMethodCallHandler(channel, (call) async {
|
||||
if (call.method == 'present') {
|
||||
presented.complete(call.arguments as Map);
|
||||
return dismissed.future;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
addTearDown(() => messenger.setMockMethodCallHandler(channel, null));
|
||||
final writes = <String>[];
|
||||
final pending = fixture.flow.requestProfile(
|
||||
(name) async => writes.add(name),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
WidgetHelpers.testable(child: ProfileSetupPage(flow: fixture.flow)),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
final configuration = await presented.future;
|
||||
final name = List.filled(count, grapheme).join();
|
||||
final response = Completer<Map<dynamic, dynamic>>();
|
||||
messenger.handlePlatformMessage(
|
||||
channel.name,
|
||||
const StandardMethodCodec().encodeMethodCall(
|
||||
MethodCall('save', {
|
||||
'session': configuration['session'],
|
||||
'name': name,
|
||||
}),
|
||||
),
|
||||
(data) => response.complete(
|
||||
const StandardMethodCodec().decodeEnvelope(data!) as Map,
|
||||
),
|
||||
);
|
||||
expect(await response.future, {'saved': count == 100});
|
||||
expect(writes, count == 100 ? [name] : isEmpty);
|
||||
dismissed.complete(count == 100 ? 'saved' : 'skip');
|
||||
await tester.pumpAndSettle();
|
||||
await pending;
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
}
|
||||
}, variant: TargetPlatformVariant.only(TargetPlatform.iOS));
|
||||
}
|
||||
Reference in New Issue
Block a user