feat(todo): a task's locations, in its payload and as quick add's + (ADR 0033, D32)
TaskLocation (name, address, position, optional OSM ID, reminder) goes in the sealed payload as `locations`, schema still 1; recent places are read from the tasks. parseQuickAdd takes `+name` of a place named before, the longest name first, kept as text as any other token (D29); an unknown `+word` stays text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
011b144ba1
commit
1db888a679
2 changed files with 300 additions and 8 deletions
|
|
@ -0,0 +1,83 @@
|
|||
import 'package:halkyon_todo_flutter/src/feature/location/model/task_location.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/data/task_payload.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/model/task.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
/// A task's locations (ADR 0033, D32).
|
||||
void main() {
|
||||
const edeka = TaskLocation(
|
||||
name: 'EDEKA',
|
||||
lat: 47.55,
|
||||
lon: 9.70,
|
||||
address: 'Ludwig-Kick-Straße 1, Aeschach, Lindau',
|
||||
osmType: 'W',
|
||||
osmId: 42,
|
||||
trigger: LocationTrigger.arrive,
|
||||
);
|
||||
const lidl = TaskLocation(name: 'Lidl', lat: 47.56, lon: 9.71);
|
||||
|
||||
group('in the sealed payload', () {
|
||||
test('every field comes back, a location without a reminder too', () {
|
||||
const task = TaskContent(
|
||||
title: 'Shop',
|
||||
projectId: 'p',
|
||||
locations: [edeka, lidl],
|
||||
);
|
||||
expect(decodeTask(encodeTask(task)), task);
|
||||
});
|
||||
|
||||
test('its schema stays 1', () {
|
||||
const task = TaskContent(
|
||||
title: 'Shop',
|
||||
projectId: 'p',
|
||||
locations: [edeka],
|
||||
);
|
||||
expect(decodeTask(encodeTask(task))!.unknown, isEmpty);
|
||||
expect(taskSchemaVersion, 1);
|
||||
});
|
||||
|
||||
test('other fields changing keep them', () {
|
||||
const task = TaskContent(
|
||||
title: 'Shop',
|
||||
projectId: 'p',
|
||||
locations: [edeka],
|
||||
);
|
||||
expect(task.copyWith(title: 'B').locations, [edeka]);
|
||||
expect(task.copyWith(locations: const []).locations, isEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
group('recent locations', () {
|
||||
test('each place once, reminding of nothing, the first first', () {
|
||||
expect(
|
||||
recentLocations([edeka, lidl, edeka.reminding(LocationTrigger.leave)]),
|
||||
[edeka.reminding(LocationTrigger.none), lidl],
|
||||
);
|
||||
});
|
||||
|
||||
test('no more than the limit', () {
|
||||
expect(recentLocations([edeka, lidl], limit: 1), hasLength(1));
|
||||
});
|
||||
|
||||
test('two of a name at different places stay two', () {
|
||||
const other = TaskLocation(name: 'EDEKA', lat: 47.60, lon: 9.65);
|
||||
expect(recentLocations([edeka, other]), hasLength(2));
|
||||
});
|
||||
});
|
||||
|
||||
test('adding a place again replaces it', () {
|
||||
expect(
|
||||
withLocation(const [
|
||||
edeka,
|
||||
lidl,
|
||||
], edeka.reminding(LocationTrigger.leave)),
|
||||
[lidl, edeka.reminding(LocationTrigger.leave)],
|
||||
);
|
||||
});
|
||||
|
||||
test("a task's row names the first and how many more", () {
|
||||
expect(locationsText(const []), isNull);
|
||||
expect(locationsText(const [lidl]), 'Lidl');
|
||||
expect(locationsText(const [edeka, lidl]), 'EDEKA +1');
|
||||
});
|
||||
}
|
||||
|
|
@ -16,6 +16,11 @@ void main() {
|
|||
];
|
||||
const labels = [(id: 'l1', name: 'HalkyonToDo'), (id: 'l2', name: 'errand')];
|
||||
|
||||
const locations = [
|
||||
(id: 'edeka', name: 'EDEKA'),
|
||||
(id: 'koros', name: 'EDEKA Koros'),
|
||||
];
|
||||
|
||||
ParsedTask parse(
|
||||
String text, {
|
||||
List<QuickAddWords> words = const [QuickAddWords.en],
|
||||
|
|
@ -26,9 +31,45 @@ void main() {
|
|||
words: words,
|
||||
projects: projects,
|
||||
labels: labels,
|
||||
locations: locations,
|
||||
ignored: ignored,
|
||||
);
|
||||
|
||||
group('+location (D32)', () {
|
||||
test('a place named before, the longest name, the case aside', () {
|
||||
final added = parse('Shop +edeka koros tomorrow');
|
||||
expect(added.title, 'Shop');
|
||||
expect(added.locationIds, ['koros']);
|
||||
expect(added.tokens.first.kind, QuickAddKind.location);
|
||||
});
|
||||
|
||||
test('several, each once', () {
|
||||
expect(parse('Shop +EDEKA +edeka +EDEKA Koros').locationIds, [
|
||||
'edeka',
|
||||
'koros',
|
||||
]);
|
||||
});
|
||||
|
||||
test("one not named before stays text: there's nothing to take", () {
|
||||
final added = parse('Shop +Rewe');
|
||||
expect(added.title, 'Shop +Rewe');
|
||||
expect(added.locationIds, isEmpty);
|
||||
});
|
||||
|
||||
test('a + inside a word is none', () {
|
||||
expect(parse('a+EDEKA').locationIds, isEmpty);
|
||||
});
|
||||
|
||||
test('kept as text where the person said so (D29), there alone', () {
|
||||
final added = parse(
|
||||
'Shop +EDEKA and +EDEKA',
|
||||
ignored: const [(start: 5, end: 11)],
|
||||
);
|
||||
expect(added.title, 'Shop +EDEKA and');
|
||||
expect(added.locationIds, ['edeka']);
|
||||
});
|
||||
});
|
||||
|
||||
group('symbols, in every language', () {
|
||||
test('#project, the longest name matching, the case aside', () {
|
||||
final added = parse('Clean Gutters #inbox');
|
||||
|
|
|
|||
Loading…
Reference in a new issue