refactor(calendar): remove deprecated reminder components

This commit is contained in:
qzl
2026-03-20 18:42:58 +08:00
parent 9ece726de0
commit 6e35fff9a4
20 changed files with 47 additions and 1143 deletions
@@ -36,7 +36,6 @@ void main() {
verifyNever(() => calendarService.getEventsForRange(any(), any()));
verifyNever(() => notificationService.rebuildUpcomingReminders(any()));
verifyNever(() => reminderActionExecutor.replayPendingActions());
});
test('fetches upcoming events after authenticated state', () async {
@@ -46,9 +45,6 @@ void main() {
when(
() => notificationService.rebuildUpcomingReminders(any()),
).thenAnswer((_) async {});
when(
() => reminderActionExecutor.replayPendingActions(),
).thenAnswer((_) async {});
await bootstrapper.syncForAuthState(
const AuthAuthenticated(
@@ -58,37 +54,5 @@ void main() {
verify(() => calendarService.getEventsForRange(any(), any())).called(1);
verify(() => notificationService.rebuildUpcomingReminders(any())).called(1);
verify(() => reminderActionExecutor.replayPendingActions()).called(1);
});
test('retries sync when previous bootstrap failed', () async {
when(
() => reminderActionExecutor.replayPendingActions(),
).thenThrow(Exception('offline'));
await bootstrapper.syncForAuthState(
const AuthAuthenticated(
user: AuthUser(id: 'u1', phone: 'a@test.com'),
),
);
when(
() => reminderActionExecutor.replayPendingActions(),
).thenAnswer((_) async {});
when(
() => calendarService.getEventsForRange(any(), any()),
).thenAnswer((_) async => []);
when(
() => notificationService.rebuildUpcomingReminders(any()),
).thenAnswer((_) async {});
await bootstrapper.syncForAuthState(
const AuthAuthenticated(
user: AuthUser(id: 'u1', phone: 'a@test.com'),
),
);
verify(() => reminderActionExecutor.replayPendingActions()).called(2);
verify(() => calendarService.getEventsForRange(any(), any())).called(1);
});
}
@@ -1,78 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:social_app/features/calendar/reminders/reminder_action_dedupe_store.dart';
void main() {
const dedupeKey = 'calendar_reminder_action_dedupe_v1';
setUp(() {
SharedPreferences.setMockInitialValues({});
});
test('markIfNew returns true first and false for duplicate id', () async {
final prefs = await SharedPreferences.getInstance();
final store = ReminderActionDedupeStore(prefs);
expect(await store.markIfNew('action_1'), isTrue);
expect(await store.markIfNew('action_1'), isFalse);
});
test('markIfNew dedupes after store re-initialization', () async {
final firstPrefs = await SharedPreferences.getInstance();
final firstStore = ReminderActionDedupeStore(firstPrefs);
expect(await firstStore.markIfNew('action_restart'), isTrue);
final secondPrefs = await SharedPreferences.getInstance();
final secondStore = ReminderActionDedupeStore(secondPrefs);
expect(await secondStore.markIfNew('action_restart'), isFalse);
});
test('markIfNew trims history to max capacity', () async {
final prefs = await SharedPreferences.getInstance();
final store = ReminderActionDedupeStore(prefs);
for (var i = 0; i < 513; i++) {
expect(await store.markIfNew('action_$i'), isTrue);
}
final stored = prefs.getStringList(dedupeKey)!;
expect(stored.length, 512);
expect(stored.first, 'action_1');
expect(stored.last, 'action_512');
});
test(
'markIfNew is serialized and does not return true twice in parallel',
() async {
final prefs = await SharedPreferences.getInstance();
final store = ReminderActionDedupeStore(
prefs,
setStringList: (key, value) async {
await Future<void>.delayed(const Duration(milliseconds: 20));
return prefs.setStringList(key, value);
},
);
final results = await Future.wait<bool>([
store.markIfNew('parallel_action'),
store.markIfNew('parallel_action'),
]);
expect(results.where((item) => item).length, 1);
expect(results.where((item) => !item).length, 1);
},
);
test('markIfNew returns false when persistence write fails', () async {
final prefs = await SharedPreferences.getInstance();
final store = ReminderActionDedupeStore(
prefs,
setStringList: (key, value) async => false,
);
expect(await store.markIfNew('action_write_fail'), isFalse);
expect(prefs.getStringList(dedupeKey), isNull);
});
}
@@ -1,13 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:social_app/core/notifications/local_notification_service.dart';
import 'package:social_app/features/calendar/data/models/schedule_item_model.dart';
import 'package:social_app/features/calendar/data/services/calendar_service.dart';
import 'package:social_app/features/calendar/reminders/models/reminder_action.dart';
import 'package:social_app/features/calendar/reminders/models/reminder_payload.dart';
import 'package:social_app/features/calendar/reminders/reminder_action_executor.dart';
import 'package:social_app/features/calendar/reminders/reminder_outbox_store.dart';
class MockCalendarService extends Mock implements CalendarService {}
@@ -17,19 +15,14 @@ class MockLocalNotificationService extends Mock
void main() {
late MockCalendarService calendarService;
late MockLocalNotificationService notificationService;
late ReminderOutboxStore outboxStore;
late ReminderActionExecutor executor;
setUp(() async {
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
setUp(() {
calendarService = MockCalendarService();
notificationService = MockLocalNotificationService();
outboxStore = ReminderOutboxStore(prefs);
executor = ReminderActionExecutor(
calendarService: calendarService,
notificationService: notificationService,
outboxStore: outboxStore,
);
});
@@ -53,33 +46,6 @@ void main() {
verify(() => notificationService.cancelEventReminder('evt_1')).called(1);
verify(() => calendarService.archiveEvent('evt_1')).called(1);
final pending = await outboxStore.listPending();
expect(pending, isEmpty);
});
test('archive failure writes pending outbox item', () async {
when(
() => notificationService.cancelEventReminder('evt_1'),
).thenAnswer((_) async {});
when(
() => calendarService.archiveEvent('evt_1'),
).thenThrow(Exception('offline'));
await executor.handleAction(
action: ReminderAction.archive,
payload: ReminderPayload(
eventId: 'evt_1',
title: 'sync',
startAt: DateTime.parse('2026-03-18T16:00:00+08:00'),
timezone: 'Asia/Shanghai',
),
);
final pending = await outboxStore.listPending();
expect(pending.length, 1);
expect(pending.first.eventId, 'evt_1');
expect(pending.first.state, ReminderOutboxState.pending);
verify(() => calendarService.archiveEvent('evt_1')).called(1);
});
test('snooze reschedules +10m when event not expired', () async {
@@ -151,24 +117,4 @@ void main() {
verify(() => calendarService.archiveEvent('evt_fallback')).called(1);
},
);
test('replay keeps pending item when targetStatus is not archived', () async {
const opId = 'op_non_archived';
await outboxStore.enqueue(
ReminderOutboxItem(
opId: opId,
eventId: 'evt_1',
action: ReminderAction.archive,
targetStatus: 'ignored',
occurredAt: DateTime.parse('2026-03-18T16:00:00+08:00'),
),
);
await executor.replayPendingActions();
final pending = await outboxStore.listPending();
expect(pending.length, 1);
expect(pending.first.opId, opId);
verifyNever(() => calendarService.archiveEvent(any()));
});
}
@@ -1,41 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:social_app/features/calendar/reminders/ui/widgets/reminder_action_sheet.dart';
void main() {
Future<void> pumpSheet(
WidgetTester tester, {
required VoidCallback onSnooze,
required VoidCallback onArchive,
}) {
return tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: ReminderActionSheet(onSnooze: onSnooze, onArchive: onArchive),
),
),
);
}
testWidgets('tap snooze button triggers onSnooze', (tester) async {
var snoozed = false;
await pumpSheet(tester, onSnooze: () => snoozed = true, onArchive: () {});
await tester.tap(find.text('稍后提醒'));
await tester.pump();
expect(snoozed, isTrue);
});
testWidgets('tap archive button triggers onArchive', (tester) async {
var archived = false;
await pumpSheet(tester, onSnooze: () {}, onArchive: () => archived = true);
await tester.tap(find.text('归档'));
await tester.pump();
expect(archived, isTrue);
});
}
@@ -1,123 +0,0 @@
import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:social_app/features/calendar/reminders/reminder_cold_start_queue.dart';
void main() {
test('replays queued actions in enqueue order', () async {
final queue = ReminderColdStartQueue();
final events = <String>[];
queue.enqueue(() async {
await Future<void>.delayed(const Duration(milliseconds: 20));
events.add('first');
});
queue.enqueue(() async {
events.add('second');
});
queue.enqueue(() async {
events.add('third');
});
await queue.replay();
expect(events, <String>['first', 'second', 'third']);
});
test('single failure does not block following queued actions', () async {
final queue = ReminderColdStartQueue();
final events = <String>[];
final errors = <Object>[];
queue.enqueue(() async {
events.add('before');
});
queue.enqueue(() async {
throw StateError('boom');
});
queue.enqueue(() async {
events.add('after');
});
final observableQueue = ReminderColdStartQueue(
onTaskError: (Object error, StackTrace _) {
errors.add(error);
},
);
observableQueue.enqueue(() async {
throw StateError('boom_observable');
});
await queue.replay();
await observableQueue.replay();
expect(events, <String>['before', 'after']);
expect(errors.length, 1);
expect(errors.first, isA<StateError>());
});
test('concurrent replay calls join the same in-flight replay', () async {
final queue = ReminderColdStartQueue();
final taskGate = Completer<void>();
var runCount = 0;
var secondReplayCompleted = false;
queue.enqueue(() async {
runCount += 1;
await taskGate.future;
});
final firstReplay = queue.replay();
final secondReplay = queue.replay().then((_) {
secondReplayCompleted = true;
});
await Future<void>.delayed(const Duration(milliseconds: 10));
expect(secondReplayCompleted, isFalse);
taskGate.complete();
await Future.wait(<Future<void>>[firstReplay, secondReplay]);
expect(runCount, 1);
expect(secondReplayCompleted, isTrue);
});
test('replay on empty queue does not block future enqueued tasks', () async {
final queue = ReminderColdStartQueue();
final events = <String>[];
await queue.replay();
queue.enqueue(() async {
events.add('after_empty_replay');
});
await queue.replay();
expect(events, <String>['after_empty_replay']);
});
test('task-triggered replay reuses in-flight replay and completes', () async {
final queue = ReminderColdStartQueue();
final events = <String>[];
var nestedReplayCompleted = false;
queue.enqueue(() async {
events.add('first');
queue.replay().then((_) {
nestedReplayCompleted = true;
});
});
queue.enqueue(() async {
events.add('second');
});
await queue.replay();
await Future<void>.delayed(Duration.zero);
expect(events, <String>['first', 'second']);
expect(nestedReplayCompleted, isTrue);
});
}
@@ -1,48 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:social_app/features/calendar/data/models/schedule_item_model.dart';
import 'package:social_app/features/calendar/reminders/reminder_overlap_policy.dart';
void main() {
final policy = ReminderOverlapPolicy();
test('groups reminders in same minute bucket', () {
final now = DateTime(2026, 3, 18, 15, 40, 0);
final eventA = ScheduleItemModel(
id: 'a',
ownerId: 'u1',
title: 'A',
startAt: DateTime(2026, 3, 18, 16, 0, 0),
endAt: DateTime(2026, 3, 18, 17, 0, 0),
metadata: ScheduleMetadata(reminderMinutes: 15),
);
final eventB = ScheduleItemModel(
id: 'b',
ownerId: 'u1',
title: 'B',
startAt: DateTime(2026, 3, 18, 16, 0, 20),
endAt: DateTime(2026, 3, 18, 17, 0, 0),
metadata: ScheduleMetadata(reminderMinutes: 15),
);
final groups = policy.groupByMinute([eventA, eventB], now: now);
expect(groups.length, 1);
expect(groups.first.events.length, 2);
expect(groups.first.isAggregate, isTrue);
});
test('returns compensation fire time when remindAt already passed', () {
final now = DateTime(2026, 3, 18, 15, 50, 0);
final event = ScheduleItemModel(
id: 'a',
ownerId: 'u1',
title: 'A',
startAt: DateTime(2026, 3, 18, 16, 0, 0),
endAt: DateTime(2026, 3, 18, 16, 30, 0),
metadata: ScheduleMetadata(reminderMinutes: 15),
);
final fireAt = policy.resolveFirstFireAt(event, now: now);
expect(fireAt, isNotNull);
expect(fireAt!.isAfter(now), isTrue);
});
}
@@ -1,60 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:social_app/features/calendar/reminders/ui/reminder_presentation_coordinator.dart';
void main() {
test('blocks foreground presentation when app is not active', () {
final coordinator = ReminderPresentationCoordinator();
final hiddenDecision = coordinator.shouldPresent(
eventId: 'event_1',
isAppActive: false,
);
final activeDecision = coordinator.shouldPresent(
eventId: 'event_1',
isAppActive: true,
);
expect(hiddenDecision, isFalse);
expect(activeDecision, isTrue);
});
test('suppresses duplicate foreground presentation inside dedupe window', () {
var fakeNow = DateTime(2026, 3, 19, 10, 0, 0);
final coordinator = ReminderPresentationCoordinator(
dedupeWindow: const Duration(seconds: 30),
now: () => fakeNow,
);
final first = coordinator.shouldPresent(
eventId: 'event_42',
isAppActive: true,
);
fakeNow = fakeNow.add(const Duration(seconds: 10));
final second = coordinator.shouldPresent(
eventId: 'event_42',
isAppActive: true,
);
expect(first, isTrue);
expect(second, isFalse);
});
test('allows same event again after dedupe window expires', () {
var fakeNow = DateTime(2026, 3, 19, 10, 0, 0);
final coordinator = ReminderPresentationCoordinator(
dedupeWindow: const Duration(seconds: 30),
now: () => fakeNow,
);
expect(
coordinator.shouldPresent(eventId: 'event_42', isAppActive: true),
isTrue,
);
fakeNow = fakeNow.add(const Duration(seconds: 31));
expect(
coordinator.shouldPresent(eventId: 'event_42', isAppActive: true),
isTrue,
);
});
}