From 9818a56fe19b94a3e3bc8c9f3d0752fe4c5f31b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E9=94=8B?= <2535831261@qq.com> Date: Thu, 16 Jul 2026 11:58:43 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=A0=E9=99=A4=E5=8D=95=E4=BE=8B=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../permission/permission_service_test.dart | 131 ---- .../platform/device_health_checker_test.dart | 88 --- test/features/dialog/record_dialog_test.dart | 130 ---- .../recording/model_clipboard_test.dart | 77 --- .../recording_display_name_test.dart | 69 -- .../recording/recording_platform_test.dart | 45 -- .../recording/view_model_recording_test.dart | 595 ------------------ .../recording/widget_record_header_test.dart | 52 -- .../widget_recording_button_test.dart | 138 ---- .../recording/widget_recording_hud_test.dart | 231 ------- ...get_recording_touch_lock_overlay_test.dart | 126 ---- test/widget_test.dart | 91 --- 13 files changed, 1 insertion(+), 1773 deletions(-) delete mode 100644 test/core/permission/permission_service_test.dart delete mode 100644 test/core/platform/device_health_checker_test.dart delete mode 100644 test/features/dialog/record_dialog_test.dart delete mode 100644 test/features/recording/model_clipboard_test.dart delete mode 100644 test/features/recording/recording_display_name_test.dart delete mode 100644 test/features/recording/recording_platform_test.dart delete mode 100644 test/features/recording/view_model_recording_test.dart delete mode 100644 test/features/recording/widget_record_header_test.dart delete mode 100644 test/features/recording/widget_recording_button_test.dart delete mode 100644 test/features/recording/widget_recording_hud_test.dart delete mode 100644 test/features/recording/widget_recording_touch_lock_overlay_test.dart delete mode 100644 test/widget_test.dart diff --git a/.gitignore b/.gitignore index bd82493..766bacd 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ app.*.map.json /android/.kotlin CLAUDE.md +AGENTS.md \ No newline at end of file diff --git a/test/core/permission/permission_service_test.dart b/test/core/permission/permission_service_test.dart deleted file mode 100644 index 90197fa..0000000 --- a/test/core/permission/permission_service_test.dart +++ /dev/null @@ -1,131 +0,0 @@ -import 'dart:io'; - -import 'package:flutter_test/flutter_test.dart'; -// ignore: depend_on_referenced_packages -import 'package:permission_handler_platform_interface/permission_handler_platform_interface.dart'; - -import 'package:recording_tool/core/permission/permission_service.dart'; - -void main() { - group('PermissionService.requestMissing', () { - late PermissionHandlerPlatform originalPlatform; - - setUp(() { - originalPlatform = PermissionHandlerPlatform.instance; - }); - - tearDown(() { - PermissionHandlerPlatform.instance = originalPlatform; - }); - - test('requests only missing permissions and skips granted ones', () async { - final platform = FakePermissionHandlerPlatform( - statuses: { - Permission.camera: PermissionStatus.granted, - Permission.microphone: PermissionStatus.denied, - }, - requestResults: { - Permission.microphone: PermissionStatus.granted, - }, - ); - PermissionHandlerPlatform.instance = platform; - - final result = await PermissionService.requestMissing([ - Permission.camera, - Permission.microphone, - ]); - - expect(platform.requestCalls, >[ - [Permission.microphone], - ]); - expect(result[Permission.camera], PermissionStatus.granted); - expect(result[Permission.microphone], PermissionStatus.granted); - }); - - test( - 'preserves permanently denied permissions without requesting them', - () async { - final platform = FakePermissionHandlerPlatform( - statuses: { - Permission.camera: PermissionStatus.permanentlyDenied, - Permission.microphone: PermissionStatus.denied, - }, - requestResults: { - Permission.microphone: PermissionStatus.granted, - }, - ); - PermissionHandlerPlatform.instance = platform; - - final result = await PermissionService.requestMissing([ - Permission.camera, - Permission.microphone, - ]); - - expect(platform.requestCalls, >[ - [Permission.microphone], - ]); - expect(result[Permission.camera], PermissionStatus.permanentlyDenied); - expect(result[Permission.microphone], PermissionStatus.granted); - }, - ); - }); - - group('iOS permission configuration', () { - test('Podfile enables camera and microphone permission macros only', () { - final podfile = File('ios/Podfile').readAsStringSync(); - - expect( - podfile, - contains('flutter_additional_ios_build_settings(target)'), - ); - expect(podfile, contains("'PERMISSION_CAMERA=1'")); - expect(podfile, contains("'PERMISSION_MICROPHONE=1'")); - expect(podfile, isNot(contains("'PERMISSION_PHOTOS=1'"))); - expect(podfile, isNot(contains("'PERMISSION_PHOTOS_ADD_ONLY=1'"))); - }); - }); -} - -class FakePermissionHandlerPlatform extends PermissionHandlerPlatform { - FakePermissionHandlerPlatform({ - required this.statuses, - required this.requestResults, - }); - - final Map statuses; - final Map requestResults; - final List> requestCalls = >[]; - - @override - Future checkPermissionStatus(Permission permission) async { - return statuses[permission] ?? PermissionStatus.denied; - } - - @override - Future checkServiceStatus(Permission permission) async { - return ServiceStatus.enabled; - } - - @override - Future openAppSettings() async { - return true; - } - - @override - Future> requestPermissions( - List permissions, - ) async { - requestCalls.add(List.unmodifiable(permissions)); - return { - for (final permission in permissions) - permission: requestResults[permission] ?? PermissionStatus.granted, - }; - } - - @override - Future shouldShowRequestPermissionRationale( - Permission permission, - ) async { - return false; - } -} diff --git a/test/core/platform/device_health_checker_test.dart b/test/core/platform/device_health_checker_test.dart deleted file mode 100644 index ba4373b..0000000 --- a/test/core/platform/device_health_checker_test.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:recording_tool/core/platform/device_health_checker.dart'; -import 'package:recording_tool/core/platform/device_health_snapshot.dart'; - -void main() { - group('DeviceHealthChecker.warningLines', () { - test('returns empty when battery and storage are healthy', () { - const snapshot = DeviceHealthSnapshot( - batteryLevelPercent: 50, - storageAvailablePercent: 50, - ); - - expect(DeviceHealthChecker.warningLines(snapshot), isEmpty); - }); - - test('returns low battery message only', () { - const snapshot = DeviceHealthSnapshot( - batteryLevelPercent: 9, - storageAvailablePercent: 50, - ); - - expect( - DeviceHealthChecker.warningLines(snapshot), - [DeviceHealthChecker.lowBatteryMessage], - ); - }); - - test('returns low storage message only', () { - const snapshot = DeviceHealthSnapshot( - batteryLevelPercent: 50, - storageAvailablePercent: 9.9, - ); - - expect( - DeviceHealthChecker.warningLines(snapshot), - [DeviceHealthChecker.lowStorageMessage], - ); - }); - - test('returns both messages when battery and storage are low', () { - const snapshot = DeviceHealthSnapshot( - batteryLevelPercent: 5, - storageAvailablePercent: 5, - ); - - expect( - DeviceHealthChecker.warningLines(snapshot), - [ - DeviceHealthChecker.lowBatteryMessage, - DeviceHealthChecker.lowStorageMessage, - ], - ); - }); - - test('does not warn at exactly threshold percent', () { - const snapshot = DeviceHealthSnapshot( - batteryLevelPercent: 10, - storageAvailablePercent: 10, - ); - - expect(DeviceHealthChecker.warningLines(snapshot), isEmpty); - }); - - test('skips battery warning when level is unknown', () { - const snapshot = DeviceHealthSnapshot( - batteryLevelPercent: null, - storageAvailablePercent: 5, - ); - - expect( - DeviceHealthChecker.warningLines(snapshot), - [DeviceHealthChecker.lowStorageMessage], - ); - }); - }); - - group('DeviceHealthSnapshot.fromMap', () { - test('parses native map fields', () { - final snapshot = DeviceHealthSnapshot.fromMap({ - 'batteryLevelPercent': 42, - 'storageAvailablePercent': 12.5, - }); - - expect(snapshot.batteryLevelPercent, 42); - expect(snapshot.storageAvailablePercent, 12.5); - }); - }); -} diff --git a/test/features/dialog/record_dialog_test.dart b/test/features/dialog/record_dialog_test.dart deleted file mode 100644 index 4283abe..0000000 --- a/test/features/dialog/record_dialog_test.dart +++ /dev/null @@ -1,130 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:recording_tool/features/dialog/dialog-record.dart'; -import 'package:recording_tool/features/recording/widgets/widget_recording_saved_dialog.dart'; -import 'package:recording_tool/gen/assets.gen.dart'; - -void main() { - Future pumpDialogHost(WidgetTester tester, Widget child) async { - await tester.pumpWidget( - ScreenUtilInit( - designSize: const Size(375, 812), - builder: (context, _) { - return MaterialApp(home: Scaffold(body: child)); - }, - ), - ); - } - - testWidgets('single button dialog shows configured content and closes', ( - tester, - ) async { - var tapped = false; - - await pumpDialogHost( - tester, - Builder( - builder: (context) { - return TextButton( - onPressed: () { - RecordDialog.showSingle( - context, - title: '无选手信息!', - buttonText: '粘贴', - onPressed: () => tapped = true, - ); - }, - child: const Text('show'), - ); - }, - ), - ); - - await tester.tap(find.text('show')); - await tester.pumpAndSettle(); - - expect(find.byType(Image), findsOneWidget); - expect(find.image(AssetImage(Assets.images.imageDialogBg.path)), findsOne); - expect(find.text('无选手信息!'), findsOneWidget); - expect(find.text('粘贴'), findsOneWidget); - - await tester.tap(find.text('粘贴')); - await tester.pumpAndSettle(); - - expect(tapped, isTrue); - expect(find.text('无选手信息!'), findsNothing); - }); - - testWidgets('double button dialog dispatches each action', (tester) async { - var leftTapped = false; - var rightTapped = false; - - await pumpDialogHost( - tester, - Builder( - builder: (context) { - return TextButton( - onPressed: () { - RecordDialog.showDouble( - context, - title: '本轮比赛视频已保存到文件夹\n请选择后续录制信息', - leftText: '继续本轮', - rightText: '录制新轮', - onLeftPressed: () => leftTapped = true, - onRightPressed: () => rightTapped = true, - ); - }, - child: const Text('show'), - ); - }, - ), - ); - - await tester.tap(find.text('show')); - await tester.pumpAndSettle(); - await tester.tap(find.text('继续本轮')); - await tester.pumpAndSettle(); - - expect(leftTapped, isTrue); - expect(rightTapped, isFalse); - - await tester.tap(find.text('show')); - await tester.pumpAndSettle(); - await tester.tap(find.text('录制新轮')); - await tester.pumpAndSettle(); - - expect(rightTapped, isTrue); - }); - - testWidgets('recording saved dialog follows design title only', ( - tester, - ) async { - await pumpDialogHost( - tester, - Builder( - builder: (context) { - return TextButton( - onPressed: () { - showRecordingSavedDialog( - context, - sessionTitle: '王东方 丨李想 空中格斗赛', - onContinueRound: () {}, - onRecordNewRound: () {}, - ); - }, - child: const Text('show'), - ); - }, - ), - ); - - await tester.tap(find.text('show')); - await tester.pumpAndSettle(); - - expect(find.text('王东方 丨李想 空中格斗赛'), findsNothing); - expect(find.text('本轮比赛视频已保存到文件夹\n请选择后续录制信息'), findsOneWidget); - expect(find.text('继续本轮'), findsOneWidget); - expect(find.text('录制新轮'), findsOneWidget); - }); -} diff --git a/test/features/recording/model_clipboard_test.dart b/test/features/recording/model_clipboard_test.dart deleted file mode 100644 index 5d3e674..0000000 --- a/test/features/recording/model_clipboard_test.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:recording_tool/features/recording/model/model_clipboard.dart'; - -void main() { - group('ClipboardRecordingModel', () { - const clipboardJson = { - 'title': '王东方 丨李想 空中格斗赛', - 'startTimestamp': 1717334400, - 'endTimestamp': 1717334400, - 'address': '广州市番禺区·粤港澳大湾区青年人才双创小镇', - }; - - test('parses mini program clipboard JSON', () { - final model = ClipboardRecordingModel.fromJson(clipboardJson); - - expect(model.title, '王东方 丨李想 空中格斗赛'); - expect(model.startTimestamp, 1717334400); - expect(model.endTimestamp, 1717334400); - expect(model.address, '广州市番禺区·粤港澳大湾区青年人才双创小镇'); - expect(model.filename, isNull); - expect(model.toJson(), clipboardJson); - }); - - test('parses JSON without optional timestamps', () { - final json = { - 'title': '郑昌梦 丨黄伟依 空中格斗赛 小学组', - 'address': '广东省汕头市番禺区青蓝街 111 号', - 'filename': '郑昌梦_黄伟依_6月3日测试-1_空中格斗赛', - }; - final model = ClipboardRecordingModel.fromJson(json); - - expect(model.title, json['title']); - expect(model.address, json['address']); - expect(model.filename, json['filename']); - expect(model.startTimestamp, isNull); - expect(model.endTimestamp, isNull); - }); - - test('parses optional filename from mini program JSON', () { - final json = { - ...clipboardJson, - 'filename': '选手名称_选手ID_赛事名称_赛项', - }; - final model = ClipboardRecordingModel.fromJson(json); - - expect(model.filename, '选手名称_选手ID_赛事名称_赛项'); - expect(model.toJson(), json); - }); - - test('throws FormatException when required field is missing', () { - final json = Map.from(clipboardJson)..remove('title'); - - expect( - () => ClipboardRecordingModel.fromJson(json), - throwsA(isA()), - ); - }); - - test('throws FormatException when optional int field has wrong type', () { - final json = {...clipboardJson, 'startTimestamp': '1717334400'}; - - expect( - () => ClipboardRecordingModel.fromJson(json), - throwsA(isA()), - ); - }); - - test('throws FormatException when required address is missing', () { - final json = Map.from(clipboardJson)..remove('address'); - - expect( - () => ClipboardRecordingModel.fromJson(json), - throwsA(isA()), - ); - }); - }); -} diff --git a/test/features/recording/recording_display_name_test.dart b/test/features/recording/recording_display_name_test.dart deleted file mode 100644 index b4b6665..0000000 --- a/test/features/recording/recording_display_name_test.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:recording_tool/features/recording/utils/recording_display_name.dart'; - -void main() { - group('sanitizeRecordingBaseName', () { - test('removes invalid path characters', () { - expect( - sanitizeRecordingBaseName(r'a/b:c*d?e"fh|i'), - 'a_b_c_d_e_f_g_h_i', - ); - }); - - test('returns null for blank input', () { - expect(sanitizeRecordingBaseName(' '), isNull); - }); - - test('truncates overly long names', () { - final long = 'a' * 200; - expect(sanitizeRecordingBaseName(long)!.length, 120); - }); - }); - - group('resolveRecordingDisplayName', () { - test('uses sanitized clipboard filename when present', () { - expect( - resolveRecordingDisplayName('选手名称_选手ID_赛事名称_赛项'), - '选手名称_选手ID_赛事名称_赛项', - ); - }); - - test('falls back to REC_ prefix when clipboard filename is empty', () { - expect(resolveRecordingDisplayName(null), startsWith('REC_')); - expect(resolveRecordingDisplayName(''), startsWith('REC_')); - }); - }); - - group('withVideoExtension', () { - test('appends mp4 on Android', () { - expect( - withVideoExtension('match', isIOS: false), - 'match.mp4', - ); - }); - - test('appends mov on iOS', () { - expect( - withVideoExtension('match', isIOS: true), - 'match.mov', - ); - }); - - test('keeps existing extension', () { - expect(withVideoExtension('a.mp4', isIOS: false), 'a.mp4'); - expect(withVideoExtension('a.MOV', isIOS: true), 'a.MOV'); - }); - }); - - group('recordingFileNameForPlatform', () { - test('combines clipboard name with platform extension', () { - expect( - recordingFileNameForPlatform( - '选手名称_选手ID_赛事名称_赛项', - isIOS: false, - ), - '选手名称_选手ID_赛事名称_赛项.mp4', - ); - }); - }); -} diff --git a/test/features/recording/recording_platform_test.dart b/test/features/recording/recording_platform_test.dart deleted file mode 100644 index 9774d92..0000000 --- a/test/features/recording/recording_platform_test.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:recording_tool/features/recording/platform/recording_channel_names.dart'; -import 'package:recording_tool/features/recording/platform/recording_platform.dart'; - -void main() { - group('RecordingChannelNames', () { - test('uses stable bridge names without package binding', () { - expect(RecordingChannelNames.method, 'app.record_tool/recording'); - expect(RecordingChannelNames.events, 'app.record_tool/recording_events'); - }); - }); - - group('RecordingPlatform support', () { - test('supports Android and iOS hosts only', () { - expect( - RecordingPlatform.supportsHost(isAndroid: true, isIOS: false), - isTrue, - ); - expect( - RecordingPlatform.supportsHost(isAndroid: false, isIOS: true), - isTrue, - ); - expect( - RecordingPlatform.supportsHost(isAndroid: false, isIOS: false), - isFalse, - ); - }); - }); - - group('RecordingStopResult', () { - test('parses file save result fields from platform payload', () { - final result = RecordingStopResult.fromMap({ - 'outputPath': '/Documents/recordings/test.mov', - 'status': {'state': 'previewing'}, - 'fileSaved': false, - 'fileErrorMessage': '保存到文件夹失败', - }); - - expect(result.outputPath, '/Documents/recordings/test.mov'); - expect(result.status.state, RecordingState.previewing); - expect(result.fileSaved, isFalse); - expect(result.fileErrorMessage, '保存到文件夹失败'); - }); - }); -} diff --git a/test/features/recording/view_model_recording_test.dart b/test/features/recording/view_model_recording_test.dart deleted file mode 100644 index a2d28f7..0000000 --- a/test/features/recording/view_model_recording_test.dart +++ /dev/null @@ -1,595 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:recording_tool/features/recording/model/model_recording_session.dart'; -import 'package:recording_tool/features/recording/platform/recording_channel_names.dart'; -import 'package:recording_tool/features/recording/platform/recording_platform.dart'; -import 'package:recording_tool/features/recording/view-model/view_model_recording.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - const defaultClipboardTitle = ''; - const validClipboardText = - '{"title":"王东方 丨李想 空中格斗赛","startTimestamp":1717334400,"endTimestamp":1717334400,"filename":"选手名称_选手ID_赛事名称_赛项","address":"广州市番禺区·粤港澳大湾区青年人才双创小镇"}'; - - Future setClipboardText(String? text) async { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(SystemChannels.platform, (call) async { - if (call.method == 'Clipboard.getData') { - return text == null ? null : {'text': text}; - } - return null; - }); - } - - tearDown(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(SystemChannels.platform, null); - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - const MethodChannel(RecordingChannelNames.method), - null, - ); - }); - - group('RecordingViewModel', () { - test('initializes with default clipboard and session state', () { - final container = ProviderContainer(); - addTearDown(container.dispose); - - final model = container.read(recordingViewModelProvider); - expect(model.hasValidClipboardInfo, isFalse); - expect(model.clipboardRecordingModel.title, defaultClipboardTitle); - expect(model.session.isPreviewReady, isFalse); - expect(model.session.isRecording, isFalse); - expect(model.session.zoomRatio, 1.0); - expect(model.session.minZoomRatio, 1.0); - expect(model.session.maxZoomRatio, 3.0); - }); - }); - - group('RecordingViewModel.setZoomRatio', () { - test('updates zoom ratio from native response', () async { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - const MethodChannel(RecordingChannelNames.method), - (call) async { - expect(call.method, 'setZoomRatio'); - expect(call.arguments, {'zoomRatio': 2.0}); - return { - 'zoomRatio': 2.0, - 'minZoomRatio': 1.0, - 'maxZoomRatio': 3.0, - }; - }, - ); - final container = ProviderContainer(); - addTearDown(container.dispose); - - await container.read(recordingViewModelProvider.notifier).setZoomRatio(2); - - final session = container.read(recordingViewModelProvider).session; - expect(session.zoomRatio, 2.0); - expect(session.minZoomRatio, 1.0); - expect(session.maxZoomRatio, 3.0); - expect(session.errorMessage, isNull); - }); - - test('passes 0.5x to native when camera capabilities allow it', () async { - final calls = []; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - const MethodChannel(RecordingChannelNames.method), - (call) async { - calls.add(call); - return { - 'zoomRatio': 0.5, - 'minZoomRatio': 0.5, - 'maxZoomRatio': 3.0, - }; - }, - ); - final container = ProviderContainer(); - addTearDown(container.dispose); - final notifier = container.read(recordingViewModelProvider.notifier); - // ignore: invalid_use_of_protected_member - notifier.state = container - .read(recordingViewModelProvider) - .copyWith( - session: const RecordingSessionState( - zoomRatio: 1.0, - minZoomRatio: 0.5, - maxZoomRatio: 3.0, - ), - ); - - await notifier.setZoomRatio(0.5); - - expect(calls.single.arguments, {'zoomRatio': 0.5}); - final session = container.read(recordingViewModelProvider).session; - expect(session.zoomRatio, 0.5); - expect(session.minZoomRatio, 0.5); - expect(session.maxZoomRatio, 3.0); - }); - - test('passes 0.6x to native when camera capabilities allow it', () async { - final calls = []; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - const MethodChannel(RecordingChannelNames.method), - (call) async { - calls.add(call); - return { - 'zoomRatio': 0.6, - 'minZoomRatio': 0.6, - 'maxZoomRatio': 3.0, - }; - }, - ); - final container = ProviderContainer(); - addTearDown(container.dispose); - final notifier = container.read(recordingViewModelProvider.notifier); - // ignore: invalid_use_of_protected_member - notifier.state = container - .read(recordingViewModelProvider) - .copyWith( - session: const RecordingSessionState( - zoomRatio: 1.0, - minZoomRatio: 0.6, - maxZoomRatio: 3.0, - ), - ); - - await notifier.setZoomRatio(0.6); - - expect(calls.single.arguments, {'zoomRatio': 0.6}); - final session = container.read(recordingViewModelProvider).session; - expect(session.zoomRatio, 0.6); - expect(session.minZoomRatio, 0.6); - expect(session.maxZoomRatio, 3.0); - }); - - test('clamps requested zoom ratio before invoking native', () async { - final calls = []; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - const MethodChannel(RecordingChannelNames.method), - (call) async { - calls.add(call); - return { - 'zoomRatio': 1.0, - 'minZoomRatio': 1.0, - 'maxZoomRatio': 1.0, - }; - }, - ); - final container = ProviderContainer(); - addTearDown(container.dispose); - - await container.read(recordingViewModelProvider.notifier).setZoomRatio(4); - - expect(calls.single.arguments, {'zoomRatio': 3.0}); - expect(container.read(recordingViewModelProvider).session.zoomRatio, 1.0); - }); - - test( - 'clamps 0.6x to 1x when camera capabilities do not allow it', - () async { - final calls = []; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - const MethodChannel(RecordingChannelNames.method), - (call) async { - calls.add(call); - return { - 'zoomRatio': 1.0, - 'minZoomRatio': 1.0, - 'maxZoomRatio': 3.0, - }; - }, - ); - final container = ProviderContainer(); - addTearDown(container.dispose); - - await container - .read(recordingViewModelProvider.notifier) - .setZoomRatio(0.6); - - expect(calls.single.arguments, {'zoomRatio': 1.0}); - expect( - container.read(recordingViewModelProvider).session.zoomRatio, - 1.0, - ); - }, - ); - - test( - 'keeps previous zoom ratio and stores error when native fails', - () async { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - const MethodChannel(RecordingChannelNames.method), - (call) async { - throw PlatformException( - code: 'ZOOM_FAILED', - message: 'Zoom is unavailable', - ); - }, - ); - final container = ProviderContainer(); - addTearDown(container.dispose); - - await container - .read(recordingViewModelProvider.notifier) - .setZoomRatio(2); - - final session = container.read(recordingViewModelProvider).session; - expect(session.zoomRatio, 1.0); - expect(session.errorMessage, '切换镜头失败,请重试'); - }, - ); - - test( - 'maps native zoom failure to user friendly lens switch message', - () async { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - const MethodChannel(RecordingChannelNames.method), - (call) async { - throw PlatformException( - code: 'ZOOM_FAILED', - message: 'Cannot switch physical camera while recording', - ); - }, - ); - final container = ProviderContainer(); - addTearDown(container.dispose); - - await container - .read(recordingViewModelProvider.notifier) - .setZoomRatio(0.6); - - final session = container.read(recordingViewModelProvider).session; - expect(session.zoomRatio, 1.0); - expect(session.errorMessage, '切换镜头失败,请重试'); - }, - ); - - test('sets switching lens while native zoom request is pending', () async { - final completer = Completer>(); - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - const MethodChannel(RecordingChannelNames.method), - (call) => completer.future, - ); - final container = ProviderContainer(); - addTearDown(container.dispose); - final notifier = container.read(recordingViewModelProvider.notifier); - - final future = notifier.setZoomRatio(2); - await Future.delayed(Duration.zero); - - expect( - container.read(recordingViewModelProvider).session.isSwitchingLens, - isTrue, - ); - - completer.complete({ - 'zoomRatio': 2.0, - 'minZoomRatio': 1.0, - 'maxZoomRatio': 3.0, - }); - await future; - - expect( - container.read(recordingViewModelProvider).session.isSwitchingLens, - isFalse, - ); - }); - }); - - group('RecordingViewModel.stopRecording', () { - test('does not call native stop while switching lens', () async { - final calls = []; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - const MethodChannel(RecordingChannelNames.method), - (call) async { - calls.add(call); - return {}; - }, - ); - final container = ProviderContainer(); - addTearDown(container.dispose); - final notifier = container.read(recordingViewModelProvider.notifier); - // ignore: invalid_use_of_protected_member - notifier.state = container - .read(recordingViewModelProvider) - .copyWith( - session: const RecordingSessionState( - status: RecordingStatus(state: RecordingState.recording), - isSwitchingLens: true, - ), - ); - - await notifier.stopRecording(); - - expect(calls, isEmpty); - }); - - test( - 'stores segment output paths when native save falls back to parts', - () async { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - const MethodChannel(RecordingChannelNames.method), - (call) async { - switch (call.method) { - case 'stopRecording': - return { - 'outputPath': 'content://recordings/part1', - 'status': { - 'state': 'error', - 'message': 'Merge failed', - }, - 'fileSaved': false, - 'fileErrorMessage': 'Merge failed', - 'segmentOutputPaths': [ - 'content://recordings/part1', - 'content://recordings/part2', - ], - }; - case 'initializePreview': - return {'state': 'previewing'}; - case 'getZoomCapabilities': - return { - 'zoomRatio': 1.0, - 'minZoomRatio': 1.0, - 'maxZoomRatio': 3.0, - }; - } - return {}; - }, - ); - final container = ProviderContainer(); - addTearDown(container.dispose); - final notifier = container.read(recordingViewModelProvider.notifier); - // ignore: invalid_use_of_protected_member - notifier.state = container - .read(recordingViewModelProvider) - .copyWith( - session: const RecordingSessionState( - status: RecordingStatus(state: RecordingState.recording), - ), - ); - - await notifier.stopRecording(); - - final session = container.read(recordingViewModelProvider).session; - expect(session.fileSaveFailed, isTrue); - expect(session.segmentOutputPaths, [ - 'content://recordings/part1', - 'content://recordings/part2', - ]); - }, - ); - }); - - group('recordingFileSavePermissionsForHost', () { - test('does not request photo permission on iOS', () { - final permissions = recordingFileSavePermissionsForHost( - isIOS: true, - isAndroid: false, - ); - - expect(permissions, isEmpty); - expect(permissions, isNot(contains(Permission.photosAddOnly))); - expect(permissions, isNot(contains(Permission.photos))); - }); - - test('requests storage permission on Android 9 and below', () { - final permissions = recordingFileSavePermissionsForHost( - isIOS: false, - isAndroid: true, - androidSdkInt: 28, - ); - - expect(permissions, [Permission.storage]); - expect(permissions, isNot(contains(Permission.videos))); - }); - - test('does not request file save permission on Android 10 and above', () { - final permissions = recordingFileSavePermissionsForHost( - isIOS: false, - isAndroid: true, - androidSdkInt: 29, - ); - - expect(permissions, isEmpty); - }); - - test('does not request file save permission on Android 13 and above', () { - final permissions = recordingFileSavePermissionsForHost( - isIOS: false, - isAndroid: true, - androidSdkInt: 33, - ); - - expect(permissions, isEmpty); - }); - - test('does not request file save permission when Android SDK is unknown', () { - final permissions = recordingFileSavePermissionsForHost( - isIOS: false, - isAndroid: true, - ); - - expect(permissions, isEmpty); - }); - }); - - group('RecordingViewModel.getClipboardContent', () { - test( - 'updates state when clipboard contains valid mini program JSON', - () async { - await setClipboardText(validClipboardText); - final container = ProviderContainer(); - addTearDown(container.dispose); - - final result = await container - .read(recordingViewModelProvider.notifier) - .getClipboardContent(); - - expect(result, ClipboardReadResult.success); - final model = container.read(recordingViewModelProvider); - expect(model.hasValidClipboardInfo, isTrue); - expect(model.clipboardRecordingModel.title, '王东方 丨李想 空中格斗赛'); - expect(model.clipboardRecordingModel.startTimestamp, 1717334400); - expect(model.clipboardRecordingModel.endTimestamp, 1717334400); - expect(model.clipboardRecordingModel.address, '广州市番禺区·粤港澳大湾区青年人才双创小镇'); - expect(model.clipboardRecordingModel.filename, '选手名称_选手ID_赛事名称_赛项'); - }, - ); - - test('returns empty when clipboard is empty', () async { - await setClipboardText(''); - final container = ProviderContainer(); - addTearDown(container.dispose); - - final result = await container - .read(recordingViewModelProvider.notifier) - .getClipboardContent(); - - expect(result, ClipboardReadResult.empty); - final model = container.read(recordingViewModelProvider); - expect(model.hasValidClipboardInfo, isFalse); - expect(model.clipboardRecordingModel.title, defaultClipboardTitle); - }); - - test('returns invalid when clipboard is not JSON', () async { - await setClipboardText('hello'); - final container = ProviderContainer(); - addTearDown(container.dispose); - - final result = await container - .read(recordingViewModelProvider.notifier) - .getClipboardContent(); - - expect(result, ClipboardReadResult.invalid); - expect( - container - .read(recordingViewModelProvider) - .clipboardRecordingModel - .title, - defaultClipboardTitle, - ); - expect( - container.read(recordingViewModelProvider).hasValidClipboardInfo, - isFalse, - ); - }); - - test('returns invalid when clipboard JSON is not an object', () async { - await setClipboardText('[1,2,3]'); - final container = ProviderContainer(); - addTearDown(container.dispose); - - final result = await container - .read(recordingViewModelProvider.notifier) - .getClipboardContent(); - - expect(result, ClipboardReadResult.invalid); - expect( - container - .read(recordingViewModelProvider) - .clipboardRecordingModel - .title, - defaultClipboardTitle, - ); - }); - - test( - 'returns invalid when clipboard JSON misses required address', - () async { - await setClipboardText('{"title":"王东方 丨李想 空中格斗赛"}'); - final container = ProviderContainer(); - addTearDown(container.dispose); - - final result = await container - .read(recordingViewModelProvider.notifier) - .getClipboardContent(); - - expect(result, ClipboardReadResult.invalid); - expect( - container - .read(recordingViewModelProvider) - .clipboardRecordingModel - .title, - defaultClipboardTitle, - ); - }, - ); - - test('updates state when clipboard omits optional timestamps', () async { - await setClipboardText( - '{"title":"郑昌梦 丨黄伟依 空中格斗赛 小学组","address":"广东省汕头市番禺区青蓝街 111 号","filename":"郑昌梦_黄伟依_6月3日测试-1_空中格斗赛"}', - ); - final container = ProviderContainer(); - addTearDown(container.dispose); - - final result = await container - .read(recordingViewModelProvider.notifier) - .getClipboardContent(); - - expect(result, ClipboardReadResult.success); - final model = container.read(recordingViewModelProvider); - expect(model.hasValidClipboardInfo, isTrue); - expect(model.clipboardRecordingModel.startTimestamp, isNull); - expect(model.clipboardRecordingModel.endTimestamp, isNull); - expect(model.clipboardRecordingModel.filename, '郑昌梦_黄伟依_6月3日测试-1_空中格斗赛'); - }); - - test('returns invalid when clipboard JSON has wrong field type', () async { - await setClipboardText( - '{"title":"王东方 丨李想 空中格斗赛","startTimestamp":"1717334400","endTimestamp":1717334400,"address":"广州市番禺区·粤港澳大湾区青年人才双创小镇"}', - ); - final container = ProviderContainer(); - addTearDown(container.dispose); - - final result = await container - .read(recordingViewModelProvider.notifier) - .getClipboardContent(); - - expect(result, ClipboardReadResult.invalid); - expect( - container - .read(recordingViewModelProvider) - .clipboardRecordingModel - .title, - defaultClipboardTitle, - ); - }); - - test('returns invalid when title is blank', () async { - await setClipboardText( - '{"title":" ","startTimestamp":1717334400,"endTimestamp":1717334400,"address":"广州市"}', - ); - final container = ProviderContainer(); - addTearDown(container.dispose); - - final result = await container - .read(recordingViewModelProvider.notifier) - .getClipboardContent(); - - expect(result, ClipboardReadResult.invalid); - expect( - container.read(recordingViewModelProvider).hasValidClipboardInfo, - isFalse, - ); - }); - }); -} diff --git a/test/features/recording/widget_record_header_test.dart b/test/features/recording/widget_record_header_test.dart deleted file mode 100644 index 4a85e43..0000000 --- a/test/features/recording/widget_record_header_test.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:recording_tool/features/recording/widgets/widget_record_header.dart'; -import 'package:recording_tool/gen/assets.gen.dart'; - -void main() { - Future pumpHeader( - WidgetTester tester, { - required bool hasValidClipboardInfo, - String? eventTitle, - }) async { - await tester.pumpWidget( - ScreenUtilInit( - designSize: const Size(375, 812), - builder: (context, _) { - return MaterialApp( - home: Scaffold( - body: RecordHeaderWidget( - hasValidClipboardInfo: hasValidClipboardInfo, - eventTitle: eventTitle, - isRecording: false, - onPasteEventInfo: () async {}, - onClearEventInfo: () {}, - ), - ), - ); - }, - ), - ); - } - - testWidgets('paste player info button uses copy image asset', (tester) async { - await pumpHeader(tester, hasValidClipboardInfo: false); - - expect(find.text('粘贴选手信息'), findsOneWidget); - expect(find.image(AssetImage(Assets.images.imageCopy.path)), findsOne); - }); - - testWidgets('clear player info button uses delete image asset', ( - tester, - ) async { - await pumpHeader( - tester, - hasValidClipboardInfo: true, - eventTitle: '王东方 丨李想 空中格斗赛', - ); - - expect(find.text('王东方 丨李想 空中格斗赛'), findsOneWidget); - expect(find.image(AssetImage(Assets.images.imageDelete.path)), findsOne); - }); -} diff --git a/test/features/recording/widget_recording_button_test.dart b/test/features/recording/widget_recording_button_test.dart deleted file mode 100644 index 2bf4680..0000000 --- a/test/features/recording/widget_recording_button_test.dart +++ /dev/null @@ -1,138 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:recording_tool/features/recording/widgets/widget_recording_button.dart'; - -void main() { - const designSize = Size(375, 812); - const morphDuration = Duration(milliseconds: 380); - - Future pumpButton( - WidgetTester tester, { - required bool isRecording, - bool isStartingRecording = false, - }) async { - await tester.pumpWidget( - ScreenUtilInit( - designSize: designSize, - builder: (context, _) { - return MaterialApp( - home: Scaffold( - body: Center( - child: RecordingControlButton( - isRecording: isRecording, - isStartingRecording: isStartingRecording, - onTap: () {}, - ), - ), - ), - ); - }, - ), - ); - await tester.pump(); - } - - Size innerCoreSize(WidgetTester tester) { - final finder = find.byWidgetPredicate( - (widget) => - widget is Container && - widget.decoration is BoxDecoration && - (widget.decoration! as BoxDecoration).color == Colors.red, - ); - return tester.getSize(finder); - } - - testWidgets('idle state uses large circular inner core', (tester) async { - await pumpButton(tester, isRecording: false); - - final size = innerCoreSize(tester); - expect(size.width, closeTo(62.r, 0.5)); - expect(size.height, closeTo(62.r, 0.5)); - }); - - testWidgets('isStartingRecording morphs to stop square before isRecording', ( - tester, - ) async { - await pumpButton( - tester, - isRecording: false, - isStartingRecording: true, - ); - - await tester.pump(morphDuration); - await tester.pump(); - - final size = innerCoreSize(tester); - expect(size.width, closeTo(22.r, 0.5)); - expect(size.height, closeTo(22.r, 0.5)); - }); - - testWidgets('isRecording forward and reverse morph without errors', ( - tester, - ) async { - await pumpButton(tester, isRecording: false); - - await tester.pumpWidget( - ScreenUtilInit( - designSize: designSize, - builder: (context, _) { - return MaterialApp( - home: Scaffold( - body: Center( - child: RecordingControlButton( - isRecording: true, - onTap: () {}, - ), - ), - ), - ); - }, - ), - ); - await tester.pump(morphDuration); - await tester.pump(); - - expect(innerCoreSize(tester).width, closeTo(22.r, 0.5)); - - await tester.pumpWidget( - ScreenUtilInit( - designSize: designSize, - builder: (context, _) { - return MaterialApp( - home: Scaffold( - body: Center( - child: RecordingControlButton( - isRecording: false, - onTap: () {}, - ), - ), - ), - ); - }, - ), - ); - await tester.pump(morphDuration); - await tester.pump(); - - expect(innerCoreSize(tester).width, closeTo(62.r, 0.5)); - }); - - testWidgets('failed start rolls morph back to idle circle', (tester) async { - await pumpButton( - tester, - isRecording: false, - isStartingRecording: true, - ); - await tester.pump(morphDuration); - await tester.pump(); - - expect(innerCoreSize(tester).width, closeTo(22.r, 0.5)); - - await pumpButton(tester, isRecording: false, isStartingRecording: false); - await tester.pump(morphDuration); - await tester.pump(); - - expect(innerCoreSize(tester).width, closeTo(62.r, 0.5)); - }); -} diff --git a/test/features/recording/widget_recording_hud_test.dart b/test/features/recording/widget_recording_hud_test.dart deleted file mode 100644 index 867f036..0000000 --- a/test/features/recording/widget_recording_hud_test.dart +++ /dev/null @@ -1,231 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:recording_tool/features/recording/widgets/widget_recording_hud.dart'; - -void main() { - Future pumpHud( - WidgetTester tester, { - double zoomRatio = 1.0, - double minZoomRatio = 1.0, - double maxZoomRatio = 3.0, - bool isRecording = false, - bool isSwitchingLens = false, - Future Function()? onStop, - ValueChanged? onZoomSelected, - }) async { - await tester.pumpWidget( - ScreenUtilInit( - designSize: const Size(375, 812), - builder: (context, _) { - return MaterialApp( - home: Scaffold( - backgroundColor: Colors.black, - body: RecordingHudWidget( - hasDndAccess: true, - isBatteryOptimizedIgnored: true, - notificationsGranted: true, - isRecording: isRecording, - isStartingRecording: false, - isSwitchingLens: isSwitchingLens, - isTouchLocked: false, - zoomRatio: zoomRatio, - minZoomRatio: minZoomRatio, - maxZoomRatio: maxZoomRatio, - onStart: () async {}, - onStop: onStop ?? () async {}, - onOpenDnd: () {}, - onOpenBattery: () {}, - onToggleTouchLock: () {}, - onZoomSelected: onZoomSelected ?? (_) {}, - ), - ), - ); - }, - ), - ); - await tester.pump(); - } - - testWidgets('shows preset zoom buttons', (tester) async { - await pumpHud(tester); - - expect(find.text('广角'), findsNothing); - expect(find.text('1x'), findsOneWidget); - expect(find.text('2x'), findsNothing); - expect(find.text('3x'), findsNothing); - }); - - testWidgets('shows wide angle when ultra-wide camera capability is 0.5', ( - tester, - ) async { - await pumpHud(tester, minZoomRatio: 0.5); - - expect(find.text('广角'), findsOneWidget); - expect(find.text('1x'), findsOneWidget); - expect(find.text('2x'), findsNothing); - expect(find.text('3x'), findsNothing); - }); - - testWidgets('shows wide angle when 0.6x camera capability supports it', ( - tester, - ) async { - await pumpHud(tester, minZoomRatio: 0.6); - - expect(find.text('广角'), findsOneWidget); - expect(find.text('1x'), findsOneWidget); - }); - - testWidgets('marks current 0.5x wide angle ratio as selected', ( - tester, - ) async { - await pumpHud(tester, zoomRatio: 0.5, minZoomRatio: 0.5); - - final selectedButton = tester.widget( - find.ancestor(of: find.text('广角'), matching: find.byType(TextButton)), - ); - expect(selectedButton.enabled, isFalse); - }); - - testWidgets('marks current 0.6x wide angle ratio as selected', ( - tester, - ) async { - await pumpHud(tester, zoomRatio: 0.6, minZoomRatio: 0.6); - - final selectedButton = tester.widget( - find.ancestor(of: find.text('广角'), matching: find.byType(TextButton)), - ); - expect(selectedButton.enabled, isFalse); - }); - - testWidgets('does not expose presets beyond max zoom ratio', (tester) async { - await pumpHud(tester, minZoomRatio: 0.5, maxZoomRatio: 0.55); - - expect(find.text('广角'), findsOneWidget); - expect(find.text('1x'), findsNothing); - }); - - testWidgets('tapping 0.5x reports 0.5 when camera capability is 0.5', ( - tester, - ) async { - double? selected; - await pumpHud( - tester, - minZoomRatio: 0.5, - onZoomSelected: (ratio) => selected = ratio, - ); - - await tester.tap(find.text('广角')); - await tester.pump(const Duration(milliseconds: 350)); - - expect(selected, 0.5); - }); - - testWidgets('tapping 0.6x reports 0.6 when camera only supports 0.6x', ( - tester, - ) async { - double? selected; - await pumpHud( - tester, - minZoomRatio: 0.6, - onZoomSelected: (ratio) => selected = ratio, - ); - - await tester.tap(find.text('广角')); - await tester.pump(const Duration(milliseconds: 350)); - - expect(selected, 0.6); - }); - - testWidgets('allows 0.6x while recording on main camera after unlock', ( - tester, - ) async { - double? selected; - await pumpHud( - tester, - minZoomRatio: 0.6, - isRecording: true, - onZoomSelected: (ratio) => selected = ratio, - ); - - final ultraWideButton = tester.widget( - find.ancestor(of: find.text('广角'), matching: find.byType(TextButton)), - ); - final mainButton = tester.widget( - find.ancestor(of: find.text('1x'), matching: find.byType(TextButton)), - ); - - expect(ultraWideButton.enabled, isTrue); - expect(mainButton.enabled, isFalse); - - await tester.tap(find.text('广角')); - await tester.pump(const Duration(milliseconds: 350)); - - expect(selected, 0.6); - }); - - testWidgets('allows 1x while recording on ultra-wide after unlock', ( - tester, - ) async { - double? selected; - await pumpHud( - tester, - zoomRatio: 0.5, - minZoomRatio: 0.5, - isRecording: true, - onZoomSelected: (ratio) => selected = ratio, - ); - - final ultraWideButton = tester.widget( - find.ancestor(of: find.text('广角'), matching: find.byType(TextButton)), - ); - final mainButton = tester.widget( - find.ancestor(of: find.text('1x'), matching: find.byType(TextButton)), - ); - - expect(ultraWideButton.enabled, isFalse); - expect(mainButton.enabled, isTrue); - - await tester.tap(find.text('1x')); - await tester.pump(const Duration(milliseconds: 350)); - - expect(selected, 1.0); - }); - - testWidgets('disables stop button while switching lens', (tester) async { - var stopped = false; - await pumpHud( - tester, - minZoomRatio: 0.6, - isRecording: true, - isSwitchingLens: true, - onStop: () async => stopped = true, - ); - - await tester.tap(find.byType(GestureDetector).last); - await tester.pump(); - - expect(stopped, isFalse); - }); - - testWidgets('disables zoom buttons while switching lens', (tester) async { - double? selected; - await pumpHud( - tester, - minZoomRatio: 0.6, - isRecording: true, - isSwitchingLens: true, - onZoomSelected: (ratio) => selected = ratio, - ); - - final ultraWideButton = tester.widget( - find.ancestor(of: find.text('广角'), matching: find.byType(TextButton)), - ); - - expect(ultraWideButton.enabled, isFalse); - await tester.tap(find.text('广角')); - await tester.pump(const Duration(milliseconds: 350)); - - expect(selected, isNull); - }); -} diff --git a/test/features/recording/widget_recording_touch_lock_overlay_test.dart b/test/features/recording/widget_recording_touch_lock_overlay_test.dart deleted file mode 100644 index 24bbf60..0000000 --- a/test/features/recording/widget_recording_touch_lock_overlay_test.dart +++ /dev/null @@ -1,126 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:recording_tool/features/recording/widgets/widget_recording_touch_lock_overlay.dart'; - -void main() { - group('resolveRecordingTouchLockUnlockIntent', () { - test('returns stopRecording for portrait bottom 30 percent', () { - final intent = resolveRecordingTouchLockUnlockIntent( - position: const Offset(120, 466.9), - size: const Size(375, 667), - ); - - expect(intent, RecordingTouchLockUnlockIntent.stopRecording); - }); - - test('returns unlockOnly for portrait area outside bottom 30 percent', () { - final intent = resolveRecordingTouchLockUnlockIntent( - position: const Offset(120, 320), - size: const Size(375, 667), - ); - - expect(intent, RecordingTouchLockUnlockIntent.unlockOnly); - }); - - test('returns stopRecording for landscape right 30 percent', () { - final intent = resolveRecordingTouchLockUnlockIntent( - position: const Offset(466.9, 120), - size: const Size(667, 375), - ); - - expect(intent, RecordingTouchLockUnlockIntent.stopRecording); - }); - - test('returns unlockOnly for landscape area outside right 30 percent', () { - final intent = resolveRecordingTouchLockUnlockIntent( - position: const Offset(320, 120), - size: const Size(667, 375), - ); - - expect(intent, RecordingTouchLockUnlockIntent.unlockOnly); - }); - }); - - group('RecordingTouchLockOverlayWidget', () { - Future pumpOverlay( - WidgetTester tester, { - required Size surfaceSize, - required ValueChanged onUnlocked, - }) async { - await tester.binding.setSurfaceSize(surfaceSize); - addTearDown(() => tester.binding.setSurfaceSize(null)); - - await tester.pumpWidget( - ScreenUtilInit( - designSize: const Size(375, 812), - builder: (context, _) { - return MaterialApp( - home: Scaffold( - body: Stack( - children: [ - RecordingTouchLockOverlayWidget( - enabled: true, - unlockHoldDuration: const Duration(seconds: 2), - onUnlocked: onUnlocked, - ), - ], - ), - ), - ); - }, - ), - ); - } - - testWidgets('long press in portrait bottom 30 percent stops recording', ( - tester, - ) async { - RecordingTouchLockUnlockIntent? receivedIntent; - await pumpOverlay( - tester, - surfaceSize: const Size(375, 667), - onUnlocked: (intent) => receivedIntent = intent, - ); - - final gesture = await tester.startGesture(const Offset(120, 600)); - await tester.pump(const Duration(seconds: 2)); - await gesture.up(); - - expect(receivedIntent, RecordingTouchLockUnlockIntent.stopRecording); - }); - - testWidgets('long press outside stop area only unlocks', (tester) async { - RecordingTouchLockUnlockIntent? receivedIntent; - await pumpOverlay( - tester, - surfaceSize: const Size(375, 667), - onUnlocked: (intent) => receivedIntent = intent, - ); - - final gesture = await tester.startGesture(const Offset(120, 320)); - await tester.pump(const Duration(seconds: 2)); - await gesture.up(); - - expect(receivedIntent, RecordingTouchLockUnlockIntent.unlockOnly); - }); - - testWidgets('releasing before hold duration does not unlock', ( - tester, - ) async { - RecordingTouchLockUnlockIntent? receivedIntent; - await pumpOverlay( - tester, - surfaceSize: const Size(375, 667), - onUnlocked: (intent) => receivedIntent = intent, - ); - - final gesture = await tester.startGesture(const Offset(120, 600)); - await tester.pump(const Duration(milliseconds: 1500)); - await gesture.up(); - await tester.pump(const Duration(seconds: 1)); - - expect(receivedIntent, isNull); - }); - }); -} diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index 892a9cf..0000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:recording_tool/app/app.dart'; -import 'package:recording_tool/features/recording/widgets/widget_recording_button.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - const validClipboardText = - '{"title":"王东方 丨李想 空中格斗赛","startTimestamp":1717334400,"endTimestamp":1717334400,"filename":"选手名称_选手ID_赛事名称_赛项","address":"广州市番禺区·粤港澳大湾区青年人才双创小镇"}'; - - String? clipboardText; - - setUp(() { - clipboardText = null; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(SystemChannels.platform, (call) async { - if (call.method == 'Clipboard.getData') { - return clipboardText == null - ? null - : {'text': clipboardText}; - } - return null; - }); - }); - - tearDown(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(SystemChannels.platform, null); - }); - - Future pumpRecordingApp(WidgetTester tester) async { - await tester.pumpWidget(const ProviderScope(child: FlutterTemplateApp())); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 500)); - await tester.pump(const Duration(seconds: 1)); - } - - testWidgets('recording app renders recording page', (tester) async { - await pumpRecordingApp(tester); - - final recordButton = find.byType(RecordingControlButton); - - expect(recordButton, findsOneWidget); - expect( - tester.getCenter(recordButton).dx, - closeTo(tester.getCenter(find.byType(Scaffold)).dx, 0.5), - ); - }); - - testWidgets('shows paste event info button when title is empty', ( - tester, - ) async { - clipboardText = ''; - - await pumpRecordingApp(tester); - - expect(find.text('粘贴选手信息'), findsOneWidget); - }); - - testWidgets('pastes valid event info from clipboard', (tester) async { - clipboardText = ''; - - await pumpRecordingApp(tester); - - clipboardText = validClipboardText; - await tester.tap(find.text('粘贴选手信息')); - await tester.pump(const Duration(milliseconds: 700)); - - expect(find.text('王东方 丨李想 空中格斗赛'), findsOneWidget); - }); - - testWidgets('shows no event info toast when pasted clipboard is invalid', ( - tester, - ) async { - clipboardText = ''; - - await pumpRecordingApp(tester); - - clipboardText = 'hello'; - await tester.tap(find.text('粘贴选手信息')); - await tester.pump(); - - expect(find.text('王东方 丨李想 空中格斗赛'), findsNothing); - expect(find.text('无选手信息'), findsOneWidget); - - await tester.pump(const Duration(seconds: 2)); - }); -}