maestro cases

This commit is contained in:
2026-03-02 19:18:35 +05:30
parent 07a0a29106
commit c0a69707e6
46 changed files with 1108 additions and 89 deletions

View File

@@ -36,8 +36,22 @@ class _OtpInputFieldState extends State<OtpInputField> {
);
final List<FocusNode> _focusNodes = List.generate(6, (_) => FocusNode());
/// Hidden field for E2E: Maestro inputText sends full OTP in one call;
/// the 6 visible boxes have maxLength:1 and would truncate.
late final TextEditingController _hiddenController;
late final FocusNode _hiddenFocusNode;
@override
void initState() {
super.initState();
_hiddenController = TextEditingController();
_hiddenFocusNode = FocusNode();
}
@override
void dispose() {
_hiddenController.dispose();
_hiddenFocusNode.dispose();
for (final TextEditingController controller in _controllers) {
controller.dispose();
}
@@ -47,6 +61,22 @@ class _OtpInputFieldState extends State<OtpInputField> {
super.dispose();
}
/// Distributes full OTP from hidden field to the 6 visible boxes and notifies Bloc.
void _syncFromHidden(BuildContext context, String value) {
final String raw = value.replaceAll(RegExp(r'\D'), '');
final String digits = raw.length > 6 ? raw.substring(0, 6) : raw;
for (int i = 0; i < 6; i++) {
final String digit = i < digits.length ? digits[i] : '';
if (_controllers[i].text != digit) {
_controllers[i].text = digit;
}
}
BlocProvider.of<AuthBloc>(context).add(AuthOtpUpdated(digits));
if (digits.length == 6) {
widget.onCompleted(digits);
}
}
/// Helper getter to compute the current OTP code from all controllers.
String get _otpCode => _controllers.map((TextEditingController c) => c.text).join();
@@ -70,57 +100,79 @@ class _OtpInputFieldState extends State<OtpInputField> {
@override
Widget build(BuildContext context) {
const double boxWidth = 45;
const double boxHeight = 56;
return Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(6, (int index) {
final TextField field = TextField(
controller: _controllers[index],
focusNode: _focusNodes[index],
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[FilteringTextInputFormatter.digitsOnly],
textAlign: TextAlign.center,
maxLength: 1,
style: UiTypography.headline3m,
decoration: InputDecoration(
counterText: '',
border: OutlineInputBorder(
borderSide: BorderSide(
color: widget.error.isNotEmpty
? UiColors.textError
: (_controllers[index].text.isNotEmpty
? UiColors.primary
: UiColors.border),
width: 2,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: UiConstants.radiusMd,
borderSide: BorderSide(
color: widget.error.isNotEmpty
? UiColors.textError
: (_controllers[index].text.isNotEmpty
? UiColors.primary
: UiColors.border),
width: 2,
SizedBox(
width: 300,
height: boxHeight,
child: Stack(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(6, (int index) {
final TextField field = TextField(
controller: _controllers[index],
focusNode: _focusNodes[index],
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[FilteringTextInputFormatter.digitsOnly],
textAlign: TextAlign.center,
maxLength: 1,
style: UiTypography.headline3m,
decoration: InputDecoration(
counterText: '',
border: OutlineInputBorder(
borderSide: BorderSide(
color: widget.error.isNotEmpty
? UiColors.textError
: (_controllers[index].text.isNotEmpty
? UiColors.primary
: UiColors.border),
width: 2,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: UiConstants.radiusMd,
borderSide: BorderSide(
color: widget.error.isNotEmpty
? UiColors.textError
: (_controllers[index].text.isNotEmpty
? UiColors.primary
: UiColors.border),
width: 2,
),
),
),
onChanged: (String value) =>
_onChanged(context: context, index: index, value: value),
);
return SizedBox(width: boxWidth, height: boxHeight, child: field);
}),
),
Positioned.fill(
child: Semantics(
identifier: 'staff_otp_input',
container: false,
child: Opacity(
opacity: 0.01,
child: TextField(
controller: _hiddenController,
focusNode: _hiddenFocusNode,
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(6),
],
maxLength: 6,
onChanged: (String value) =>
_syncFromHidden(context, value),
),
),
),
onChanged: (String value) =>
_onChanged(context: context, index: index, value: value),
);
return SizedBox(
width: 45,
height: 56,
child: index == 0
? Semantics(
identifier: 'staff_otp_input',
child: field,
)
: field,
);
}),
),
],
),
),
if (widget.error.isNotEmpty)
Padding(

View File

@@ -84,6 +84,7 @@ class _PhoneInputFormFieldState extends State<PhoneInputFormField> {
Expanded(
child: Semantics(
identifier: 'staff_phone_input',
container: false, // Merge with TextField so tap/input reach the actual field
child: TextField(
controller: _controller,
keyboardType: TextInputType.phone,

View File

@@ -152,9 +152,14 @@ class DocumentCard extends StatelessWidget {
Widget _buildActionButton(DocumentStatus status) {
final bool isVerified = status == DocumentStatus.verified;
return InkWell(
onTap: onTap,
borderRadius: UiConstants.radiusSm,
return Semantics(
identifier: 'staff_document_upload',
label: isVerified
? t.staff_documents.card.view
: t.staff_documents.card.upload,
child: InkWell(
onTap: onTap,
borderRadius: UiConstants.radiusSm,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Row(
@@ -174,6 +179,7 @@ class DocumentCard extends StatelessWidget {
],
),
),
),
);
}
}