diff --git a/.github/workflows/deploy_app_dev.yaml b/.github/workflows/deploy_app_dev.yaml index fd03a14a..89a9fe49 100644 --- a/.github/workflows/deploy_app_dev.yaml +++ b/.github/workflows/deploy_app_dev.yaml @@ -16,7 +16,7 @@ jobs: flutter-version: "3.6.0-0.1.pre" channel: "beta" - run: flutter packages get - - run: flutter build web --web-renderer canvaskit -t lib/main_dev.dart + - run: flutter build web --web-renderer canvaskit -t lib/main_dev.dart --dart-define SHARING_ENABLED=false - uses: FirebaseExtended/action-hosting-deploy@v0 with: repoToken: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/deploy_app_production.yaml b/.github/workflows/deploy_app_production.yaml index 8dcacb2b..e6399325 100644 --- a/.github/workflows/deploy_app_production.yaml +++ b/.github/workflows/deploy_app_production.yaml @@ -14,7 +14,7 @@ jobs: flutter-version: "3.6.0-0.1.pre" channel: "beta" - run: flutter packages get - - run: flutter build web --web-renderer canvaskit -t lib/main_prod.dart + - run: flutter build web --web-renderer canvaskit -t lib/main_prod.dart --dart-define SHARING_ENABLED=false - uses: FirebaseExtended/action-hosting-deploy@v0 with: repoToken: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.vscode/launch.json b/.vscode/launch.json index 47ade1f5..c0393e43 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -13,7 +13,9 @@ "-d", "chrome", "--web-renderer", - "canvaskit" + "canvaskit", + "--dart-define", + "SHARING_ENABLED=true" ] }, { diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index bf3f0803..78249aac 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -19,6 +19,8 @@ import 'package:holobooth_ui/holobooth_ui.dart'; Future bootstrap({ required String convertUrl, required FirebaseOptions firebaseOptions, + required String shareUrl, + required String assetBucketUrl, }) async { WidgetsFlutterBinding.ensureInitialized(); Bloc.observer = AppBlocObserver(); @@ -39,6 +41,8 @@ Future bootstrap({ final avatarDetectorRepository = AvatarDetectorRepository(); final convertRepository = ConvertRepository( url: convertUrl, + shareUrl: shareUrl, + assetBucketUrl: assetBucketUrl, ); unawaited( diff --git a/lib/convert/bloc/convert_bloc.dart b/lib/convert/bloc/convert_bloc.dart index 79b0c572..6bcf3954 100644 --- a/lib/convert/bloc/convert_bloc.dart +++ b/lib/convert/bloc/convert_bloc.dart @@ -33,6 +33,7 @@ class ConvertBloc extends Bloc { gifPath: result.gifUrl, status: ConvertStatus.videoCreated, firstFrameProcessed: result.firstFrame, + twitterShareUrl: result.twitterShareUrl, ), ); } catch (error, stackTrace) { diff --git a/lib/convert/bloc/convert_state.dart b/lib/convert/bloc/convert_state.dart index 1a25907f..8f48a3f2 100644 --- a/lib/convert/bloc/convert_state.dart +++ b/lib/convert/bloc/convert_state.dart @@ -6,24 +6,32 @@ class ConvertState extends Equatable { this.gifPath = '', this.status = ConvertStatus.creatingVideo, this.firstFrameProcessed, + this.twitterShareUrl = '', + this.facebookShareUrl = '', }); final String videoPath; final String gifPath; final ConvertStatus status; final Uint8List? firstFrameProcessed; + final String twitterShareUrl; + final String facebookShareUrl; ConvertState copyWith({ String? videoPath, String? gifPath, ConvertStatus? status, Uint8List? firstFrameProcessed, + String? twitterShareUrl, + String? facebookShareUrl, }) { return ConvertState( videoPath: videoPath ?? this.videoPath, gifPath: gifPath ?? this.gifPath, status: status ?? this.status, firstFrameProcessed: firstFrameProcessed ?? this.firstFrameProcessed, + twitterShareUrl: twitterShareUrl ?? this.twitterShareUrl, + facebookShareUrl: facebookShareUrl ?? this.facebookShareUrl, ); } @@ -33,6 +41,8 @@ class ConvertState extends Equatable { gifPath, status, firstFrameProcessed, + twitterShareUrl, + facebookShareUrl ]; } diff --git a/lib/convert/widgets/convert_finished.dart b/lib/convert/widgets/convert_finished.dart index e3fb0b6b..936f39dd 100644 --- a/lib/convert/widgets/convert_finished.dart +++ b/lib/convert/widgets/convert_finished.dart @@ -36,16 +36,21 @@ class _ConvertFinishedState extends State try { await loadAudio(); await playAudio(); + } catch (_) { + } finally { _finishConvert(); - } catch (_) {} + } } void _finishConvert() { - final state = context.read().state; + final convertBloc = context.read(); + final state = convertBloc.state; + Navigator.of(context).push( SharePage.route( videoPath: state.videoPath, firstFrame: state.firstFrameProcessed ?? Uint8List.fromList([]), + convertBloc: convertBloc, ), ); } diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index bc760207..cbb3d90e 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -177,5 +177,9 @@ "downloadOptionVideo": "Video Format", "@downloadOptionVideo": { "description": "Text shown to download as a video." + }, + "sharingDisabled": "Sharing disabled", + "@sharingDisabled": { + "description": "Text displayed when trying to share and it is disabled." } } diff --git a/lib/main_dev.dart b/lib/main_dev.dart index 39cc177f..9e2c3e5b 100644 --- a/lib/main_dev.dart +++ b/lib/main_dev.dart @@ -3,8 +3,14 @@ import 'package:holobooth/firebase_options_dev.dart'; Future main() async { const convertUrl = 'https://convert-it4sycsdja-uc.a.run.app'; + const shareUrl = + 'https://96e0700d-fe2c-4609-b43c-87093e447b75.web.app/share/'; + const assetBucketUrl = + 'https://storage.googleapis.com/io-photobooth-dev.appspot.com/uploads/'; await bootstrap( convertUrl: convertUrl, firebaseOptions: DefaultFirebaseOptions.currentPlatform, + shareUrl: shareUrl, + assetBucketUrl: assetBucketUrl, ); } diff --git a/lib/main_prod.dart b/lib/main_prod.dart index c2791dc1..59a4af6b 100644 --- a/lib/main_prod.dart +++ b/lib/main_prod.dart @@ -3,8 +3,13 @@ import 'package:holobooth/firebase_options_prod.dart'; Future main() async { const convertUrl = 'https://convert-fge4q4vwia-uc.a.run.app'; + const shareUrl = 'https://holobooth.flutter.dev/share/'; + const assetBucketUrl = + 'https://storage.googleapis.com/holobooth-prod.appspot.com/uploads/'; await bootstrap( convertUrl: convertUrl, firebaseOptions: DefaultFirebaseOptions.currentPlatform, + shareUrl: shareUrl, + assetBucketUrl: assetBucketUrl, ); } diff --git a/lib/share/bloc/share_bloc.dart b/lib/share/bloc/share_bloc.dart index 6bd04710..1d62a917 100644 --- a/lib/share/bloc/share_bloc.dart +++ b/lib/share/bloc/share_bloc.dart @@ -6,6 +6,7 @@ import 'package:equatable/equatable.dart'; part 'share_event.dart'; part 'share_state.dart'; +// TODO(oscar): to be deleted in next PR. class ShareBloc extends Bloc { ShareBloc({Uint8List? thumbnail, required String videoPath}) : super( diff --git a/lib/share/view/share_page.dart b/lib/share/view/share_page.dart index ae4883cc..877f5a59 100644 --- a/lib/share/view/share_page.dart +++ b/lib/share/view/share_page.dart @@ -2,6 +2,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:holobooth/convert/convert.dart'; import 'package:holobooth/footer/footer.dart'; import 'package:holobooth/share/share.dart'; import 'package:holobooth_ui/holobooth_ui.dart'; @@ -11,29 +12,38 @@ class SharePage extends StatelessWidget { super.key, required this.firstFrame, required this.videoPath, + required this.convertBloc, }); final Uint8List firstFrame; final String videoPath; + final ConvertBloc convertBloc; static Route route({ required Uint8List firstFrame, required String videoPath, + required ConvertBloc convertBloc, }) => AppPageRoute( builder: (_) => SharePage( firstFrame: firstFrame, videoPath: videoPath, + convertBloc: convertBloc, ), ); @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => ShareBloc( - thumbnail: firstFrame, - videoPath: videoPath, - ), + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => ShareBloc( + thumbnail: firstFrame, + videoPath: videoPath, + ), + ), + BlocProvider.value(value: convertBloc), + ], child: const ShareView(), ); } diff --git a/lib/share/widgets/facebook_button.dart b/lib/share/widgets/facebook_button.dart index 99a44365..4830d806 100644 --- a/lib/share/widgets/facebook_button.dart +++ b/lib/share/widgets/facebook_button.dart @@ -1,29 +1,32 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:holobooth/assets/assets.dart'; +import 'package:holobooth/convert/convert.dart'; import 'package:holobooth/l10n/l10n.dart'; -import 'package:holobooth/share/share.dart'; import 'package:holobooth_ui/holobooth_ui.dart'; class FacebookButton extends StatelessWidget { - const FacebookButton({super.key}); + const FacebookButton({ + super.key, + this.sharingEnabled = const bool.fromEnvironment('SHARING_ENABLED'), + }); + + final bool sharingEnabled; @override Widget build(BuildContext context) { final l10n = context.l10n; return GradientOutlinedButton( onPressed: () { - final state = context.read().state; - if (state.shareStatus.isSuccess && - state.shareUrl == ShareUrl.facebook) { - Navigator.of(context).pop(); - openLink(state.facebookShareUrl); - return; - } - context - .read() - .add(const ShareTapped(shareUrl: ShareUrl.facebook)); + if (sharingEnabled) { + final facebookShareUrl = + context.read().state.facebookShareUrl; + openLink(facebookShareUrl); + } else { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(l10n.sharingDisabled))); + } Navigator.of(context).pop(); }, label: l10n.shareDialogFacebookButtonText, diff --git a/lib/share/widgets/share_button.dart b/lib/share/widgets/share_button.dart index 7460bb3a..f16e8509 100644 --- a/lib/share/widgets/share_button.dart +++ b/lib/share/widgets/share_button.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:holobooth/convert/convert.dart'; import 'package:holobooth/l10n/l10n.dart'; import 'package:holobooth/share/share.dart'; import 'package:holobooth_ui/holobooth_ui.dart'; @@ -15,8 +16,11 @@ class ShareButton extends StatelessWidget { onPressed: () async { await showAppDialog( context: context, - child: BlocProvider.value( - value: context.read(), + child: MultiBlocProvider( + providers: [ + BlocProvider.value(value: context.read()), + BlocProvider.value(value: context.read()), + ], child: const ShareDialog(), ), ); diff --git a/lib/share/widgets/twitter_button.dart b/lib/share/widgets/twitter_button.dart index c3725a31..8a73353a 100644 --- a/lib/share/widgets/twitter_button.dart +++ b/lib/share/widgets/twitter_button.dart @@ -1,29 +1,34 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:holobooth/assets/assets.dart'; +import 'package:holobooth/convert/convert.dart'; import 'package:holobooth/l10n/l10n.dart'; -import 'package:holobooth/share/share.dart'; import 'package:holobooth_ui/holobooth_ui.dart'; class TwitterButton extends StatelessWidget { - const TwitterButton({super.key}); + const TwitterButton({ + super.key, + this.sharingEnabled = const bool.fromEnvironment( + 'SHARING_ENABLED', + ), + }); + + final bool sharingEnabled; @override Widget build(BuildContext context) { final l10n = context.l10n; return GradientOutlinedButton( onPressed: () { - final state = context.read().state; - if (state.shareStatus.isSuccess && state.shareUrl == ShareUrl.twitter) { - Navigator.of(context).pop(); - openLink(state.twitterShareUrl); - return; + if (sharingEnabled) { + final twitterShareUrl = + context.read().state.twitterShareUrl; + + openLink(twitterShareUrl); + } else { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(l10n.sharingDisabled))); } - - context - .read() - .add(const ShareTapped(shareUrl: ShareUrl.twitter)); - Navigator.of(context).pop(); }, label: l10n.shareDialogTwitterButtonText, diff --git a/packages/convert_repository/lib/src/convert_repository.dart b/packages/convert_repository/lib/src/convert_repository.dart index e7a3658a..a53b149a 100644 --- a/packages/convert_repository/lib/src/convert_repository.dart +++ b/packages/convert_repository/lib/src/convert_repository.dart @@ -12,8 +12,11 @@ class ConvertRepository { /// {@macro convert_repository} ConvertRepository({ required String url, + required String shareUrl, + required String assetBucketUrl, MultipartRequest Function()? multipartRequestBuilder, - }) { + }) : _shareUrl = shareUrl, + _assetBucketUrl = assetBucketUrl { _multipartRequestBuilder = multipartRequestBuilder ?? () => MultipartRequest('POST', Uri.parse(url)); } @@ -21,6 +24,8 @@ class ConvertRepository { late final MultipartRequest Function() _multipartRequestBuilder; final _processedFrames = []; + final String _shareUrl; + final String _assetBucketUrl; /// 16 is the minimum amount of time that you can delay /// an operation on a web browser. @@ -50,6 +55,20 @@ class ConvertRepository { return _processedFrames; } + String _getShareUrl(String fullPath) { + // TODO(OSCAR): We could do the parsing on the cloud function + final assetName = fullPath.replaceAll(_assetBucketUrl, ''); + return _shareUrl + assetName; + } + + String _getTwitterShareUrl(String shareUrl, String shareText) { + return 'https://twitter.com/intent/tweet?url=$shareUrl&text=$shareText'; + } + + String _getFacebookShareUrl(String shareUrl, String shareText) { + return 'https://www.facebook.com/sharer.php?u=$shareUrl"e=$shareText'; + } + /// Converts a list of images to video using firebase functions. /// /// On success, returns the video path from the cloud storage. @@ -80,10 +99,16 @@ class ConvertRepository { if (response.statusCode == 200) { final rawData = await response.stream.bytesToString(); final json = jsonDecode(rawData) as Map; - return GenerateVideoResponse.fromJson( + final videoResponse = GenerateVideoResponse.fromJson( json, frames.first, ); + final shareUrl = _getShareUrl(videoResponse.gifUrl); + final shareText = Uri.encodeComponent('Hey from Social Media!'); + return videoResponse.copyWith( + twitterShareUrl: _getTwitterShareUrl(shareUrl, shareText), + facebookShareUrl: _getFacebookShareUrl(shareUrl, shareText), + ); } else { throw const GenerateVideoException('Failed to convert frames'); } diff --git a/packages/convert_repository/lib/src/model/generate_video_response.dart b/packages/convert_repository/lib/src/model/generate_video_response.dart index 760a350f..1b9be5c0 100644 --- a/packages/convert_repository/lib/src/model/generate_video_response.dart +++ b/packages/convert_repository/lib/src/model/generate_video_response.dart @@ -9,6 +9,8 @@ class GenerateVideoResponse { required this.videoUrl, required this.gifUrl, required this.firstFrame, + this.twitterShareUrl = '', + this.facebookShareUrl = '', }); /// {@macro generate_video_response} @@ -31,4 +33,27 @@ class GenerateVideoResponse { /// First frame of the video generated. final Uint8List firstFrame; + + /// Twitter share url. + final String twitterShareUrl; + + /// Facebook share url. + final String facebookShareUrl; + + /// CopyWith + GenerateVideoResponse copyWith({ + String? videoUrl, + String? gifUrl, + Uint8List? firstFrame, + String? twitterShareUrl, + String? facebookShareUrl, + }) { + return GenerateVideoResponse( + videoUrl: videoUrl ?? this.videoUrl, + gifUrl: gifUrl ?? this.gifUrl, + firstFrame: firstFrame ?? this.firstFrame, + twitterShareUrl: twitterShareUrl ?? this.twitterShareUrl, + facebookShareUrl: facebookShareUrl ?? this.facebookShareUrl, + ); + } } diff --git a/packages/convert_repository/test/src/convert_repository_test.dart b/packages/convert_repository/test/src/convert_repository_test.dart index c19de8a8..d33a8097 100644 --- a/packages/convert_repository/test/src/convert_repository_test.dart +++ b/packages/convert_repository/test/src/convert_repository_test.dart @@ -25,6 +25,8 @@ void main() { convertRepository = ConvertRepository( multipartRequestBuilder: () => multipartRequest, url: '', + assetBucketUrl: '', + shareUrl: '', ); streamedResponse = _MockStreamedResponse(); @@ -43,12 +45,23 @@ void main() { }); test('can be instantiated', () { - expect(ConvertRepository(url: ''), isNotNull); + expect( + ConvertRepository( + url: '', + assetBucketUrl: '', + shareUrl: '', + ), + isNotNull, + ); }); group('generateVideo', () { test('throws exception if multipartRequest not set up', () { - final repository = ConvertRepository(url: 'url'); + final repository = ConvertRepository( + url: 'url', + assetBucketUrl: '', + shareUrl: '', + ); expect(repository.generateVideo(frames), throwsException); }); diff --git a/packages/convert_repository/test/src/model/generate_video_response_test.dart b/packages/convert_repository/test/src/model/generate_video_response_test.dart new file mode 100644 index 00000000..95f5c088 --- /dev/null +++ b/packages/convert_repository/test/src/model/generate_video_response_test.dart @@ -0,0 +1,20 @@ +import 'dart:typed_data'; + +import 'package:convert_repository/convert_repository.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('GenerateVideoResponse', () { + test('copyWith', () { + final response1 = GenerateVideoResponse( + videoUrl: 'videoUrl', + gifUrl: 'gifUrl', + firstFrame: Uint8List(1), + ); + final response2 = response1.copyWith(videoUrl: 'videoUrl2'); + expect(response1.gifUrl, equals(response2.gifUrl)); + expect(response1.firstFrame, equals(response2.firstFrame)); + expect(response1.videoUrl, isNot(response2.videoUrl)); + }); + }); +} diff --git a/test/convert/widgets/convert_finished_test.dart b/test/convert/widgets/convert_finished_test.dart index 11b85fa9..8590d536 100644 --- a/test/convert/widgets/convert_finished_test.dart +++ b/test/convert/widgets/convert_finished_test.dart @@ -24,6 +24,11 @@ void main() { setUp(() { convertBloc = _MockConvertBloc(); + when(() => convertBloc.state).thenReturn( + ConvertState( + firstFrameProcessed: Uint8List.fromList(transparentImage), + ), + ); audioPlayer = _MockAudioPlayer(); when(() => audioPlayer.setAsset(any())).thenAnswer((_) async => null); when(() => audioPlayer.play()).thenAnswer((_) async {}); @@ -56,25 +61,15 @@ void main() { testWidgets( 'set asset correctly', (WidgetTester tester) async { - await tester.pumpApp( - ConvertFinished( - dimension: 300, - ), - ); + await tester.pumpSubject(ConvertFinished(dimension: 300), convertBloc); + await tester.pumpAndSettle(); verify(() => audioPlayer.setAsset(any())).called(1); }, ); testWidgets('renders correctly with loading finish image', (tester) async { - await tester.pumpWidget( - const MaterialApp( - home: ConvertFinished( - dimension: 300, - ), - ), - ); - + await tester.pumpSubject(ConvertFinished(dimension: 300), convertBloc); expect( find.byWidgetPredicate( (widget) => @@ -90,22 +85,23 @@ void main() { testWidgets( 'after the play sound, navigates to SharePage', (WidgetTester tester) async { - when(() => convertBloc.state).thenReturn( - ConvertState( - firstFrameProcessed: Uint8List.fromList(transparentImage), - ), - ); - await tester.pumpApp( - BlocProvider.value( - value: convertBloc, - child: ConvertFinished( - dimension: 300, - ), - ), - ); + await tester.pumpSubject(ConvertFinished(dimension: 300), convertBloc); await tester.pumpAndSettle(); expect(find.byType(SharePage), findsOneWidget); }, ); }); } + +extension on WidgetTester { + Future pumpSubject( + ConvertFinished subject, + ConvertBloc convertBloc, + ) => + pumpApp( + BlocProvider.value( + value: convertBloc, + child: subject, + ), + ); +} diff --git a/test/share/view/share_page_test.dart b/test/share/view/share_page_test.dart index 74d11833..2b948e35 100644 --- a/test/share/view/share_page_test.dart +++ b/test/share/view/share_page_test.dart @@ -3,6 +3,7 @@ import 'dart:typed_data'; import 'package:bloc_test/bloc_test.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:holobooth_ui/holobooth_ui.dart'; import 'package:mocktail/mocktail.dart'; @@ -14,6 +15,9 @@ import '../../helpers/helpers.dart'; class _MockShareBloc extends MockBloc implements ShareBloc {} +class _MockConvertBloc extends MockBloc + implements ConvertBloc {} + class _MockUrlLauncher extends Mock with MockPlatformInterfaceMixin implements UrlLauncherPlatform {} @@ -21,9 +25,17 @@ class _MockUrlLauncher extends Mock void main() { group('SharePage', () { final firstFrame = Uint8List.fromList(transparentImage); + late ConvertBloc convertBloc; + + setUp(() => convertBloc = _MockConvertBloc()); + test('is routable', () { expect( - SharePage.route(firstFrame: firstFrame, videoPath: ''), + SharePage.route( + firstFrame: firstFrame, + videoPath: '', + convertBloc: convertBloc, + ), isA>(), ); }); diff --git a/test/share/widgets/facebook_button_test.dart b/test/share/widgets/facebook_button_test.dart index ff70c5f0..09f8d16f 100644 --- a/test/share/widgets/facebook_button_test.dart +++ b/test/share/widgets/facebook_button_test.dart @@ -1,6 +1,8 @@ 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 'package:plugin_platform_interface/plugin_platform_interface.dart'; @@ -8,24 +10,27 @@ import 'package:url_launcher_platform_interface/url_launcher_platform_interface. import '../../helpers/helpers.dart'; -class _MockShareBloc extends MockBloc - implements ShareBloc {} +class _MockConvertBloc extends MockBloc + implements ConvertBloc {} class _MockUrlLauncher extends Mock with MockPlatformInterfaceMixin implements UrlLauncherPlatform {} void main() { - group('FacebookButton', () { - late ShareBloc shareBloc; - late UrlLauncherPlatform mock; + late ConvertBloc convertBloc; + late UrlLauncherPlatform mock; + group('FacebookButton', () { + const facebookUrl = 'https://facebook.com'; setUp(() { mock = _MockUrlLauncher(); UrlLauncherPlatform.instance = mock; - shareBloc = _MockShareBloc(); + convertBloc = _MockConvertBloc(); - when(() => shareBloc.state).thenReturn(ShareState()); + when(() => convertBloc.state).thenReturn( + ConvertState(facebookShareUrl: facebookUrl), + ); when(() => mock.canLaunch(any())).thenAnswer((_) async => true); when( () => mock.launchUrl(any(), any()), @@ -37,42 +42,45 @@ void main() { }); testWidgets('dismissed after tapping', (tester) async { - await tester.pumpSubject(FacebookButton(), shareBloc); + await tester.pumpSubject(FacebookButton(), convertBloc); await tester.tap(find.byType(FacebookButton)); await tester.pumpAndSettle(); expect(find.byType(FacebookButton), findsNothing); - verify( - () => shareBloc.add(const ShareTapped(shareUrl: ShareUrl.facebook)), - ).called(1); }); - testWidgets('opens link when sharing is successful', (tester) async { - when(() => shareBloc.state).thenReturn( - ShareState( - shareStatus: ShareStatus.success, - shareUrl: ShareUrl.facebook, - facebookShareUrl: 'https://facebook.com', - ), + testWidgets('opens link if sharing enabled', (tester) async { + await tester.pumpSubject( + FacebookButton(sharingEnabled: true), + convertBloc, ); - await tester.pumpSubject(FacebookButton(), shareBloc); await tester.tap(find.byType(FacebookButton)); await tester.pumpAndSettle(); verify( - () => mock.launchUrl('https://facebook.com', any()), + () => mock.launchUrl(facebookUrl, any()), ).called(1); expect(find.byType(FacebookButton), findsNothing); - verifyNever( - () => shareBloc.add(const ShareTapped(shareUrl: ShareUrl.facebook)), - ); + }); + + testWidgets('shows Snackbar if sharing disabled', (tester) async { + await tester.pumpSubject(FacebookButton(), convertBloc); + await tester.tap(find.byType(FacebookButton)); + await tester.pump(kThemeAnimationDuration); + await tester.pump(kThemeAnimationDuration); + + expect(find.byType(SnackBar), findsOneWidget); }); }); } extension on WidgetTester { - Future pumpSubject(FacebookButton subject, ShareBloc bloc) => pumpApp( + Future pumpSubject( + FacebookButton subject, + ConvertBloc bloc, + ) => + pumpApp( MultiBlocProvider( providers: [BlocProvider.value(value: bloc)], - child: subject, + child: Scaffold(body: subject), ), ); } diff --git a/test/share/widgets/share_button_test.dart b/test/share/widgets/share_button_test.dart index 0aae5bfc..6555c4dc 100644 --- a/test/share/widgets/share_button_test.dart +++ b/test/share/widgets/share_button_test.dart @@ -2,6 +2,7 @@ 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'; @@ -10,23 +11,28 @@ import '../../helpers/helpers.dart'; class _MockShareBloc extends MockBloc implements ShareBloc {} +class _MockConvertBloc extends MockBloc + implements ConvertBloc {} + void main() { group('ShareButton', () { late ShareBloc shareBloc; + late ConvertBloc convertBloc; setUp(() { shareBloc = _MockShareBloc(); when(() => shareBloc.state).thenReturn(ShareState()); + convertBloc = _MockConvertBloc(); + when(() => convertBloc.state).thenReturn(ConvertState()); }); testWidgets( 'opens ShareDialog on tap', (tester) async { final subject = ShareButton(); - await tester.pumpSubject(subject, shareBloc); + await tester.pumpSubject(subject, shareBloc, convertBloc); await tester.tap(find.byWidget(subject)); await tester.pumpAndSettle(); - expect(find.byType(ShareDialog), findsOneWidget); }, ); @@ -34,11 +40,18 @@ void main() { } extension on WidgetTester { - Future pumpSubject(ShareButton subject, ShareBloc shareBloc) { + Future pumpSubject( + ShareButton subject, + ShareBloc shareBloc, + ConvertBloc convertBloc, + ) { return pumpApp( Scaffold( - body: BlocProvider.value( - value: shareBloc, + body: MultiBlocProvider( + providers: [ + BlocProvider.value(value: shareBloc), + BlocProvider.value(value: convertBloc), + ], child: subject, ), ), diff --git a/test/share/widgets/twitter_button_test.dart b/test/share/widgets/twitter_button_test.dart index 8ff08ffd..3034b8d9 100644 --- a/test/share/widgets/twitter_button_test.dart +++ b/test/share/widgets/twitter_button_test.dart @@ -1,7 +1,8 @@ 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/share/bloc/share_bloc.dart'; +import 'package:holobooth/convert/convert.dart'; import 'package:holobooth/share/widgets/widgets.dart'; import 'package:mocktail/mocktail.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; @@ -9,24 +10,27 @@ import 'package:url_launcher_platform_interface/url_launcher_platform_interface. import '../../helpers/helpers.dart'; -class _MockShareBloc extends MockBloc - implements ShareBloc {} +class _MockConvertBloc extends MockBloc + implements ConvertBloc {} class _MockUrlLauncher extends Mock with MockPlatformInterfaceMixin implements UrlLauncherPlatform {} void main() { - late ShareBloc shareBloc; + late ConvertBloc convertBloc; late UrlLauncherPlatform mock; group('TwitterButton', () { + const twitterShareUrl = 'https://twitter.com'; setUp(() { mock = _MockUrlLauncher(); UrlLauncherPlatform.instance = mock; - shareBloc = _MockShareBloc(); + convertBloc = _MockConvertBloc(); - when(() => shareBloc.state).thenReturn(ShareState()); + when(() => convertBloc.state).thenReturn( + ConvertState(twitterShareUrl: twitterShareUrl), + ); when(() => mock.canLaunch(any())).thenAnswer((_) async => true); when( () => mock.launchUrl(any(), any()), @@ -38,41 +42,45 @@ void main() { }); testWidgets('dismissed after tapping', (tester) async { - await tester.pumpSubject(TwitterButton(), shareBloc); + await tester.pumpSubject(TwitterButton(), convertBloc); await tester.tap(find.byType(TwitterButton)); await tester.pumpAndSettle(); expect(find.byType(TwitterButton), findsNothing); - verify(() => shareBloc.add(const ShareTapped(shareUrl: ShareUrl.twitter))) - .called(1); }); - testWidgets('opens link when sharing is successful', (tester) async { - when(() => shareBloc.state).thenReturn( - ShareState( - shareStatus: ShareStatus.success, - shareUrl: ShareUrl.twitter, - twitterShareUrl: 'https://twitter.com', - ), + testWidgets('opens link if sharing enabled', (tester) async { + await tester.pumpSubject( + TwitterButton(sharingEnabled: true), + convertBloc, ); - await tester.pumpSubject(TwitterButton(), shareBloc); await tester.tap(find.byType(TwitterButton)); await tester.pumpAndSettle(); verify( - () => mock.launchUrl('https://twitter.com', any()), + () => mock.launchUrl(twitterShareUrl, any()), ).called(1); expect(find.byType(TwitterButton), findsNothing); - verifyNever( - () => shareBloc.add(const ShareTapped(shareUrl: ShareUrl.twitter)), - ); + }); + + testWidgets('shows Snackbar if sharing disabled', (tester) async { + await tester.pumpSubject(TwitterButton(), convertBloc); + await tester.tap(find.byType(TwitterButton)); + await tester.pump(kThemeAnimationDuration); + await tester.pump(kThemeAnimationDuration); + + expect(find.byType(SnackBar), findsOneWidget); }); }); } extension on WidgetTester { - Future pumpSubject(TwitterButton subject, ShareBloc bloc) => pumpApp( + Future pumpSubject( + TwitterButton subject, + ConvertBloc bloc, + ) => + pumpApp( MultiBlocProvider( providers: [BlocProvider.value(value: bloc)], - child: subject, + child: Scaffold(body: subject), ), ); }