only-bible-app v3.7.2+24

#kotlin#android#ios

git clone https://git.pyrossh.dev/only-bible-app

The only bible app you will ever need. No ads. No in-app purchases. No distractions.


64b2ff0Peter John 2026-08-09T15:29:41+05:30
more improved testing
test/app_logic_test.dart CHANGED
@@ -340,22 +340,10 @@ void main() {
340
340
  });
341
341
  });
342
342
 
343
- group("RemoveHighlightAction", () {
344
- test("removes the highlight and clears the selection", () async {
343
+ // RemoveHighlightAction's "removes the highlight and clears the selection"
345
- final bible = buildTestBible();
346
- final verse = bible.bookAt(0)!.chapterAt(0)!.verseAt(0)!;
347
- final store = Store<AppState>(
348
- initialState: AppState(
349
- bible: bible,
350
- highlights: {"0:0:0": 1},
351
- selectedVerses: [verse],
352
- ),
353
- );
354
- await store.dispatchAndWait(RemoveHighlightAction([verse]));
344
+ // behavior is covered by test/verse_selection_integration_test.dart, which
355
- expect(store.state.highlights.containsKey("0:0:0"), isFalse);
345
+ // exercises it (and the tap -> action wiring around it) through real UI
356
- expect(store.state.selectedVerses, isEmpty);
346
+ // interaction rather than a bare dispatch.
357
- });
358
- });
359
347
 
360
348
  group("adjacentChapter", () {
361
349
  test("moves to the next chapter within the same book", () {
@@ -389,20 +377,17 @@ void main() {
389
377
  });
390
378
  });
391
379
 
392
- group("UpdateChapterAction", () {
393
- test("updates savedBook/savedChapter", () async {
380
+ // UpdateChapterAction is a trivial 2-line reducer, and its effect is
394
- final bible = buildTestBible();
381
+ // already exercised end-to-end by the GoToChapterAction/NextChapterAction/
395
- final store = Store<AppState>(
382
+ // etc. integration tests below - a standalone dispatch-and-assert test for
396
- initialState: AppState(bible: bible, savedBook: 0, savedChapter: 0),
383
+ // it added no signal beyond what those already cover.
397
- );
398
- await store.dispatchAndWait(UpdateChapterAction(1, 0));
399
- expect(store.state.savedBook, 1);
400
- expect(store.state.savedChapter, 0);
401
- });
402
- });
403
384
 
404
385
  group("SyncCurrentChapterAction", () {
405
386
  test("is a no-op when book/chapter already match", () async {
387
+ // Unlike the "updates state when they differ" half of this reducer -
388
+ // now covered end-to-end by the integration tests below - this checks
389
+ // object identity is preserved (avoids an unnecessary rebuild), which
390
+ // isn't something a UI-driven test can observe.
406
391
  final bible = buildTestBible();
407
392
  final store = Store<AppState>(
408
393
  initialState: AppState(bible: bible, savedBook: 1, savedChapter: 0),
@@ -411,16 +396,6 @@ void main() {
411
396
  await store.dispatchAndWait(SyncCurrentChapterAction(1, 0));
412
397
  expect(identical(store.state, stateBefore), isTrue);
413
398
  });
414
-
415
- test("updates state when book/chapter differ", () async {
416
- final bible = buildTestBible();
417
- final store = Store<AppState>(
418
- initialState: AppState(bible: bible, savedBook: 0, savedChapter: 0),
419
- );
420
- await store.dispatchAndWait(SyncCurrentChapterAction(1, 0));
421
- expect(store.state.savedBook, 1);
422
- expect(store.state.savedChapter, 0);
423
- });
424
399
  });
425
400
 
426
401
  // These used to be untestable here: they all call stopAudioPlayback(),
test/navigation_integration_test.dart ADDED
@@ -0,0 +1,135 @@
1
+ // Integration-style tests: these mount the real App widget and drive it
2
+ // through actual taps (find + tap real keys/text), rather than dispatching
3
+ // actions programmatically against a bare store/router. That distinction
4
+ // matters: an earlier bug (App._syncCurrentChapter clobbering
5
+ // savedBook/savedChapter right after a push()) went unnoticed by unit tests
6
+ // that dispatched actions directly, because they never mounted the widget
7
+ // tree that wires the router's listener up in the first place. Driving the
8
+ // same flows through the UI is what actually would have caught it.
9
+ import "package:flutter/material.dart";
10
+ import "package:flutter_test/flutter_test.dart";
11
+ import "package:async_redux/async_redux.dart";
12
+ import "package:only_bible_app/app.dart";
13
+ import "package:only_bible_app/store/app_state.dart";
14
+ import "package:only_bible_app/widgets/book_select_sheet.dart";
15
+ import "package:only_bible_app/widgets/book_tile.dart";
16
+ import "app_logic_test.dart" show buildTestBible;
17
+
18
+ /// Taps the [label] tile inside whichever [BookSelectSheet] is currently on
19
+ /// screen - scoped there (rather than a bare find.text(label)) because plain
20
+ /// text like a chapter number ("1", "2") can otherwise collide with the
21
+ /// current-chapter number shown in the app bar behind the sheet.
22
+ Future<void> tapSheetTile(WidgetTester tester, String label) async {
23
+ final sheet = find.byType(BookSelectSheet);
24
+ await tester.tap(find.descendant(of: sheet, matching: find.text(label)));
25
+ await tester.pumpAndSettle();
26
+ }
27
+
28
+ bool _isSelectedTile(Widget w, String label) =>
29
+ w is BookTile && w.label == label && w.isSelected;
30
+
31
+ void main() {
32
+ TestWidgetsFlutterBinding.ensureInitialized();
33
+
34
+ testWidgets(
35
+ "navigating via the book+chapter selector updates the displayed chapter, and "
36
+ "reopening the selector highlights that new book/chapter - not the one "
37
+ "navigated away from (regression: App._syncCurrentChapter used to clobber "
38
+ "savedBook/savedChapter right after the push, so the selector kept "
39
+ "highlighting the pre-navigation position)",
40
+ (tester) async {
41
+ final bible = buildTestBible();
42
+ final store = Store<AppState>(
43
+ initialState: AppState(bible: bible, savedBook: 0, savedChapter: 0),
44
+ );
45
+ await tester.pumpWidget(
46
+ App(globalNavigatorKey: GlobalKey<NavigatorState>(), store: store),
47
+ );
48
+ await tester.pumpAndSettle();
49
+
50
+ // Sanity check: Genesis 1 is on screen to start with. Verse text
51
+ // renders as rich text with a "1 " verse-number span prefixed, so this
52
+ // is a substring match rather than find.text's exact match.
53
+ expect(find.textContaining("In the beginning..."), findsOneWidget);
54
+
55
+ // Open the book selector via the app bar's book title.
56
+ await tester.tap(find.byKey(const Key("bookTitle")));
57
+ await tester.pumpAndSettle();
58
+
59
+ // Pick Revelation (a multi-chapter book, so this animates to the
60
+ // chapter grid rather than closing immediately).
61
+ await tapSheetTile(tester, "Rev");
62
+
63
+ // Pick chapter 2.
64
+ await tapSheetTile(tester, "2");
65
+
66
+ // The displayed chapter should now be Revelation 2.
67
+ expect(find.textContaining("Rev 2:1"), findsOneWidget);
68
+ expect(store.state.savedBook, 2);
69
+ expect(store.state.savedChapter, 1);
70
+
71
+ // Reopen the book selector - this is the exact regression.
72
+ await tester.tap(find.byKey(const Key("bookTitle")));
73
+ await tester.pumpAndSettle();
74
+
75
+ final sheet = find.byType(BookSelectSheet);
76
+ expect(
77
+ find.descendant(
78
+ of: sheet,
79
+ matching: find.byWidgetPredicate((w) => _isSelectedTile(w, "Rev")),
80
+ ),
81
+ findsOneWidget,
82
+ );
83
+ expect(
84
+ find.descendant(
85
+ of: sheet,
86
+ matching: find.byWidgetPredicate((w) => _isSelectedTile(w, "Gen")),
87
+ ),
88
+ findsNothing,
89
+ );
90
+ },
91
+ );
92
+
93
+ testWidgets(
94
+ "swiping to the next chapter after using the picker keeps the app bar's chapter number in sync",
95
+ (tester) async {
96
+ final bible = buildTestBible();
97
+ final store = Store<AppState>(
98
+ initialState: AppState(bible: bible, savedBook: 0, savedChapter: 0),
99
+ );
100
+ await tester.pumpWidget(
101
+ App(globalNavigatorKey: GlobalKey<NavigatorState>(), store: store),
102
+ );
103
+ await tester.pumpAndSettle();
104
+
105
+ // Jump to Revelation 1 via the picker (push).
106
+ await tester.tap(find.byKey(const Key("bookTitle")));
107
+ await tester.pumpAndSettle();
108
+ await tapSheetTile(tester, "Rev");
109
+ await tapSheetTile(tester, "1");
110
+ expect(find.textContaining("Rev 1:1"), findsOneWidget);
111
+
112
+ // Swipe left to advance to chapter 2 (pushReplacement on top of the
113
+ // still-2-entry stack the picker's push left behind).
114
+ await tester.fling(
115
+ find.byType(SingleChildScrollView),
116
+ const Offset(-400, 0),
117
+ 800,
118
+ );
119
+ await tester.pumpAndSettle();
120
+
121
+ expect(find.textContaining("Rev 2:1"), findsOneWidget);
122
+ expect(store.state.savedBook, 2);
123
+ expect(store.state.savedChapter, 1);
124
+ // The app bar's chapter number (built from the same live state) must
125
+ // agree with what's actually on screen.
126
+ expect(
127
+ find.descendant(
128
+ of: find.byKey(const Key("chapterTitle")),
129
+ matching: find.text("2"),
130
+ ),
131
+ findsOneWidget,
132
+ );
133
+ },
134
+ );
135
+ }
test/verse_selection_integration_test.dart ADDED
@@ -0,0 +1,65 @@
1
+ // Integration-style tests driving verse selection and highlighting through
2
+ // real taps on the rendered Home screen and MenuOverlay, rather than
3
+ // dispatching SelectVerseAction/SetHighlightAction/RemoveHighlightAction
4
+ // directly against a bare store. Tapping through the actual widgets is what
5
+ // verifies the UI wiring (GestureDetector -> action, MenuOverlay appearing
6
+ // only while verses are selected, etc.) alongside the reducer logic itself.
7
+ import "package:flutter/material.dart";
8
+ import "package:flutter_test/flutter_test.dart";
9
+ import "package:async_redux/async_redux.dart";
10
+ import "package:only_bible_app/app.dart";
11
+ import "package:only_bible_app/store/app_state.dart";
12
+ import "package:only_bible_app/widgets/highlight_button.dart";
13
+ import "package:only_bible_app/widgets/menu_overlay.dart";
14
+ import "app_logic_test.dart" show buildTestBible;
15
+
16
+ void main() {
17
+ TestWidgetsFlutterBinding.ensureInitialized();
18
+
19
+ testWidgets(
20
+ "tapping a verse selects it and shows the menu overlay; tapping a highlight color "
21
+ "applies it and clears the selection; tapping remove clears the highlight",
22
+ (tester) async {
23
+ final bible = buildTestBible();
24
+ final store = Store<AppState>(
25
+ initialState: AppState(bible: bible, savedBook: 0, savedChapter: 0),
26
+ );
27
+ await tester.pumpWidget(
28
+ App(globalNavigatorKey: GlobalKey<NavigatorState>(), store: store),
29
+ );
30
+ await tester.pumpAndSettle();
31
+
32
+ // No selection yet - the overlay shouldn't be showing.
33
+ expect(find.byType(MenuOverlay), findsNothing);
34
+
35
+ // Tap the first verse ("In the beginning...") to select it.
36
+ await tester.tap(find.textContaining("In the beginning..."));
37
+ await tester.pumpAndSettle();
38
+
39
+ expect(store.state.selectedVerses, hasLength(1));
40
+ expect(store.state.selectedVerses.single.index, 0);
41
+ expect(find.byType(MenuOverlay), findsOneWidget);
42
+
43
+ // Tap the first highlight color in the now-visible overlay.
44
+ await tester.tap(find.byType(HighlightButton).first);
45
+ await tester.pumpAndSettle();
46
+
47
+ expect(store.state.highlights["0:0:0"], 0);
48
+ // Applying a highlight clears the selection, so the overlay goes away.
49
+ expect(store.state.selectedVerses, isEmpty);
50
+ expect(find.byType(MenuOverlay), findsNothing);
51
+
52
+ // Select the now-highlighted verse again and remove its highlight.
53
+ await tester.tap(find.textContaining("In the beginning..."));
54
+ await tester.pumpAndSettle();
55
+ expect(find.byType(MenuOverlay), findsOneWidget);
56
+
57
+ await tester.tap(find.byIcon(Icons.cancel_outlined));
58
+ await tester.pumpAndSettle();
59
+
60
+ expect(store.state.highlights.containsKey("0:0:0"), isFalse);
61
+ expect(store.state.selectedVerses, isEmpty);
62
+ expect(find.byType(MenuOverlay), findsNothing);
63
+ },
64
+ );
65
+ }