mirror of
https://github.com/kuhyx/wake-alarm.git
synced 2026-11-10 18:07:05 +01:00
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
144 lines
5.0 KiB
Dart
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']);
|
|
});
|
|
}
|