chore: Halkyon Learn moves into apps/learn

Its server, client, app and curator, and the tools that write its
assets (maps, Anki fixtures, icons), as they are; paths are fixed in
the commits that follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-19 20:08:06 +02:00
commit 649499be23
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -0,0 +1,80 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
import 'package:halkyon_learn_flutter/src/feature/note/model/note_content.dart';
import 'package:halkyon_learn_flutter/src/feature/note/model/note_merge.dart';
import 'package:halkyon_learn_flutter/src/feature/note/model/note_revision.dart';
/// Rich content holding just [text], enough to tell versions apart.
RichContent text(String text) => RichContent({
'document': {'type': 'page', 'note': text},
});
NoteContent note(String title, String body) =>
NoteContent(title: title, body: text(body));
/// A revision with the id [id] (ids sort by creation, like UUID v7).
NoteRevision revision(
String id,
NoteContent content, {
Set<String> parents = const {},
}) => NoteRevision(id: id, parents: parents, content: content);
void main() {
test('the newest revision of a chain is the current one', () {
final merged = mergeNoteRevisions([
revision('r1', note('Particles', 'は marks the topic')),
revision('r2', note('Particles', 'は and が'), parents: {'r1'}),
])!;
expect(merged.content, note('Particles', 'は and が'));
expect(merged.heads, {'r2'});
expect(merged.conflicts, isEmpty);
});
test('a title changed on one device and the body on another both '
'survive', () {
final merged = mergeNoteRevisions([
revision('r1', note('Particles', 'は marks the topic')),
revision('r2', note('は and が', 'は marks the topic'), parents: {'r1'}),
revision('r3', note('Particles', 'は and が'), parents: {'r1'}),
])!;
expect(merged.content, note('は and が', 'は and が'));
expect(merged.conflicts, isEmpty);
});
test('a body changed differently on two devices is a conflict with both '
'versions, the newest showing', () {
final merged = mergeNoteRevisions([
revision('r1', note('Particles', 'は marks the topic')),
revision('r2', note('Particles', 'は: topic'), parents: {'r1'}),
revision('r3', note('Particles', 'が: subject'), parents: {'r1'}),
])!;
expect(merged.conflicts, {
NoteField.body: [text('は: topic'), text('が: subject')],
});
expect(merged.content.body, text('が: subject'));
expect(merged.heads, {'r2', 'r3'});
});
test('without revisions there is no note', () {
expect(mergeNoteRevisions(const []), isNull);
});
group('NoteContent', () {
test('is blank with neither title nor text', () {
expect(NoteContent.empty().isBlank, isTrue);
expect(note(' ', '').isBlank, isTrue);
expect(note('Particles', '').isBlank, isFalse);
expect(
NoteContent(body: RichContent.plain('は')).isBlank,
isFalse,
);
});
test('is stored with its title trimmed', () {
expect(note(' Particles ', 'は').normalized, note('Particles', 'は'));
});
});
}