Files
IKAR/test/attachment_gallery_test.dart
T

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;
}
}