1+ import 'dart:convert' ;
2+
13import 'package:checks/checks.dart' ;
4+ import 'package:flutter/material.dart' ;
25import 'package:flutter_test/flutter_test.dart' ;
6+ import 'package:http/http.dart' as http;
7+ import 'package:zulip/api/model/events.dart' ;
38import 'package:zulip/api/model/model.dart' ;
9+ import 'package:zulip/api/route/channels.dart' ;
10+ import 'package:zulip/model/localizations.dart' ;
11+ import 'package:zulip/model/store.dart' ;
412import 'package:zulip/widgets/channel_list.dart' ;
13+ import 'package:zulip/widgets/icons.dart' ;
514
15+ import '../api/fake_api.dart' ;
616import '../model/binding.dart' ;
717import '../example_data.dart' as eg;
18+ import '../stdlib_checks.dart' ;
19+ import 'dialog_checks.dart' ;
820import 'test_app.dart' ;
921
1022void main () {
1123 TestZulipBinding .ensureInitialized ();
24+ late FakeApiConnection connection;
25+ late PerAccountStore store;
1226
1327 Future <void > setupChannelListPage (WidgetTester tester, {
1428 required List <ZulipStream > streams,
@@ -18,8 +32,10 @@ void main() {
1832 final initialSnapshot = eg.initialSnapshot (
1933 subscriptions: subscriptions,
2034 streams: streams,
21- );
35+ realmUsers : [eg.selfUser] );
2236 await testBinding.globalStore.add (eg.selfAccount, initialSnapshot);
37+ store = await testBinding.globalStore.perAccount (eg.selfAccount.id);
38+ connection = store.connection as FakeApiConnection ;
2339
2440 await tester.pumpWidget (TestZulipApp (accountId: eg.selfAccount.id, child: const ChannelListPage ()));
2541
@@ -67,4 +83,160 @@ void main() {
6783 check (listedStreamNames (tester)).deepEquals (['a' , 'b' , 'c' ]);
6884 });
6985 });
86+
87+ group ('subscription toggle' , () {
88+ final zulipLocalizations = GlobalLocalizations .zulipLocalizations;
89+
90+ Future <ZulipStream > prepareSingleStream (WidgetTester tester) async {
91+ final stream = eg.stream ();
92+ await setupChannelListPage (tester, streams: [stream], subscriptions: []);
93+ return stream;
94+ }
95+
96+ Future <void > tapSubscribeButton (WidgetTester tester) async {
97+ await tester.tap (find.byIcon (Icons .add));
98+ }
99+
100+ Future <void > waitAndCheckSnackbarIsShown (WidgetTester tester, String message, String channelName) async {
101+ await tester.pump (Duration .zero);
102+ await tester.pumpAndSettle ();
103+ final richTextFinder = find.byWidgetPredicate (
104+ (widget) => widget is RichText && widget.text.toPlainText ().contains (message));
105+ check (richTextFinder.evaluate ()).single;
106+ final richTextWidget = tester.widget <RichText >(richTextFinder);
107+ check (richTextWidget.text.toPlainText ()).contains (message);
108+ check (richTextWidget.text.toPlainText ()).contains (channelName);
109+ check (find.descendant (
110+ of: richTextFinder,
111+ matching: find.byIcon (ZulipIcons .hash_sign),
112+ ).evaluate ()).single;
113+ }
114+
115+ testWidgets ('is affected by subscription events' , (WidgetTester tester) async {
116+ final stream = await prepareSingleStream (tester);
117+ connection.prepare (json: SubscribeToChannelsResult (
118+ subscribed: {eg.selfUser.email: [stream.name]},
119+ alreadySubscribed: {}).toJson ());
120+
121+ check (find.byIcon (Icons .add).evaluate ()).isNotEmpty ();
122+
123+ await store.handleEvent (SubscriptionAddEvent (id: 1 ,
124+ subscriptions: [eg.subscription (stream)]));
125+ await tester.pumpAndSettle ();
126+
127+ check (find.byIcon (Icons .add).evaluate ()).isEmpty ();
128+
129+ await store.handleEvent (SubscriptionRemoveEvent (id: 2 , streamIds: [stream.streamId]));
130+ await tester.pumpAndSettle ();
131+
132+ check (find.byIcon (Icons .add).evaluate ()).isNotEmpty ();
133+ });
134+
135+ testWidgets ('is disabled while loading' , (WidgetTester tester) async {
136+ final stream = eg.stream ();
137+ await setupChannelListPage (tester, streams: [stream], subscriptions: []);
138+ connection.prepare (json: SubscribeToChannelsResult (
139+ subscribed: {eg.selfUser.email: [stream.name]},
140+ alreadySubscribed: {}).toJson ());
141+ await tapSubscribeButton (tester);
142+ await tester.pump ();
143+
144+ check (tester.widget <IconButton >(
145+ find.byType (IconButton )).onPressed).isNull ();
146+
147+ await tester.pump (const Duration (seconds: 2 ));
148+
149+ check (tester.widget <IconButton >(
150+ find.byType (IconButton )).onPressed).isNotNull ();
151+ });
152+
153+ testWidgets ('is disabled while loading and enabled back when loading fails' , (WidgetTester tester) async {
154+ final stream = eg.stream ();
155+ await setupChannelListPage (tester, streams: [stream], subscriptions: []);
156+ connection.prepare (exception: http.ClientException ('Oops' ), delay: const Duration (seconds: 2 ));
157+ await tapSubscribeButton (tester);
158+ await tester.pump ();
159+
160+ check (tester.widget <IconButton >(
161+ find.byType (IconButton )).onPressed).isNull ();
162+
163+ await tester.pump (const Duration (seconds: 2 ));
164+
165+ check (tester.widget <IconButton >(
166+ find.byType (IconButton )).onPressed).isNotNull ();
167+ });
168+
169+ group ('subscribe' , () {
170+ testWidgets ('is shown only for streams that user is not subscribed to' , (tester) async {
171+ final streams = [eg.stream (), eg.stream (), eg.subscription (eg.stream ())];
172+ final subscriptions = [streams[2 ] as Subscription ];
173+ await setupChannelListPage (tester, streams: streams, subscriptions: subscriptions);
174+
175+ check (find.byIcon (Icons .add).evaluate ().length).equals (2 );
176+ });
177+
178+ testWidgets ('smoke api' , (tester) async {
179+ final stream = await prepareSingleStream (tester);
180+ connection.prepare (json: SubscribeToChannelsResult (
181+ subscribed: {eg.selfUser.email: [stream.name]},
182+ alreadySubscribed: {}).toJson ());
183+ await tapSubscribeButton (tester);
184+
185+ await tester.pump (Duration .zero);
186+ await tester.pumpAndSettle ();
187+ check (connection.lastRequest).isA< http.Request > ()
188+ ..method.equals ('POST' )
189+ ..url.path.equals ('/api/v1/users/me/subscriptions' )
190+ ..bodyFields.deepEquals ({
191+ 'subscriptions' : jsonEncode ([{'name' : stream.name}])
192+ });
193+ });
194+
195+ testWidgets ('shows a snackbar when subscription passes' , (WidgetTester tester) async {
196+ final stream = await prepareSingleStream (tester);
197+ connection.prepare (json: SubscribeToChannelsResult (
198+ subscribed: {eg.selfUser.email: [stream.name]},
199+ alreadySubscribed: {}).toJson ());
200+ await tapSubscribeButton (tester);
201+
202+ await waitAndCheckSnackbarIsShown (tester,
203+ zulipLocalizations.messageSubscribedToChannel, stream.name);
204+ });
205+
206+ testWidgets ('shows a snackbar when already subscribed' , (WidgetTester tester) async {
207+ final stream = await prepareSingleStream (tester);
208+ connection.prepare (json: SubscribeToChannelsResult (
209+ subscribed: {},
210+ alreadySubscribed: {eg.selfUser.email: [stream.name]}).toJson ());
211+ await tapSubscribeButton (tester);
212+
213+ await waitAndCheckSnackbarIsShown (tester,
214+ zulipLocalizations.messageAlreadySubscribedToChannel, stream.name);
215+ });
216+
217+ testWidgets ('shows a snackbar when subscription fails' , (WidgetTester tester) async {
218+ final stream = await prepareSingleStream (tester);
219+ connection.prepare (json: SubscribeToChannelsResult (
220+ subscribed: {},
221+ alreadySubscribed: {},
222+ unauthorized: [stream.name]).toJson ());
223+ await tapSubscribeButton (tester);
224+
225+ await waitAndCheckSnackbarIsShown (tester,
226+ zulipLocalizations.errorFailedToSubscribedToChannel, stream.name);
227+ });
228+
229+ testWidgets ('catch-all api errors' , (WidgetTester tester) async {
230+ await prepareSingleStream (tester);
231+ connection.prepare (exception: http.ClientException ('Oops' ));
232+ await tapSubscribeButton (tester);
233+ await tester.pump (Duration .zero);
234+ await tester.pumpAndSettle ();
235+
236+ checkErrorDialog (tester,
237+ expectedTitle: zulipLocalizations.errorFailedToSubscribedToChannel,
238+ expectedMessage: 'NetworkException: Oops (ClientException: Oops)' );
239+ });
240+ });
241+ });
70242}
0 commit comments