Compare commits
2
Commits
f3ff7fc016
...
kyc-67
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
288a2f0c02 | ||
|
|
4605a197ad |
No files matched your search
@@ -211,3 +211,51 @@ smoke testing on the local demo opened the selector, searched for Poland and
|
||||
confirmed that the profile displays Poland after selection. Focused and full
|
||||
journey tests verify the stored `PL` code. Generated localization/build outputs
|
||||
remain ignored and uncommitted.
|
||||
|
||||
## Birthday entry — task #67
|
||||
|
||||
Task [#67](https://git.sendico.io/tech/sxchnge/issues/67), branch `kyc-67`.
|
||||
Delivery: [PR #70](https://git.sendico.io/tech/sxchnge/pulls/70).
|
||||
This change is stacked on the residence-country selector in PR #65; merge #65
|
||||
before the birthday PR.
|
||||
|
||||
The profile uses labeled Day, Month and Year controls. Day and year accept numeric
|
||||
entry with birthday autofill hints; month selection uses localized month names
|
||||
and standard Material keyboard/touch interaction. Customers can enter their birth
|
||||
year directly. No calendar paging or transport-format punctuation is required.
|
||||
The controls share a row when space allows and stack at narrow widths or enlarged
|
||||
text sizes. English and Russian labels, month names and errors follow the app locale.
|
||||
|
||||
Valid calendar dates are normalized to the existing `IdentityProfile.dateOfBirth`
|
||||
`YYYY-MM-DD` value through `IdentityProvider.updateProfile`. Partial, impossible
|
||||
and future dates clear that profile value so Continue cannot retain an earlier
|
||||
valid birthday. Local input components remain visible while correcting a date;
|
||||
complete invalid dates receive a specific live-region error below the group.
|
||||
Leap years, zero day/year and calendar rollover are checked. Today's date follows
|
||||
the existing model policy; no minimum age or arbitrary oldest birth year is added.
|
||||
Prefilled dates, locale changes and external profile replacement synchronize the
|
||||
controls. Back navigation restores the valid saved date. Busy state disables all
|
||||
three controls. Controllers are disposed with the widget.
|
||||
|
||||
The existing DTO, mapper, service, domain model and provider boundaries are retained.
|
||||
No dependency, transport or backend change is needed. This remains part of the KYC
|
||||
preparation-to-submission journey defined above: existing acceptance, failure,
|
||||
retry and abandonment signals apply, without new field events or date collection.
|
||||
The existing server-side review measurement gaps remain unchanged.
|
||||
|
||||
UX review and automated checks cover entry/correction, leap dates, invalid and
|
||||
future-date recovery, numeric restrictions, prefill/replacement/clearing, locale
|
||||
switching, disabled state, Tab/Enter/arrow-key month selection, Escape cancellation,
|
||||
and Russian controls/errors at 320px with 1.6 text scale. Labeled and minimum-size
|
||||
tap-target guidelines pass. Integration checks verify that invalid dates block
|
||||
Continue, Back preserves the birthday, and EN/RU flows still reach submission
|
||||
acceptance. Native screen-reader and mobile keyboard/autofill testing remain
|
||||
release checks; widget semantics are not a claim of device-level validation.
|
||||
|
||||
Validation: all four changed Dart files pass the format check; `flutter analyze`
|
||||
in `frontend/app` reports no issues; `make test-frontend` passes all 230 tests,
|
||||
including the seven new birthday widget tests and the new navigation/gating test.
|
||||
`flutter build web` passes, including its Wasm compatibility dry run. Browser
|
||||
smoke testing of the release build entered 23 April 1990, verified the inline
|
||||
error for 31 April and restored 23 April without losing month or year.
|
||||
Generated localization and build outputs remain ignored and uncommitted.
|
||||
@@ -0,0 +1,206 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../generated/i18n/app_localizations.dart';
|
||||
|
||||
/// Birthday entry with a canonical date string at the profile boundary.
|
||||
class BirthField extends StatefulWidget {
|
||||
const BirthField({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
final String value;
|
||||
final ValueChanged<String> onChanged;
|
||||
final bool enabled;
|
||||
|
||||
@override
|
||||
State<BirthField> createState() => _BirthFieldState();
|
||||
}
|
||||
|
||||
class _BirthFieldState extends State<BirthField> {
|
||||
final _day = TextEditingController();
|
||||
final _year = TextEditingController();
|
||||
final _monthField = GlobalKey<FormFieldState<int>>();
|
||||
int? _month;
|
||||
late String _emitted;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_readValue();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(BirthField oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.value != _emitted) {
|
||||
_readValue();
|
||||
_monthField.currentState?.didChange(_month);
|
||||
}
|
||||
}
|
||||
|
||||
void _readValue() {
|
||||
_emitted = widget.value;
|
||||
final parts = RegExp(r'^(\d{4})-(\d{2})-(\d{2})$').firstMatch(widget.value);
|
||||
_year.text = parts?.group(1) ?? '';
|
||||
final month = int.tryParse(parts?.group(2) ?? '');
|
||||
_month = month != null && month >= 1 && month <= 12 ? month : null;
|
||||
_day.text = parts?.group(3) ?? '';
|
||||
}
|
||||
|
||||
DateTime? get _date {
|
||||
final day = int.tryParse(_day.text);
|
||||
final year = int.tryParse(_year.text);
|
||||
if (day == null ||
|
||||
year == null ||
|
||||
year < 1 ||
|
||||
_year.text.length != 4 ||
|
||||
_month == null) {
|
||||
return null;
|
||||
}
|
||||
final date = DateTime(year, _month!, day);
|
||||
return date.year == year && date.month == _month && date.day == day
|
||||
? date
|
||||
: null;
|
||||
}
|
||||
|
||||
bool _future(DateTime date) =>
|
||||
date.isAfter(DateUtils.dateOnly(DateTime.now()));
|
||||
|
||||
void _change() {
|
||||
setState(() {});
|
||||
final date = _date;
|
||||
_emitted = date == null || _future(date)
|
||||
? ''
|
||||
: '${_year.text}-${_month.toString().padLeft(2, '0')}-'
|
||||
'${_day.text.padLeft(2, '0')}';
|
||||
widget.onChanged(_emitted);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_day.dispose();
|
||||
_year.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l = AppLocalizations.of(context)!;
|
||||
final date = _date;
|
||||
final complete =
|
||||
_day.text.isNotEmpty && _month != null && _year.text.length == 4;
|
||||
final error = !complete
|
||||
? null
|
||||
: date == null
|
||||
? l.kycBirthInvalid
|
||||
: _future(date)
|
||||
? l.kycBirthFuture
|
||||
: null;
|
||||
final months = DateFormat.MMMM(Localizations.localeOf(context).toString());
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(l.kycBirth, style: Theme.of(context).textTheme.labelLarge),
|
||||
const SizedBox(height: 8),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final day = _number(
|
||||
key: 'birth-day',
|
||||
controller: _day,
|
||||
label: l.kycBirthDay,
|
||||
length: 2,
|
||||
autofill: AutofillHints.birthdayDay,
|
||||
);
|
||||
final month = DropdownButtonFormField<int>(
|
||||
key: _monthField,
|
||||
initialValue: _month,
|
||||
isExpanded: true,
|
||||
decoration: InputDecoration(labelText: l.kycBirthMonth),
|
||||
items: [
|
||||
for (var month = 1; month <= 12; month++)
|
||||
DropdownMenuItem(
|
||||
value: month,
|
||||
child: Text(months.format(DateTime(2000, month))),
|
||||
),
|
||||
],
|
||||
onChanged: !widget.enabled
|
||||
? null
|
||||
: (value) {
|
||||
_month = value;
|
||||
_change();
|
||||
},
|
||||
);
|
||||
final year = _number(
|
||||
key: 'birth-year',
|
||||
controller: _year,
|
||||
label: l.kycBirthYear,
|
||||
length: 4,
|
||||
autofill: AutofillHints.birthdayYear,
|
||||
);
|
||||
final scale = MediaQuery.textScalerOf(context).scale(16) / 16;
|
||||
if (constraints.maxWidth < 400 * scale) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
day,
|
||||
const SizedBox(height: 16),
|
||||
month,
|
||||
const SizedBox(height: 16),
|
||||
year,
|
||||
],
|
||||
);
|
||||
}
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(flex: 2, child: day),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(flex: 4, child: month),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(flex: 3, child: year),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
if (error != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Semantics(
|
||||
liveRegion: true,
|
||||
child: Text(
|
||||
error,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _number({
|
||||
required String key,
|
||||
required TextEditingController controller,
|
||||
required String label,
|
||||
required int length,
|
||||
required String autofill,
|
||||
}) => TextFormField(
|
||||
key: ValueKey(key),
|
||||
controller: controller,
|
||||
enabled: widget.enabled,
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.next,
|
||||
autofillHints: [autofill],
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
LengthLimitingTextInputFormatter(length),
|
||||
],
|
||||
decoration: InputDecoration(labelText: label),
|
||||
onChanged: (_) => _change(),
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:sxchnge_kyc/sxchnge_kyc.dart';
|
||||
|
||||
import 'birth.dart';
|
||||
import 'country.dart';
|
||||
|
||||
import '../generated/i18n/app_localizations.dart';
|
||||
@@ -37,17 +38,11 @@ class ProfileForm extends StatelessWidget {
|
||||
onChanged: (value) => update(name: value),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
BirthField(
|
||||
key: const ValueKey('kyc-birth'),
|
||||
initialValue: p.profile.dateOfBirth,
|
||||
value: p.profile.dateOfBirth,
|
||||
enabled: !p.busy,
|
||||
keyboardType: TextInputType.datetime,
|
||||
autofillHints: const [AutofillHints.birthday],
|
||||
decoration: InputDecoration(
|
||||
labelText: l.kycBirth,
|
||||
helperText: l.kycBirthHelp,
|
||||
),
|
||||
onChanged: (value) => update(birth: value.trim()),
|
||||
onChanged: (value) => update(birth: value),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
CountryField(
|
||||
|
||||
@@ -269,7 +269,11 @@
|
||||
"kycProfileHelp": "Enter your details exactly as shown on your identity document.",
|
||||
"kycFullName": "Full legal name",
|
||||
"kycBirth": "Date of birth",
|
||||
"kycBirthHelp": "YYYY-MM-DD, for example 1990-04-23",
|
||||
"kycBirthDay": "Day",
|
||||
"kycBirthMonth": "Month",
|
||||
"kycBirthYear": "Year",
|
||||
"kycBirthInvalid": "Enter a valid date of birth.",
|
||||
"kycBirthFuture": "Date of birth cannot be in the future.",
|
||||
"kycCountry": "Country of residence",
|
||||
"kycCountrySelect": "Select a country",
|
||||
"kycCountrySearch": "Search countries",
|
||||
|
||||
@@ -269,7 +269,11 @@
|
||||
"kycProfileHelp": "Укажите данные точно как в документе, удостоверяющем личность.",
|
||||
"kycFullName": "Полное имя по документу",
|
||||
"kycBirth": "Дата рождения",
|
||||
"kycBirthHelp": "ГГГГ-ММ-ДД, например 1990-04-23",
|
||||
"kycBirthDay": "День",
|
||||
"kycBirthMonth": "Месяц",
|
||||
"kycBirthYear": "Год",
|
||||
"kycBirthInvalid": "Укажите корректную дату рождения.",
|
||||
"kycBirthFuture": "Дата рождения не может быть в будущем.",
|
||||
"kycCountry": "Страна проживания",
|
||||
"kycCountrySelect": "Выберите страну",
|
||||
"kycCountrySearch": "Поиск страны",
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:sxchnge_theme/sxchnge_theme.dart';
|
||||
|
||||
import 'package:sxchnge_app/kyc/birth.dart';
|
||||
|
||||
import 'package:sxchnge_app/generated/i18n/app_localizations.dart';
|
||||
|
||||
void main() {
|
||||
final day = find.byKey(const ValueKey('birth-day'));
|
||||
final year = find.byKey(const ValueKey('birth-year'));
|
||||
final month = find.byType(DropdownButtonFormField<int>);
|
||||
|
||||
Future<void> mount(
|
||||
WidgetTester t,
|
||||
ValueNotifier<String> value, {
|
||||
Locale locale = const Locale('en'),
|
||||
bool enabled = true,
|
||||
}) async {
|
||||
await t.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: SxchngeTheme.light(),
|
||||
locale: locale,
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: Scaffold(
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: ValueListenableBuilder<String>(
|
||||
valueListenable: value,
|
||||
builder: (_, date, _) => BirthField(
|
||||
value: date,
|
||||
enabled: enabled,
|
||||
onChanged: (date) => value.value = date,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await t.pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> select(WidgetTester t, String name) async {
|
||||
await t.ensureVisible(month);
|
||||
await t.tap(month);
|
||||
await t.pumpAndSettle();
|
||||
await t.tap(find.text(name).last);
|
||||
await t.pumpAndSettle();
|
||||
}
|
||||
|
||||
testWidgets('enters and corrects a birthday without ISO syntax', (t) async {
|
||||
final value = ValueNotifier('');
|
||||
addTearDown(value.dispose);
|
||||
await mount(t, value);
|
||||
expect(find.text('Date of birth'), findsOneWidget);
|
||||
await t.enterText(day, '23');
|
||||
await select(t, 'April');
|
||||
expect(value.value, '');
|
||||
await t.enterText(year, '1990');
|
||||
await t.pumpAndSettle();
|
||||
expect(value.value, '1990-04-23');
|
||||
await t.enterText(day, '9');
|
||||
await t.pumpAndSettle();
|
||||
expect(value.value, '1990-04-09');
|
||||
await t.enterText(year, '19');
|
||||
await t.pumpAndSettle();
|
||||
expect(value.value, '');
|
||||
// A provider rebuild must retain the other components of an incomplete date.
|
||||
expect(t.widget<TextFormField>(day).controller!.text, '9');
|
||||
expect(find.text('April'), findsOneWidget);
|
||||
await t.enterText(year, '1985');
|
||||
await t.pumpAndSettle();
|
||||
expect(value.value, '1985-04-09');
|
||||
});
|
||||
|
||||
testWidgets('leap dates, impossible days and future dates recover inline', (
|
||||
t,
|
||||
) async {
|
||||
final value = ValueNotifier('2000-02-29');
|
||||
addTearDown(value.dispose);
|
||||
await mount(t, value);
|
||||
expect(find.text('February'), findsOneWidget);
|
||||
await t.enterText(year, '2001');
|
||||
await t.pumpAndSettle();
|
||||
expect(value.value, '');
|
||||
expect(find.text('Enter a valid date of birth.'), findsOneWidget);
|
||||
expect(t.widget<TextFormField>(day).controller!.text, '29');
|
||||
await t.enterText(year, '2004');
|
||||
await t.pumpAndSettle();
|
||||
expect(value.value, '2004-02-29');
|
||||
expect(find.text('Enter a valid date of birth.'), findsNothing);
|
||||
await t.enterText(day, '31');
|
||||
await select(t, 'April');
|
||||
expect(value.value, '');
|
||||
expect(find.text('Enter a valid date of birth.'), findsOneWidget);
|
||||
await t.enterText(day, '30');
|
||||
await t.enterText(year, '${DateTime.now().year + 1}');
|
||||
await t.pumpAndSettle();
|
||||
expect(value.value, '');
|
||||
expect(find.text('Date of birth cannot be in the future.'), findsOneWidget);
|
||||
await t.enterText(year, '1990');
|
||||
await t.pumpAndSettle();
|
||||
expect(value.value, '1990-04-30');
|
||||
await t.enterText(day, '0');
|
||||
await t.pumpAndSettle();
|
||||
expect(value.value, '');
|
||||
expect(find.text('Enter a valid date of birth.'), findsOneWidget);
|
||||
await t.enterText(day, '1');
|
||||
await t.enterText(year, '0000');
|
||||
await t.pumpAndSettle();
|
||||
expect(value.value, '');
|
||||
expect(find.text('Enter a valid date of birth.'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'prefill, external replacement, locale change and clear synchronize',
|
||||
(t) async {
|
||||
final value = ValueNotifier('1990-04-23');
|
||||
addTearDown(value.dispose);
|
||||
await mount(t, value);
|
||||
expect(t.widget<TextFormField>(year).controller!.text, '1990');
|
||||
value.value = '1985-12-09';
|
||||
await t.pumpAndSettle();
|
||||
expect(t.widget<TextFormField>(day).controller!.text, '09');
|
||||
expect(t.widget<TextFormField>(year).controller!.text, '1985');
|
||||
expect(find.text('December'), findsOneWidget);
|
||||
await mount(t, value, locale: const Locale('ru'));
|
||||
expect(find.text('Дата рождения'), findsOneWidget);
|
||||
expect(find.text('декабрь'), findsOneWidget);
|
||||
expect(value.value, '1985-12-09');
|
||||
await select(t, 'апрель');
|
||||
expect(value.value, '1985-04-09');
|
||||
value.value = '';
|
||||
await t.pumpAndSettle();
|
||||
expect(t.widget<TextFormField>(day).controller!.text, '');
|
||||
expect(t.widget<TextFormField>(year).controller!.text, '');
|
||||
expect(find.text('апрель'), findsNothing);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('digits and length limits keep each component usable', (t) async {
|
||||
final value = ValueNotifier('1990-04-23');
|
||||
addTearDown(value.dispose);
|
||||
await mount(t, value);
|
||||
await t.enterText(day, '');
|
||||
await t.enterText(day, 'a123');
|
||||
await t.enterText(year, '');
|
||||
await t.enterText(year, 'b19856');
|
||||
await t.pumpAndSettle();
|
||||
expect(t.widget<TextFormField>(day).controller!.text, '12');
|
||||
expect(t.widget<TextFormField>(year).controller!.text, '1985');
|
||||
expect(value.value, '1985-04-12');
|
||||
});
|
||||
|
||||
testWidgets('busy state disables every birthday control', (t) async {
|
||||
final value = ValueNotifier('1990-04-23');
|
||||
addTearDown(value.dispose);
|
||||
await mount(t, value, enabled: false);
|
||||
expect(t.widget<TextFormField>(day).enabled, isFalse);
|
||||
expect(t.widget<TextFormField>(year).enabled, isFalse);
|
||||
expect(t.widget<DropdownButtonFormField<int>>(month).onChanged, isNull);
|
||||
expect(value.value, '1990-04-23');
|
||||
});
|
||||
|
||||
testWidgets('keyboard can traverse, choose month, cancel and edit year', (
|
||||
t,
|
||||
) async {
|
||||
final value = ValueNotifier('1990-04-23');
|
||||
addTearDown(value.dispose);
|
||||
await mount(t, value);
|
||||
await t.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||
await t.pump();
|
||||
expect(
|
||||
t
|
||||
.widget<EditableText>(
|
||||
find.descendant(of: day, matching: find.byType(EditableText)),
|
||||
)
|
||||
.focusNode
|
||||
.hasFocus,
|
||||
isTrue,
|
||||
);
|
||||
await t.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||
await t.sendKeyEvent(LogicalKeyboardKey.enter);
|
||||
await t.pumpAndSettle();
|
||||
await t.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||||
await t.sendKeyEvent(LogicalKeyboardKey.enter);
|
||||
await t.pumpAndSettle();
|
||||
expect(value.value, '1990-05-23');
|
||||
await t.sendKeyEvent(LogicalKeyboardKey.enter);
|
||||
await t.pumpAndSettle();
|
||||
await t.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await t.pumpAndSettle();
|
||||
expect(value.value, '1990-05-23');
|
||||
await t.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||
await t.pump();
|
||||
expect(
|
||||
t
|
||||
.widget<EditableText>(
|
||||
find.descendant(of: year, matching: find.byType(EditableText)),
|
||||
)
|
||||
.focusNode
|
||||
.hasFocus,
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'Russian controls and errors fit 320px with large text and labeled targets',
|
||||
(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);
|
||||
final semantics = t.ensureSemantics();
|
||||
final value = ValueNotifier('1990-09-23');
|
||||
addTearDown(value.dispose);
|
||||
await mount(t, value, locale: const Locale('ru'));
|
||||
expect(find.text('сентябрь'), findsOneWidget);
|
||||
expect(t, meetsGuideline(labeledTapTargetGuideline));
|
||||
expect(t, meetsGuideline(androidTapTargetGuideline));
|
||||
await t.enterText(day, '31');
|
||||
await t.pumpAndSettle();
|
||||
expect(find.text('Укажите корректную дату рождения.'), findsOneWidget);
|
||||
await select(t, 'март');
|
||||
expect(value.value, '1990-03-31');
|
||||
expect(t.takeException(), isNull);
|
||||
semantics.dispose();
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import 'package:sxchnge_analytics/sxchnge_analytics.dart';
|
||||
|
||||
import 'package:sxchnge_kyc/sxchnge_kyc.dart';
|
||||
|
||||
import 'package:sxchnge_app/kyc/birth.dart';
|
||||
import 'package:sxchnge_app/kyc/country.dart';
|
||||
import 'package:sxchnge_app/main.dart';
|
||||
import 'package:sxchnge_app/providers/locale.dart';
|
||||
@@ -66,10 +67,16 @@ void main() {
|
||||
return service;
|
||||
}
|
||||
|
||||
Future<void> submit(WidgetTester t) async {
|
||||
Future<void> profile(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 t.enterText(find.byKey(const ValueKey('birth-day')), '23');
|
||||
await tap(t, find.byType(DropdownButtonFormField<int>));
|
||||
// The month label follows the selected app locale.
|
||||
await tap(t, find.textContaining(RegExp(r'^(April|апрель)$')).last);
|
||||
await t.enterText(find.byKey(const ValueKey('birth-year')), '1990');
|
||||
await t.pumpAndSettle();
|
||||
expect(t.widget<BirthField>(find.byType(BirthField)).value, '1990-04-23');
|
||||
await tap(
|
||||
t,
|
||||
find.descendant(
|
||||
@@ -85,6 +92,10 @@ void main() {
|
||||
'PL',
|
||||
);
|
||||
await t.pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> submit(WidgetTester t) async {
|
||||
await profile(t);
|
||||
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')));
|
||||
@@ -93,6 +104,41 @@ void main() {
|
||||
await tap(t, find.byKey(const ValueKey('kyc-submit')));
|
||||
}
|
||||
|
||||
testWidgets(
|
||||
'invalid birthday blocks advancing and valid birthday survives Back',
|
||||
(t) async {
|
||||
await open(t);
|
||||
await profile(t);
|
||||
await tap(t, find.byKey(const ValueKey('pick-passport')));
|
||||
final next = find.byKey(const ValueKey('kyc-next'));
|
||||
await t.enterText(find.byKey(const ValueKey('birth-day')), '31');
|
||||
await t.pumpAndSettle();
|
||||
expect(t.widget<FilledButton>(next).onPressed, isNull);
|
||||
expect(find.text('Enter a valid date of birth.'), findsOneWidget);
|
||||
await t.enterText(find.byKey(const ValueKey('birth-day')), '23');
|
||||
await t.pumpAndSettle();
|
||||
expect(t.widget<FilledButton>(next).onPressed, isNotNull);
|
||||
await tap(t, next);
|
||||
await tap(t, find.text('Back'));
|
||||
expect(t.widget<BirthField>(find.byType(BirthField)).value, '1990-04-23');
|
||||
expect(
|
||||
t
|
||||
.widget<TextFormField>(find.byKey(const ValueKey('birth-day')))
|
||||
.controller!
|
||||
.text,
|
||||
'23',
|
||||
);
|
||||
expect(find.text('April'), findsOneWidget);
|
||||
expect(
|
||||
t
|
||||
.widget<TextFormField>(find.byKey(const ValueKey('birth-year')))
|
||||
.controller!
|
||||
.text,
|
||||
'1990',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('two photos, review, pending and approval with service limits', (
|
||||
t,
|
||||
) async {
|
||||
|
||||
Reference in new issue
Block a user