feat(app): 完善播放协议搜索与空状态

This commit is contained in:
Zhou Weidong committed 2026-10-23 12:38:47 +08:00
1 parent 671e0ce143
commit ef29fc5f15
85 files changed
+2976 -768

No files matched your search

+2 -2
View File
@@ -12,8 +12,8 @@ import '../support/fake_haptics.dart';
import '../support/fake_interaction_repositories.dart';
void main() {
testWidgets('double tap within 320ms never pauses', (tester) async {
for (final delay in [120, 250, 300]) {
testWidgets('double tap within 400ms never pauses', (tester) async {
for (final delay in [120, 250, 300, 380]) {
final repository = FakeInteractionRepository();
final haptics = FakeHaptics();
final key = GlobalKey<_HarnessState>();
@@ -0,0 +1,29 @@
// Author: fruit.com.cn
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/pages/player/widgets/player_double_tap_hearts.dart';
void main() {
testWidgets('should_show_three_medium_hearts_when_double_tap_burst_starts', (
tester,
) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(body: PlayerDoubleTapHeartBurst(onCompleted: _noop)),
),
);
await tester.pump(const Duration(milliseconds: 160));
expect(find.byIcon(Icons.favorite_rounded), findsNWidgets(3));
expect(_size(tester, 0), 48);
expect(_size(tester, 1), 36);
expect(_size(tester, 2), 40);
});
}
double _size(WidgetTester tester, int index) {
return tester.widget<Icon>(find.byKey(Key('double-tap-heart-$index'))).size!;
}
void _noop() {}
+77 -2
View File
@@ -118,6 +118,75 @@ void main() {
expect(disabledText.style?.color, AppColors.subtle);
});
testWidgets('should_enable_quality_when_ticket_uses_lowercase_codes', (
tester,
) async {
await _openSheet(
tester,
qualities: const ['AUTO', '360p', '480p', '720p', '1080p'],
);
expect(find.text('720P'), findsOneWidget);
expect(find.text('1080P'), findsOneWidget);
expect(find.textContaining('暂不可用'), findsNothing);
final enabled = tester.widget<Text>(
find.descendant(
of: find.byKey(const Key('player-more-quality-720P')),
matching: find.byType(Text),
),
);
expect(enabled.style?.color, isNot(AppColors.subtle));
});
testWidgets('should_report_selected_quality_when_a_rendition_is_tapped', (
tester,
) async {
String? picked;
await _openSheet(
tester,
qualities: const ['AUTO', '720p'],
onQuality: (quality) async {
picked = quality;
return true;
},
);
await tester.ensureVisible(find.byKey(const Key('player-more-quality-720P')));
await tester.tap(find.byKey(const Key('player-more-quality-720P')));
await tester.pump();
expect(picked, '720P');
final selected = tester.widget<Text>(
find.descendant(
of: find.byKey(const Key('player-more-quality-720P')),
matching: find.byType(Text),
),
);
expect(selected.style?.color, AppColors.onSurface);
});
testWidgets('should_restore_previous_quality_when_switch_fails', (
tester,
) async {
await _openSheet(
tester,
qualities: const ['AUTO', '720p'],
onQuality: (_) async => false,
);
await tester.ensureVisible(find.byKey(const Key('player-more-quality-720P')));
await tester.tap(find.byKey(const Key('player-more-quality-720P')));
await tester.pumpAndSettle();
final restored = tester.widget<Text>(
find.descendant(
of: find.byKey(const Key('player-more-quality-AUTO')),
matching: find.byType(Text),
),
);
expect(restored.style?.color, AppColors.onSurface);
});
testWidgets('should_trigger_pip_and_close_sheet_when_tapped', (tester) async {
var pipCalls = 0;
await _openSheet(tester, onPip: () => pipCalls++);
@@ -142,7 +211,12 @@ void main() {
});
}
Future<void> _openSheet(WidgetTester tester, {VoidCallback? onPip}) async {
Future<void> _openSheet(
WidgetTester tester, {
VoidCallback? onPip,
Future<bool> Function(String quality)? onQuality,
List<String> qualities = const ['AUTO'],
}) async {
await tester.pumpWidget(
MaterialApp(
supportedLocales: AppLocalizations.supportedLocales,
@@ -158,11 +232,12 @@ Future<void> _openSheet(WidgetTester tester, {VoidCallback? onPip}) async {
isScrollControlled: true,
builder: (_) => PlayerMoreSheet(
l10n: AppLocalizations.of(context)!,
state: const PlayerMoreState(speed: 1.0, qualities: ['AUTO']),
state: PlayerMoreState(speed: 1.0, qualities: qualities),
actions: PlayerMoreActions(
onSpeedChanged: (_) {},
onClear: () {},
onDownload: () {},
onQualityChanged: onQuality ?? (_) async => true,
onPip: onPip,
),
),
@@ -69,6 +69,51 @@ void main() {
expect(_opacity(tester), 0);
expect(find.byKey(const Key('player-debug-video')), findsNothing);
});
testWidgets('should_cover_new_video_only_while_handoff_is_held', (
tester,
) async {
final current = VideoPlayerController.networkUrl(
Uri.parse('http://192.168.31.187:8081/files/720.m3u8'),
);
final previous = VideoPlayerController.networkUrl(
Uri.parse('http://192.168.31.187:8081/files/360.m3u8'),
);
addTearDown(current.dispose);
addTearDown(previous.dispose);
const ready = VideoPlayerValue(
duration: Duration(minutes: 1),
size: Size(1280, 720),
position: Duration(seconds: 3),
isInitialized: true,
isPlaying: true,
);
current.value = ready;
previous.value = ready;
Future<void> pump(VideoPlayerController? handoff) {
return tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: PlayerVideoSurface(
controller: current,
handoff: handoff,
videoUrl: current.dataSource,
theater: false,
),
),
),
);
}
await pump(previous);
expect(find.byKey(const Key('player-video-handoff')), findsOneWidget);
expect(_opacity(tester), 1);
await pump(null);
expect(find.byKey(const Key('player-video-handoff')), findsNothing);
expect(find.byType(VideoPlayer), findsOneWidget);
});
}
Future<VideoPlayerController> _pumpSurface(
+28 -1
View File
@@ -154,12 +154,37 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('feed progress thumb stays on the finger when dragged right', (
tester,
) async {
await _pumpFeedOverlay(tester);
final bar = find.byKey(const Key('home-feed-progress'));
final gesture = await tester.startGesture(tester.getCenter(bar));
await gesture.moveBy(const Offset(800, 0));
await tester.pump();
final thumbCenter = tester
.getRect(find.byKey(const Key('home-feed-progress-thumb')))
.center
.dx;
final playedRight = tester
.getRect(find.byKey(const Key('home-feed-progress-played')))
.right;
expect(thumbCenter, closeTo(tester.getRect(bar).right, 0.5));
expect(playedRight, closeTo(thumbCenter, 0.5));
await gesture.up();
await tester.pumpAndSettle();
});
testWidgets('full player drag clears chrome and release restores it', (
tester,
) async {
await _pumpFullPlayerOverlay(tester);
expect(find.byKey(const Key('player-controls-top')), findsOneWidget);
expect(find.byKey(const Key('player-bottom-actions')), findsOneWidget);
final progressTop = tester.getTopLeft(find.byType(Slider)).dy;
final gesture = await tester.startGesture(
tester.getCenter(find.byKey(const Key('player-progress'))),
@@ -170,8 +195,9 @@ void main() {
expect(_opacity(tester, const Key('player-controls-top')), 0);
expect(_opacity(tester, const Key('player-interaction-rail')), 0);
expect(_offstage(tester, const Key('player-controls-bottom')), isTrue);
expect(_offstage(tester, const Key('player-bottom-actions')), isTrue);
expect(_opacity(tester, const Key('player-bottom-actions')), 0);
expect(find.byKey(const Key('player-progress')), findsOneWidget);
expect(tester.getTopLeft(find.byType(Slider)).dy, progressTop);
await gesture.up();
await tester.pumpAndSettle();
@@ -179,6 +205,7 @@ void main() {
expect(_opacity(tester, const Key('player-controls-top')), 1);
expect(_opacity(tester, const Key('player-interaction-rail')), 1);
expect(_offstage(tester, const Key('player-bottom-actions')), isFalse);
expect(tester.getTopLeft(find.byType(Slider)).dy, progressTop);
});
}