import 'dart:async'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hepan_everyday/features/feedback/feedback_page.dart'; import 'package:hepan_everyday/features/feedback/feedback_submission.dart'; class RecordingSender implements FeedbackSender { final submissions = []; Completer? pending; bool fail = false; bool uncertain = false; @override Future send(FeedbackSubmission submission) async { submissions.add(submission); if (pending != null) await pending!.future; if (uncertain) throw FeedbackDeliveryUncertain(submission.id); if (fail) throw const FeedbackException('网络暂时不可用,请重试'); } } Future reveal( WidgetTester tester, Finder target, { bool backwards = false, }) async { await tester.scrollUntilVisible( target, backwards ? -250 : 250, scrollable: find.byType(Scrollable).first, ); await tester.pump(); } void main() { testWidgets('uncertain delivery retains original payload for status check', ( tester, ) async { final sender = RecordingSender()..uncertain = true; await tester.pumpWidget(MaterialApp(home: FeedbackPage(sender: sender))); await tester.enterText(find.byKey(const Key('feedback-message')), '原来的反馈'); await reveal(tester, find.byKey(const Key('feedback-submit'))); await tester.tap(find.byKey(const Key('feedback-submit'))); await tester.pumpAndSettle(); expect(find.text('提交成功'), findsNothing); expect(find.text('提交状态待确认'), findsOneWidget); expect(find.byKey(const Key('feedback-message')), findsNothing); expect(find.byKey(const Key('feedback-submit')), findsNothing); final original = sender.submissions.single; expect( tester .widget( find.byKey(const Key('feedback-uncertain-id')), ) .data, original.id, ); sender.uncertain = false; await reveal(tester, find.byKey(const Key('feedback-check-original'))); await tester.tap(find.byKey(const Key('feedback-check-original'))); await tester.pumpAndSettle(); expect(sender.submissions, hasLength(2)); expect(identical(sender.submissions[0], sender.submissions[1]), isTrue); expect(find.text('提交成功'), findsOneWidget); }); test('submission trims relevant fields and rejects whitespace', () { expect( () => FeedbackSubmission.problem(id: 'a', message: ' '), throwsA(isA()), ); final problem = FeedbackSubmission.problem( id: 'a', message: ' 页面建议 ', contact: ' ', ); expect(problem.message, '页面建议'); expect(problem.contact, isNull); expect(problem.school, isNull); final application = FeedbackSubmission.development( id: 'b', school: ' 某大学 ', contact: ' 微信 abc ', ); expect(application.school, '某大学'); expect(application.contact, '微信 abc'); expect(application.message, isNull); expect(application.attachments, isEmpty); expect( () => FeedbackSubmission.development(id: 'b', school: ' ', contact: 'x'), throwsA(isA()), ); expect( () => FeedbackSubmission.development(id: 'b', school: 'x', contact: ' '), throwsA(isA()), ); }); test('attachment read stops at byte limit and sanitizes names', () async { var reads = 0; Stream> oversized() async* { reads++; yield [1, 2, 3]; reads++; yield [4, 5, 6]; reads++; yield [7]; } await expectLater( FeedbackAttachment.read(name: 'x.png', stream: oversized(), maxBytes: 4), throwsA(isA()), ); expect(reads, 2); final attachment = await FeedbackAttachment.read( name: '../../unsafe\r\nname.PNG', stream: Stream.value([1, 2]), ); expect(attachment.filename, 'unsafename.PNG'); expect(attachment.contentType, 'image/png'); await expectLater( FeedbackAttachment.read(name: 'x.exe', stream: Stream.value([1])), throwsA(isA()), ); }); test('submission enforces attachment count and total limits', () async { final file = await FeedbackAttachment.read( name: 'x.jpg', stream: Stream.value([1]), ); expect( () => FeedbackSubmission.problem( id: 'a', message: 'x', attachments: [file, file, file, file], ), throwsA(isA()), ); final large = await FeedbackAttachment.read( name: 'x.mp4', stream: Stream.value(Uint8List(8 * 1024 * 1024)), ); expect( () => FeedbackSubmission.problem( id: 'a', message: 'x', attachments: [large, large, large], ), throwsA(isA()), ); }); testWidgets('problem requires message; contact is optional', (tester) async { final sender = RecordingSender(); await tester.pumpWidget(MaterialApp(home: FeedbackPage(sender: sender))); await reveal(tester, find.byKey(const Key('feedback-submit'))); await tester.tap(find.byKey(const Key('feedback-submit'))); await tester.pumpAndSettle(); expect(find.text('请填写问题或建议'), findsOneWidget); expect(sender.submissions, isEmpty); await reveal( tester, find.byKey(const Key('feedback-message')), backwards: true, ); await tester.enterText(find.byKey(const Key('feedback-message')), ' 建议内容 '); await reveal(tester, find.byKey(const Key('feedback-submit'))); await tester.tap(find.byKey(const Key('feedback-submit'))); await tester.pumpAndSettle(); expect(sender.submissions.single.message, '建议内容'); expect(sender.submissions.single.contact, isNull); expect(find.text('提交成功'), findsOneWidget); }); testWidgets('development requires school/contact and excludes hidden data', ( tester, ) async { final sender = RecordingSender(); await tester.pumpWidget( MaterialApp( home: FeedbackPage( sender: sender, pickAttachment: () async => FeedbackAttachment.read( name: 'hidden.png', stream: Stream.value([1, 2, 3]), ), ), ), ); await tester.enterText(find.byKey(const Key('feedback-message')), '旧问题'); await tester.ensureVisible(find.byKey(const Key('feedback-add-media'))); await tester.tap(find.byKey(const Key('feedback-add-media'))); await tester.pumpAndSettle(); await reveal(tester, find.text('我要参与开发'), backwards: true); await tester.tap(find.text('我要参与开发')); await tester.pumpAndSettle(); expect(find.byKey(const Key('feedback-message')), findsNothing); expect(find.byKey(const Key('feedback-add-media')), findsNothing); await reveal(tester, find.byKey(const Key('feedback-submit'))); await tester.tap(find.byKey(const Key('feedback-submit'))); await tester.pumpAndSettle(); expect(find.text('请填写学校'), findsOneWidget); expect(find.text('请填写联系方式'), findsOneWidget); await tester.enterText(find.byKey(const Key('feedback-school')), ' 某大学 '); await tester.enterText( find.byKey(const Key('feedback-contact')), ' QQ 123 ', ); await reveal(tester, find.byKey(const Key('feedback-submit'))); await tester.tap(find.byKey(const Key('feedback-submit'))); await tester.pumpAndSettle(); final submission = sender.submissions.single; expect(submission.kind, FeedbackKind.development); expect(submission.school, '某大学'); expect(submission.contact, 'QQ 123'); expect(submission.message, isNull); expect(submission.attachments, isEmpty); }); testWidgets( 'pending send prevents duplicates; failure preserves data and retry ID', (tester) async { final sender = RecordingSender() ..pending = Completer() ..fail = true; await tester.pumpWidget(MaterialApp(home: FeedbackPage(sender: sender))); await tester.enterText(find.byKey(const Key('feedback-message')), '问题'); await reveal(tester, find.byKey(const Key('feedback-submit'))); await tester.tap(find.byKey(const Key('feedback-submit'))); await tester.pump(); await tester.tap(find.byKey(const Key('feedback-submit'))); await tester.pump(); expect(sender.submissions, hasLength(1)); expect(find.text('提交成功'), findsNothing); sender.pending!.complete(); await tester.pumpAndSettle(); expect(find.text('网络暂时不可用,请重试'), findsOneWidget); expect(find.text('问题'), findsOneWidget); sender ..pending = null ..fail = false; await reveal(tester, find.byKey(const Key('feedback-submit'))); await tester.tap(find.byKey(const Key('feedback-submit'))); await tester.pumpAndSettle(); expect(sender.submissions, hasLength(2)); expect(sender.submissions[0].id, sender.submissions[1].id); expect(find.text('提交成功'), findsOneWidget); }, ); testWidgets('editing a failed request starts a new submission ID', ( tester, ) async { final sender = RecordingSender()..fail = true; await tester.pumpWidget(MaterialApp(home: FeedbackPage(sender: sender))); await tester.enterText(find.byKey(const Key('feedback-message')), '原来的问题'); await reveal(tester, find.byKey(const Key('feedback-submit'))); await tester.tap(find.byKey(const Key('feedback-submit'))); await tester.pumpAndSettle(); await reveal( tester, find.byKey(const Key('feedback-message')), backwards: true, ); await tester.enterText(find.byKey(const Key('feedback-message')), '更新的问题'); sender.fail = false; await reveal(tester, find.byKey(const Key('feedback-submit'))); await tester.tap(find.byKey(const Key('feedback-submit'))); await tester.pumpAndSettle(); expect(sender.submissions[1].id, isNot(sender.submissions[0].id)); expect(sender.submissions[1].message, '更新的问题'); }); testWidgets('unconfigured service gives honest error and preserves input', ( tester, ) async { await tester.pumpWidget(const MaterialApp(home: FeedbackPage())); await tester.enterText(find.byKey(const Key('feedback-message')), '问题'); await reveal(tester, find.byKey(const Key('feedback-submit'))); await tester.tap(find.byKey(const Key('feedback-submit'))); await tester.pumpAndSettle(); expect(find.text(UnconfiguredFeedbackSender.message), findsOneWidget); expect(find.text('提交成功'), findsNothing); expect(find.text('问题'), findsOneWidget); }); testWidgets('selected media can be removed and survives send failure', ( tester, ) async { final sender = RecordingSender()..fail = true; await tester.pumpWidget( MaterialApp( home: FeedbackPage( sender: sender, pickAttachment: () async => FeedbackAttachment.read( name: 'screen.png', stream: Stream.value([1, 2, 3]), ), ), ), ); await tester.enterText(find.byKey(const Key('feedback-message')), '问题'); await tester.ensureVisible(find.byKey(const Key('feedback-add-media'))); await tester.tap(find.byKey(const Key('feedback-add-media'))); await tester.pumpAndSettle(); expect(find.text('screen.png'), findsOneWidget); await reveal(tester, find.byKey(const Key('feedback-submit'))); await tester.tap(find.byKey(const Key('feedback-submit'))); await tester.pumpAndSettle(); expect(sender.submissions.single.attachments, hasLength(1)); expect(find.text('screen.png'), findsOneWidget); await tester.ensureVisible(find.byTooltip('移除 screen.png')); await tester.tap(find.byTooltip('移除 screen.png')); await tester.pumpAndSettle(); expect(find.text('screen.png'), findsNothing); }); }