Files

237 lines
8.6 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/kyc/country.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 t.enterText(find.byKey(const ValueKey('kyc-name')), 'Example Person');
await t.enterText(find.byKey(const ValueKey('kyc-birth')), '1990-04-23');
await tap(
t,
find.descendant(
of: find.byKey(const ValueKey('kyc-country')),
matching: find.byType(OutlinedButton),
),
);
await t.enterText(find.byKey(const ValueKey('country-search')), 'PL');
await t.pumpAndSettle();
await tap(t, find.byType(ListTile));
expect(
t.widget<CountryField>(find.byKey(const ValueKey('kyc-country'))).value,
'PL',
);
await t.pumpAndSettle();
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('optional onboarding can be skipped before collecting proofs', (
t,
) async {
t.platformDispatcher.defaultRouteNameTestValue = '/identity?onboarding=1';
addTearDown(t.platformDispatcher.clearDefaultRouteNameTestValue);
await t.pumpWidget(
SxchngeApp(
identityService: DummyIdentityService(delay: Duration.zero),
photoService: Photos(bytes),
),
);
await t.pumpAndSettle();
final router = GoRouter.of(
t.element(find.byKey(const ValueKey('kyc-start'))),
);
await tap(t, find.text('Keep basic limits'));
expect(router.routeInformationProvider.value.uri.path, '/accounts');
expect(find.byType(AlertDialog), findsNothing);
});
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();
});
}