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,128 @@ 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 imageSrcUrlStr = 'https://chat.example/user_uploads/thumbnail/2/ce/nvoNL2LaZOciwGZ-FYagddtK/image.jpg/840x560.webp' ;
213
+ final imageSrcUrl = Uri .parse (imageSrcUrlStr);
214
+ final message = eg.streamMessage (stream: channel,
215
+ topic: 'test topic' , contentMarkdown: ContentExample .imageSingle.html);
216
+
217
+ Future <void > setupMessageListPage (WidgetTester tester) async {
218
+ addTearDown (testBinding.reset);
219
+ final subscription = eg.subscription (channel);
220
+ await testBinding.globalStore.add (eg.selfAccount, eg.initialSnapshot (
221
+ streams: [channel], subscriptions: [subscription]));
222
+ store = await testBinding.globalStore.perAccount (eg.selfAccount.id);
223
+ connection = store.connection as FakeApiConnection ;
224
+ await store.addUser (eg.selfUser);
225
+
226
+ connection.prepare (json:
227
+ eg.newestGetMessagesResult (foundOldest: true , messages: [message]).toJson ());
228
+
229
+ await tester.pumpWidget (TestZulipApp (accountId: eg.selfAccount.id,
230
+ child: MessageListPage (initNarrow: const CombinedFeedNarrow ())));
231
+
232
+ await tester.pumpAndSettle ();
233
+ }
234
+
235
+ testWidgets ('Hero animation occurs smoothly when opening lightbox from message list' , (tester) async {
236
+ prepareBoringImageHttpClient ();
237
+
238
+ await setupMessageListPage (tester);
239
+
240
+ final messageContentFinder = find.byWidgetPredicate ((widget) =>
241
+ widget is MessageContent && widget.message.id == message.id
242
+ );
243
+ final messageListImageFinder = find.descendant (
244
+ of: messageContentFinder,
245
+ matching: find.byType (RealmContentNetworkImage )
246
+ );
247
+ final initialImagePosition = tester.getRect (messageListImageFinder);
248
+ await tester.tap (messageListImageFinder);
249
+ await tester.pump ();
250
+ // pump to start hero animation
251
+ await tester.pump ();
252
+
253
+ final heroAnimationDuration = Duration (milliseconds: 300 );
254
+ final steps = 150 ;
255
+ final stepDuration = heroAnimationDuration ~ / steps;
256
+ List <Rect > animatedPositions = [];
257
+ for (int i = 1 ; i <= steps; i++ ) {
258
+ await tester.pump (stepDuration);
259
+
260
+ final animatedFlightImageFinder = find.byWidgetPredicate ((widget) =>
261
+ widget is RealmContentNetworkImage && widget.src == imageSrcUrl
262
+ );
263
+ animatedPositions.add (tester.getRect (animatedFlightImageFinder));
264
+ }
265
+
266
+ final totalDistance = sqrt (
267
+ pow (initialImagePosition.top - animatedPositions.last.top, 2 ) +
268
+ pow (initialImagePosition.left - animatedPositions.last.left, 2 ));
269
+
270
+ Rect previousPosition = initialImagePosition;
271
+ double maxStepDistance = 0.0 ;
272
+ for (final position in animatedPositions) {
273
+ final stepDistance = sqrt (
274
+ pow (position.top - previousPosition.top, 2 ) +
275
+ pow (position.left - previousPosition.left, 2 ));
276
+ maxStepDistance = max (maxStepDistance, stepDistance);
277
+ check (position).not ((pos) => pos.equals (previousPosition));
278
+
279
+ previousPosition = position;
280
+ }
281
+ check (maxStepDistance).isLessThan (0.03 * totalDistance);
282
+
283
+ debugNetworkImageHttpClientProvider = null ;
284
+ });
285
+
286
+ testWidgets ('no hero animation occurs between different message list pages for same image' , (tester) async {
287
+ prepareBoringImageHttpClient ();
288
+
289
+ await setupMessageListPage (tester);
290
+
291
+ final imageFinder = find.byWidgetPredicate ((widget) =>
292
+ widget is RealmContentNetworkImage && widget.src == imageSrcUrl
293
+ );
294
+ final firstImagePosition = tester.getRect (imageFinder);
295
+ final firstElement = imageFinder.evaluate ().single;
296
+
297
+ connection.prepare (json:
298
+ eg.newestGetMessagesResult (foundOldest: true , messages: [message]).toJson ());
299
+ await tester.tap (find.descendant (
300
+ of: find.byType (StreamMessageRecipientHeader ),
301
+ matching: find.text ('test topic' )));
302
+ await tester.pumpAndSettle ();
303
+
304
+ final secondImagePosition = tester.getRect (imageFinder);
305
+ final secondElement = imageFinder.evaluate ().single;
306
+
307
+ await tester.tap (find.byType (BackButton ));
308
+ await tester.pump ();
309
+
310
+ final heroAnimationDuration = Duration (milliseconds: 300 );
311
+ final steps = 150 ;
312
+ final stepDuration = heroAnimationDuration ~ / steps;
313
+ for (int i = 0 ; i < steps; i++ ) {
314
+ await tester.pump (stepDuration);
315
+
316
+ final currentImages = imageFinder.evaluate ();
317
+ check (currentImages).unorderedEquals ([firstElement, secondElement]);
318
+
319
+ check (tester.getRect (
320
+ find.byElementPredicate ((e) => e == firstElement))).equals (firstImagePosition);
321
+ check (tester.getRect (
322
+ find.byElementPredicate ((e) => e == secondElement))).equals (secondImagePosition);
323
+ }
324
+
325
+ debugNetworkImageHttpClientProvider = null ;
326
+ });
327
+ });
328
+
200
329
group ('_ImageLightboxPage' , () {
201
330
final src = Uri .parse ('https://chat.example/lightbox-image.png' );
202
331
@@ -216,6 +345,7 @@ void main() {
216
345
unawaited (navigator.push (getImageLightboxRoute (
217
346
accountId: eg.selfAccount.id,
218
347
message: message ?? eg.streamMessage (),
348
+ messageImageContext: navigator.context,
219
349
src: src,
220
350
thumbnailUrl: thumbnailUrl,
221
351
originalHeight: null ,
0 commit comments