Add feedback service, reminder diagnostics and smooth week transitions
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hepan_everyday/features/feedback/feedback_http_sender.dart';
|
||||
import 'package:hepan_everyday/features/feedback/feedback_submission.dart';
|
||||
|
||||
class FakeFeedbackHttpTransport implements FeedbackHttpTransport {
|
||||
int calls = 0;
|
||||
Uri? endpoint;
|
||||
Map<String, String>? headers;
|
||||
Uint8List? body;
|
||||
FeedbackHttpResponse response = FeedbackHttpResponse(
|
||||
200,
|
||||
utf8.encode('{"success":true,"submission_id":"same-id","status":"sent"}'),
|
||||
);
|
||||
Completer<FeedbackHttpResponse>? pending;
|
||||
|
||||
@override
|
||||
Future<FeedbackHttpResponse> post({
|
||||
required Uri endpoint,
|
||||
required Map<String, String> headers,
|
||||
required Uint8List body,
|
||||
required Duration timeout,
|
||||
}) async {
|
||||
calls++;
|
||||
this.endpoint = endpoint;
|
||||
this.headers = headers;
|
||||
this.body = body;
|
||||
return pending == null ? response : await pending!.future;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
test(
|
||||
'matching uncertain delivery is distinct and never exposes server detail',
|
||||
() async {
|
||||
final transport = FakeFeedbackHttpTransport();
|
||||
final sender = HttpFeedbackSender(
|
||||
endpoint: 'https://example.com/v1/feedback',
|
||||
transport: transport,
|
||||
);
|
||||
final submission = FeedbackSubmission.problem(
|
||||
id: 'same-id',
|
||||
message: '问题',
|
||||
);
|
||||
for (final code in ['delivery_uncertain', 'storage_unavailable']) {
|
||||
transport.response = FeedbackHttpResponse(
|
||||
503,
|
||||
utf8.encode(
|
||||
jsonEncode({
|
||||
'success': false,
|
||||
'error': code,
|
||||
'detail': 'private SMTP error',
|
||||
'submission_id': 'same-id',
|
||||
'status': 'uncertain',
|
||||
}),
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
sender.send(submission),
|
||||
throwsA(
|
||||
isA<FeedbackDeliveryUncertain>()
|
||||
.having((error) => error.submissionId, 'original ID', 'same-id')
|
||||
.having(
|
||||
(error) => error.message,
|
||||
'safe instructions',
|
||||
allOf(
|
||||
contains('请勿修改'),
|
||||
isNot(contains('private SMTP error')),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
transport.response = FeedbackHttpResponse(
|
||||
503,
|
||||
utf8.encode(
|
||||
jsonEncode({
|
||||
'success': false,
|
||||
'error': 'delivery_uncertain',
|
||||
'submission_id': 'other-id',
|
||||
'status': 'uncertain',
|
||||
}),
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
sender.send(submission),
|
||||
throwsA(
|
||||
allOf(
|
||||
isA<FeedbackException>(),
|
||||
isNot(isA<FeedbackDeliveryUncertain>()),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'permanent media/validation failures give actionable safe messages',
|
||||
() async {
|
||||
final transport = FakeFeedbackHttpTransport();
|
||||
final sender = HttpFeedbackSender(
|
||||
endpoint: 'https://example.com/v1/feedback',
|
||||
transport: transport,
|
||||
);
|
||||
final submission = FeedbackSubmission.problem(
|
||||
id: 'same-id',
|
||||
message: '问题',
|
||||
);
|
||||
for (final rejection in [
|
||||
(status: 415, code: 'unsupported_media', expected: '移除附件'),
|
||||
(status: 422, code: 'validation_error', expected: '必填内容'),
|
||||
]) {
|
||||
transport.response = FeedbackHttpResponse(
|
||||
rejection.status,
|
||||
utf8.encode(
|
||||
jsonEncode({
|
||||
'success': false,
|
||||
'error': rejection.code,
|
||||
'detail': 'untrusted detail',
|
||||
}),
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
sender.send(submission),
|
||||
throwsA(
|
||||
isA<FeedbackException>().having(
|
||||
(error) => error.message,
|
||||
'actionable message',
|
||||
allOf(
|
||||
contains(rejection.expected),
|
||||
isNot(contains('untrusted detail')),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
transport.response = FeedbackHttpResponse(
|
||||
503,
|
||||
utf8.encode(
|
||||
jsonEncode({
|
||||
'success': false,
|
||||
'error': 'smtp_unavailable',
|
||||
'submission_id': 'same-id',
|
||||
'status': 'retryable',
|
||||
}),
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
sender.send(submission),
|
||||
throwsA(
|
||||
allOf(
|
||||
isA<FeedbackException>().having(
|
||||
(error) => error.message,
|
||||
'safe retry',
|
||||
contains('原提交'),
|
||||
),
|
||||
isNot(isA<FeedbackDeliveryUncertain>()),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('requires HTTPS before contacting transport', () async {
|
||||
final transport = FakeFeedbackHttpTransport();
|
||||
final submission = FeedbackSubmission.problem(id: 'same-id', message: '问题');
|
||||
for (final endpoint in [
|
||||
'',
|
||||
'http://example.com/v1/feedback',
|
||||
'https://user:pass@example.com/v1/feedback',
|
||||
]) {
|
||||
await expectLater(
|
||||
HttpFeedbackSender(
|
||||
endpoint: endpoint,
|
||||
transport: transport,
|
||||
).send(submission),
|
||||
throwsA(isA<FeedbackException>()),
|
||||
);
|
||||
}
|
||||
expect(transport.calls, 0);
|
||||
});
|
||||
|
||||
test(
|
||||
'problem multipart contains relevant fields and repeated media parts',
|
||||
() async {
|
||||
final transport = FakeFeedbackHttpTransport();
|
||||
final file = await FeedbackAttachment.read(
|
||||
name: 'x.png',
|
||||
stream: Stream.value([1, 2, 3]),
|
||||
);
|
||||
final sender = HttpFeedbackSender(
|
||||
endpoint: 'https://example.com/v1/feedback',
|
||||
transport: transport,
|
||||
);
|
||||
await sender.send(
|
||||
FeedbackSubmission.problem(
|
||||
id: 'same-id',
|
||||
message: ' 问题 ',
|
||||
attachments: [file, file],
|
||||
),
|
||||
);
|
||||
final body = utf8.decode(transport.body!, allowMalformed: true);
|
||||
expect(body, contains('name="submission_id"\r\n\r\nsame-id'));
|
||||
expect(body, contains('name="type"\r\n\r\nproblem'));
|
||||
expect(body, contains('name="message"\r\n\r\n问题'));
|
||||
expect(body, isNot(contains('name="school"')));
|
||||
expect(body, isNot(contains('name="contact"')));
|
||||
expect('name="attachments"'.allMatches(body), hasLength(2));
|
||||
expect(body, contains('Content-Type: image/png'));
|
||||
expect(
|
||||
transport.headers!['Content-Type'],
|
||||
startsWith('multipart/form-data; boundary='),
|
||||
);
|
||||
expect(transport.headers!.keys, isNot(contains('Authorization')));
|
||||
},
|
||||
);
|
||||
|
||||
test('development multipart sends school and contact without problem/media fields', () async {
|
||||
final transport = FakeFeedbackHttpTransport();
|
||||
final sender = HttpFeedbackSender(
|
||||
endpoint: 'https://example.com/v1/feedback',
|
||||
transport: transport,
|
||||
);
|
||||
await sender.send(
|
||||
FeedbackSubmission.development(
|
||||
id: 'same-id',
|
||||
school: '某大学',
|
||||
contact: 'QQ 123',
|
||||
),
|
||||
);
|
||||
final body = utf8.decode(transport.body!);
|
||||
expect(body, contains('name="type"\r\n\r\ndevelopment'));
|
||||
expect(body, contains('name="school"\r\n\r\n某大学'));
|
||||
expect(body, contains('name="contact"\r\n\r\nQQ 123'));
|
||||
expect(body, isNot(contains('name="message"')));
|
||||
expect(body, isNot(contains('name="attachments"')));
|
||||
});
|
||||
|
||||
test('only matching confirmed sent response is successful', () async {
|
||||
final transport = FakeFeedbackHttpTransport();
|
||||
final sender = HttpFeedbackSender(
|
||||
endpoint: 'https://example.com/v1/feedback',
|
||||
transport: transport,
|
||||
);
|
||||
final submission = FeedbackSubmission.problem(id: 'same-id', message: '问题');
|
||||
for (final response in [
|
||||
FeedbackHttpResponse(200, utf8.encode('{"success":true}')),
|
||||
FeedbackHttpResponse(
|
||||
200,
|
||||
utf8.encode('{"success":true,"submission_id":"other","status":"sent"}'),
|
||||
),
|
||||
FeedbackHttpResponse(
|
||||
200,
|
||||
utf8.encode(
|
||||
'{"success":true,"submission_id":"same-id","status":"queued"}',
|
||||
),
|
||||
),
|
||||
FeedbackHttpResponse(
|
||||
200,
|
||||
utf8.encode(
|
||||
'{"success":false,"submission_id":"same-id","status":"sent"}',
|
||||
),
|
||||
),
|
||||
FeedbackHttpResponse(
|
||||
500,
|
||||
utf8.encode(
|
||||
'{"success":true,"submission_id":"same-id","status":"sent"}',
|
||||
),
|
||||
),
|
||||
FeedbackHttpResponse(200, utf8.encode('bad JSON')),
|
||||
FeedbackHttpResponse(200, Uint8List(65537)),
|
||||
]) {
|
||||
transport.response = response;
|
||||
await expectLater(
|
||||
sender.send(submission),
|
||||
throwsA(isA<FeedbackException>()),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test(
|
||||
'timeout exposes safe error and repeated request keeps submission ID',
|
||||
() async {
|
||||
final transport = FakeFeedbackHttpTransport()
|
||||
..pending = Completer<FeedbackHttpResponse>();
|
||||
final sender = HttpFeedbackSender(
|
||||
endpoint: 'https://example.com/v1/feedback',
|
||||
transport: transport,
|
||||
timeout: const Duration(milliseconds: 1),
|
||||
);
|
||||
final submission = FeedbackSubmission.problem(
|
||||
id: 'same-id',
|
||||
message: '问题',
|
||||
);
|
||||
await expectLater(
|
||||
sender.send(submission),
|
||||
throwsA(isA<FeedbackException>()),
|
||||
);
|
||||
transport.pending = null;
|
||||
await sender.send(submission);
|
||||
expect(transport.calls, 2);
|
||||
expect(
|
||||
utf8.decode(transport.body!),
|
||||
contains('name="submission_id"\r\n\r\nsame-id'),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user