The image fills the 94% viewer with a transparent letterbox, so taps on the dimmed canvas beside the image hit the viewer and did nothing — only the 6% margin dismissed. Add onTapUp that dismisses when the tap lands outside the actual painted-image rect (computed from the RenderImage + applyBoxFit, transformed for any zoom/pan). Double-tap reset, scroll zoom, pan, Esc, and the close button are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
101 lines
3.9 KiB
Dart
101 lines
3.9 KiB
Dart
/// T-252: ClideLightbox — full-screen zoom/pan overlay primitive.
|
||
library;
|
||
|
||
import 'package:clide/widgets/widgets.dart';
|
||
import 'package:flutter/gestures.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:flutter/widgets.dart';
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
|
||
import '../helpers/kernel_fixture.dart';
|
||
import '../helpers/widget_harness.dart';
|
||
|
||
bool _textHas(Object? w, String s) => w is ClideText && w.data.contains(s);
|
||
|
||
void main() {
|
||
late KernelFixture f;
|
||
setUp(() async => f = await KernelFixture.create());
|
||
tearDown(() => f.dispose());
|
||
|
||
Widget box(VoidCallback onDismiss) => MediaQuery(
|
||
data: const MediaQueryData(size: Size(800, 600)),
|
||
child: ClideLightbox(onDismiss: onDismiss, child: const SizedBox(width: 200, height: 150)),
|
||
);
|
||
|
||
testWidgets('renders the zoom hint and an InteractiveViewer', (tester) async {
|
||
await tester.pumpWidget(harness(f, box(() {})));
|
||
await tester.pumpAndSettle();
|
||
expect(find.byType(InteractiveViewer), findsOneWidget);
|
||
expect(find.byWidgetPredicate((w) => _textHas(w, 'Esc to close')), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('Esc dismisses', (tester) async {
|
||
var dismissed = false;
|
||
await tester.pumpWidget(harness(f, box(() => dismissed = true)));
|
||
await tester.pumpAndSettle();
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||
await tester.pumpAndSettle();
|
||
expect(dismissed, isTrue);
|
||
});
|
||
|
||
testWidgets('the close button dismisses', (tester) async {
|
||
var dismissed = false;
|
||
await tester.pumpWidget(harness(f, box(() => dismissed = true)));
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.ancestor(of: find.byType(ClideIcon), matching: find.byType(GestureDetector)));
|
||
await tester.pumpAndSettle();
|
||
expect(dismissed, isTrue);
|
||
});
|
||
|
||
testWidgets('double-tap resets without dismissing', (tester) async {
|
||
var dismissed = false;
|
||
await tester.pumpWidget(harness(f, box(() => dismissed = true)));
|
||
await tester.pumpAndSettle();
|
||
final viewer = find.byType(InteractiveViewer);
|
||
await tester.tap(viewer);
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
await tester.tap(viewer);
|
||
await tester.pumpAndSettle();
|
||
expect(dismissed, isFalse);
|
||
expect(find.byType(InteractiveViewer), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('a tap on the dimmed canvas dismisses; a tap on the image does not (T-309)', (tester) async {
|
||
final img = (await tester.runAsync(() => createTestImage(width: 100, height: 100)))!; // 1:1; real async
|
||
var dismissed = false;
|
||
await tester.pumpWidget(harness(
|
||
f,
|
||
MediaQuery(
|
||
data: const MediaQueryData(size: Size(800, 600)),
|
||
child: ClideLightbox(onDismiss: () => dismissed = true, child: RawImage(image: img, fit: BoxFit.contain)),
|
||
),
|
||
));
|
||
await tester.pumpAndSettle();
|
||
|
||
// The 94% box is ~752×564; a 1:1 image fits to 564×564 centred, leaving
|
||
// ~94px side margins. A tap in the left margin is dimmed canvas → dismiss.
|
||
final r = tester.getRect(find.byType(InteractiveViewer));
|
||
await tester.tapAt(Offset(r.left + 10, r.center.dy));
|
||
await tester.pump(const Duration(milliseconds: 350)); // past the double-tap delay
|
||
expect(dismissed, isTrue);
|
||
|
||
// A tap on the image itself does not dismiss.
|
||
dismissed = false;
|
||
await tester.tapAt(r.center);
|
||
await tester.pump(const Duration(milliseconds: 350));
|
||
expect(dismissed, isFalse);
|
||
});
|
||
|
||
testWidgets('scroll wheel zooms in and out', (tester) async {
|
||
await tester.pumpWidget(harness(f, box(() {})));
|
||
await tester.pumpAndSettle();
|
||
final center = tester.getCenter(find.byType(InteractiveViewer));
|
||
final pointer = TestPointer(1, PointerDeviceKind.mouse)..hover(center);
|
||
await tester.sendEventToBinding(pointer.scroll(const Offset(0, -100))); // in
|
||
await tester.pumpAndSettle();
|
||
await tester.sendEventToBinding(pointer.scroll(const Offset(0, 100))); // out
|
||
await tester.pumpAndSettle();
|
||
expect(find.byType(InteractiveViewer), findsOneWidget);
|
||
});
|
||
}
|