focus-flow/test/timeline_state_test.dart

219 lines
6.7 KiB
Dart

import 'package:adhd_scheduler_core/scheduler_core.dart';
import 'package:test/test.dart';
void main() {
group('Timeline item view model', () {
final createdAt = DateTime(2026, 6, 20, 8);
final start = DateTime(2026, 6, 20, 9);
final end = DateTime(2026, 6, 20, 9, 30);
const project = ProjectProfile(
id: 'home',
name: 'Home',
colorKey: 'project-home',
);
final mapper = TimelineItemMapper.fromProjects(const [project]);
test('maps task fields to display tokens', () {
final task = scheduledTask(
id: 'dishes',
title: 'Wash dishes',
projectId: project.id,
type: TaskType.flexible,
reward: RewardLevel.high,
difficulty: DifficultyLevel.hard,
createdAt: createdAt,
start: start,
end: end,
);
final item = mapper.fromTask(task);
expect(item.id, 'dishes');
expect(item.displayTitle, 'Wash dishes');
expect(item.taskType, TaskType.flexible);
expect(item.projectColorToken, 'project-home');
expect(item.backgroundToken, TimelineBackgroundToken.flexible);
expect(item.rewardIconToken, TimelineRewardIconToken.high);
expect(item.difficultyIconToken, TimelineDifficultyIconToken.hard);
expect(item.category, TimelineItemCategory.taskCard);
expect(item.quickActions, [
TimelineQuickAction.done,
TimelineQuickAction.push,
TimelineQuickAction.backlog,
TimelineQuickAction.breakUp,
]);
});
test('flexible task cards display duration while retaining position', () {
final task = scheduledTask(
id: 'laundry',
title: 'Fold laundry',
projectId: project.id,
type: TaskType.flexible,
durationMinutes: 25,
createdAt: createdAt,
start: start,
end: start.add(const Duration(minutes: 25)),
);
final item = mapper.fromTask(task);
expect(item.showsExplicitTime, isFalse);
expect(item.durationMinutes, 25);
expect(item.start, start);
expect(item.end, start.add(const Duration(minutes: 25)));
});
test('flexible duration can fall back to scheduled interval', () {
final task = scheduledTask(
id: 'admin',
title: 'Handle admin',
projectId: project.id,
type: TaskType.flexible,
createdAt: createdAt,
start: start,
end: start.add(const Duration(minutes: 45)),
);
final item = mapper.fromTask(task);
expect(item.showsExplicitTime, isFalse);
expect(item.durationMinutes, 45);
});
test('inflexible critical and locked items show explicit times', () {
for (final type in [
TaskType.inflexible,
TaskType.critical,
TaskType.locked,
]) {
final task = scheduledTask(
id: type.name,
title: type.name,
projectId: project.id,
type: type,
createdAt: createdAt,
start: start,
end: end,
);
final item = mapper.fromTask(task);
expect(item.showsExplicitTime, isTrue, reason: type.name);
expect(item.start, start, reason: type.name);
expect(item.end, end, reason: type.name);
expect(item.durationMinutes, isNull, reason: type.name);
}
});
test('locked item is an overlay without normal task quick actions', () {
final task = scheduledTask(
id: 'sleep',
title: 'Sleep',
projectId: 'locked',
type: TaskType.locked,
createdAt: createdAt,
start: DateTime(2026, 6, 20, 0),
end: DateTime(2026, 6, 20, 7),
);
final item = mapper.fromTask(task);
expect(item.category, TimelineItemCategory.overlay);
expect(item.projectColorToken, 'locked');
expect(item.backgroundToken, TimelineBackgroundToken.locked);
expect(item.quickActions, isEmpty);
});
test('hidden locked occurrences stay hidden until reveal mode is active',
() {
final occurrence = LockedBlockOccurrence(
name: 'Work',
interval: TimeInterval(
start: DateTime(2026, 6, 20, 9),
end: DateTime(2026, 6, 20, 17),
),
hiddenByDefault: true,
lockedBlockId: 'work-hours',
projectId: project.id,
);
final hiddenItem = mapper.fromLockedOccurrence(
occurrence,
revealHiddenLockedBlocks: false,
);
final revealedItem = mapper.fromLockedOccurrence(
occurrence,
revealHiddenLockedBlocks: true,
);
expect(hiddenItem, isNull);
expect(revealedItem, isNotNull);
expect(revealedItem!.id, 'locked-block:work-hours');
expect(revealedItem.displayTitle, 'Work');
expect(revealedItem.start, DateTime(2026, 6, 20, 9));
expect(revealedItem.end, DateTime(2026, 6, 20, 17));
expect(revealedItem.showsExplicitTime, isTrue);
expect(revealedItem.category, TimelineItemCategory.overlay);
expect(revealedItem.projectColorToken, 'project-home');
expect(revealedItem.quickActions, isEmpty);
});
test('visible locked occurrences can produce overlays without reveal mode',
() {
final occurrence = LockedBlockOccurrence(
name: 'Appointment',
interval: TimeInterval(
start: DateTime(2026, 6, 20, 11),
end: DateTime(2026, 6, 20, 12),
),
hiddenByDefault: false,
overrideId: 'appointment-override',
);
final item = mapper.fromLockedOccurrence(
occurrence,
revealHiddenLockedBlocks: false,
);
expect(item, isNotNull);
expect(item!.id, 'locked-override:appointment-override');
expect(item.displayTitle, 'Appointment');
expect(item.category, TimelineItemCategory.overlay);
expect(item.taskType, TaskType.locked);
expect(item.backgroundToken, TimelineBackgroundToken.locked);
expect(item.rewardIconToken, TimelineRewardIconToken.notSet);
expect(item.difficultyIconToken, TimelineDifficultyIconToken.notSet);
expect(item.showsExplicitTime, isTrue);
expect(item.quickActions, isEmpty);
});
});
}
Task scheduledTask({
required String id,
required String title,
required String projectId,
required TaskType type,
required DateTime createdAt,
required DateTime start,
required DateTime end,
RewardLevel reward = RewardLevel.notSet,
DifficultyLevel difficulty = DifficultyLevel.notSet,
int? durationMinutes,
}) {
return Task(
id: id,
title: title,
projectId: projectId,
type: type,
status: TaskStatus.planned,
reward: reward,
difficulty: difficulty,
durationMinutes: durationMinutes,
scheduledStart: start,
scheduledEnd: end,
createdAt: createdAt,
updatedAt: createdAt,
);
}