Files
TPSchedule/mobile/test/features/feedback_test.dart
T

321 lines
12 KiB
Dart

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 = <FeedbackSubmission>[];
Completer<void>? pending;
bool fail = false;
bool uncertain = false;
@override
Future<void> 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<void> 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<SelectableText>(
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<FeedbackException>()),
);
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<FeedbackException>()),
);
expect(
() => FeedbackSubmission.development(id: 'b', school: 'x', contact: ' '),
throwsA(isA<FeedbackException>()),
);
});
test('attachment read stops at byte limit and sanitizes names', () async {
var reads = 0;
Stream<List<int>> 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<FeedbackException>()),
);
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<FeedbackException>()),
);
});
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<FeedbackException>()),
);
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<FeedbackException>()),
);
});
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<void>()
..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);
});
}