mirror of
https://github.com/kuhyx/wake-alarm.git
synced 2026-11-10 19:27:02 +01:00
- Sick days screen (own name, sick icon, cat-2 fill): tap today or a later day to mark it sick, tap again to clear; past days inert. Writes the sickdays wire format (state sick/cleared + actor, one HLC per write) to sickdays-sync/devices via crdt_sync syncLog. - Mutual exclusion in both pickers: each syncs the other pool on open and refuses a date it holds, naming the pool; an unreadable pool refuses all. - Native SickDays cache (fail closed via DayCache); AlarmReceiver skips the ring with reason "sick_day" (no markFired, no ring_started); StickCheck verdict sick_day, streak skips sick days; stakes card says so. - Pre-alarm pool sync: allow-while-idle alarm at 21:00 and A-30 (request code 1006) runs a headless engine (poolSyncMain) that pulls both pools from RTDB and rewrites both native caches, so a PC mark arrives with the app closed. The ring decision stays a cache read. - JVM unit tests (JUnit 4.13.2) for DayCache, RingDecision, PoolSyncSchedule, StickCheck; Dart tests; Dart->Python interop check. pylint hook skipped: verify.py imports sickdays, not in the hook env until the sickdays-v0.1.0 tag exists; run by hand in a sickdays venv (10.00/10). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xfj1sxKH9VBPqpcAni8Jry
110 lines
3.4 KiB
Dart
110 lines
3.4 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:wake_alarm_sync/screens/free_days_screen.dart';
|
|
import 'package:wake_alarm_sync/services/alarm_scheduler.dart';
|
|
import 'package:wake_alarm_sync/services/free_day_pool.dart';
|
|
import 'package:wake_alarm_sync/services/free_day_sync.dart';
|
|
import 'package:wake_alarm_sync/services/sick_day_pool.dart';
|
|
import 'package:wake_alarm_sync/services/sick_day_sync.dart';
|
|
import 'package:wake_alarm_sync/ui/theme.dart';
|
|
|
|
import 'fake_day_pool_store.dart';
|
|
|
|
final _today = DateTime(2026, 10, 16);
|
|
|
|
/// The free-day picker's half of "a date is never in both pools".
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
late AlarmScheduler scheduler;
|
|
late FreeDayPool free;
|
|
|
|
setUp(() async {
|
|
const channel = MethodChannel(AlarmScheduler.channelName);
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
|
.setMockMethodCallHandler(channel, (call) async => null);
|
|
scheduler = AlarmScheduler(channel: channel);
|
|
free = await FreeDayPool.open(
|
|
persistence: MemPersistence(),
|
|
deviceId: 'phone',
|
|
);
|
|
});
|
|
|
|
Future<void> pumpScreen(
|
|
WidgetTester tester,
|
|
Future<SickDayPool> Function() openSick,
|
|
) async {
|
|
tester.view
|
|
..physicalSize = const Size(1080, 2400)
|
|
..devicePixelRatio = 2.625;
|
|
addTearDown(tester.view.reset);
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: buildAppTheme(),
|
|
home: FreeDaysScreen(
|
|
openPool: () async => free,
|
|
syncer: FreeDaySync(
|
|
openClient: () async => null,
|
|
scheduler: scheduler,
|
|
),
|
|
openSickPool: openSick,
|
|
sickSyncer: SickDaySync(
|
|
openClient: () async => null,
|
|
scheduler: scheduler,
|
|
),
|
|
now: () => _today,
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
Future<void> tapDay(WidgetTester tester, DateTime day) async {
|
|
final cell = find.byKey(
|
|
ValueKey<String>('free-day-${FreeDayPool.toIso(day)}'),
|
|
);
|
|
await tester.ensureVisible(cell);
|
|
await tester.tap(cell);
|
|
await tester.pumpAndSettle();
|
|
await tester.drag(find.byType(ListView), const Offset(0, 3000));
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
testWidgets('a sick day is refused, naming the sick-day pool', (
|
|
tester,
|
|
) async {
|
|
final sick = await SickDayPool.open(
|
|
persistence: MemPersistence(),
|
|
deviceId: 'phone',
|
|
);
|
|
await sick.add(DateTime(2026, 10, 20), isFree: (_) => false, now: _today);
|
|
await pumpScreen(tester, () async => sick);
|
|
|
|
await tapDay(tester, DateTime(2026, 10, 20));
|
|
|
|
expect(free.isFree(DateTime(2026, 10, 20)), isFalse);
|
|
expect(
|
|
find.text('2026-10-20 is a sick day — clear it in Sick days first.'),
|
|
findsOneWidget,
|
|
);
|
|
// A normal day still books.
|
|
await tapDay(tester, DateTime(2026, 10, 21));
|
|
expect(free.isFree(DateTime(2026, 10, 21)), isTrue);
|
|
});
|
|
|
|
testWidgets('an unreadable sick-day pool refuses every booking', (
|
|
tester,
|
|
) async {
|
|
await pumpScreen(
|
|
tester,
|
|
() async => throw const FileSystemException('corrupt'),
|
|
);
|
|
await tapDay(tester, DateTime(2026, 10, 21));
|
|
expect(free.isFree(DateTime(2026, 10, 21)), isFalse);
|
|
expect(find.textContaining('Sick-day pool unavailable'), findsOneWidget);
|
|
});
|
|
}
|