feat(flutter): 接入互动评论点赞举报
This commit is contained in:
1 parent
45f5f25640
commit
d361034226
39 files changed
+3298
-462
No files matched your search
@@ -0,0 +1,129 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/data/remote/api_client.dart';
|
||||
import 'package:fruitshorts/core/data/remote/comment_api.dart';
|
||||
|
||||
import '../support/fake_http_adapter.dart';
|
||||
|
||||
void main() {
|
||||
String envelope(Object? data) =>
|
||||
jsonEncode({'code': 0, 'message': 'ok', 'data': data});
|
||||
|
||||
Map<String, dynamic> commentJson({
|
||||
String id = '2111012753262120962',
|
||||
String content = 'Great episode',
|
||||
String status = 'published',
|
||||
bool mine = false,
|
||||
bool liked = true,
|
||||
String parentId = '0',
|
||||
String rootId = '0',
|
||||
}) => {
|
||||
'id': id,
|
||||
'dramaId': '5',
|
||||
'episodeId': '7',
|
||||
'parentId': parentId,
|
||||
'rootId': rootId,
|
||||
'userId': '2111012753262120001',
|
||||
'uid': 'viewer-1',
|
||||
'nickname': 'Viewer',
|
||||
'avatarUrl': '',
|
||||
'content': content,
|
||||
'status': status,
|
||||
'auditReason': '',
|
||||
'likeCount': '12',
|
||||
'replyCount': 2,
|
||||
'reportCount': '0',
|
||||
'createdAt': '2026-10-17T12:30:00',
|
||||
'mine': mine,
|
||||
'liked': liked,
|
||||
};
|
||||
|
||||
test('should_parse_cursor_page_and_string_ids_when_comment_list_is_loaded', () async {
|
||||
final adapter = FakeHttpAdapter((options) async {
|
||||
return FakeHttpResponse(
|
||||
envelope({
|
||||
'list': [commentJson()],
|
||||
'nextCursor': '2111012753262120962',
|
||||
'hasMore': true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
final api = CommentApi(
|
||||
ApiClient(baseUrl: 'http://127.0.0.1:8081', adapter: adapter),
|
||||
);
|
||||
|
||||
final page = await api.page(dramaId: '5', cursor: '99', size: 20);
|
||||
|
||||
expect(adapter.requests.single.method, 'GET');
|
||||
expect(adapter.requests.single.uri.path, '/app/comments');
|
||||
expect(adapter.requests.single.queryParameters, {
|
||||
'dramaId': '5',
|
||||
'cursor': '99',
|
||||
'size': '20',
|
||||
});
|
||||
expect(page.comments.single.id, '2111012753262120962');
|
||||
expect(page.comments.single.dramaId, '5');
|
||||
expect(page.comments.single.episodeId, '7');
|
||||
expect(page.comments.single.likeCount, 12);
|
||||
expect(page.nextCursor, '2111012753262120962');
|
||||
expect(page.hasMore, isTrue);
|
||||
});
|
||||
|
||||
test('should_send_parent_id_only_for_reply_and_parse_replaced_content_when_published', () async {
|
||||
final adapter = FakeHttpAdapter((options) async {
|
||||
return FakeHttpResponse(
|
||||
envelope(
|
||||
commentJson(
|
||||
id: '2111012753262120999',
|
||||
content: 'great ***',
|
||||
status: 'pending',
|
||||
mine: true,
|
||||
parentId: '2111012753262120962',
|
||||
rootId: '2111012753262120962',
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
final api = CommentApi(
|
||||
ApiClient(baseUrl: 'http://127.0.0.1:8081', adapter: adapter),
|
||||
);
|
||||
|
||||
final reply = await api.publish(
|
||||
dramaId: '5',
|
||||
content: 'great badword',
|
||||
parentId: '2111012753262120962',
|
||||
);
|
||||
|
||||
expect(adapter.requests.single.method, 'POST');
|
||||
expect(adapter.requests.single.uri.path, '/app/comments');
|
||||
expect(adapter.requests.single.data, {
|
||||
'dramaId': '5',
|
||||
'content': 'great badword',
|
||||
'parentId': '2111012753262120962',
|
||||
});
|
||||
expect(reply.content, 'great ***');
|
||||
expect(reply.status, 'pending');
|
||||
expect(reply.parentId, '2111012753262120962');
|
||||
expect(reply.rootId, '2111012753262120962');
|
||||
});
|
||||
|
||||
test('should_delete_comment_by_string_id_when_requested', () async {
|
||||
final adapter = FakeHttpAdapter(
|
||||
(_) async => FakeHttpResponse(envelope(null)),
|
||||
);
|
||||
final api = CommentApi(
|
||||
ApiClient(baseUrl: 'http://127.0.0.1:8081', adapter: adapter),
|
||||
);
|
||||
|
||||
await api.delete('2111012753262120962');
|
||||
|
||||
expect(adapter.requests.single.method, 'DELETE');
|
||||
expect(
|
||||
adapter.requests.single.uri.path,
|
||||
'/app/comments/2111012753262120962',
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/data/models/comment.dart';
|
||||
import 'package:fruitshorts/core/data/models/interaction.dart';
|
||||
import 'package:fruitshorts/core/data/remote/api_client.dart';
|
||||
import 'package:fruitshorts/core/data/services/comment_controller.dart';
|
||||
|
||||
import '../support/fake_interaction_repositories.dart';
|
||||
|
||||
void main() {
|
||||
CommentController controller(
|
||||
FakeCommentRepository comments, {
|
||||
FakeInteractionRepository? interactions,
|
||||
}) => CommentController(
|
||||
comments: comments,
|
||||
interactions: interactions ?? FakeInteractionRepository(),
|
||||
dramaId: '5',
|
||||
);
|
||||
|
||||
test('should_append_next_page_with_cursor_when_has_more_is_true', () async {
|
||||
final repository = FakeCommentRepository(
|
||||
pages: [
|
||||
CommentPage(
|
||||
comments: [commentFixture(id: '2')],
|
||||
nextCursor: '2',
|
||||
hasMore: true,
|
||||
),
|
||||
CommentPage(comments: [commentFixture(id: '1')]),
|
||||
],
|
||||
);
|
||||
final model = controller(repository);
|
||||
|
||||
await model.load();
|
||||
await model.loadMore();
|
||||
|
||||
expect(model.comments.map((item) => item.id), ['2', '1']);
|
||||
expect(model.hasMore, isFalse);
|
||||
expect(repository.pageCalls[0].cursor, isNull);
|
||||
expect(repository.pageCalls[1].cursor, '2');
|
||||
expect(repository.pageCalls.every((call) => call.size == 20), isTrue);
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
test('should_use_server_content_and_pending_status_when_review_is_returned', () async {
|
||||
final repository = FakeCommentRepository(
|
||||
publishResult: commentFixture(
|
||||
id: '3',
|
||||
content: 'replaced ***',
|
||||
status: 'pending',
|
||||
mine: true,
|
||||
),
|
||||
);
|
||||
final model = controller(repository);
|
||||
|
||||
final result = await model.publish(' replaced badword ');
|
||||
|
||||
expect(result.isSuccess, isTrue);
|
||||
expect(repository.publishCalls.single.content, 'replaced badword');
|
||||
expect(model.comments.single.content, 'replaced ***');
|
||||
expect(model.comments.single.isPending, isTrue);
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
test('should_expose_backend_message_and_keep_existing_list_when_publish_is_blocked', () async {
|
||||
final repository = FakeCommentRepository(
|
||||
publishError: const ApiException(400, '评论包含敏感词'),
|
||||
);
|
||||
final model = controller(repository);
|
||||
await model.load();
|
||||
|
||||
final result = await model.publish('blocked');
|
||||
|
||||
expect(result.isSuccess, isFalse);
|
||||
expect(result.errorMessage, '评论包含敏感词');
|
||||
expect(model.comments, isEmpty);
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
test('should_insert_reply_under_root_and_request_replies_with_root_id', () async {
|
||||
final root = commentFixture(id: '10', replyCount: 1);
|
||||
final reply = commentFixture(
|
||||
id: '11',
|
||||
parentId: '10',
|
||||
rootId: '10',
|
||||
content: 'Reply',
|
||||
mine: true,
|
||||
);
|
||||
final repository = FakeCommentRepository(
|
||||
pages: [
|
||||
CommentPage(comments: [root]),
|
||||
CommentPage(comments: [reply]),
|
||||
],
|
||||
publishResult: reply,
|
||||
);
|
||||
final model = controller(repository);
|
||||
await model.load();
|
||||
|
||||
await model.loadReplies('10');
|
||||
final result = await model.publish('Reply', replyTo: root);
|
||||
|
||||
expect(result.isSuccess, isTrue);
|
||||
expect(repository.publishCalls.single.parentId, '10');
|
||||
expect(repository.pageCalls.last.rootId, '10');
|
||||
expect(model.repliesFor('10').map((item) => item.id), ['11']);
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
test('should_restore_comment_at_original_position_when_delete_fails', () async {
|
||||
final first = commentFixture(id: '2', mine: true);
|
||||
final second = commentFixture(id: '1');
|
||||
final repository = FakeCommentRepository(
|
||||
pages: [
|
||||
CommentPage(comments: [first, second]),
|
||||
],
|
||||
deleteError: const ApiException(500, '删除失败'),
|
||||
);
|
||||
final model = controller(repository);
|
||||
await model.load();
|
||||
|
||||
final result = await model.delete(first);
|
||||
|
||||
expect(result.isSuccess, isFalse);
|
||||
expect(result.errorMessage, '删除失败');
|
||||
expect(model.comments.map((item) => item.id), ['2', '1']);
|
||||
expect(repository.deleteCalls, ['2']);
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
test('should_remove_own_comment_when_delete_succeeds', () async {
|
||||
final own = commentFixture(id: '2', mine: true);
|
||||
final repository = FakeCommentRepository(
|
||||
pages: [
|
||||
CommentPage(comments: [own]),
|
||||
],
|
||||
);
|
||||
final model = controller(repository);
|
||||
await model.load();
|
||||
|
||||
final result = await model.delete(own);
|
||||
|
||||
expect(result.isSuccess, isTrue);
|
||||
expect(model.comments, isEmpty);
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
test('should_optimistically_update_comment_like_and_apply_server_count', () async {
|
||||
final comment = commentFixture(id: '2', likeCount: 3, liked: false);
|
||||
final interactions = FakeInteractionRepository(
|
||||
nextLikeResult: const LikeToggleResult(liked: true, count: 8),
|
||||
);
|
||||
final repository = FakeCommentRepository(
|
||||
pages: [
|
||||
CommentPage(comments: [comment]),
|
||||
],
|
||||
);
|
||||
final model = controller(repository, interactions: interactions);
|
||||
await model.load();
|
||||
|
||||
final pending = model.toggleLike(comment);
|
||||
expect(model.comments.single.liked, isTrue);
|
||||
expect(model.comments.single.likeCount, 4);
|
||||
final result = await pending;
|
||||
|
||||
expect(result.isSuccess, isTrue);
|
||||
expect(model.comments.single.liked, isTrue);
|
||||
expect(model.comments.single.likeCount, 8);
|
||||
expect(interactions.likeTargets, ['COMMENT:2']);
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
test('should_rollback_comment_like_when_request_fails', () async {
|
||||
final comment = commentFixture(id: '2', likeCount: 3, liked: true);
|
||||
final interactions = FakeInteractionRepository()
|
||||
..likeError = const ApiException(500, '点赞失败');
|
||||
final repository = FakeCommentRepository(
|
||||
pages: [
|
||||
CommentPage(comments: [comment]),
|
||||
],
|
||||
);
|
||||
final model = controller(repository, interactions: interactions);
|
||||
await model.load();
|
||||
|
||||
final result = await model.toggleLike(comment);
|
||||
|
||||
expect(result.isSuccess, isFalse);
|
||||
expect(result.errorMessage, '点赞失败');
|
||||
expect(model.comments.single.liked, isTrue);
|
||||
expect(model.comments.single.likeCount, 3);
|
||||
model.dispose();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/data/models/comment.dart';
|
||||
import 'package:fruitshorts/core/data/models/drama.dart';
|
||||
import 'package:fruitshorts/core/data/models/interaction.dart';
|
||||
import 'package:fruitshorts/core/data/remote/api_client.dart';
|
||||
import 'package:fruitshorts/core/data/remote/auth_api.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/comment_repository.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/interaction_repository.dart';
|
||||
import 'package:fruitshorts/core/data/services/auth_controller.dart';
|
||||
import 'package:fruitshorts/core/data/services/auth_session_store.dart';
|
||||
import 'package:fruitshorts/l10n/app_localizations.dart';
|
||||
import 'package:fruitshorts/pages/player/widgets/drama_comments_sheet.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
|
||||
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
|
||||
|
||||
import '../support/fake_http_adapter.dart';
|
||||
import '../support/fake_interaction_repositories.dart';
|
||||
|
||||
const _drama = Drama(
|
||||
id: '5',
|
||||
title: 'The Signal',
|
||||
synopsis: 'synopsis',
|
||||
posterUrl: '/files/5.jpg',
|
||||
);
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
SharedPreferencesAsyncPlatform.instance =
|
||||
InMemorySharedPreferencesAsync.withData({
|
||||
'auth.accessToken': 'test-access',
|
||||
'auth.refreshToken': 'test-refresh',
|
||||
'auth.user': '{"uid":"9","nickname":"Me","avatarUrl":""}',
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('should_show_server_comments_and_append_next_cursor_page', (
|
||||
tester,
|
||||
) async {
|
||||
final repository = FakeCommentRepository(
|
||||
pages: [
|
||||
CommentPage(
|
||||
comments: [
|
||||
commentFixture(id: '10', content: 'Great episode', replyCount: 1),
|
||||
],
|
||||
nextCursor: '10',
|
||||
hasMore: true,
|
||||
),
|
||||
CommentPage(
|
||||
comments: [commentFixture(id: '9', content: 'Second page')],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
await _pumpSheet(tester, repository: repository);
|
||||
expect(find.text('Great episode'), findsOneWidget);
|
||||
expect(find.byKey(const Key('comment-list-load-more')), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byKey(const Key('comment-list-load-more')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Second page'), findsOneWidget);
|
||||
expect(repository.pageCalls.last.cursor, '10');
|
||||
});
|
||||
|
||||
testWidgets('should_keep_input_and_show_backend_message_when_comment_is_blocked', (
|
||||
tester,
|
||||
) async {
|
||||
final repository = FakeCommentRepository(
|
||||
pages: [const CommentPage()],
|
||||
publishError: const ApiException(400, '评论包含敏感词'),
|
||||
);
|
||||
|
||||
await _pumpSheet(tester, repository: repository);
|
||||
await tester.enterText(
|
||||
find.byKey(const Key('comment-input')),
|
||||
'blocked text',
|
||||
);
|
||||
await tester.tap(find.byKey(const Key('comment-post')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('评论包含敏感词'), findsOneWidget);
|
||||
final field = tester.widget<TextField>(
|
||||
find.byKey(const Key('comment-input')),
|
||||
);
|
||||
expect(field.controller?.text, 'blocked text');
|
||||
});
|
||||
|
||||
testWidgets('should_show_review_badge_and_server_replaced_content', (
|
||||
tester,
|
||||
) async {
|
||||
final repository = FakeCommentRepository(
|
||||
pages: [const CommentPage()],
|
||||
publishResult: commentFixture(
|
||||
id: '11',
|
||||
content: 'hello ***',
|
||||
status: 'pending',
|
||||
mine: true,
|
||||
),
|
||||
);
|
||||
|
||||
await _pumpSheet(tester, repository: repository);
|
||||
await tester.enterText(
|
||||
find.byKey(const Key('comment-input')),
|
||||
'hello badword',
|
||||
);
|
||||
await tester.tap(find.byKey(const Key('comment-post')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('hello ***'), findsOneWidget);
|
||||
expect(find.text('Under review'), findsOneWidget);
|
||||
final field = tester.widget<TextField>(
|
||||
find.byKey(const Key('comment-input')),
|
||||
);
|
||||
expect(field.controller?.text, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('should_send_parent_id_when_replying_to_top_level_comment', (
|
||||
tester,
|
||||
) async {
|
||||
final root = commentFixture(id: '10', content: 'Root', replyCount: 0);
|
||||
final repository = FakeCommentRepository(
|
||||
pages: [
|
||||
CommentPage(comments: [root]),
|
||||
],
|
||||
publishResult: commentFixture(
|
||||
id: '11',
|
||||
parentId: '10',
|
||||
rootId: '10',
|
||||
content: 'My reply',
|
||||
mine: true,
|
||||
),
|
||||
);
|
||||
|
||||
await _pumpSheet(tester, repository: repository);
|
||||
await tester.tap(find.byKey(const Key('comment-reply-10')));
|
||||
await tester.pump();
|
||||
expect(find.textContaining('Replying to'), findsOneWidget);
|
||||
await tester.enterText(
|
||||
find.byKey(const Key('comment-input')),
|
||||
'My reply',
|
||||
);
|
||||
await tester.tap(find.byKey(const Key('comment-post')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(repository.publishCalls.single.parentId, '10');
|
||||
expect(find.text('My reply'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should_show_error_and_retry_when_comment_page_fails', (
|
||||
tester,
|
||||
) async {
|
||||
final repository = FakeCommentRepository(
|
||||
pageError: StateError('offline'),
|
||||
);
|
||||
|
||||
await _pumpSheet(tester, repository: repository);
|
||||
|
||||
expect(find.byKey(const Key('comment-list-error')), findsOneWidget);
|
||||
expect(find.byKey(const Key('comment-list-retry')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should_update_comment_like_count_when_server_returns_count', (
|
||||
tester,
|
||||
) async {
|
||||
final repository = FakeCommentRepository(
|
||||
pages: [
|
||||
CommentPage(
|
||||
comments: [commentFixture(id: '10', content: 'Like me')],
|
||||
),
|
||||
],
|
||||
);
|
||||
final interactions = FakeInteractionRepository()
|
||||
..nextLikeResult = const LikeToggleResult(liked: true, count: 7);
|
||||
|
||||
await _pumpSheet(
|
||||
tester,
|
||||
repository: repository,
|
||||
interactions: interactions,
|
||||
);
|
||||
await tester.tap(find.byKey(const Key('comment-like-10')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(interactions.likeTargets, ['COMMENT:10']);
|
||||
expect(find.text('7'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should_report_comment_and_show_success_when_submitted', (
|
||||
tester,
|
||||
) async {
|
||||
final repository = FakeCommentRepository(
|
||||
pages: [
|
||||
CommentPage(
|
||||
comments: [commentFixture(id: '10', content: 'Report me')],
|
||||
),
|
||||
],
|
||||
);
|
||||
final interactions = FakeInteractionRepository();
|
||||
|
||||
await _pumpSheet(
|
||||
tester,
|
||||
repository: repository,
|
||||
interactions: interactions,
|
||||
);
|
||||
await tester.tap(find.byKey(const Key('comment-menu-10')));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Report comment'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(const Key('report-submit')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(interactions.reports.single.targetType, InteractionTargetType.comment);
|
||||
expect(interactions.reports.single.targetId, '10');
|
||||
expect(interactions.reports.single.reasonCode, 'SPAM');
|
||||
expect(find.text('Report submitted. Thank you.'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should_show_backend_message_when_comment_report_is_duplicated', (
|
||||
tester,
|
||||
) async {
|
||||
final repository = FakeCommentRepository(
|
||||
pages: [
|
||||
CommentPage(
|
||||
comments: [commentFixture(id: '10', content: 'Report me')],
|
||||
),
|
||||
],
|
||||
);
|
||||
final interactions = FakeInteractionRepository()
|
||||
..reportError = const ApiException(200003, '你已举报过该内容,我们正在处理');
|
||||
|
||||
await _pumpSheet(
|
||||
tester,
|
||||
repository: repository,
|
||||
interactions: interactions,
|
||||
);
|
||||
await tester.tap(find.byKey(const Key('comment-menu-10')));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Report comment'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(const Key('report-submit')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('你已举报过该内容,我们正在处理'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should_open_auth_gate_without_reporting_when_signed_out', (
|
||||
tester,
|
||||
) async {
|
||||
SharedPreferencesAsyncPlatform.instance =
|
||||
InMemorySharedPreferencesAsync.empty();
|
||||
final repository = FakeCommentRepository(
|
||||
pages: [
|
||||
CommentPage(
|
||||
comments: [commentFixture(id: '10', content: 'Report me')],
|
||||
),
|
||||
],
|
||||
);
|
||||
final interactions = FakeInteractionRepository();
|
||||
|
||||
await _pumpSheet(
|
||||
tester,
|
||||
repository: repository,
|
||||
interactions: interactions,
|
||||
);
|
||||
await tester.tap(find.byKey(const Key('comment-menu-10')));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Report comment'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Sign in to continue'), findsOneWidget);
|
||||
expect(interactions.reports, isEmpty);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _pumpSheet(
|
||||
WidgetTester tester, {
|
||||
required FakeCommentRepository repository,
|
||||
FakeInteractionRepository? interactions,
|
||||
}) async {
|
||||
final auth = await _auth();
|
||||
await tester.pumpWidget(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
Provider<CommentRepository>.value(value: repository),
|
||||
Provider<InteractionRepository>.value(
|
||||
value: interactions ?? FakeInteractionRepository(),
|
||||
),
|
||||
ChangeNotifierProvider<AuthController>.value(value: auth),
|
||||
],
|
||||
child: MaterialApp(
|
||||
locale: const Locale('en'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: const Scaffold(
|
||||
backgroundColor: Color(0xFF232328),
|
||||
body: DramaCommentsSheet(drama: _drama),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<AuthController> _auth() async {
|
||||
final client = ApiClient(
|
||||
baseUrl: 'http://127.0.0.1:8081',
|
||||
adapter: FakeHttpAdapter(
|
||||
(_) async => FakeHttpResponse('{"code":0,"message":"ok","data":null}'),
|
||||
),
|
||||
);
|
||||
final auth = AuthController(AuthApi(client), AuthSessionStore());
|
||||
await auth.load();
|
||||
return auth;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/data/services/platform_share_service.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
test('should_keep_title_synopsis_footer_order_when_share_text_is_sent', () async {
|
||||
String? sharedText;
|
||||
const channel = MethodChannel('com.stormg.lumadrama/share');
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, (call) async {
|
||||
sharedText = (call.arguments as Map)['text'] as String;
|
||||
return null;
|
||||
});
|
||||
addTearDown(
|
||||
() => TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, null),
|
||||
);
|
||||
|
||||
await const PlatformShareService().shareDrama(
|
||||
title: 'The Signal',
|
||||
synopsis: 'A short drama',
|
||||
footer: 'FruitShorts',
|
||||
);
|
||||
|
||||
expect(sharedText, 'The Signal\nA short drama\nFruitShorts');
|
||||
});
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/data/services/engagement_controller.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/engagement_repository.dart';
|
||||
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
|
||||
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
|
||||
|
||||
void main() {
|
||||
test('should_keep_saved_liked_and_comments_by_drama_id_when_toggled', () async {
|
||||
final repository = InMemoryEngagementRepository();
|
||||
final first = EngagementController(repository);
|
||||
await first.load();
|
||||
await first.toggleSaved('signal');
|
||||
await first.toggleLiked('signal');
|
||||
await first.addComment('signal', ' Great clip ');
|
||||
|
||||
expect(first.isSaved('signal'), isTrue);
|
||||
expect(first.isSaved('city'), isFalse);
|
||||
expect(first.isLiked('city'), isFalse);
|
||||
expect(first.commentsFor('signal'), ['Great clip']);
|
||||
expect(first.commentsFor('city'), isEmpty);
|
||||
|
||||
final reopened = EngagementController(repository);
|
||||
await reopened.load();
|
||||
expect(reopened.isSaved('signal'), isTrue);
|
||||
expect(reopened.isLiked('signal'), isTrue);
|
||||
expect(reopened.commentsFor('signal'), ['Great clip']);
|
||||
await reopened.toggleSaved('signal');
|
||||
expect(reopened.isSaved('signal'), isFalse);
|
||||
});
|
||||
|
||||
test('should_survive_new_repository_instance_when_engagement_is_local', () async {
|
||||
SharedPreferencesAsyncPlatform.instance =
|
||||
InMemorySharedPreferencesAsync.empty();
|
||||
final first = EngagementController(SharedPreferencesEngagementRepository());
|
||||
await first.load();
|
||||
await first.toggleSaved('city');
|
||||
await first.addComment('city', 'Local note');
|
||||
|
||||
final reopened = EngagementController(
|
||||
SharedPreferencesEngagementRepository(),
|
||||
);
|
||||
await reopened.load();
|
||||
expect(reopened.isSaved('city'), isTrue);
|
||||
expect(reopened.commentsFor('city'), ['Local note']);
|
||||
});
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/data/models/engagement_snapshot.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/engagement_repository.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
|
||||
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
|
||||
|
||||
/// Mirrors the private key used by [SharedPreferencesEngagementRepository].
|
||||
const _key = 'engagement.v1.demo-local-v1';
|
||||
|
||||
void main() {
|
||||
late SharedPreferencesAsync preferences;
|
||||
|
||||
setUp(() {
|
||||
SharedPreferencesAsyncPlatform.instance =
|
||||
InMemorySharedPreferencesAsync.empty();
|
||||
preferences = SharedPreferencesAsync();
|
||||
});
|
||||
|
||||
SharedPreferencesEngagementRepository repository() =>
|
||||
SharedPreferencesEngagementRepository(preferences: preferences);
|
||||
|
||||
Future<EngagementSnapshot> loadRaw(String raw) async {
|
||||
await preferences.setString(_key, raw);
|
||||
return repository().load();
|
||||
}
|
||||
|
||||
test('should_read_empty_snapshot_when_first_launch', () async {
|
||||
final snapshot = await repository().load();
|
||||
|
||||
expect(snapshot.savedDramaIds, isEmpty);
|
||||
expect(snapshot.likedDramaIds, isEmpty);
|
||||
expect(snapshot.commentsByDrama, isEmpty);
|
||||
});
|
||||
|
||||
test('should_survive_save_and_reload_round_trip_when_snapshot_is_stored', () async {
|
||||
await repository().save(
|
||||
const EngagementSnapshot(
|
||||
savedDramaIds: {'signal', 'city'},
|
||||
likedDramaIds: {'city'},
|
||||
commentsByDrama: {
|
||||
'signal': ['first note', 'second note'],
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final loaded = await repository().load();
|
||||
|
||||
expect(loaded.savedDramaIds, {'signal', 'city'});
|
||||
expect(loaded.likedDramaIds, {'city'});
|
||||
expect(loaded.commentsByDrama, {
|
||||
'signal': ['first note', 'second note'],
|
||||
});
|
||||
});
|
||||
|
||||
test('should_round_trip_as_empty_collections_when_snapshot_is_empty', () async {
|
||||
await repository().save(const EngagementSnapshot());
|
||||
|
||||
final loaded = await repository().load();
|
||||
|
||||
expect(loaded.savedDramaIds, isEmpty);
|
||||
expect(loaded.likedDramaIds, isEmpty);
|
||||
expect(loaded.commentsByDrama, isEmpty);
|
||||
});
|
||||
|
||||
test('should_fall_back_to_empty_snapshot_when_payload_is_corrupt_or_not_an_object', () async {
|
||||
for (final raw in <String>[
|
||||
'',
|
||||
'not json at all',
|
||||
'[]',
|
||||
'"a bare string"',
|
||||
'42',
|
||||
'null',
|
||||
]) {
|
||||
final snapshot = await loadRaw(raw);
|
||||
|
||||
expect(snapshot.savedDramaIds, isEmpty, reason: raw);
|
||||
expect(snapshot.likedDramaIds, isEmpty, reason: raw);
|
||||
expect(snapshot.commentsByDrama, isEmpty, reason: raw);
|
||||
}
|
||||
});
|
||||
|
||||
test('should_drop_mistyped_fields_when_payload_has_wrong_types', () async {
|
||||
final snapshot = await loadRaw(
|
||||
jsonEncode({
|
||||
'saved': 'signal',
|
||||
'liked': {'city': true},
|
||||
'comments': {
|
||||
'signal': 'a plain string',
|
||||
'city': ['kept', 7, null, true],
|
||||
'town': <Object>[],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(snapshot.savedDramaIds, isEmpty);
|
||||
expect(snapshot.likedDramaIds, isEmpty);
|
||||
expect(snapshot.commentsByDrama, {
|
||||
'city': ['kept'],
|
||||
'town': <String>[],
|
||||
});
|
||||
});
|
||||
|
||||
test('should_fall_back_per_field_when_payload_keys_are_missing', () async {
|
||||
final snapshot = await loadRaw(jsonEncode({'saved': ['signal']}));
|
||||
|
||||
expect(snapshot.savedDramaIds, {'signal'});
|
||||
expect(snapshot.likedDramaIds, isEmpty);
|
||||
expect(snapshot.commentsByDrama, isEmpty);
|
||||
});
|
||||
|
||||
test('should_write_only_json_primitives_when_saving', () async {
|
||||
await repository().save(
|
||||
const EngagementSnapshot(
|
||||
savedDramaIds: {'signal'},
|
||||
likedDramaIds: {'city'},
|
||||
commentsByDrama: {
|
||||
'signal': ['note'],
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final raw = await preferences.getString(_key);
|
||||
expect(raw, isNotNull);
|
||||
|
||||
final decoded = jsonDecode(raw!) as Map<String, dynamic>;
|
||||
expect(decoded['saved'], ['signal']);
|
||||
expect(decoded['liked'], ['city']);
|
||||
expect(decoded['comments'], {
|
||||
'signal': ['note'],
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/data/models/drama.dart';
|
||||
import 'package:fruitshorts/core/formatting/localized_content.dart';
|
||||
import 'package:fruitshorts/l10n/app_localizations_en.dart';
|
||||
import 'package:fruitshorts/l10n/app_localizations_id.dart';
|
||||
import 'package:fruitshorts/l10n/app_localizations_zh.dart';
|
||||
|
||||
Drama _drama({required String id, String title = 'raw', String synopsis = 'raw'}) =>
|
||||
@@ -16,6 +17,7 @@ Drama _drama({required String id, String title = 'raw', String synopsis = 'raw'}
|
||||
void main() {
|
||||
final en = AppLocalizationsEn();
|
||||
final zh = AppLocalizationsZh();
|
||||
final id = AppLocalizationsId();
|
||||
|
||||
test('should_map_each_seeded_drama_to_its_own_localized_copy_when_looked_up', () {
|
||||
final signal = _drama(id: 'the-signal-demo');
|
||||
@@ -93,4 +95,21 @@ void main() {
|
||||
}
|
||||
expect(localizedEpisodeTitle(zh, 12), zh.episodeLabel(12));
|
||||
});
|
||||
|
||||
test('should_build_share_copy_from_localized_title_synopsis_and_footer', () {
|
||||
final signal = _drama(id: 'the-signal-demo');
|
||||
final copies = {
|
||||
for (final l10n in [en, zh, id])
|
||||
'${localizedDramaTitle(l10n, signal)}\n'
|
||||
'${localizedDramaSynopsis(l10n, signal)}\n'
|
||||
'${l10n.shareFooter}',
|
||||
};
|
||||
|
||||
expect(copies, hasLength(3));
|
||||
for (final copy in copies) {
|
||||
expect(copy, contains('FruitShorts'));
|
||||
expect(copy.split('\n'), hasLength(3));
|
||||
expect(copy.split('\n').every((line) => line.isNotEmpty), isTrue);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/data/models/interaction.dart';
|
||||
import 'package:fruitshorts/core/data/remote/api_client.dart';
|
||||
import 'package:fruitshorts/core/data/remote/interaction_api.dart';
|
||||
|
||||
import '../support/fake_http_adapter.dart';
|
||||
|
||||
void main() {
|
||||
String envelope(Object? data) =>
|
||||
jsonEncode({'code': 0, 'message': 'ok', 'data': data});
|
||||
|
||||
test('should_send_like_target_and_parse_string_count_when_toggled', () async {
|
||||
final adapter = FakeHttpAdapter(
|
||||
(_) async => FakeHttpResponse(
|
||||
envelope({'liked': true, 'count': '2111012753262120962'}),
|
||||
),
|
||||
);
|
||||
final api = InteractionApi(
|
||||
ApiClient(baseUrl: 'http://127.0.0.1:8081', adapter: adapter),
|
||||
);
|
||||
|
||||
final result = await api.toggleLike(
|
||||
targetType: InteractionTargetType.drama,
|
||||
targetId: '5',
|
||||
);
|
||||
|
||||
expect(adapter.requests.single.method, 'POST');
|
||||
expect(adapter.requests.single.uri.path, '/app/likes/toggle');
|
||||
expect(adapter.requests.single.data, {
|
||||
'targetType': 'DRAMA',
|
||||
'targetId': '5',
|
||||
});
|
||||
expect(result.liked, isTrue);
|
||||
expect(result.count, 2111012753262120962);
|
||||
});
|
||||
|
||||
test('should_submit_fixed_reason_code_and_optional_description_when_reporting', () async {
|
||||
final adapter = FakeHttpAdapter(
|
||||
(_) async => FakeHttpResponse(envelope('2111012753262120999')),
|
||||
);
|
||||
final api = InteractionApi(
|
||||
ApiClient(baseUrl: 'http://127.0.0.1:8081', adapter: adapter),
|
||||
);
|
||||
|
||||
final reportId = await api.report(
|
||||
const ReportRequest(
|
||||
targetType: InteractionTargetType.comment,
|
||||
targetId: '7',
|
||||
reasonCode: 'SPAM',
|
||||
description: 'Repeated advertising',
|
||||
),
|
||||
);
|
||||
|
||||
expect(adapter.requests.single.uri.path, '/app/reports');
|
||||
expect(adapter.requests.single.data, {
|
||||
'targetType': 'COMMENT',
|
||||
'targetId': '7',
|
||||
'reasonCode': 'SPAM',
|
||||
'description': 'Repeated advertising',
|
||||
});
|
||||
expect(reportId, '2111012753262120999');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/data/models/interaction.dart';
|
||||
import 'package:fruitshorts/core/data/remote/api_client.dart';
|
||||
import 'package:fruitshorts/core/data/services/interaction_controller.dart';
|
||||
|
||||
import '../support/fake_interaction_repositories.dart';
|
||||
|
||||
void main() {
|
||||
test('should_update_drama_like_optimistically_and_use_server_state_when_toggle_succeeds', () async {
|
||||
final repository = FakeInteractionRepository();
|
||||
final gate = Completer<void>();
|
||||
repository.likeGate = gate;
|
||||
final controller = InteractionController(repository);
|
||||
|
||||
final future = controller.toggleDramaLike('5');
|
||||
expect(controller.isLiked('5'), isTrue);
|
||||
expect(repository.likeTargets, ['DRAMA:5']);
|
||||
|
||||
repository.nextLikeResult = const LikeToggleResult(liked: false, count: 0);
|
||||
gate.complete();
|
||||
final result = await future;
|
||||
|
||||
expect(result.isSuccess, isTrue);
|
||||
expect(controller.isLiked('5'), isFalse);
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
test('should_rollback_drama_like_when_toggle_fails', () async {
|
||||
final repository = FakeInteractionRepository(
|
||||
nextLikeResult: const LikeToggleResult(liked: true, count: 3),
|
||||
);
|
||||
final controller = InteractionController(repository);
|
||||
await controller.toggleDramaLike('5');
|
||||
repository.likeError = const ApiException(500, '点赞失败');
|
||||
|
||||
final result = await controller.toggleDramaLike('5');
|
||||
|
||||
expect(result.isSuccess, isFalse);
|
||||
expect(result.errorMessage, '点赞失败');
|
||||
expect(controller.isLiked('5'), isTrue);
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
test('should_clear_account_scoped_drama_likes_when_session_changes', () async {
|
||||
final repository = FakeInteractionRepository(
|
||||
nextLikeResult: const LikeToggleResult(liked: true, count: 1),
|
||||
);
|
||||
final controller = InteractionController(repository);
|
||||
await controller.toggleDramaLike('5');
|
||||
|
||||
controller.clear();
|
||||
|
||||
expect(controller.isLiked('5'), isFalse);
|
||||
controller.dispose();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/data/models/interaction.dart';
|
||||
import 'package:fruitshorts/l10n/app_localizations.dart';
|
||||
import 'package:fruitshorts/pages/interaction/widgets/report_dialog.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('should_return_selected_reason_and_description_when_submitted', (
|
||||
tester,
|
||||
) async {
|
||||
ReportDialogResult? result;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
locale: const Locale('en'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: Builder(
|
||||
builder: (context) => Scaffold(
|
||||
body: TextButton(
|
||||
key: const Key('open-report-dialog'),
|
||||
onPressed: () async {
|
||||
result = await showReportDialog(
|
||||
context,
|
||||
title: 'Report comment',
|
||||
);
|
||||
},
|
||||
child: const Text('Open'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.byKey(const Key('open-report-dialog')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Spam'), findsOneWidget);
|
||||
expect(find.text('Harassment'), findsOneWidget);
|
||||
expect(find.text('Sexual content'), findsOneWidget);
|
||||
expect(find.text('Advertising'), findsOneWidget);
|
||||
expect(find.text('Other'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byKey(const Key('report-reason-AD')));
|
||||
await tester.enterText(
|
||||
find.byKey(const Key('report-description')),
|
||||
'Repeat promotion',
|
||||
);
|
||||
await tester.tap(find.byKey(const Key('report-submit')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(result?.reason, ReportReason.ad);
|
||||
expect(result?.description, 'Repeat promotion');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fruitshorts/core/data/models/comment.dart';
|
||||
import 'package:fruitshorts/core/data/models/interaction.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/comment_repository.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/interaction_repository.dart';
|
||||
|
||||
class CommentPageCall {
|
||||
const CommentPageCall({
|
||||
required this.dramaId,
|
||||
required this.rootId,
|
||||
required this.cursor,
|
||||
required this.size,
|
||||
});
|
||||
|
||||
final String dramaId;
|
||||
final String? rootId;
|
||||
final String? cursor;
|
||||
final int size;
|
||||
}
|
||||
|
||||
class PublishCall {
|
||||
const PublishCall({
|
||||
required this.dramaId,
|
||||
required this.episodeId,
|
||||
required this.content,
|
||||
required this.parentId,
|
||||
});
|
||||
|
||||
final String dramaId;
|
||||
final String? episodeId;
|
||||
final String content;
|
||||
final String? parentId;
|
||||
}
|
||||
|
||||
class FakeCommentRepository implements CommentRepository {
|
||||
FakeCommentRepository({
|
||||
List<CommentPage>? pages,
|
||||
this.publishResult,
|
||||
this.publishError,
|
||||
this.deleteError,
|
||||
this.pageError,
|
||||
}) : pages = pages ?? const [];
|
||||
|
||||
final List<CommentPage> pages;
|
||||
final DramaComment? publishResult;
|
||||
final Object? publishError;
|
||||
final Object? deleteError;
|
||||
final Object? pageError;
|
||||
final List<CommentPageCall> pageCalls = [];
|
||||
final List<PublishCall> publishCalls = [];
|
||||
final List<String> deleteCalls = [];
|
||||
int _pageIndex = 0;
|
||||
|
||||
@override
|
||||
Future<CommentPage> page({
|
||||
required String dramaId,
|
||||
String? rootId,
|
||||
String? cursor,
|
||||
int size = commentPageSize,
|
||||
}) async {
|
||||
pageCalls.add(
|
||||
CommentPageCall(
|
||||
dramaId: dramaId,
|
||||
rootId: rootId,
|
||||
cursor: cursor,
|
||||
size: size,
|
||||
),
|
||||
);
|
||||
final error = pageError;
|
||||
if (error != null) throw error;
|
||||
if (_pageIndex >= pages.length) return const CommentPage();
|
||||
return pages[_pageIndex++];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DramaComment> publish({
|
||||
required String dramaId,
|
||||
String? episodeId,
|
||||
required String content,
|
||||
String? parentId,
|
||||
}) async {
|
||||
publishCalls.add(
|
||||
PublishCall(
|
||||
dramaId: dramaId,
|
||||
episodeId: episodeId,
|
||||
content: content,
|
||||
parentId: parentId,
|
||||
),
|
||||
);
|
||||
final error = publishError;
|
||||
if (error != null) throw error;
|
||||
return publishResult ??
|
||||
commentFixture(
|
||||
id: 'new-comment',
|
||||
dramaId: dramaId,
|
||||
content: content,
|
||||
mine: true,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete(String commentId) async {
|
||||
deleteCalls.add(commentId);
|
||||
final error = deleteError;
|
||||
if (error != null) throw error;
|
||||
}
|
||||
}
|
||||
|
||||
class FakeInteractionRepository implements InteractionRepository {
|
||||
FakeInteractionRepository({
|
||||
this.nextLikeResult = const LikeToggleResult(liked: true, count: 1),
|
||||
});
|
||||
|
||||
LikeToggleResult nextLikeResult;
|
||||
Object? likeError;
|
||||
Object? reportError;
|
||||
Completer<void>? likeGate;
|
||||
String reportId = 'report-1';
|
||||
final List<String> likeTargets = [];
|
||||
final List<ReportRequest> reports = [];
|
||||
|
||||
@override
|
||||
Future<LikeToggleResult> toggleLike({
|
||||
required InteractionTargetType targetType,
|
||||
required String targetId,
|
||||
}) async {
|
||||
likeTargets.add('${targetType.apiValue}:$targetId');
|
||||
final gate = likeGate;
|
||||
if (gate != null) await gate.future;
|
||||
final error = likeError;
|
||||
if (error != null) throw error;
|
||||
return nextLikeResult;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> report(ReportRequest request) async {
|
||||
reports.add(request);
|
||||
final error = reportError;
|
||||
if (error != null) throw error;
|
||||
return reportId;
|
||||
}
|
||||
}
|
||||
|
||||
DramaComment commentFixture({
|
||||
String id = '1001',
|
||||
String dramaId = '5',
|
||||
String? parentId,
|
||||
String? rootId,
|
||||
String content = 'A comment',
|
||||
String status = 'published',
|
||||
int likeCount = 0,
|
||||
int replyCount = 0,
|
||||
bool liked = false,
|
||||
bool mine = false,
|
||||
}) => DramaComment(
|
||||
id: id,
|
||||
dramaId: dramaId,
|
||||
episodeId: '0',
|
||||
parentId: parentId ?? '0',
|
||||
rootId: rootId ?? '0',
|
||||
userId: mine ? '9' : '8',
|
||||
uid: mine ? 'me' : 'viewer',
|
||||
nickname: mine ? 'Me' : 'Viewer',
|
||||
avatarUrl: '',
|
||||
content: content,
|
||||
status: status,
|
||||
auditReason: '',
|
||||
likeCount: likeCount,
|
||||
replyCount: replyCount,
|
||||
reportCount: 0,
|
||||
createdAt: DateTime(2026, 10, 17, 12),
|
||||
mine: mine,
|
||||
liked: liked,
|
||||
);
|
||||
+79
-2
@@ -4,15 +4,20 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/app.dart';
|
||||
import 'package:fruitshorts/core/data/models/comment.dart';
|
||||
import 'package:fruitshorts/core/data/models/interaction.dart';
|
||||
import 'package:fruitshorts/pages/player/player_view.dart';
|
||||
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
|
||||
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
|
||||
|
||||
import 'support/fake_catalog.dart';
|
||||
import 'support/fake_category.dart';
|
||||
import 'support/fake_interaction_repositories.dart';
|
||||
import 'support/widget_player.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/favorite_repository.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/catalog_repository.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/comment_repository.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/interaction_repository.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/playback_repository.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart';
|
||||
import 'package:fruitshorts/core/data/services/playback_session.dart';
|
||||
@@ -31,6 +36,11 @@ void main() {
|
||||
});
|
||||
}
|
||||
|
||||
void seedSignedOutSession() {
|
||||
SharedPreferencesAsyncPlatform.instance =
|
||||
InMemorySharedPreferencesAsync.empty();
|
||||
}
|
||||
|
||||
setUp(() {
|
||||
seedSignedInSession();
|
||||
});
|
||||
@@ -40,6 +50,8 @@ void main() {
|
||||
Widget appUnderTest({
|
||||
PlayerFactory? playerFactory,
|
||||
CatalogRepository? catalogRepository,
|
||||
CommentRepository? commentRepository,
|
||||
InteractionRepository? interactionRepository,
|
||||
}) {
|
||||
final catalog = catalogRepository ?? FakeCatalogRepository();
|
||||
return LumaDramaApp(
|
||||
@@ -51,6 +63,9 @@ void main() {
|
||||
cards: FakeCatalogRepository().dramas,
|
||||
),
|
||||
playbackRepository: FakePlaybackRepository(),
|
||||
commentRepository: commentRepository ?? FakeCommentRepository(),
|
||||
interactionRepository:
|
||||
interactionRepository ?? FakeInteractionRepository(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -415,6 +430,10 @@ void main() {
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
String? sharedText;
|
||||
final interactions = FakeInteractionRepository();
|
||||
final comments = FakeCommentRepository(
|
||||
pages: [const CommentPage()],
|
||||
);
|
||||
const channel = MethodChannel('com.stormg.lumadrama/share');
|
||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, (
|
||||
call,
|
||||
@@ -430,7 +449,10 @@ void main() {
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
appUnderTest(),
|
||||
appUnderTest(
|
||||
commentRepository: comments,
|
||||
interactionRepository: interactions,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(const Key('tab-home')));
|
||||
@@ -450,7 +472,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(const Key('rail-comments')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Only visible on this device'), findsOneWidget);
|
||||
expect(find.text('No comments yet.'), findsOneWidget);
|
||||
await tester.enterText(find.byKey(const Key('comment-input')), 'Nice demo');
|
||||
await tester.tap(find.byKey(const Key('comment-post')));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -459,12 +481,67 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byKey(const Key('rail-like')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(interactions.likeTargets, contains('DRAMA:the-signal-demo'));
|
||||
await tester.tap(find.byKey(const Key('rail-share')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(sharedText, contains('The Signal'));
|
||||
expect(sharedText, contains('A demo series for vertical playback'));
|
||||
expect(sharedText, contains('FruitShorts'));
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('should_open_auth_gate_without_liking_when_signed_out', (
|
||||
tester,
|
||||
) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
seedSignedOutSession();
|
||||
final interactions = FakeInteractionRepository();
|
||||
|
||||
await tester.pumpWidget(
|
||||
appUnderTest(interactionRepository: interactions),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(const Key('tab-home')));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(const Key('rail-like')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Sign in to continue'), findsOneWidget);
|
||||
expect(interactions.likeTargets, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('should_report_drama_from_detail_more_menu_when_submitted', (
|
||||
tester,
|
||||
) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
final interactions = FakeInteractionRepository();
|
||||
|
||||
await tester.pumpWidget(
|
||||
appUnderTest(interactionRepository: interactions),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.binding.handlePopRoute();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(const Key('drawer-open-series')));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(const Key('detail-more')));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Report drama'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(const Key('report-submit')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(interactions.reports.single.targetType, InteractionTargetType.drama);
|
||||
expect(find.text('Report submitted. Thank you.'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should_open_drawer_and_stay_on_home_when_system_back_is_pressed', (tester) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
||||
Reference in new issue
Block a user