// Author: fruit.com.cn import 'dart:convert'; 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/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/core/platform/haptics.dart'; import 'package:fruitshorts/core/theme/app_colors.dart'; import 'package:fruitshorts/core/widgets/empty_state.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_haptics.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_gate_guest_comment_input_without_focus', (tester) async { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); final repository = FakeCommentRepository(pages: [const CommentPage()]); await _pumpSheet(tester, repository: repository); final input = find.byKey(const Key('comment-input'), skipOffstage: false); final placeholder = find.byKey(const Key('comment-input-placeholder')); expect(placeholder, findsOneWidget); expect(find.byKey(const Key('comment-input')), findsNothing); expect(find.text('Sign in to comment'), findsOneWidget); expect(tester.widget(input).focusNode?.hasFocus, isFalse); await tester.tap(placeholder); await tester.pumpAndSettle(); expect(find.text('Sign in to continue'), findsOneWidget); expect(tester.widget(input).focusNode?.hasFocus, isFalse); await tester.tap(find.text('Not now')); await tester.pumpAndSettle(); expect(tester.widget(input).focusNode?.hasFocus, isFalse); }); testWidgets('should_focus_comment_input_after_successful_login', ( tester, ) async { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); final repository = FakeCommentRepository(pages: [const CommentPage()]); final auth = 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.pump(); await tester.pump(const Duration(milliseconds: 300)); expect( tester .widget(find.byKey(const Key('comment-input'))) .focusNode ?.hasFocus, isTrue, ); }); testWidgets('should_focus_real_input_when_fake_send_is_tapped', ( tester, ) async { final repository = FakeCommentRepository(pages: [const CommentPage()]); await _pumpSheet(tester, repository: repository); expect(find.byKey(const Key('comment-input')), findsNothing); await tester.tap(find.byKey(const Key('comment-post'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('comment-input-placeholder')), findsNothing); final input = find.byKey(const Key('comment-input')); expect(input, findsOneWidget); expect(tester.widget(input).focusNode?.hasFocus, isTrue); expect( tester.widget(input).textAlignVertical, TextAlignVertical.center, ); await tester.enterText(input, 'First line\nSecond line'); await tester.pump(); final multilineField = tester.widget(input); expect(multilineField.textAlignVertical, TextAlignVertical.top); expect(multilineField.style?.height, 1.4); }); testWidgets('should_gate_guest_post_without_creating_comment', ( tester, ) async { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); final repository = FakeCommentRepository(pages: [const CommentPage()]); await _pumpSheet(tester, repository: repository); await tester.tap(find.byKey(const Key('comment-post'))); await tester.pumpAndSettle(); expect(find.text('Sign in to continue'), findsOneWidget); expect(repository.publishCalls, isEmpty); }); 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); expect( tester.widget(find.byKey(const Key('comment-count-label'))).data, 'Comments', ); await tester.tap(find.byKey(const Key('comment-list-load-more'))); await tester.pumpAndSettle(); expect(find.text('Second page'), findsOneWidget); expect(find.text('2 comments'), findsOneWidget); expect(repository.pageCalls.last.cursor, '10'); }); testWidgets('should_show_server_total_that_includes_replies', (tester) async { final repository = FakeCommentRepository( pages: [ CommentPage( comments: [ commentFixture(id: '10', content: 'Root one'), commentFixture(id: '11', content: 'Root two'), ], ), ], countResult: 9, ); await _pumpSheet(tester, repository: repository); expect(find.text('9 comments'), findsOneWidget); expect(repository.countCalls, ['5']); }); testWidgets('should_fall_back_to_exact_count_when_server_total_fails', ( tester, ) async { final repository = FakeCommentRepository( pages: [ CommentPage( comments: [ commentFixture(id: '10', content: 'Root one'), commentFixture(id: '11', content: 'Root two'), ], ), ], countError: StateError('offline'), ); await _pumpSheet(tester, repository: repository); expect(find.text('2 comments'), findsOneWidget); }); testWidgets('should_show_compact_empty_state_when_no_comments_exist', ( tester, ) async { final repository = FakeCommentRepository(pages: [const CommentPage()]); await _pumpSheet(tester, repository: repository); expect(find.byKey(const Key('comment-list-empty')), findsOneWidget); expect(find.byType(EmptyState), findsOneWidget); expect(tester.widget(find.byType(EmptyState)).compact, isTrue); expect(find.text('No comments yet.'), findsOneWidget); }); 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.tap(find.byKey(const Key('comment-input-placeholder'))); await tester.pumpAndSettle(); await tester.enterText( find.byKey(const Key('comment-input')), 'blocked text', ); await tester.pump(); await tester.tap(find.byKey(const Key('comment-post'))); await tester.pumpAndSettle(); expect(find.text('评论包含敏感词'), findsOneWidget); final field = tester.widget( find.byKey(const Key('comment-input')), ); expect(field.controller?.text, 'blocked text'); // 发送失败时保留输入与焦点,用户可以直接改完再发。 expect(field.focusNode?.hasFocus, isTrue); }, ); testWidgets('should_clear_and_keep_input_focused_after_successful_post', ( tester, ) async { addTearDown(tester.view.resetViewInsets); final repository = FakeCommentRepository( pages: [const CommentPage()], publishResult: commentFixture(id: '11', content: 'Nice demo', mine: true), countResult: 1, ); await _pumpSheet(tester, repository: repository); final input = find.byKey(const Key('comment-input')); tester.view.viewInsets = const FakeViewPadding(bottom: 180); await tester.tap(find.byKey(const Key('comment-input-placeholder'))); await tester.pumpAndSettle(); expect(tester.widget(input).focusNode?.hasFocus, isTrue); await tester.enterText(input, 'Nice demo'); await tester.pump(); expect( tester .widget(find.byKey(const Key('comment-post'))) .onPressed, isNotNull, ); await tester.tap(find.byKey(const Key('comment-post'))); await tester.pumpAndSettle(); expect(tester.widget(input).controller?.text, isEmpty); expect(tester.widget(input).focusNode?.hasFocus, isTrue); expect(tester.widget(input).maxLines, 4); expect(find.byKey(const Key('comment-list')), findsOneWidget); expect(find.byKey(const Key('comment-count-label')), findsOneWidget); expect( tester .widget(find.byKey(const Key('comment-post'))) .onPressed, isNull, ); expect(repository.countCalls, ['5', '5']); }); testWidgets('should_keep_composer_separate_when_keyboard_opens', ( tester, ) async { tester.view.physicalSize = const Size(390, 844); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); addTearDown(tester.view.resetViewInsets); final repository = FakeCommentRepository( pages: [ CommentPage( comments: [commentFixture(id: '10', content: 'A comment')], ), ], ); await _pumpSheet(tester, repository: repository); expect(find.byKey(const Key('comment-list')), findsOneWidget); expect(find.byKey(const Key('comment-input')), findsNothing); expect(find.byKey(const Key('comment-input-placeholder')), findsOneWidget); final hiddenInput = find.byKey( const Key('comment-input'), skipOffstage: false, ); final editableBefore = tester.state( find.descendant( of: hiddenInput, matching: find.byType(EditableText, skipOffstage: false), skipOffstage: false, ), ); final sheet = find.byKey(const Key('player-comments-sheet')); final list = find.byKey(const Key('comment-list')); final heightBeforeKeyboard = tester.getSize(sheet).height; tester.view.viewInsets = const FakeViewPadding(bottom: 180); await tester.tap(find.byKey(const Key('comment-input-placeholder'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('comment-list')), findsOneWidget); expect(find.byKey(const Key('comment-count-label')), findsOneWidget); expect(find.byKey(const Key('comment-composer')), findsOneWidget); expect(find.byKey(const Key('comment-input-surface')), findsOneWidget); expect(find.byKey(const Key('comment-input-placeholder')), findsNothing); expect( tester.widget(find.byKey(const Key('comment-input'))).maxLines, 4, ); final editableAfter = tester.state( find.descendant( of: find.byKey(const Key('comment-input')), matching: find.byType(EditableText), ), ); expect(identical(editableBefore, editableAfter), isTrue); expect(sheet, findsOneWidget); expect(list, findsOneWidget); expect(tester.getSize(sheet).height, closeTo(heightBeforeKeyboard, 0.1)); expect(find.byKey(const Key('comment-count-label')), findsOneWidget); final field = tester.widget( find.byKey(const Key('comment-input')), ); expect(field.minLines, 1); expect(field.maxLines, 4); expect(field.style?.fontSize, 15); expect( tester .widget(find.byKey(const Key('comment-post'))) .onPressed, isNull, ); expect(tester.getSize(find.byKey(const Key('comment-post'))).height, 32); final listRect = tester.getRect(list); final inputRect = tester.getRect(find.byKey(const Key('comment-input'))); expect(listRect.overlaps(inputRect), isFalse); expect(listRect.bottom, lessThanOrEqualTo(inputRect.top)); expect(find.byIcon(Icons.image_outlined), findsNothing); expect(find.byIcon(Icons.emoji_emotions_outlined), findsNothing); expect(find.byIcon(Icons.alternate_email_rounded), findsNothing); await tester.enterText(find.byKey(const Key('comment-input')), 'Nice'); await tester.pump(); expect( tester .widget(find.byKey(const Key('comment-post'))) .onPressed, isNotNull, ); final editableDuringKeyboard = tester.state( find.descendant( of: find.byKey(const Key('comment-input')), matching: find.byType(EditableText), ), ); expect(identical(editableAfter, editableDuringKeyboard), isTrue); expect(tester.getBottomLeft(sheet).dy, closeTo(844 - 180, 0.1)); expect( tester.widget(list).padding?.resolve(TextDirection.ltr).bottom, greaterThanOrEqualTo(180 + 44), ); expect( tester.getBottomLeft(find.byKey(const Key('comment-input'))).dy, lessThanOrEqualTo(844 - 180), ); expect( tester .widget(find.byKey(const Key('comment-input'))) .focusNode ?.hasFocus, isTrue, ); await tester.tapAt(const Offset(20, 20)); await tester.pump(); expect(tester.widget(hiddenInput).focusNode?.hasFocus, isFalse); tester.view.viewInsets = const FakeViewPadding(); await tester.pumpAndSettle(); expect(list, findsOneWidget); expect(find.byKey(const Key('comment-count-label')), findsOneWidget); expect(find.byKey(const Key('comment-composer')), findsOneWidget); expect(find.byKey(const Key('comment-input-placeholder')), findsOneWidget); expect(find.byKey(const Key('comment-input')), findsNothing); expect(tester.getSize(sheet).height, closeTo(heightBeforeKeyboard, 0.1)); expect(tester.getSize(sheet).height, lessThan(520)); final editableAfterKeyboard = tester.state( find.descendant( of: hiddenInput, matching: find.byType(EditableText, skipOffstage: false), skipOffstage: false, ), ); expect(identical(editableAfter, editableAfterKeyboard), isTrue); expect(tester.widget(hiddenInput).controller?.text, 'Nice'); expect(tester.widget(hiddenInput).focusNode?.hasFocus, isFalse); }); testWidgets('should_dismiss_from_outside_in_two_steps', (tester) async { tester.view.physicalSize = const Size(390, 844); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); addTearDown(tester.view.resetViewInsets); final repository = FakeCommentRepository( pages: [ CommentPage( comments: [commentFixture(id: '10', content: 'A comment')], ), ], ); await _pumpSheet(tester, repository: repository); final sheet = find.byKey(const Key('player-comments-sheet')); final input = find.byKey(const Key('comment-input')); final hiddenInput = find.byKey( const Key('comment-input'), skipOffstage: false, ); expect( find.byKey(const Key('player-comments-dismiss-layer')), findsOneWidget, ); tester.view.viewInsets = const FakeViewPadding(bottom: 180); await tester.tap(find.byKey(const Key('comment-input-placeholder'))); await tester.pumpAndSettle(); expect(sheet, findsOneWidget); expect(input, findsOneWidget); expect(tester.widget(input).focusNode?.hasFocus, isTrue); await tester.tap(find.byKey(const Key('comment-list'))); await tester.pump(); expect(sheet, findsOneWidget); await tester.tapAt(const Offset(20, 20)); await tester.pump(); expect(sheet, findsOneWidget); expect(tester.widget(hiddenInput).focusNode?.hasFocus, isFalse); expect(find.byKey(const Key('comment-input-placeholder')), findsOneWidget); expect(input, findsNothing); tester.view.viewInsets = const FakeViewPadding(); await tester.pumpAndSettle(); expect(sheet, findsOneWidget); await tester.tapAt(const Offset(20, 20)); await tester.pumpAndSettle(); expect(sheet, findsNothing); }); testWidgets('should_use_polished_comment_sizes', (tester) async { final repository = FakeCommentRepository( pages: [ CommentPage( comments: [commentFixture(id: '10', content: 'Great episode')], ), ], ); await _pumpSheet(tester, repository: repository); expect( tester.getSize(find.byKey(const Key('comment-post'))), const Size(38, 38), ); expect( tester.getSize(find.byKey(const Key('comment-input-placeholder'))).height, 44, ); final placeholderLabel = find.byKey( const Key('comment-input-placeholder-label'), ); final placeholderSendIcon = find.descendant( of: find.byKey(const Key('comment-post')), matching: find.byIcon(Icons.send_rounded), ); expect( tester.getCenter(placeholderLabel).dy, closeTo(tester.getCenter(placeholderSendIcon).dy, 1), ); expect(find.byKey(const Key('player-comments-drag-handle')), findsNothing); expect( tester.getSize(find.byKey(const Key('player-comments-close'))), const Size(44, 44), ); expect( find.descendant( of: find.byKey(const Key('player-comments-close')), matching: find.byIcon(Icons.keyboard_arrow_down_rounded), ), findsOneWidget, ); final title = tester.widget( find.byKey(const Key('comment-count-label')), ); expect(title.style?.fontSize, 16); expect(title.style?.fontWeight, FontWeight.w600); expect( tester.getCenter(find.byKey(const Key('comment-count-label'))).dx, closeTo( tester.getCenter(find.byKey(const Key('player-comments-sheet'))).dx, 0.5, ), ); final avatar = tester.widget( find.descendant( of: find.byKey(const Key('comment-item-10')), matching: find.byType(CircleAvatar), ), ); expect(avatar.radius, 20); final body = tester.widget(find.text('Great episode')); expect(body.style?.fontSize, 16); expect(body.style?.height, 1.4); final composer = tester.widget( find.byKey(const Key('comment-input-placeholder')), ); final decoration = composer.decoration! as BoxDecoration; expect(decoration.color, AppColors.commentInput); expect(decoration.borderRadius, BorderRadius.circular(22)); }); testWidgets('should_hide_report_and_delete_actions', (tester) async { final repository = FakeCommentRepository( pages: [ CommentPage( comments: [ commentFixture(id: '10', content: 'My comment', mine: true), ], ), ], ); await _pumpSheet(tester, repository: repository); expect( find.byWidgetPredicate( (widget) => widget.key is ValueKey && (widget.key! as ValueKey).value.startsWith('comment-menu-'), ), findsNothing, ); expect(find.text('Delete comment'), findsNothing); expect(find.text('Report comment'), findsNothing); expect(find.byIcon(Icons.more_horiz_rounded), findsNothing); }); testWidgets('should_size_short_list_to_content', (tester) async { tester.view.physicalSize = const Size(390, 844); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); final repository = FakeCommentRepository( pages: [ CommentPage( comments: [ commentFixture(id: '10', content: 'Short list one'), commentFixture(id: '11', content: 'Short list two'), ], ), ], ); await _pumpSheet(tester, repository: repository); final sheet = find.byKey(const Key('player-comments-sheet')); expect(tester.getSize(sheet).height, lessThan(520)); final lastCommentBottom = tester .getBottomLeft(find.byKey(const Key('comment-item-11'))) .dy; final composerTop = tester .getTopLeft(find.byKey(const Key('comment-composer'))) .dy; expect(composerTop - lastCommentBottom, lessThan(80)); }); testWidgets('should_keep_minimum_height_for_empty_state', (tester) async { final repository = FakeCommentRepository(pages: [const CommentPage()]); await _pumpSheet(tester, repository: repository); expect( tester.getSize(find.byKey(const Key('player-comments-sheet'))).height, greaterThanOrEqualTo(220), ); }); testWidgets('should_keep_long_comment_list_scrollable', (tester) async { tester.view.physicalSize = const Size(390, 844); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); final repository = FakeCommentRepository( pages: [CommentPage(comments: _comments(30))], ); await _pumpSheet(tester, repository: repository); final scrollable = find .descendant( of: find.byKey(const Key('comment-list')), matching: find.byType(Scrollable), ) .first; final position = tester.state(scrollable).position; expect(position.maxScrollExtent, greaterThan(0)); await tester.drag( find.byKey(const Key('comment-list')), const Offset(0, -300), ); await tester.pumpAndSettle(); expect(position.pixels, greaterThan(0)); expect( tester.getSize(find.byKey(const Key('player-comments-sheet'))).height, 520, ); }); testWidgets('should_hide_comment_like_controls', (tester) async { final repository = FakeCommentRepository( pages: [ CommentPage( comments: [commentFixture(id: '10', content: 'No likes shown')], ), ], ); await _pumpSheet(tester, repository: repository); expect( find.byWidgetPredicate( (widget) => widget.key is ValueKey && (widget.key! as ValueKey).value.startsWith('comment-like-'), ), findsNothing, ); expect(find.byIcon(Icons.favorite_rounded), findsNothing); expect(find.byIcon(Icons.favorite_border_rounded), findsNothing); }); testWidgets('should_close_sheet_from_down_arrow', (tester) async { final repository = FakeCommentRepository(pages: [const CommentPage()]); await _pumpSheet(tester, repository: repository); await tester.tap(find.byKey(const Key('player-comments-close'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('player-comments-sheet')), findsNothing); }); 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, ), ); final haptics = FakeHaptics(); await _pumpSheet(tester, repository: repository, haptics: haptics); await tester.tap(find.byKey(const Key('comment-input-placeholder'))); await tester.pumpAndSettle(); await tester.enterText( find.byKey(const Key('comment-input')), 'hello badword', ); await tester.pump(); await tester.tap(find.byKey(const Key('comment-post'))); await tester.pumpAndSettle(); FocusManager.instance.primaryFocus?.unfocus(); await tester.pumpAndSettle(); expect(find.text('hello ***'), findsOneWidget); expect(find.text('Under review'), findsOneWidget); expect(haptics.lightImpacts, 1); final field = tester.widget( find.byKey(const Key('comment-input'), skipOffstage: false), ); 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.pump(); await tester.tap(find.byKey(const Key('comment-post'))); await tester.pumpAndSettle(); expect(repository.publishCalls.single.parentId, '10'); FocusManager.instance.primaryFocus?.unfocus(); await tester.pumpAndSettle(); 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); }); } Future _pumpSheet( WidgetTester tester, { required FakeCommentRepository repository, FakeHaptics? haptics, }) async { final auth = await _auth(); await tester.pumpWidget( MultiProvider( providers: [ Provider.value(value: repository), Provider.value( value: FakeInteractionRepository(), ), ChangeNotifierProvider.value(value: auth), Provider.value(value: haptics ?? FakeHaptics()), ], child: MaterialApp( locale: const Locale('en'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, initialRoute: '/comments', routes: { '/': (_) => const Scaffold(backgroundColor: AppColors.ink), '/comments': (_) => const Scaffold( backgroundColor: AppColors.ink, resizeToAvoidBottomInset: false, body: Align( alignment: Alignment.bottomCenter, child: DramaCommentsSheet(drama: _drama), ), ), }, ), ), ); await tester.pumpAndSettle(); return auth; } Future _auth() async { final client = ApiClient( baseUrl: 'http://127.0.0.1:8081', adapter: FakeHttpAdapter((options) async { if (options.uri.path == '/app/auth/login') { return FakeHttpResponse( jsonEncode({ 'code': 0, 'message': 'ok', 'data': { 'accessToken': 'access', 'refreshToken': 'refresh', 'expiresIn': 7200, 'user': {'uid': '9', 'nickname': 'Viewer', 'avatarUrl': ''}, }, }), ); } return FakeHttpResponse('{"code":0,"message":"ok","data":null}'); }), ); final store = AuthSessionStore(); final session = await store.read(); final auth = _TestAuthController( AuthApi(client), store, signedIn: session != null, ); await auth.load(); return auth; } List _comments(int count) => [ for (var index = 0; index < count; index++) commentFixture(id: '${index + 10}', content: 'Comment ${index + 1}'), ]; class _TestAuthController extends AuthController { _TestAuthController(super.api, super.store, {required this._signedIn}); bool _signedIn; @override bool get isSignedIn => _signedIn; void setSignedIn(bool value) { _signedIn = value; notifyListeners(); } }