mirror of
https://github.com/flutter/holobooth.git
synced 2026-03-13 10:30:50 +08:00
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:
2
.github/workflows/deploy_app_dev.yaml
vendored
2
.github/workflows/deploy_app_dev.yaml
vendored
@@ -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 }}"
|
||||
|
||||
2
.github/workflows/deploy_app_production.yaml
vendored
2
.github/workflows/deploy_app_production.yaml
vendored
@@ -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
4
.vscode/launch.json
vendored
@@ -13,7 +13,9 @@
|
||||
"-d",
|
||||
"chrome",
|
||||
"--web-renderer",
|
||||
"canvaskit"
|
||||
"canvaskit",
|
||||
"--dart-define",
|
||||
"SHARING_ENABLED=true"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"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<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');
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>>(),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user