forked from eva/focus-flow
test(backlog): seed demo board data
This commit is contained in:
parent
60039be127
commit
82f1fd43b7
4 changed files with 476 additions and 14 deletions
|
|
@ -83,3 +83,20 @@ Freeform Tags Metadata.md` for a later migration-backed feature.
|
|||
4. Focused application-management tests cover stable empty columns, bucket and
|
||||
summary consistency, search/filter behavior, child previews, not-found detail
|
||||
responses, read-only slot suggestions, and missing-duration handling.
|
||||
|
||||
## Block 02 Demo Seed Data
|
||||
|
||||
1. The Flutter demo composition now seeds Backlog board data as real scheduler
|
||||
`Task` and `ProjectProfile` records, not widget-local mock rows.
|
||||
2. The seed matches the feasible textual targets from Chunk 2.7: 24 backlog
|
||||
tasks, project counts of Home 5 / Work 8 / Personal 5 / Learning 4 / Side
|
||||
Project 2, priority counts of High 7 / Medium 9 / Low 8, and 12h 15m total
|
||||
estimated time.
|
||||
3. The plan/mockup contains a count conflict: the mockup summary says 24 tasks,
|
||||
while the listed column badge counts add to 22. The current seed keeps 24
|
||||
repository-backed backlog tasks so the summary target stays truthful through
|
||||
public read models.
|
||||
4. `Review Q3 budget` is seeded with Work, 60 minutes, High priority,
|
||||
medium reward, and easy difficulty. Finance/review freeform tags, notes text,
|
||||
and exact drawer slot copy remain deferred with the approved metadata scope
|
||||
decision above.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,30 @@
|
|||
|
||||
part of '../demo_scheduler_composition.dart';
|
||||
|
||||
/// Top-level helper that performs the `_demoProjects` operation for this file.
|
||||
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
|
||||
List<ProjectProfile> _demoProjects() {
|
||||
return [
|
||||
ProjectProfile(id: 'home', name: 'Home', colorKey: 'project-home'),
|
||||
ProjectProfile(id: 'work', name: 'Work', colorKey: 'project-work'),
|
||||
ProjectProfile(
|
||||
id: 'personal',
|
||||
name: 'Personal',
|
||||
colorKey: 'project-personal',
|
||||
),
|
||||
ProjectProfile(
|
||||
id: 'learning',
|
||||
name: 'Learning',
|
||||
colorKey: 'project-learning',
|
||||
),
|
||||
ProjectProfile(
|
||||
id: 'side-project',
|
||||
name: 'Side Project',
|
||||
colorKey: 'project-side-project',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// Top-level helper that performs the `_todaySeedTasks` operation for this file.
|
||||
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
|
||||
List<Task> _todaySeedTasks(CivilDate date, DateTime createdAt) {
|
||||
|
|
@ -123,6 +147,284 @@ List<Task> _todaySeedTasks(CivilDate date, DateTime createdAt) {
|
|||
];
|
||||
}
|
||||
|
||||
/// Top-level helper that performs the `_backlogSeedTasks` operation for this file.
|
||||
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
|
||||
List<Task> _backlogSeedTasks(CivilDate date, DateTime createdAt) {
|
||||
return [
|
||||
_backlogSeedTask(
|
||||
id: 'reply-to-3-emails',
|
||||
title: 'Reply to 3 emails',
|
||||
projectId: 'home',
|
||||
order: 1,
|
||||
priority: PriorityLevel.medium,
|
||||
reward: RewardLevel.low,
|
||||
difficulty: DifficultyLevel.easy,
|
||||
durationMinutes: 10,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
_backlogSeedTask(
|
||||
id: 'update-project-status',
|
||||
title: 'Update project status',
|
||||
projectId: 'work',
|
||||
order: 2,
|
||||
priority: PriorityLevel.medium,
|
||||
reward: RewardLevel.low,
|
||||
difficulty: DifficultyLevel.easy,
|
||||
durationMinutes: 15,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
_backlogSeedTask(
|
||||
id: 'schedule-dentist-appt',
|
||||
title: 'Schedule dentist appt',
|
||||
projectId: 'personal',
|
||||
order: 3,
|
||||
priority: PriorityLevel.medium,
|
||||
reward: RewardLevel.low,
|
||||
difficulty: DifficultyLevel.easy,
|
||||
durationMinutes: 10,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
_backlogSeedTask(
|
||||
id: 'clean-up-downloads-folder',
|
||||
title: 'Clean up downloads folder',
|
||||
projectId: 'home',
|
||||
order: 4,
|
||||
priority: PriorityLevel.medium,
|
||||
reward: RewardLevel.low,
|
||||
difficulty: DifficultyLevel.easy,
|
||||
durationMinutes: 10,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
_backlogSeedTask(
|
||||
id: 'plan-tomorrow',
|
||||
title: 'Plan tomorrow',
|
||||
projectId: 'work',
|
||||
order: 5,
|
||||
priority: PriorityLevel.medium,
|
||||
reward: RewardLevel.low,
|
||||
difficulty: DifficultyLevel.easy,
|
||||
durationMinutes: 10,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
_backlogSeedTask(
|
||||
id: 'review-q3-budget',
|
||||
title: 'Review Q3 budget',
|
||||
projectId: 'work',
|
||||
order: 6,
|
||||
priority: PriorityLevel.high,
|
||||
reward: RewardLevel.medium,
|
||||
difficulty: DifficultyLevel.easy,
|
||||
durationMinutes: 60,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
_backlogSeedTask(
|
||||
id: 'plan-next-week',
|
||||
title: 'Plan next week',
|
||||
projectId: 'work',
|
||||
order: 7,
|
||||
priority: PriorityLevel.high,
|
||||
reward: RewardLevel.high,
|
||||
difficulty: DifficultyLevel.medium,
|
||||
durationMinutes: 30,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
_backlogSeedTask(
|
||||
id: 'define-weekly-goals',
|
||||
title: 'Define weekly goals',
|
||||
projectId: 'work',
|
||||
order: 8,
|
||||
priority: PriorityLevel.high,
|
||||
reward: RewardLevel.medium,
|
||||
difficulty: DifficultyLevel.medium,
|
||||
durationMinutes: 15,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
_backlogSeedTask(
|
||||
id: 'time-block-schedule',
|
||||
title: 'Time block schedule',
|
||||
projectId: 'work',
|
||||
order: 9,
|
||||
priority: PriorityLevel.high,
|
||||
reward: RewardLevel.medium,
|
||||
difficulty: DifficultyLevel.medium,
|
||||
durationMinutes: 15,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
_backlogSeedTask(
|
||||
id: 'organize-kitchen-drawers',
|
||||
title: 'Organize kitchen drawers',
|
||||
projectId: 'home',
|
||||
order: 10,
|
||||
priority: PriorityLevel.low,
|
||||
reward: RewardLevel.low,
|
||||
difficulty: DifficultyLevel.medium,
|
||||
durationMinutes: 45,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
_backlogSeedTask(
|
||||
id: 'renew-car-registration',
|
||||
title: 'Renew car registration',
|
||||
projectId: 'home',
|
||||
order: 11,
|
||||
priority: PriorityLevel.high,
|
||||
reward: RewardLevel.low,
|
||||
difficulty: DifficultyLevel.medium,
|
||||
durationMinutes: 25,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
_backlogSeedTask(
|
||||
id: 'build-analytics-dashboard',
|
||||
title: 'Build analytics dashboard',
|
||||
projectId: 'work',
|
||||
order: 12,
|
||||
priority: PriorityLevel.high,
|
||||
reward: RewardLevel.high,
|
||||
difficulty: DifficultyLevel.hard,
|
||||
durationMinutes: 120,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
_backlogSeedTask(
|
||||
id: 'write-marketing-strategy',
|
||||
title: 'Write marketing strategy',
|
||||
projectId: 'work',
|
||||
order: 13,
|
||||
priority: PriorityLevel.high,
|
||||
reward: RewardLevel.high,
|
||||
difficulty: DifficultyLevel.medium,
|
||||
durationMinutes: 90,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
_backlogSeedTask(
|
||||
id: 'complete-online-course',
|
||||
title: 'Complete online course',
|
||||
projectId: 'learning',
|
||||
order: 14,
|
||||
priority: PriorityLevel.medium,
|
||||
reward: RewardLevel.high,
|
||||
difficulty: DifficultyLevel.medium,
|
||||
durationMinutes: 90,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
_backlogSeedTask(
|
||||
id: 'module-1-foundations',
|
||||
title: 'Module 1: Foundations',
|
||||
projectId: 'learning',
|
||||
order: 15,
|
||||
priority: PriorityLevel.medium,
|
||||
reward: RewardLevel.medium,
|
||||
difficulty: DifficultyLevel.hard,
|
||||
durationMinutes: 15,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
_backlogSeedTask(
|
||||
id: 'module-2-deep-dive',
|
||||
title: 'Module 2: Deep Dive',
|
||||
projectId: 'learning',
|
||||
order: 16,
|
||||
priority: PriorityLevel.low,
|
||||
reward: RewardLevel.medium,
|
||||
difficulty: DifficultyLevel.hard,
|
||||
durationMinutes: 15,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
_backlogSeedTask(
|
||||
id: 'module-3-practice',
|
||||
title: 'Module 3: Practice',
|
||||
projectId: 'learning',
|
||||
order: 17,
|
||||
priority: PriorityLevel.low,
|
||||
reward: RewardLevel.medium,
|
||||
difficulty: DifficultyLevel.hard,
|
||||
durationMinutes: 15,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
_backlogSeedTask(
|
||||
id: 'learn-guitar',
|
||||
title: 'Learn guitar',
|
||||
projectId: 'personal',
|
||||
order: 18,
|
||||
priority: PriorityLevel.low,
|
||||
reward: RewardLevel.low,
|
||||
difficulty: DifficultyLevel.medium,
|
||||
durationMinutes: 30,
|
||||
backlogTags: const {BacklogTag.wishlist},
|
||||
createdAt: createdAt,
|
||||
),
|
||||
_backlogSeedTask(
|
||||
id: 'start-youtube-channel',
|
||||
title: 'Start YouTube channel',
|
||||
projectId: 'side-project',
|
||||
order: 19,
|
||||
priority: PriorityLevel.low,
|
||||
reward: RewardLevel.low,
|
||||
difficulty: DifficultyLevel.medium,
|
||||
durationMinutes: 20,
|
||||
backlogTags: const {BacklogTag.wishlist},
|
||||
createdAt: createdAt,
|
||||
),
|
||||
_backlogSeedTask(
|
||||
id: 'travel-to-japan',
|
||||
title: 'Travel to Japan',
|
||||
projectId: 'personal',
|
||||
order: 20,
|
||||
priority: PriorityLevel.medium,
|
||||
reward: RewardLevel.medium,
|
||||
difficulty: DifficultyLevel.medium,
|
||||
durationMinutes: 30,
|
||||
backlogTags: const {BacklogTag.wishlist},
|
||||
createdAt: createdAt,
|
||||
),
|
||||
_backlogSeedTask(
|
||||
id: 'home-office-upgrade',
|
||||
title: 'Home office upgrade',
|
||||
projectId: 'home',
|
||||
order: 21,
|
||||
priority: PriorityLevel.low,
|
||||
reward: RewardLevel.low,
|
||||
difficulty: DifficultyLevel.medium,
|
||||
durationMinutes: 20,
|
||||
backlogTags: const {BacklogTag.wishlist},
|
||||
createdAt: createdAt,
|
||||
),
|
||||
_backlogSeedTask(
|
||||
id: 'read-2-books',
|
||||
title: 'Read 2 books',
|
||||
projectId: 'personal',
|
||||
order: 22,
|
||||
priority: PriorityLevel.low,
|
||||
reward: RewardLevel.low,
|
||||
difficulty: DifficultyLevel.medium,
|
||||
durationMinutes: 15,
|
||||
backlogTags: const {BacklogTag.wishlist},
|
||||
createdAt: createdAt,
|
||||
),
|
||||
_backlogSeedTask(
|
||||
id: 'sketch-app-icon-ideas',
|
||||
title: 'Sketch app icon ideas',
|
||||
projectId: 'side-project',
|
||||
order: 23,
|
||||
priority: PriorityLevel.medium,
|
||||
reward: RewardLevel.medium,
|
||||
difficulty: DifficultyLevel.easy,
|
||||
durationMinutes: 15,
|
||||
backlogTags: const {BacklogTag.wishlist},
|
||||
createdAt: createdAt,
|
||||
),
|
||||
_backlogSeedTask(
|
||||
id: 'archive-photo-backlog',
|
||||
title: 'Archive photo backlog',
|
||||
projectId: 'personal',
|
||||
order: 24,
|
||||
priority: PriorityLevel.low,
|
||||
reward: RewardLevel.low,
|
||||
difficulty: DifficultyLevel.medium,
|
||||
durationMinutes: 15,
|
||||
backlogTags: const {BacklogTag.wishlist},
|
||||
createdAt: createdAt,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// Top-level helper that performs the `_seedTask` operation for this file.
|
||||
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
|
||||
Task _seedTask({
|
||||
|
|
@ -161,3 +463,34 @@ Task _seedTask({
|
|||
updatedAt: completedAt ?? createdAt,
|
||||
);
|
||||
}
|
||||
|
||||
/// Top-level helper that performs the `_backlogSeedTask` operation for this file.
|
||||
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
|
||||
Task _backlogSeedTask({
|
||||
required String id,
|
||||
required String title,
|
||||
required String projectId,
|
||||
required int order,
|
||||
required PriorityLevel priority,
|
||||
required RewardLevel reward,
|
||||
required DifficultyLevel difficulty,
|
||||
required int durationMinutes,
|
||||
required DateTime createdAt,
|
||||
Set<BacklogTag> backlogTags = const <BacklogTag>{},
|
||||
}) {
|
||||
final orderedCreatedAt = createdAt.add(Duration(minutes: order));
|
||||
return Task(
|
||||
id: id,
|
||||
title: title,
|
||||
projectId: projectId,
|
||||
type: TaskType.flexible,
|
||||
status: TaskStatus.backlog,
|
||||
priority: priority,
|
||||
reward: reward,
|
||||
difficulty: difficulty,
|
||||
durationMinutes: durationMinutes,
|
||||
backlogTags: backlogTags,
|
||||
createdAt: orderedCreatedAt,
|
||||
updatedAt: orderedCreatedAt,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,24 +91,13 @@ class DemoSchedulerComposition implements SchedulerAppComposition {
|
|||
CivilDate.fromDateTime((clock ?? const SystemClock()).now());
|
||||
final readAt = _instant(resolvedDate, 15, 55);
|
||||
final createdAt = _instant(resolvedDate, 8);
|
||||
final project = ProjectProfile(
|
||||
id: projectId,
|
||||
name: 'Home',
|
||||
colorKey: 'project-home',
|
||||
);
|
||||
final projects = _demoProjects();
|
||||
final tasks = <Task>[
|
||||
if (includeTodayItems) ..._todaySeedTasks(resolvedDate, createdAt),
|
||||
if (includeBacklogItems)
|
||||
Task.quickCapture(
|
||||
id: 'later-backlog-note',
|
||||
title: 'Review reusable grocery list',
|
||||
projectId: projectId,
|
||||
createdAt: createdAt,
|
||||
updatedAt: createdAt,
|
||||
),
|
||||
if (includeBacklogItems) ..._backlogSeedTasks(resolvedDate, createdAt),
|
||||
];
|
||||
final store = InMemoryApplicationUnitOfWork(
|
||||
initialProjects: [project],
|
||||
initialProjects: projects,
|
||||
initialOwnerSettings: [
|
||||
OwnerSettings(
|
||||
ownerId: ownerId,
|
||||
|
|
@ -191,6 +180,22 @@ class DemoSchedulerComposition implements SchedulerAppComposition {
|
|||
);
|
||||
}
|
||||
|
||||
/// Creates a generic read controller for the Backlog board.
|
||||
UiReadController<BacklogBoardQueryResult> createBacklogBoardController() {
|
||||
return ApplicationReadController<BacklogBoardQueryResult>(
|
||||
read: () {
|
||||
return managementUseCases.getBacklogBoard(
|
||||
GetBacklogBoardRequest(
|
||||
context: context('ui-read-backlog-board'),
|
||||
localDate: date,
|
||||
sortKey: BacklogSortKey.age,
|
||||
),
|
||||
);
|
||||
},
|
||||
isEmpty: (state) => state.summary.totalTaskCount == 0,
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates the command controller used by interactive scheduler controls.
|
||||
@override
|
||||
SchedulerCommandController createCommandController({
|
||||
|
|
|
|||
107
apps/focus_flow_flutter/test/app/demo_backlog_seed_test.dart
Normal file
107
apps/focus_flow_flutter/test/app/demo_backlog_seed_test.dart
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/// Tests Demo Backlog Seed behavior in the FocusFlow Flutter app.
|
||||
library;
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:scheduler_core/scheduler_core.dart';
|
||||
|
||||
import 'package:focus_flow_flutter/app/demo_scheduler_composition.dart';
|
||||
|
||||
/// Runs demo Backlog seed tests.
|
||||
void main() {
|
||||
test('seeded backlog board exposes mockup-scale domain data', () async {
|
||||
final composition = DemoSchedulerComposition.seeded(
|
||||
selectedDate: CivilDate(2025, 5, 20),
|
||||
);
|
||||
addTearDown(composition.dispose);
|
||||
|
||||
final board = (await composition.managementUseCases.getBacklogBoard(
|
||||
GetBacklogBoardRequest(
|
||||
context: composition.context('demo-backlog-board'),
|
||||
localDate: composition.date,
|
||||
sortKey: BacklogSortKey.age,
|
||||
),
|
||||
)).requireValue;
|
||||
final items = board.columns.expand((column) => column.items).toList();
|
||||
|
||||
expect(board.summary.totalTaskCount, 24);
|
||||
expect(board.summary.totalEstimatedMinutes, 735);
|
||||
expect(_distributionCounts(board.summary.projectDistribution), {
|
||||
'Home': 5,
|
||||
'Learning': 4,
|
||||
'Personal': 5,
|
||||
'Side Project': 2,
|
||||
'Work': 8,
|
||||
});
|
||||
expect(_distributionCounts(board.summary.priorityDistribution), {
|
||||
'High': 7,
|
||||
'Medium': 9,
|
||||
'Low': 8,
|
||||
});
|
||||
expect(items.map((item) => item.title), containsAll(_visibleSeedTitles));
|
||||
|
||||
final review = items.singleWhere(
|
||||
(item) => item.title == 'Review Q3 budget',
|
||||
);
|
||||
expect(review.projectName, 'Work');
|
||||
expect(review.durationMinutes, 60);
|
||||
expect(review.priority, PriorityLevel.high);
|
||||
expect(review.reward, RewardLevel.medium);
|
||||
expect(review.difficulty, DifficultyLevel.easy);
|
||||
});
|
||||
|
||||
test('seeded review detail stays within deferred metadata scope', () async {
|
||||
final composition = DemoSchedulerComposition.seeded(
|
||||
selectedDate: CivilDate(2025, 5, 20),
|
||||
);
|
||||
addTearDown(composition.dispose);
|
||||
|
||||
final detail = (await composition.managementUseCases.getBacklogTaskDetail(
|
||||
GetBacklogTaskDetailRequest(
|
||||
context: composition.context('demo-review-detail'),
|
||||
localDate: composition.date,
|
||||
taskId: 'review-q3-budget',
|
||||
),
|
||||
)).requireValue;
|
||||
|
||||
expect(detail.item.title, 'Review Q3 budget');
|
||||
expect(detail.notes, isNull);
|
||||
expect(detail.tags, isEmpty);
|
||||
expect(detail.suggestedSlots, isNotEmpty);
|
||||
});
|
||||
}
|
||||
|
||||
/// Visible task titles from the Backlog board mockup that must be present in the seed.
|
||||
const _visibleSeedTitles = <String>[
|
||||
'Reply to 3 emails',
|
||||
'Update project status',
|
||||
'Schedule dentist appt',
|
||||
'Clean up downloads folder',
|
||||
'Plan tomorrow',
|
||||
'Review Q3 budget',
|
||||
'Plan next week',
|
||||
'Define weekly goals',
|
||||
'Time block schedule',
|
||||
'Organize kitchen drawers',
|
||||
'Build analytics dashboard',
|
||||
'Write marketing strategy',
|
||||
'Complete online course',
|
||||
'Module 1: Foundations',
|
||||
'Module 2: Deep Dive',
|
||||
'Module 3: Practice',
|
||||
'Learn guitar',
|
||||
'Start YouTube channel',
|
||||
'Travel to Japan',
|
||||
'Home office upgrade',
|
||||
'Read 2 books',
|
||||
];
|
||||
|
||||
/// Top-level helper that performs the `_distributionCounts` operation for this file.
|
||||
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
|
||||
Map<String, int> _distributionCounts(
|
||||
BacklogDistributionReadModel distribution,
|
||||
) {
|
||||
return {for (final entry in distribution.entries) entry.label: entry.count};
|
||||
}
|
||||
Loading…
Reference in a new issue