diff --git a/lib/widgets/autocomplete.dart b/lib/widgets/autocomplete.dart index ba5dea1b84b..ece46a326f0 100644 --- a/lib/widgets/autocomplete.dart +++ b/lib/widgets/autocomplete.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'content.dart'; import 'store.dart'; import '../model/autocomplete.dart'; import '../model/compose.dart'; @@ -119,10 +120,11 @@ class _ComposeAutocompleteState extends State with PerAccou Widget _buildItem(BuildContext _, int index) { final option = _resultsToDisplay[index]; + Widget avatar; String label; switch (option) { case UserMentionAutocompleteResult(:var userId): - // TODO(#227) avatar + avatar = Avatar(userId: userId, size: 32, borderRadius: 3); label = PerAccountStoreWidget.of(context).users[userId]!.fullName; } return InkWell( @@ -130,8 +132,13 @@ class _ComposeAutocompleteState extends State with PerAccou _onTapOption(option); }, child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text(label))); + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Row( + children: [ + avatar, + const SizedBox(width: 8), + Text(label), + ]))); } @override diff --git a/test/widgets/autocomplete_test.dart b/test/widgets/autocomplete_test.dart index 6f540111ca9..98d879fa551 100644 --- a/test/widgets/autocomplete_test.dart +++ b/test/widgets/autocomplete_test.dart @@ -6,6 +6,7 @@ import 'package:zulip/api/model/model.dart'; import 'package:zulip/api/route/messages.dart'; import 'package:zulip/model/compose.dart'; import 'package:zulip/model/narrow.dart'; +import 'package:zulip/model/store.dart'; import 'package:zulip/widgets/message_list.dart'; import 'package:zulip/widgets/store.dart'; @@ -13,11 +14,17 @@ import '../api/fake_api.dart'; import '../example_data.dart' as eg; import '../model/binding.dart'; import '../model/test_store.dart'; +import 'content_test.dart'; /// Simulates loading a [MessageListPage] and tapping to focus the compose input. /// /// Also adds [users] to the [PerAccountStore], /// so they can show up in autocomplete. +/// +/// Also sets [debugNetworkImageHttpClientProvider] to return a constant image. +/// +/// The caller must set [debugNetworkImageHttpClientProvider] back to null +/// before the end of the test. Future setupToComposeInput(WidgetTester tester, { required List users, }) async { @@ -39,6 +46,8 @@ Future setupToComposeInput(WidgetTester tester, { messages: [message], ).toJson()); + prepareBoringImageHttpClient(); + await tester.pumpWidget( MaterialApp( localizationsDelegates: ZulipLocalizations.localizationsDelegates, @@ -65,10 +74,25 @@ void main() { TestZulipBinding.ensureInitialized(); group('ComposeAutocomplete', () { + + Finder findNetworkImage(String url) { + return find.byWidgetPredicate((widget) => switch(widget) { + Image(image: NetworkImage(url: var imageUrl)) when imageUrl == url => true, + _ => false, + }); + } + + void checkUserShown(User user, PerAccountStore store, {required bool expected}) { + check(find.text(user.fullName).evaluate().length).equals(expected ? 1 : 0); + final avatarFinder = + findNetworkImage(store.tryResolveUrl(user.avatarUrl!).toString()); + check(avatarFinder.evaluate().length).equals(expected ? 1 : 0); + } + testWidgets('options appear, disappear, and change correctly', (WidgetTester tester) async { - final user1 = eg.user(userId: 1, fullName: 'User One'); - final user2 = eg.user(userId: 2, fullName: 'User Two'); - final user3 = eg.user(userId: 3, fullName: 'User Three'); + final user1 = eg.user(userId: 1, fullName: 'User One', avatarUrl: 'user1.png'); + final user2 = eg.user(userId: 2, fullName: 'User Two', avatarUrl: 'user2.png'); + final user3 = eg.user(userId: 3, fullName: 'User Three', avatarUrl: 'user3.png'); final composeInputFinder = await setupToComposeInput(tester, users: [user1, user2, user3]); final store = await testBinding.globalStore.perAccount(eg.selfAccount.id); @@ -77,34 +101,37 @@ void main() { await tester.enterText(composeInputFinder, 'hello @user '); await tester.enterText(composeInputFinder, 'hello @user t'); await tester.pumpAndSettle(); // async computation; options appear + // "User Two" and "User Three" appear, but not "User One" - check(tester.widgetList(find.text('User One'))).isEmpty(); - tester.widget(find.text('User Two')); - tester.widget(find.text('User Three')); + checkUserShown(user1, store, expected: false); + checkUserShown(user2, store, expected: true); + checkUserShown(user3, store, expected: true); // Finishing autocomplete updates compose box; causes options to disappear await tester.tap(find.text('User Three')); await tester.pump(); check(tester.widget(composeInputFinder).controller!.text) .contains(mention(user3, users: store.users)); - check(tester.widgetList(find.text('User One'))).isEmpty(); - check(tester.widgetList(find.text('User Two'))).isEmpty(); - check(tester.widgetList(find.text('User Three'))).isEmpty(); + checkUserShown(user1, store, expected: false); + checkUserShown(user2, store, expected: false); + checkUserShown(user3, store, expected: false); // Then a new autocomplete intent brings up options again // TODO(#226): Remove this extra edit when this bug is fixed. await tester.enterText(composeInputFinder, 'hello @user tw'); await tester.enterText(composeInputFinder, 'hello @user two'); await tester.pumpAndSettle(); // async computation; options appear - tester.widget(find.text('User Two')); + checkUserShown(user2, store, expected: true); // Removing autocomplete intent causes options to disappear // TODO(#226): Remove one of these edits when this bug is fixed. await tester.enterText(composeInputFinder, ''); await tester.enterText(composeInputFinder, ' '); - check(tester.widgetList(find.text('User One'))).isEmpty(); - check(tester.widgetList(find.text('User Two'))).isEmpty(); - check(tester.widgetList(find.text('User Three'))).isEmpty(); + checkUserShown(user1, store, expected: false); + checkUserShown(user2, store, expected: false); + checkUserShown(user3, store, expected: false); + + debugNetworkImageHttpClientProvider = null; }); }); }