feat: share in social (#349)

* chore: wip

* feat: twitter link

* test: repository

* test: coverage

* chore: comment

* chore: refactor

* chore: refactor

* Update lib/main_prod.dart

Co-authored-by: Erick <erickzanardoo@gmail.com>

* Update lib/main_prod.dart

Co-authored-by: Erick <erickzanardoo@gmail.com>

* feat: enable sharing just for developers

* chore: localized text

* chore: comments

* chore: wip

* chore: naming

* feat: share facebook

* chore: rename

* chore: format

* chore: typo

* chore: nit

Co-authored-by: Erick <erickzanardoo@gmail.com>
This commit is contained in:
Oscar
2023-01-16 16:51:56 +01:00
committed by GitHub
parent 88b6eeeba4
commit 2941bee640
24 changed files with 300 additions and 120 deletions

View File

@@ -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 }}"

View File

@@ -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 }}"

4
.vscode/launch.json vendored
View File

@@ -13,7 +13,9 @@
"-d",
"chrome",
"--web-renderer",
"canvaskit"
"canvaskit",
"--dart-define",
"SHARING_ENABLED=true"
]
},
{

View File

@@ -19,6 +19,8 @@ import 'package:holobooth_ui/holobooth_ui.dart';
Future<void> bootstrap({
required String convertUrl,
required FirebaseOptions firebaseOptions,
required String shareUrl,
required String assetBucketUrl,
}) async {
WidgetsFlutterBinding.ensureInitialized();
Bloc.observer = AppBlocObserver();
@@ -39,6 +41,8 @@ Future<void> bootstrap({
final avatarDetectorRepository = AvatarDetectorRepository();
final convertRepository = ConvertRepository(
url: convertUrl,
shareUrl: shareUrl,
assetBucketUrl: assetBucketUrl,
);
unawaited(

View File

@@ -33,6 +33,7 @@ class ConvertBloc extends Bloc<ConvertEvent, ConvertState> {
gifPath: result.gifUrl,
status: ConvertStatus.videoCreated,
firstFrameProcessed: result.firstFrame,
twitterShareUrl: result.twitterShareUrl,
),
);
} catch (error, stackTrace) {

View File

@@ -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
];
}

View File

@@ -36,16 +36,21 @@ class _ConvertFinishedState extends State<ConvertFinished>
try {
await loadAudio();
await playAudio();
} catch (_) {
} finally {
_finishConvert();
} catch (_) {}
}
}
void _finishConvert() {
final state = context.read<ConvertBloc>().state;
final convertBloc = context.read<ConvertBloc>();
final state = convertBloc.state;
Navigator.of(context).push(
SharePage.route(
videoPath: state.videoPath,
firstFrame: state.firstFrameProcessed ?? Uint8List.fromList([]),
convertBloc: convertBloc,
),
);
}

View File

@@ -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."
}
}

View File

@@ -3,8 +3,14 @@ import 'package:holobooth/firebase_options_dev.dart';
Future<void> 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,
);
}

View File

@@ -3,8 +3,13 @@ import 'package:holobooth/firebase_options_prod.dart';
Future<void> 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,
);
}

View File

@@ -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<ShareEvent, ShareState> {
ShareBloc({Uint8List? thumbnail, required String videoPath})
: super(

View File

@@ -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<void> 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(),
);
}

View File

@@ -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<ShareBloc>().state;
if (state.shareStatus.isSuccess &&
state.shareUrl == ShareUrl.facebook) {
Navigator.of(context).pop();
openLink(state.facebookShareUrl);
return;
}
context
.read<ShareBloc>()
.add(const ShareTapped(shareUrl: ShareUrl.facebook));
if (sharingEnabled) {
final facebookShareUrl =
context.read<ConvertBloc>().state.facebookShareUrl;
openLink(facebookShareUrl);
} else {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(l10n.sharingDisabled)));
}
Navigator.of(context).pop();
},
label: l10n.shareDialogFacebookButtonText,

View File

@@ -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<void>(
context: context,
child: BlocProvider.value(
value: context.read<ShareBloc>(),
child: MultiBlocProvider(
providers: [
BlocProvider.value(value: context.read<ShareBloc>()),
BlocProvider.value(value: context.read<ConvertBloc>()),
],
child: const ShareDialog(),
),
);

View File

@@ -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<ShareBloc>().state;
if (state.shareStatus.isSuccess && state.shareUrl == ShareUrl.twitter) {
Navigator.of(context).pop();
openLink(state.twitterShareUrl);
return;
if (sharingEnabled) {
final twitterShareUrl =
context.read<ConvertBloc>().state.twitterShareUrl;
openLink(twitterShareUrl);
} else {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(l10n.sharingDisabled)));
}
context
.read<ShareBloc>()
.add(const ShareTapped(shareUrl: ShareUrl.twitter));
Navigator.of(context).pop();
},
label: l10n.shareDialogTwitterButtonText,

View File

@@ -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 = <Uint8List>[];
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&quote=$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<String, dynamic>;
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');
}

View File

@@ -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,
);
}
}

View File

@@ -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);
});

View File

@@ -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));
});
});
}

View File

@@ -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<void> pumpSubject(
ConvertFinished subject,
ConvertBloc convertBloc,
) =>
pumpApp(
BlocProvider.value(
value: convertBloc,
child: subject,
),
);
}

View File

@@ -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<ShareEvent, ShareState>
implements ShareBloc {}
class _MockConvertBloc extends MockBloc<ConvertEvent, ConvertState>
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<AppPageRoute<void>>(),
);
});

View File

@@ -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<ShareEvent, ShareState>
implements ShareBloc {}
class _MockConvertBloc extends MockBloc<ConvertEvent, ConvertState>
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<void> pumpSubject(FacebookButton subject, ShareBloc bloc) => pumpApp(
Future<void> pumpSubject(
FacebookButton subject,
ConvertBloc bloc,
) =>
pumpApp(
MultiBlocProvider(
providers: [BlocProvider.value(value: bloc)],
child: subject,
child: Scaffold(body: subject),
),
);
}

View File

@@ -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<ShareEvent, ShareState>
implements ShareBloc {}
class _MockConvertBloc extends MockBloc<ConvertEvent, ConvertState>
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<void> pumpSubject(ShareButton subject, ShareBloc shareBloc) {
Future<void> 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,
),
),

View File

@@ -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<ShareEvent, ShareState>
implements ShareBloc {}
class _MockConvertBloc extends MockBloc<ConvertEvent, ConvertState>
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<void> pumpSubject(TwitterButton subject, ShareBloc bloc) => pumpApp(
Future<void> pumpSubject(
TwitterButton subject,
ConvertBloc bloc,
) =>
pumpApp(
MultiBlocProvider(
providers: [BlocProvider.value(value: bloc)],
child: subject,
child: Scaffold(body: subject),
),
);
}