Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 67b3854

Browse files
committedSep 4, 2023
profile: Implement profile screen for users
Added profile screen with user information and custom profile fields, linked from sender name and avatar in message list. User presence (#196) and user status (#197) are not yet displayed or tracked here. Fixes: #195
1 parent 2cb3e7b commit 67b3854

File tree

5 files changed

+549
-5
lines changed

5 files changed

+549
-5
lines changed
 

‎lib/widgets/message_list.dart

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import 'compose_box.dart';
1414
import 'content.dart';
1515
import 'icons.dart';
1616
import 'page.dart';
17+
import 'profile.dart';
1718
import 'sticky_header.dart';
1819
import 'store.dart';
1920

@@ -580,14 +581,22 @@ class MessageWithSender extends StatelessWidget {
580581
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
581582
Padding(
582583
padding: const EdgeInsets.fromLTRB(3, 6, 11, 0),
583-
child: Avatar(userId: message.senderId, size: 35, borderRadius: 4)),
584+
child: GestureDetector(
585+
onTap: () => Navigator.push(context,
586+
ProfilePage.buildRoute(context: context,
587+
userId: message.senderId)),
588+
child: Avatar(userId: message.senderId, size: 35, borderRadius: 4))),
584589
Expanded(
585590
child: Column(
586591
crossAxisAlignment: CrossAxisAlignment.stretch,
587592
children: [
588593
const SizedBox(height: 3),
589-
Text(message.senderFullName, // TODO get from user data
590-
style: const TextStyle(fontWeight: FontWeight.bold)),
594+
GestureDetector(
595+
onTap: () => Navigator.push(context,
596+
ProfilePage.buildRoute(context: context,
597+
userId: message.senderId)),
598+
child: Text(message.senderFullName, // TODO get from user data
599+
style: const TextStyle(fontWeight: FontWeight.bold))),
591600
const SizedBox(height: 4),
592601
MessageContent(message: message, content: content),
593602
])),

‎lib/widgets/profile.dart

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
import 'dart:convert';
2+
3+
import 'package:flutter/material.dart';
4+
5+
import '../api/model/model.dart';
6+
import '../model/content.dart';
7+
import '../model/narrow.dart';
8+
import 'content.dart';
9+
import 'message_list.dart';
10+
import 'page.dart';
11+
import 'store.dart';
12+
13+
class _TextStyles {
14+
static const profileText = TextStyle(fontSize: 20);
15+
static const customProfileFieldLabel = TextStyle(fontSize: 15, fontWeight: FontWeight.bold);
16+
static const customProfileFieldText = TextStyle(fontSize: 15);
17+
}
18+
19+
class ProfilePage extends StatelessWidget {
20+
const ProfilePage({super.key, required this.userId});
21+
22+
final int userId;
23+
24+
static Route<void> buildRoute({required BuildContext context, required int userId}) {
25+
return MaterialAccountWidgetRoute(context: context,
26+
page: ProfilePage(userId: userId));
27+
}
28+
29+
@override
30+
Widget build(BuildContext context) {
31+
final store = PerAccountStoreWidget.of(context);
32+
final user = store.users[userId];
33+
if (user == null) {
34+
return const _ProfileErrorPage();
35+
}
36+
37+
final items = [
38+
Center(
39+
child: Avatar(userId: userId, size: 200, borderRadius: 200 / 8)),
40+
const SizedBox(height: 16),
41+
Text(user.fullName,
42+
textAlign: TextAlign.center,
43+
style: _TextStyles.profileText.merge(const TextStyle(fontWeight: FontWeight.bold))),
44+
// TODO(#291) render email field
45+
Text(roleToLabel(user.role),
46+
textAlign: TextAlign.center,
47+
style: _TextStyles.profileText),
48+
// TODO(#197) render user status
49+
// TODO(#196) render active status
50+
// TODO(#292) render user local time
51+
52+
_ProfileDataTable(profileData: user.profileData),
53+
const SizedBox(height: 16),
54+
FilledButton.icon(
55+
onPressed: () => Navigator.push(context,
56+
MessageListPage.buildRoute(context: context,
57+
narrow: DmNarrow.withUser(userId, selfUserId: store.account.userId))),
58+
icon: const Icon(Icons.email),
59+
label: const Text("Send direct message")),
60+
];
61+
62+
return Scaffold(
63+
appBar: AppBar(title: Text(user.fullName)),
64+
body: SingleChildScrollView(
65+
child: Center(
66+
child: ConstrainedBox(
67+
constraints: const BoxConstraints(maxWidth: 760),
68+
child: Padding(
69+
padding: const EdgeInsets.all(16),
70+
child: Column(
71+
crossAxisAlignment: CrossAxisAlignment.stretch,
72+
children: items))))));
73+
}
74+
}
75+
76+
class _ProfileErrorPage extends StatelessWidget {
77+
const _ProfileErrorPage();
78+
79+
@override
80+
Widget build(BuildContext context) {
81+
return Scaffold(
82+
appBar: AppBar(title: const Text("Error")),
83+
body: const SingleChildScrollView(
84+
child: Padding(
85+
padding: EdgeInsets.fromLTRB(16, 32, 16, 16),
86+
child: Row(
87+
mainAxisAlignment: MainAxisAlignment.center,
88+
children: [
89+
Icon(Icons.error),
90+
Text("Could not show user profile."),
91+
]))));
92+
}
93+
}
94+
95+
String roleToLabel(UserRole role) {
96+
return switch (role) {
97+
UserRole.owner => "Owner",
98+
UserRole.administrator => "Administrator",
99+
UserRole.moderator => "Moderator",
100+
UserRole.member => "Member",
101+
UserRole.guest => "Guest",
102+
UserRole.unknown => "Unknown",
103+
};
104+
}
105+
106+
class _ProfileDataTable extends StatelessWidget {
107+
const _ProfileDataTable({required this.profileData});
108+
109+
final Map<int, ProfileFieldUserData>? profileData;
110+
111+
static T? _safeDecode<T>(T Function(Map<String, dynamic>) fromJson, String data) {
112+
try {
113+
return fromJson(jsonDecode(data));
114+
} on FormatException {
115+
return null;
116+
} on TypeError {
117+
return null;
118+
}
119+
}
120+
121+
Widget? _buildCustomProfileFieldValue(BuildContext context, String value, CustomProfileField realmField) {
122+
final store = PerAccountStoreWidget.of(context);
123+
124+
switch (realmField.type) {
125+
case CustomProfileFieldType.link:
126+
return _LinkWidget(url: value, text: value);
127+
128+
case CustomProfileFieldType.choice:
129+
final choiceFieldData = _safeDecode(CustomProfileFieldChoiceDataItem.parseChoices, realmField.fieldData);
130+
if (choiceFieldData == null) return null;
131+
final choiceItem = choiceFieldData[value];
132+
return (choiceItem == null) ? null : _TextWidget(text: choiceItem.text);
133+
134+
case CustomProfileFieldType.externalAccount:
135+
final externalAccountFieldData = _safeDecode(CustomProfileFieldExternalAccountData.fromJson, realmField.fieldData);
136+
if (externalAccountFieldData == null) return null;
137+
final urlPattern = externalAccountFieldData.urlPattern ??
138+
store.realmDefaultExternalAccounts[externalAccountFieldData.subtype]?.urlPattern;
139+
if (urlPattern == null) return null;
140+
final url = urlPattern.replaceFirst("%(username)s", value);
141+
return _LinkWidget(url: url, text: value);
142+
143+
case CustomProfileFieldType.user:
144+
// TODO(server): This is completely undocumented. The key to
145+
// reverse-engineering it was:
146+
// https://github.com/zulip/zulip/blob/18230fcd9/static/js/settings_account.js#L247
147+
final List<int> userIds;
148+
try {
149+
userIds = (jsonDecode(value) as List<dynamic>).map((e) => e as int).toList();
150+
} on FormatException {
151+
return null;
152+
} on TypeError {
153+
return null;
154+
}
155+
return Column(
156+
children: userIds.map((userId) => _UserWidget(userId: userId)).toList());
157+
158+
case CustomProfileFieldType.date:
159+
// TODO(server): The value's format is undocumented, but empirically
160+
// it's a date in ISO format, like 2000-01-01.
161+
// That's readable as is, but:
162+
// TODO format this date using user's locale.
163+
return _TextWidget(text: value);
164+
165+
case CustomProfileFieldType.shortText:
166+
case CustomProfileFieldType.longText:
167+
case CustomProfileFieldType.pronouns:
168+
// The web client appears to treat `longText` identically to `shortText`;
169+
// `pronouns` is explicitly meant to display the same as `shortText`.
170+
return _TextWidget(text: value);
171+
172+
case CustomProfileFieldType.unknown:
173+
return null;
174+
}
175+
}
176+
177+
@override
178+
Widget build(BuildContext context) {
179+
final store = PerAccountStoreWidget.of(context);
180+
if (profileData == null) return const SizedBox.shrink();
181+
182+
List<Widget> items = [const SizedBox(height: 16)];
183+
184+
for (final realmField in store.customProfileFields) {
185+
final profileField = profileData![realmField.id];
186+
if (profileField == null) continue;
187+
final widget = _buildCustomProfileFieldValue(context, profileField.value, realmField);
188+
if (widget == null) continue;
189+
190+
items.add(Row(
191+
crossAxisAlignment: CrossAxisAlignment.baseline,
192+
textBaseline: TextBaseline.alphabetic,
193+
children: [
194+
SizedBox(width: 96,
195+
child: Text(realmField.name, style: _TextStyles.customProfileFieldLabel)),
196+
const SizedBox(width: 8),
197+
Expanded(child: widget),
198+
]));
199+
items.add(const SizedBox(height: 8));
200+
}
201+
202+
return Column(children: items);
203+
}
204+
}
205+
206+
class _LinkWidget extends StatelessWidget {
207+
const _LinkWidget({required this.url, required this.text});
208+
209+
final String url;
210+
final String text;
211+
212+
@override
213+
Widget build(BuildContext context) {
214+
final linkNode = LinkNode(url: url, nodes: [TextNode(text)]);
215+
final paragraph = Paragraph(node: ParagraphNode(nodes: [linkNode], links: [linkNode]));
216+
return Padding(
217+
padding: const EdgeInsets.symmetric(horizontal: 4),
218+
child: MouseRegion(
219+
cursor: SystemMouseCursors.click,
220+
child: paragraph));
221+
}
222+
}
223+
224+
class _TextWidget extends StatelessWidget {
225+
const _TextWidget({required this.text});
226+
227+
final String text;
228+
229+
@override
230+
Widget build(BuildContext context) {
231+
return Padding(
232+
padding: const EdgeInsets.all(4),
233+
child: Text(text, style: _TextStyles.customProfileFieldText));
234+
}
235+
}
236+
237+
class _UserWidget extends StatelessWidget {
238+
const _UserWidget({required this.userId});
239+
240+
final int userId;
241+
242+
@override
243+
Widget build(BuildContext context) {
244+
final store = PerAccountStoreWidget.of(context);
245+
final user = store.users[userId];
246+
final fullName = user?.fullName ?? "(unknown user)";
247+
return InkWell(
248+
onTap: () => Navigator.push(context,
249+
ProfilePage.buildRoute(context: context,
250+
userId: userId)),
251+
child: Padding(
252+
padding: const EdgeInsets.all(4),
253+
child: Row(children: [
254+
Avatar(userId: userId, size: 32, borderRadius: 32 / 8),
255+
const SizedBox(width: 8),
256+
Expanded(child: Text(fullName, style: _TextStyles.customProfileFieldText)), // TODO(#196) render active status
257+
])));
258+
}
259+
}

‎test/example_data.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ User user({
1515
String? email,
1616
String? fullName,
1717
String? avatarUrl,
18+
Map<int, ProfileFieldUserData>? profileData,
1819
}) {
1920
return User(
2021
userId: userId ?? 123, // TODO generate example IDs
@@ -32,7 +33,7 @@ User user({
3233
timezone: 'UTC',
3334
avatarUrl: avatarUrl,
3435
avatarVersion: 0,
35-
profileData: null,
36+
profileData: profileData,
3637
);
3738
}
3839

@@ -265,8 +266,8 @@ InitialSnapshot initialSnapshot({
265266
subscriptions: subscriptions ?? [], // TODO add subscriptions to default
266267
unreadMsgs: unreadMsgs ?? _unreadMsgs(),
267268
streams: streams ?? [], // TODO add streams to default
268-
realmDefaultExternalAccounts: realmDefaultExternalAccounts ?? {},
269269
userSettings: userSettings, // TODO add userSettings to default
270+
realmDefaultExternalAccounts: realmDefaultExternalAccounts ?? {},
270271
maxFileUploadSizeMib: maxFileUploadSizeMib ?? 25,
271272
realmUsers: realmUsers ?? [],
272273
realmNonActiveUsers: realmNonActiveUsers ?? [],

‎test/widgets/profile_page_checks.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import 'package:checks/checks.dart';
2+
import 'package:zulip/widgets/profile.dart';
3+
4+
extension ProfilePageChecks on Subject<ProfilePage> {
5+
Subject<int> get userId => has((x) => x.userId, 'userId');
6+
}

‎test/widgets/profile_test.dart

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
import 'package:checks/checks.dart';
2+
import 'package:flutter/foundation.dart';
3+
import 'package:flutter/material.dart';
4+
import 'package:flutter_test/flutter_test.dart';
5+
import 'package:url_launcher/url_launcher.dart';
6+
import 'package:zulip/api/model/initial_snapshot.dart';
7+
import 'package:zulip/api/model/model.dart';
8+
import 'package:zulip/model/narrow.dart';
9+
import 'package:zulip/widgets/content.dart';
10+
import 'package:zulip/widgets/message_list.dart';
11+
import 'package:zulip/widgets/page.dart';
12+
import 'package:zulip/widgets/profile.dart';
13+
import 'package:zulip/widgets/store.dart';
14+
15+
import '../example_data.dart' as eg;
16+
import '../model/binding.dart';
17+
import '../model/test_store.dart';
18+
import '../test_navigation.dart';
19+
import 'message_list_checks.dart';
20+
import 'page_checks.dart';
21+
import 'profile_page_checks.dart';
22+
23+
Future<void> setupPage(WidgetTester tester, {
24+
required int pageUserId,
25+
List<User>? users,
26+
List<CustomProfileField>? customProfileFields,
27+
Map<String, RealmDefaultExternalAccount>? realmDefaultExternalAccounts,
28+
NavigatorObserver? navigatorObserver
29+
}) async {
30+
addTearDown(testBinding.reset);
31+
32+
final initialSnapshot = eg.initialSnapshot(
33+
customProfileFields: customProfileFields,
34+
realmDefaultExternalAccounts: realmDefaultExternalAccounts);
35+
await testBinding.globalStore.add(eg.selfAccount, initialSnapshot);
36+
final store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
37+
38+
store.addUser(eg.selfUser);
39+
if (users != null) {
40+
store.addUsers(users);
41+
}
42+
43+
await tester.pumpWidget(
44+
GlobalStoreWidget(
45+
child: MaterialApp(
46+
navigatorObservers: navigatorObserver != null ? [navigatorObserver] : [],
47+
home: PerAccountStoreWidget(
48+
accountId: eg.selfAccount.id,
49+
child: ProfilePage(userId: pageUserId)))));
50+
51+
// global store, per-account store, and page get loaded
52+
await tester.pumpAndSettle();
53+
}
54+
55+
CustomProfileField mkCustomProfileField(int id, CustomProfileFieldType type, {int? order, bool? displayInProfileSummary, String? fieldData}) {
56+
return CustomProfileField(
57+
id: id,
58+
order: order ?? id,
59+
name: "field$id",
60+
hint: "hint$id",
61+
displayInProfileSummary: displayInProfileSummary ?? true,
62+
fieldData: fieldData ?? "",
63+
type: type,
64+
);
65+
}
66+
67+
void main() {
68+
TestZulipBinding.ensureInitialized();
69+
70+
group('ProfilePage', () {
71+
testWidgets('page builds; profile page renders', (WidgetTester tester) async {
72+
final user = eg.user(userId: 1, fullName: "test user");
73+
74+
await setupPage(tester, users:[user], pageUserId: user.userId);
75+
76+
check(because: "find user avatar", find.byType(Avatar).evaluate()).length.equals(1);
77+
check(because: "find user name", find.text("test user").evaluate()).isNotEmpty();
78+
});
79+
80+
testWidgets('page builds; profile page renders with profileData', (WidgetTester tester) async {
81+
await setupPage(tester,
82+
users: [
83+
eg.user(userId: 1, profileData: {
84+
0: ProfileFieldUserData(value: "shortTextValue"),
85+
1: ProfileFieldUserData(value: "longTextValue"),
86+
2: ProfileFieldUserData(value: "x"),
87+
3: ProfileFieldUserData(value: "dateValue"),
88+
4: ProfileFieldUserData(value: "http://example/linkValue"),
89+
5: ProfileFieldUserData(value: "[2]"),
90+
6: ProfileFieldUserData(value: "externalValue"),
91+
7: ProfileFieldUserData(value: "pronounsValue"),
92+
}),
93+
eg.user(userId: 2),
94+
],
95+
pageUserId: 1,
96+
customProfileFields: [
97+
mkCustomProfileField(0, CustomProfileFieldType.shortText),
98+
mkCustomProfileField(1, CustomProfileFieldType.longText),
99+
mkCustomProfileField(2, CustomProfileFieldType.choice, fieldData: '{"x": {"text": "choiceValue", "order": "1"}}'),
100+
mkCustomProfileField(3, CustomProfileFieldType.date),
101+
mkCustomProfileField(4, CustomProfileFieldType.link),
102+
mkCustomProfileField(5, CustomProfileFieldType.user),
103+
mkCustomProfileField(6, CustomProfileFieldType.externalAccount, fieldData: '{"subtype": "external1"}'),
104+
mkCustomProfileField(7, CustomProfileFieldType.pronouns),
105+
], realmDefaultExternalAccounts: {
106+
"external1": RealmDefaultExternalAccount(
107+
name: "external1",
108+
text: "",
109+
hint: "",
110+
urlPattern: "https://example/%(username)s")});
111+
112+
final testCases = [
113+
(find.text("field0"), find.text("shortTextValue"), CustomProfileFieldType.shortText),
114+
(find.text("field1"), find.text("longTextValue"), CustomProfileFieldType.longText),
115+
(find.text("field2"), find.text("choiceValue"), CustomProfileFieldType.choice),
116+
(find.text("field3"), find.text("dateValue"), CustomProfileFieldType.date),
117+
(find.text("field4"), find.text("http://example/linkValue"), CustomProfileFieldType.link),
118+
(find.text("field5"), find.byType(Avatar), CustomProfileFieldType.user),
119+
(find.text("field6"), find.text("http://example/externalValue"), CustomProfileFieldType.externalAccount),
120+
(find.text("field7"), find.text("pronounsValue"), CustomProfileFieldType.pronouns),
121+
];
122+
final profileDataTable = find.byType(Column).last;
123+
testCases.map((testCase) {
124+
Finder labelFinder = testCase.$1;
125+
Finder fieldFinder = testCase.$2;
126+
CustomProfileFieldType testCaseType = testCase.$3;
127+
check(
128+
because: "find label for $testCaseType",
129+
find.descendant(of: profileDataTable, matching: labelFinder).evaluate().length
130+
).equals(1);
131+
check(
132+
because: "find field for $testCaseType",
133+
find.descendant(of: profileDataTable, matching: fieldFinder).evaluate().length
134+
).equals(1);
135+
});
136+
});
137+
138+
testWidgets('page builds; error page shows up if data is missing', (WidgetTester tester) async {
139+
await setupPage(tester, pageUserId: eg.selfUser.userId + 1989);
140+
check(because: "find no user avatar", find.byType(Avatar).evaluate()).isEmpty();
141+
check(because: "find error icon", find.byIcon(Icons.error).evaluate()).isNotEmpty();
142+
});
143+
144+
testWidgets('page builds; link type will navigate', (WidgetTester tester) async {
145+
const testUrl = "http://example/url";
146+
final user = eg.user(userId: 1, profileData: {
147+
0: ProfileFieldUserData(value: testUrl),
148+
});
149+
150+
await setupPage(tester,
151+
users: [user],
152+
pageUserId: user.userId,
153+
customProfileFields: [mkCustomProfileField(0, CustomProfileFieldType.link)],
154+
);
155+
156+
await tester.tap(find.text(testUrl));
157+
final expectedMode = defaultTargetPlatform == TargetPlatform.android ?
158+
LaunchMode.externalApplication : LaunchMode.platformDefault;
159+
check(testBinding.takeLaunchUrlCalls())
160+
.single.equals((url: Uri.parse(testUrl), mode: expectedMode));
161+
});
162+
163+
testWidgets('page builds; external link type navigates away', (WidgetTester tester) async {
164+
final user = eg.user(userId: 1, profileData: {
165+
0: ProfileFieldUserData(value: "externalValue"),
166+
});
167+
168+
await setupPage(tester,
169+
users: [user],
170+
pageUserId: user.userId,
171+
customProfileFields: [mkCustomProfileField(0, CustomProfileFieldType.externalAccount, fieldData:'{"subtype": "external1"}')],
172+
realmDefaultExternalAccounts: {
173+
"external1": RealmDefaultExternalAccount(
174+
name: "external1",
175+
text: "",
176+
hint: "",
177+
urlPattern: "http://example/%(username)s")},
178+
);
179+
180+
await tester.tap(find.text("externalValue"));
181+
final expectedMode = defaultTargetPlatform == TargetPlatform.android ?
182+
LaunchMode.externalApplication : LaunchMode.platformDefault;
183+
check(testBinding.takeLaunchUrlCalls())
184+
.single.equals((url: Uri.parse("http://example/externalValue"), mode: expectedMode));
185+
});
186+
187+
testWidgets('page builds; user links to profile', (WidgetTester tester) async {
188+
final users = [
189+
eg.user(userId: 1, profileData: {
190+
0: ProfileFieldUserData(value: "[2]"),
191+
}),
192+
eg.user(userId: 2, fullName: "test user"),
193+
];
194+
final pushedRoutes = <Route<dynamic>>[];
195+
final testNavObserver = TestNavigatorObserver()
196+
..onPushed = (route, prevRoute) => pushedRoutes.add(route);
197+
198+
await setupPage(tester,
199+
users: users,
200+
pageUserId: 1,
201+
customProfileFields: [mkCustomProfileField(0, CustomProfileFieldType.user)],
202+
navigatorObserver: testNavObserver,
203+
);
204+
205+
final textFinder = find.text("test user");
206+
check(textFinder.evaluate()).length.equals(1);
207+
final fieldContainer = find.ancestor(of: textFinder, matching: find.byType(Column)).first;
208+
final targetWidget = find.descendant(of: fieldContainer, matching:find.byType(Avatar));
209+
await tester.tap(targetWidget, warnIfMissed: false);
210+
check(pushedRoutes).last.isA<WidgetRoute>().page.isA<ProfilePage>().userId.equals(2);
211+
});
212+
213+
testWidgets('page builds; dm links to correct narrow', (WidgetTester tester) async {
214+
final pushedRoutes = <Route<dynamic>>[];
215+
final testNavObserver = TestNavigatorObserver()
216+
..onPushed = (route, prevRoute) => pushedRoutes.add(route);
217+
final expectedNarrow = DmNarrow(
218+
allRecipientIds: [eg.selfUser.userId, 1]..sort(),
219+
selfUserId: eg.selfUser.userId);
220+
221+
await setupPage(tester,
222+
users: [eg.user(userId: 1)],
223+
pageUserId: 1,
224+
navigatorObserver: testNavObserver,
225+
);
226+
227+
final targetWidget = find.byIcon(Icons.email);
228+
await tester.tap(targetWidget, warnIfMissed: false);
229+
check(pushedRoutes).last.isA<WidgetRoute>().page
230+
.isA<MessageListPage>()
231+
.narrow.equals(expectedNarrow);
232+
});
233+
234+
testWidgets('page builds; user links render multiple avatars', (WidgetTester tester) async {
235+
final users = [
236+
eg.user(userId: 1, profileData: {
237+
0: ProfileFieldUserData(value: "[2,3]"),
238+
}),
239+
eg.user(userId: 2, fullName: "test user1"),
240+
eg.user(userId: 3, fullName: "test user2"),
241+
];
242+
243+
await setupPage(tester,
244+
users: users,
245+
pageUserId: 1,
246+
customProfileFields: [mkCustomProfileField(0, CustomProfileFieldType.user)],
247+
);
248+
249+
final findAvatars = find.byType(Avatar);
250+
check(findAvatars.evaluate()).length.equals(2 + 1);
251+
});
252+
253+
testWidgets('page builds; ensure long values do not overflow', (WidgetTester tester) async {
254+
final longString = List.filled(400, "X").join();
255+
final user = eg.user(userId: 1, fullName: longString, profileData: {
256+
0: ProfileFieldUserData(value: longString),
257+
1: ProfileFieldUserData(value: "[1]"),
258+
});
259+
260+
await setupPage(tester, users:[user], pageUserId: user.userId, customProfileFields: [
261+
mkCustomProfileField(0, CustomProfileFieldType.shortText),
262+
mkCustomProfileField(1, CustomProfileFieldType.user),
263+
]);
264+
265+
check(because: "find user name", find.text(longString).evaluate()).isNotEmpty();
266+
});
267+
268+
});
269+
}

0 commit comments

Comments
 (0)
Please sign in to comment.