feat(todo): comments on a task (ADR 0033, D36)

Plain text with links, written, edited and deleted in the task's sheet,
counted on its row. They are sealed in `task_comment`, the task they belong
to with them, and go with their task.

Whats-New: Comments: write on a task, edit or delete what you wrote, and open its links.
Whats-New-De: Kommentare: Schreibe zu einer Aufgabe, bearbeite oder lösche es und öffne Links darin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-06 19:53:28 +01:00
commit 016b74ff74
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -9,6 +9,8 @@ import 'package:halkyon_app/testing.dart';
import 'package:halkyon_todo_flutter/src/app/todo_app.dart';
import 'package:halkyon_todo_flutter/src/app/todo_dependencies.dart';
import 'package:halkyon_todo_flutter/src/core/privacy/todo_keys.dart';
import 'package:halkyon_todo_flutter/src/feature/comment/data/comment_repository.dart';
import 'package:halkyon_todo_flutter/src/feature/comment/widget/task_comments.dart';
import 'package:halkyon_todo_flutter/src/feature/filter/data/filter_repository.dart';
import 'package:halkyon_todo_flutter/src/feature/filter/model/saved_filter.dart';
import 'package:halkyon_todo_flutter/src/feature/filter/widget/filter_editor.dart';
@ -70,6 +72,7 @@ void main() {
late MemorySealedRowGateway taskRows;
late MemorySealedRowGateway labelRows;
late MemorySealedRowGateway filterRows;
late MemorySealedRowGateway commentRows;
late MemorySealedRowGateway settingsRows;
late MemoryReminderGateway reminders;
late MemoryLocationGateway locationGateway;
@ -101,6 +104,7 @@ void main() {
taskRows = MemorySealedRowGateway();
labelRows = MemorySealedRowGateway();
filterRows = MemorySealedRowGateway();
commentRows = MemorySealedRowGateway();
settingsRows = MemorySealedRowGateway();
reminders = MemoryReminderGateway();
locationGateway = MemoryLocationGateway();
@ -204,6 +208,7 @@ void main() {
),
labels: LabelRepository(labelRows, keys),
filters: FilterRepository(filterRows, keys),
comments: CommentRepository(commentRows, keys),
settings: settings,
reminders: reminders,
locationReminders: locationGateway,
@ -406,6 +411,101 @@ void main() {
expect(find.text('No labels yet.'), findsOneWidget);
});
group('comments (D36)', () {
Future<void> openTask(WidgetTester tester) async {
await tester.runAsync(() async {
final inbox = await ProjectRepository(
projectRows,
keys,
).ensureInbox();
await TaskRepository(
taskRows,
keys,
).add(TaskContent(title: 'Vaccination', projectId: inbox));
});
projectRows.bringRemoteChanges();
taskRows.bringRemoteChanges();
await tester.pumpAndSettle();
await goTab(tester, 'Inbox');
await tester.tap(find.text('Vaccination'));
await tester.pumpAndSettle();
}
Future<void> write(WidgetTester tester, String text) async {
await tester.ensureVisible(find.byKey(commentFieldKey));
await tester.pumpAndSettle();
await tester.enterText(find.byKey(commentFieldKey), text);
await tester.pumpAndSettle();
await tester.tap(find.byKey(commentSendKey));
await tester.pumpAndSettle();
commentRows.bringRemoteChanges();
await tester.pumpAndSettle();
}
testWidgets('a comment written shows in the sheet, the field empties, '
'and nothing blank is sent', (tester) async {
await start(tester, usedBefore: true);
await openTask(tester);
await tester.ensureVisible(find.byKey(commentFieldKey));
await tester.pumpAndSettle();
await tester.enterText(find.byKey(commentFieldKey), ' ');
await tester.pumpAndSettle();
expect(
tester.widget<IconButton>(find.byKey(commentSendKey)).onPressed,
isNull,
);
await write(tester, 'Ask the vet');
expect(find.text('Ask the vet', findRichText: true), findsOneWidget);
expect(find.text('Comments 1'), findsOneWidget);
expect(
tester
.widget<TextField>(find.byKey(commentFieldKey))
.controller!
.text,
isEmpty,
);
});
testWidgets('a comment edited says so; one deleted is gone, after '
'asking', (tester) async {
await start(tester, usedBefore: true);
await openTask(tester);
await write(tester, 'Ask the vet');
await tester.tap(find.byTooltip('Comment menu'));
await tester.pumpAndSettle();
await tester.tap(find.text('Edit comment'));
await tester.pumpAndSettle();
await tester.enterText(
find.descendant(
of: find.byType(AlertDialog),
matching: find.byType(TextField),
),
'Ask the nurse',
);
await tester.tap(find.text('Save'));
await tester.pumpAndSettle();
commentRows.bringRemoteChanges();
await tester.pumpAndSettle();
expect(find.text('Ask the nurse', findRichText: true), findsOneWidget);
expect(find.textContaining(', edited'), findsOneWidget);
await tester.tap(find.byTooltip('Comment menu'));
await tester.pumpAndSettle();
await tester.tap(find.text('Delete comment'));
await tester.pumpAndSettle();
await tester.tap(find.text('Delete comment').last);
await tester.pumpAndSettle();
commentRows.bringRemoteChanges();
await tester.pumpAndSettle();
expect(find.text('Comments'), findsOneWidget);
});
});
group('filters and labels (D35)', () {
Future<void> openFilters(WidgetTester tester) async {
await goTab(tester, 'Browse');