test(client): 用例名统一为 should_<期望>_when_<条件>(155/155)
为什么:红线第 8 条门禁立即生效,要求 Flutter 测试命名合规;此前 155 条用例名是英文自然语言 或中文描述,合规数 0/155,属存量欠账,按"先补齐再上新功能"在本批处理。 - 25 个测试文件、155 条用例改名,按「文件 + 旧名」精确定位,每条断言命中恰好 1 次才替换 - 只改名字字面量:改动行 155 删 + 155 增,无一行不含字符串字面量(断言与用例体零改动) - 门禁:dart analyze lib test = No issues found;flutter test = 155 全过
This commit is contained in:
1 parent
1853e1845c
commit
0e5d550b50
25 files changed
+155
-155
No files matched your search
@@ -54,7 +54,7 @@ void main() {
|
||||
);
|
||||
}
|
||||
|
||||
test('should store the session and expose a signed-in state when register succeeds', () async {
|
||||
test('should_store_session_and_expose_signed_in_state_when_register_succeeds', () async {
|
||||
late AuthController auth;
|
||||
final adapter = FakeHttpAdapter((options) async {
|
||||
expect(options.uri.path, '/app/auth/register');
|
||||
@@ -77,7 +77,7 @@ void main() {
|
||||
expect((await AuthSessionStore().read())?.refreshToken, 'r1');
|
||||
});
|
||||
|
||||
test('should refresh once and retry with the new token when a response carries code 401', () async {
|
||||
test('should_refresh_once_and_retry_with_new_token_when_response_code_is_401', () async {
|
||||
var calls = 0;
|
||||
late AuthController auth;
|
||||
final adapter = FakeHttpAdapter((options) async {
|
||||
@@ -119,7 +119,7 @@ void main() {
|
||||
expect(auth.accessToken, 'a2');
|
||||
});
|
||||
|
||||
test('should clear the session when the refresh itself fails', () async {
|
||||
test('should_clear_session_when_refresh_itself_fails', () async {
|
||||
late AuthController auth;
|
||||
final adapter = FakeHttpAdapter((options) async {
|
||||
if (options.uri.path == '/app/auth/refresh') {
|
||||
@@ -147,7 +147,7 @@ void main() {
|
||||
expect(await AuthSessionStore().read(), isNull);
|
||||
});
|
||||
|
||||
test('should clear the local session when sign out hits a server error', () async {
|
||||
test('should_clear_local_session_when_sign_out_hits_server_error', () async {
|
||||
late AuthController auth;
|
||||
final adapter = FakeHttpAdapter((options) async {
|
||||
if (options.uri.path == '/app/auth/logout') {
|
||||
|
||||
@@ -45,7 +45,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
testWidgets('should show the email error and stay on the page when the form is empty', (tester) async {
|
||||
testWidgets('should_show_email_error_and_stay_on_page_when_form_is_empty', (tester) async {
|
||||
await pumpAuthPage(tester);
|
||||
|
||||
expect(find.text('Welcome back'), findsOneWidget);
|
||||
@@ -57,7 +57,7 @@ void main() {
|
||||
expect(find.text('Welcome back'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should reject a weak password locally when only letters are typed', (tester) async {
|
||||
testWidgets('should_reject_weak_password_locally_when_only_letters_are_typed', (tester) async {
|
||||
await pumpAuthPage(tester);
|
||||
|
||||
await tester.enterText(find.byType(TextField).first, 'a@b.com');
|
||||
@@ -68,7 +68,7 @@ void main() {
|
||||
expect(find.text('Password needs both letters and numbers'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should flip between sign in and sign up when the switch link is tapped', (tester) async {
|
||||
testWidgets('should_flip_between_sign_in_and_sign_up_when_switch_link_is_tapped', (tester) async {
|
||||
await pumpAuthPage(tester);
|
||||
expect(find.text('Welcome back'), findsOneWidget);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:nontonki/core/data/repositories/catalog_repository.dart';
|
||||
import 'package:nontonki/core/data/services/catalog_controller.dart';
|
||||
|
||||
void main() {
|
||||
test('空目录与加载失败可区分,重试后恢复', () async {
|
||||
test('should_distinguish_empty_catalog_from_load_failure_when_retry_succeeds', () async {
|
||||
final repository = ScriptedCatalogRepository();
|
||||
final controller = CatalogController(repository);
|
||||
|
||||
@@ -30,7 +30,7 @@ void main() {
|
||||
expect(controller.sections.single.title, '热门推荐');
|
||||
});
|
||||
|
||||
test('详情按需加载:同一部短剧只请求一次', () async {
|
||||
test('should_request_drama_detail_once_when_it_is_loaded_on_demand', () async {
|
||||
final repository = ScriptedCatalogRepository();
|
||||
final controller = CatalogController(repository);
|
||||
repository.feed = HomeFeed(
|
||||
@@ -53,7 +53,7 @@ void main() {
|
||||
expect(controller.dramas.single.episodes, hasLength(2));
|
||||
});
|
||||
|
||||
test('播放票据:可播放写入地址,锁定集保持无地址', () async {
|
||||
test('should_write_play_url_for_free_episode_and_keep_locked_episode_empty_when_ticket_is_issued', () async {
|
||||
final repository = ScriptedCatalogRepository();
|
||||
final controller = CatalogController(repository);
|
||||
repository.tickets['10'] = const PlayTicket(
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:nontonki/core/data/models/drama.dart';
|
||||
|
||||
void main() {
|
||||
test('a list card carries one playable preview and the total count', () {
|
||||
test('should_carry_one_preview_and_total_count_when_list_card_is_parsed', () {
|
||||
final drama = Drama.fromCardJson(const {
|
||||
'id': 5,
|
||||
'title': '闪婚后',
|
||||
@@ -29,7 +29,7 @@ void main() {
|
||||
expect(drama.isHot, isTrue);
|
||||
});
|
||||
|
||||
test('a list card without a playable preview has no episodes', () {
|
||||
test('should_have_no_episodes_when_list_card_has_no_playable_preview', () {
|
||||
final drama = Drama.fromCardJson(const {
|
||||
'id': 6,
|
||||
'title': '替嫁新娘',
|
||||
@@ -40,7 +40,7 @@ void main() {
|
||||
expect(drama.episodeCount, 12);
|
||||
});
|
||||
|
||||
test('a detail response counts the episodes it ships', () {
|
||||
test('should_count_shipped_episodes_when_detail_response_is_parsed', () {
|
||||
final drama = Drama.fromDetailJson(const {
|
||||
'id': 5,
|
||||
'title': '闪婚后',
|
||||
|
||||
@@ -11,7 +11,7 @@ void main() {
|
||||
InMemorySharedPreferencesAsync.empty();
|
||||
});
|
||||
|
||||
test('no stored choice keeps the device locale', () async {
|
||||
test('should_keep_device_locale_when_no_choice_is_stored', () async {
|
||||
final controller = LocaleController();
|
||||
var notifications = 0;
|
||||
controller.addListener(() => notifications++);
|
||||
@@ -22,7 +22,7 @@ void main() {
|
||||
expect(notifications, 1);
|
||||
});
|
||||
|
||||
test('a chosen language survives a restart', () async {
|
||||
test('should_restore_chosen_language_when_app_restarts', () async {
|
||||
final first = LocaleController();
|
||||
await first.select('zh');
|
||||
expect(first.locale, const Locale('zh'));
|
||||
@@ -32,7 +32,7 @@ void main() {
|
||||
expect(reopened.locale, const Locale('zh'));
|
||||
});
|
||||
|
||||
test('selecting english clears a stored chinese preference', () async {
|
||||
test('should_clear_stored_chinese_preference_when_english_is_selected', () async {
|
||||
await LocaleController().select('zh');
|
||||
|
||||
final reopened = LocaleController();
|
||||
@@ -44,7 +44,7 @@ void main() {
|
||||
expect(third.locale, const Locale('en'));
|
||||
});
|
||||
|
||||
test('an unsupported code clears the locale instead of throwing', () async {
|
||||
test('should_clear_locale_when_code_is_unsupported', () async {
|
||||
final controller = LocaleController();
|
||||
await controller.select('zh');
|
||||
expect(controller.locale, const Locale('zh'));
|
||||
@@ -54,7 +54,7 @@ void main() {
|
||||
expect(controller.locale, isNull);
|
||||
});
|
||||
|
||||
test('an unsupported stored code resolves to the device locale', () async {
|
||||
test('should_resolve_to_device_locale_when_stored_code_is_unsupported', () async {
|
||||
await SharedPreferencesAsync().setString('ui.locale', 'fr');
|
||||
|
||||
final controller = LocaleController();
|
||||
@@ -63,7 +63,7 @@ void main() {
|
||||
expect(controller.locale, isNull);
|
||||
});
|
||||
|
||||
test('a broken preference store never breaks load or select', () async {
|
||||
test('should_survive_load_and_select_when_preference_store_is_broken', () async {
|
||||
SharedPreferencesAsyncPlatform.instance = _BrokenPreferences();
|
||||
final controller = LocaleController();
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ void main() {
|
||||
});
|
||||
|
||||
group('platform support', () {
|
||||
test('android is the only supported target', () {
|
||||
test('should_support_only_android_when_target_is_checked', () {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
expect(pip().isPlatformSupported, isTrue);
|
||||
|
||||
@@ -50,7 +50,7 @@ void main() {
|
||||
});
|
||||
|
||||
group('enter', () {
|
||||
test('is refused without touching the channel off android', () async {
|
||||
test('should_refuse_without_touching_channel_when_target_is_not_android', () async {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
||||
final calls = <MethodCall>[];
|
||||
messenger.setMockMethodCallHandler(channel, (call) async {
|
||||
@@ -63,7 +63,7 @@ void main() {
|
||||
expect(calls, isEmpty);
|
||||
});
|
||||
|
||||
test('a missing native answer counts as a refusal', () async {
|
||||
test('should_count_as_refused_when_native_answer_is_missing', () async {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
messenger.setMockMethodCallHandler(channel, (call) async => null);
|
||||
addTearDown(() => messenger.setMockMethodCallHandler(channel, null));
|
||||
@@ -71,7 +71,7 @@ void main() {
|
||||
expect(await pip().enter(), isFalse);
|
||||
});
|
||||
|
||||
test('a native success is reported as entered', () async {
|
||||
test('should_report_entered_when_native_call_succeeds', () async {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
final calls = <MethodCall>[];
|
||||
messenger.setMockMethodCallHandler(channel, (call) async {
|
||||
@@ -87,7 +87,7 @@ void main() {
|
||||
});
|
||||
|
||||
group('state listener', () {
|
||||
test('pip state changes reach the listener in order', () async {
|
||||
test('should_deliver_pip_state_changes_in_order_when_native_events_arrive', () async {
|
||||
final received = <bool>[];
|
||||
pip().setStateListener(received.add);
|
||||
|
||||
@@ -97,7 +97,7 @@ void main() {
|
||||
expect(received, [true, false]);
|
||||
});
|
||||
|
||||
test('only a literal true is read as enabled', () async {
|
||||
test('should_treat_only_literal_true_as_enabled_when_native_value_arrives', () async {
|
||||
final received = <bool>[];
|
||||
pip().setStateListener(received.add);
|
||||
|
||||
@@ -108,7 +108,7 @@ void main() {
|
||||
expect(received, [false, false, false]);
|
||||
});
|
||||
|
||||
test('unrelated native methods are ignored', () async {
|
||||
test('should_ignore_unrelated_native_methods_when_events_arrive', () async {
|
||||
final received = <bool>[];
|
||||
pip().setStateListener(received.add);
|
||||
|
||||
@@ -118,7 +118,7 @@ void main() {
|
||||
expect(received, [true]);
|
||||
});
|
||||
|
||||
test('clearing the listener silences later callbacks', () async {
|
||||
test('should_silence_later_callbacks_when_listener_is_cleared', () async {
|
||||
final received = <bool>[];
|
||||
final observer = pip()..setStateListener(received.add);
|
||||
await sendToDart('onPipChanged', true);
|
||||
|
||||
@@ -14,7 +14,7 @@ void main() {
|
||||
return jsonEncode({'code': code, 'message': message, 'data': data});
|
||||
}
|
||||
|
||||
test('should return the data field when the envelope code is zero', () async {
|
||||
test('should_return_data_field_when_envelope_code_is_zero', () async {
|
||||
final adapter = FakeHttpAdapter(
|
||||
(_) async => FakeHttpResponse(envelope({'id': 7})),
|
||||
);
|
||||
@@ -24,7 +24,7 @@ void main() {
|
||||
expect((data as Map)['id'], 7);
|
||||
});
|
||||
|
||||
test('should throw an ApiException carrying the server message when the envelope code is not zero', () async {
|
||||
test('should_throw_api_exception_with_server_message_when_envelope_code_is_not_zero', () async {
|
||||
final adapter = FakeHttpAdapter(
|
||||
(_) async => FakeHttpResponse(
|
||||
envelope(null, code: 40001, message: '邮箱格式不正确'),
|
||||
@@ -42,7 +42,7 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('should map a body without envelope to an ApiException carrying the http status', () async {
|
||||
test('should_map_to_api_exception_with_http_status_when_body_has_no_envelope', () async {
|
||||
final adapter = FakeHttpAdapter((_) async => FakeHttpResponse('oops', 502));
|
||||
final client = ApiClient(baseUrl: 'http://127.0.0.1:8081', adapter: adapter);
|
||||
|
||||
@@ -54,7 +54,7 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('should send the bearer token and the device header when both are available', () async {
|
||||
test('should_send_bearer_token_and_device_header_when_both_are_available', () async {
|
||||
final adapter = FakeHttpAdapter((_) async => FakeHttpResponse(envelope(null)));
|
||||
final client = ApiClient(
|
||||
baseUrl: 'http://127.0.0.1:8081',
|
||||
@@ -71,7 +71,7 @@ void main() {
|
||||
expect(request.headers['Accept'], 'application/json');
|
||||
});
|
||||
|
||||
test('should put the query map into the request uri', () async {
|
||||
test('should_put_query_map_into_request_uri_when_query_is_given', () async {
|
||||
final adapter = FakeHttpAdapter((_) async => FakeHttpResponse(envelope(null)));
|
||||
final client = ApiClient(baseUrl: 'http://127.0.0.1:8081', adapter: adapter);
|
||||
|
||||
@@ -83,7 +83,7 @@ void main() {
|
||||
expect(uri.queryParameters['page'], '2');
|
||||
});
|
||||
|
||||
test('should not refresh the token when the call opts out of auth retry', () async {
|
||||
test('should_skip_refresh_when_call_opts_out_of_auth_retry', () async {
|
||||
var refreshed = false;
|
||||
final adapter = FakeHttpAdapter(
|
||||
(_) async => FakeHttpResponse(envelope(null, code: 401, message: '登录已过期')),
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:nontonki/pages/discover/widgets/cover_carousel.dart';
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
testWidgets('the cube turns and keeps the title on the poster', (tester) async {
|
||||
testWidgets('should_turn_cube_and_keep_title_on_poster_when_carousel_pages', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
|
||||
@@ -28,7 +28,7 @@ Drama _drama(String id, {List<int> numbers = const [1, 2, 3]}) => Drama(
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
test('the constructor resolves the resume position of episode one', () async {
|
||||
test('should_resolve_resume_position_of_episode_one_when_constructed', () async {
|
||||
final history = FakeWatchHistory()..saved = const Duration(seconds: 4);
|
||||
final logic = DiscoverLogic(history: history, drama: _drama('signal'));
|
||||
|
||||
@@ -39,7 +39,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('refreshPosition re-reads after the drama is swapped', () async {
|
||||
test('should_reread_resume_position_when_drama_is_swapped', () async {
|
||||
final history = FakeWatchHistory();
|
||||
final logic = DiscoverLogic(history: history, drama: _drama('signal'));
|
||||
|
||||
@@ -54,7 +54,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('a list card without episodes resumes from the last watch', () async {
|
||||
test('should_resume_from_last_watch_when_list_card_has_no_episodes', () async {
|
||||
final history = FakeWatchHistory()
|
||||
..saved = const Duration(seconds: 9)
|
||||
..lastEpisode = 'signal-2';
|
||||
@@ -68,7 +68,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('a list card without history has no resume position', () async {
|
||||
test('should_have_no_resume_position_when_list_card_has_no_history', () async {
|
||||
final history = FakeWatchHistory();
|
||||
final logic = DiscoverLogic(
|
||||
history: history,
|
||||
@@ -80,7 +80,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('selectCategory stores the index and notifies once', () {
|
||||
test('should_store_index_and_notify_once_when_category_is_selected', () {
|
||||
final logic = DiscoverLogic(history: FakeWatchHistory(), drama: _drama('s'));
|
||||
var notifications = 0;
|
||||
logic.addListener(() => notifications++);
|
||||
@@ -92,7 +92,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('search visibility follows the flag or the typed text', () {
|
||||
test('should_show_search_when_flag_is_on_or_text_is_typed', () {
|
||||
final logic = DiscoverLogic(history: FakeWatchHistory(), drama: _drama('s'));
|
||||
|
||||
expect(logic.showSearch, isFalse);
|
||||
@@ -109,7 +109,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('dramaFor matches by id and tolerates a missing id', () {
|
||||
test('should_match_by_id_and_tolerate_missing_id_when_looking_up_drama', () {
|
||||
final logic = DiscoverLogic(history: FakeWatchHistory(), drama: _drama('s'));
|
||||
final catalog = [_drama('signal'), _drama('city')];
|
||||
|
||||
@@ -120,7 +120,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('syncDrama only adopts a drama while the page is active', () {
|
||||
test('should_adopt_drama_only_when_page_is_active', () {
|
||||
final history = FakeWatchHistory();
|
||||
final logic = DiscoverLogic(history: history, drama: _drama('signal'));
|
||||
|
||||
@@ -163,7 +163,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
testWidgets('a search request opens and focuses the field when active', (
|
||||
testWidgets('should_open_and_focus_field_when_search_is_requested_and_page_is_active', (
|
||||
tester,
|
||||
) async {
|
||||
final logic = DiscoverLogic(history: FakeWatchHistory(), drama: _drama('s'));
|
||||
@@ -196,7 +196,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
testWidgets('a hidden page never opens or focuses the search field', (
|
||||
testWidgets('should_keep_search_field_closed_when_page_is_hidden', (
|
||||
tester,
|
||||
) async {
|
||||
final logic = DiscoverLogic(history: FakeWatchHistory(), drama: _drama('s'));
|
||||
@@ -231,7 +231,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
testWidgets('act returns the choice for a page and forwards it when embedded', (
|
||||
testWidgets('should_return_choice_and_forward_it_when_act_runs_embedded', (
|
||||
tester,
|
||||
) async {
|
||||
final logic = DiscoverLogic(history: FakeWatchHistory(), drama: _drama('s'));
|
||||
@@ -278,7 +278,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
testWidgets('a card without playable media shows the artwork notice', (
|
||||
testWidgets('should_show_artwork_notice_when_card_has_no_playable_media', (
|
||||
tester,
|
||||
) async {
|
||||
final logic = DiscoverLogic(history: FakeWatchHistory(), drama: _drama('s'));
|
||||
@@ -306,7 +306,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
testWidgets('an embedded card forwards the chosen drama', (tester) async {
|
||||
testWidgets('should_forward_chosen_drama_when_card_is_embedded', (tester) async {
|
||||
final logic = DiscoverLogic(history: FakeWatchHistory(), drama: _drama('s'));
|
||||
late BuildContext context;
|
||||
Drama? selected;
|
||||
@@ -333,7 +333,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('notify after dispose is a no-op', () {
|
||||
test('should_do_nothing_when_notifying_after_dispose', () {
|
||||
final logic = DiscoverLogic(history: FakeWatchHistory(), drama: _drama('s'));
|
||||
var notifications = 0;
|
||||
logic.addListener(() => notifications++);
|
||||
|
||||
@@ -42,7 +42,7 @@ Drama _detail(String id) => Drama(
|
||||
);
|
||||
|
||||
void main() {
|
||||
test('a list card is upgraded to the full episode list', () async {
|
||||
test('should_upgrade_to_full_episode_list_when_detail_loads_for_list_card', () async {
|
||||
var calls = 0;
|
||||
final logic = DramaDetailLogic(
|
||||
drama: _card('signal'),
|
||||
@@ -61,7 +61,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('a failed load keeps the card and reports the reason', () async {
|
||||
test('should_keep_card_and_report_reason_when_load_fails', () async {
|
||||
final logic = DramaDetailLogic(
|
||||
drama: _card('signal'),
|
||||
history: FakeWatchHistory(),
|
||||
@@ -79,7 +79,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('an already loaded detail skips the request', () async {
|
||||
test('should_skip_request_when_detail_is_already_loaded', () async {
|
||||
var calls = 0;
|
||||
final logic = DramaDetailLogic(
|
||||
drama: _detail('signal'),
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:shared_preferences_platform_interface/in_memory_shared_preferenc
|
||||
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
|
||||
|
||||
void main() {
|
||||
test('saved, liked and comments are kept by drama id', () async {
|
||||
test('should_keep_saved_liked_and_comments_by_drama_id_when_toggled', () async {
|
||||
final repository = InMemoryEngagementRepository();
|
||||
final first = EngagementController(repository);
|
||||
await first.load();
|
||||
@@ -28,7 +28,7 @@ void main() {
|
||||
expect(reopened.isSaved('signal'), isFalse);
|
||||
});
|
||||
|
||||
test('local engagement survives a new repository instance', () async {
|
||||
test('should_survive_new_repository_instance_when_engagement_is_local', () async {
|
||||
SharedPreferencesAsyncPlatform.instance =
|
||||
InMemorySharedPreferencesAsync.empty();
|
||||
final first = EngagementController(SharedPreferencesEngagementRepository());
|
||||
|
||||
@@ -27,7 +27,7 @@ void main() {
|
||||
return repository().load();
|
||||
}
|
||||
|
||||
test('a first launch reads an empty snapshot', () async {
|
||||
test('should_read_empty_snapshot_when_first_launch', () async {
|
||||
final snapshot = await repository().load();
|
||||
|
||||
expect(snapshot.savedDramaIds, isEmpty);
|
||||
@@ -35,7 +35,7 @@ void main() {
|
||||
expect(snapshot.commentsByDrama, isEmpty);
|
||||
});
|
||||
|
||||
test('a snapshot survives a save and reload round trip', () async {
|
||||
test('should_survive_save_and_reload_round_trip_when_snapshot_is_stored', () async {
|
||||
await repository().save(
|
||||
const EngagementSnapshot(
|
||||
savedDramaIds: {'signal', 'city'},
|
||||
@@ -55,7 +55,7 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
test('an empty snapshot round trips as empty collections', () async {
|
||||
test('should_round_trip_as_empty_collections_when_snapshot_is_empty', () async {
|
||||
await repository().save(const EngagementSnapshot());
|
||||
|
||||
final loaded = await repository().load();
|
||||
@@ -65,7 +65,7 @@ void main() {
|
||||
expect(loaded.commentsByDrama, isEmpty);
|
||||
});
|
||||
|
||||
test('corrupt or non-object payloads fall back to an empty snapshot', () async {
|
||||
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',
|
||||
@@ -82,7 +82,7 @@ void main() {
|
||||
}
|
||||
});
|
||||
|
||||
test('wrongly typed fields are dropped instead of throwing', () async {
|
||||
test('should_drop_mistyped_fields_when_payload_has_wrong_types', () async {
|
||||
final snapshot = await loadRaw(
|
||||
jsonEncode({
|
||||
'saved': 'signal',
|
||||
@@ -103,7 +103,7 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
test('a payload missing keys falls back per field', () async {
|
||||
test('should_fall_back_per_field_when_payload_keys_are_missing', () async {
|
||||
final snapshot = await loadRaw(jsonEncode({'saved': ['signal']}));
|
||||
|
||||
expect(snapshot.savedDramaIds, {'signal'});
|
||||
@@ -111,7 +111,7 @@ void main() {
|
||||
expect(snapshot.commentsByDrama, isEmpty);
|
||||
});
|
||||
|
||||
test('save writes only json primitives', () async {
|
||||
test('should_write_only_json_primitives_when_saving', () async {
|
||||
await repository().save(
|
||||
const EngagementSnapshot(
|
||||
savedDramaIds: {'signal'},
|
||||
|
||||
@@ -17,7 +17,7 @@ void main() {
|
||||
final en = AppLocalizationsEn();
|
||||
final zh = AppLocalizationsZh();
|
||||
|
||||
test('each seeded drama maps onto its own localized copy', () {
|
||||
test('should_map_each_seeded_drama_to_its_own_localized_copy_when_looked_up', () {
|
||||
final signal = _drama(id: 'the-signal-demo');
|
||||
final city = _drama(id: 'city-lights-demo');
|
||||
final vengeance = _drama(id: 'vengeance-demo');
|
||||
@@ -31,7 +31,7 @@ void main() {
|
||||
expect(localizedDramaSynopsis(en, vengeance), en.dramaVengeanceSynopsis);
|
||||
});
|
||||
|
||||
test('the mapping does not collapse two dramas onto one entry', () {
|
||||
test('should_keep_two_dramas_apart_when_localized_copy_is_mapped', () {
|
||||
final titles = {
|
||||
localizedDramaTitle(en, _drama(id: 'the-signal-demo')),
|
||||
localizedDramaTitle(en, _drama(id: 'city-lights-demo')),
|
||||
@@ -47,7 +47,7 @@ void main() {
|
||||
expect(synopses, hasLength(3));
|
||||
});
|
||||
|
||||
test('localized copy is translated rather than reused verbatim', () {
|
||||
test('should_translate_localized_copy_when_content_is_looked_up', () {
|
||||
final signal = _drama(id: 'the-signal-demo');
|
||||
|
||||
expect(localizedDramaTitle(zh, signal), zh.dramaSignalTitle);
|
||||
@@ -55,7 +55,7 @@ void main() {
|
||||
expect(localizedDramaTitle(zh, signal), isNot(en.dramaSignalTitle));
|
||||
});
|
||||
|
||||
test('an unknown drama keeps the catalog text in every locale', () {
|
||||
test('should_keep_catalog_text_in_every_locale_when_drama_is_unknown', () {
|
||||
final custom = _drama(
|
||||
id: 'custom-demo',
|
||||
title: 'Custom Title',
|
||||
@@ -68,7 +68,7 @@ void main() {
|
||||
expect(localizedDramaSynopsis(zh, custom), 'Custom synopsis');
|
||||
});
|
||||
|
||||
test('the raw catalog text is ignored for a seeded drama', () {
|
||||
test('should_ignore_raw_catalog_text_when_drama_is_seeded', () {
|
||||
final signal = _drama(
|
||||
id: 'the-signal-demo',
|
||||
title: 'stale title',
|
||||
@@ -79,14 +79,14 @@ void main() {
|
||||
expect(localizedDramaSynopsis(en, signal), isNot('stale synopsis'));
|
||||
});
|
||||
|
||||
test('episodes one to four use named titles', () {
|
||||
test('should_use_named_titles_when_episode_number_is_one_to_four', () {
|
||||
expect(localizedEpisodeTitle(en, 1), en.episodeOneTitle);
|
||||
expect(localizedEpisodeTitle(en, 2), en.episodeTwoTitle);
|
||||
expect(localizedEpisodeTitle(en, 3), en.episodeThreeTitle);
|
||||
expect(localizedEpisodeTitle(en, 4), en.episodeFourTitle);
|
||||
});
|
||||
|
||||
test('later episodes fall back to a numbered label', () {
|
||||
test('should_fall_back_to_numbered_label_when_episode_number_is_later', () {
|
||||
for (final number in <int>[5, 12, 100]) {
|
||||
expect(localizedEpisodeTitle(en, number), en.episodeLabel(number));
|
||||
expect(localizedEpisodeTitle(en, number), contains('$number'));
|
||||
|
||||
@@ -18,7 +18,7 @@ void main() {
|
||||
|
||||
final dramas = [_drama('signal'), _drama('city')];
|
||||
|
||||
test('the first catalog entry becomes the current drama', () {
|
||||
test('should_make_first_catalog_entry_the_current_drama_when_catalog_loads', () {
|
||||
final logic = HomeLogic(dramas);
|
||||
|
||||
expect(logic.currentDrama.id, 'signal');
|
||||
@@ -30,11 +30,11 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('an empty catalog cannot build the shell', () {
|
||||
test('should_not_build_shell_when_catalog_is_empty', () {
|
||||
expect(() => HomeLogic(const []), throwsStateError);
|
||||
});
|
||||
|
||||
test('a plain tab switch raises no request counter', () {
|
||||
test('should_not_raise_request_counter_when_only_tab_switches', () {
|
||||
final logic = HomeLogic(dramas);
|
||||
var notifications = 0;
|
||||
logic.addListener(() => notifications++);
|
||||
@@ -50,7 +50,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('each select flag raises exactly its own counter', () {
|
||||
test('should_raise_only_its_own_counter_when_each_select_flag_is_used', () {
|
||||
final logic = HomeLogic(dramas);
|
||||
|
||||
logic.select(1, search: true);
|
||||
@@ -71,7 +71,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('the watchlist shortcut defaults to the second section', () {
|
||||
test('should_default_to_second_section_when_watchlist_shortcut_is_used', () {
|
||||
final logic = HomeLogic(dramas);
|
||||
|
||||
logic.select(2, watchlist: true);
|
||||
@@ -82,7 +82,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('openProfile and openLibrary disagree about the profile flag', () {
|
||||
test('should_differ_on_profile_flag_when_open_profile_and_open_library_are_used', () {
|
||||
final logic = HomeLogic(dramas);
|
||||
|
||||
logic.openProfile();
|
||||
@@ -103,7 +103,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('system back returns home, and home itself opens the drawer', () {
|
||||
test('should_return_home_and_open_drawer_when_system_back_is_pressed', () {
|
||||
final logic = HomeLogic(dramas);
|
||||
|
||||
logic.onSystemBack();
|
||||
@@ -134,7 +134,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('drawer state only notifies on an actual change', () {
|
||||
test('should_notify_only_on_actual_change_when_drawer_state_is_set', () {
|
||||
final logic = HomeLogic(dramas);
|
||||
var notifications = 0;
|
||||
logic.addListener(() => notifications++);
|
||||
@@ -151,7 +151,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('drama and immersive callbacks update state', () {
|
||||
test('should_update_state_when_drama_and_immersive_callbacks_fire', () {
|
||||
final logic = HomeLogic(dramas);
|
||||
|
||||
logic.onDramaChanged(dramas[1]);
|
||||
@@ -162,7 +162,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('my page actions map onto root tabs', () {
|
||||
test('should_map_onto_root_tabs_when_my_page_actions_fire', () {
|
||||
final logic = HomeLogic(dramas);
|
||||
|
||||
logic.myAction(MyPageAction.discover);
|
||||
@@ -179,7 +179,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('notifying after dispose is a no-op', () {
|
||||
test('should_do_nothing_when_notifying_after_dispose', () {
|
||||
final logic = HomeLogic(dramas);
|
||||
var notifications = 0;
|
||||
logic.addListener(() => notifications++);
|
||||
@@ -190,7 +190,7 @@ void main() {
|
||||
expect(notifications, 0);
|
||||
});
|
||||
|
||||
testWidgets('discover actions map onto root tabs', (tester) async {
|
||||
testWidgets('should_map_onto_root_tabs_when_discover_actions_fire', (tester) async {
|
||||
final logic = HomeLogic(dramas);
|
||||
late BuildContext context;
|
||||
await tester.pumpWidget(
|
||||
|
||||
@@ -15,7 +15,7 @@ void main() {
|
||||
|
||||
tearDownAll(client.close);
|
||||
|
||||
test('should surface a business error when signing in with an unknown email', () async {
|
||||
test('should_surface_business_error_when_sign_in_uses_unknown_email', () async {
|
||||
try {
|
||||
await api.login(
|
||||
email: 'no-such-user-for-dio-check@example.com',
|
||||
|
||||
@@ -19,7 +19,7 @@ void main() {
|
||||
}
|
||||
}
|
||||
|
||||
test('首页聚合:能解析出轮播与分区', () async {
|
||||
test('should_parse_banner_and_sections_when_home_feed_is_fetched', () async {
|
||||
if (!await serverUp()) {
|
||||
markTestSkipped('客户端接口服务未启动,已跳过联调测试');
|
||||
return;
|
||||
@@ -33,7 +33,7 @@ void main() {
|
||||
expect(card.posterUrl, startsWith('/files/'));
|
||||
});
|
||||
|
||||
test('详情:剧集按下发顺序排列,可播放集带地址', () async {
|
||||
test('should_order_episodes_by_number_and_attach_url_when_detail_is_fetched', () async {
|
||||
if (!await serverUp()) {
|
||||
markTestSkipped('客户端接口服务未启动,已跳过联调测试');
|
||||
return;
|
||||
@@ -55,7 +55,7 @@ void main() {
|
||||
}
|
||||
});
|
||||
|
||||
test('播放票据:可播放时返回地址', () async {
|
||||
test('should_return_play_url_when_episode_is_playable', () async {
|
||||
if (!await serverUp()) {
|
||||
markTestSkipped('客户端接口服务未启动,已跳过联调测试');
|
||||
return;
|
||||
|
||||
@@ -32,7 +32,7 @@ Drama _drama(String id, {List<int> numbers = const [1, 2, 3]}) => Drama(
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
test('the constructor keeps the requested section and reads episode one', () async {
|
||||
test('should_keep_requested_section_and_read_episode_one_when_constructed', () async {
|
||||
final history = FakeWatchHistory()..saved = const Duration(seconds: 7);
|
||||
final logic = MineLogic(
|
||||
history: history,
|
||||
@@ -47,7 +47,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('a list card without episodes resumes from the last watch', () async {
|
||||
test('should_resume_from_last_watch_when_list_card_has_no_episodes', () async {
|
||||
final history = FakeWatchHistory()
|
||||
..saved = const Duration(seconds: 7)
|
||||
..lastEpisode = 'signal-3';
|
||||
@@ -62,7 +62,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('section and filter selections notify', () {
|
||||
test('should_notify_when_section_and_filter_are_selected', () {
|
||||
final logic = MineLogic(
|
||||
history: FakeWatchHistory(),
|
||||
drama: _drama('s'),
|
||||
@@ -81,7 +81,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('syncPage only adopts a drama while the page is active', () {
|
||||
test('should_adopt_drama_only_when_page_is_active', () {
|
||||
final history = FakeWatchHistory();
|
||||
final logic = MineLogic(
|
||||
history: history,
|
||||
@@ -125,7 +125,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('a section request resets the section even while the page is inactive', () {
|
||||
test('should_reset_section_when_section_request_arrives_while_page_is_inactive', () {
|
||||
final logic = MineLogic(
|
||||
history: FakeWatchHistory(),
|
||||
drama: _drama('s'),
|
||||
@@ -146,7 +146,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
testWidgets('act returns the choice for a page and forwards it when embedded', (
|
||||
testWidgets('should_return_choice_and_forward_it_when_act_runs_embedded', (
|
||||
tester,
|
||||
) async {
|
||||
final logic = MineLogic(
|
||||
@@ -197,7 +197,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
testWidgets('the language sheet switches the app locale', (tester) async {
|
||||
testWidgets('should_switch_app_locale_when_language_sheet_is_used', (tester) async {
|
||||
SharedPreferencesAsyncPlatform.instance =
|
||||
InMemorySharedPreferencesAsync.empty();
|
||||
final locale = LocaleController();
|
||||
@@ -239,7 +239,7 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('notify after dispose is a no-op', () {
|
||||
test('should_do_nothing_when_notifying_after_dispose', () {
|
||||
final logic = MineLogic(
|
||||
history: FakeWatchHistory(),
|
||||
drama: _drama('s'),
|
||||
|
||||
@@ -17,7 +17,7 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
test('warm playback starts before old native disposal finishes', () async {
|
||||
test('should_start_warm_playback_when_old_native_disposal_is_still_running', () async {
|
||||
final release = Completer<void>();
|
||||
final factory = _Factory(disposeGates: {'ep-0': release});
|
||||
final session = PlaybackSession(
|
||||
@@ -41,7 +41,7 @@ void main() {
|
||||
await session.close();
|
||||
});
|
||||
|
||||
test('failed native release blocks another route from allocating', () async {
|
||||
test('should_block_another_route_from_allocating_when_native_release_fails', () async {
|
||||
final factory = _Factory(disposeFailures: {'ep-0'});
|
||||
final coordinator = PlaybackCoordinator();
|
||||
PlaybackSession createSession() => PlaybackSession(
|
||||
@@ -68,7 +68,7 @@ void main() {
|
||||
});
|
||||
|
||||
test(
|
||||
'next is prepared silently and promoted without another initialize',
|
||||
'should_prepare_next_silently_and_promote_it_when_swiped',
|
||||
() async {
|
||||
final factory = _Factory();
|
||||
final prepared = <String>[];
|
||||
@@ -103,7 +103,7 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test('in-flight preload is reused when swiped before ready', () async {
|
||||
test('should_reuse_in_flight_preload_when_swiped_before_ready', () async {
|
||||
final gate = Completer<void>();
|
||||
final factory = _Factory(gates: {'ep-1': gate});
|
||||
final session = PlaybackSession(
|
||||
@@ -127,7 +127,7 @@ void main() {
|
||||
await session.close();
|
||||
});
|
||||
|
||||
test('rapid reverse and skipped selection discard stale warm work', () async {
|
||||
test('should_discard_stale_warm_work_when_reversing_rapidly_or_skipping_selection', () async {
|
||||
final gate = Completer<void>();
|
||||
final factory = _Factory(gates: {'ep-1': gate});
|
||||
final session = PlaybackSession(
|
||||
@@ -153,7 +153,7 @@ void main() {
|
||||
});
|
||||
|
||||
test(
|
||||
'category adjacency and locked next never preload the wrong episode',
|
||||
'should_never_preload_wrong_episode_when_next_is_locked_or_category_based',
|
||||
() async {
|
||||
final factory = _Factory();
|
||||
final session = PlaybackSession(
|
||||
@@ -172,7 +172,7 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test('preload failure is isolated and selecting it retries once', () async {
|
||||
test('should_isolate_preload_failure_and_retry_once_when_selected', () async {
|
||||
final factory = _Factory(failOnce: {'ep-1'});
|
||||
final session = PlaybackSession(
|
||||
episodes: episodes,
|
||||
@@ -190,7 +190,7 @@ void main() {
|
||||
await session.close();
|
||||
});
|
||||
|
||||
test('completion in background never starts either player', () async {
|
||||
test('should_start_no_player_when_completion_happens_in_background', () async {
|
||||
final gate = Completer<void>();
|
||||
final factory = _Factory(gates: {'ep-1': gate});
|
||||
final session = PlaybackSession(
|
||||
@@ -215,7 +215,7 @@ void main() {
|
||||
await session.close();
|
||||
});
|
||||
|
||||
test('closing during preloading disposes pending resources', () async {
|
||||
test('should_dispose_pending_resources_when_closing_during_preload', () async {
|
||||
final gate = Completer<void>();
|
||||
final factory = _Factory(gates: {'ep-1': gate});
|
||||
final session = PlaybackSession(
|
||||
@@ -233,7 +233,7 @@ void main() {
|
||||
});
|
||||
|
||||
test(
|
||||
'home and series share a two-player budget across route handoff',
|
||||
'should_share_two_player_budget_when_handing_off_between_home_and_series',
|
||||
() async {
|
||||
final factory = _Factory();
|
||||
final coordinator = PlaybackCoordinator();
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:nontonki/core/data/models/drama.dart';
|
||||
import 'package:nontonki/core/data/services/playback_queue.dart';
|
||||
|
||||
void main() {
|
||||
test('home feed advances between dramas, series player between episodes', () {
|
||||
test('should_advance_between_dramas_on_home_and_between_episodes_in_series_when_queue_is_built', () {
|
||||
final dramas = [_drama('a', 3), _drama('b', 2), _drama('c', 4)];
|
||||
|
||||
final home = PlaybackQueue.home(dramas);
|
||||
@@ -22,7 +22,7 @@ void main() {
|
||||
expect(series.nextIndex(2), isNull);
|
||||
});
|
||||
|
||||
test('dramas without episodes are left out of the home queue', () {
|
||||
test('should_leave_dramas_out_when_they_have_no_episodes', () {
|
||||
final home = PlaybackQueue.home([
|
||||
_drama('a', 2),
|
||||
Drama(
|
||||
@@ -36,7 +36,7 @@ void main() {
|
||||
expect(home.entryAt(0).drama.id, 'a');
|
||||
});
|
||||
|
||||
test('a list card with a preview episode plays on the home feed', () {
|
||||
test('should_play_preview_episode_when_list_card_has_one', () {
|
||||
final home = PlaybackQueue.home([
|
||||
Drama.fromCardJson(const {
|
||||
'id': 5,
|
||||
|
||||
@@ -17,7 +17,7 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
test('locked episode never creates or initializes a player', () async {
|
||||
test('should_create_no_player_when_episode_is_locked', () async {
|
||||
final factory = FakeFactory();
|
||||
final session = PlaybackSession(
|
||||
episodes: episodes,
|
||||
@@ -30,7 +30,7 @@ void main() {
|
||||
await session.close();
|
||||
});
|
||||
|
||||
test('rapid selection drops stale initialize and keeps one player', () async {
|
||||
test('should_drop_stale_initialize_and_keep_one_player_when_selection_is_rapid', () async {
|
||||
final factory = FakeFactory(holdFirstInitialize: true);
|
||||
final session = PlaybackSession(
|
||||
episodes: episodes,
|
||||
@@ -52,7 +52,7 @@ void main() {
|
||||
});
|
||||
|
||||
test(
|
||||
'background and overlay pause without forgetting playback intent',
|
||||
'should_pause_without_forgetting_intent_when_backgrounding_or_overlay_shows',
|
||||
() async {
|
||||
final factory = FakeFactory();
|
||||
final session = PlaybackSession(
|
||||
@@ -80,7 +80,7 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test('resume preparation finishes before video starts', () async {
|
||||
test('should_finish_resume_preparation_before_video_starts_when_resuming', () async {
|
||||
final factory = FakeFactory();
|
||||
final prepared = Completer<void>();
|
||||
final session = PlaybackSession(
|
||||
@@ -99,7 +99,7 @@ void main() {
|
||||
await session.close();
|
||||
});
|
||||
|
||||
test('pausing then swiping to the next episode resumes playback', () async {
|
||||
test('should_resume_playback_when_swiping_after_pause', () async {
|
||||
final factory = FakeFactory();
|
||||
final session = PlaybackSession(
|
||||
episodes: episodes,
|
||||
@@ -121,7 +121,7 @@ void main() {
|
||||
await session.close();
|
||||
});
|
||||
|
||||
test('re-selecting the same episode keeps the user pause', () async {
|
||||
test('should_keep_user_pause_when_reselecting_the_same_episode', () async {
|
||||
final factory = FakeFactory();
|
||||
final session = PlaybackSession(
|
||||
episodes: episodes,
|
||||
@@ -140,7 +140,7 @@ void main() {
|
||||
await session.close();
|
||||
});
|
||||
|
||||
test('automatic advance respects visibility and playback intent', () async {
|
||||
test('should_respect_visibility_and_intent_when_advancing_automatically', () async {
|
||||
final factory = FakeFactory();
|
||||
final session = PlaybackSession(
|
||||
episodes: episodes,
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:nontonki/pages/player/widgets/seek_progress_bar.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
void main() {
|
||||
test('seek target stays inside playable duration', () {
|
||||
test('should_keep_seek_target_inside_playable_duration_when_dragging', () {
|
||||
const duration = Duration(seconds: 12);
|
||||
expect(seekPositionForFraction(0, duration), Duration.zero);
|
||||
expect(
|
||||
@@ -14,7 +14,7 @@ void main() {
|
||||
expect(seekPositionForFraction(1, duration), lessThan(duration));
|
||||
});
|
||||
|
||||
testWidgets('horizontal slider drag previews and seeks on release', (
|
||||
testWidgets('should_preview_while_dragging_and_seek_on_release_when_slider_is_horizontal', (
|
||||
tester,
|
||||
) async {
|
||||
final video = ValueNotifier<VideoPlayerValue>(
|
||||
@@ -61,7 +61,7 @@ void main() {
|
||||
expect(target!, lessThan(const Duration(seconds: 12)));
|
||||
});
|
||||
|
||||
testWidgets('compact seek bar shows time only while dragging', (
|
||||
testWidgets('should_show_time_only_while_dragging_when_seek_bar_is_compact', (
|
||||
tester,
|
||||
) async {
|
||||
final video = ValueNotifier<VideoPlayerValue>(
|
||||
|
||||
@@ -12,7 +12,7 @@ Episode _episode(String videoUrl) => Episode(
|
||||
);
|
||||
|
||||
void main() {
|
||||
test('相对播放地址补上服务地址', () {
|
||||
test('should_prefix_service_base_when_play_url_is_relative', () {
|
||||
final uri = VideoPlayerPort.resolveUri(
|
||||
_episode('/files/episode/202610/clip.mp4'),
|
||||
);
|
||||
@@ -27,7 +27,7 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('已经是绝对地址的不再拼接', () {
|
||||
test('should_keep_url_untouched_when_play_url_is_absolute', () {
|
||||
final uri = VideoPlayerPort.resolveUri(
|
||||
_episode('https://cdn.example.test/clip.mp4'),
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:shared_preferences_platform_interface/in_memory_shared_preferenc
|
||||
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
|
||||
|
||||
void main() {
|
||||
test('resume position clamps invalid and completed values', () {
|
||||
test('should_clamp_invalid_and_completed_values_when_resolving_resume_position', () {
|
||||
const duration = Duration(seconds: 8);
|
||||
expect(
|
||||
clampResumePosition(const Duration(seconds: -1), duration),
|
||||
@@ -25,7 +25,7 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('watch history is scoped by profile, drama and episode', () async {
|
||||
test('should_scope_history_by_profile_drama_and_episode_when_saving', () async {
|
||||
SharedPreferencesAsyncPlatform.instance =
|
||||
InMemorySharedPreferencesAsync.empty();
|
||||
final history = SharedPreferencesWatchHistoryRepository();
|
||||
@@ -53,7 +53,7 @@ void main() {
|
||||
expect(await history.lastEpisodeId('profile-a', 'drama-b'), isNull);
|
||||
});
|
||||
|
||||
test('resume prefers the first loaded episode', () async {
|
||||
test('should_prefer_first_loaded_episode_when_resuming', () async {
|
||||
SharedPreferencesAsyncPlatform.instance =
|
||||
InMemorySharedPreferencesAsync.empty();
|
||||
final history = SharedPreferencesWatchHistoryRepository();
|
||||
@@ -87,7 +87,7 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('resume falls back to the last watched episode for a list card', () async {
|
||||
test('should_fall_back_to_last_watched_episode_when_resuming_a_list_card', () async {
|
||||
SharedPreferencesAsyncPlatform.instance =
|
||||
InMemorySharedPreferencesAsync.empty();
|
||||
final history = SharedPreferencesWatchHistoryRepository();
|
||||
@@ -112,7 +112,7 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('a list card with no history has no resume position', () async {
|
||||
test('should_have_no_resume_position_when_list_card_has_no_history', () async {
|
||||
SharedPreferencesAsyncPlatform.instance =
|
||||
InMemorySharedPreferencesAsync.empty();
|
||||
final history = SharedPreferencesWatchHistoryRepository();
|
||||
|
||||
@@ -55,7 +55,7 @@ void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('displayEpisode', () {
|
||||
test('falls back until a number is picked for that drama', () {
|
||||
test('should_fall_back_until_episode_number_is_picked_for_that_drama_when_resolving', () {
|
||||
final logic = _logic(FakeWatchHistory());
|
||||
final drama = _drama('signal');
|
||||
final fallback = drama.episodes.first;
|
||||
@@ -70,7 +70,7 @@ void main() {
|
||||
expect(logic.displayEpisode(city, city.episodes.first).number, 1);
|
||||
});
|
||||
|
||||
test('an unknown picked number stops overriding the fallback', () {
|
||||
test('should_stop_overriding_fallback_when_picked_number_is_unknown', () {
|
||||
final logic = _logic(FakeWatchHistory());
|
||||
final drama = _drama('signal');
|
||||
final fallback = drama.episodes.first;
|
||||
@@ -82,7 +82,7 @@ void main() {
|
||||
});
|
||||
|
||||
group('nextVisibleIndex', () {
|
||||
test('walks the visible list and stops at the end', () {
|
||||
test('should_walk_visible_list_and_stop_at_the_end_when_advancing', () {
|
||||
final logic = _logic(FakeWatchHistory());
|
||||
logic.visibleIndices = [0, 2, 4];
|
||||
|
||||
@@ -91,7 +91,7 @@ void main() {
|
||||
expect(logic.nextVisibleIndex(4), isNull);
|
||||
});
|
||||
|
||||
test('an index outside the visible list has no successor', () {
|
||||
test('should_have_no_successor_when_index_is_outside_visible_list', () {
|
||||
final logic = _logic(FakeWatchHistory());
|
||||
logic.visibleIndices = [0, 2, 4];
|
||||
|
||||
@@ -100,7 +100,7 @@ void main() {
|
||||
expect(logic.nextVisibleIndex(-1), isNull);
|
||||
});
|
||||
|
||||
test('an empty visible list has no successor', () {
|
||||
test('should_have_no_successor_when_visible_list_is_empty', () {
|
||||
final logic = _logic(FakeWatchHistory());
|
||||
logic.visibleIndices = [];
|
||||
|
||||
@@ -109,7 +109,7 @@ void main() {
|
||||
});
|
||||
|
||||
group('surface flags', () {
|
||||
test('clearing the screen hides the chrome until it is restored', () {
|
||||
test('should_hide_chrome_until_restored_when_screen_is_cleared', () {
|
||||
final logic = _logic(FakeWatchHistory());
|
||||
final immersive = <bool>[];
|
||||
logic.onImmersiveChanged = immersive.add;
|
||||
@@ -135,7 +135,7 @@ void main() {
|
||||
expect(immersive, [true, false]);
|
||||
});
|
||||
|
||||
test('a picture in picture callback outranks the cleared screen flag', () async {
|
||||
test('should_outrank_cleared_screen_flag_when_picture_in_picture_callback_fires', () async {
|
||||
final factory = _RecordingFactory();
|
||||
final drama = _drama('signal');
|
||||
final logic = _logic(FakeWatchHistory());
|
||||
@@ -164,7 +164,7 @@ void main() {
|
||||
await _drain(logic);
|
||||
});
|
||||
|
||||
test('a picture in picture callback is ignored once unmounted', () {
|
||||
test('should_ignore_picture_in_picture_callback_when_unmounted', () {
|
||||
final logic = _logic(FakeWatchHistory());
|
||||
logic.isMounted = () => false;
|
||||
|
||||
@@ -175,7 +175,7 @@ void main() {
|
||||
});
|
||||
|
||||
group('history writes', () {
|
||||
test('progress writes are serialized in call order', () async {
|
||||
test('should_serialize_progress_writes_in_call_order_when_saving', () async {
|
||||
final history = FakeWatchHistory();
|
||||
final logic = _logic(history);
|
||||
final drama = _drama('signal');
|
||||
@@ -197,7 +197,7 @@ void main() {
|
||||
]);
|
||||
});
|
||||
|
||||
test('a failed write does not drop the writes after it', () async {
|
||||
test('should_keep_later_writes_when_an_earlier_write_fails', () async {
|
||||
final history = FakeWatchHistory();
|
||||
final logic = _logic(history);
|
||||
final drama = _drama('signal');
|
||||
@@ -219,7 +219,7 @@ void main() {
|
||||
expect(history.writes, ['demo-local-v1|signal|signal-2|2000']);
|
||||
});
|
||||
|
||||
test('saving with no tracked player writes nothing', () async {
|
||||
test('should_write_nothing_when_no_player_is_tracked', () async {
|
||||
final history = FakeWatchHistory();
|
||||
final logic = _logic(history);
|
||||
|
||||
@@ -232,7 +232,7 @@ void main() {
|
||||
});
|
||||
|
||||
group('start', () {
|
||||
test('a detail route plays a single series', () async {
|
||||
test('should_play_single_series_when_route_is_detail', () async {
|
||||
final factory = _RecordingFactory();
|
||||
final drama = _drama('signal');
|
||||
final logic = _logic(FakeWatchHistory());
|
||||
@@ -255,7 +255,7 @@ void main() {
|
||||
expect(factory.live, 0);
|
||||
});
|
||||
|
||||
test('home mode plays one episode per feed drama', () async {
|
||||
test('should_play_one_episode_per_feed_drama_when_in_home_mode', () async {
|
||||
final factory = _RecordingFactory();
|
||||
final feed = [_drama('signal'), _drama('city'), _drama('vengeance')];
|
||||
final logic = _logic(FakeWatchHistory());
|
||||
@@ -281,7 +281,7 @@ void main() {
|
||||
await _drain(logic);
|
||||
});
|
||||
|
||||
test('a locked episode never reaches the player factory', () async {
|
||||
test('should_never_reach_player_factory_when_episode_is_locked', () async {
|
||||
final factory = _RecordingFactory();
|
||||
final drama = _drama('locked', isFree: false);
|
||||
final logic = _logic(FakeWatchHistory());
|
||||
@@ -300,7 +300,7 @@ void main() {
|
||||
await _drain(logic);
|
||||
});
|
||||
|
||||
test('starting from a later index only preloads what follows it', () async {
|
||||
test('should_preload_only_what_follows_when_starting_from_a_later_index', () async {
|
||||
final factory = _RecordingFactory();
|
||||
final drama = _drama('signal');
|
||||
final logic = _logic(FakeWatchHistory());
|
||||
@@ -320,7 +320,7 @@ void main() {
|
||||
await _drain(logic);
|
||||
});
|
||||
|
||||
test('an inactive route holds no players until it becomes visible', () async {
|
||||
test('should_hold_no_players_when_route_is_inactive', () async {
|
||||
final factory = _RecordingFactory();
|
||||
final drama = _drama('signal');
|
||||
final logic = _logic(FakeWatchHistory());
|
||||
@@ -341,7 +341,7 @@ void main() {
|
||||
});
|
||||
|
||||
group('lifecycle', () {
|
||||
test('backgrounding pauses the active player and resuming plays it', () async {
|
||||
test('should_pause_active_player_when_backgrounded_and_play_it_when_resumed', () async {
|
||||
final factory = _RecordingFactory();
|
||||
final drama = _drama('signal');
|
||||
final logic = _logic(FakeWatchHistory());
|
||||
@@ -368,7 +368,7 @@ void main() {
|
||||
await _drain(logic);
|
||||
});
|
||||
|
||||
test('an inactive route stops playing without losing the intent', () async {
|
||||
test('should_stop_playing_without_losing_intent_when_route_becomes_inactive', () async {
|
||||
final factory = _RecordingFactory();
|
||||
final drama = _drama('signal');
|
||||
final logic = _logic(FakeWatchHistory());
|
||||
@@ -398,7 +398,7 @@ void main() {
|
||||
});
|
||||
|
||||
group('dispose', () {
|
||||
test('is safe to repeat and releases every player', () async {
|
||||
test('should_release_every_player_when_dispose_runs_twice', () async {
|
||||
final factory = _RecordingFactory();
|
||||
final drama = _drama('signal');
|
||||
final logic = _logic(FakeWatchHistory());
|
||||
@@ -417,7 +417,7 @@ void main() {
|
||||
expect(() => logic.dispose(), returnsNormally);
|
||||
});
|
||||
|
||||
test('disables further notifications', () async {
|
||||
test('should_disable_further_notifications_when_disposed', () async {
|
||||
final factory = _RecordingFactory();
|
||||
final drama = _drama('signal');
|
||||
final logic = _logic(FakeWatchHistory());
|
||||
|
||||
+14
-14
@@ -28,7 +28,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'home swipe promotes next without recreating it or showing loading',
|
||||
'should_promote_next_without_recreating_it_or_showing_loading_when_home_is_swiped',
|
||||
(tester) async {
|
||||
final created = <String>[];
|
||||
await tester.pumpWidget(
|
||||
@@ -57,7 +57,7 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('immersive home opens the series detail', (tester) async {
|
||||
testWidgets('should_open_series_detail_when_immersive_home_is_tapped', (tester) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
@@ -82,7 +82,7 @@ void main() {
|
||||
expect(find.byTooltip('Explore series'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('clear screen hides tabs and restores them on tap', (
|
||||
testWidgets('should_hide_tabs_and_restore_them_when_clear_screen_is_tapped', (
|
||||
tester,
|
||||
) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
@@ -107,7 +107,7 @@ void main() {
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('home quick drawer opens current series and switches root tabs', (
|
||||
testWidgets('should_open_current_series_and_switch_root_tabs_when_quick_drawer_is_used', (
|
||||
tester,
|
||||
) async {
|
||||
tester.view.physicalSize = const Size(360, 720);
|
||||
@@ -162,7 +162,7 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('interface language changes without a caption control', (
|
||||
testWidgets('should_change_interface_language_when_locale_is_picked', (
|
||||
tester,
|
||||
) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
@@ -193,7 +193,7 @@ void main() {
|
||||
expect(find.text('热门'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('home controls fit a compact Android viewport', (tester) async {
|
||||
testWidgets('should_fit_compact_android_viewport_when_home_controls_are_shown', (tester) async {
|
||||
tester.view.physicalSize = const Size(360, 720);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
@@ -217,7 +217,7 @@ void main() {
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('My page fits a compact Android viewport and returns home', (
|
||||
testWidgets('should_fit_compact_android_viewport_and_return_home_when_my_page_is_shown', (
|
||||
tester,
|
||||
) async {
|
||||
tester.view.physicalSize = const Size(360, 720);
|
||||
@@ -241,7 +241,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'Discover shows three mock playable dramas and one pending poster',
|
||||
'should_show_three_playable_dramas_and_one_pending_poster_when_discover_is_opened',
|
||||
(tester) async {
|
||||
tester.view.physicalSize = const Size(360, 720);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
@@ -276,7 +276,7 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('bottom tabs stay on one route', (tester) async {
|
||||
testWidgets('should_stay_on_one_route_when_bottom_tabs_are_used', (tester) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
@@ -327,7 +327,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'home swipe changes drama, entered player swipe changes episode',
|
||||
'should_change_drama_on_home_swipe_and_episode_on_player_swipe_when_swiping',
|
||||
(tester) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
@@ -359,7 +359,7 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('shorts page has no category navigation', (tester) async {
|
||||
testWidgets('should_hide_category_navigation_when_shorts_page_is_shown', (tester) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
@@ -381,7 +381,7 @@ void main() {
|
||||
expect(find.text('City Lights & Velvet Nights'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
testWidgets('right rail saves, comments, likes and invokes share in order', (
|
||||
testWidgets('should_save_comment_like_and_share_in_order_when_right_rail_is_used', (
|
||||
tester,
|
||||
) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
@@ -439,7 +439,7 @@ void main() {
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('system back opens the drawer and stays on home', (tester) async {
|
||||
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;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
@@ -466,7 +466,7 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('My watchlist lists saved dramas across the home feed', (
|
||||
testWidgets('should_list_saved_dramas_across_home_feed_when_watchlist_is_opened', (
|
||||
tester,
|
||||
) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
|
||||
Reference in new issue
Block a user