initial commit: LeafViewer, LeafZoomable, LeafContextMenu, LeafPopover, LeafImage, misc widgets + tests

This commit is contained in:
foxyz committed 2026-09-02 21:58:54 +03:00
commit 9bb0ff9187
156 files changed
+9887

No files matched your search

+120
View File
@@ -0,0 +1,120 @@
import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:leaf_widgets/leaf_widgets.dart';
Future<void> _openMenu(WidgetTester tester) async {
final gesture = await tester.startGesture(
tester.getCenter(find.byType(LeafContextMenu)),
kind: PointerDeviceKind.mouse,
buttons: kSecondaryButton,
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 200)); // entrance animation
await gesture.up();
}
Widget _host(List<ContextMenuEntry> entries) => MaterialApp(
home: Scaffold(
body: Center(
child: LeafContextMenu(entries: entries, child: const Text('trigger')),
),
),
);
void main() {
testWidgets('async action: spinner → check, menu locked while running, auto-dismiss on success', (tester) async {
final completer = Completer<void>();
await tester.pumpWidget(_host([ContextMenuAsyncAction(label: 'Copy image', task: (_) => completer.future)]));
await _openMenu(tester);
expect(find.text('Copy image'), findsOneWidget);
await tester.tap(find.text('Copy image'));
await tester.pump();
expect(find.text('Copying…'), findsOneWidget);
expect(find.byType(CircularProgressIndicator), findsOneWidget);
// Outside taps are ignored while the task runs.
await tester.tapAt(const Offset(5, 5));
await tester.pump();
expect(find.text('Copying…'), findsOneWidget);
completer.complete();
await tester.pump();
expect(find.text('Copied'), findsOneWidget);
expect(find.byIcon(Icons.check), findsOneWidget);
await tester.pump(const Duration(milliseconds: 700)); // doneDuration
await tester.pump(const Duration(milliseconds: 150)); // exit animation
expect(find.text('Copied'), findsNothing);
});
testWidgets('async action: reported progress drives a determinate bar', (tester) async {
final completer = Completer<void>();
await tester.pumpWidget(_host([
ContextMenuAsyncAction(label: 'Copy image', task: (onProgress) {
onProgress?.call(0.5);
return completer.future;
}),
]));
await _openMenu(tester);
await tester.tap(find.text('Copy image'));
await tester.pump();
final bar = tester.widget<LinearProgressIndicator>(find.byType(LinearProgressIndicator));
expect(bar.value, 0.5);
completer.complete();
await tester.pump();
await tester.pump(const Duration(milliseconds: 900));
});
testWidgets('async action: failure shows error label and calls onError', (tester) async {
Object? seen;
await tester.pumpWidget(_host([
ContextMenuAsyncAction(
label: 'Copy image',
task: (_) async => throw StateError('boom'),
onError: (e, _) => seen = e,
),
]));
await _openMenu(tester);
await tester.tap(find.text('Copy image'));
await tester.pump();
expect(find.text('Failed'), findsOneWidget);
expect(find.byIcon(Icons.error_outline), findsOneWidget);
expect(seen, isA<StateError>());
await tester.pump(const Duration(milliseconds: 1400)); // errorDuration
await tester.pump(const Duration(milliseconds: 150)); // exit animation
expect(find.text('Failed'), findsNothing);
});
testWidgets('other entries are disabled while an async action runs', (tester) async {
final completer = Completer<void>();
var syncTaps = 0;
await tester.pumpWidget(_host([
ContextMenuAction(label: 'Sync item', onTap: () => syncTaps++),
ContextMenuAsyncAction(label: 'Copy image', task: (_) => completer.future),
]));
await _openMenu(tester);
await tester.tap(find.text('Copy image'));
await tester.pump();
await tester.tap(find.text('Sync item'));
await tester.pump();
expect(syncTaps, 0);
expect(find.text('Copying…'), findsOneWidget);
completer.complete();
await tester.pump();
await tester.pump(const Duration(milliseconds: 900));
});
}