test: cover selected date persistence lifecycle
This commit is contained in:
parent
98fdd0792b
commit
a47bdc67bb
4 changed files with 198 additions and 2 deletions
|
|
@ -202,6 +202,7 @@ class DemoSchedulerComposition implements SchedulerAppComposition {
|
|||
selectedDate: selectedDate,
|
||||
refreshReads: refreshReads,
|
||||
quickCaptureProjectId: defaultProjectId,
|
||||
operationIdPrefix: 'ui-command-${readAt.microsecondsSinceEpoch}',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -157,6 +157,7 @@ final class PersistentSchedulerComposition implements SchedulerAppComposition {
|
|||
selectedDate: selectedDate,
|
||||
refreshReads: refreshReads,
|
||||
quickCaptureProjectId: defaultProjectId,
|
||||
operationIdPrefix: 'ui-command-${readAt.microsecondsSinceEpoch}',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,10 @@ class SchedulerCommandController extends ChangeNotifier {
|
|||
required this.selectedDate,
|
||||
required this.refreshReads,
|
||||
required this.quickCaptureProjectId,
|
||||
String operationIdPrefix = 'ui-command',
|
||||
DateTime Function()? now,
|
||||
}) : now = now ?? _utcNow;
|
||||
}) : _operationIdPrefix = operationIdPrefix,
|
||||
now = now ?? _utcNow;
|
||||
|
||||
/// Scheduler write use cases invoked by this controller.
|
||||
final V1ApplicationCommandUseCases commands;
|
||||
|
|
@ -30,6 +32,10 @@ class SchedulerCommandController extends ChangeNotifier {
|
|||
/// Project id used by quick-capture commands.
|
||||
final String quickCaptureProjectId;
|
||||
|
||||
/// Private state stored as `_operationIdPrefix` for this implementation.
|
||||
/// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
|
||||
final String _operationIdPrefix;
|
||||
|
||||
/// Clock used by direct UI commands such as marking a task complete.
|
||||
final DateTime Function() now;
|
||||
|
||||
|
|
@ -219,7 +225,7 @@ class SchedulerCommandController extends ChangeNotifier {
|
|||
/// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
|
||||
String _nextOperationId() {
|
||||
_sequence += 1;
|
||||
return 'ui-command-$_sequence';
|
||||
return '$_operationIdPrefix-$_sequence';
|
||||
}
|
||||
|
||||
/// Runs the `_setState` helper used inside this library.
|
||||
|
|
|
|||
|
|
@ -80,6 +80,118 @@ void main() {
|
|||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'selected future-day schedule completion and uncomplete persist across reopen',
|
||||
(tester) async {
|
||||
final path = await _runAsync(tester, _tempDatabasePath);
|
||||
final tomorrow = _date.addDays(1);
|
||||
const title = 'Schedule tomorrow from selected date';
|
||||
|
||||
final selectedDate = tomorrow;
|
||||
var composition = await _openComposition(
|
||||
tester,
|
||||
path: path,
|
||||
selectedDate: selectedDate,
|
||||
clock: FixedClock(_instant(8)),
|
||||
);
|
||||
var commandController = composition.createCommandController(
|
||||
refreshReads: () async {},
|
||||
selectedDate: () => selectedDate,
|
||||
);
|
||||
await tester.runAsync(
|
||||
() => commandController.quickCaptureToBacklog(title),
|
||||
);
|
||||
expect(commandController.state, isA<SchedulerCommandSuccess>());
|
||||
|
||||
final backlogBeforeSchedule = await _readBacklog(tester, composition);
|
||||
final captured = backlogBeforeSchedule.items.single.task;
|
||||
await tester.runAsync(
|
||||
() => commandController.scheduleBacklogItem(
|
||||
taskId: captured.id,
|
||||
durationMinutes: 30,
|
||||
expectedUpdatedAt: captured.updatedAt,
|
||||
),
|
||||
);
|
||||
expect(commandController.state, isA<SchedulerCommandSuccess>());
|
||||
expect((await _readBacklog(tester, composition)).items, isEmpty);
|
||||
|
||||
commandController.dispose();
|
||||
await _closeComposition(tester, composition);
|
||||
|
||||
composition = await _openComposition(
|
||||
tester,
|
||||
path: path,
|
||||
selectedDate: selectedDate,
|
||||
clock: FixedClock(_instant(8, 10)),
|
||||
);
|
||||
var scheduled = await _readTodayItem(
|
||||
tester,
|
||||
composition,
|
||||
tomorrow,
|
||||
title,
|
||||
);
|
||||
expect(scheduled.taskStatus, TaskStatus.planned);
|
||||
expect(CivilDate.fromDateTime(scheduled.start!.toUtc()), tomorrow);
|
||||
|
||||
commandController = composition.createCommandController(
|
||||
refreshReads: () async {},
|
||||
selectedDate: () => selectedDate,
|
||||
);
|
||||
await tester.runAsync(
|
||||
() => commandController.completeTask(
|
||||
taskId: scheduled.id,
|
||||
taskType: scheduled.taskType,
|
||||
),
|
||||
);
|
||||
expect(commandController.state, isA<SchedulerCommandSuccess>());
|
||||
commandController.dispose();
|
||||
await _closeComposition(tester, composition);
|
||||
|
||||
composition = await _openComposition(
|
||||
tester,
|
||||
path: path,
|
||||
selectedDate: selectedDate,
|
||||
clock: FixedClock(_instant(8, 20)),
|
||||
);
|
||||
final completed = await _readTodayItem(
|
||||
tester,
|
||||
composition,
|
||||
tomorrow,
|
||||
title,
|
||||
);
|
||||
expect(completed.taskStatus, TaskStatus.completed);
|
||||
expect(completed.item.completedAt, isNotNull);
|
||||
|
||||
commandController = composition.createCommandController(
|
||||
refreshReads: () async {},
|
||||
selectedDate: () => selectedDate,
|
||||
);
|
||||
await tester.runAsync(
|
||||
() => commandController.uncompleteTask(taskId: completed.id),
|
||||
);
|
||||
expect(commandController.state, isA<SchedulerCommandSuccess>());
|
||||
commandController.dispose();
|
||||
await _closeComposition(tester, composition);
|
||||
|
||||
composition = await _openComposition(
|
||||
tester,
|
||||
path: path,
|
||||
selectedDate: selectedDate,
|
||||
clock: FixedClock(_instant(8, 30)),
|
||||
);
|
||||
addTearDown(() => tester.runAsync(composition.dispose));
|
||||
final reopened = await _readTodayItem(
|
||||
tester,
|
||||
composition,
|
||||
tomorrow,
|
||||
title,
|
||||
);
|
||||
expect(reopened.taskStatus, TaskStatus.planned);
|
||||
expect(reopened.item.completedAt, isNull);
|
||||
expect((await _readBacklog(tester, composition)).items, isEmpty);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Fixed date used by persistent Flutter tests.
|
||||
|
|
@ -99,6 +211,82 @@ Future<String> _tempDatabasePath() async {
|
|||
return '${directory.path}/scheduler.sqlite';
|
||||
}
|
||||
|
||||
/// Opens a persistent composition for a test database.
|
||||
Future<PersistentSchedulerComposition> _openComposition(
|
||||
WidgetTester tester, {
|
||||
required String path,
|
||||
required CivilDate selectedDate,
|
||||
required Clock clock,
|
||||
}) {
|
||||
return _runAsync(
|
||||
tester,
|
||||
() => PersistentSchedulerComposition.open(
|
||||
sqlitePath: path,
|
||||
selectedDate: selectedDate,
|
||||
clock: clock,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Closes [composition] outside the widget-test fake async zone.
|
||||
Future<void> _closeComposition(
|
||||
WidgetTester tester,
|
||||
PersistentSchedulerComposition composition,
|
||||
) async {
|
||||
await tester.runAsync(composition.dispose);
|
||||
}
|
||||
|
||||
/// Reads the backlog through the management use case.
|
||||
Future<BacklogQueryResult> _readBacklog(
|
||||
WidgetTester tester,
|
||||
PersistentSchedulerComposition composition,
|
||||
) async {
|
||||
final result = await _runAsync(
|
||||
tester,
|
||||
() => composition.managementUseCases.getBacklog(
|
||||
GetBacklogRequest(
|
||||
context: composition.context('flutter-read-backlog'),
|
||||
sortKey: BacklogSortKey.age,
|
||||
),
|
||||
),
|
||||
);
|
||||
return result.requireValue;
|
||||
}
|
||||
|
||||
/// Reads one Today timeline item by [title].
|
||||
Future<TodayTimelineItem> _readTodayItem(
|
||||
WidgetTester tester,
|
||||
PersistentSchedulerComposition composition,
|
||||
CivilDate date,
|
||||
String title,
|
||||
) async {
|
||||
final state = await _readToday(tester, composition, date);
|
||||
return state.timelineItems.singleWhere(
|
||||
(item) => item.item.displayTitle == title,
|
||||
);
|
||||
}
|
||||
|
||||
/// Reads Today state for [date].
|
||||
Future<TodayState> _readToday(
|
||||
WidgetTester tester,
|
||||
PersistentSchedulerComposition composition,
|
||||
CivilDate date,
|
||||
) async {
|
||||
final result = await _runAsync(
|
||||
tester,
|
||||
() => composition.todayQuery.execute(
|
||||
GetTodayStateRequest(
|
||||
context: composition.context(
|
||||
'flutter-read-today-${date.toIsoString()}',
|
||||
),
|
||||
date: date,
|
||||
includeNextFlexibleItem: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
return result.requireValue;
|
||||
}
|
||||
|
||||
/// Runs real async work outside the widget-test fake async zone.
|
||||
Future<T> _runAsync<T>(
|
||||
WidgetTester tester,
|
||||
|
|
|
|||
Loading…
Reference in a new issue