Skip to content

Commit

Permalink
Add proper tests
Browse files Browse the repository at this point in the history
  • Loading branch information
holybiber committed Jul 23, 2024
1 parent 0246e73 commit 4f45eac
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 25 deletions.
14 changes: 7 additions & 7 deletions lib/data/languages.dart
Original file line number Diff line number Diff line change
Expand Up @@ -219,16 +219,16 @@ class LanguageController extends FamilyNotifier<Language, String> {
if (file is File) {
pdfFiles.add(file.basename);
} else {
debugPrint("Found unexpected element $file in the PDF directory");
debugPrint('Found unexpected element $file in the PDF directory');
}
}
}

// Store everything in our data structures
for (var element in structure["worksheets"]) {
for (var element in structure['worksheets']) {
// TODO add error handling
pageIndex.add(element['page']);
String? pdfName; // Stores PDF file name if it is available
String? pdfName; // Stores PDF file name (full path) if it is available
if (element.containsKey('pdf') && pdfFiles.contains(element['pdf'])) {
pdfName = join(pdfPath, element['pdf']);
pdfFiles.remove(element['pdf']);
Expand All @@ -239,7 +239,7 @@ class LanguageController extends FamilyNotifier<Language, String> {

// Consistency checking...
if (pdfFiles.isNotEmpty) {
debugPrint("Found unexpected PDF file(s): $pdfFiles");
debugPrint('Found unexpected PDF file(s): $pdfFiles');
}
await _checkConsistency(fileSystem.directory(path), pages);

Expand All @@ -251,7 +251,7 @@ class LanguageController extends FamilyNotifier<Language, String> {
if (file is File) {
images[file.basename] = Image(file.basename);
} else {
debugPrint("Found unexpected element $file in files/ directory");
debugPrint('Found unexpected element $file in files/ directory');
}
}
}
Expand Down Expand Up @@ -348,9 +348,9 @@ class Page {
final String version;

/// Full path of the associated PDF file if it exists on the device
final String? pdfName;
final String? pdfPath;

const Page(this.name, this.title, this.fileName, this.version, this.pdfName);
const Page(this.name, this.title, this.fileName, this.version, this.pdfPath);
}

/// Holds properties of an image.
Expand Down
67 changes: 52 additions & 15 deletions lib/widgets/share_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,48 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:open_filex/open_filex.dart';
import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher.dart';

// TODO: Use build_runner with flutter_gen instead
const String openPdfImage = 'assets/file-document-outline.png';
const String sharePdfImage = 'assets/file-document-arrow-right-outline.png';
const String shareLinkImage = 'assets/link.png';

/// A class around all sharing functionality to enable testing
/// (Packages: share_plus, url_launcher, open_filex)
///
/// Other options for testing (dependency injection etc.) are hard to use here
/// because of the usage of context.findAncestorWidgetOfExactType
/// so this is probably the best way to do it
class ShareService {
/// Wraps Share.share() (package share_plus)
Future<void> share(String text) {
return Share.share(text);
}

/// Wraps Share.shareXFiles() (package share_plus)
///
/// Not using the same argument List<XFile> because that would make it
/// harder to verify that the function gets called with correct arguments
/// with mocktail
Future<ShareResult> shareFile(String path) {
return Share.shareXFiles([XFile(path)]);
}

/// Wraps launchUrl (package url_launcher)
Future<bool> launchUrl(Uri url) {
return launchUrl(url);
}

/// Wraps OpenFilex.open (package open_filex)
Future<OpenResult> open(String? filePath) {
return OpenFilex.open(filePath);
}
}

/// A provider for all sharing functionality to enable testing
final shareProvider = Provider<ShareService>((ref) {
return ShareService();
});

/// Share button in the top right corner of the main view.
/// Opens a dropdown with several sharing options.
Expand All @@ -23,8 +64,10 @@ class ShareButton extends ConsumerWidget {
final viewPage = context.findAncestorWidgetOfExactType<ViewPage>()!;
String currentPage = viewPage.page;
String currentLang = viewPage.langCode;
String url = 'https://www.4training.net/$currentPage/$currentLang';
String? pdfFile =
ref.watch(languageProvider(currentLang)).pages[currentPage]?.pdfName;
ref.watch(languageProvider(currentLang)).pages[currentPage]?.pdfPath;
final shareService = ref.watch(shareProvider);
// Color for the PDF-related entries (greyed out if PDF is not available)
Color pdfColor = (pdfFile != null)
? Theme.of(context).colorScheme.onSurface
Expand Down Expand Up @@ -56,14 +99,12 @@ class ShareButton extends ConsumerWidget {
dense: true,
title: Text(context.l10n.openPdf,
style: TextStyle(color: pdfColor)),
leading: ImageIcon(
const AssetImage(
"assets/file-document-outline.png"),
leading: ImageIcon(const AssetImage(openPdfImage),
color: pdfColor),
onTap: () async {
if (pdfFile != null) {
menuController.close();
var result = await OpenFilex.open(pdfFile);
var result = await shareService.open(pdfFile);
debugPrint(
'OpenResult: ${result.message}; ${result.type}');
} else {
Expand All @@ -79,14 +120,12 @@ class ShareButton extends ConsumerWidget {
dense: true,
title: Text(context.l10n.sharePdf,
style: TextStyle(color: pdfColor)),
leading: ImageIcon(
const AssetImage(
"assets/file-document-arrow-right-outline.png"),
leading: ImageIcon(const AssetImage(sharePdfImage),
color: pdfColor),
onTap: () async {
if (pdfFile != null) {
menuController.close();
await Share.shareXFiles([XFile(pdfFile)]);
unawaited(shareService.shareFile(pdfFile));
} else {
await showDialog(
context: context,
Expand All @@ -103,17 +142,15 @@ class ShareButton extends ConsumerWidget {
leading: const Icon(Icons.open_in_browser),
onTap: () async {
menuController.close();
unawaited(launchUrl(Uri.parse(
'https://www.4training.net/$currentPage/$currentLang')));
unawaited(shareService.launchUrl(Uri.parse(url)));
}),
ListTile(
dense: true,
title: Text(context.l10n.shareLink),
leading: const ImageIcon(AssetImage("assets/link.png")),
leading: const ImageIcon(AssetImage(shareLinkImage)),
onTap: () {
menuController.close();
Share.share(
'https://www.4training.net/$currentPage/$currentLang');
shareService.share(url);
},
),
])
Expand Down
3 changes: 1 addition & 2 deletions test/assets-de/html-de-main/structure/contents.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
"page": "God's_Story_(five_fingers)",
"title": "Gottes Geschichte (f\u00fcnf Finger)",
"filename": "Gottes_Geschichte_(f\u00fcnf_Finger).html",
"version": "2.1",
"pdf": "Gottes_Geschichte_(f\u00fcnf_Finger).pdf"
"version": "2.1"
},
{
"page": "Forgiving_Step_by_Step",
Expand Down
Binary file not shown.
1 change: 1 addition & 0 deletions test/globals_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ void main() {
'https://github.com/4training/pdf-de/archive/refs/heads/main.zip'));
expect(Globals.getAssetsDir('de'), equals('assets-de'));
expect(Globals.getResourcesDir('de'), equals('html-de-main'));
expect(Globals.getPdfDir('de'), equals('pdf-de-main'));
expect(
Globals.getCommitsSince('de', DateTime.utc(2023)),
equals(
Expand Down
7 changes: 6 additions & 1 deletion test/languages_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,11 @@ void main() {
expect(content, contains('src="data:image/png;base64,'));
// This should still be there as the image file is missing
expect(content, contains('src="files/Hand_5.png"'));
// PDF should be available
expect(deTest.state.pages['Forgiving_Step_by_Step']?.pdfPath,
equals('assets-de/pdf-de-main/Schritte_der_Vergebung.pdf'));
// This PDF is missing
expect(deTest.state.pages['MissingTest']?.pdfPath, isNull);

// Test Languages.getPageTitles()
expect(
Expand All @@ -314,7 +319,7 @@ void main() {
'Schritte der Vergebung',
'MissingTest'
]));
expect(deTest.state.sizeInKB, 84);
expect(deTest.state.sizeInKB, 163);
expect(deTest.state.path, equals('assets-de/html-de-main'));

// Test some error handling
Expand Down
185 changes: 185 additions & 0 deletions test/share_button_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import 'package:app4training/data/app_language.dart';
import 'package:app4training/data/languages.dart';
import 'package:app4training/l10n/l10n.dart';
import 'package:app4training/routes/view_page.dart';
import 'package:app4training/widgets/share_button.dart';
import 'package:flutter/material.dart' hide Page;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:open_filex/open_filex.dart';
import 'package:share_plus/share_plus.dart';

import 'app_language_test.dart';
import 'languages_test.dart';

// To simplify testing the ShareButton widget in different locales
class TestShareButton extends ConsumerWidget {
const TestShareButton({super.key});

@override
Widget build(BuildContext context, WidgetRef ref) {
return MaterialApp(
locale: ref.watch(appLanguageProvider).locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
// we need ViewPage here because ShareButton uses
// context.findAncestorWidgetOfExactType<ViewPage>() to get current page
home: const ViewPage('Healing', 'de'));
}
}

/// Find an ImageIcon with an AssetImage by the name of the asset
/// (from our asset/ folder)
Finder findAssetImageIcon(String assetName, [Color? color]) {
return find.byWidgetPredicate((Widget widget) =>
widget is ImageIcon &&
((color == null) || (widget.color == color)) &&
widget.image is AssetImage &&
(widget.image as AssetImage).assetName == assetName);
}

class MockShareService extends Mock implements ShareService {}

void main() {
testWidgets('Smoke test: open the share menu (English locale)',
(WidgetTester tester) async {
await tester.pumpWidget(ProviderScope(overrides: [
appLanguageProvider.overrideWith(() => TestAppLanguage('en')),
], child: const TestShareButton()));

expect(find.byIcon(Icons.share), findsOneWidget);
expect(find.text('Open PDF'), findsNothing);

await tester.tap(find.byType(ShareButton));
await tester.pump();

expect(find.text('Open PDF'), findsOneWidget);
expect(find.text('Share PDF'), findsOneWidget);
expect(find.text('Open in browser'), findsOneWidget);
expect(find.text('Share link'), findsOneWidget);

expect(find.byIcon(Icons.share), findsOneWidget);
expect(find.byIcon(Icons.open_in_browser), findsOneWidget);
expect(findAssetImageIcon(openPdfImage), findsOneWidget);
expect(findAssetImageIcon(sharePdfImage), findsOneWidget);
expect(findAssetImageIcon(shareLinkImage), findsOneWidget);
});

testWidgets('Test when PDFs are not available', (WidgetTester tester) async {
await tester.pumpWidget(ProviderScope(overrides: [
appLanguageProvider.overrideWith(() => TestAppLanguage('de'))
], child: const TestShareButton()));

expect(find.byIcon(Icons.share), findsOneWidget);
expect(find.text('PDF öffnen'), findsNothing);

await tester.tap(find.byType(ShareButton));
await tester.pump();

// Check also that list items are greyed out
final BuildContext context = tester.element(find.byType(ShareButton));
final disabledColor = Theme.of(context).disabledColor;

// Try to open PDF - should not work because PDF is missing
expect(find.text('PDF öffnen'), findsOneWidget);
await tester.tap(findAssetImageIcon(openPdfImage, disabledColor));
await tester.pumpAndSettle();
expect(find.byType(PdfNotAvailableDialog), findsOneWidget);
await tester.tap(find.text('Okay'));
await tester.pumpAndSettle();
expect(find.text('PDF öffnen'), findsNothing); // menu is closed again

await tester.tap(find.byType(ShareButton));
await tester.pump();

// Try to share PDF - should not work because PDF is missing
expect(findAssetImageIcon(sharePdfImage, disabledColor), findsOneWidget);
final sharePdfText = find.text('PDF teilen');
final textWidget = tester.widget<Text>(sharePdfText);
expect(textWidget.style?.color, equals(disabledColor));
await tester.tap(sharePdfText); // This time tap on the text
await tester.pumpAndSettle();
expect(find.byType(PdfNotAvailableDialog), findsOneWidget);
await tester.tap(find.text('Okay'));
await tester.pumpAndSettle();
expect(find.text('PDF teilen'), findsNothing); // menu is closed again
});

testWidgets('Test all sharing features', (WidgetTester tester) async {
const String testUrl = 'https://www.4training.net/Healing/de';
const String testPath = '/path/to/Healing.pdf';

final mockShareService = MockShareService();
when(() => mockShareService.share(any(that: isA<String>())))
.thenAnswer((_) async {});
when(() => mockShareService.launchUrl(Uri.parse(testUrl)))
.thenAnswer((_) async => true);
when(() => mockShareService.shareFile(any())).thenAnswer((_) async {
return const ShareResult('Success', ShareResultStatus.success);
});
when(() => mockShareService.open(any(that: isA<String>())))
.thenAnswer((_) async {
return OpenResult();
});

await tester.pumpWidget(ProviderScope(overrides: [
appLanguageProvider.overrideWith(() => TestAppLanguage('de')),
shareProvider.overrideWithValue(mockShareService),
languageProvider.overrideWith(() => TestLanguageController(
downloadedLanguages: [
'de'
],
pages: {
'Healing': const Page('test', 'test', 'test', '1.0', testPath)
}))
], child: const TestShareButton()));

await tester.tap(find.byType(ShareButton));
await tester.pump();

// Check that list items aren't greyed out
final BuildContext context = tester.element(find.byType(ShareButton));
final disabledColor = Theme.of(context).disabledColor;
final onSurfaceColor = Theme.of(context).colorScheme.onSurface;
expect(findAssetImageIcon(openPdfImage, onSurfaceColor), findsOneWidget);
expect(findAssetImageIcon(sharePdfImage, onSurfaceColor), findsOneWidget);
expect(findAssetImageIcon(sharePdfImage, disabledColor), findsNothing);

// Open PDF
expect(find.text('PDF öffnen'), findsOneWidget);
await tester.tap(findAssetImageIcon(openPdfImage));
await tester.pumpAndSettle();
verify(() => mockShareService.open(testPath)).called(1);
expect(find.text('PDF öffnen'), findsNothing);

await tester.tap(find.byType(ShareButton));
await tester.pump();

// Share PDF
await tester.tap(find.text('PDF teilen')); // This time tap on the text
await tester.pumpAndSettle();
verify(() => mockShareService.shareFile(testPath)).called(1);
expect(find.text('PDF teilen'), findsNothing);

await tester.tap(find.byType(ShareButton));
await tester.pump();

// Open in browser
expect(find.text('Im Browser öffnen'), findsOneWidget);
await tester.tap(find.byIcon(Icons.open_in_browser));
await tester.pumpAndSettle();
expect(find.byIcon(Icons.open_in_browser), findsNothing);
verify(() => mockShareService.launchUrl(Uri.parse(testUrl))).called(1);

await tester.tap(find.byType(ShareButton));
await tester.pump();

// Share link
expect(find.text('Link teilen'), findsOneWidget);
await tester.tap(findAssetImageIcon(shareLinkImage));
await tester.pump();
expect(findAssetImageIcon(shareLinkImage), findsNothing);
verify(() => mockShareService.share(testUrl)).called(1);
});
}

0 comments on commit 4f45eac

Please sign in to comment.