1
1
import 'dart:async' ;
2
+ import 'dart:math' ;
2
3
3
4
import 'package:checks/checks.dart' ;
4
5
import 'package:clock/clock.dart' ;
@@ -10,12 +11,18 @@ import 'package:video_player_platform_interface/video_player_platform_interface.
10
11
import 'package:video_player/video_player.dart' ;
11
12
import 'package:zulip/api/model/model.dart' ;
12
13
import 'package:zulip/model/localizations.dart' ;
14
+ import 'package:zulip/model/narrow.dart' ;
15
+ import 'package:zulip/model/store.dart' ;
13
16
import 'package:zulip/widgets/app.dart' ;
14
17
import 'package:zulip/widgets/content.dart' ;
15
18
import 'package:zulip/widgets/lightbox.dart' ;
19
+ import 'package:zulip/widgets/message_list.dart' ;
16
20
21
+ import '../api/fake_api.dart' ;
17
22
import '../example_data.dart' as eg;
18
23
import '../model/binding.dart' ;
24
+ import '../model/content_test.dart' ;
25
+ import '../model/test_store.dart' ;
19
26
import '../test_images.dart' ;
20
27
import 'dialog_checks.dart' ;
21
28
import 'test_app.dart' ;
@@ -197,6 +204,113 @@ class FakeVideoPlayerPlatform extends Fake
197
204
void main () {
198
205
TestZulipBinding .ensureInitialized ();
199
206
207
+ group ('LightboxHero' , () {
208
+ late PerAccountStore store;
209
+ late FakeApiConnection connection;
210
+
211
+ final channel = eg.stream ();
212
+ final message = eg.streamMessage (stream: channel,
213
+ topic: 'test topic' , contentMarkdown: ContentExample .imageSingle.html);
214
+
215
+ // From ContentExample.imageSingle.
216
+ final imageSrcUrlStr = 'https://chat.example/user_uploads/thumbnail/2/ce/nvoNL2LaZOciwGZ-FYagddtK/image.jpg/840x560.webp' ;
217
+ final imageSrcUrl = Uri .parse (imageSrcUrlStr);
218
+ final imageFinder = find.byWidgetPredicate (
219
+ (widget) => widget is RealmContentNetworkImage && widget.src == imageSrcUrl);
220
+
221
+ Future <void > setupMessageListPage (WidgetTester tester) async {
222
+ addTearDown (testBinding.reset);
223
+ final subscription = eg.subscription (channel);
224
+ await testBinding.globalStore.add (eg.selfAccount, eg.initialSnapshot (
225
+ streams: [channel], subscriptions: [subscription]));
226
+ store = await testBinding.globalStore.perAccount (eg.selfAccount.id);
227
+ connection = store.connection as FakeApiConnection ;
228
+ await store.addUser (eg.selfUser);
229
+
230
+ connection.prepare (json:
231
+ eg.newestGetMessagesResult (foundOldest: true , messages: [message]).toJson ());
232
+ await tester.pumpWidget (TestZulipApp (accountId: eg.selfAccount.id,
233
+ child: MessageListPage (initNarrow: const CombinedFeedNarrow ())));
234
+ await tester.pumpAndSettle ();
235
+ }
236
+
237
+ testWidgets ('Hero animation occurs smoothly when opening lightbox from message list' , (tester) async {
238
+ double dist (Rect a, Rect b) =>
239
+ sqrt (pow (a.top - b.top, 2 ) + pow (a.left - b.left, 2 ));
240
+
241
+ prepareBoringImageHttpClient ();
242
+
243
+ await setupMessageListPage (tester);
244
+
245
+ final initialImagePosition = tester.getRect (imageFinder);
246
+ await tester.tap (imageFinder);
247
+ await tester.pump ();
248
+ // pump to start hero animation
249
+ await tester.pump ();
250
+
251
+ const heroAnimationDuration = Duration (milliseconds: 300 );
252
+ const steps = 150 ;
253
+ final stepDuration = heroAnimationDuration ~ / steps;
254
+ final animatedPositions = < Rect > [];
255
+ for (int i = 1 ; i <= steps; i++ ) {
256
+ await tester.pump (stepDuration);
257
+ animatedPositions.add (tester.getRect (imageFinder));
258
+ }
259
+
260
+ final totalDistance = dist (initialImagePosition, animatedPositions.last);
261
+ Rect previousPosition = initialImagePosition;
262
+ double maxStepDistance = 0.0 ;
263
+ for (final position in animatedPositions) {
264
+ final stepDistance = dist (previousPosition, position);
265
+ maxStepDistance = max (maxStepDistance, stepDistance);
266
+ check (position).not ((pos) => pos.equals (previousPosition));
267
+
268
+ previousPosition = position;
269
+ }
270
+ check (maxStepDistance).isLessThan (0.03 * totalDistance);
271
+
272
+ debugNetworkImageHttpClientProvider = null ;
273
+ });
274
+
275
+ testWidgets ('no hero animation occurs between different message list pages for same image' , (tester) async {
276
+ Rect getElementRect (Element element) =>
277
+ tester.getRect (find.byElementPredicate ((e) => e == element));
278
+
279
+ prepareBoringImageHttpClient ();
280
+
281
+ await setupMessageListPage (tester);
282
+
283
+ final firstElement = tester.element (imageFinder);
284
+ final firstImagePosition = getElementRect (firstElement);
285
+
286
+ connection.prepare (json:
287
+ eg.newestGetMessagesResult (foundOldest: true , messages: [message]).toJson ());
288
+ await tester.tap (find.descendant (
289
+ of: find.byType (StreamMessageRecipientHeader ),
290
+ matching: find.text ('test topic' )));
291
+ await tester.pumpAndSettle ();
292
+
293
+ final secondElement = tester.element (imageFinder);
294
+ final secondImagePosition = getElementRect (secondElement);
295
+
296
+ await tester.tap (find.byType (BackButton ));
297
+ await tester.pump ();
298
+
299
+ const heroAnimationDuration = Duration (milliseconds: 300 );
300
+ const steps = 150 ;
301
+ final stepDuration = heroAnimationDuration ~ / steps;
302
+ for (int i = 0 ; i < steps; i++ ) {
303
+ await tester.pump (stepDuration);
304
+ check (tester.elementList (imageFinder))
305
+ .unorderedEquals ([firstElement, secondElement]);
306
+ check (getElementRect (firstElement)).equals (firstImagePosition);
307
+ check (getElementRect (secondElement)).equals (secondImagePosition);
308
+ }
309
+
310
+ debugNetworkImageHttpClientProvider = null ;
311
+ });
312
+ });
313
+
200
314
group ('_ImageLightboxPage' , () {
201
315
final src = Uri .parse ('https://chat.example/lightbox-image.png' );
202
316
@@ -216,6 +330,7 @@ void main() {
216
330
unawaited (navigator.push (getImageLightboxRoute (
217
331
accountId: eg.selfAccount.id,
218
332
message: message ?? eg.streamMessage (),
333
+ messageImageContext: navigator.context,
219
334
src: src,
220
335
thumbnailUrl: thumbnailUrl,
221
336
originalHeight: null ,
0 commit comments