feat(apps): update UI screens and shared components
- Update home screen with new composer and interactions - Update settings screens with new profile flow - Update calendar share dialog - Update contacts screen - Add new shared widgets: confirm_sheet, phone_prefix_selector - Add new formatters: phone_display_formatter - Update tests for modified components
This commit is contained in:
@@ -1,17 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:social_app/features/home/ui/widgets/home_background_field.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('home background field renders layered glow surfaces', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(home: Scaffold(body: HomeBackgroundField())),
|
||||
);
|
||||
|
||||
expect(find.byKey(homeBackgroundFieldKey), findsOneWidget);
|
||||
expect(find.byKey(homeTopGlowKey), findsOneWidget);
|
||||
expect(find.byKey(homeBottomGlowKey), findsOneWidget);
|
||||
});
|
||||
}
|
||||
@@ -1,266 +0,0 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:social_app/core/theme/design_tokens.dart';
|
||||
import 'package:social_app/shared/widgets/message_composer.dart';
|
||||
|
||||
Widget _buildTestApp({
|
||||
required MessageComposerMode mode,
|
||||
required MessageComposerProcess process,
|
||||
required bool hasMessage,
|
||||
required bool isWaitingAgent,
|
||||
VoidCallback? onHoldStart,
|
||||
VoidCallback? onHoldEnd,
|
||||
VoidCallback? onHoldCancel,
|
||||
}) {
|
||||
return MaterialApp(
|
||||
home: Scaffold(
|
||||
body: MessageComposer(
|
||||
mode: mode,
|
||||
process: process,
|
||||
hasMessage: hasMessage,
|
||||
isWaitingAgent: isWaitingAgent,
|
||||
iconSize: 24,
|
||||
composerMinHeight: 48,
|
||||
onTapPlus: () {},
|
||||
onTapRightAction: () {},
|
||||
onHoldToSpeakStart: onHoldStart ?? () {},
|
||||
onHoldToSpeakEnd: onHoldEnd ?? () {},
|
||||
onHoldToSpeakMoveUpdate: (_) {},
|
||||
onHoldToSpeakCancel: onHoldCancel ?? () {},
|
||||
textInputChild: const SizedBox.shrink(),
|
||||
recordingAnimation: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('MessageComposer', () {
|
||||
testWidgets('renders one unified rounded composer container', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
_buildTestApp(
|
||||
mode: MessageComposerMode.text,
|
||||
process: MessageComposerProcess.idle,
|
||||
hasMessage: false,
|
||||
isWaitingAgent: false,
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byKey(messageComposerContainerKey), findsOneWidget);
|
||||
expect(find.byKey(messageComposerShellKey), findsOneWidget);
|
||||
expect(find.byKey(messageComposerInnerKey), findsOneWidget);
|
||||
|
||||
final containerFinder = find.byKey(messageComposerContainerKey);
|
||||
final shellFinder = find.byKey(messageComposerShellKey);
|
||||
final plusFinder = find.byKey(messageComposerPlusButtonKey);
|
||||
final rightFinder = find.byKey(messageComposerRightButtonKey);
|
||||
|
||||
expect(
|
||||
find.descendant(of: containerFinder, matching: plusFinder),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.descendant(of: containerFinder, matching: rightFinder),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
final container = tester.widget<Container>(shellFinder);
|
||||
final decoration = container.decoration! as BoxDecoration;
|
||||
expect(decoration.color, AppColors.homeComposerShell);
|
||||
expect(
|
||||
(decoration.border! as Border).top.color,
|
||||
AppColors.homeComposerBorder,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('recording state keeps unified floating shell', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
_buildTestApp(
|
||||
mode: MessageComposerMode.holdToSpeak,
|
||||
process: MessageComposerProcess.recording,
|
||||
hasMessage: false,
|
||||
isWaitingAgent: false,
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byKey(messageComposerShellKey), findsOneWidget);
|
||||
expect(find.byKey(messageComposerInnerKey), findsOneWidget);
|
||||
expect(find.text('松开发送'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('right action icon follows state priority', (tester) async {
|
||||
Future<IconData> rightIconFor({
|
||||
required MessageComposerMode mode,
|
||||
required MessageComposerProcess process,
|
||||
required bool hasMessage,
|
||||
required bool isWaitingAgent,
|
||||
}) async {
|
||||
await tester.pumpWidget(
|
||||
_buildTestApp(
|
||||
mode: mode,
|
||||
process: process,
|
||||
hasMessage: hasMessage,
|
||||
isWaitingAgent: isWaitingAgent,
|
||||
),
|
||||
);
|
||||
|
||||
final iconFinder = find.descendant(
|
||||
of: find.byKey(messageComposerRightButtonKey),
|
||||
matching: find.byType(Icon),
|
||||
);
|
||||
expect(iconFinder, findsOneWidget);
|
||||
final iconWidget = tester.widget<Icon>(iconFinder.first);
|
||||
expect(iconWidget.icon, isNotNull);
|
||||
return iconWidget.icon!;
|
||||
}
|
||||
|
||||
expect(
|
||||
await rightIconFor(
|
||||
mode: MessageComposerMode.text,
|
||||
process: MessageComposerProcess.idle,
|
||||
hasMessage: false,
|
||||
isWaitingAgent: true,
|
||||
),
|
||||
LucideIcons.square,
|
||||
);
|
||||
|
||||
expect(
|
||||
await rightIconFor(
|
||||
mode: MessageComposerMode.holdToSpeak,
|
||||
process: MessageComposerProcess.idle,
|
||||
hasMessage: true,
|
||||
isWaitingAgent: false,
|
||||
),
|
||||
LucideIcons.send,
|
||||
);
|
||||
|
||||
expect(
|
||||
await rightIconFor(
|
||||
mode: MessageComposerMode.holdToSpeak,
|
||||
process: MessageComposerProcess.idle,
|
||||
hasMessage: false,
|
||||
isWaitingAgent: false,
|
||||
),
|
||||
LucideIcons.keyboard,
|
||||
);
|
||||
|
||||
expect(
|
||||
await rightIconFor(
|
||||
mode: MessageComposerMode.text,
|
||||
process: MessageComposerProcess.idle,
|
||||
hasMessage: false,
|
||||
isWaitingAgent: false,
|
||||
),
|
||||
LucideIcons.mic,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('recording hint appears only while recording', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
_buildTestApp(
|
||||
mode: MessageComposerMode.holdToSpeak,
|
||||
process: MessageComposerProcess.idle,
|
||||
hasMessage: false,
|
||||
isWaitingAgent: false,
|
||||
),
|
||||
);
|
||||
expect(find.byKey(messageComposerRecordingHintKey), findsNothing);
|
||||
|
||||
await tester.pumpWidget(
|
||||
_buildTestApp(
|
||||
mode: MessageComposerMode.holdToSpeak,
|
||||
process: MessageComposerProcess.recording,
|
||||
hasMessage: false,
|
||||
isWaitingAgent: false,
|
||||
),
|
||||
);
|
||||
expect(find.byKey(messageComposerRecordingHintKey), findsOneWidget);
|
||||
expect(find.text('松开发送,上滑取消'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('composer height remains stable across mode switches', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
_buildTestApp(
|
||||
mode: MessageComposerMode.text,
|
||||
process: MessageComposerProcess.idle,
|
||||
hasMessage: false,
|
||||
isWaitingAgent: false,
|
||||
),
|
||||
);
|
||||
final textHeight = tester.getSize(
|
||||
find.byKey(messageComposerContainerKey),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
_buildTestApp(
|
||||
mode: MessageComposerMode.holdToSpeak,
|
||||
process: MessageComposerProcess.idle,
|
||||
hasMessage: false,
|
||||
isWaitingAgent: false,
|
||||
),
|
||||
);
|
||||
final holdHeight = tester.getSize(
|
||||
find.byKey(messageComposerContainerKey),
|
||||
);
|
||||
|
||||
expect(textHeight.height, holdHeight.height);
|
||||
});
|
||||
|
||||
testWidgets('invokes long press start/end callbacks in hold mode', (
|
||||
tester,
|
||||
) async {
|
||||
var started = false;
|
||||
var ended = false;
|
||||
|
||||
await tester.pumpWidget(
|
||||
_buildTestApp(
|
||||
mode: MessageComposerMode.holdToSpeak,
|
||||
process: MessageComposerProcess.idle,
|
||||
hasMessage: false,
|
||||
isWaitingAgent: false,
|
||||
onHoldStart: () => started = true,
|
||||
onHoldEnd: () => ended = true,
|
||||
),
|
||||
);
|
||||
|
||||
final center = tester.getCenter(find.byKey(messageComposerHoldAreaKey));
|
||||
final gesture = await tester.startGesture(center);
|
||||
await tester.pump(kLongPressTimeout + const Duration(milliseconds: 10));
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
expect(started, isTrue);
|
||||
expect(ended, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('invokes long press cancel callback when gesture canceled', (
|
||||
tester,
|
||||
) async {
|
||||
var canceled = false;
|
||||
|
||||
await tester.pumpWidget(
|
||||
_buildTestApp(
|
||||
mode: MessageComposerMode.holdToSpeak,
|
||||
process: MessageComposerProcess.idle,
|
||||
hasMessage: false,
|
||||
isWaitingAgent: false,
|
||||
onHoldCancel: () => canceled = true,
|
||||
),
|
||||
);
|
||||
|
||||
final center = tester.getCenter(find.byKey(messageComposerHoldAreaKey));
|
||||
final gesture = await tester.startGesture(center);
|
||||
await tester.pump(kLongPressTimeout + const Duration(milliseconds: 10));
|
||||
await gesture.cancel();
|
||||
await tester.pump();
|
||||
|
||||
expect(canceled, isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -195,9 +195,7 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('switching to text mode does not auto focus input', (
|
||||
tester,
|
||||
) async {
|
||||
testWidgets('switching to text mode auto focuses input', (tester) async {
|
||||
await pumpHomeScreen(tester);
|
||||
|
||||
await tester.tap(find.byKey(messageComposerRightButtonKey));
|
||||
@@ -205,12 +203,10 @@ void main() {
|
||||
await tester.pump();
|
||||
|
||||
final editable = tester.widget<EditableText>(find.byType(EditableText));
|
||||
expect(editable.focusNode.hasFocus, isFalse);
|
||||
expect(editable.focusNode.hasFocus, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('single tap on input focuses text field after mode switch', (
|
||||
tester,
|
||||
) async {
|
||||
testWidgets('single tap on input keeps text field focused', (tester) async {
|
||||
await pumpHomeScreen(tester);
|
||||
|
||||
await tester.tap(find.byKey(messageComposerRightButtonKey));
|
||||
@@ -224,7 +220,9 @@ void main() {
|
||||
expect(editable.focusNode.hasFocus, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('tap focused input triggers keyboard show once', (tester) async {
|
||||
testWidgets('switching to text mode triggers keyboard show fallback', (
|
||||
tester,
|
||||
) async {
|
||||
var showCalls = 0;
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(SystemChannels.textInput, (call) async {
|
||||
@@ -242,15 +240,92 @@ void main() {
|
||||
await tester.tap(find.byKey(messageComposerRightButtonKey));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 130));
|
||||
|
||||
await tester.tap(find.byType(EditableText));
|
||||
expect(showCalls, greaterThanOrEqualTo(1));
|
||||
});
|
||||
|
||||
testWidgets('tap center of input lane focuses text field', (tester) async {
|
||||
await pumpHomeScreen(tester);
|
||||
|
||||
await tester.tap(find.byKey(messageComposerRightButtonKey));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
final composerRect = tester.getRect(find.byKey(messageComposerInnerKey));
|
||||
final centerLaneTap = Offset(
|
||||
composerRect.left + composerRect.width * 0.5,
|
||||
composerRect.center.dy,
|
||||
);
|
||||
|
||||
await tester.tapAt(centerLaneTap);
|
||||
await tester.pump();
|
||||
|
||||
final editable = tester.widget<EditableText>(find.byType(EditableText));
|
||||
expect(editable.focusNode.hasFocus, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('tap focused input triggers at most one keyboard show', (
|
||||
tester,
|
||||
) async {
|
||||
var showCalls = 0;
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(SystemChannels.textInput, (call) async {
|
||||
if (call.method == 'TextInput.show') {
|
||||
showCalls += 1;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
addTearDown(() {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(SystemChannels.textInput, null);
|
||||
});
|
||||
|
||||
await pumpHomeScreen(tester);
|
||||
await tester.tap(find.byKey(messageComposerRightButtonKey));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 130));
|
||||
|
||||
showCalls = 0;
|
||||
|
||||
await tester.tap(find.byType(EditableText));
|
||||
await tester.pump();
|
||||
|
||||
expect(showCalls, 1);
|
||||
expect(showCalls, lessThanOrEqualTo(1));
|
||||
});
|
||||
|
||||
testWidgets('double toggle returns to hold-to-speak mode', (tester) async {
|
||||
await pumpHomeScreen(tester);
|
||||
|
||||
await tester.tap(find.byKey(messageComposerRightButtonKey));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
expect(find.byType(EditableText), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byKey(messageComposerRightButtonKey));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(EditableText), findsNothing);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('rapid triple toggle ends in text mode with focused input', (
|
||||
tester,
|
||||
) async {
|
||||
await pumpHomeScreen(tester);
|
||||
|
||||
await tester.tap(find.byKey(messageComposerRightButtonKey));
|
||||
await tester.pump();
|
||||
await tester.tap(find.byKey(messageComposerRightButtonKey));
|
||||
await tester.pump();
|
||||
await tester.tap(find.byKey(messageComposerRightButtonKey));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
final editable = tester.widget<EditableText>(find.byType(EditableText));
|
||||
expect(editable.focusNode.hasFocus, isTrue);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('release during delayed start continues to transcribe path', (
|
||||
|
||||
Reference in New Issue
Block a user