// Author: fruit.com.cn import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:fruitshorts/core/data/services/sprite_thumbnail_asset_loader.dart'; import 'package:fruitshorts/core/widgets/remote_image.dart'; import 'package:fruitshorts/pages/player/widgets/player_opening_frame.dart'; const String _spriteVtt = ''' WEBVTT 00:00:00.000 --> 00:00:01.000 sprite_001.jpg#xywh=0,0,160,90 '''; void main() { test('should_return_no_opening_frame_when_thumbnails_url_is_empty', () { expect(playerOpeningFrame(null, posterUrl: ' '), isNull); expect(playerOpeningFrame(' '), isNull); }); test('should_keep_opening_frame_when_only_poster_is_available', () { expect( playerOpeningFrame(null, posterUrl: 'https://cdn.test/poster.jpg'), isA(), ); }); testWidgets('should_load_and_reuse_first_sprite_frame', (tester) async { final loader = _FakeSpriteLoader(); final cache = OpeningFrameCache(); addTearDown(cache.clear); final first = await cache.load( 'https://cdn.test/thumbnails.vtt', loader: loader, ); final second = await cache.load( 'https://cdn.test/thumbnails.vtt', loader: loader, ); expect(first, isNotNull); expect(second, same(first)); expect(loader.vttCalls, 1); expect(loader.spriteCalls, ['sprite_001.jpg']); }); testWidgets('should_render_decoded_opening_frame', (tester) async { final loader = _FakeSpriteLoader(); final cache = OpeningFrameCache(); addTearDown(cache.clear); await tester.pumpWidget( MaterialApp( home: PlayerOpeningFrame( thumbnailsUrl: 'https://cdn.test/thumbnails.vtt', loader: loader, cache: cache, ), ), ); await tester.pumpAndSettle(); expect(find.byKey(const Key('player-opening-sprite')), findsOneWidget); }); testWidgets('should_show_poster_while_sprite_is_loading', (tester) async { final cache = OpeningFrameCache(); addTearDown(cache.clear); await tester.pumpWidget( MaterialApp( home: PlayerOpeningFrame( thumbnailsUrl: 'https://cdn.test/thumbnails.vtt', posterUrl: 'https://cdn.test/poster.jpg', loader: _FakeSpriteLoader(), cache: cache, ), ), ); expect(find.byKey(const Key('player-opening-poster')), findsOneWidget); }); testWidgets('should_use_surface_fit_without_extra_frame_constraint', ( tester, ) async { await tester.pumpWidget( const MaterialApp( home: PlayerOpeningFrame( posterUrl: 'https://cdn.test/poster.jpg', ), ), ); final poster = tester.widget( find.byKey(const Key('player-opening-poster')), ); expect(poster.fit, BoxFit.cover); expect(find.byType(AspectRatio), findsNothing); }); test('should_forward_video_fit_to_opening_frame', () { final frame = playerOpeningFrame( null, posterUrl: 'https://cdn.test/poster.jpg', fit: BoxFit.contain, ); expect(frame, isA()); expect((frame! as PlayerOpeningFrame).fit, BoxFit.contain); }); testWidgets('should_reuse_decoded_sprite_when_parent_rebuilds', ( tester, ) async { final loader = _FakeSpriteLoader(); final cache = OpeningFrameCache(); addTearDown(cache.clear); Widget build() => MaterialApp( home: PlayerOpeningFrame( thumbnailsUrl: 'https://cdn.test/thumbnails.vtt', posterUrl: 'https://cdn.test/poster.jpg', loader: loader, cache: cache, ), ); await tester.pumpWidget(build()); await tester.pumpAndSettle(); expect(loader.vttCalls, 1); expect(loader.spriteCalls, hasLength(1)); await tester.pumpWidget(build()); await tester.pumpAndSettle(); expect(loader.vttCalls, 1); expect(loader.spriteCalls, hasLength(1)); expect(find.byKey(const Key('player-opening-sprite')), findsOneWidget); }); } class _FakeSpriteLoader implements SpriteThumbnailAssetLoader { int vttCalls = 0; final List spriteCalls = []; @override Future loadVtt(Uri uri) async { vttCalls++; return _spriteVtt; } @override Future loadSprite(Uri uri) async { spriteCalls.add(uri.pathSegments.last); return SpriteThumbnailDecodedImage( image: await _createImage(320, 180), originalWidth: 320, originalHeight: 180, ); } } 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.blue, ); final picture = recorder.endRecording(); final image = await picture.toImage(width, height); picture.dispose(); return image; }