@@ -113,8 +119,9 @@ void main() {
expect(tester.takeException(), isNull);
});
- testWidgets('renders a table with empty | cells (fill-in-the-blank)',
- (tester) async {
+ testWidgets('renders a table with empty | cells (fill-in-the-blank)', (
+ tester,
+ ) async {
// Real Time_with_God table shape: lots of empty cells for users to fill in.
await pump(tester, '''
@@ -144,8 +151,9 @@ void main() {
expect(tester.takeException(), isNull);
});
- testWidgets('renders a nested inside a (Time_with_God shape)',
- (tester) async {
+ testWidgets('renders a nested inside a (Time_with_God shape)', (
+ tester,
+ ) async {
// This is the exact shape from Time_with_God line 70: a table inside a
// list item inside a ul. It uses a before the nested table.
await pump(tester, '''
@@ -175,8 +183,9 @@ void main() {
expect(tester.takeException(), isNull);
});
- testWidgets('renders the real Time_with_God fixture end-to-end',
- (tester) async {
+ testWidgets('renders the real Time_with_God fixture end-to-end', (
+ tester,
+ ) async {
// Pumping the exact page content that was crashing in production.
// Critically:
// 1. enable semantics — on a real device the semantics pass walks the
@@ -190,8 +199,10 @@ void main() {
final semantics = tester.ensureSemantics();
try {
await tester.binding.setSurfaceSize(const Size(390, 844));
- final html = File('test/assets-en/html-en-main/Time_with_God.html')
- .readAsStringSync();
+ final html =
+ File(
+ 'test/assets-en/html-en-main/Time_with_God.html',
+ ).readAsStringSync();
await pump(tester, html);
expect(tester.takeException(), isNull);
} finally {
@@ -200,8 +211,9 @@ void main() {
}
});
- testWidgets('renders a table wrapped in the standard page chrome',
- (tester) async {
+ testWidgets('renders a table wrapped in the standard page chrome', (
+ tester,
+ ) async {
// Mimics the structure of a real Time With God page: headings,
// paragraphs, and a table together.
await pump(tester, '''
diff --git a/test/language_downloader_test.dart b/test/language_downloader_test.dart
index 331cb32..8aaba07 100644
--- a/test/language_downloader_test.dart
+++ b/test/language_downloader_test.dart
@@ -24,14 +24,15 @@ Uint8List createTestZip(Map files) {
/// Helper to set up a mock Dio response for a URL returning zip bytes.
void mockDioGet(MockDio dio, String url, Uint8List zipBytes) {
- when(() => dio.get>(
- url,
- options: any(named: 'options'),
- )).thenAnswer((_) async => Response(
- data: zipBytes.toList(),
- statusCode: 200,
- requestOptions: RequestOptions(path: url),
- ));
+ when(
+ () => dio.get>(url, options: any(named: 'options')),
+ ).thenAnswer(
+ (_) async => Response(
+ data: zipBytes.toList(),
+ statusCode: 200,
+ requestOptions: RequestOptions(path: url),
+ ),
+ );
}
void main() {
@@ -68,41 +69,53 @@ void main() {
expect(await downloader.isDownloaded('de'), true);
final contentsJson = fs.file(
- '/app-docs/assets-de/${Globals.getResourcesDir('de')}/structure/contents.json');
+ '/app-docs/assets-de/${Globals.getResourcesDir('de')}/structure/contents.json',
+ );
expect(await contentsJson.exists(), true);
expect(await contentsJson.readAsString(), '{"worksheets":[]}');
final indexHtml = fs.file(
- '/app-docs/assets-de/${Globals.getResourcesDir('de')}/index.html');
+ '/app-docs/assets-de/${Globals.getResourcesDir('de')}/index.html',
+ );
expect(await indexHtml.exists(), true);
final pdfFile = fs.file(
- '/app-docs/assets-de/${Globals.getPdfDir('de')}/test.pdf');
+ '/app-docs/assets-de/${Globals.getPdfDir('de')}/test.pdf',
+ );
expect(await pdfFile.exists(), true);
// No staging dir left behind
expect(await fs.directory('/app-docs/assets-de.staging').exists(), false);
});
- test('Network failure: nothing remains at pathFor and no staging leftover',
- () async {
- // HTML download succeeds but PDF download throws
- final htmlZip = createTestZip({
- '${Globals.getResourcesDir('fr')}/index.html': 'Bonjour',
- });
- mockDioGet(dio, Globals.getRemoteUrlHtml('fr'), htmlZip);
- when(() => dio.get>(
+ test(
+ 'Network failure: nothing remains at pathFor and no staging leftover',
+ () async {
+ // HTML download succeeds but PDF download throws
+ final htmlZip = createTestZip({
+ '${Globals.getResourcesDir('fr')}/index.html': 'Bonjour',
+ });
+ mockDioGet(dio, Globals.getRemoteUrlHtml('fr'), htmlZip);
+ when(
+ () => dio.get>(
Globals.getRemoteUrlPdf('fr'),
options: any(named: 'options'),
- )).thenThrow(DioException(
- requestOptions: RequestOptions(path: Globals.getRemoteUrlPdf('fr')),
- ));
+ ),
+ ).thenThrow(
+ DioException(
+ requestOptions: RequestOptions(path: Globals.getRemoteUrlPdf('fr')),
+ ),
+ );
- await expectLater(downloader.download('fr'), throwsA(isA()));
+ await expectLater(
+ downloader.download('fr'),
+ throwsA(isA()),
+ );
- expect(await downloader.isDownloaded('fr'), false);
- expect(await fs.directory('/app-docs/assets-fr.staging').exists(), false);
- });
+ expect(await downloader.isDownloaded('fr'), false);
+ expect(await fs.directory('/app-docs/assets-fr.staging').exists(), false);
+ },
+ );
test('Corrupted zip: cleanup same as network failure', () async {
// A zip header (PK\x03\x04) followed by garbage triggers a real decode error
@@ -132,22 +145,41 @@ void main() {
'${Globals.getResourcesDir('it')}/file.html': 'ciao',
});
mockDioGet(dio, Globals.getRemoteUrlHtml('it'), htmlZip);
- when(() => dio.get>(
- Globals.getRemoteUrlPdf('it'),
- options: any(named: 'options'),
- )).thenThrow(DioException(
- requestOptions: RequestOptions(path: Globals.getRemoteUrlPdf('it')),
- ));
+ when(
+ () => dio.get>(
+ Globals.getRemoteUrlPdf('it'),
+ options: any(named: 'options'),
+ ),
+ ).thenThrow(
+ DioException(
+ requestOptions: RequestOptions(path: Globals.getRemoteUrlPdf('it')),
+ ),
+ );
await expectLater(downloader.download('it'), throwsA(isA()));
// Prior data is intact
expect(await downloader.isDownloaded('it'), true);
expect(
- await fs.file('/app-docs/assets-it/existing.txt').readAsString(),
- 'precious');
+ await fs.file('/app-docs/assets-it/existing.txt').readAsString(),
+ 'precious',
+ );
});
+ test(
+ 'Killed while cleaning up the old version: the leftover is removed '
+ 'and the new version kept, so the next download can swap again',
+ () async {
+ await fs.file('/app-docs/assets-it/new.txt').create(recursive: true);
+ await fs.file('/app-docs/assets-it.old/old.txt').create(recursive: true);
+
+ await downloader.restoreInterruptedDownload('it');
+
+ expect(await fs.file('/app-docs/assets-it/new.txt').exists(), true);
+ expect(await fs.directory('/app-docs/assets-it.old').exists(), false);
+ },
+ );
+
test('Different languages download in parallel', () async {
final deHtmlGate = Completer();
final frHtmlGate = Completer();
@@ -167,10 +199,12 @@ void main() {
'${Globals.getPdfDir('fr')}/file.pdf': 'fr-pdf',
});
- when(() => dio.get>(
- Globals.getRemoteUrlHtml('de'),
- options: any(named: 'options'),
- )).thenAnswer((_) async {
+ when(
+ () => dio.get>(
+ Globals.getRemoteUrlHtml('de'),
+ options: any(named: 'options'),
+ ),
+ ).thenAnswer((_) async {
deHtmlInFlight = true;
await deHtmlGate.future;
return Response(
@@ -179,18 +213,24 @@ void main() {
requestOptions: RequestOptions(),
);
});
- when(() => dio.get>(
- Globals.getRemoteUrlPdf('de'),
- options: any(named: 'options'),
- )).thenAnswer((_) async => Response(
- data: pdfZip1.toList(),
- statusCode: 200,
- requestOptions: RequestOptions(),
- ));
- when(() => dio.get>(
- Globals.getRemoteUrlHtml('fr'),
- options: any(named: 'options'),
- )).thenAnswer((_) async {
+ when(
+ () => dio.get>(
+ Globals.getRemoteUrlPdf('de'),
+ options: any(named: 'options'),
+ ),
+ ).thenAnswer(
+ (_) async => Response(
+ data: pdfZip1.toList(),
+ statusCode: 200,
+ requestOptions: RequestOptions(),
+ ),
+ );
+ when(
+ () => dio.get>(
+ Globals.getRemoteUrlHtml('fr'),
+ options: any(named: 'options'),
+ ),
+ ).thenAnswer((_) async {
frHtmlInFlight = true;
await frHtmlGate.future;
return Response(
@@ -199,14 +239,18 @@ void main() {
requestOptions: RequestOptions(),
);
});
- when(() => dio.get>(
- Globals.getRemoteUrlPdf('fr'),
- options: any(named: 'options'),
- )).thenAnswer((_) async => Response(
- data: pdfZip2.toList(),
- statusCode: 200,
- requestOptions: RequestOptions(),
- ));
+ when(
+ () => dio.get>(
+ Globals.getRemoteUrlPdf('fr'),
+ options: any(named: 'options'),
+ ),
+ ).thenAnswer(
+ (_) async => Response(
+ data: pdfZip2.toList(),
+ statusCode: 200,
+ requestOptions: RequestOptions(),
+ ),
+ );
final f1 = downloader.download('de');
await Future.delayed(Duration.zero);
@@ -232,10 +276,12 @@ void main() {
'${Globals.getPdfDir('de')}/file.pdf': 'de-pdf',
});
- when(() => dio.get>(
- Globals.getRemoteUrlHtml('de'),
- options: any(named: 'options'),
- )).thenAnswer((_) async {
+ when(
+ () => dio.get>(
+ Globals.getRemoteUrlHtml('de'),
+ options: any(named: 'options'),
+ ),
+ ).thenAnswer((_) async {
htmlCallCount++;
await gate.future;
return Response(
@@ -244,14 +290,18 @@ void main() {
requestOptions: RequestOptions(),
);
});
- when(() => dio.get>(
- Globals.getRemoteUrlPdf('de'),
- options: any(named: 'options'),
- )).thenAnswer((_) async => Response(
- data: pdfZip.toList(),
- statusCode: 200,
- requestOptions: RequestOptions(),
- ));
+ when(
+ () => dio.get>(
+ Globals.getRemoteUrlPdf('de'),
+ options: any(named: 'options'),
+ ),
+ ).thenAnswer(
+ (_) async => Response(
+ data: pdfZip.toList(),
+ statusCode: 200,
+ requestOptions: RequestOptions(),
+ ),
+ );
final f1 = downloader.download('de');
await Future.delayed(Duration.zero);
@@ -264,88 +314,108 @@ void main() {
expect(htmlCallCount, 2);
});
- test('Only kMaxParallelZipDecodes archives are decoded at the same time',
- () async {
- // Every decode blocks on a gate so we can watch how many run at once
- var inFlight = 0;
- var maxInFlight = 0;
- final gates = >[];
- Future> gatedDecoder(Uint8List zipBytes) async {
- inFlight++;
- maxInFlight = max(maxInFlight, inFlight);
- final gate = Completer();
- gates.add(gate);
- await gate.future;
- inFlight--;
- return decodeZipEntries(zipBytes);
- }
-
- downloader = LanguageDownloaderImpl(
- root: root, dio: dio, fileSystem: fs, zipDecoder: gatedDecoder);
-
- const langCodes = ['de', 'fr', 'es'];
- for (final langCode in langCodes) {
- mockDioGet(dio, Globals.getRemoteUrlHtml(langCode),
- createTestZip({'${Globals.getResourcesDir('de')}/f.html': 'x'}));
- mockDioGet(dio, Globals.getRemoteUrlPdf(langCode),
- createTestZip({'${Globals.getPdfDir('de')}/f.pdf': 'x'}));
- }
-
- final downloads = [for (final code in langCodes) downloader.download(code)];
- var finished = false;
- unawaited(Future.wait(downloads).then((_) => finished = true));
-
- // Let all three downloads reach their first decode
- for (var i = 0; i < 5; i++) {
- await Future.delayed(Duration.zero);
- }
- expect(inFlight, kMaxParallelZipDecodes);
- expect(gates.length, kMaxParallelZipDecodes);
-
- // Now let them through one by one - a queued decode may only start
- // once a running one has finished
- for (var i = 0; !finished && i < 100; i++) {
- if (gates.isNotEmpty) gates.removeAt(0).complete();
- await Future.delayed(Duration.zero);
- }
- expect(finished, isTrue);
- expect(maxInFlight, kMaxParallelZipDecodes);
- for (final langCode in langCodes) {
- expect(await downloader.isDownloaded(langCode), true);
- }
- });
-
- test('Crash recovery: pre-seeded staging dir is wiped by next download',
- () async {
- // Simulate crashed prior run leaving a staging dir
- await fs
- .directory('/app-docs/assets-de.staging/leftover')
- .create(recursive: true);
- await fs
- .file('/app-docs/assets-de.staging/leftover/junk.txt')
- .writeAsString('crash-leftover');
-
- final htmlZip = createTestZip({
- '${Globals.getResourcesDir('de')}/fresh.html': 'Fresh',
- });
- final pdfZip = createTestZip({
- '${Globals.getPdfDir('de')}/fresh.pdf': 'fresh-pdf',
- });
-
- mockDioGet(dio, Globals.getRemoteUrlHtml('de'), htmlZip);
- mockDioGet(dio, Globals.getRemoteUrlPdf('de'), pdfZip);
-
- await downloader.download('de');
+ test(
+ 'Only kMaxParallelZipDecodes archives are decoded at the same time',
+ () async {
+ // Every decode blocks on a gate so we can watch how many run at once
+ var inFlight = 0;
+ var maxInFlight = 0;
+ final gates = >[];
+ Future> gatedDecoder(Uint8List zipBytes) async {
+ inFlight++;
+ maxInFlight = max(maxInFlight, inFlight);
+ final gate = Completer();
+ gates.add(gate);
+ await gate.future;
+ inFlight--;
+ return decodeZipEntries(zipBytes);
+ }
+
+ downloader = LanguageDownloaderImpl(
+ root: root,
+ dio: dio,
+ fileSystem: fs,
+ zipDecoder: gatedDecoder,
+ );
- // Old staging leftover is gone
- expect(
- await fs.file('/app-docs/assets-de/leftover/junk.txt').exists(), false);
- // Fresh content is there
- expect(
+ const langCodes = ['de', 'fr', 'es'];
+ for (final langCode in langCodes) {
+ mockDioGet(
+ dio,
+ Globals.getRemoteUrlHtml(langCode),
+ createTestZip({'${Globals.getResourcesDir('de')}/f.html': 'x'}),
+ );
+ mockDioGet(
+ dio,
+ Globals.getRemoteUrlPdf(langCode),
+ createTestZip({'${Globals.getPdfDir('de')}/f.pdf': 'x'}),
+ );
+ }
+
+ final downloads = [
+ for (final code in langCodes) downloader.download(code),
+ ];
+ var finished = false;
+ unawaited(Future.wait(downloads).then((_) => finished = true));
+
+ // Let all three downloads reach their first decode
+ for (var i = 0; i < 5; i++) {
+ await Future.delayed(Duration.zero);
+ }
+ expect(inFlight, kMaxParallelZipDecodes);
+ expect(gates.length, kMaxParallelZipDecodes);
+
+ // Now let them through one by one - a queued decode may only start
+ // once a running one has finished
+ for (var i = 0; !finished && i < 100; i++) {
+ if (gates.isNotEmpty) gates.removeAt(0).complete();
+ await Future.delayed(Duration.zero);
+ }
+ expect(finished, isTrue);
+ expect(maxInFlight, kMaxParallelZipDecodes);
+ for (final langCode in langCodes) {
+ expect(await downloader.isDownloaded(langCode), true);
+ }
+ },
+ );
+
+ test(
+ 'Crash recovery: pre-seeded staging dir is wiped by next download',
+ () async {
+ // Simulate crashed prior run leaving a staging dir
+ await fs
+ .directory('/app-docs/assets-de.staging/leftover')
+ .create(recursive: true);
+ await fs
+ .file('/app-docs/assets-de.staging/leftover/junk.txt')
+ .writeAsString('crash-leftover');
+
+ final htmlZip = createTestZip({
+ '${Globals.getResourcesDir('de')}/fresh.html': 'Fresh',
+ });
+ final pdfZip = createTestZip({
+ '${Globals.getPdfDir('de')}/fresh.pdf': 'fresh-pdf',
+ });
+
+ mockDioGet(dio, Globals.getRemoteUrlHtml('de'), htmlZip);
+ mockDioGet(dio, Globals.getRemoteUrlPdf('de'), pdfZip);
+
+ await downloader.download('de');
+
+ // Old staging leftover is gone
+ expect(
+ await fs.file('/app-docs/assets-de/leftover/junk.txt').exists(),
+ false,
+ );
+ // Fresh content is there
+ expect(
await fs
.file(
- '/app-docs/assets-de/${Globals.getResourcesDir('de')}/fresh.html')
+ '/app-docs/assets-de/${Globals.getResourcesDir('de')}/fresh.html',
+ )
.exists(),
- true);
- });
+ true,
+ );
+ },
+ );
}
diff --git a/test/language_selection_button_test.dart b/test/language_selection_button_test.dart
index cbc45d8..13701a7 100644
--- a/test/language_selection_button_test.dart
+++ b/test/language_selection_button_test.dart
@@ -20,24 +20,36 @@ class TestLanguagesButton extends ConsumerWidget {
@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 LanguagesButton uses
- // context.findAncestorWidgetOfExactType() to get current page
- home: const ViewPage('Healing', 'de'));
+ locale: ref.watch(appLanguageProvider).locale,
+ localizationsDelegates: AppLocalizations.localizationsDelegates,
+ supportedLocales: AppLocalizations.supportedLocales,
+ // we need ViewPage here because LanguagesButton uses
+ // context.findAncestorWidgetOfExactType() to get current page
+ home: const ViewPage('Healing', 'de'),
+ );
}
}
void main() {
- testWidgets('Test with only German downloaded, English as appLanguage',
- (WidgetTester tester) async {
- await tester.pumpWidget(ProviderScope(overrides: [
- appLanguageProvider.overrideWith(() => TestAppLanguage('en')),
- languageProvider.overrideWith2((languageCode) => TestLanguageController(
- downloadedLanguages: ['de'],
- pages: {'Healing': const Page('test', 'test', 'test', '1.0', null)}))
- ], child: const TestLanguagesButton()));
+ testWidgets('Test with only German downloaded, English as appLanguage', (
+ WidgetTester tester,
+ ) async {
+ await tester.pumpWidget(
+ ProviderScope(
+ overrides: [
+ appLanguageProvider.overrideWith(() => TestAppLanguage('en')),
+ languageProvider.overrideWith2(
+ (languageCode) => TestLanguageController(
+ downloadedLanguages: ['de'],
+ pages: {
+ 'Healing': const Page('test', 'test', 'test', '1.0', null),
+ },
+ ),
+ ),
+ ],
+ child: const TestLanguagesButton(),
+ ),
+ );
expect(find.byIcon(Icons.translate), findsOneWidget);
expect(find.text('German (de)'), findsNothing);
@@ -53,14 +65,25 @@ void main() {
expect(find.text('Manage languages'), findsOneWidget);
});
- testWidgets('Test with 5 languages downloaded, German as appLanguage',
- (WidgetTester tester) async {
- await tester.pumpWidget(ProviderScope(overrides: [
- appLanguageProvider.overrideWith(() => TestAppLanguage('de')),
- languageProvider.overrideWith2((languageCode) => TestLanguageController(
- downloadedLanguages: ['de', 'en', 'fr', 'es', 'ar'],
- pages: {'Healing': const Page('test', 'test', 'test', '1.0', null)}))
- ], child: const TestLanguagesButton()));
+ testWidgets('Test with 5 languages downloaded, German as appLanguage', (
+ WidgetTester tester,
+ ) async {
+ await tester.pumpWidget(
+ ProviderScope(
+ overrides: [
+ appLanguageProvider.overrideWith(() => TestAppLanguage('de')),
+ languageProvider.overrideWith2(
+ (languageCode) => TestLanguageController(
+ downloadedLanguages: ['de', 'en', 'fr', 'es', 'ar'],
+ pages: {
+ 'Healing': const Page('test', 'test', 'test', '1.0', null),
+ },
+ ),
+ ),
+ ],
+ child: const TestLanguagesButton(),
+ ),
+ );
expect(find.byIcon(Icons.translate), findsOneWidget);
expect(find.text('Deutsch (de)'), findsNothing);
@@ -79,7 +102,7 @@ void main() {
'Deutsch (de)',
'Englisch (en)',
'Französisch (fr)',
- 'Spanisch (es)'
+ 'Spanisch (es)',
];
List |