diff --git a/app/test/keyboard_operability_test.dart b/app/test/keyboard_operability_test.dart index 88ba543..b8b0a7d 100644 --- a/app/test/keyboard_operability_test.dart +++ b/app/test/keyboard_operability_test.dart @@ -157,7 +157,7 @@ void main() { addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); - await LogStorageService.instance.writeLog({ + await _pumpHistoryScreen(tester, { '2026-07-30': [ const FoodEntry( id: 'has-id', @@ -173,14 +173,6 @@ void main() { ], }); - // HistoryScreen loads via a fire-and-forget Future in initState that the - // frame scheduler does not track, so settle inside runAsync. - await tester.runAsync(() async { - await tester.pumpWidget(const MaterialApp(home: HistoryScreen())); - await Future.delayed(const Duration(milliseconds: 50)); - await tester.pumpAndSettle(); - }); - // Delete used to be bound only to onLongPress, which has no keyboard // equivalent whatsoever. expect( @@ -198,7 +190,7 @@ void main() { addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); - await LogStorageService.instance.writeLog({ + await _pumpHistoryScreen(tester, { '2026-07-30': [ const FoodEntry( time: '2026-07-30T09:00:00+02:00', @@ -213,12 +205,6 @@ void main() { ], }); - await tester.runAsync(() async { - await tester.pumpWidget(const MaterialApp(home: HistoryScreen())); - await Future.delayed(const Duration(milliseconds: 50)); - await tester.pumpAndSettle(); - }); - // Delete stays id-only to avoid ambiguous time+desc matches, so the // control must be absent rather than present-and-failing. expect(find.byTooltip('Delete entry'), findsNothing); @@ -228,3 +214,27 @@ void main() { /// A do-nothing month-navigation callback. void _noop() {} + +/// Seed [log] to disk, then pump [HistoryScreen] until its load has painted. +/// +/// Both halves must happen inside `runAsync`, and that is the whole point of +/// this helper. `LogStorageService` here is backed by a `FileDocumentStore`, +/// so `writeLog` awaits real `dart:io`; awaited from the test body it waits +/// on a `Future` the fake-async zone never completes, and the test hangs +/// rather than failing -- reported only as "did not complete" once the suite +/// times out. `HistoryScreen` then loads through a fire-and-forget `Future` +/// in `initState`, which needs the same real clock to resolve. +/// +/// The pumping is a bounded manual loop rather than `pumpAndSettle()`, which +/// inside `runAsync` waits on a frame scheduler this screen never reports as +/// idle -- another hang, with the same unhelpful symptom. +Future _pumpHistoryScreen(WidgetTester tester, DayLog log) async { + await tester.runAsync(() async { + await LogStorageService.instance.writeLog(log); + await tester.pumpWidget(const MaterialApp(home: HistoryScreen())); + for (var i = 0; i < 10; i++) { + await Future.delayed(const Duration(milliseconds: 20)); + await tester.pump(); + } + }); +} diff --git a/scripts/ci_mirror.sh b/scripts/ci_mirror.sh index 3639d7d..2be99e0 100755 --- a/scripts/ci_mirror.sh +++ b/scripts/ci_mirror.sh @@ -85,7 +85,20 @@ run_precommit_all_files() { run_pytest_clean_venv() { log "pytest in the clean venv (mirrors the Tests workflow)" - "$VENV_DIR/bin/python" -m pytest "$@" || fail "pytest (clean requirements.txt venv)" + # Under xvfb-run, exactly as python-tests.yml does. The gate's real-Tk + # tests walk a focus ring via tk_focusNext, which consults mapped-ness + # and real focus: on a developer's live display the window never takes + # focus and the ring collapses to one widget, so TestRealFocusRing fails + # here while passing in CI. Mirroring the workflow's display is the whole + # point of this script -- without it the mirror reports red on a green + # tree, which is worse than not mirroring at all. + if ! command -v xvfb-run >/dev/null 2>&1; then + fail "xvfb-run is not on PATH (install xorg-server-xvfb); the real-Tk +gate tests need the same virtual display the Tests workflow gives them" + fi + xvfb-run -a -s "-screen 0 1600x1200x24" \ + "$VENV_DIR/bin/python" -m pytest "$@" \ + || fail "pytest (clean requirements.txt venv)" } # Run flutter with the calling git hook's environment stripped.