Skip to content

Commit ec4078d

Browse files
lightbox test: draft
1 parent 6d8be06 commit ec4078d

File tree

3 files changed

+164
-10
lines changed

3 files changed

+164
-10
lines changed

lib/widgets/lightbox.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,9 @@ class LightboxHero extends StatelessWidget {
6969
}
7070
}
7171

72-
class _CopyLinkButton extends StatelessWidget {
73-
const _CopyLinkButton({required this.url});
72+
@visibleForTesting
73+
class CopyLinkButton extends StatelessWidget {
74+
const CopyLinkButton({super.key, required this.url});
7475

7576
final Uri url;
7677

@@ -249,7 +250,7 @@ class _ImageLightboxPageState extends State<_ImageLightboxPage> {
249250
color: color,
250251
elevation: elevation,
251252
child: Row(children: [
252-
_CopyLinkButton(url: widget.src),
253+
CopyLinkButton(url: widget.src),
253254
// TODO(#43): Share image
254255
// TODO(#42): Download image
255256
]),

test/test_images.dart

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,34 @@ import 'package:flutter_test/flutter_test.dart';
1212
/// before the end of the test.
1313
// TODO(upstream) simplify callers by using addTearDown: https://github.com/flutter/flutter/issues/123189
1414
// See also: https://github.com/flutter/flutter/issues/121917
15-
FakeImageHttpClient prepareBoringImageHttpClient() {
15+
FakeImageHttpClient prepareBoringImageHttpClient({
16+
List<int> content = kSolidBlueAvatar,
17+
}) {
1618
final httpClient = FakeImageHttpClient();
1719
debugNetworkImageHttpClientProvider = () => httpClient;
1820
httpClient.request.response
1921
..statusCode = HttpStatus.ok
20-
..content = kSolidBlueAvatar;
22+
..content = content;
2123
return httpClient;
2224
}
2325

2426
class FakeImageHttpClient extends Fake implements HttpClient {
27+
final Map<Uri, FakeImageHttpClientRequest> requests = {};
2528
final FakeImageHttpClientRequest request = FakeImageHttpClientRequest();
2629

2730
@override
28-
Future<HttpClientRequest> getUrl(Uri url) async => request;
31+
Future<HttpClientRequest> getUrl(Uri url) {
32+
final req = requests[url] ?? request;
33+
return req.delay == Duration.zero
34+
? Future.value(req)
35+
: Future.delayed(req.delay, () => req);
36+
}
2937
}
3038

3139
class FakeImageHttpClientRequest extends Fake implements HttpClientRequest {
40+
final Duration delay;
41+
FakeImageHttpClientRequest({this.delay = Duration.zero});
42+
3243
final FakeImageHttpClientResponse response = FakeImageHttpClientResponse();
3344

3445
@override
@@ -61,7 +72,7 @@ class FakeImageHttpClientResponse extends Fake implements HttpClientResponse {
6172

6273
@override
6374
StreamSubscription<List<int>> listen(void Function(List<int> event)? onData, {Function? onError, void Function()? onDone, bool? cancelOnError}) {
64-
return Stream.value(content).listen(
75+
return Stream.fromIterable([content]).listen(
6576
onData, onDone: onDone, onError: onError, cancelOnError: cancelOnError);
6677
}
6778
}
@@ -84,3 +95,30 @@ const List<int> kSolidBlueAvatar = [
8495
0x78, 0x00, 0x01, 0x1e, 0xcd, 0x28, 0xcd, 0x00, 0x00, 0x00, 0x00, 0x49,
8596
0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
8697
];
98+
99+
/// A 1000x1000 PNG image of solid Zulip blue, [kZulipBrandColor].
100+
// Make with same SVG as `kSolidBlueAvatar` and
101+
// with `inkscape tmp.svg -w 1000 --export-png=tmp1.png`,
102+
// `zopflipng tmp1.png tmp.png`,
103+
// and `xxd -i tmp.png`.
104+
const List<int> kSolidBlueLargeAvatar = [
105+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
106+
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x03, 0xe8, 0x00, 0x00, 0x03, 0xe8,
107+
0x01, 0x03, 0x00, 0x00, 0x00, 0x77, 0x6d, 0x46, 0xa7, 0x00, 0x00, 0x00,
108+
0x03, 0x50, 0x4c, 0x54, 0x45, 0x64, 0x92, 0xfe, 0xf1, 0xd6, 0x69, 0xa5,
109+
0x00, 0x00, 0x00, 0x91, 0x49, 0x44, 0x41, 0x54, 0x78, 0x01, 0xed, 0xc1,
110+
0x31, 0x01, 0x00, 0x00, 0x00, 0xc2, 0x20, 0xfb, 0xa7, 0x36, 0xc5, 0x3e,
111+
0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
112+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
113+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
114+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
115+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
116+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
117+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
118+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
119+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
120+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
121+
0x00, 0x00, 0x00, 0xb1, 0x03, 0xec, 0x3f, 0x00, 0x01, 0x2f, 0x9b, 0x3e,
122+
0x0a, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60,
123+
0x82
124+
];

test/widgets/lightbox_test.dart

Lines changed: 118 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import 'dart:async';
2+
import 'dart:ui';
23

34
import 'package:checks/checks.dart';
45
import 'package:clock/clock.dart';
6+
import 'package:flutter/rendering.dart';
57
import 'package:flutter/services.dart';
68
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
79
import 'package:flutter_test/flutter_test.dart';
@@ -17,7 +19,9 @@ import 'package:zulip/widgets/lightbox.dart';
1719
import 'package:zulip/widgets/store.dart';
1820

1921
import '../example_data.dart' as eg;
22+
import '../flutter_checks.dart';
2023
import '../model/binding.dart';
24+
import '../test_clipboard.dart';
2125
import '../test_images.dart';
2226
import 'dialog_checks.dart';
2327

@@ -198,12 +202,33 @@ class FakeVideoPlayerPlatform extends Fake
198202
void main() {
199203
TestZulipBinding.ensureInitialized();
200204

205+
testWidgets('CopyLinkButton', (tester) async {
206+
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
207+
SystemChannels.platform,
208+
MockClipboard().handleMethodCall,
209+
);
210+
211+
final url = Uri.parse('https://chat.example/media');
212+
await tester.pumpWidget(MaterialApp(
213+
home: Scaffold(body: CopyLinkButton(url: url)),
214+
localizationsDelegates: ZulipLocalizations.localizationsDelegates,
215+
supportedLocales: ZulipLocalizations.supportedLocales,
216+
));
217+
await tester.tap(find.byIcon(Icons.copy));
218+
await tester.pumpAndSettle();
219+
final data = await Clipboard.getData('text/plain');
220+
check(data).isNotNull().text.equals(url.toString());
221+
});
222+
201223
group('_ImageLightboxPage', () {
202224
final src = Uri.parse('https://chat.example/lightbox-image.png');
225+
// final thumbnail = Uri.parse('https://chat.example/thumbnail/lightbox-image.png/840x560.webp');
203226

204227
Future<void> setupPage(WidgetTester tester, {
205228
Message? message,
206229
required Uri? thumbnailUrl,
230+
double? originalWidth,
231+
double? originalHeight,
207232
}) async {
208233
addTearDown(testBinding.reset);
209234
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot());
@@ -216,8 +241,8 @@ void main() {
216241
message: message ?? eg.streamMessage(),
217242
src: src,
218243
thumbnailUrl: thumbnailUrl,
219-
originalHeight: null,
220-
originalWidth: null,
244+
originalWidth: originalWidth,
245+
originalHeight: originalHeight,
221246
));
222247
await tester.pump(); // per-account store
223248
await tester.pump(const Duration(milliseconds: 301)); // nav transition
@@ -232,6 +257,7 @@ void main() {
232257
check(image.src).equals(src);
233258

234259
debugNetworkImageHttpClientProvider = null;
260+
PaintingBinding.instance.imageCache.clear();
235261
});
236262

237263
testWidgets('app bar shows sender name and date', (tester) async {
@@ -250,6 +276,7 @@ void main() {
250276
.contains('Jul 23, 2024 23:12:24');
251277

252278
debugNetworkImageHttpClientProvider = null;
279+
PaintingBinding.instance.imageCache.clear();
253280
});
254281

255282
testWidgets('header and footer hidden and shown by tapping image', (tester) async {
@@ -271,9 +298,97 @@ void main() {
271298
tester.widget(find.byType(BottomAppBar));
272299

273300
debugNetworkImageHttpClientProvider = null;
301+
PaintingBinding.instance.imageCache.clear();
302+
});
303+
304+
// FAILS
305+
// testWidgets('thumbnail shown first, then main image replaces it', (tester) async {
306+
// final httpClient = FakeImageHttpClient();
307+
// debugNetworkImageHttpClientProvider = () => httpClient;
308+
// httpClient.requests[src] = FakeImageHttpClientRequest()
309+
// ..response.statusCode = HttpStatus.ok
310+
// ..response.content = kSolidBlueLargeAvatar;
311+
// httpClient.requests[thumbnail] = FakeImageHttpClientRequest()
312+
// ..response.statusCode = HttpStatus.ok
313+
// ..response.content = kSolidBlueAvatar;
314+
315+
// final message = eg.streamMessage(sender: eg.otherUser);
316+
// await setupPage(
317+
// tester,
318+
// message: message,
319+
// thumbnailUrl: thumbnail,
320+
// originalWidth: 1000,
321+
// originalHeight: 1000);
322+
323+
// // Both visible
324+
// tester.widget(find.byWidgetPredicate(
325+
// (widget) => widget is RealmContentNetworkImage && widget.src == thumbnail));
326+
// tester.widget(find.byWidgetPredicate(
327+
// (widget) => widget is RealmContentNetworkImage && widget.src == src));
328+
329+
// print('pump');
330+
// await tester.pump();
331+
// print('done pump');
332+
// tester.widgetList<RealmContentNetworkImage>(find.byType(RealmContentNetworkImage)).forEach((w) => print(w.src));
333+
334+
// print('pump');
335+
// await tester.pump();
336+
// print('done pump');
337+
// tester.widgetList<RealmContentNetworkImage>(find.byType(RealmContentNetworkImage)).forEach((w) => print(w.src));
338+
339+
// // TODO verify only src visible
340+
341+
// debugNetworkImageHttpClientProvider = null;
342+
// PaintingBinding.instance.imageCache.clear();
343+
// });
344+
345+
testWidgets('image is scaled down to fit', (tester) async {
346+
tester.view.physicalSize = const Size(800, 600);
347+
tester.view.devicePixelRatio = 1.0;
348+
prepareBoringImageHttpClient(content: kSolidBlueLargeAvatar);
349+
final message = eg.streamMessage(sender: eg.otherUser);
350+
await setupPage(tester, message: message, thumbnailUrl: null);
351+
352+
// Ensure only src image is visible
353+
tester.widgetList(find.descendant(
354+
of: find.byType(RealmContentNetworkImage),
355+
matching: find.byType(RawImage)),
356+
).single;
357+
358+
// Image is 1000x1000, but widget should fit the view size.
359+
final renderImage = tester.renderObject<RenderImage>(find.descendant(
360+
of: find.byType(RealmContentNetworkImage),
361+
matching: find.byType(RawImage)));
362+
check(renderImage.size).equals(tester.view.physicalSize);
363+
364+
debugNetworkImageHttpClientProvider = null;
365+
PaintingBinding.instance.imageCache.clear();
274366
});
275367

276-
// TODO test _CopyLinkButton
368+
// FAILS
369+
// testWidgets('image is smaller than view size, it doesn\'t scale up', (tester) async {
370+
// tester.view.physicalSize = const Size(800, 600);
371+
// tester.view.devicePixelRatio = 1.0;
372+
// prepareBoringImageHttpClient(content: kSolidBlueAvatar);
373+
// final message = eg.streamMessage(sender: eg.otherUser);
374+
// await setupPage(tester, message: message, thumbnailUrl: null);
375+
376+
// // Ensure only src image is visible
377+
// tester.widgetList(find.descendant(
378+
// of: find.byType(RealmContentNetworkImage),
379+
// matching: find.byType(RawImage)),
380+
// ).single;
381+
382+
// // Image is 100x100, it should stay that size and shouldn't scale up.
383+
// final renderImage = tester.renderObject<RenderImage>(find.descendant(
384+
// of: find.byType(RealmContentNetworkImage),
385+
// matching: find.byType(RawImage)));
386+
// check(renderImage.size).equals(const Size(100, 100));
387+
388+
// debugNetworkImageHttpClientProvider = null;
389+
// PaintingBinding.instance.imageCache.clear();
390+
// });
391+
277392
// TODO test thumbnail gets shown, then gets replaced when main image loads
278393
// TODO test image is scaled down to fit, but not up
279394
// TODO test image doesn't change size when header and footer hidden/shown

0 commit comments

Comments
 (0)