From 370a9d236315cb0102c61ee514f5874e28bc5ab1 Mon Sep 17 00:00:00 2001 From: Vinod Singh Date: Sun, 11 Jan 2026 14:19:47 +0530 Subject: [PATCH 1/2] new-dm test: Use helper for finding elements in UserTile. --- test/widgets/new_dm_sheet_test.dart | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/test/widgets/new_dm_sheet_test.dart b/test/widgets/new_dm_sheet_test.dart index b5cf9d8301..825f5beae6 100644 --- a/test/widgets/new_dm_sheet_test.dart +++ b/test/widgets/new_dm_sheet_test.dart @@ -254,17 +254,24 @@ 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))); + 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(icon).icon.equals(ZulipIcons.check_circle_checked); + 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(icon).icon.equals(ZulipIcons.check_circle_unchecked); + check(findInUserTile(user, find.byIcon(ZulipIcons.check_circle_unchecked))) + .findsOne(); + check(findInUserTile(user, find.byIcon(ZulipIcons.check_circle_checked))) + .findsNothing(); } } From 896c8b393f80461f239fc46c5583ae28e326e61f Mon Sep 17 00:00:00 2001 From: Vinod Singh Date: Sun, 11 Jan 2026 14:28:33 +0530 Subject: [PATCH 2/2] avatar: Show placeholder on image load error. Previously, AvatarImage showed a blank space when the user was null or the image URL failed to load. This commit adds a placeholder icon in all such cases and updates the tests to verify this new behavior. Fixes #1558. Co-authored-by: Chris Bobbe --- lib/widgets/user.dart | 5 ++- test/widgets/user_test.dart | 74 ++++++++++++++++++++++++++++++++++--- 2 files changed, 71 insertions(+), 8 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/widgets/user_test.dart b/test/widgets/user_test.dart index f84196a488..01f4919833 100644 --- a/test/widgets/user_test.dart +++ b/test/widgets/user_test.dart @@ -1,11 +1,12 @@ +import 'dart:io'; + import 'package:checks/checks.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; +import 'package:flutter_checks/flutter_checks.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:zulip/model/store.dart'; +import 'package:zulip/widgets/icons.dart'; import 'package:zulip/widgets/image.dart'; -import 'package:zulip/widgets/store.dart'; import 'package:zulip/widgets/user.dart'; import '../example_data.dart' as eg; @@ -13,6 +14,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(); @@ -20,6 +22,11 @@ void main() { group('AvatarImage', () { late PerAccountStore store; + final findPlaceholder = find.descendant( + of: find.byType(AvatarImage), + matching: find.byIcon(ZulipIcons.person), + ); + Future actualUrl(WidgetTester tester, String avatarUrl, [double? size]) async { addTearDown(testBinding.reset); await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); @@ -28,9 +35,9 @@ 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 +85,60 @@ void main() { check(await actualUrl(tester, avatarUrl)).isNull(); debugNetworkImageHttpClientProvider = null; }); + + testWidgets('shows placeholder when user is not found', (tester) async { + addTearDown(testBinding.reset); + await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); + final store = await testBinding.globalStore.perAccount(eg.selfAccount.id); + + const nonExistentUserId = 9999999; + check(store.getUser(nonExistentUserId)).isNull(); + + await tester.pumpWidget( + TestZulipApp(accountId: eg.selfAccount.id, + child: AvatarImage(userId: nonExistentUserId, size: 30))); + await tester.pump(); + check(findPlaceholder).findsOne(); + }); + + testWidgets('shows placeholder when user avatarUrl is null', (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.pump(); + check(findPlaceholder).findsOne(); + }); + + testWidgets('shows placeholder when image fails to load', (tester) async { + final httpClient = FakeImageHttpClient(); + debugNetworkImageHttpClientProvider = () => httpClient; + httpClient.request.response + ..statusCode = HttpStatus.notFound + ..content = []; + + addTearDown(testBinding.reset); + 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.pump(); + await tester.pump(); + check(findPlaceholder).findsOne(); + + debugNetworkImageHttpClientProvider = null; + }); + }); }