forked from eva/focus-flow
test(backlog): complete board validation handoff
This commit is contained in:
parent
3b950358c2
commit
bfba203d1e
21 changed files with 298 additions and 11 deletions
|
|
@ -12,6 +12,9 @@ Included documents cover:
|
|||
ADRs, public API baseline, and requirements traceability matrix.
|
||||
- UI Plan 1, the implemented compact Today timeline mockup plan, including its
|
||||
source mockups and completed block handoff notes.
|
||||
- UI Plan 2, the implemented Backlog board plan, including public scheduler
|
||||
read models, command-backed drawer actions, board summary, and validation
|
||||
handoff notes.
|
||||
- Persistence Plan 1, the implemented SQLite runtime persistence slice for
|
||||
normal Flutter startup and close/reopen task lifecycle proof.
|
||||
- Date Selection 01, the completed day navigation and date picker plan for the
|
||||
|
|
|
|||
|
|
@ -219,3 +219,26 @@ Freeform Tags Metadata.md` for a later migration-backed feature.
|
|||
8. The shared application-command change detector now includes Backlog metadata
|
||||
such as `backlogTags`, so metadata-only commands are persisted even when the
|
||||
command clock equals the task's previous `updatedAt`.
|
||||
|
||||
## Block 09 Validation and Handoff
|
||||
|
||||
1. Added final widget coverage for the default Backlog shell controls, empty
|
||||
New Task title validation, and responsive no-overflow smoke checks at 1672,
|
||||
1440, 1280, and 1100 px desktop widths.
|
||||
2. Added a SQLite schema-column regression test and kept Drift table DSL input
|
||||
files out of the custom runtime coverage denominator. The fresh coverage
|
||||
gate reports 80.34% package `lib/` line coverage.
|
||||
3. Preserved the forbidden-import boundary test so Flutter widgets and
|
||||
controllers continue to use public scheduler APIs instead of scheduler
|
||||
internals, Drift, SQLite adapters, notification internals, backup/export
|
||||
internals, or direct OS APIs outside approved runtime boundaries.
|
||||
4. Final validation passed with:
|
||||
- `scripts/test.sh`
|
||||
- `cd apps/focus_flow_flutter && flutter analyze`
|
||||
- `cd apps/focus_flow_flutter && flutter test`
|
||||
5. Manual live UI review was not possible in this session because the local X11
|
||||
display was refusing new clients with `Maximum number of clients reached`.
|
||||
The diagnosed cause was many duplicate `xbindkeys` clients, not a Flutter
|
||||
build failure.
|
||||
6. Deferred items remain limited to the approved metadata scope: real notes,
|
||||
freeform tags, project edits, and the Backlog drawer `View day` shortcut.
|
||||
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 2.2 MiB |
|
|
@ -12,9 +12,15 @@ on-disk SQLite database and bootstraps owner settings plus the default Home
|
|||
project when they are missing.
|
||||
|
||||
The compact Today screen can toggle completion for currently rendered task
|
||||
cards. Backlog widgets and quick-capture controls exist for tests and focused
|
||||
composition paths, but the sidebar Backlog navigation remains out of scope for
|
||||
this screen.
|
||||
cards. The sidebar Backlog navigation opens the board surface backed by public
|
||||
scheduler application APIs. Backlog supports the four board buckets, search,
|
||||
filters, sort/group controls, summary data, the right-anchored detail drawer,
|
||||
task creation, scheduling, Break Up child-task creation, Someday tagging, and
|
||||
non-destructive archive/remove from active Backlog.
|
||||
|
||||
Backlog notes, project edits, and freeform tag edits intentionally remain
|
||||
read-only or non-mutating placeholders until the deferred metadata migration and
|
||||
application command are implemented.
|
||||
|
||||
## Runtime Data
|
||||
|
||||
|
|
@ -102,13 +108,13 @@ boundary.
|
|||
|
||||
## Intentionally No-op
|
||||
|
||||
- Sidebar navigation.
|
||||
- Date navigation and calendar buttons.
|
||||
- Compact/Normal toggle.
|
||||
- Settings button.
|
||||
- Show upcoming.
|
||||
- Timeline card action icons.
|
||||
- Modal Push, Move to Backlog, and Break up buttons.
|
||||
- Timeline modal Break up.
|
||||
- Backlog drawer project selector, add-tag control, and View day shortcut.
|
||||
|
||||
## Persistence Proof
|
||||
|
||||
|
|
@ -171,7 +177,7 @@ Run these from `apps/focus_flow_flutter/`.
|
|||
|
||||
## Next Plans
|
||||
|
||||
- Wire visible Backlog navigation and quick-capture UI into the main shell.
|
||||
- Wire functional Push, Move to Backlog, and Break up actions.
|
||||
- Add migration-backed notes, project edits, and freeform tags for Backlog
|
||||
metadata.
|
||||
- Add real navigation/settings.
|
||||
- Add Shield/Recovery only in a later scope.
|
||||
|
|
|
|||
|
|
@ -54,10 +54,19 @@ void main() {
|
|||
findsOneWidget,
|
||||
);
|
||||
expect(find.byKey(const ValueKey('backlog-search-field')), findsOneWidget);
|
||||
expect(find.text('Search backlog...'), findsOneWidget);
|
||||
expect(find.text('Filters'), findsOneWidget);
|
||||
expect(find.text('Sort: Custom'), findsOneWidget);
|
||||
expect(find.text('Group: None'), findsOneWidget);
|
||||
expect(find.text('Board'), findsOneWidget);
|
||||
expect(find.text('New Task'), findsOneWidget);
|
||||
expect(find.text('Backlog summary'), findsOneWidget);
|
||||
expect(find.text('24 tasks'), findsOneWidget);
|
||||
expect(find.text('Do Next'), findsOneWidget);
|
||||
expect(find.text('Need Time Block'), findsOneWidget);
|
||||
expect(find.text('Break Up First'), findsOneWidget);
|
||||
expect(find.text('Not Now'), findsOneWidget);
|
||||
expect(find.text('Review Q3 budget'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('seeded Today renders backend-backed compact mockup data', (
|
||||
|
|
|
|||
|
|
@ -207,6 +207,31 @@ void main() {
|
|||
expect(find.text('Draft outline'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('new task dialog validates an empty title without writing', (
|
||||
tester,
|
||||
) async {
|
||||
final harness = _BacklogCommandHarness.empty();
|
||||
addTearDown(harness.dispose);
|
||||
|
||||
await harness.boardController.load();
|
||||
await _pumpBacklogScreen(
|
||||
tester,
|
||||
harness.boardController,
|
||||
commandController: harness.commandController,
|
||||
);
|
||||
|
||||
await tester.tap(find.byKey(const ValueKey('backlog-new-task-button')));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(
|
||||
find.byKey(const ValueKey('backlog-new-task-create-button')),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Title is required.'), findsOneWidget);
|
||||
expect(harness.commandController.state, isA<SchedulerCommandIdle>());
|
||||
expect(harness.store.currentTasks, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('schedule action uses command controller and refreshes board', (
|
||||
tester,
|
||||
) async {
|
||||
|
|
@ -570,6 +595,51 @@ void main() {
|
|||
expect(requests.last.boardFilters.isEmpty, isTrue);
|
||||
expect(find.text('Filters'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('layout avoids overflow at review desktop widths', (
|
||||
tester,
|
||||
) async {
|
||||
final item = _boardItemWithChildren();
|
||||
final board = _boardWithItem(item);
|
||||
final selectedDates = SelectedDateController(
|
||||
initialDate: CivilDate(2026, 7, 7),
|
||||
);
|
||||
final controller = BacklogBoardController(
|
||||
readBoard: (_) async => ApplicationResult.success(board),
|
||||
readTaskDetail: (taskId, localDate) async {
|
||||
return ApplicationResult.success(_detailFor(item));
|
||||
},
|
||||
selectedDates: selectedDates,
|
||||
dateLabelFor: (date) => date.toIsoString(),
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
addTearDown(selectedDates.dispose);
|
||||
|
||||
await controller.load();
|
||||
for (final surfaceSize in _reviewSurfaceSizes) {
|
||||
await _pumpBacklogScreen(
|
||||
tester,
|
||||
controller,
|
||||
surfaceSize: surfaceSize,
|
||||
frameSize: Size(surfaceSize.width - 24, surfaceSize.height - 40),
|
||||
);
|
||||
expect(
|
||||
tester.takeException(),
|
||||
isNull,
|
||||
reason: 'Unexpected layout exception at $surfaceSize.',
|
||||
);
|
||||
}
|
||||
|
||||
await controller.selectTask('plan-week');
|
||||
await _pumpBacklogScreen(
|
||||
tester,
|
||||
controller,
|
||||
surfaceSize: const Size(1100, 760),
|
||||
frameSize: const Size(1076, 720),
|
||||
);
|
||||
expect(find.byKey(const ValueKey('backlog-task-drawer')), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
|
||||
/// Pumps [controller] inside the dark FocusFlow app shell test harness.
|
||||
|
|
@ -577,16 +647,18 @@ Future<void> _pumpBacklogScreen(
|
|||
WidgetTester tester,
|
||||
BacklogBoardController controller, {
|
||||
SchedulerCommandController? commandController,
|
||||
Size surfaceSize = const Size(1320, 820),
|
||||
Size frameSize = const Size(1300, 780),
|
||||
}) async {
|
||||
await tester.binding.setSurfaceSize(const Size(1320, 820));
|
||||
await tester.binding.setSurfaceSize(surfaceSize);
|
||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: FocusFlowTheme.dark(),
|
||||
home: Scaffold(
|
||||
body: SizedBox(
|
||||
width: 1300,
|
||||
height: 780,
|
||||
width: frameSize.width,
|
||||
height: frameSize.height,
|
||||
child: BacklogBoardScreen(
|
||||
controller: controller,
|
||||
commandController: commandController,
|
||||
|
|
@ -598,6 +670,14 @@ Future<void> _pumpBacklogScreen(
|
|||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
/// Desktop widths used for final Backlog layout smoke tests.
|
||||
const _reviewSurfaceSizes = [
|
||||
Size(1672, 940),
|
||||
Size(1440, 860),
|
||||
Size(1280, 820),
|
||||
Size(1100, 760),
|
||||
];
|
||||
|
||||
/// Lets unawaited UI command futures finish and repaint their final state.
|
||||
Future<void> _settleAsyncUi(WidgetTester tester) async {
|
||||
await tester.pumpAndSettle();
|
||||
|
|
|
|||
|
|
@ -46,6 +46,159 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test('opens schema version 2 with expected columns', () async {
|
||||
final db = SchedulerDb(NativeDatabase.memory());
|
||||
addTearDown(db.close);
|
||||
|
||||
Future<List<String>> columnsFor(String tableName) async {
|
||||
final rows =
|
||||
await db.customSelect('PRAGMA table_info("$tableName")').get();
|
||||
return rows.map((row) => row.data['name']! as String).toList();
|
||||
}
|
||||
|
||||
expect(await columnsFor('tasks'), <String>[
|
||||
'id',
|
||||
'owner_id',
|
||||
'title',
|
||||
'project_id',
|
||||
'parent_id',
|
||||
'type',
|
||||
'status',
|
||||
'priority',
|
||||
'reward',
|
||||
'difficulty',
|
||||
'duration_minutes',
|
||||
'scheduled_start_utc',
|
||||
'scheduled_end_utc',
|
||||
'actual_start_utc',
|
||||
'actual_end_utc',
|
||||
'completed_at_utc',
|
||||
'backlog_tags_json',
|
||||
'reminder_override',
|
||||
'stats_json',
|
||||
'backlog_entered_at_utc',
|
||||
'backlog_entered_provenance',
|
||||
'revision',
|
||||
'created_at_utc',
|
||||
'updated_at_utc',
|
||||
]);
|
||||
expect(await columnsFor('task_activities'), <String>[
|
||||
'id',
|
||||
'owner_id',
|
||||
'task_id',
|
||||
'project_id',
|
||||
'operation_id',
|
||||
'code',
|
||||
'occurred_at_utc',
|
||||
'metadata_json',
|
||||
]);
|
||||
expect(await columnsFor('projects'), <String>[
|
||||
'id',
|
||||
'owner_id',
|
||||
'name',
|
||||
'color_key',
|
||||
'default_priority',
|
||||
'default_reward',
|
||||
'default_difficulty',
|
||||
'default_reminder_profile',
|
||||
'default_duration_minutes',
|
||||
'archived_at_utc',
|
||||
'revision',
|
||||
'created_at_utc',
|
||||
'updated_at_utc',
|
||||
]);
|
||||
expect(await columnsFor('project_statistics'), <String>[
|
||||
'project_id',
|
||||
'owner_id',
|
||||
'completed_task_count',
|
||||
'duration_minute_counts_json',
|
||||
'completion_time_bucket_counts_json',
|
||||
'total_pushes_before_completion',
|
||||
'completed_after_push_count',
|
||||
'reward_counts_json',
|
||||
'difficulty_counts_json',
|
||||
'reminder_profile_counts_json',
|
||||
'revision',
|
||||
'created_at_utc',
|
||||
'updated_at_utc',
|
||||
]);
|
||||
expect(await columnsFor('locked_blocks'), <String>[
|
||||
'id',
|
||||
'owner_id',
|
||||
'name',
|
||||
'date',
|
||||
'start_time',
|
||||
'end_time',
|
||||
'recurrence_json',
|
||||
'hidden_by_default',
|
||||
'project_id',
|
||||
'archived_at_utc',
|
||||
'revision',
|
||||
'created_at_utc',
|
||||
'updated_at_utc',
|
||||
]);
|
||||
expect(await columnsFor('locked_overrides'), <String>[
|
||||
'id',
|
||||
'owner_id',
|
||||
'locked_block_id',
|
||||
'date',
|
||||
'type',
|
||||
'name',
|
||||
'start_time',
|
||||
'end_time',
|
||||
'hidden_by_default',
|
||||
'project_id',
|
||||
'revision',
|
||||
'created_at_utc',
|
||||
'updated_at_utc',
|
||||
]);
|
||||
expect(await columnsFor('settings'), <String>[
|
||||
'owner_id',
|
||||
'timezone_id',
|
||||
'day_start_minutes',
|
||||
'day_end_minutes',
|
||||
'compact_mode',
|
||||
'backlog_staleness_json',
|
||||
'revision',
|
||||
'created_at_utc',
|
||||
'updated_at_utc',
|
||||
]);
|
||||
expect(await columnsFor('snapshots'), <String>[
|
||||
'id',
|
||||
'owner_id',
|
||||
'captured_at_utc',
|
||||
'operation_name',
|
||||
'source_date',
|
||||
'target_date',
|
||||
'window_json',
|
||||
'tasks_json',
|
||||
'locked_intervals_json',
|
||||
'required_visible_intervals_json',
|
||||
'notices_json',
|
||||
'changes_json',
|
||||
'overlaps_json',
|
||||
'retention_expires_utc',
|
||||
'truncated',
|
||||
'revision',
|
||||
'created_at_utc',
|
||||
'updated_at_utc',
|
||||
]);
|
||||
expect(await columnsFor('application_operations'), <String>[
|
||||
'operation_id',
|
||||
'owner_id',
|
||||
'operation_name',
|
||||
'committed_at_utc',
|
||||
]);
|
||||
expect(await columnsFor('notice_acknowledgements'), <String>[
|
||||
'owner_id',
|
||||
'notice_id',
|
||||
'acknowledged_at_utc',
|
||||
'revision',
|
||||
'created_at_utc',
|
||||
'updated_at_utc',
|
||||
]);
|
||||
});
|
||||
|
||||
test('migrates schema version 1 file and preserves existing rows', () async {
|
||||
final directory = await Directory.systemTemp.createTemp(
|
||||
'focus-flow-sqlite-migration-',
|
||||
|
|
|
|||
|
|
@ -88,5 +88,18 @@ bool _includeSourceFile(String path) {
|
|||
final normalized = path.replaceAll('\\', '/');
|
||||
return normalized.contains('/packages/') &&
|
||||
normalized.contains('/lib/') &&
|
||||
!normalized.endsWith('.g.dart');
|
||||
!normalized.endsWith('.g.dart') &&
|
||||
!_isBuilderInputSource(normalized);
|
||||
}
|
||||
|
||||
/// Reports whether [path] is source input for code generation, not runtime code.
|
||||
///
|
||||
/// Drift table declarations are intentionally consumed by the builder and then
|
||||
/// executed through generated database classes. Runtime schema behavior is
|
||||
/// covered by SQLite open and migration tests, while the generated files remain
|
||||
/// excluded by the existing `.g.dart` rule above.
|
||||
bool _isBuilderInputSource(String path) {
|
||||
return path.contains(
|
||||
'/packages/scheduler_persistence_sqlite/lib/src/scheduler_db/tables/',
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue