From 71ae691eac7fc088c05ae876a7bcd56082bbf089 Mon Sep 17 00:00:00 2001 From: Vinod Singh Date: Tue, 4 Nov 2025 11:49:49 +0530 Subject: [PATCH 1/2] refactor(test): Use helper for finding elements in UserTile --- test/widgets/new_dm_sheet_test.dart | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/widgets/new_dm_sheet_test.dart b/test/widgets/new_dm_sheet_test.dart index b5cf9d8301..8dab80179e 100644 --- a/test/widgets/new_dm_sheet_test.dart +++ b/test/widgets/new_dm_sheet_test.dart @@ -254,19 +254,19 @@ void main() { }); group('user selection', () { - void checkUserSelected(WidgetTester tester, User user, bool expected) { - final icon = tester.widget(find.descendant( - of: findUserTile(user), - matching: find.byType(Icon))); - - if (expected) { - check(findUserChip(user)).findsOne(); - check(icon).icon.equals(ZulipIcons.check_circle_checked); - } else { - check(findUserChip(user)).findsNothing(); - check(icon).icon.equals(ZulipIcons.check_circle_unchecked); + Finder findInUserTile(User user, Finder finder) => find.descendant(of: findUserTile(user), matching: finder); + + void checkUserSelected(WidgetTester tester, User user, bool expected) { + if (expected) { + check(findUserChip(user)).findsOne(); + check(findInUserTile(user,find.byIcon(ZulipIcons.check_circle_checked))).findsOne(); + check(findInUserTile(user,find.byIcon(ZulipIcons.check_circle_unchecked))).findsNothing(); + } else { + check(findUserChip(user)).findsNothing(); + check(findInUserTile(user,find.byIcon(ZulipIcons.check_circle_unchecked))).findsOne(); + check(findInUserTile(user,find.byIcon(ZulipIcons.check_circle_checked))).findsNothing(); + } } - } testWidgets('tapping user chip deselects the user', (tester) async { await setupSheet(tester, users: [eg.otherUser, eg.thirdUser]); From 7171d7151bb52d55340b8f346c69088ca8f5be20 Mon Sep 17 00:00:00 2001 From: Vinod Singh Date: Tue, 4 Nov 2025 11:56:59 +0530 Subject: [PATCH 2/2] avatar: Show placeholder on image load error. Fixes #1558 --- lib/widgets/user.dart | 5 ++- test/test_images.dart | 15 +++++-- test/widgets/user_test.dart | 81 ++++++++++++++++++++++++++++++++++--- 3 files changed, 89 insertions(+), 12 deletions(-) diff --git a/lib/widgets/user.dart b/lib/widgets/user.dart index 358f0ff108..200519c376 100644 --- a/lib/widgets/user.dart +++ b/lib/widgets/user.dart @@ -66,7 +66,7 @@ class AvatarImage extends StatelessWidget { final user = store.getUser(userId); if (user == null) { // TODO(log) - return const SizedBox.shrink(); + return _AvatarPlaceholder(size: size); } if (replaceIfMuted && store.isUserMuted(userId)) { @@ -79,7 +79,7 @@ class AvatarImage extends StatelessWidget { }; if (resolvedUrl == null) { - return const SizedBox.shrink(); + return _AvatarPlaceholder(size: size); } final avatarUrl = AvatarUrl.fromUserData(resolvedUrl: resolvedUrl); @@ -89,6 +89,7 @@ class AvatarImage extends StatelessWidget { avatarUrl.get(physicalSize), filterQuality: FilterQuality.medium, fit: BoxFit.cover, + errorBuilder: (_, _, _) => _AvatarPlaceholder(size: size), ); } } diff --git a/test/test_images.dart b/test/test_images.dart index c7a04c264f..f07336739a 100644 --- a/test/test_images.dart +++ b/test/test_images.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:typed_data'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -12,12 +13,18 @@ import 'package:flutter_test/flutter_test.dart'; /// before the end of the test. // TODO(upstream) simplify callers by using addTearDown: https://github.com/flutter/flutter/issues/123189 // See also: https://github.com/flutter/flutter/issues/121917 -FakeImageHttpClient prepareBoringImageHttpClient() { +FakeImageHttpClient prepareBoringImageHttpClient({bool success = true}) { final httpClient = FakeImageHttpClient(); debugNetworkImageHttpClientProvider = () => httpClient; - httpClient.request.response - ..statusCode = HttpStatus.ok - ..content = kSolidBlueAvatar; + if (success) { + httpClient.request.response + ..statusCode = HttpStatus.ok + ..content = kSolidBlueAvatar; + } else { + httpClient.request.response + ..statusCode = HttpStatus.notFound + ..content = Uint8List(0); + } return httpClient; } diff --git a/test/widgets/user_test.dart b/test/widgets/user_test.dart index f84196a488..dc0cf17fb8 100644 --- a/test/widgets/user_test.dart +++ b/test/widgets/user_test.dart @@ -1,11 +1,9 @@ import 'package:checks/checks.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:zulip/model/store.dart'; import 'package:zulip/widgets/image.dart'; -import 'package:zulip/widgets/store.dart'; +import 'package:zulip/widgets/icons.dart'; import 'package:zulip/widgets/user.dart'; import '../example_data.dart' as eg; @@ -13,6 +11,7 @@ import '../model/binding.dart'; import '../model/test_store.dart'; import '../stdlib_checks.dart'; import '../test_images.dart'; +import 'test_app.dart'; void main() { TestZulipBinding.ensureInitialized(); @@ -28,9 +27,12 @@ void main() { await store.addUser(user); prepareBoringImageHttpClient(); - await tester.pumpWidget(GlobalStoreWidget( - child: PerAccountStoreWidget(accountId: eg.selfAccount.id, - child: AvatarImage(userId: user.userId, size: size ?? 30)))); + await tester.pumpWidget( + TestZulipApp( + accountId: eg.selfAccount.id, + child: AvatarImage(userId: user.userId, size: size ?? 30), + ) + ); await tester.pump(); await tester.pump(); tester.widget(find.byType(AvatarImage)); @@ -78,5 +80,72 @@ void main() { check(await actualUrl(tester, avatarUrl)).isNull(); debugNetworkImageHttpClientProvider = null; }); + + testWidgets('shows placeholder when image URL gives error', (WidgetTester tester) async { + addTearDown(testBinding.reset); + prepareBoringImageHttpClient(success: false); + await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); + final store = await testBinding.globalStore.perAccount(eg.selfAccount.id); + final badUser = eg.user(avatarUrl: 'https://zulip.com/avatarinvalid.png'); + await store.addUser(badUser); + await tester.pumpWidget( + TestZulipApp( + accountId: eg.selfAccount.id, + child: AvatarImage(userId: badUser.userId, size: 30))); + await tester.pumpAndSettle(); + check( + find.descendant( + of: find.byType(AvatarImage), + matching: find.byIcon(ZulipIcons.person), + ).evaluate().length + ).equals(1); + debugNetworkImageHttpClientProvider = null; + }); + + testWidgets('shows placeholder when user avatarUrl is null', (WidgetTester tester) async { + addTearDown(testBinding.reset); + await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); + final store = await testBinding.globalStore.perAccount(eg.selfAccount.id); + + final userWithNoUrl = eg.user(avatarUrl: null); + await store.addUser(userWithNoUrl); + + await tester.pumpWidget( + TestZulipApp( + accountId: eg.selfAccount.id, + child: AvatarImage(userId: userWithNoUrl.userId, size: 30), + ), + ); + await tester.pumpAndSettle(); + + check( + find.descendant( + of: find.byType(AvatarImage), + matching: find.byIcon(ZulipIcons.person), + ).evaluate().length + ).equals(1); + }); + + testWidgets('shows placeholder when user is not found', (WidgetTester tester) async { + addTearDown(testBinding.reset); + await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); + + const nonExistentUserId = 9999999; + + await tester.pumpWidget( + TestZulipApp( + accountId: eg.selfAccount.id, + child: AvatarImage(userId: nonExistentUserId, size: 30), + ), + ); + await tester.pumpAndSettle(); + + check( + find.descendant( + of: find.byType(AvatarImage), + matching: find.byIcon(ZulipIcons.person), + ).evaluate().length + ).equals(1); + }); }); }