// Author: fruit.com.cn import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:fruitshorts/core/data/services/playback_thumbnail_service.dart'; import 'package:fruitshorts/core/data/services/sprite_thumbnail_axis.dart'; import 'package:fruitshorts/pages/player/widgets/scrub_preview_card.dart'; void main() { testWidgets('should_render_sprite_crop_without_requesting_local_frame', ( tester, ) async { final image = await _createImage(320, 180); final cue = const SpriteThumbnailCue( start: Duration.zero, end: Duration(seconds: 2), spriteFile: 'sprite_001.jpg', sourceRect: Rect.fromLTWH(0, 0, 160, 90), ); final axis = _FakeAxisSource( axisReady: true, frame: SpriteThumbnailFrame( image: image, sourceRect: const Rect.fromLTWH(0, 0, 160, 90), spriteUrl: 'https://cdn.test/sprite_001.jpg', cue: cue, ), ); final loader = _CountingThumbnailLoader(); final localService = _localService(loader); addTearDown(image.dispose); addTearDown(axis.dispose); addTearDown(localService.dispose); await tester.pumpWidget( _card( localService: localService, axis: axis, thumbnailsUrl: 'https://cdn.test/thumbnails.vtt', ), ); await tester.pump(); final preview = tester.widget( find.byKey(const Key('scrub-preview-sprite')), ); expect(preview.sourceRect, const Rect.fromLTWH(0, 0, 160, 90)); expect(find.byKey(const Key('scrub-preview-frame')), findsNothing); expect(loader.calls, 0); }); testWidgets('should_fallback_to_local_frame_when_thumbnails_url_is_missing', ( tester, ) async { final loader = _CountingThumbnailLoader(); final localService = _localService(loader); final axis = _FakeAxisSource(axisReady: false); addTearDown(axis.dispose); addTearDown(localService.dispose); await tester.pumpWidget( _card(localService: localService, axis: axis, thumbnailsUrl: null), ); await tester.pump(); expect(loader.calls, 1); expect(find.byKey(const Key('scrub-preview-poster')), findsOneWidget); expect(find.byKey(const Key('scrub-preview-sprite')), findsNothing); }); testWidgets('should_keep_poster_when_axis_and_local_frame_both_fail', ( tester, ) async { final loader = _CountingThumbnailLoader(); final localService = _localService(loader); final axis = _FakeAxisSource(axisReady: true); addTearDown(axis.dispose); addTearDown(localService.dispose); await tester.pumpWidget( _card( localService: localService, axis: axis, thumbnailsUrl: 'https://cdn.test/empty.vtt', ), ); await tester.pump(); expect(loader.calls, 1); expect(find.byKey(const Key('scrub-preview-poster')), findsOneWidget); expect(find.byKey(const Key('scrub-preview-sprite')), findsNothing); expect(find.byKey(const Key('scrub-preview-frame')), findsNothing); }); } Widget _card({ required PlaybackThumbnailService localService, required SpriteThumbnailAxisSource axis, required String? thumbnailsUrl, }) { return MaterialApp( home: Scaffold( body: Center( child: ScrubPreviewCard( service: localService, spriteAxisService: axis, thumbnailsUrl: thumbnailsUrl, episodeId: 'episode-1', posterUrl: '', position: const Duration(seconds: 1), duration: const Duration(seconds: 10), ), ), ), ); } PlaybackThumbnailService _localService(PlaybackThumbnailFrameLoader loader) { return PlaybackThumbnailService( frameLoader: loader, sourceResolver: (_) => const PlaybackThumbnailSource(remoteUrl: 'https://example/video.mp4'), prefetchDelay: const Duration(hours: 1), ); } class _FakeAxisSource extends ChangeNotifier implements SpriteThumbnailAxisSource { _FakeAxisSource({required this.axisReady, this.frame}); final bool axisReady; final SpriteThumbnailFrame? frame; @override SpriteThumbnailFrame? cachedFrameAt( String? thumbnailsUrl, Duration position, ) => frame; @override bool hasAxis(String? thumbnailsUrl) => axisReady; } class _CountingThumbnailLoader implements PlaybackThumbnailFrameLoader { int calls = 0; @override Future load(PlaybackThumbnailFrameRequest request) async { calls++; return null; } @override void dispose() {} } Future _createImage(int width, int height) async { final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); canvas.drawRect( Rect.fromLTWH(0, 0, width.toDouble(), height.toDouble()), Paint()..color = Colors.orange, ); final picture = recorder.endRecording(); final image = await picture.toImage(width, height); picture.dispose(); return image; }