feat: download video feature (#362)

* feat: adding download video feature

* final adjustments and fixes

* fix tests

* fix tests

* pr suggestions and UI improvements

* PR suggestions

* pr sggestions
This commit is contained in:
Erick
2023-01-17 13:23:46 -03:00
committed by GitHub
parent 6125e4faca
commit c1e7ba461b
30 changed files with 733 additions and 32 deletions

View File

@ -0,0 +1,22 @@
name: download_repository
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
pull_request:
paths:
- "packages/download_repository/**"
- ".github/workflows/download_repository.yaml"
branches:
- main
jobs:
build:
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
with:
flutter_channel: "beta"
flutter_version: 3.6.0-0.1.pre
coverage_excludes: "**/*.gen.dart"
working_directory: packages/download_repository

View File

@ -50,7 +50,7 @@ describe('Download API', () => {
test('Invalid file extension returns 400', async () => {
const req = Object.assign({}, baseReq, {
path: 'wrong.gif',
path: 'wrong.avi',
});
const res = {

View File

@ -4,7 +4,7 @@ import * as path from 'path';
import { UPLOAD_PATH, ALLOWED_HOSTS } from '../config';
const VALID_VIDEO_EXT = [ '.mp4' ];
const VALID_VIDEO_EXT = [ '.mp4', '.gif' ];
/**
* Get the files and writes it on the response.

View File

@ -1,6 +1,7 @@
import 'package:authentication_repository/authentication_repository.dart';
import 'package:avatar_detector_repository/avatar_detector_repository.dart';
import 'package:convert_repository/convert_repository.dart';
import 'package:download_repository/download_repository.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:holobooth/l10n/l10n.dart';
@ -13,11 +14,13 @@ class App extends StatelessWidget {
required this.authenticationRepository,
required this.avatarDetectorRepository,
required this.convertRepository,
required this.downloadRepository,
});
final AuthenticationRepository authenticationRepository;
final AvatarDetectorRepository avatarDetectorRepository;
final ConvertRepository convertRepository;
final DownloadRepository downloadRepository;
@override
Widget build(BuildContext context) {
@ -26,6 +29,7 @@ class App extends StatelessWidget {
RepositoryProvider.value(value: authenticationRepository),
RepositoryProvider.value(value: avatarDetectorRepository),
RepositoryProvider.value(value: convertRepository),
RepositoryProvider.value(value: downloadRepository),
],
child: AnimatedFadeIn(
child: ResponsiveLayoutBuilder(

View File

@ -5,6 +5,7 @@ import 'package:authentication_repository/authentication_repository.dart';
import 'package:avatar_detector_repository/avatar_detector_repository.dart';
import 'package:bloc/bloc.dart';
import 'package:convert_repository/convert_repository.dart';
import 'package:download_repository/download_repository.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/scheduler.dart';
@ -45,6 +46,8 @@ Future<void> bootstrap({
assetBucketUrl: assetBucketUrl,
);
final downloadRepository = DownloadRepository();
unawaited(
Future.wait([
Flame.images.load('holobooth_avatar.png'),
@ -57,6 +60,7 @@ Future<void> bootstrap({
authenticationRepository: authenticationRepository,
avatarDetectorRepository: avatarDetectorRepository,
convertRepository: convertRepository,
downloadRepository: downloadRepository,
),
),
(error, stackTrace) {

View File

@ -0,0 +1,35 @@
import 'package:bloc/bloc.dart';
import 'package:download_repository/download_repository.dart';
import 'package:equatable/equatable.dart';
part 'download_event.dart';
part 'download_state.dart';
class DownloadBloc extends Bloc<DownloadEvent, DownloadState> {
DownloadBloc({
required String videoPath,
required DownloadRepository downloadRepository,
}) : _downloadRepository = downloadRepository,
super(DownloadState.initial(videoPath: videoPath)) {
on<DownloadRequested>(_onDownloadEvent);
}
final DownloadRepository _downloadRepository;
Future<void> _onDownloadEvent(
DownloadRequested event,
Emitter<DownloadState> emit,
) async {
emit(state.copyWith(status: DownloadStatus.fetching));
final videoHash = state.videoPath.split('/').last.split('.').first;
final fileName = '$videoHash.${event.extension}';
final mimeType = event.extension == 'mp4' ? 'video/mp4' : 'image/gif';
await _downloadRepository.downloadFile(
fileName,
mimeType,
);
emit(state.copyWith(status: DownloadStatus.completed));
}
}

View File

@ -0,0 +1,18 @@
part of 'download_bloc.dart';
abstract class DownloadEvent extends Equatable {
const DownloadEvent();
}
class DownloadRequested extends DownloadEvent {
const DownloadRequested(this.extension);
const DownloadRequested.video() : this('mp4');
const DownloadRequested.gif() : this('gif');
final String extension;
@override
List<Object?> get props => [extension];
}

View File

@ -0,0 +1,38 @@
part of 'download_bloc.dart';
enum DownloadStatus {
idle,
fetching,
completed,
}
class DownloadState extends Equatable {
const DownloadState({
required this.videoPath,
required this.status,
});
const DownloadState.initial({
required String videoPath,
}) : this(
videoPath: videoPath,
status: DownloadStatus.idle,
);
final String videoPath;
final DownloadStatus status;
DownloadState copyWith({
String? videoPath,
DownloadStatus? status,
}) {
return DownloadState(
videoPath: videoPath ?? this.videoPath,
status: status ?? this.status,
);
}
@override
List<Object?> get props => [videoPath, status];
}

View File

@ -1,2 +1,3 @@
export 'bloc/download_bloc.dart';
export 'view/view.dart';
export 'widgets/widgets.dart';

View File

@ -1,5 +1,6 @@
import 'dart:typed_data';
import 'package:download_repository/download_repository.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:holobooth/convert/convert.dart';
@ -37,6 +38,12 @@ class SharePage extends StatelessWidget {
return MultiBlocProvider(
providers: [
BlocProvider.value(value: convertBloc),
BlocProvider(
create: (_) => DownloadBloc(
videoPath: convertBloc.state.videoPath,
downloadRepository: context.read<DownloadRepository>(),
),
),
],
child: const ShareView(),
);

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:holobooth/l10n/l10n.dart';
import 'package:holobooth/share/share.dart';
import 'package:holobooth_ui/holobooth_ui.dart';
class DownloadButton extends StatefulWidget {
@ -15,30 +17,53 @@ class _DownloadButtonState extends State<DownloadButton> {
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final state = context.watch<DownloadBloc>().state;
final isLoading = state.status == DownloadStatus.fetching;
return CompositedTransformTarget(
link: layerLink,
child: GradientOutlinedButton(
icon: const Icon(
Icons.file_download_rounded,
color: HoloBoothColors.white,
),
icon: isLoading
? const SizedBox(
width: 25,
height: 25,
child: CircularProgressIndicator(
color: HoloBoothColors.convertLoading,
),
)
: const Icon(
Icons.file_download_rounded,
color: HoloBoothColors.white,
),
label: l10n.sharePageDownloadButtonText,
onPressed: () {
showDialog<void>(
context: context,
barrierColor: HoloBoothColors.transparent,
builder: (context) => DownloadOptionDialog(layerLink: layerLink),
);
},
onPressed: isLoading
? null
: () {
final downloadBloc = context.read<DownloadBloc>();
showDialog<void>(
context: context,
barrierColor: HoloBoothColors.transparent,
builder: (context) => BlocProvider.value(
value: downloadBloc,
child: DownloadOptionDialog(
layerLink: layerLink,
),
),
);
},
),
);
}
}
class DownloadOptionDialog extends StatelessWidget {
const DownloadOptionDialog({super.key, required this.layerLink});
const DownloadOptionDialog({
super.key,
required this.layerLink,
});
final LayerLink layerLink;
@override
Widget build(BuildContext context) {
return CompositedTransformFollower(
@ -74,6 +99,7 @@ class DownloadAsAGifButton extends StatelessWidget {
),
onPressed: () {
Navigator.of(context).pop();
context.read<DownloadBloc>().add(const DownloadRequested.gif());
},
icon: ShaderMask(
shaderCallback: (bounds) {
@ -103,6 +129,7 @@ class DownloadAsAVideoButton extends StatelessWidget {
),
onPressed: () {
Navigator.of(context).pop();
context.read<DownloadBloc>().add(const DownloadRequested.video());
},
icon: ShaderMask(
shaderCallback: (bounds) {

43
packages/download_repository/.gitignore vendored Normal file
View File

@ -0,0 +1,43 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# VSCode related
.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
pubspec.lock
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Test related
coverage

View File

@ -0,0 +1 @@
include: package:very_good_analysis/analysis_options.3.1.0.yaml

View File

@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="102" height="20">
<linearGradient id="b" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1" />
<stop offset="1" stop-opacity=".1" />
</linearGradient>
<clipPath id="a">
<rect width="102" height="20" rx="3" fill="#fff" />
</clipPath>
<g clip-path="url(#a)">
<path fill="#555" d="M0 0h59v20H0z" />
<path fill="#44cc11" d="M59 0h43v20H59z" />
<path fill="url(#b)" d="M0 0h102v20H0z" />
</g>
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110">
<text x="305" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="490">coverage</text>
<text x="305" y="140" transform="scale(.1)" textLength="490">coverage</text>
<text x="795" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="330">100%</text>
<text x="795" y="140" transform="scale(.1)" textLength="330">100%</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,4 @@
/// Repository responsible for fetching and downloading a file.
library download_repository;
export 'src/download_repository.dart';

View File

@ -0,0 +1,36 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:camera/camera.dart';
import 'package:http/http.dart' as http;
/// {@template download_repository}
/// Repository responsible for fetching and downloading a file.
/// {@endtemplate}
class DownloadRepository {
/// {@macro download_repository}
DownloadRepository({
Future<http.Response> Function(Uri)? get,
XFile Function(Uint8List, {String? mimeType})? parseBytes,
}) {
_get = get ?? http.get;
_parseBytes = parseBytes ?? XFile.fromData;
}
late final Future<http.Response> Function(Uri) _get;
late final XFile Function(Uint8List, {String? mimeType}) _parseBytes;
/// Fetches the given [fileName] and save it locally.
Future<void> downloadFile(String fileName, String mimeType) async {
final uri = Uri.parse('/download/$fileName');
final response = await _get(
uri,
);
final bytes = response.bodyBytes;
final file = _parseBytes(
bytes,
mimeType: mimeType,
);
await file.saveTo(fileName);
}
}

View File

@ -0,0 +1,20 @@
name: download_repository
description: Repository responsible for fetching and downloading a file.
version: 0.1.0+1
publish_to: none
environment:
sdk: ">=2.18.0 <3.0.0"
flutter: 3.3.7
dependencies:
camera: ^0.10.0+3
flutter:
sdk: flutter
http: ^0.13.5
dev_dependencies:
flutter_test:
sdk: flutter
mocktail: ^0.3.0
very_good_analysis: ^3.1.0

View File

@ -0,0 +1,38 @@
// ignore_for_file: prefer_const_constructors
import 'dart:typed_data';
import 'package:camera/camera.dart';
import 'package:download_repository/download_repository.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart';
import 'package:mocktail/mocktail.dart';
class MockResponse extends Mock implements Response {}
class MockXFile extends Mock implements XFile {}
void main() {
group('DownloadRepository', () {
test('can be instantiated', () {
expect(DownloadRepository(), isNotNull);
});
test('fetches and save the file', () async {
final response = MockResponse();
final file = MockXFile();
when(() => response.bodyBytes).thenReturn(Uint8List(1));
when(() => file.saveTo(any())).thenAnswer((_) async {});
final repo = DownloadRepository(
get: (_) async => response,
parseBytes: (_, {String? mimeType}) => file,
);
await repo.downloadFile('file', 'video/mp4');
verify(() => file.saveTo('file')).called(1);
});
});
}

View File

@ -247,6 +247,13 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.4.1"
download_repository:
dependency: "direct main"
description:
path: "packages/download_repository"
relative: true
source: path
version: "0.1.0+1"
equatable:
dependency: "direct main"
description:

View File

@ -21,6 +21,8 @@ dependencies:
convert_repository:
path: packages/convert_repository
cross_file: ^0.3.3+2
download_repository:
path: packages/download_repository
equatable: ^2.0.5
face_geometry:
path: packages/face_geometry

View File

@ -1,6 +1,7 @@
import 'package:authentication_repository/authentication_repository.dart';
import 'package:avatar_detector_repository/avatar_detector_repository.dart';
import 'package:convert_repository/convert_repository.dart';
import 'package:download_repository/download_repository.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:holobooth/app/app.dart';
@ -15,6 +16,8 @@ class _MockAuthenticationRepository extends Mock
class _MockConvertRepository extends Mock implements ConvertRepository {}
class _MockDownloadRepository extends Mock implements DownloadRepository {}
class _MockAvatarDetectorRepository extends Mock
implements AvatarDetectorRepository {
_MockAvatarDetectorRepository() {
@ -31,6 +34,7 @@ void main() {
authenticationRepository: _MockAuthenticationRepository(),
avatarDetectorRepository: _MockAvatarDetectorRepository(),
convertRepository: _MockConvertRepository(),
downloadRepository: _MockDownloadRepository(),
),
);
final materialApp = tester.widget<MaterialApp>(find.byType(MaterialApp));
@ -47,6 +51,7 @@ void main() {
authenticationRepository: _MockAuthenticationRepository(),
avatarDetectorRepository: _MockAvatarDetectorRepository(),
convertRepository: _MockConvertRepository(),
downloadRepository: _MockDownloadRepository(),
),
);
final materialApp = tester.widget<MaterialApp>(find.byType(MaterialApp));
@ -62,6 +67,7 @@ void main() {
authenticationRepository: _MockAuthenticationRepository(),
avatarDetectorRepository: _MockAvatarDetectorRepository(),
convertRepository: _MockConvertRepository(),
downloadRepository: _MockDownloadRepository(),
),
);
expect(find.byType(LandingPage), findsOneWidget);

View File

@ -45,7 +45,9 @@ void main() {
),
);
},
build: () => ConvertBloc(convertRepository: convertRepository),
build: () => ConvertBloc(
convertRepository: convertRepository,
),
act: (bloc) => bloc.add(GenerateVideoRequested(frames)),
expect: () => [
ConvertState(),
@ -64,7 +66,9 @@ void main() {
when(() => convertRepository.generateVideo(any()))
.thenThrow(Exception());
},
build: () => ConvertBloc(convertRepository: convertRepository),
build: () => ConvertBloc(
convertRepository: convertRepository,
),
act: (bloc) => bloc.add(GenerateVideoRequested(frames)),
expect: () => [
ConvertState(),

View File

@ -2,6 +2,7 @@ import 'dart:typed_data';
import 'package:avatar_detector_repository/avatar_detector_repository.dart';
import 'package:convert_repository/convert_repository.dart';
import 'package:download_repository/download_repository.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
@ -13,6 +14,8 @@ import 'package:tensorflow_models/tensorflow_models.dart' as tf;
class _MockConvertRepository extends Mock implements ConvertRepository {}
class _MockDownloadRepository extends Mock implements DownloadRepository {}
class _MockAvatarDetectorRepository extends Mock
implements AvatarDetectorRepository {
_MockAvatarDetectorRepository() {
@ -31,6 +34,7 @@ extension PumpApp on WidgetTester {
Widget widget, {
AvatarDetectorRepository? avatarDetectorRepository,
ConvertRepository? convertRepository,
DownloadRepository? downloadRepository,
}) async {
return mockNetworkImages(() async {
return pumpWidget(
@ -43,6 +47,9 @@ extension PumpApp on WidgetTester {
RepositoryProvider.value(
value: convertRepository ?? _MockConvertRepository(),
),
RepositoryProvider.value(
value: downloadRepository ?? _MockDownloadRepository(),
),
],
child: MaterialApp(
localizationsDelegates: const [

View File

@ -42,6 +42,9 @@ class _MockImage extends Mock implements ui.Image {}
class _MockAudioPlayer extends Mock implements AudioPlayer {}
class _MockConvertBloc extends MockBloc<ConvertEvent, ConvertState>
implements ConvertBloc {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
@ -144,6 +147,7 @@ void main() {
group('PhotoBoothView', () {
late PhotoBoothBloc photoBoothBloc;
late ConvertBloc convertBloc;
late InExperienceSelectionBloc inExperienceSelectionBloc;
late AvatarDetectorBloc avatarDetectorBloc;
@ -161,6 +165,9 @@ void main() {
when(() => avatarDetectorBloc.state).thenReturn(
AvatarDetectorState(status: AvatarDetectorStatus.loaded),
);
convertBloc = _MockConvertBloc();
when(() => convertBloc.state).thenReturn(const ConvertState());
});
testWidgets('plays audio', (WidgetTester tester) async {
@ -169,6 +176,7 @@ void main() {
photoBoothBloc: photoBoothBloc,
inExperienceSelectionBloc: inExperienceSelectionBloc,
avatarDetectorBloc: avatarDetectorBloc,
convertBloc: convertBloc,
);
await tester.pump();
@ -192,6 +200,7 @@ void main() {
photoBoothBloc: photoBoothBloc,
inExperienceSelectionBloc: inExperienceSelectionBloc,
avatarDetectorBloc: avatarDetectorBloc,
convertBloc: convertBloc,
);
/// Wait for the player to complete
@ -209,6 +218,7 @@ extension on WidgetTester {
required PhotoBoothBloc photoBoothBloc,
required InExperienceSelectionBloc inExperienceSelectionBloc,
required AvatarDetectorBloc avatarDetectorBloc,
required ConvertBloc convertBloc,
}) =>
pumpApp(
MultiBlocProvider(
@ -216,6 +226,7 @@ extension on WidgetTester {
BlocProvider.value(value: photoBoothBloc),
BlocProvider.value(value: inExperienceSelectionBloc),
BlocProvider.value(value: avatarDetectorBloc),
BlocProvider.value(value: convertBloc),
],
child: subject,
),

View File

@ -0,0 +1,73 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:download_repository/download_repository.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:holobooth/share/share.dart';
import 'package:mocktail/mocktail.dart';
class _MockDownloadRepository extends Mock implements DownloadRepository {}
void main() {
late DownloadRepository downloadRepository;
group('DownloadBloc', () {
setUp(() {
downloadRepository = _MockDownloadRepository();
});
group('download', () {
blocTest<DownloadBloc, DownloadState>(
'downloads the current converted file',
setUp: () {
when(() => downloadRepository.downloadFile(any(), any()))
.thenAnswer((_) async {});
},
build: () => DownloadBloc(
downloadRepository: downloadRepository,
videoPath: 'https://storage/1234.mp4',
),
act: (bloc) => bloc.add(DownloadRequested('mp4')),
verify: (_) {
verify(
() => downloadRepository.downloadFile(any(), any()),
).called(1);
},
);
blocTest<DownloadBloc, DownloadState>(
'correctly downloads a mp4',
setUp: () {
when(() => downloadRepository.downloadFile(any(), any()))
.thenAnswer((_) async {});
},
build: () => DownloadBloc(
downloadRepository: downloadRepository,
videoPath: 'https://storage/1234.mp4',
),
act: (bloc) => bloc.add(DownloadRequested('mp4')),
verify: (_) {
verify(
() => downloadRepository.downloadFile('1234.mp4', 'video/mp4'),
).called(1);
},
);
blocTest<DownloadBloc, DownloadState>(
'correctly downloads a gif',
setUp: () {
when(() => downloadRepository.downloadFile(any(), any()))
.thenAnswer((_) async {});
},
build: () => DownloadBloc(
downloadRepository: downloadRepository,
videoPath: 'https://storage/1234.mp4',
),
act: (bloc) => bloc.add(DownloadRequested('gif')),
verify: (_) {
verify(
() => downloadRepository.downloadFile('1234.gif', 'image/gif'),
).called(1);
},
);
});
});
}

View File

@ -0,0 +1,17 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:holobooth/share/share.dart';
void main() {
group('DownloadRequested', () {
test('can be instantiated', () {
expect(DownloadRequested(''), isNotNull);
expect(DownloadRequested.video().extension, equals('mp4'));
expect(DownloadRequested.gif().extension, equals('gif'));
});
test('supports equality', () {
expect(DownloadRequested(''), equals(DownloadRequested('')));
expect(DownloadRequested(''), isNot(equals(DownloadRequested('a'))));
});
});
}

View File

@ -0,0 +1,89 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:holobooth/share/share.dart';
void main() {
group('DownloadState', () {
test('can be instantiated', () {
expect(
DownloadState(
videoPath: '',
status: DownloadStatus.idle,
),
isNotNull,
);
});
test('supports equality', () {
expect(
DownloadState(
videoPath: '',
status: DownloadStatus.idle,
),
equals(
DownloadState(
videoPath: '',
status: DownloadStatus.idle,
),
),
);
expect(
DownloadState(
videoPath: '',
status: DownloadStatus.idle,
),
isNot(
equals(
DownloadState(
videoPath: 'a',
status: DownloadStatus.idle,
),
),
),
);
expect(
DownloadState(
videoPath: '',
status: DownloadStatus.idle,
),
isNot(
equals(
DownloadState(
videoPath: '',
status: DownloadStatus.fetching,
),
),
),
);
});
test('copyWith returns a new instance with the updated values', () {
expect(
DownloadState(
videoPath: '',
status: DownloadStatus.idle,
).copyWith(videoPath: 'a'),
equals(
DownloadState(
videoPath: 'a',
status: DownloadStatus.idle,
),
),
);
expect(
DownloadState(
videoPath: '',
status: DownloadStatus.idle,
).copyWith(status: DownloadStatus.fetching),
equals(
DownloadState(
videoPath: '',
status: DownloadStatus.fetching,
),
),
);
});
});
}

View File

@ -16,6 +16,9 @@ import '../../helpers/helpers.dart';
class _MockConvertBloc extends MockBloc<ConvertEvent, ConvertState>
implements ConvertBloc {}
class _MockDownloadBloc extends MockBloc<DownloadEvent, DownloadState>
implements DownloadBloc {}
class _MockUrlLauncher extends Mock
with MockPlatformInterfaceMixin
implements UrlLauncherPlatform {}
@ -25,11 +28,17 @@ void main() {
late Uint8List firstFrame;
late ConvertBloc convertBloc;
late DownloadBloc downloadBloc;
setUp(() async {
final image = await createTestImage(height: 10, width: 10);
final bytesImage = await image.toByteData(format: ImageByteFormat.png);
firstFrame = bytesImage!.buffer.asUint8List();
convertBloc = _MockConvertBloc();
downloadBloc = _MockDownloadBloc();
when(() => downloadBloc.state)
.thenReturn(const DownloadState.initial(videoPath: ''));
});
test('is routable', () {
@ -47,6 +56,7 @@ void main() {
group('ShareView', () {
late UrlLauncherPlatform mock;
late ConvertBloc convertBloc;
late DownloadBloc downloadBloc;
setUp(() {
mock = _MockUrlLauncher();
@ -58,6 +68,10 @@ void main() {
when(
() => mock.launchUrl(any(), any()),
).thenAnswer((_) async => true);
downloadBloc = _MockDownloadBloc();
when(() => downloadBloc.state)
.thenReturn(const DownloadState.initial(videoPath: ''));
});
setUpAll(() {
@ -67,7 +81,8 @@ void main() {
testWidgets('renders ShareBackground', (tester) async {
await tester.pumpSubject(
ShareView(),
convertBloc,
convertBloc: convertBloc,
downloadBloc: downloadBloc,
);
expect(find.byType(ShareBackground), findsOneWidget);
});
@ -75,7 +90,8 @@ void main() {
testWidgets('contains a ShareBody', (tester) async {
await tester.pumpSubject(
ShareView(),
convertBloc,
convertBloc: convertBloc,
downloadBloc: downloadBloc,
);
expect(find.byType(ShareBody), findsOneWidget);
});
@ -83,9 +99,17 @@ void main() {
}
extension on WidgetTester {
Future<void> pumpSubject(ShareView subject, ConvertBloc bloc) => pumpApp(
BlocProvider.value(
value: bloc,
Future<void> pumpSubject(
ShareView subject, {
required ConvertBloc convertBloc,
required DownloadBloc downloadBloc,
}) =>
pumpApp(
MultiBlocProvider(
providers: [
BlocProvider.value(value: convertBloc),
BlocProvider.value(value: downloadBloc),
],
child: subject,
),
);

View File

@ -1,12 +1,58 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:holobooth/convert/convert.dart';
import 'package:holobooth/share/share.dart';
import 'package:mocktail/mocktail.dart';
import '../../helpers/helpers.dart';
class _MockConvertBloc extends MockBloc<ConvertEvent, ConvertState>
implements ConvertBloc {}
class _MockDownloadBloc extends MockBloc<DownloadEvent, DownloadState>
implements DownloadBloc {}
void main() {
group('DownloadButton', () {
late ConvertBloc convertBloc;
late DownloadBloc downloadBloc;
setUp(() {
convertBloc = _MockConvertBloc();
when(() => convertBloc.state).thenReturn(
const ConvertState(videoPath: 'https://storage/videoPath.mp4'),
);
downloadBloc = _MockDownloadBloc();
when(() => downloadBloc.state).thenReturn(
const DownloadState.initial(videoPath: 'https://storage/videoPath.mp4'),
);
});
testWidgets('renders a loading indicator when loading', (tester) async {
when(() => downloadBloc.state).thenReturn(
const DownloadState(
videoPath: 'https://storage/videoPath.mp4',
status: DownloadStatus.fetching,
),
);
await tester.pumpSubject(
DownloadButton(),
convertBloc: convertBloc,
downloadBloc: downloadBloc,
);
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
testWidgets('opens DownloadOptionDialog on tap', (tester) async {
await tester.pumpApp(DownloadButton());
await tester.pumpSubject(
DownloadButton(),
convertBloc: convertBloc,
downloadBloc: downloadBloc,
);
await tester.tap(find.byType(DownloadButton));
await tester.pumpAndSettle();
expect(find.byType(DownloadOptionDialog), findsOneWidget);
@ -14,7 +60,11 @@ void main() {
testWidgets('closes DownloadOptionDialog tapping on DownloadAsAGifButton',
(tester) async {
await tester.pumpApp(DownloadButton());
await tester.pumpSubject(
DownloadButton(),
convertBloc: convertBloc,
downloadBloc: downloadBloc,
);
await tester.tap(find.byType(DownloadButton));
await tester.pumpAndSettle();
await tester.tap(find.byType(DownloadAsAGifButton));
@ -24,12 +74,64 @@ void main() {
testWidgets('closes DownloadOptionDialog tapping on DownloadAsAVideoButton',
(tester) async {
await tester.pumpApp(DownloadButton());
await tester.pumpSubject(
DownloadButton(),
convertBloc: convertBloc,
downloadBloc: downloadBloc,
);
await tester.tap(find.byType(DownloadButton));
await tester.pumpAndSettle();
await tester.tap(find.byType(DownloadAsAVideoButton));
await tester.pumpAndSettle();
expect(find.byType(DownloadOptionDialog), findsNothing);
});
testWidgets('downloads the gif when tapping on DownloadAsAGifButton',
(tester) async {
await tester.pumpSubject(
DownloadButton(),
convertBloc: convertBloc,
downloadBloc: downloadBloc,
);
await tester.tap(find.byType(DownloadButton));
await tester.pumpAndSettle();
await tester.tap(find.byType(DownloadAsAGifButton));
await tester.pumpAndSettle();
verify(() => downloadBloc.add(DownloadRequested('gif'))).called(1);
});
testWidgets('downloads the mp4 when tapping on DownloadAsAVideoButton',
(tester) async {
await tester.pumpSubject(
DownloadButton(),
convertBloc: convertBloc,
downloadBloc: downloadBloc,
);
await tester.tap(find.byType(DownloadButton));
await tester.pumpAndSettle();
await tester.tap(find.byType(DownloadAsAVideoButton));
await tester.pumpAndSettle();
verify(() => downloadBloc.add(DownloadRequested('mp4'))).called(1);
});
});
}
extension BuildSubect on WidgetTester {
Future<void> pumpSubject(
Widget subject, {
required ConvertBloc convertBloc,
required DownloadBloc downloadBloc,
}) async {
await pumpApp(
MultiBlocProvider(
providers: [
BlocProvider.value(value: convertBloc),
BlocProvider.value(value: downloadBloc),
],
child: subject,
),
);
}
}

View File

@ -15,9 +15,13 @@ import '../../helpers/helpers.dart';
class _MockConvertBloc extends MockBloc<ConvertEvent, ConvertState>
implements ConvertBloc {}
class _MockDownloadBloc extends MockBloc<DownloadEvent, DownloadState>
implements DownloadBloc {}
void main() {
group('ShareBody', () {
late ConvertBloc convertBloc;
late DownloadBloc downloadBloc;
late Uint8List thumbnail;
setUp(() async {
@ -27,13 +31,22 @@ void main() {
thumbnail = bytesImage!.buffer.asUint8List();
when(() => convertBloc.state)
.thenReturn(ConvertState(firstFrameProcessed: thumbnail));
downloadBloc = _MockDownloadBloc();
when(() => downloadBloc.state).thenReturn(
const DownloadState.initial(videoPath: ''),
);
});
testWidgets(
'renders SmallShareBody in small layout',
(WidgetTester tester) async {
tester.setSmallDisplaySize();
await tester.pumpSubject(ShareBody(), convertBloc);
await tester.pumpSubject(
ShareBody(),
convertBloc: convertBloc,
downloadBloc: downloadBloc,
);
expect(find.byType(SmallShareBody), findsOneWidget);
},
);
@ -42,18 +55,30 @@ void main() {
'renders LargeShareBody in large layout',
(WidgetTester tester) async {
tester.setLargeDisplaySize();
await tester.pumpSubject(ShareBody(), convertBloc);
await tester.pumpSubject(
ShareBody(),
convertBloc: convertBloc,
downloadBloc: downloadBloc,
);
expect(find.byType(LargeShareBody), findsOneWidget);
},
);
testWidgets('displays a ShareButton', (tester) async {
await tester.pumpSubject(ShareBody(), convertBloc);
await tester.pumpSubject(
ShareBody(),
convertBloc: convertBloc,
downloadBloc: downloadBloc,
);
expect(find.byType(ShareButton), findsOneWidget);
});
testWidgets('displays a DownloadButton', (tester) async {
await tester.pumpSubject(ShareBody(), convertBloc);
await tester.pumpSubject(
ShareBody(),
convertBloc: convertBloc,
downloadBloc: downloadBloc,
);
expect(
find.byType(DownloadButton),
findsOneWidget,
@ -61,7 +86,11 @@ void main() {
});
testWidgets('displays a RetakeButton', (tester) async {
await tester.pumpSubject(ShareBody(), convertBloc);
await tester.pumpSubject(
ShareBody(),
convertBloc: convertBloc,
downloadBloc: downloadBloc,
);
expect(
find.byType(RetakeButton),
findsOneWidget,
@ -71,7 +100,11 @@ void main() {
testWidgets(
'RetakeButton navigates to photobooth when pressed',
(tester) async {
await tester.pumpSubject(ShareBody(), convertBloc);
await tester.pumpSubject(
ShareBody(),
convertBloc: convertBloc,
downloadBloc: downloadBloc,
);
final finder = find.byType(RetakeButton);
await tester.ensureVisible(finder);
await tester.tap(finder);
@ -84,9 +117,17 @@ void main() {
}
extension on WidgetTester {
Future<void> pumpSubject(ShareBody subject, ConvertBloc bloc) => pumpApp(
Future<void> pumpSubject(
ShareBody subject, {
required ConvertBloc convertBloc,
required DownloadBloc downloadBloc,
}) =>
pumpApp(
MultiBlocProvider(
providers: [BlocProvider.value(value: bloc)],
providers: [
BlocProvider.value(value: convertBloc),
BlocProvider.value(value: downloadBloc),
],
child: SingleChildScrollView(child: subject),
),
);