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,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:social_app/core/api/i_api_client.dart';
|
||||
@@ -5,9 +7,15 @@ import 'package:social_app/features/chat/data/models/ag_ui_event.dart';
|
||||
import 'package:social_app/features/chat/data/services/ag_ui_service.dart';
|
||||
|
||||
class _FakeApiClient implements IApiClient {
|
||||
_FakeApiClient({required this.sseLines});
|
||||
_FakeApiClient({
|
||||
required this.sseLines,
|
||||
this.sseLineStreamFactory,
|
||||
this.runIdFactory,
|
||||
});
|
||||
|
||||
final List<String> sseLines;
|
||||
final Stream<String> Function()? sseLineStreamFactory;
|
||||
final String Function()? runIdFactory;
|
||||
|
||||
@override
|
||||
Future<Response<T>> delete<T>(String path, {data, Options? options}) {
|
||||
@@ -24,6 +32,10 @@ class _FakeApiClient implements IApiClient {
|
||||
String path, {
|
||||
Map<String, String>? headers,
|
||||
}) async {
|
||||
final streamFactory = sseLineStreamFactory;
|
||||
if (streamFactory != null) {
|
||||
return streamFactory();
|
||||
}
|
||||
return Stream<String>.fromIterable(sseLines);
|
||||
}
|
||||
|
||||
@@ -34,10 +46,11 @@ class _FakeApiClient implements IApiClient {
|
||||
|
||||
@override
|
||||
Future<Response<T>> post<T>(String path, {data, Options? options}) async {
|
||||
final runIdFactory = this.runIdFactory;
|
||||
final payload = <String, dynamic>{
|
||||
'taskId': 'task-1',
|
||||
'threadId': 'thread-1',
|
||||
'runId': 'run-new',
|
||||
'runId': runIdFactory != null ? runIdFactory() : 'run-new',
|
||||
'created': true,
|
||||
};
|
||||
return Response<T>(
|
||||
@@ -149,4 +162,110 @@ void main() {
|
||||
expect(events[2], isA<RunFinishedEvent>());
|
||||
},
|
||||
);
|
||||
|
||||
test('cancelCurrentRun actively closes current SSE subscription', () async {
|
||||
var streamCancelled = false;
|
||||
final streamController = StreamController<String>(
|
||||
onCancel: () {
|
||||
streamCancelled = true;
|
||||
},
|
||||
);
|
||||
|
||||
final service = AgUiService(
|
||||
apiClient: _FakeApiClient(
|
||||
sseLines: const <String>[],
|
||||
sseLineStreamFactory: () => streamController.stream,
|
||||
),
|
||||
);
|
||||
|
||||
final sendFuture = service.sendMessage('hello');
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
await service.cancelCurrentRun();
|
||||
|
||||
await sendFuture;
|
||||
expect(streamCancelled, isTrue);
|
||||
await streamController.close();
|
||||
});
|
||||
|
||||
test(
|
||||
'new sendMessage cancels previous SSE subscription explicitly',
|
||||
() async {
|
||||
var firstStreamCancelled = false;
|
||||
final firstController = StreamController<String>(
|
||||
onCancel: () {
|
||||
firstStreamCancelled = true;
|
||||
},
|
||||
);
|
||||
final secondController = StreamController<String>();
|
||||
final streamQueue = <StreamController<String>>[
|
||||
firstController,
|
||||
secondController,
|
||||
];
|
||||
var streamIndex = 0;
|
||||
var runIndex = 0;
|
||||
|
||||
final service = AgUiService(
|
||||
apiClient: _FakeApiClient(
|
||||
sseLines: const <String>[],
|
||||
sseLineStreamFactory: () => streamQueue[streamIndex++].stream,
|
||||
runIdFactory: () {
|
||||
runIndex += 1;
|
||||
return 'run-$runIndex';
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final firstSendFuture = service.sendMessage('first');
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
final secondSendFuture = service.sendMessage('second');
|
||||
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
for (final line in _buildSseEvent(
|
||||
id: '21',
|
||||
type: AgUiEventTypeWire.runStarted,
|
||||
payload: '{"type":"RUN_STARTED","threadId":"thread-1","runId":"run-2"}',
|
||||
)) {
|
||||
secondController.add(line);
|
||||
}
|
||||
for (final line in _buildSseEvent(
|
||||
id: '22',
|
||||
type: AgUiEventTypeWire.runFinished,
|
||||
payload:
|
||||
'{"type":"RUN_FINISHED","threadId":"thread-1","runId":"run-2"}',
|
||||
)) {
|
||||
secondController.add(line);
|
||||
}
|
||||
await secondController.close();
|
||||
|
||||
await firstSendFuture;
|
||||
await secondSendFuture;
|
||||
|
||||
expect(firstStreamCancelled, isTrue);
|
||||
await firstController.close();
|
||||
},
|
||||
);
|
||||
|
||||
test('sendMessage surfaces event callback exceptions', () async {
|
||||
final service = AgUiService(
|
||||
apiClient: _FakeApiClient(
|
||||
sseLines: <String>[
|
||||
..._buildSseEvent(
|
||||
id: '31',
|
||||
type: AgUiEventTypeWire.runStarted,
|
||||
payload:
|
||||
'{"type":"RUN_STARTED","threadId":"thread-1","runId":"run-new"}',
|
||||
),
|
||||
..._buildSseEvent(
|
||||
id: '32',
|
||||
type: AgUiEventTypeWire.runFinished,
|
||||
payload:
|
||||
'{"type":"RUN_FINISHED","threadId":"thread-1","runId":"run-new"}',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
service.onEvent = (_) => throw StateError('event callback failed');
|
||||
|
||||
await expectLater(service.sendMessage('hello'), throwsA(isA<StateError>()));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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', (
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:social_app/core/di/injection.dart';
|
||||
import 'package:social_app/features/auth/data/auth_repository.dart';
|
||||
import 'package:social_app/features/auth/presentation/bloc/auth_bloc.dart';
|
||||
import 'package:social_app/features/auth/presentation/bloc/auth_event.dart';
|
||||
import 'package:social_app/features/auth/presentation/bloc/auth_state.dart';
|
||||
import 'package:social_app/features/settings/ui/screens/change_password_screen.dart';
|
||||
|
||||
class MockAuthRepository extends Mock implements AuthRepository {}
|
||||
|
||||
void main() {
|
||||
late MockAuthRepository mockAuthRepository;
|
||||
late AuthBloc authBloc;
|
||||
|
||||
setUp(() async {
|
||||
mockAuthRepository = MockAuthRepository();
|
||||
await sl.reset();
|
||||
sl.registerSingleton<AuthRepository>(mockAuthRepository);
|
||||
|
||||
authBloc = AuthBloc(mockAuthRepository);
|
||||
authBloc.add(
|
||||
const AuthLoggedIn(
|
||||
user: AuthUser(id: 'user-1', email: 'tester@example.com'),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await authBloc.close();
|
||||
await sl.reset();
|
||||
});
|
||||
|
||||
Future<void> pumpScreen(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
BlocProvider<AuthBloc>.value(
|
||||
value: authBloc,
|
||||
child: const MaterialApp(home: ChangePasswordScreen()),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
testWidgets('确认修改按钮在验证码发送前不可点击', (tester) async {
|
||||
when(
|
||||
() => mockAuthRepository.requestPasswordReset(any()),
|
||||
).thenAnswer((_) async {});
|
||||
|
||||
await pumpScreen(tester);
|
||||
|
||||
final confirmButton = tester.widget<ElevatedButton>(
|
||||
find.widgetWithText(ElevatedButton, '确认修改'),
|
||||
);
|
||||
expect(confirmButton.onPressed, isNull);
|
||||
expect(find.text('设置新密码'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('发送验证码倒计时期间不会重复触发请求', (tester) async {
|
||||
final completer = Completer<void>();
|
||||
when(
|
||||
() => mockAuthRepository.requestPasswordReset(any()),
|
||||
).thenAnswer((_) => completer.future);
|
||||
|
||||
await pumpScreen(tester);
|
||||
|
||||
await tester.tap(find.widgetWithText(ElevatedButton, '发送验证码'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('60 秒后可重发'), findsOneWidget);
|
||||
expect(find.text('设置新密码'), findsOneWidget);
|
||||
|
||||
verify(
|
||||
() => mockAuthRepository.requestPasswordReset('tester@example.com'),
|
||||
).called(1);
|
||||
|
||||
completer.complete();
|
||||
});
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:social_app/features/settings/ui/widgets/account_section_card.dart';
|
||||
import 'package:social_app/features/settings/ui/widgets/settings_page_scaffold.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('AccountSectionCard renders title and description', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: AccountSectionCard(
|
||||
title: '基础信息',
|
||||
description: '请填写公开展示资料',
|
||||
child: Text('内容区'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('基础信息'), findsOneWidget);
|
||||
expect(find.text('请填写公开展示资料'), findsOneWidget);
|
||||
expect(find.text('内容区'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('SettingsPageScaffold renders header and footer', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SettingsPageScaffold(
|
||||
title: '编辑资料',
|
||||
body: const Text('主体内容'),
|
||||
footer: const Text('底部操作区'),
|
||||
onBack: () {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('编辑资料'), findsOneWidget);
|
||||
expect(find.text('主体内容'), findsOneWidget);
|
||||
expect(find.text('底部操作区'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('SettingsPageScaffold renders body without footer', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SettingsPageScaffold(
|
||||
title: '账户',
|
||||
body: const Text('主体内容'),
|
||||
onBack: () {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('账户'), findsOneWidget);
|
||||
expect(find.text('主体内容'), findsOneWidget);
|
||||
expect(find.text('底部操作区'), findsNothing);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user