242 lines
8.2 KiB
Dart
242 lines
8.2 KiB
Dart
import 'dart:io';
|
|
import 'dart:async';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
// ignore: depend_on_referenced_packages
|
|
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
|
|
import 'package:material_symbols_icons/symbols.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:ikar/core/media/gallery_source.dart';
|
|
import 'package:ikar/frontend/widgets/attachment/attachment_sheet.dart';
|
|
import 'package:ikar/l10n/app_localizations.dart';
|
|
|
|
class FakeGallery implements GallerySource {
|
|
FakeGallery({
|
|
this.permission = GalleryPermission.granted,
|
|
this.fail = false,
|
|
this.count = 8,
|
|
this.hang = false,
|
|
});
|
|
GalleryPermission permission;
|
|
int requests = 0;
|
|
int settingsOpened = 0;
|
|
final bool fail;
|
|
final int count;
|
|
final bool hang;
|
|
@override
|
|
Future<GalleryPermission> ensurePermission() async {
|
|
requests++;
|
|
return permission;
|
|
}
|
|
|
|
@override
|
|
Future<GalleryPermission> currentPermission() async => permission;
|
|
@override
|
|
Future<GalleryPage> load({int offset = 0, int limit = 120}) async {
|
|
if (hang) return Completer<GalleryPage>().future;
|
|
if (fail) throw StateError('gallery unavailable');
|
|
return GalleryPage(
|
|
items: List.generate(
|
|
count,
|
|
(i) => GalleryItem.fromFile(File('missing-$i.jpg')),
|
|
),
|
|
hasMore: false,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<void> openSettings() async {
|
|
settingsOpened++;
|
|
}
|
|
|
|
@override
|
|
Future<void> manageAccess() async {}
|
|
}
|
|
|
|
void main() {
|
|
setUp(
|
|
() => SharedPreferences.setMockInitialValues({
|
|
'attachment_gallery_access_explained_v1': true,
|
|
}),
|
|
);
|
|
Future<void> openSheet(WidgetTester tester, FakeGallery source) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
locale: const Locale('ru'),
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: Scaffold(body: AttachmentSheet(gallerySource: source)),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 300));
|
|
}
|
|
|
|
for (final allow in [true, false]) {
|
|
testWidgets('first gallery opening explains access, allow=$allow', (
|
|
tester,
|
|
) async {
|
|
SharedPreferences.setMockInitialValues({});
|
|
final source = FakeGallery(permission: GalleryPermission.denied);
|
|
await openSheet(tester, source);
|
|
await tester.pump(const Duration(milliseconds: 300));
|
|
expect(find.byType(AlertDialog), findsOneWidget);
|
|
expect(source.requests, 0);
|
|
final dialog = find.byType(AlertDialog);
|
|
await tester.tap(
|
|
find.descendant(
|
|
of: dialog,
|
|
matching: find.byType(allow ? FilledButton : TextButton),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(source.requests, allow ? 1 : 0);
|
|
expect(find.byType(AlertDialog), findsNothing);
|
|
expect(
|
|
(await SharedPreferences.getInstance()).getBool(
|
|
'attachment_gallery_access_explained_v1',
|
|
),
|
|
isTrue,
|
|
);
|
|
});
|
|
}
|
|
testWidgets('full access skips introduction and permission request', (
|
|
tester,
|
|
) async {
|
|
SharedPreferences.setMockInitialValues({});
|
|
final source = FakeGallery();
|
|
await openSheet(tester, source);
|
|
expect(find.byType(AlertDialog), findsNothing);
|
|
expect(source.requests, 0);
|
|
});
|
|
testWidgets(
|
|
'empty limited gallery retains settings action and refreshes on return',
|
|
(tester) async {
|
|
final source = FakeGallery(
|
|
permission: GalleryPermission.limited,
|
|
count: 0,
|
|
);
|
|
await openSheet(tester, source);
|
|
expect(
|
|
find.textContaining('Доступны только выбранные фото'),
|
|
findsOneWidget,
|
|
);
|
|
await tester.tap(find.text('Настройки'));
|
|
expect(source.settingsOpened, 1);
|
|
source.permission = GalleryPermission.granted;
|
|
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive);
|
|
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed);
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
find.textContaining('Доступны только выбранные фото'),
|
|
findsNothing,
|
|
);
|
|
},
|
|
);
|
|
testWidgets(
|
|
'system picker sends chosen photos when gallery access is denied',
|
|
(tester) async {
|
|
final original = ImagePickerPlatform.instance;
|
|
final picker = FakePicker()..files = [XFile('one.jpg'), XFile('two.jpg')];
|
|
ImagePickerPlatform.instance = picker;
|
|
addTearDown(() => ImagePickerPlatform.instance = original);
|
|
List<PickedPhoto>? sent;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
locale: const Locale('ru'),
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: Scaffold(
|
|
body: Builder(
|
|
builder: (context) => TextButton(
|
|
onPressed: () => showModalBottomSheet<void>(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
builder: (_) => AttachmentSheet(
|
|
gallerySource: FakeGallery(
|
|
permission: GalleryPermission.denied,
|
|
),
|
|
onSend: (photos, caption) => sent = photos,
|
|
),
|
|
),
|
|
child: const Text('open'),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.tap(find.text('open'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.widgetWithText(TextButton, 'Камера'));
|
|
await tester.pumpAndSettle();
|
|
expect(picker.source, ImageSource.camera);
|
|
expect(find.byType(AttachmentSheet), findsOneWidget);
|
|
await tester.tap(find.widgetWithText(TextButton, 'Галерея'));
|
|
await tester.pumpAndSettle();
|
|
expect(tester.takeException(), isNull);
|
|
await tester.tap(find.byIcon(Symbols.send));
|
|
await tester.pumpAndSettle();
|
|
expect(sent?.map((photo) => photo.item.localFile?.path), [
|
|
'one.jpg',
|
|
'two.jpg',
|
|
]);
|
|
expect(find.byType(AttachmentSheet), findsNothing);
|
|
},
|
|
);
|
|
for (final scenario in ['photos', 'empty', 'denied', 'error', 'timeout']) {
|
|
testWidgets(
|
|
'attachment gallery $scenario keeps camera visible and finite',
|
|
(tester) async {
|
|
await tester.binding.setSurfaceSize(const Size(400, 850));
|
|
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
locale: const Locale('ru'),
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: Scaffold(
|
|
body: AttachmentSheet(
|
|
gallerySource: FakeGallery(
|
|
permission: scenario == 'denied'
|
|
? GalleryPermission.denied
|
|
: GalleryPermission.granted,
|
|
fail: scenario == 'error',
|
|
hang: scenario == 'timeout',
|
|
count: scenario == 'empty' ? 0 : 8,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 200));
|
|
if (scenario == 'timeout') {
|
|
expect(find.widgetWithText(TextButton, 'Камера'), findsOneWidget);
|
|
await tester.pump(const Duration(seconds: 21));
|
|
await tester.pump();
|
|
expect(find.text('Не удалось загрузить галерею'), findsOneWidget);
|
|
}
|
|
expect(tester.takeException(), isNull);
|
|
expect(find.widgetWithText(TextButton, 'Камера'), findsOneWidget);
|
|
expect(find.widgetWithText(TextButton, 'Галерея'), findsOneWidget);
|
|
expect(find.byType(CustomScrollView), findsOneWidget);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class FakePicker extends ImagePickerPlatform {
|
|
List<XFile> files = [];
|
|
ImageSource? source;
|
|
@override
|
|
Future<List<XFile>> getMedia({required MediaOptions options}) async => files;
|
|
@override
|
|
Future<XFile?> getImageFromSource({
|
|
required ImageSource source,
|
|
ImagePickerOptions options = const ImagePickerOptions(),
|
|
}) async {
|
|
this.source = source;
|
|
return null;
|
|
}
|
|
}
|