Skip to content

Commit

Permalink
autocomplete: Add user avatars to user-mention autocompletes
Browse files Browse the repository at this point in the history
Fixes: #227
  • Loading branch information
sm-sayedi committed Mar 25, 2024
1 parent dc09d43 commit a78d4e0
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 21 deletions.
13 changes: 10 additions & 3 deletions lib/widgets/autocomplete.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';

import 'content.dart';
import 'store.dart';
import '../model/autocomplete.dart';
import '../model/compose.dart';
Expand Down Expand Up @@ -119,19 +120,25 @@ class _ComposeAutocompleteState extends State<ComposeAutocomplete> 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: 28, borderRadius: 3);
label = PerAccountStoreWidget.of(context).users[userId]!.fullName;
}
return InkWell(
onTap: () {
_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
Expand Down
43 changes: 40 additions & 3 deletions test/widgets/autocomplete_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ 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';

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.
///
Expand All @@ -39,6 +41,8 @@ Future<Finder> setupToComposeInput(WidgetTester tester, {
messages: [message],
).toJson());

prepareBoringImageHttpClient();

await tester.pumpWidget(
MaterialApp(
localizationsDelegates: ZulipLocalizations.localizationsDelegates,
Expand All @@ -65,10 +69,18 @@ void main() {
TestZulipBinding.ensureInitialized();

group('ComposeAutocomplete', () {

Finder findNetworkImage(String url) {
return find.byWidgetPredicate((widget) =>
widget is Image &&
widget.image is NetworkImage &&
(widget.image as NetworkImage).url == url);
}

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);

Expand All @@ -77,34 +89,59 @@ 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();
final user1AvatarFinder =
findNetworkImage(store.tryResolveUrl('user1.png').toString());
check(tester.widgetList(user1AvatarFinder)).isEmpty();

tester.widget(find.text('User Two'));
final user2AvatarFinder =
findNetworkImage(store.tryResolveUrl('user2.png').toString());
tester.widget(user2AvatarFinder);

tester.widget(find.text('User Three'));
final user3AvatarFinder =
findNetworkImage(store.tryResolveUrl('user3.png').toString());
tester.widget(user3AvatarFinder);

// Finishing autocomplete updates compose box; causes options to disappear
await tester.tap(find.text('User Three'));
await tester.pump();
check(tester.widget<TextField>(composeInputFinder).controller!.text)
.contains(mention(user3, users: store.users));
check(tester.widgetList(find.text('User One'))).isEmpty();
check(tester.widgetList(user1AvatarFinder)).isEmpty();

check(tester.widgetList(find.text('User Two'))).isEmpty();
check(tester.widgetList(user2AvatarFinder)).isEmpty();

check(tester.widgetList(find.text('User Three'))).isEmpty();
check(tester.widgetList(user3AvatarFinder)).isEmpty();

// 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'));
tester.widget(user2AvatarFinder);

// 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(user1AvatarFinder)).isEmpty();

check(tester.widgetList(find.text('User Two'))).isEmpty();
check(tester.widgetList(user2AvatarFinder)).isEmpty();

check(tester.widgetList(find.text('User Three'))).isEmpty();
check(tester.widgetList(user3AvatarFinder)).isEmpty();

debugNetworkImageHttpClientProvider = null;
});
});
}
31 changes: 16 additions & 15 deletions test/widgets/content_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@ import 'dialog_checks.dart';
import 'message_list_checks.dart';
import 'page_checks.dart';


/// Set [debugNetworkImageHttpClientProvider] to return a constant image.
///
/// Returns the [FakeImageHttpClient] that handles the requests.
///
/// The caller must set [debugNetworkImageHttpClientProvider] back to null
/// before the end of the test.
FakeImageHttpClient prepareBoringImageHttpClient() {
final httpClient = FakeImageHttpClient();
debugNetworkImageHttpClientProvider = () => httpClient;
httpClient.request.response
..statusCode = HttpStatus.ok
..content = kSolidBlueAvatar;
return httpClient;
}

void main() {
// For testing a new content feature:
//
Expand Down Expand Up @@ -64,21 +80,6 @@ void main() {
});
}

/// Set [debugNetworkImageHttpClientProvider] to return a constant image.
///
/// Returns the [FakeImageHttpClient] that handles the requests.
///
/// The caller must set [debugNetworkImageHttpClientProvider] back to null
/// before the end of the test.
FakeImageHttpClient prepareBoringImageHttpClient() {
final httpClient = FakeImageHttpClient();
debugNetworkImageHttpClientProvider = () => httpClient;
httpClient.request.response
..statusCode = HttpStatus.ok
..content = kSolidBlueAvatar;
return httpClient;
}

group('Heading', () {
testWidgets('plain h6', (tester) async {
await prepareContentBare(tester,
Expand Down

0 comments on commit a78d4e0

Please sign in to comment.