merge: 合并 K98 登录键盘避让
This commit is contained in:
commit
ecdff54f2e
6 files changed
+332
-9
No files matched your search
@@ -92,3 +92,34 @@ Running Gradle task 'assembleDebug'... 25.8s
|
|||||||
|
|
||||||
解读:静态分析零告警,555 项全量测试通过,debug APK 构建成功;
|
解读:静态分析零告警,555 项全量测试通过,debug APK 构建成功;
|
||||||
`pubspec.lock` 已还原,无依赖变更,未安装或操作真机。
|
`pubspec.lock` 已还原,无依赖变更,未安装或操作真机。
|
||||||
|
|
||||||
|
## 2026-10-21 K98 修复登录与手机号登录键盘遮挡
|
||||||
|
|
||||||
|
- 已完整读取 K98 `docs/plans/INBOX.md`。
|
||||||
|
- 已从最新 `origin/develop` 创建 `fix/k98-auth-keyboard`。
|
||||||
|
- 计划见 FruitServer `docs/plans/2026-10-21-K98-auth-keyboard.md`。
|
||||||
|
- 将统一 viewInsets/channel 有效键盘高度,并给登录页与手机号弹窗增加
|
||||||
|
焦点/键盘变化后的字段滚动校正。
|
||||||
|
- 已新增 `effectiveKeyboardInset`,兼容 iOS 实时 viewInsets 与 Android
|
||||||
|
`adjustNothing` 下的 MethodChannel 键盘高度。
|
||||||
|
- 登录/注册页已增加 ScrollController、notifier 监听和邮箱/密码焦点校正;
|
||||||
|
手机号弹窗已同样接入手机号/验证码框滚动校正。
|
||||||
|
- 已补 iOS 登录/注册、Android channel、手机号验证码框键盘避让测试。
|
||||||
|
|
||||||
|
门禁原始输出:
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ dart analyze lib test
|
||||||
|
Analyzing lib, test...
|
||||||
|
No issues found!
|
||||||
|
|
||||||
|
$ flutter test --no-pub
|
||||||
|
00:52 +559: All tests passed!
|
||||||
|
|
||||||
|
$ flutter build apk --debug
|
||||||
|
Running Gradle task 'assembleDebug'... 26.2s
|
||||||
|
✓ Built build/app/outputs/flutter-apk/app-debug.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
解读:静态分析零告警,559 项全量测试通过,debug APK 构建成功;
|
||||||
|
`pubspec.lock` 已还原,无依赖变更,未安装或操作真机。
|
||||||
@@ -1,8 +1,29 @@
|
|||||||
// Author: fruit.com.cn
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
/// iOS 使用实时 viewInsets;Android adjustNothing 下取 MethodChannel 最大值。
|
||||||
|
double effectiveKeyboardInset(
|
||||||
|
BuildContext context,
|
||||||
|
KeyboardInsetNotifier notifier, {
|
||||||
|
double maxRatio = 0.6,
|
||||||
|
}) {
|
||||||
|
final media = MediaQuery.of(context);
|
||||||
|
final viewInset = media.viewInsets.bottom;
|
||||||
|
final channelInset =
|
||||||
|
!kIsWeb && defaultTargetPlatform == TargetPlatform.android
|
||||||
|
? notifier.value / media.devicePixelRatio
|
||||||
|
: 0.0;
|
||||||
|
final inset = math.max(viewInset, channelInset);
|
||||||
|
final maxInset = media.size.height * maxRatio;
|
||||||
|
if (!inset.isFinite || inset > maxInset) return 0;
|
||||||
|
return math.max(0, inset);
|
||||||
|
}
|
||||||
|
|
||||||
/// Provides the Android IME height even when `adjustNothing` keeps
|
/// Provides the Android IME height even when `adjustNothing` keeps
|
||||||
/// `MediaQuery.viewInsets` at zero.
|
/// `MediaQuery.viewInsets` at zero.
|
||||||
class KeyboardInsetNotifier extends ChangeNotifier {
|
class KeyboardInsetNotifier extends ChangeNotifier {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:provider/provider.dart';
|
|||||||
import '../../core/data/remote/api_client.dart';
|
import '../../core/data/remote/api_client.dart';
|
||||||
import '../../core/data/services/auth_controller.dart';
|
import '../../core/data/services/auth_controller.dart';
|
||||||
import '../../core/platform/haptics.dart';
|
import '../../core/platform/haptics.dart';
|
||||||
|
import '../../core/platform/keyboard_inset.dart';
|
||||||
import '../../core/theme/app_colors.dart';
|
import '../../core/theme/app_colors.dart';
|
||||||
import '../../core/theme/app_dimens.dart';
|
import '../../core/theme/app_dimens.dart';
|
||||||
import '../../core/theme/app_theme.dart';
|
import '../../core/theme/app_theme.dart';
|
||||||
@@ -101,6 +102,9 @@ class _AuthPageState extends State<AuthPage> {
|
|||||||
late final TextEditingController _passwordController;
|
late final TextEditingController _passwordController;
|
||||||
late final FocusNode _emailFocus;
|
late final FocusNode _emailFocus;
|
||||||
late final FocusNode _passwordFocus;
|
late final FocusNode _passwordFocus;
|
||||||
|
late final ScrollController _scrollController;
|
||||||
|
final _keyboardInset = KeyboardInsetNotifier.instance;
|
||||||
|
double _lastKeyboardInset = 0;
|
||||||
bool _obscurePassword = true;
|
bool _obscurePassword = true;
|
||||||
bool _emailEntryOpen = false;
|
bool _emailEntryOpen = false;
|
||||||
|
|
||||||
@@ -112,17 +116,87 @@ class _AuthPageState extends State<AuthPage> {
|
|||||||
_passwordController = TextEditingController();
|
_passwordController = TextEditingController();
|
||||||
_emailFocus = FocusNode(debugLabel: 'auth-email-focus');
|
_emailFocus = FocusNode(debugLabel: 'auth-email-focus');
|
||||||
_passwordFocus = FocusNode(debugLabel: 'auth-password-focus');
|
_passwordFocus = FocusNode(debugLabel: 'auth-password-focus');
|
||||||
|
_scrollController = ScrollController();
|
||||||
|
_emailFocus.addListener(_onFocusChanged);
|
||||||
|
_passwordFocus.addListener(_onFocusChanged);
|
||||||
|
_keyboardInset.addListener(_onKeyboardInsetChanged);
|
||||||
|
unawaited(_keyboardInset.start());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
final inset = effectiveKeyboardInset(context, _keyboardInset);
|
||||||
|
if ((inset - _lastKeyboardInset).abs() >= 1) {
|
||||||
|
_lastKeyboardInset = inset;
|
||||||
|
_scheduleRevealFocusedField();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_keyboardInset.removeListener(_onKeyboardInsetChanged);
|
||||||
|
_emailFocus.removeListener(_onFocusChanged);
|
||||||
|
_passwordFocus.removeListener(_onFocusChanged);
|
||||||
_emailController.dispose();
|
_emailController.dispose();
|
||||||
_passwordController.dispose();
|
_passwordController.dispose();
|
||||||
_emailFocus.dispose();
|
_emailFocus.dispose();
|
||||||
_passwordFocus.dispose();
|
_passwordFocus.dispose();
|
||||||
|
_scrollController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onKeyboardInsetChanged() {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {});
|
||||||
|
_scheduleRevealFocusedField();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onFocusChanged() {
|
||||||
|
if (_emailFocus.hasFocus || _passwordFocus.hasFocus) {
|
||||||
|
_scheduleRevealFocusedField();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scheduleRevealFocusedField() {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) _revealFocusedField();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _revealFocusedField() {
|
||||||
|
if (!_scrollController.hasClients) return;
|
||||||
|
final focus = _passwordFocus.hasFocus
|
||||||
|
? _passwordFocus
|
||||||
|
: _emailFocus.hasFocus
|
||||||
|
? _emailFocus
|
||||||
|
: null;
|
||||||
|
final fieldContext = focus?.context;
|
||||||
|
final renderObject = fieldContext?.findRenderObject();
|
||||||
|
if (renderObject is! RenderBox || !renderObject.hasSize) return;
|
||||||
|
final keyboardInset = effectiveKeyboardInset(context, _keyboardInset);
|
||||||
|
if (keyboardInset <= 0) return;
|
||||||
|
final fieldBottom = renderObject
|
||||||
|
.localToGlobal(Offset(0, renderObject.size.height))
|
||||||
|
.dy;
|
||||||
|
final visibleBottom =
|
||||||
|
MediaQuery.sizeOf(context).height - keyboardInset - 24;
|
||||||
|
final overlap = fieldBottom - visibleBottom;
|
||||||
|
if (overlap <= 0) return;
|
||||||
|
final position = _scrollController.position;
|
||||||
|
final target = (position.pixels + overlap)
|
||||||
|
.clamp(position.minScrollExtent, position.maxScrollExtent)
|
||||||
|
.toDouble();
|
||||||
|
if (target <= position.pixels + 0.5) return;
|
||||||
|
unawaited(
|
||||||
|
_scrollController.animateTo(
|
||||||
|
target,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
String _errorText(AppLocalizations l10n, AuthFieldError error) {
|
String _errorText(AppLocalizations l10n, AuthFieldError error) {
|
||||||
switch (error) {
|
switch (error) {
|
||||||
case AuthFieldError.emailRequired:
|
case AuthFieldError.emailRequired:
|
||||||
@@ -246,12 +320,13 @@ class _AuthPageState extends State<AuthPage> {
|
|||||||
: _topContentPadding;
|
: _topContentPadding;
|
||||||
final bottom =
|
final bottom =
|
||||||
_bottomContentPadding +
|
_bottomContentPadding +
|
||||||
MediaQuery.viewInsetsOf(context).bottom;
|
effectiveKeyboardInset(context, _keyboardInset);
|
||||||
final available = constraints.maxHeight - top - bottom;
|
final available = constraints.maxHeight - top - bottom;
|
||||||
final minHeight = available > 0 ? available : 0.0;
|
final minHeight = available > 0 ? available : 0.0;
|
||||||
return AutofillGroup(
|
return AutofillGroup(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
key: const Key('auth-scroll'),
|
key: const Key('auth-scroll'),
|
||||||
|
controller: _scrollController,
|
||||||
keyboardDismissBehavior:
|
keyboardDismissBehavior:
|
||||||
ScrollViewKeyboardDismissBehavior.onDrag,
|
ScrollViewKeyboardDismissBehavior.onDrag,
|
||||||
padding: EdgeInsets.fromLTRB(
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import 'package:provider/provider.dart';
|
|||||||
import '../../core/data/remote/api_client.dart';
|
import '../../core/data/remote/api_client.dart';
|
||||||
import '../../core/data/services/auth_controller.dart';
|
import '../../core/data/services/auth_controller.dart';
|
||||||
import '../../core/platform/haptics.dart';
|
import '../../core/platform/haptics.dart';
|
||||||
|
import '../../core/platform/keyboard_inset.dart';
|
||||||
import '../../core/theme/app_colors.dart';
|
import '../../core/theme/app_colors.dart';
|
||||||
import '../../l10n/app_localizations.dart';
|
import '../../l10n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -46,7 +47,10 @@ class _PhoneAuthSheetState extends State<PhoneAuthSheet> {
|
|||||||
late final TextEditingController _code;
|
late final TextEditingController _code;
|
||||||
late final FocusNode _phoneFocus;
|
late final FocusNode _phoneFocus;
|
||||||
late final FocusNode _codeFocus;
|
late final FocusNode _codeFocus;
|
||||||
|
late final ScrollController _scrollController;
|
||||||
|
final _keyboardInset = KeyboardInsetNotifier.instance;
|
||||||
Timer? _countdown;
|
Timer? _countdown;
|
||||||
|
double _lastKeyboardInset = 0;
|
||||||
bool _signUp = false;
|
bool _signUp = false;
|
||||||
bool _sending = false;
|
bool _sending = false;
|
||||||
bool _submitting = false;
|
bool _submitting = false;
|
||||||
@@ -61,18 +65,88 @@ class _PhoneAuthSheetState extends State<PhoneAuthSheet> {
|
|||||||
_code = TextEditingController();
|
_code = TextEditingController();
|
||||||
_phoneFocus = FocusNode(debugLabel: 'phone-auth-number');
|
_phoneFocus = FocusNode(debugLabel: 'phone-auth-number');
|
||||||
_codeFocus = FocusNode(debugLabel: 'phone-auth-code');
|
_codeFocus = FocusNode(debugLabel: 'phone-auth-code');
|
||||||
|
_scrollController = ScrollController();
|
||||||
|
_phoneFocus.addListener(_onFocusChanged);
|
||||||
|
_codeFocus.addListener(_onFocusChanged);
|
||||||
|
_keyboardInset.addListener(_onKeyboardInsetChanged);
|
||||||
|
unawaited(_keyboardInset.start());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
final inset = effectiveKeyboardInset(context, _keyboardInset);
|
||||||
|
if ((inset - _lastKeyboardInset).abs() >= 1) {
|
||||||
|
_lastKeyboardInset = inset;
|
||||||
|
_scheduleRevealFocusedField();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_keyboardInset.removeListener(_onKeyboardInsetChanged);
|
||||||
|
_phoneFocus.removeListener(_onFocusChanged);
|
||||||
|
_codeFocus.removeListener(_onFocusChanged);
|
||||||
_countdown?.cancel();
|
_countdown?.cancel();
|
||||||
_phone.dispose();
|
_phone.dispose();
|
||||||
_code.dispose();
|
_code.dispose();
|
||||||
_phoneFocus.dispose();
|
_phoneFocus.dispose();
|
||||||
_codeFocus.dispose();
|
_codeFocus.dispose();
|
||||||
|
_scrollController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onKeyboardInsetChanged() {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {});
|
||||||
|
_scheduleRevealFocusedField();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onFocusChanged() {
|
||||||
|
if (_phoneFocus.hasFocus || _codeFocus.hasFocus) {
|
||||||
|
_scheduleRevealFocusedField();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scheduleRevealFocusedField() {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) _revealFocusedField();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _revealFocusedField() {
|
||||||
|
if (!_scrollController.hasClients) return;
|
||||||
|
final focus = _codeFocus.hasFocus
|
||||||
|
? _codeFocus
|
||||||
|
: _phoneFocus.hasFocus
|
||||||
|
? _phoneFocus
|
||||||
|
: null;
|
||||||
|
final fieldContext = focus?.context;
|
||||||
|
final renderObject = fieldContext?.findRenderObject();
|
||||||
|
if (renderObject is! RenderBox || !renderObject.hasSize) return;
|
||||||
|
final keyboardInset = effectiveKeyboardInset(context, _keyboardInset);
|
||||||
|
if (keyboardInset <= 0) return;
|
||||||
|
final fieldBottom = renderObject
|
||||||
|
.localToGlobal(Offset(0, renderObject.size.height))
|
||||||
|
.dy;
|
||||||
|
final visibleBottom =
|
||||||
|
MediaQuery.sizeOf(context).height - keyboardInset - 24;
|
||||||
|
final overlap = fieldBottom - visibleBottom;
|
||||||
|
if (overlap <= 0) return;
|
||||||
|
final position = _scrollController.position;
|
||||||
|
final target = (position.pixels + overlap)
|
||||||
|
.clamp(position.minScrollExtent, position.maxScrollExtent)
|
||||||
|
.toDouble();
|
||||||
|
if (target <= position.pixels + 0.5) return;
|
||||||
|
unawaited(
|
||||||
|
_scrollController.animateTo(
|
||||||
|
target,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
String get _target => _phone.text.replaceAll(RegExp(r'[\s-]'), '');
|
String get _target => _phone.text.replaceAll(RegExp(r'[\s-]'), '');
|
||||||
|
|
||||||
bool get _phoneValid => _target.length >= _phoneMinLength;
|
bool get _phoneValid => _target.length >= _phoneMinLength;
|
||||||
@@ -99,9 +173,8 @@ class _PhoneAuthSheetState extends State<PhoneAuthSheet> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
_startCountdown();
|
_startCountdown();
|
||||||
setState(() => _error = null);
|
setState(() => _error = null);
|
||||||
ScaffoldMessenger.of(
|
ScaffoldMessenger.of(context)
|
||||||
context,
|
.showSnackBar(SnackBar(content: Text(l10n.authCodeSent)));
|
||||||
).showSnackBar(SnackBar(content: Text(l10n.authCodeSent)));
|
|
||||||
_codeFocus.requestFocus();
|
_codeFocus.requestFocus();
|
||||||
} on ApiException catch (error) {
|
} on ApiException catch (error) {
|
||||||
if (mounted) setState(() => _error = error.message);
|
if (mounted) setState(() => _error = error.message);
|
||||||
@@ -166,16 +239,19 @@ class _PhoneAuthSheetState extends State<PhoneAuthSheet> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final bottom = MediaQuery.viewInsetsOf(context).bottom;
|
final bottom = effectiveKeyboardInset(context, _keyboardInset);
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(bottom: bottom),
|
padding: EdgeInsets.only(bottom: bottom),
|
||||||
child: Container(
|
child: Container(
|
||||||
key: const Key('phone-auth-sheet'),
|
key: const Key('phone-auth-sheet'),
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: AppColors.card,
|
color: AppColors.card,
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(_sheetRadius)),
|
borderRadius: BorderRadius.vertical(
|
||||||
|
top: Radius.circular(_sheetRadius),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
|
controller: _scrollController,
|
||||||
padding: const EdgeInsets.fromLTRB(22, 10, 22, 26),
|
padding: const EdgeInsets.fromLTRB(22, 10, 22, 26),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -278,9 +354,10 @@ class _PhoneAuthSheetState extends State<PhoneAuthSheet> {
|
|||||||
onSubmitted: (_) => _submit(),
|
onSubmitted: (_) => _submit(),
|
||||||
onChanged: (_) => setState(() => _error = null),
|
onChanged: (_) => setState(() => _error = null),
|
||||||
style: const TextStyle(color: AppColors.onSurface, fontSize: 15),
|
style: const TextStyle(color: AppColors.onSurface, fontSize: 15),
|
||||||
decoration: _decoration(l10n.authCodeLabel, l10n.authCodeHint).copyWith(
|
decoration: _decoration(
|
||||||
suffixIcon: _sendButton(l10n),
|
l10n.authCodeLabel,
|
||||||
),
|
l10n.authCodeHint,
|
||||||
|
).copyWith(suffixIcon: _sendButton(l10n)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
|
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
|
||||||
@@ -263,6 +265,90 @@ void main() {
|
|||||||
expect(tester.takeException(), isNull);
|
expect(tester.takeException(), isNull);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testWidgets('should_keep_password_above_ios_keyboard', (tester) async {
|
||||||
|
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
||||||
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
||||||
|
tester.view.physicalSize = const Size(390, 844);
|
||||||
|
tester.view.devicePixelRatio = 1;
|
||||||
|
tester.view.viewInsets = const FakeViewPadding(bottom: 300);
|
||||||
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
|
addTearDown(tester.view.resetDevicePixelRatio);
|
||||||
|
addTearDown(tester.view.resetViewInsets);
|
||||||
|
|
||||||
|
await pumpAuthPage(tester);
|
||||||
|
await tester.tap(find.byKey(const Key('auth-password')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
_expectAboveKeyboard(tester, const Key('auth-password'), 300);
|
||||||
|
debugDefaultTargetPlatformOverride = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should_keep_signup_password_above_ios_keyboard', (tester) async {
|
||||||
|
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
||||||
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
||||||
|
tester.view.physicalSize = const Size(390, 844);
|
||||||
|
tester.view.devicePixelRatio = 1;
|
||||||
|
tester.view.viewInsets = const FakeViewPadding(bottom: 300);
|
||||||
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
|
addTearDown(tester.view.resetDevicePixelRatio);
|
||||||
|
addTearDown(tester.view.resetViewInsets);
|
||||||
|
|
||||||
|
await pumpAuthPage(tester, signUp: true);
|
||||||
|
await tester.tap(find.byKey(const Key('auth-password')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
_expectAboveKeyboard(tester, const Key('auth-password'), 300);
|
||||||
|
debugDefaultTargetPlatformOverride = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should_keep_password_above_android_channel_keyboard', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||||
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
||||||
|
const keyboardChannel = MethodChannel('com.stormg.lumadrama/keyboard');
|
||||||
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||||
|
keyboardChannel,
|
||||||
|
(call) async => call.method == 'get' ? 300 : null,
|
||||||
|
);
|
||||||
|
addTearDown(() {
|
||||||
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||||
|
keyboardChannel,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
tester.view.physicalSize = const Size(390, 844);
|
||||||
|
tester.view.devicePixelRatio = 1;
|
||||||
|
tester.view.viewInsets = const FakeViewPadding();
|
||||||
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
|
addTearDown(tester.view.resetDevicePixelRatio);
|
||||||
|
addTearDown(tester.view.resetViewInsets);
|
||||||
|
|
||||||
|
await pumpAuthPage(tester);
|
||||||
|
await tester.binding.defaultBinaryMessenger.handlePlatformMessage(
|
||||||
|
keyboardChannel.name,
|
||||||
|
const StandardMethodCodec().encodeMethodCall(
|
||||||
|
const MethodCall('insetsChanged', 300),
|
||||||
|
),
|
||||||
|
(_) {},
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
await tester.tap(find.byKey(const Key('auth-password')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
_expectAboveKeyboard(tester, const Key('auth-password'), 300);
|
||||||
|
debugDefaultTargetPlatformOverride = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _expectAboveKeyboard(WidgetTester tester, Key fieldKey, double inset) {
|
||||||
|
final screenHeight =
|
||||||
|
tester.view.physicalSize.height / tester.view.devicePixelRatio;
|
||||||
|
expect(
|
||||||
|
tester.getBottomLeft(find.byKey(fieldKey)).dy,
|
||||||
|
lessThanOrEqualTo(screenHeight - inset),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PendingAuthApi extends AuthApi {
|
class _PendingAuthApi extends AuthApi {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:fruitshorts/core/data/remote/api_client.dart';
|
import 'package:fruitshorts/core/data/remote/api_client.dart';
|
||||||
@@ -97,6 +98,38 @@ void main() {
|
|||||||
expect(find.text('Enter a valid phone number'), findsOneWidget);
|
expect(find.text('Enter a valid phone number'), findsOneWidget);
|
||||||
expect(adapter.requests, isEmpty);
|
expect(adapter.requests, isEmpty);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('should_keep_code_above_ios_keyboard', (tester) async {
|
||||||
|
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
||||||
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
||||||
|
tester.view.physicalSize = const Size(390, 844);
|
||||||
|
tester.view.devicePixelRatio = 1;
|
||||||
|
tester.view.viewInsets = const FakeViewPadding(bottom: 300);
|
||||||
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
|
addTearDown(tester.view.resetDevicePixelRatio);
|
||||||
|
addTearDown(tester.view.resetViewInsets);
|
||||||
|
final adapter = FakeHttpAdapter(
|
||||||
|
(_) async => FakeHttpResponse(
|
||||||
|
jsonEncode({'code': 0, 'message': 'ok', 'data': null}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final auth = AuthController(
|
||||||
|
AuthApi(ApiClient(baseUrl: 'http://127.0.0.1:8081', adapter: adapter)),
|
||||||
|
AuthSessionStore(),
|
||||||
|
);
|
||||||
|
await _pumpHost(tester, auth);
|
||||||
|
await tester.tap(find.byKey(const Key('open-phone-sheet')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('auth-phone-code')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
tester.getBottomLeft(find.byKey(const Key('auth-phone-code'))).dy,
|
||||||
|
lessThanOrEqualTo(844 - 300),
|
||||||
|
);
|
||||||
|
debugDefaultTargetPlatformOverride = null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _pumpHost(WidgetTester tester, AuthController auth) {
|
Future<void> _pumpHost(WidgetTester tester, AuthController auth) {
|
||||||
|
|||||||
Reference in new issue
Block a user