feat(interaction): 支持游客点赞收藏评论

This commit is contained in:
Zhou Weidong committed 2026-10-26 22:33:14 +08:00
1 parent 188811fde6
commit a3d50eb935
18 files changed
+364 -121

No files matched your search

+20 -24
View File
@@ -135,7 +135,9 @@ void main() {
debugDefaultTargetPlatformOverride = null;
});
testWidgets('should_gate_guest_comment_input_without_focus', (tester) async {
testWidgets('should_open_guest_comment_composer_without_login', (
tester,
) async {
SharedPreferencesAsyncPlatform.instance =
InMemorySharedPreferencesAsync.empty();
final repository = FakeCommentRepository(pages: [const CommentPage()]);
@@ -144,34 +146,20 @@ void main() {
final placeholder = find.byKey(const Key('comment-input-placeholder'));
expect(placeholder, findsOneWidget);
expect(find.text('Sign in to comment'), findsOneWidget);
await tester.tap(placeholder);
await tester.pumpAndSettle();
expect(find.text('Sign in to continue'), findsOneWidget);
await tester.tap(find.text('Not now'));
await tester.pumpAndSettle();
expect(find.byKey(const Key('comment-composer-sheet')), findsNothing);
expect(placeholder, findsOneWidget);
expect(find.byKey(const Key('comment-composer-sheet')), findsOneWidget);
expect(find.byKey(const Key('auth-landing')), findsNothing);
});
testWidgets('should_focus_comment_input_after_successful_login', (
tester,
) async {
testWidgets('should_focus_guest_comment_input_immediately', (tester) async {
SharedPreferencesAsyncPlatform.instance =
InMemorySharedPreferencesAsync.empty();
final repository = FakeCommentRepository(pages: [const CommentPage()]);
final auth = await _pumpSheet(tester, repository: repository);
await _pumpSheet(tester, repository: repository);
await tester.tap(find.byKey(const Key('comment-input-placeholder')));
await tester.pump();
await tester.pump(const Duration(milliseconds: 300));
await tester.tap(find.text('Sign In'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 300));
(auth as _TestAuthController).setSignedIn(true);
await tester.pump();
Navigator.of(tester.element(find.byKey(const Key('auth-landing')))).pop();
await tester.pumpAndSettle();
expect(
@@ -218,9 +206,7 @@ void main() {
expect(multilineField.style?.height, 1.4);
});
testWidgets('should_gate_guest_post_without_creating_comment', (
tester,
) async {
testWidgets('should_publish_guest_comment_without_login', (tester) async {
SharedPreferencesAsyncPlatform.instance =
InMemorySharedPreferencesAsync.empty();
final repository = FakeCommentRepository(pages: [const CommentPage()]);
@@ -229,8 +215,18 @@ void main() {
await tester.tap(find.byKey(const Key('comment-post')));
await tester.pumpAndSettle();
expect(find.text('Sign in to continue'), findsOneWidget);
expect(repository.publishCalls, isEmpty);
await tester.enterText(
find.byKey(const Key('comment-composer-input')),
'Guest comment',
);
await tester.pump();
await tester.tap(find.byKey(const Key('comment-composer-post')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('auth-landing')), findsNothing);
expect(repository.publishCalls, hasLength(1));
expect(repository.publishCalls.single.content, 'Guest comment');
expect(find.text('Guest comment'), findsOneWidget);
});
testWidgets('should_show_server_comments_and_append_next_cursor_page', (
+54 -15
View File
@@ -596,9 +596,7 @@ void main() {
expect(find.byType(Slider), findsOneWidget);
});
testWidgets('should_pause_feed_when_auth_page_is_pushed_and_resume_on_pop', (
tester,
) async {
testWidgets('should_keep_feed_playing_when_guest_saves', (tester) async {
final players = <_RecordingPlayer>[];
await _pumpApp(
tester,
@@ -615,18 +613,23 @@ void main() {
await tester.tap(find.byKey(const Key('rail-save')));
await tester.pumpAndSettle();
await tester.tap(find.text('Sign In'));
await tester.pumpAndSettle();
expect(find.byType(AuthPage), findsOneWidget);
expect(player.playing, isFalse);
await tester.pageBack();
await tester.pumpAndSettle();
expect(find.byType(AuthPage), findsNothing);
expect(player.playing, isTrue);
});
testWidgets('should_like_without_login_from_recommend_feed', (tester) async {
final interactions = FakeInteractionRepository();
await _pumpApp(tester, interactions: interactions);
await tester.tap(find.byKey(const Key('tab-recommend')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('rail-like')));
await tester.pumpAndSettle();
expect(find.byType(AuthPage), findsNothing);
expect(interactions.likeTargets, ['EPISODE:the-signal-demo-1']);
});
testWidgets('should_keep_video_surface_background_black', (tester) async {
await _pumpApp(tester);
await _openSeriesPlayer(tester);
@@ -643,7 +646,7 @@ void main() {
expect(find.byKey(const Key('player-fallback-scrim')), findsNothing);
});
testWidgets('should_gate_comments_before_opening_sheet_when_signed_out', (
testWidgets('should_open_comments_without_login', (
tester,
) async {
await _pumpApp(tester);
@@ -652,8 +655,8 @@ void main() {
await tester.tap(find.byKey(const Key('player-rail-comments')));
await tester.pumpAndSettle();
expect(find.text('Sign in to continue'), findsOneWidget);
expect(find.byKey(const Key('player-comments-sheet')), findsNothing);
expect(find.byKey(const Key('player-comments-sheet')), findsOneWidget);
expect(find.byType(AuthPage), findsNothing);
});
testWidgets('should_open_feed_comments_when_signed_in', (tester) async {
@@ -1872,6 +1875,12 @@ void main() {
testWidgets('should_open_unlock_panel_with_honest_method_states', (
tester,
) async {
SharedPreferencesAsyncPlatform.instance =
InMemorySharedPreferencesAsync.withData({
'auth.accessToken': 'access',
'auth.refreshToken': 'refresh',
'auth.user': '{"uid":"9","nickname":"Viewer","avatarUrl":""}',
});
final interactions = FakeInteractionRepository();
await _pumpApp(
tester,
@@ -1905,6 +1914,36 @@ void main() {
find.text('Available unlock methods vary by title.'),
findsOneWidget,
);
});
testWidgets('should_require_login_before_opening_unlock_flow', (
tester,
) async {
await _pumpApp(
tester,
playback: FakePlaybackRepository(
ticket: const PlayTicket(
episodeId: 'city-lights-demo-1',
accessible: false,
accessType: PlayTicket.coinRequired,
unlockCoin: 30,
),
),
);
await _openSeriesPlayer(tester);
await tester.tap(find.byKey(const Key('player-open-unlock')));
await tester.pump();
await tester.pump(const Duration(milliseconds: 300));
expect(find.byKey(const Key('player-unlock-panel')), findsNothing);
final signIn = find.widgetWithText(FilledButton, 'Sign In');
await tester.ensureVisible(signIn);
await tester.tap(signIn);
await tester.pump();
await tester.pump(const Duration(milliseconds: 300));
expect(find.byType(AuthPage), findsOneWidget);
});
}
@@ -0,0 +1,183 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/data/models/drama.dart';
import 'package:fruitshorts/core/data/models/play_ticket.dart';
import 'package:fruitshorts/core/data/models/player_port.dart';
import 'package:fruitshorts/core/data/models/wallet.dart';
import 'package:fruitshorts/core/data/repositories/commerce_repository.dart';
import 'package:fruitshorts/core/data/repositories/playback_repository.dart';
import 'package:fruitshorts/core/data/services/android_picture_in_picture.dart';
import 'package:fruitshorts/core/data/services/playback_session.dart';
import 'package:fruitshorts/core/data/services/wallet_controller.dart';
import 'package:fruitshorts/pages/player/player_logic.dart';
import 'package:fruitshorts/pages/player/player_view.dart';
import '../support/fake_watch_history.dart';
const _lockedDrama = Drama(
id: 'locked',
title: 'Locked',
synopsis: 'synopsis',
posterUrl: '/files/locked.png',
episodes: [
Episode(
id: 'locked-1',
number: 1,
title: 'Locked episode',
isFree: false,
locked: true,
),
],
);
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test('should_not_request_ticket_for_guest_on_locked_episode', () async {
final playback = FakePlaybackRepository();
final logic = _logic(playback, signedIn: false);
logic.attach(
const PlayerPage(drama: _lockedDrama, initialIndex: 0),
isMounted: () => true,
);
logic.start(
factory: (_) => _NoopPlayer(),
coordinator: PlaybackCoordinator(),
);
await _drain(logic);
expect(playback.requests, isEmpty);
expect(logic.session.isLocked, isTrue);
logic.dispose();
await _drain(logic);
});
test('should_request_ticket_after_user_is_signed_in', () async {
final playback = FakePlaybackRepository(
ticket: const PlayTicket(
episodeId: 'locked-1',
accessible: false,
accessType: PlayTicket.coinRequired,
unlockCoin: 30,
),
);
final logic = _logic(playback, signedIn: true);
logic.attach(
const PlayerPage(drama: _lockedDrama, initialIndex: 0),
isMounted: () => true,
);
logic.start(
factory: (_) => _NoopPlayer(),
coordinator: PlaybackCoordinator(),
);
await _drain(logic);
expect(playback.requests, ['locked-1']);
expect(logic.blockedEpisodeId, 'locked-1');
logic.dispose();
await _drain(logic);
});
test('should_resume_unlock_and_play_for_same_episode_after_login', () async {
var signedIn = false;
final playback = FakePlaybackRepository(
ticket: const PlayTicket(
episodeId: 'locked-1',
accessible: true,
accessType: 'ENTITLED',
playUrl: '/files/locked-1.mp4',
),
);
final logic = _logic(
playback,
signedIn: false,
isSignedIn: () => signedIn,
wallet: WalletController(_UnlockCommerceRepository()),
);
logic.attach(
const PlayerPage(drama: _lockedDrama, initialIndex: 0),
isMounted: () => true,
);
logic.start(
factory: (_) => _NoopPlayer(),
coordinator: PlaybackCoordinator(),
);
await _drain(logic);
expect(playback.requests, isEmpty);
expect(logic.blockedEpisodeId, 'locked-1');
signedIn = true;
final unlocked = await logic.unlockEpisode(0);
await _drain(logic);
expect(unlocked, isTrue);
expect(playback.requests, ['locked-1']);
expect(logic.session.active, isNotNull);
logic.dispose();
await _drain(logic);
});
}
PlayerLogic _logic(
PlaybackRepository playback, {
bool signedIn = true,
bool Function()? isSignedIn,
WalletController? wallet,
}) {
return PlayerLogic(
history: FakeWatchHistory(),
playback: playback,
wallet: wallet,
pictureInPicture: AndroidPictureInPicture(
channel: const MethodChannel('test/player_locked_auth_flow/pip'),
),
waitForFrame: () async {},
isSignedIn: isSignedIn ?? () => signedIn,
);
}
Future<void> _drain(PlayerLogic logic) async {
for (var attempt = 0; attempt < 4; attempt++) {
await pumpEventQueue();
await logic.session.playbackSettled;
await logic.session.preloadSettled;
}
}
class _NoopPlayer implements PlayerPort {
@override
Future<void> initialize() async {}
@override
Future<void> play() async {}
@override
Future<void> pause() async {}
@override
Future<void> dispose() async {}
}
class _UnlockCommerceRepository implements CommerceRepository {
@override
Future<WalletSnapshot> fetchWallet() async => const WalletSnapshot(
wallet: CoinWallet(
coinBalance: 100,
totalRechargedCoin: 0,
totalConsumedCoin: 0,
totalRefundedCoin: 0,
),
);
@override
Future<UnlockResult> unlock(String episodeId) async => const UnlockResult(
unlocked: true,
accessType: 'ENTITLED',
coinCost: 30,
coinBalance: 70,
);
}
+3 -3
View File
@@ -266,7 +266,7 @@ void main() {
expect(tester.takeException(), isNull);
});
testWidgets('should_open_auth_gate_without_liking_when_signed_out', (
testWidgets('should_like_without_login_when_signed_out', (
tester,
) async {
SharedPreferencesAsyncPlatform.instance =
@@ -279,8 +279,8 @@ void main() {
await tester.tap(find.byKey(const Key('player-rail-like')));
await tester.pumpAndSettle();
expect(find.text('Sign in to continue'), findsOneWidget);
expect(interactions.likeTargets, isEmpty);
expect(find.text('Sign in to continue'), findsNothing);
expect(interactions.likeTargets, ['EPISODE:city-lights-demo-1']);
});
testWidgets('should_show_saved_player_drama_in_favorites', (tester) async {