|
| 1 | +import 'package:checks/checks.dart'; |
| 2 | +import 'package:flutter/material.dart'; |
| 3 | +import 'package:flutter/rendering.dart'; |
| 4 | +import 'package:flutter_test/flutter_test.dart'; |
| 5 | +import 'package:zulip/api/model/events.dart'; |
| 6 | +import 'package:zulip/api/model/model.dart'; |
| 7 | +import 'package:zulip/model/narrow.dart'; |
| 8 | +import 'package:zulip/widgets/content.dart'; |
| 9 | +import 'package:zulip/widgets/icons.dart'; |
| 10 | +import 'package:zulip/widgets/recent_dm_conversations.dart'; |
| 11 | +import 'package:zulip/widgets/store.dart'; |
| 12 | + |
| 13 | +import '../example_data.dart' as eg; |
| 14 | +import '../flutter_checks.dart'; |
| 15 | +import '../model/binding.dart'; |
| 16 | +import '../model/test_store.dart'; |
| 17 | +import '../test_navigation.dart'; |
| 18 | +import 'content_checks.dart'; |
| 19 | + |
| 20 | +Future<void> setupPage(WidgetTester tester, { |
| 21 | + required List<DmMessage> dmMessages, |
| 22 | + required List<User> users, |
| 23 | + NavigatorObserver? navigatorObserver, |
| 24 | + String? newNameForSelfUser, |
| 25 | +}) async { |
| 26 | + addTearDown(TestZulipBinding.instance.reset); |
| 27 | + |
| 28 | + await TestZulipBinding.instance.globalStore.add(eg.selfAccount, eg.initialSnapshot()); |
| 29 | + final store = await TestZulipBinding.instance.globalStore.perAccount(eg.selfAccount.id); |
| 30 | + |
| 31 | + store.addUser(eg.selfUser); |
| 32 | + for (final user in users) { |
| 33 | + store.addUser(user); |
| 34 | + } |
| 35 | + |
| 36 | + for (final dmMessage in dmMessages) { |
| 37 | + store.handleEvent(MessageEvent(id: 1, message: dmMessage)); |
| 38 | + } |
| 39 | + |
| 40 | + if (newNameForSelfUser != null) { |
| 41 | + store.handleEvent(RealmUserUpdateEvent(id: 1, userId: eg.selfUser.userId, |
| 42 | + fullName: newNameForSelfUser)); |
| 43 | + } |
| 44 | + |
| 45 | + await tester.pumpWidget( |
| 46 | + GlobalStoreWidget( |
| 47 | + child: MaterialApp( |
| 48 | + navigatorObservers: navigatorObserver != null ? [navigatorObserver] : [], |
| 49 | + home: PerAccountStoreWidget( |
| 50 | + accountId: eg.selfAccount.id, |
| 51 | + child: const RecentDmConversationsPage())))); |
| 52 | + |
| 53 | + // global store, per-account store, and page get loaded |
| 54 | + await tester.pumpAndSettle(); |
| 55 | +} |
| 56 | + |
| 57 | +void main() { |
| 58 | + TestZulipBinding.ensureInitialized(); |
| 59 | + |
| 60 | + group('RecentDmConversationsPage', () { |
| 61 | + Finder findConversationItem(Narrow narrow) { |
| 62 | + return find.byWidgetPredicate( |
| 63 | + (widget) => |
| 64 | + widget is RecentDmConversationsItem && widget.narrow == narrow, |
| 65 | + ); |
| 66 | + } |
| 67 | + |
| 68 | + testWidgets('page builds; conversations appear in order', (WidgetTester tester) async { |
| 69 | + final user1 = eg.user(userId: 1); |
| 70 | + final user2 = eg.user(userId: 2); |
| 71 | + |
| 72 | + final message1 = eg.dmMessage(id: 1, from: eg.selfUser, to: [user1]); // 1:1 |
| 73 | + final message2 = eg.dmMessage(id: 2, from: eg.selfUser, to: []); // self-1:1 |
| 74 | + final message3 = eg.dmMessage(id: 3, from: eg.selfUser, to: [user1, user2]); // group |
| 75 | + |
| 76 | + await setupPage(tester, users: [user1, user2], |
| 77 | + dmMessages: [message1, message2, message3]); |
| 78 | + |
| 79 | + final items = tester.widgetList<RecentDmConversationsItem>(find.byType(RecentDmConversationsItem)).toList(); |
| 80 | + check(items).length.equals(3); |
| 81 | + check(items[0].narrow).equals(DmNarrow.ofMessage(message3, selfUserId: eg.selfUser.userId)); |
| 82 | + check(items[1].narrow).equals(DmNarrow.ofMessage(message2, selfUserId: eg.selfUser.userId)); |
| 83 | + check(items[2].narrow).equals(DmNarrow.ofMessage(message1, selfUserId: eg.selfUser.userId)); |
| 84 | + }); |
| 85 | + |
| 86 | + testWidgets('fling to scroll down', (WidgetTester tester) async { |
| 87 | + final List<User> users = []; |
| 88 | + final List<DmMessage> messages = []; |
| 89 | + for (int i = 0; i < 30; i++) { |
| 90 | + final user = eg.user(userId: i, fullName: 'User ${i.toString()}'); |
| 91 | + users.add(user); |
| 92 | + messages.add(eg.dmMessage(id: i, from: eg.selfUser, to: [user])); |
| 93 | + } |
| 94 | + |
| 95 | + await setupPage(tester, users: users, dmMessages: messages); |
| 96 | + |
| 97 | + final oldestConversationFinder = findConversationItem( |
| 98 | + DmNarrow.ofMessage(messages.first, selfUserId: eg.selfUser.userId)); |
| 99 | + |
| 100 | + check(tester.any(oldestConversationFinder)).isFalse(); // not onscreen |
| 101 | + await tester.fling(find.byType(RecentDmConversationsPage), |
| 102 | + const Offset(0, -200), 4000); |
| 103 | + await tester.pumpAndSettle(); |
| 104 | + check(tester.any(oldestConversationFinder)).isTrue(); // onscreen |
| 105 | + }); |
| 106 | + }); |
| 107 | + |
| 108 | + group('RecentDmConversationsItem', () { |
| 109 | + group('appearance', () { |
| 110 | + void checkAvatar(WidgetTester tester, DmNarrow narrow) { |
| 111 | + final shape = tester.widget<AvatarShape>( |
| 112 | + find.descendant( |
| 113 | + of: find.byType(RecentDmConversationsItem), |
| 114 | + matching: find.byType(AvatarShape), |
| 115 | + )); |
| 116 | + check(shape) |
| 117 | + ..size.equals(32) |
| 118 | + ..borderRadius.equals(3); |
| 119 | + |
| 120 | + switch (narrow.otherRecipientIds) { |
| 121 | + case []: // self-1:1 |
| 122 | + check(shape).child.isA<AvatarImage>().userId.equals(eg.selfUser.userId); |
| 123 | + case [var otherUserId]: // 1:1 |
| 124 | + check(shape).child.isA<AvatarImage>().userId.equals(otherUserId); |
| 125 | + default: // group |
| 126 | + // TODO(#232): syntax like `check(find(…), findsOneWidget)` |
| 127 | + tester.widget(find.descendant( |
| 128 | + of: find.byWidget(shape.child), |
| 129 | + matching: find.byIcon(ZulipIcons.group_dm), |
| 130 | + )); |
| 131 | + } |
| 132 | + } |
| 133 | + |
| 134 | + void checkTitle(WidgetTester tester, String expectedText, [int? expectedLines]) { |
| 135 | + // TODO(#232): syntax like `check(find(…), findsOneWidget)` |
| 136 | + final widget = tester.widget(find.descendant( |
| 137 | + of: find.byType(RecentDmConversationsItem), |
| 138 | + matching: find.text(expectedText), |
| 139 | + )); |
| 140 | + if (expectedLines != null) { |
| 141 | + final renderObject = tester.renderObject<RenderParagraph>(find.byWidget(widget)); |
| 142 | + check(renderObject.size.height).equals( |
| 143 | + 20.0 // line height |
| 144 | + * expectedLines); |
| 145 | + } |
| 146 | + } |
| 147 | + |
| 148 | + group('self-1:1', () { |
| 149 | + testWidgets('has right content', (WidgetTester tester) async { |
| 150 | + final message = eg.dmMessage(from: eg.selfUser, to: []); |
| 151 | + await setupPage(tester, users: [], dmMessages: [message]); |
| 152 | + |
| 153 | + checkAvatar(tester, DmNarrow.ofMessage(message, selfUserId: eg.selfUser.userId)); |
| 154 | + checkTitle(tester, eg.selfUser.fullName); |
| 155 | + }); |
| 156 | + |
| 157 | + testWidgets('short name takes one line', (WidgetTester tester) async { |
| 158 | + final message = eg.dmMessage(from: eg.selfUser, to: []); |
| 159 | + const name = 'Short name'; |
| 160 | + await setupPage(tester, users: [], dmMessages: [message], |
| 161 | + newNameForSelfUser: name); |
| 162 | + checkTitle(tester, name, 1); |
| 163 | + }); |
| 164 | + |
| 165 | + testWidgets('very long name takes two lines (must be ellipsized)', (WidgetTester tester) async { |
| 166 | + final message = eg.dmMessage(from: eg.selfUser, to: []); |
| 167 | + const name = 'Long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name'; |
| 168 | + await setupPage(tester, users: [], dmMessages: [message], |
| 169 | + newNameForSelfUser: name); |
| 170 | + checkTitle(tester, name, 2); |
| 171 | + }); |
| 172 | + }); |
| 173 | + |
| 174 | + group('1:1', () { |
| 175 | + testWidgets('has right content', (WidgetTester tester) async { |
| 176 | + final user = eg.user(userId: 1); |
| 177 | + final message = eg.dmMessage(from: eg.selfUser, to: [user]); |
| 178 | + await setupPage(tester, users: [user], dmMessages: [message]); |
| 179 | + |
| 180 | + checkAvatar(tester, DmNarrow.ofMessage(message, selfUserId: eg.selfUser.userId)); |
| 181 | + checkTitle(tester, user.fullName); |
| 182 | + }); |
| 183 | + |
| 184 | + testWidgets('no error when user somehow missing from store.users', (WidgetTester tester) async { |
| 185 | + final user = eg.user(userId: 1); |
| 186 | + final message = eg.dmMessage(from: eg.selfUser, to: [user]); |
| 187 | + await setupPage(tester, |
| 188 | + users: [], // exclude user |
| 189 | + dmMessages: [message], |
| 190 | + ); |
| 191 | + |
| 192 | + checkAvatar(tester, DmNarrow.ofMessage(message, selfUserId: eg.selfUser.userId)); |
| 193 | + checkTitle(tester, '(unknown user)'); |
| 194 | + }); |
| 195 | + |
| 196 | + testWidgets('short name takes one line', (WidgetTester tester) async { |
| 197 | + final user = eg.user(userId: 1, fullName: 'Short name'); |
| 198 | + final message = eg.dmMessage(from: eg.selfUser, to: [user]); |
| 199 | + await setupPage(tester, users: [user], dmMessages: [message]); |
| 200 | + checkTitle(tester, user.fullName, 1); |
| 201 | + }); |
| 202 | + |
| 203 | + testWidgets('very long name takes two lines (must be ellipsized)', (WidgetTester tester) async { |
| 204 | + final user = eg.user(userId: 1, fullName: 'Long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name long name'); |
| 205 | + final message = eg.dmMessage(from: eg.selfUser, to: [user]); |
| 206 | + await setupPage(tester, users: [user], dmMessages: [message]); |
| 207 | + checkTitle(tester, user.fullName, 2); |
| 208 | + }); |
| 209 | + }); |
| 210 | + |
| 211 | + group('group', () { |
| 212 | + List<User> usersList(int count) { |
| 213 | + final result = <User>[]; |
| 214 | + for (int i = 0; i < count; i++) { |
| 215 | + result.add(eg.user(userId: i, fullName: 'User ${i.toString()}')); |
| 216 | + } |
| 217 | + return result; |
| 218 | + } |
| 219 | + |
| 220 | + testWidgets('has right content', (WidgetTester tester) async { |
| 221 | + final users = usersList(2); |
| 222 | + final user0 = users[0]; |
| 223 | + final user1 = users[1]; |
| 224 | + final message = eg.dmMessage(from: eg.selfUser, to: [user0, user1]); |
| 225 | + await setupPage(tester, users: users, dmMessages: [message]); |
| 226 | + |
| 227 | + checkAvatar(tester, DmNarrow.ofMessage(message, selfUserId: eg.selfUser.userId)); |
| 228 | + checkTitle(tester, '${user0.fullName}, ${user1.fullName}'); |
| 229 | + }); |
| 230 | + |
| 231 | + testWidgets('no error when one user somehow missing from store.users', (WidgetTester tester) async { |
| 232 | + final users = usersList(2); |
| 233 | + final user0 = users[0]; |
| 234 | + final user1 = users[1]; |
| 235 | + final message = eg.dmMessage(from: eg.selfUser, to: [user0, user1]); |
| 236 | + await setupPage(tester, |
| 237 | + users: [user0], // exclude user1 |
| 238 | + dmMessages: [message], |
| 239 | + ); |
| 240 | + |
| 241 | + checkAvatar(tester, DmNarrow.ofMessage(message, selfUserId: eg.selfUser.userId)); |
| 242 | + checkTitle(tester, '${user0.fullName}, (unknown user)'); |
| 243 | + }); |
| 244 | + |
| 245 | + testWidgets('few names takes one line', (WidgetTester tester) async { |
| 246 | + final users = usersList(2); |
| 247 | + final message = eg.dmMessage(from: eg.selfUser, to: users); |
| 248 | + await setupPage(tester, users: users, dmMessages: [message]); |
| 249 | + checkTitle(tester, users.map((u) => u.fullName).join(', '), 1); |
| 250 | + }); |
| 251 | + |
| 252 | + testWidgets('very many names takes two lines (must be ellipsized)', (WidgetTester tester) async { |
| 253 | + final users = usersList(40); |
| 254 | + final message = eg.dmMessage(from: eg.selfUser, to: users); |
| 255 | + await setupPage(tester, users: users, dmMessages: [message]); |
| 256 | + checkTitle(tester, users.map((u) => u.fullName).join(', '), 2); |
| 257 | + }); |
| 258 | + }); |
| 259 | + }); |
| 260 | + |
| 261 | + group('on tap, navigates to message list', () { |
| 262 | + Future<void> runAndCheck(WidgetTester tester, { |
| 263 | + required DmMessage message, |
| 264 | + required List<User> users |
| 265 | + }) async { |
| 266 | + final expectedNarrow = DmNarrow.ofMessage(message, selfUserId: eg.selfUser.userId); |
| 267 | + final pushedRoutes = <Route<dynamic>>[]; |
| 268 | + final testNavObserver = TestNavigatorObserver() |
| 269 | + ..onPushed = (route, prevRoute) => pushedRoutes.add(route); |
| 270 | + |
| 271 | + await setupPage(tester, users: users, |
| 272 | + dmMessages: [message], |
| 273 | + navigatorObserver: testNavObserver); |
| 274 | + |
| 275 | + await tester.tap(find.byType(RecentDmConversationsItem)); |
| 276 | + // no `tester.pump`, to avoid having to mock API response for [MessageListPage] |
| 277 | + |
| 278 | + check(pushedRoutes).last.settings |
| 279 | + ..name.equals('message_list') |
| 280 | + ..arguments.equals(expectedNarrow); |
| 281 | + } |
| 282 | + |
| 283 | + testWidgets('1:1', (WidgetTester tester) async { |
| 284 | + final user = eg.user(userId: 1, fullName: 'User 1'); |
| 285 | + await runAndCheck(tester, users: [user], |
| 286 | + message: eg.dmMessage(id: 1, from: eg.selfUser, to: [user])); |
| 287 | + }); |
| 288 | + |
| 289 | + testWidgets('self-1:1', (WidgetTester tester) async { |
| 290 | + await runAndCheck(tester, users: [], |
| 291 | + message: eg.dmMessage(id: 1, from: eg.selfUser, to: [])); |
| 292 | + }); |
| 293 | + |
| 294 | + testWidgets('group', (WidgetTester tester) async { |
| 295 | + final user1 = eg.user(userId: 1, fullName: 'User 1'); |
| 296 | + final user2 = eg.user(userId: 2, fullName: 'User 2'); |
| 297 | + await runAndCheck(tester, users: [user1, user2], |
| 298 | + message: eg.dmMessage(id: 1, from: eg.selfUser, to: [user1, user2])); |
| 299 | + }); |
| 300 | + }); |
| 301 | + }); |
| 302 | +} |
0 commit comments