mirror of https://github.com/flutter/pinball.git
feat(pinball): added the footer widget with tests (#253)
* feat(pinball): added the footer widget with tests * chore: addressed review feedback * chore: refactor helper methods and mock classes for footer links testpull/272/head
parent
3991207d62
commit
2b0e8b98b8
@ -0,0 +1,77 @@
|
|||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pinball/l10n/l10n.dart';
|
||||||
|
import 'package:pinball/theme/theme.dart';
|
||||||
|
import 'package:pinball_ui/pinball_ui.dart';
|
||||||
|
|
||||||
|
/// {@template footer}
|
||||||
|
/// Footer widget with links to the main tech stack.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class Footer extends StatelessWidget {
|
||||||
|
/// {@macro footer}
|
||||||
|
const Footer({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(50, 0, 50, 32),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: const [
|
||||||
|
_MadeWithFlutterAndFirebase(),
|
||||||
|
_GoogleIO(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GoogleIO extends StatelessWidget {
|
||||||
|
const _GoogleIO({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return Text(
|
||||||
|
l10n.footerGoogleIOText,
|
||||||
|
style: theme.textTheme.bodyText1!.copyWith(color: AppColors.white),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MadeWithFlutterAndFirebase extends StatelessWidget {
|
||||||
|
const _MadeWithFlutterAndFirebase({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return RichText(
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
text: TextSpan(
|
||||||
|
text: l10n.footerMadeWithText,
|
||||||
|
style: theme.textTheme.bodyText1!.copyWith(color: AppColors.white),
|
||||||
|
children: <TextSpan>[
|
||||||
|
TextSpan(
|
||||||
|
text: l10n.footerFlutterLinkText,
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () => openLink('https://flutter.dev'),
|
||||||
|
style: const TextStyle(
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const TextSpan(text: ' & '),
|
||||||
|
TextSpan(
|
||||||
|
text: l10n.footerFirebaseLinkText,
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () => openLink('https://firebase.google.com'),
|
||||||
|
style: const TextStyle(
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,7 @@
|
|||||||
library pinball_ui;
|
library pinball_ui;
|
||||||
|
|
||||||
|
export 'package:url_launcher/url_launcher.dart';
|
||||||
|
export 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
|
||||||
|
|
||||||
export 'src/dialog/dialog.dart';
|
export 'src/dialog/dialog.dart';
|
||||||
|
export 'src/external_links/external_links.dart';
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
/// Opens the given [url] in a new tab of the host browser
|
||||||
|
Future<void> openLink(String url, {VoidCallback? onError}) async {
|
||||||
|
final uri = Uri.parse(url);
|
||||||
|
if (await canLaunchUrl(uri)) {
|
||||||
|
await launchUrl(uri);
|
||||||
|
} else if (onError != null) {
|
||||||
|
onError();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:pinball_ui/pinball_ui.dart';
|
||||||
|
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
|
||||||
|
|
||||||
|
class MockUrlLauncher extends Mock
|
||||||
|
with MockPlatformInterfaceMixin
|
||||||
|
implements UrlLauncherPlatform {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late UrlLauncherPlatform urlLauncher;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
urlLauncher = MockUrlLauncher();
|
||||||
|
UrlLauncherPlatform.instance = urlLauncher;
|
||||||
|
});
|
||||||
|
|
||||||
|
group('openLink', () {
|
||||||
|
test('launches the link', () async {
|
||||||
|
when(
|
||||||
|
() => urlLauncher.canLaunch(any()),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => true,
|
||||||
|
);
|
||||||
|
when(
|
||||||
|
() => urlLauncher.launch(
|
||||||
|
any(),
|
||||||
|
useSafariVC: any(named: 'useSafariVC'),
|
||||||
|
useWebView: any(named: 'useWebView'),
|
||||||
|
enableJavaScript: any(named: 'enableJavaScript'),
|
||||||
|
enableDomStorage: any(named: 'enableDomStorage'),
|
||||||
|
universalLinksOnly: any(named: 'universalLinksOnly'),
|
||||||
|
headers: any(named: 'headers'),
|
||||||
|
),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => true,
|
||||||
|
);
|
||||||
|
await openLink('uri');
|
||||||
|
verify(
|
||||||
|
() => urlLauncher.launch(
|
||||||
|
any(),
|
||||||
|
useSafariVC: any(named: 'useSafariVC'),
|
||||||
|
useWebView: any(named: 'useWebView'),
|
||||||
|
enableJavaScript: any(named: 'enableJavaScript'),
|
||||||
|
enableDomStorage: any(named: 'enableDomStorage'),
|
||||||
|
universalLinksOnly: any(named: 'universalLinksOnly'),
|
||||||
|
headers: any(named: 'headers'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('executes the onError callback when it cannot launch', () async {
|
||||||
|
var wasCalled = false;
|
||||||
|
when(
|
||||||
|
() => urlLauncher.canLaunch(any()),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => false,
|
||||||
|
);
|
||||||
|
when(
|
||||||
|
() => urlLauncher.launch(
|
||||||
|
any(),
|
||||||
|
useSafariVC: any(named: 'useSafariVC'),
|
||||||
|
useWebView: any(named: 'useWebView'),
|
||||||
|
enableJavaScript: any(named: 'enableJavaScript'),
|
||||||
|
enableDomStorage: any(named: 'enableDomStorage'),
|
||||||
|
universalLinksOnly: any(named: 'universalLinksOnly'),
|
||||||
|
headers: any(named: 'headers'),
|
||||||
|
),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => true,
|
||||||
|
);
|
||||||
|
await openLink(
|
||||||
|
'url',
|
||||||
|
onError: () {
|
||||||
|
wasCalled = true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await expectLater(wasCalled, isTrue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,100 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:pinball/footer/footer.dart';
|
||||||
|
import 'package:pinball_ui/pinball_ui.dart';
|
||||||
|
|
||||||
|
import '../helpers/helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Footer', () {
|
||||||
|
late UrlLauncherPlatform urlLauncher;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
urlLauncher = MockUrlLauncher();
|
||||||
|
UrlLauncherPlatform.instance = urlLauncher;
|
||||||
|
});
|
||||||
|
testWidgets('renders "Made with..." and "Google I/O"', (tester) async {
|
||||||
|
await tester.pumpApp(const Footer());
|
||||||
|
expect(find.text('Google I/O'), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
find.byWidgetPredicate(
|
||||||
|
(widget) =>
|
||||||
|
widget is RichText &&
|
||||||
|
widget.text.toPlainText() == 'Made with Flutter & Firebase',
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'tapping on "Flutter" opens the flutter website',
|
||||||
|
(tester) async {
|
||||||
|
when(() => urlLauncher.canLaunch(any())).thenAnswer((_) async => true);
|
||||||
|
when(
|
||||||
|
() => urlLauncher.launch(
|
||||||
|
any(),
|
||||||
|
useSafariVC: any(named: 'useSafariVC'),
|
||||||
|
useWebView: any(named: 'useWebView'),
|
||||||
|
enableJavaScript: any(named: 'enableJavaScript'),
|
||||||
|
enableDomStorage: any(named: 'enableDomStorage'),
|
||||||
|
universalLinksOnly: any(named: 'universalLinksOnly'),
|
||||||
|
headers: any(named: 'headers'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => true);
|
||||||
|
await tester.pumpApp(const Footer());
|
||||||
|
final flutterTextFinder = find.byWidgetPredicate(
|
||||||
|
(widget) => widget is RichText && tapTextSpan(widget, 'Flutter'),
|
||||||
|
);
|
||||||
|
await tester.tap(flutterTextFinder);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
verify(
|
||||||
|
() => urlLauncher.launch(
|
||||||
|
'https://flutter.dev',
|
||||||
|
useSafariVC: any(named: 'useSafariVC'),
|
||||||
|
useWebView: any(named: 'useWebView'),
|
||||||
|
enableJavaScript: any(named: 'enableJavaScript'),
|
||||||
|
enableDomStorage: any(named: 'enableDomStorage'),
|
||||||
|
universalLinksOnly: any(named: 'universalLinksOnly'),
|
||||||
|
headers: any(named: 'headers'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'tapping on "Firebase" opens the firebase website',
|
||||||
|
(tester) async {
|
||||||
|
when(() => urlLauncher.canLaunch(any())).thenAnswer((_) async => true);
|
||||||
|
when(
|
||||||
|
() => urlLauncher.launch(
|
||||||
|
any(),
|
||||||
|
useSafariVC: any(named: 'useSafariVC'),
|
||||||
|
useWebView: any(named: 'useWebView'),
|
||||||
|
enableJavaScript: any(named: 'enableJavaScript'),
|
||||||
|
enableDomStorage: any(named: 'enableDomStorage'),
|
||||||
|
universalLinksOnly: any(named: 'universalLinksOnly'),
|
||||||
|
headers: any(named: 'headers'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => true);
|
||||||
|
await tester.pumpApp(const Footer());
|
||||||
|
final firebaseTextFinder = find.byWidgetPredicate(
|
||||||
|
(widget) => widget is RichText && tapTextSpan(widget, 'Firebase'),
|
||||||
|
);
|
||||||
|
await tester.tap(firebaseTextFinder);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
verify(
|
||||||
|
() => urlLauncher.launch(
|
||||||
|
'https://firebase.google.com',
|
||||||
|
useSafariVC: any(named: 'useSafariVC'),
|
||||||
|
useWebView: any(named: 'useWebView'),
|
||||||
|
enableJavaScript: any(named: 'enableJavaScript'),
|
||||||
|
enableDomStorage: any(named: 'enableDomStorage'),
|
||||||
|
universalLinksOnly: any(named: 'universalLinksOnly'),
|
||||||
|
headers: any(named: 'headers'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
bool tapTextSpan(RichText richText, String text) {
|
||||||
|
final isTapped = !richText.text.visitChildren(
|
||||||
|
(visitor) => _findTextAndTap(visitor, text),
|
||||||
|
);
|
||||||
|
return isTapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _findTextAndTap(InlineSpan visitor, String text) {
|
||||||
|
if (visitor is TextSpan && visitor.text == text) {
|
||||||
|
(visitor.recognizer as TapGestureRecognizer?)?.onTap?.call();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
Loading…
Reference in new issue