only-bible-app v3.7.2+24
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.
64b2ff0
— Peter 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
|
-
|
|
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
|
-
|
|
344
|
+
// behavior is covered by test/verse_selection_integration_test.dart, which
|
|
355
|
-
|
|
345
|
+
// exercises it (and the tap -> action wiring around it) through real UI
|
|
356
|
-
|
|
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
|
-
|
|
380
|
+
// UpdateChapterAction is a trivial 2-line reducer, and its effect is
|
|
394
|
-
|
|
381
|
+
// already exercised end-to-end by the GoToChapterAction/NextChapterAction/
|
|
395
|
-
|
|
382
|
+
// etc. integration tests below - a standalone dispatch-and-assert test for
|
|
396
|
-
|
|
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
|
+
}
|