wake-alarm/phone_app/test/session_screen_checkin_test.dart
Krzysztof kuhy Rudnicki c3c129b068 Make the morning session the carrot: earn 11:00 by finishing it in time
On 2026-09-19 the phone was powered off after the bathroom tag and nothing
on the PC noticed: every finished session was published and nothing read
it. Now wake-alarm distils the phone's morning into an HMAC-signed
~/.local/state/wake_alarm/morning_session.json (wake-alarm-session.timer,
07:00-11:00 every 2 min) that screen-locker and leetcode-guard read to
defer their locks -- provisionally until min(start + 90 min, 09:30) while
the session is live, until 11:00 once it completed inside that cutoff,
and not at all for a session finished later (the phone switched on at
noon completes the legs then; that must un-lock nothing).

The phone publishes the session at start as well as at finish
(current_session, cleared with the finish stamp), so the PC can tell
"started, not finished" from "never started". A required 1-5 energy tap
joins the weigh-in and ships as latest_energy with a PC reader mirroring
_weight (the shared half is _field_reader).

Also fixes the vmbox harness, red since the 2026-09-11 home-tidy: the
guest tree lives at ~/wake-alarm, and the INSTALL-OK grep matched vm's
echo of the command line. Takes Flutter 3.47.5.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tp54ewdXRLaimUuT8LgYyn
2026-09-19 16:40:51 +02:00

144 lines
5.0 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:wake_alarm_sync/screens/session_screen.dart';
import 'package:wake_alarm_sync/services/alarm_scheduler.dart';
import 'package:wake_alarm_sync/services/session_store.dart';
import 'package:wake_alarm_sync/services/session_tag_settings.dart';
/// The check-in half of the session screen (energy + weight) and the two
/// publishes: split from `session_screen_test.dart` at the 250-line cap.
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late DateTime now;
late List<String> publishes;
late List<bool> storeHadSession;
Completer<String>? holdFirst;
void mockChannel() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel(AlarmScheduler.channelName),
(call) async => call.method == 'hasNfc' ? true : null,
);
}
Future<void> tap(String tagId) async {
const codec = StandardMethodCodec();
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.handlePlatformMessage(
AlarmScheduler.channelName,
codec.encodeMethodCall(MethodCall('onNfcTag', tagId)),
(_) {},
);
}
setUp(() async {
now = DateTime(2026, 9, 19, 8, 4);
publishes = <String>[];
storeHadSession = <bool>[];
holdFirst = null;
SharedPreferences.setMockInitialValues(<String, Object>{});
mockChannel();
await SessionTagSettings.replaceAll(<String>['bath', 'ward', 'desk'], '');
});
Future<void> pumpSession(WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
routes: <String, WidgetBuilder>{
'/': (_) => SessionScreen(
clock: () => now,
publish: () async {
// What the real publisher stages: is the session on disk yet?
storeHadSession.add(await SessionStore().loadCurrent() != null);
final hold = holdFirst;
if (publishes.isEmpty && hold != null) {
publishes.add('start (held)');
return await hold.future;
}
publishes.add(publishes.isEmpty ? 'start' : 'finish');
return 'pushed';
},
),
'/ring': (_) => const Scaffold(body: Text('RING')),
},
),
);
await tester.pump();
await tester.pump();
}
Future<void> submit(WidgetTester tester, String kg) async {
await tester.enterText(find.byType(TextField), kg);
await tester.tap(find.text('Log weight'));
await tester.pump();
}
testWidgets('the start publish runs after the session is stored', (
tester,
) async {
await pumpSession(tester);
expect(publishes, <String>['start']);
expect(storeHadSession, <bool>[true]);
});
testWidgets('no energy tapped: the weight is refused and nothing sticks', (
tester,
) async {
await pumpSession(tester);
await submit(tester, '72');
expect(find.textContaining('Tap your energy'), findsOneWidget);
expect(find.byType(TextField), findsOneWidget);
expect((await SessionStore().loadCurrent())?.pendingWeightKg, isNull);
});
testWidgets('energy then weight records both on the session', (tester) async {
await pumpSession(tester);
await tester.tap(find.widgetWithText(ChoiceChip, '5'));
await tester.pump();
await submit(tester, '72');
expect(find.textContaining('now tap the bathroom tag'), findsOneWidget);
final session = await SessionStore().loadCurrent();
expect(session?.energy, 5);
expect(session?.pendingWeightKg, 72);
});
testWidgets('an unknown tag and bad weights are reported', (tester) async {
await pumpSession(tester);
await tester.tap(find.widgetWithText(ChoiceChip, '3'));
await tester.pump();
await submit(tester, 'x');
expect(find.textContaining('Enter your weight'), findsOneWidget);
await submit(tester, '1');
expect(find.textContaining('not a body weight'), findsOneWidget);
expect(find.byType(TextField), findsOneWidget);
await tap('mystery');
await tester.pump();
expect(find.text('Unknown tag'), findsOneWidget);
});
testWidgets('the finish publish waits for a start publish in flight', (
tester,
) async {
// No session tags: the check-in is the whole morning, so it finishes
// seconds after it started, while the start publish may still be out.
await SessionTagSettings.replaceAll(<String>['', '', ''], '');
holdFirst = Completer<String>();
await pumpSession(tester);
await tester.tap(find.widgetWithText(ChoiceChip, '2'));
await tester.pump();
await submit(tester, '72');
await tester.pump();
expect(publishes, <String>['start (held)']);
holdFirst!.complete('late');
await tester.pump();
await tester.pump();
expect(publishes, <String>['start (held)', 'finish']);
});
}