200 lines
7.3 KiB
Dart
200 lines
7.3 KiB
Dart
import 'dart:ui' as ui;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import 'package:sxchnge_analytics/sxchnge_analytics.dart';
|
|
|
|
import 'package:sxchnge_kyc/sxchnge_kyc.dart';
|
|
|
|
import 'package:sxchnge_app/main.dart';
|
|
import 'package:sxchnge_app/providers/locale.dart';
|
|
|
|
class Photos implements PhotoService {
|
|
Photos(this.bytes);
|
|
final Uint8List bytes;
|
|
KycError? error;
|
|
@override
|
|
Future<Photo?> pick() async {
|
|
if (error != null) throw KycException(error!);
|
|
return Photo(bytes);
|
|
}
|
|
}
|
|
|
|
Future<void> tap(WidgetTester t, Finder finder) async {
|
|
await t.ensureVisible(finder);
|
|
await t.tap(finder);
|
|
await t.pumpAndSettle();
|
|
}
|
|
|
|
void main() {
|
|
late Uint8List bytes;
|
|
setUpAll(() async {
|
|
final r = ui.PictureRecorder();
|
|
final c = ui.Canvas(r);
|
|
c.drawRect(const Rect.fromLTWH(0, 0, 600, 600), Paint());
|
|
final pic = r.endRecording();
|
|
final image = await pic.toImage(600, 600);
|
|
bytes = (await image.toByteData(format: ui.ImageByteFormat.png))!.buffer
|
|
.asUint8List();
|
|
image.dispose();
|
|
pic.dispose();
|
|
});
|
|
Future<DummyIdentityService> open(
|
|
WidgetTester t, {
|
|
Photos? photos,
|
|
Locale? locale,
|
|
}) async {
|
|
t.platformDispatcher.defaultRouteNameTestValue = '/identity';
|
|
addTearDown(t.platformDispatcher.clearDefaultRouteNameTestValue);
|
|
final service = DummyIdentityService(delay: Duration.zero);
|
|
final provider = LocaleProvider();
|
|
if (locale != null) await provider.setLocale(locale);
|
|
await t.pumpWidget(
|
|
SxchngeApp(
|
|
identityService: service,
|
|
photoService: photos ?? Photos(bytes),
|
|
localeProvider: provider,
|
|
),
|
|
);
|
|
await t.pumpAndSettle();
|
|
return service;
|
|
}
|
|
|
|
Future<void> submit(WidgetTester t) async {
|
|
await tap(t, find.byKey(const ValueKey('kyc-start')));
|
|
await tap(t, find.byKey(const ValueKey('pick-passport')));
|
|
await tap(t, find.byKey(const ValueKey('kyc-next')));
|
|
await tap(t, find.byKey(const ValueKey('pick-selfie')));
|
|
await tap(t, find.byKey(const ValueKey('kyc-next')));
|
|
await tap(t, find.byType(CheckboxListTile));
|
|
await tap(t, find.byKey(const ValueKey('kyc-submit')));
|
|
}
|
|
|
|
testWidgets('two photos, review, pending and approval with service limits', (
|
|
t,
|
|
) async {
|
|
await open(t);
|
|
expect(find.byKey(const ValueKey('kyc-unverified')), findsOneWidget);
|
|
await submit(t);
|
|
expect(find.byKey(const ValueKey('kyc-pending')), findsOneWidget);
|
|
await tap(t, find.byType(ExpansionTile));
|
|
await tap(t, find.text('Approve'));
|
|
expect(find.byKey(const ValueKey('kyc-approved')), findsOneWidget);
|
|
expect(find.text('Verified'), findsOneWidget);
|
|
expect(t.takeException(), null);
|
|
});
|
|
testWidgets('denial provides explanation and support, no resubmit', (
|
|
t,
|
|
) async {
|
|
await open(t);
|
|
await submit(t);
|
|
await tap(t, find.byType(ExpansionTile));
|
|
await tap(t, find.text('Decline'));
|
|
expect(find.byKey(const ValueKey('kyc-denied')), findsOneWidget);
|
|
expect(find.byKey(const ValueKey('kyc-start')), findsNothing);
|
|
await tap(t, find.text('Contact support'));
|
|
expect(find.byType(AlertDialog), findsOneWidget);
|
|
});
|
|
testWidgets('correctable rejection can be resubmitted', (t) async {
|
|
await open(t);
|
|
await submit(t);
|
|
await tap(t, find.byType(ExpansionTile));
|
|
await tap(t, find.text('Request clearer passport'));
|
|
expect(find.byKey(const ValueKey('kyc-changesRequested')), findsOneWidget);
|
|
await submit(t);
|
|
expect(find.byKey(const ValueKey('kyc-pending')), findsOneWidget);
|
|
});
|
|
testWidgets('picker error recovery and discard confirmation', (t) async {
|
|
final photos = Photos(bytes)..error = KycError.permission;
|
|
await open(t, photos: photos);
|
|
await tap(t, find.byKey(const ValueKey('kyc-start')));
|
|
await tap(t, find.byKey(const ValueKey('pick-passport')));
|
|
expect(find.byKey(const ValueKey('kyc-error')), findsOneWidget);
|
|
photos.error = null;
|
|
await tap(t, find.byKey(const ValueKey('pick-passport')));
|
|
await tap(t, find.text('Keep basic limits'));
|
|
expect(find.byType(AlertDialog), findsOneWidget);
|
|
await tap(t, find.text('Keep editing'));
|
|
expect(find.byKey(const ValueKey('pick-passport')), findsOneWidget);
|
|
await tap(t, find.text('Keep basic limits'));
|
|
await tap(t, find.text('Discard & leave'));
|
|
expect(find.byKey(const ValueKey('kyc-unverified')), findsOneWidget);
|
|
});
|
|
testWidgets(
|
|
'320px Russian at large text works through submission and keyboard',
|
|
(t) async {
|
|
t.view.physicalSize = const Size(320, 900);
|
|
t.view.devicePixelRatio = 1;
|
|
t.platformDispatcher.textScaleFactorTestValue = 1.6;
|
|
addTearDown(t.view.resetPhysicalSize);
|
|
addTearDown(t.view.resetDevicePixelRatio);
|
|
addTearDown(t.platformDispatcher.clearTextScaleFactorTestValue);
|
|
await open(t, locale: const Locale('ru'));
|
|
expect(find.text('Верификация и лимиты'), findsOneWidget);
|
|
await t.sendKeyEvent(LogicalKeyboardKey.tab);
|
|
await t.pump();
|
|
await submit(t);
|
|
expect(find.byKey(const ValueKey('kyc-pending')), findsOneWidget);
|
|
expect(t.takeException(), null);
|
|
},
|
|
);
|
|
testWidgets('status failure exposes retry without asserting basic state', (
|
|
t,
|
|
) async {
|
|
t.platformDispatcher.defaultRouteNameTestValue = '/identity';
|
|
addTearDown(t.platformDispatcher.clearDefaultRouteNameTestValue);
|
|
final service = DummyIdentityService(delay: Duration.zero)
|
|
..nextError = KycError.unavailable;
|
|
await t.pumpWidget(
|
|
SxchngeApp(
|
|
identityService: service,
|
|
photoService: Photos(bytes),
|
|
analyticsService: DummyAnalyticsService(),
|
|
),
|
|
);
|
|
await t.pumpAndSettle();
|
|
expect(find.byKey(const ValueKey('kyc-unverified')), findsNothing);
|
|
await tap(t, find.text('Refresh status & limits'));
|
|
expect(find.byKey(const ValueKey('kyc-unverified')), findsOneWidget);
|
|
});
|
|
testWidgets('route guard preserves draft when navigation is cancelled', (
|
|
t,
|
|
) async {
|
|
await open(t);
|
|
await tap(t, find.byKey(const ValueKey('kyc-start')));
|
|
await tap(t, find.byKey(const ValueKey('pick-passport')));
|
|
final router = GoRouter.of(t.element(find.text('Verification & limits')));
|
|
router.go('/');
|
|
await t.pumpAndSettle();
|
|
expect(find.byType(AlertDialog), findsOneWidget);
|
|
await tap(t, find.text('Keep editing'));
|
|
expect(find.byKey(const ValueKey('pick-passport')), findsOneWidget);
|
|
router.go('/');
|
|
await t.pumpAndSettle();
|
|
await tap(t, find.text('Discard & leave'));
|
|
expect(find.byKey(const ValueKey('kyc-start')), findsNothing);
|
|
expect(find.text('Explore your accounts'), findsOneWidget);
|
|
});
|
|
testWidgets('overview controls have accessible labels and touch targets', (
|
|
t,
|
|
) async {
|
|
final semantics = t.ensureSemantics();
|
|
await open(t);
|
|
expect(t, meetsGuideline(labeledTapTargetGuideline));
|
|
expect(t, meetsGuideline(androidTapTargetGuideline));
|
|
final start = find.byKey(const ValueKey('kyc-start'));
|
|
await t.ensureVisible(start);
|
|
final buttonContext = t.element(start);
|
|
FocusScope.of(buttonContext).requestFocus(Focus.of(buttonContext));
|
|
await t.sendKeyEvent(LogicalKeyboardKey.tab);
|
|
await t.pump();
|
|
expect(t.takeException(), null);
|
|
semantics.dispose();
|
|
});
|
|
}
|