test(timeline): add timeline state regression suite

This commit is contained in:
Ashley Venn 2026-06-24 13:59:26 -07:00
parent 88434e3805
commit abf069db1f
4 changed files with 171 additions and 5 deletions

View file

@ -1,6 +1,6 @@
# V1 Block 08 — Today Timeline State
Status: In progress — Chunks 8.1, 8.2, and 8.3 complete
Status: Complete
Purpose: Prepare domain/view-state data for the Today timeline without building full Flutter UI yet unless explicitly requested.
@ -149,6 +149,15 @@ Acceptance criteria:
- Mapping rules are tested without depending on a UI framework.
- Existing scheduling, backlog, locked-block, and task-action tests still pass.
Completed:
- Expanded timeline regression coverage for all task-type background tokens.
- Added exhaustive reward icon token coverage for every reward level plus `notSet`.
- Added exhaustive difficulty icon token coverage for every difficulty level plus `notSet`.
- Added focused compact-mode reduced-output regression coverage.
- Confirmed existing timeline tests cover flexible duration display, required explicit time display, reveal-gated locked overlays, locked overlay category, project color tokens, and flexible quick actions.
- Verified with `dart format lib test`, `dart analyze`, and `dart test`.
Commit suggestion:
```text

View file

@ -1,6 +1,6 @@
# V1 Block 08 — Today Timeline State
Status: In progress — Chunks 8.1, 8.2, and 8.3 complete
Status: Complete
Purpose: Prepare domain/view-state data for the Today timeline without building full Flutter UI yet unless explicitly requested.
@ -149,6 +149,15 @@ Acceptance criteria:
- Mapping rules are tested without depending on a UI framework.
- Existing scheduling, backlog, locked-block, and task-action tests still pass.
Completed:
- Expanded timeline regression coverage for all task-type background tokens.
- Added exhaustive reward icon token coverage for every reward level plus `notSet`.
- Added exhaustive difficulty icon token coverage for every difficulty level plus `notSet`.
- Added focused compact-mode reduced-output regression coverage.
- Confirmed existing timeline tests cover flexible duration display, required explicit time display, reveal-gated locked overlays, locked overlay category, project color tokens, and flexible quick actions.
- Verified with `dart format lib test`, `dart analyze`, and `dart test`.
Commit suggestion:
```text

View file

@ -1,7 +1,7 @@
# Current Software Plan
Execute active V1 block documents in numeric order. Blocks 01-07 are completed
and archived; the next active block is Block 08.
Execute active V1 block documents in numeric order. Blocks 01-08 are completed
and archived; the next active block is Block 09.
## Execution rules
@ -33,6 +33,6 @@ Do not infer a new level name.
5. `V1_BLOCK_05_Recurring_Locked_Blocks.md`
6. `V1_BLOCK_06_Task_Actions_State_Transitions_UPDATED.md` — archived
7. `V1_BLOCK_07_Child_Tasks.md` — archived
8. `V1_BLOCK_08_Today_Timeline_State.md`
8. `V1_BLOCK_08_Today_Timeline_State.md` — archived
9. `V1_BLOCK_09_Persistence_Preparation.md`
10. `V1_BLOCK_10_Testing_Documentation_Handoff.md`

View file

@ -44,6 +44,96 @@ void main() {
]);
});
test('background token maps from every task type', () {
final expectedTokens = {
TaskType.flexible: TimelineBackgroundToken.flexible,
TaskType.inflexible: TimelineBackgroundToken.inflexible,
TaskType.critical: TimelineBackgroundToken.critical,
TaskType.locked: TimelineBackgroundToken.locked,
TaskType.surprise: TimelineBackgroundToken.surprise,
TaskType.freeSlot: TimelineBackgroundToken.freeSlot,
};
for (final entry in expectedTokens.entries) {
final task = scheduledTask(
id: entry.key.name,
title: entry.key.name,
projectId: project.id,
type: entry.key,
createdAt: createdAt,
start: start,
end: end,
);
expect(
mapper.fromTask(task).backgroundToken,
entry.value,
reason: entry.key.name,
);
}
});
test('reward icon token handles every reward level plus not set', () {
final expectedTokens = {
RewardLevel.notSet: TimelineRewardIconToken.notSet,
RewardLevel.veryLow: TimelineRewardIconToken.veryLow,
RewardLevel.low: TimelineRewardIconToken.low,
RewardLevel.medium: TimelineRewardIconToken.medium,
RewardLevel.high: TimelineRewardIconToken.high,
RewardLevel.veryHigh: TimelineRewardIconToken.veryHigh,
};
for (final entry in expectedTokens.entries) {
final task = scheduledTask(
id: entry.key.name,
title: entry.key.name,
projectId: project.id,
type: TaskType.flexible,
reward: entry.key,
createdAt: createdAt,
start: start,
end: end,
);
expect(
mapper.fromTask(task).rewardIconToken,
entry.value,
reason: entry.key.name,
);
}
});
test('difficulty icon token handles every difficulty level plus not set',
() {
final expectedTokens = {
DifficultyLevel.notSet: TimelineDifficultyIconToken.notSet,
DifficultyLevel.veryEasy: TimelineDifficultyIconToken.veryEasy,
DifficultyLevel.easy: TimelineDifficultyIconToken.easy,
DifficultyLevel.medium: TimelineDifficultyIconToken.medium,
DifficultyLevel.hard: TimelineDifficultyIconToken.hard,
DifficultyLevel.veryHard: TimelineDifficultyIconToken.veryHard,
};
for (final entry in expectedTokens.entries) {
final task = scheduledTask(
id: entry.key.name,
title: entry.key.name,
projectId: project.id,
type: TaskType.flexible,
difficulty: entry.key,
createdAt: createdAt,
start: start,
end: end,
);
expect(
mapper.fromTask(task).difficultyIconToken,
entry.value,
reason: entry.key.name,
);
}
});
test('flexible task cards display duration while retaining position', () {
final task = scheduledTask(
id: 'laundry',
@ -331,6 +421,64 @@ void main() {
expect(state.nextRequiredItem!.id, 'required');
expect(state.compactItems.map((item) => item.id), ['required']);
});
test('compact mode returns only the reduced current and next items', () {
final current = scheduledTask(
id: 'current',
title: 'Current',
projectId: project.id,
type: TaskType.flexible,
createdAt: createdAt,
start: DateTime(2026, 6, 20, 9),
end: DateTime(2026, 6, 20, 10),
);
final nextRequired = scheduledTask(
id: 'next-required',
title: 'Next required',
projectId: project.id,
type: TaskType.inflexible,
createdAt: createdAt,
start: DateTime(2026, 6, 20, 10),
end: DateTime(2026, 6, 20, 10, 30),
);
final laterRequired = scheduledTask(
id: 'later-required',
title: 'Later required',
projectId: project.id,
type: TaskType.critical,
createdAt: createdAt,
start: DateTime(2026, 6, 20, 12),
end: DateTime(2026, 6, 20, 12, 30),
);
final laterFlexible = scheduledTask(
id: 'later-flexible',
title: 'Later flexible',
projectId: project.id,
type: TaskType.flexible,
createdAt: createdAt,
start: DateTime(2026, 6, 20, 13),
end: DateTime(2026, 6, 20, 13, 30),
);
final state = mapper.compactStateForTasks(
tasks: [laterFlexible, laterRequired, nextRequired, current],
now: DateTime(2026, 6, 20, 9, 15),
manualCompactModeEnabled: true,
);
expect(state.compactItems.map((item) => item.id), [
'current',
'next-required',
]);
expect(
state.compactItems.map((item) => item.id),
isNot(contains('later-required')),
);
expect(
state.compactItems.map((item) => item.id),
isNot(contains('later-flexible')),
);
});
});
}