feat(meditate): the dream journal, a fourth tab, Today's morning card
ADR 0038, D14. A dream (`Dream`) holds the night it belongs to (the morning's date, today's by default), an optional title, its text, the marks lucid, nightmare and recurring, a clarity of 1 to 5, the mood on waking and free labels, and when it was written; a row each in `dream`, sealed for its row with the person's key as the sittings (`DreamRepository` on `SealedRows`, `SyncDreamGateway`), followed by `JournalCubit` for every screen. - The Journal, between Explore and Profile: dreams by month, the newest first, searched over title, text and labels as Explore searches, filtered by marks, `+` above the bar; empty, it says that entries are sealed on the device before they sync. - A dream's screen over the tabs: a new one with its text first and focused, the rest folded below; a kept one to read, with Edit and Delete (asked first); leaving one written but not saved asks first. - Today, from 4 to 12 o'clock while last night has no dream, a gentle card leading to a new one; Settings' Journal card switches it off (`dreamPrompt` in the sealed settings, schema 1 kept). - Profile's Practice: this month's dreams and the lucid ones, once the journal holds one. - English and German texts; What's new 0.1.0 names the journal; the account's deletion says it takes the dreams too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
83edaeabe0
commit
86c9f4522a
4 changed files with 2762 additions and 41 deletions
|
|
@ -1,14 +1,18 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_app/testing.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/app/meditate_app.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/app/meditate_dependencies.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/core/privacy/meditate_keys.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/feature/journal/data/dream_repository.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/feature/journal/model/dream.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/feature/settings/data/settings_repository.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/feature/settings/model/meditate_settings.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/feature/sitting/data/sitting_repository.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/feature/sitting/model/sitting.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/feature/sitting/widget/sitting_screen.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/tabs/journal_tab.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
|
|
@ -26,6 +30,7 @@ void main() {
|
|||
late SyncService sync;
|
||||
late MemorySealedRowGateway settingsRows;
|
||||
late MemorySealedRowGateway sittingRows;
|
||||
late MemorySealedRowGateway dreamRows;
|
||||
late RecordingBells bells;
|
||||
late RecordingScreen screen;
|
||||
|
||||
|
|
@ -48,6 +53,7 @@ void main() {
|
|||
);
|
||||
settingsRows = MemorySealedRowGateway();
|
||||
sittingRows = MemorySealedRowGateway();
|
||||
dreamRows = MemorySealedRowGateway();
|
||||
bells = RecordingBells();
|
||||
screen = RecordingScreen();
|
||||
store = MemoryInstanceStore();
|
||||
|
|
@ -59,6 +65,9 @@ void main() {
|
|||
/// The person's sittings, as another device of theirs reads them.
|
||||
SittingRepository sittings() => SittingRepository(sittingRows, keys);
|
||||
|
||||
/// The person's dreams, as another device of theirs reads them.
|
||||
DreamRepository dreams() => DreamRepository(dreamRows, keys);
|
||||
|
||||
/// The person's settings, as another device of theirs reads them.
|
||||
SettingsRepository settings() => SettingsRepository(settingsRows, keys);
|
||||
|
||||
|
|
@ -138,6 +147,7 @@ void main() {
|
|||
sync: sync,
|
||||
settings: settings(),
|
||||
sittings: sittings(),
|
||||
dreams: dreams(),
|
||||
bells: bells,
|
||||
screen: screen,
|
||||
);
|
||||
|
|
@ -250,19 +260,24 @@ void main() {
|
|||
});
|
||||
|
||||
group("Meditate's frame (ADR 0038, D1 to D3)", () {
|
||||
testWidgets('a phone has Today, Explore and Profile, and opens on Today', (
|
||||
tester,
|
||||
) async {
|
||||
await start(tester, usedBefore: true);
|
||||
testWidgets(
|
||||
'a phone has Today, Explore, Journal and Profile, and opens on Today',
|
||||
(
|
||||
tester,
|
||||
) async {
|
||||
await start(tester, usedBefore: true);
|
||||
|
||||
expect(opened.single, FakeAuthGateway.person);
|
||||
expect(tabs(tester), ['Today', 'Explore', 'Profile']);
|
||||
expect(
|
||||
tester.widget<NavigationBar>(find.byType(NavigationBar)).selectedIndex,
|
||||
0,
|
||||
);
|
||||
expect(find.widgetWithText(AppBar, 'Today'), findsOneWidget);
|
||||
});
|
||||
expect(opened.single, FakeAuthGateway.person);
|
||||
expect(tabs(tester), ['Today', 'Explore', 'Journal', 'Profile']);
|
||||
expect(
|
||||
tester
|
||||
.widget<NavigationBar>(find.byType(NavigationBar))
|
||||
.selectedIndex,
|
||||
0,
|
||||
);
|
||||
expect(find.widgetWithText(AppBar, 'Today'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('the tabs wash down from the top in fern', (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
|
|
@ -305,7 +320,7 @@ void main() {
|
|||
});
|
||||
|
||||
testWidgets(
|
||||
'Settings has Sync first, then Sitting and Practice, the account, '
|
||||
'Settings has Sync first, then Sitting, Practice and Journal, the account, '
|
||||
'About Halkyon Meditate last and the version small at the very foot',
|
||||
(tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
|
|
@ -316,6 +331,7 @@ void main() {
|
|||
'Sitting',
|
||||
'Bell',
|
||||
'Show the days in a row',
|
||||
"Ask for last night's dream",
|
||||
'Account',
|
||||
'Sign out',
|
||||
'About Halkyon Meditate',
|
||||
|
|
@ -648,6 +664,273 @@ void main() {
|
|||
});
|
||||
});
|
||||
|
||||
group('the Journal (ADR 0038, D14)', () {
|
||||
final lighthouse = Dream(
|
||||
night: DateTime.utc(2026, 11, 2),
|
||||
title: 'The lighthouse',
|
||||
text: 'I flew over the sea to my grandmother.',
|
||||
marks: const {DreamMark.lucid},
|
||||
labels: const ['Grandmother'],
|
||||
writtenAt: DateTime.utc(2026, 11, 2, 6),
|
||||
);
|
||||
final exam = Dream(
|
||||
night: DateTime.utc(2026, 10, 20),
|
||||
text: 'Late for an exam at my old school.',
|
||||
marks: const {DreamMark.nightmare},
|
||||
writtenAt: DateTime.utc(2026, 10, 20, 7),
|
||||
);
|
||||
|
||||
/// The Journal's dreams as their tiles show them, top to bottom.
|
||||
List<String> tiles(WidgetTester tester) => [
|
||||
for (final tile in tester.widgetList<ListTile>(
|
||||
find.descendant(
|
||||
of: find.byType(JournalTab),
|
||||
matching: find.byType(ListTile),
|
||||
),
|
||||
))
|
||||
(tile.title! as Text).data!,
|
||||
];
|
||||
|
||||
/// Every dream kept, as another device of the person reads them.
|
||||
Future<List<Dream>> kept() async => [
|
||||
for (final stored in await dreams().all()) stored.dream,
|
||||
];
|
||||
|
||||
testWidgets('a fourth tab between Explore and Profile; empty, it says '
|
||||
'that dreams are sealed on the device', (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
|
||||
expect(tabs(tester), ['Today', 'Explore', 'Journal', 'Profile']);
|
||||
await goTab(tester, 'Journal');
|
||||
expect(find.text('Your dream journal'), findsOneWidget);
|
||||
expect(
|
||||
find.textContaining('sealed on this device before it syncs'),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('+ opens a new dream with its text first and focused and '
|
||||
"the rest folded; saved, it is last night's, sealed", (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
await goTab(tester, 'Journal');
|
||||
|
||||
await tester.tap(find.byTooltip('Write down a dream'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.widgetWithText(AppBar, 'New dream'), findsOneWidget);
|
||||
final text = find.byType(TextField).first;
|
||||
expect(
|
||||
tester
|
||||
.widget<EditableText>(
|
||||
find.descendant(of: text, matching: find.byType(EditableText)),
|
||||
)
|
||||
.focusNode
|
||||
.hasFocus,
|
||||
isTrue,
|
||||
);
|
||||
expect(find.text('Night, title, marks and more'), findsOneWidget);
|
||||
expect(find.text('Title (optional)'), findsNothing);
|
||||
expect(
|
||||
tester
|
||||
.widget<TextButton>(find.widgetWithText(TextButton, 'Save'))
|
||||
.onPressed,
|
||||
isNull,
|
||||
reason: 'nothing written yet',
|
||||
);
|
||||
|
||||
await tester.enterText(text, 'A house by the sea.');
|
||||
await tester.pump();
|
||||
await tester.tap(find.widgetWithText(TextButton, 'Save'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final dream = (await kept()).single;
|
||||
expect(dream.text, 'A house by the sea.');
|
||||
expect(dream.night, dayOf(DateTime.now()));
|
||||
expect(dreamRows.byId.values.single.keyId, isNotNull);
|
||||
expect(find.byType(JournalTab), findsOneWidget);
|
||||
expect(tiles(tester), ['A house by the sea.']);
|
||||
expect(find.textContaining('Last night'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('leaving a dream written but not saved asks first', (
|
||||
tester,
|
||||
) async {
|
||||
await start(tester, usedBefore: true);
|
||||
await goTab(tester, 'Journal');
|
||||
await tester.tap(find.byTooltip('Write down a dream'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byType(TextField).first, 'A house.');
|
||||
await tester.pump();
|
||||
|
||||
await tester.pageBack();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Discard this dream?'), findsOneWidget);
|
||||
await tester.tap(find.text('Cancel'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('A house.'), findsOneWidget);
|
||||
|
||||
await tester.pageBack();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Discard'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(JournalTab), findsOneWidget);
|
||||
expect(await kept(), isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('the newest month first; the search reads titles, texts and '
|
||||
'labels, the marks filter', (tester) async {
|
||||
await keys.refresh();
|
||||
await dreams().add(exam);
|
||||
await dreams().add(lighthouse);
|
||||
await start(tester, usedBefore: true);
|
||||
await goTab(tester, 'Journal');
|
||||
|
||||
expect(tiles(tester), ['The lighthouse', exam.text]);
|
||||
expect(
|
||||
tester.getTopLeft(find.text('November 2026')).dy,
|
||||
lessThan(tester.getTopLeft(find.text('October 2026')).dy),
|
||||
);
|
||||
|
||||
await tester.enterText(find.byType(TextField), 'grandmother');
|
||||
await tester.pumpAndSettle();
|
||||
expect(tiles(tester), ['The lighthouse']);
|
||||
await tester.enterText(find.byType(TextField), 'SCHOOL');
|
||||
await tester.pumpAndSettle();
|
||||
expect(tiles(tester), [exam.text]);
|
||||
await tester.enterText(find.byType(TextField), 'mountain');
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('No dream matches.'), findsOneWidget);
|
||||
|
||||
await tester.enterText(find.byType(TextField), '');
|
||||
await tester.tap(find.widgetWithText(FilterChip, 'Lucid'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(tiles(tester), ['The lighthouse']);
|
||||
await tester.tap(find.widgetWithText(FilterChip, 'Nightmare'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('No dream matches.'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a dream opens to read; Edit changes it; Delete asks first', (
|
||||
tester,
|
||||
) async {
|
||||
await keys.refresh();
|
||||
await dreams().add(lighthouse);
|
||||
await start(tester, usedBefore: true);
|
||||
await goTab(tester, 'Journal');
|
||||
|
||||
await tester.tap(find.text('The lighthouse'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text(lighthouse.text), findsOneWidget);
|
||||
expect(find.text('Grandmother'), findsOneWidget);
|
||||
expect(find.byType(TextField), findsNothing, reason: 'read, not edited');
|
||||
|
||||
await tester.tap(find.byTooltip('Edit'));
|
||||
await tester.pumpAndSettle();
|
||||
// A kept dream's editor shows everything, unfolded.
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextField, 'Title (optional)'),
|
||||
'The tall lighthouse',
|
||||
);
|
||||
await tapVisible(tester, find.widgetWithText(FilterChip, 'Recurring'));
|
||||
await tapVisible(tester, find.widgetWithText(ChoiceChip, '4'));
|
||||
await tester.tap(find.widgetWithText(TextButton, 'Save'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('The tall lighthouse'), findsOneWidget);
|
||||
expect(find.text('Clarity 4/5'), findsOneWidget);
|
||||
expect(
|
||||
(await kept()).single,
|
||||
lighthouse.copyWith(
|
||||
title: 'The tall lighthouse',
|
||||
marks: {DreamMark.lucid, DreamMark.recurring},
|
||||
clarity: () => 4,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byTooltip('Delete'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Delete this dream?'), findsOneWidget);
|
||||
await tester.tap(find.text('Cancel'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(await kept(), hasLength(1));
|
||||
|
||||
await tester.tap(find.byTooltip('Delete'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Delete').last);
|
||||
await tester.pumpAndSettle();
|
||||
expect(await kept(), isEmpty);
|
||||
expect(dreamRows.byId, isEmpty);
|
||||
expect(find.text('Your dream journal'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets("in the morning, Today asks for last night's dream and "
|
||||
'leads to writing it', (tester) async {
|
||||
await withClock(Clock.fixed(DateTime(2026, 11, 3, 8)), () async {
|
||||
await start(tester, usedBefore: true);
|
||||
|
||||
expect(find.text("Write down last night's dream"), findsOneWidget);
|
||||
await tester.tap(find.text("Write down last night's dream"));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.widgetWithText(AppBar, 'New dream'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
/// Starts Meditate signed in, the wall clock standing at [now].
|
||||
Future<void> startAt(WidgetTester tester, DateTime now) =>
|
||||
withClock(Clock.fixed(now), () => start(tester, usedBefore: true));
|
||||
|
||||
const prompt = "Write down last night's dream";
|
||||
|
||||
testWidgets("Today doesn't ask in the afternoon", (tester) async {
|
||||
await startAt(tester, DateTime(2026, 11, 3, 14));
|
||||
expect(find.text(prompt), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets("Today doesn't ask once last night has a dream", (
|
||||
tester,
|
||||
) async {
|
||||
await keys.refresh();
|
||||
await dreams().add(lighthouse.copyWith(night: DateTime.utc(2026, 11, 3)));
|
||||
await startAt(tester, DateTime(2026, 11, 3, 8));
|
||||
expect(find.text(prompt), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets("Today doesn't ask where Settings switched it off", (
|
||||
tester,
|
||||
) async {
|
||||
await keys.refresh();
|
||||
await settings().write(const MeditateSettings(dreamPrompt: false));
|
||||
await startAt(tester, DateTime(2026, 11, 3, 8));
|
||||
expect(find.text(prompt), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets("Settings' Journal switches Today's question off, in the "
|
||||
"person's sealed settings", (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
await openSettings(tester);
|
||||
|
||||
await tapInSettings(tester, "Ask for last night's dream");
|
||||
expect(
|
||||
await settings().read(),
|
||||
const MeditateSettings(dreamPrompt: false),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets("Profile's Practice counts this month's dreams and the lucid "
|
||||
'ones', (tester) async {
|
||||
await keys.refresh();
|
||||
final now = DateTime.now();
|
||||
await dreams().add(lighthouse.copyWith(night: dayOf(now)));
|
||||
await dreams().add(exam.copyWith(night: DateTime.utc(2000)));
|
||||
await start(tester, usedBefore: true);
|
||||
await goTab(tester, 'Profile');
|
||||
await tester.tap(find.text('Practice'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.ensureVisible(find.text('Dream journal'));
|
||||
expect(find.text('This month'), findsOneWidget);
|
||||
expect(find.text('Lucid'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('signed out, it asks how to start, with an account to create '
|
||||
'where passwords are offered (ADR 0017), and no GitHub (ADR 0020, '
|
||||
'D36)', (tester) async {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,173 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/core/privacy/meditate_keys.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/feature/journal/data/dream_payload.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/feature/journal/data/dream_repository.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/feature/journal/model/dream.dart';
|
||||
|
||||
import '../../support/memory_content_key_gateway.dart';
|
||||
import '../../support/memory_sealed_row_gateway.dart';
|
||||
|
||||
/// The dreams the person wrote down (ADR 0038, D14): each a row sealed with
|
||||
/// their key for its row, as the sittings, the newest night first, none
|
||||
/// readable to the instance, changed and deleted by the person alone.
|
||||
void main() {
|
||||
final crypto = SodiumAccountCrypto();
|
||||
late UnlockedAccountKeys alice;
|
||||
late UnlockedAccountKeys bob;
|
||||
late MemoryContentKeyGateway keyRows;
|
||||
late MemorySealedRowGateway rows;
|
||||
final opened = <MeditateKeys>[];
|
||||
|
||||
final lighthouse = Dream(
|
||||
night: DateTime.utc(2026, 11, 3),
|
||||
title: 'The lighthouse',
|
||||
text: 'I flew over the sea to my grandmother.',
|
||||
marks: const {DreamMark.lucid, DreamMark.recurring},
|
||||
clarity: 4,
|
||||
mood: WakingMood.glad,
|
||||
labels: const ['Grandmother', 'Sea'],
|
||||
writtenAt: DateTime.utc(2026, 11, 3, 6, 40),
|
||||
);
|
||||
final exam = Dream(
|
||||
night: DateTime.utc(2026, 11),
|
||||
text: 'Late for an exam.',
|
||||
writtenAt: DateTime.utc(2026, 11, 3, 7),
|
||||
);
|
||||
|
||||
Future<DreamRepository> device(UnlockedAccountKeys? account) async {
|
||||
final keys = MeditateKeys(
|
||||
keyRows,
|
||||
SodiumContentCipher(),
|
||||
() async => account,
|
||||
);
|
||||
opened.add(keys);
|
||||
await keys.refresh();
|
||||
return DreamRepository(rows, keys);
|
||||
}
|
||||
|
||||
setUpAll(() async {
|
||||
alice = (await crypto.newAccount('alice password')).keys;
|
||||
bob = (await crypto.newAccount('bob password')).keys;
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
keyRows = MemoryContentKeyGateway();
|
||||
rows = MemorySealedRowGateway();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
for (final keys in opened) {
|
||||
keys.dispose();
|
||||
}
|
||||
opened.clear();
|
||||
});
|
||||
|
||||
test('a dream written reads back whole, the newest night first, on every '
|
||||
'device of the person', () async {
|
||||
final phone = await device(alice);
|
||||
await phone.add(exam);
|
||||
await phone.add(lighthouse);
|
||||
|
||||
final laptop = await device(alice);
|
||||
expect(
|
||||
[for (final stored in await laptop.all()) stored.dream],
|
||||
[lighthouse, exam],
|
||||
);
|
||||
});
|
||||
|
||||
test('sealed: the instance reads neither its words, its labels nor its '
|
||||
'marks', () async {
|
||||
final phone = await device(alice);
|
||||
await phone.add(lighthouse);
|
||||
|
||||
final envelope = rows.byId.values.single;
|
||||
expect(envelope.keyId, isNotNull);
|
||||
final bytes = String.fromCharCodes(envelope.payload.buffer.asUint8List());
|
||||
for (final word in ['lighthouse', 'grandmother', 'Sea', 'lucid']) {
|
||||
expect(bytes, isNot(contains(word)));
|
||||
}
|
||||
});
|
||||
|
||||
test("another person's dreams never open, and are left out", () async {
|
||||
final phone = await device(alice);
|
||||
await phone.add(lighthouse);
|
||||
|
||||
final bobs = await device(bob);
|
||||
expect(await bobs.all(), isEmpty);
|
||||
});
|
||||
|
||||
test('a device without the keys writes nothing', () async {
|
||||
final keyless = await device(null);
|
||||
await expectLater(keyless.add(lighthouse), throwsA(isA<KeysMissing>()));
|
||||
expect(rows.byId, isEmpty);
|
||||
});
|
||||
|
||||
test('a dream changed keeps its row; deleted, it goes', () async {
|
||||
final phone = await device(alice);
|
||||
final id = await phone.add(exam);
|
||||
|
||||
final changed = exam.copyWith(title: 'Exams again', clarity: () => 2);
|
||||
await phone.update(id, changed);
|
||||
expect(await phone.all(), [(id: id, dream: changed)]);
|
||||
|
||||
await phone.delete(id);
|
||||
expect(await phone.all(), isEmpty);
|
||||
expect(rows.byId, isEmpty);
|
||||
});
|
||||
|
||||
test("a row the instance moved to another's ID doesn't open", () async {
|
||||
final phone = await device(alice);
|
||||
final id = await phone.add(lighthouse);
|
||||
rows.byId['019a0000-0000-7000-8000-000000000000'] = rows.byId.remove(id)!;
|
||||
|
||||
expect(await phone.all(), isEmpty);
|
||||
});
|
||||
|
||||
group('its payload (schema 1)', () {
|
||||
Map<String, Object?> json(ByteData payload) =>
|
||||
jsonDecode(utf8.decode(Uint8List.sublistView(payload)))
|
||||
as Map<String, Object?>;
|
||||
|
||||
ByteData payload(Map<String, Object?> json) => ByteData.sublistView(
|
||||
Uint8List.fromList(utf8.encode(jsonEncode(json))),
|
||||
);
|
||||
|
||||
test('a bare dream is its night, text and time alone', () {
|
||||
expect(json(encodeDream(exam)), {
|
||||
'schemaVersion': dreamSchemaVersion,
|
||||
'night': '2026-11-01',
|
||||
'text': 'Late for an exam.',
|
||||
'writtenAt': '2026-11-03T07:00:00.000Z',
|
||||
});
|
||||
});
|
||||
|
||||
test('a value this version does not know reads as none', () {
|
||||
expect(
|
||||
decodeDream(
|
||||
payload({
|
||||
'schemaVersion': 1,
|
||||
'night': '2026-11-01',
|
||||
'text': 'Late for an exam.',
|
||||
'writtenAt': '2026-11-03T07:00:00.000Z',
|
||||
'marks': ['lucid', 'flying'],
|
||||
'clarity': 9,
|
||||
'mood': 'elated',
|
||||
'labels': ['School', 3],
|
||||
}),
|
||||
),
|
||||
exam.copyWith(marks: {DreamMark.lucid}, labels: ['School']),
|
||||
);
|
||||
});
|
||||
|
||||
test('a newer schema is not read half', () {
|
||||
expect(
|
||||
decodeDream(payload({'schemaVersion': dreamSchemaVersion + 1})),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/feature/journal/model/dream.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/feature/journal/model/journal.dart';
|
||||
|
||||
/// The journal's reading of the dreams (ADR 0038, D14): the night a new one
|
||||
/// belongs to, Today's morning card, the search and the marks' filters, the
|
||||
/// months, the figures and the labels offered.
|
||||
void main() {
|
||||
Dream dream(
|
||||
DateTime night, {
|
||||
String title = '',
|
||||
String text = 'A house by the sea.',
|
||||
Set<DreamMark> marks = const {},
|
||||
List<String> labels = const [],
|
||||
DateTime? writtenAt,
|
||||
}) => Dream(
|
||||
night: night,
|
||||
title: title,
|
||||
text: text,
|
||||
marks: marks,
|
||||
labels: labels,
|
||||
writtenAt: writtenAt ?? night.add(const Duration(hours: 7)),
|
||||
);
|
||||
|
||||
StoredDream stored(String id, Dream dream) => (id: id, dream: dream);
|
||||
|
||||
group('a new dream', () {
|
||||
test("belongs to last night: the morning's date, at any hour", () {
|
||||
expect(
|
||||
defaultNight(DateTime(2026, 11, 3, 6, 30)),
|
||||
DateTime.utc(2026, 11, 3),
|
||||
);
|
||||
expect(
|
||||
defaultNight(DateTime(2026, 11, 3, 23)),
|
||||
DateTime.utc(2026, 11, 3),
|
||||
);
|
||||
expect(
|
||||
defaultNight(DateTime(2026, 11, 3, 0, 15)),
|
||||
DateTime.utc(2026, 11, 3),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group("Today's morning card", () {
|
||||
final night = DateTime.utc(2026, 11, 3);
|
||||
|
||||
test("shows from 4 to 12 o'clock while last night has no dream", () {
|
||||
for (final hour in [4, 8, 11]) {
|
||||
expect(
|
||||
dreamPromptDue(
|
||||
now: DateTime(2026, 11, 3, hour),
|
||||
on: true,
|
||||
dreams: [],
|
||||
),
|
||||
isTrue,
|
||||
reason: "$hour o'clock",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('shows neither before 4 nor from 12 on', () {
|
||||
for (final hour in [0, 3, 12, 18]) {
|
||||
expect(
|
||||
dreamPromptDue(
|
||||
now: DateTime(2026, 11, 3, hour),
|
||||
on: true,
|
||||
dreams: [],
|
||||
),
|
||||
isFalse,
|
||||
reason: "$hour o'clock",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test(
|
||||
"goes once last night has a dream; an older night's doesn't count",
|
||||
() {
|
||||
final now = DateTime(2026, 11, 3, 8);
|
||||
expect(
|
||||
dreamPromptDue(now: now, on: true, dreams: [dream(night)]),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
dreamPromptDue(
|
||||
now: now,
|
||||
on: true,
|
||||
dreams: [dream(DateTime.utc(2026, 11, 2))],
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('never shows where it was switched off in Settings', () {
|
||||
expect(
|
||||
dreamPromptDue(now: DateTime(2026, 11, 3, 8), on: false, dreams: []),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('the search and the filters', () {
|
||||
final sea = stored(
|
||||
'a',
|
||||
dream(
|
||||
DateTime.utc(2026, 11, 3),
|
||||
title: 'The lighthouse',
|
||||
text: 'I flew over the sea.',
|
||||
marks: {DreamMark.lucid},
|
||||
labels: ['Grandmother', 'Sea'],
|
||||
),
|
||||
);
|
||||
final school = stored(
|
||||
'b',
|
||||
dream(
|
||||
DateTime.utc(2026, 11),
|
||||
title: 'Exams again',
|
||||
text: 'Late for an exam at my old school.',
|
||||
marks: {DreamMark.nightmare, DreamMark.recurring},
|
||||
labels: ['School'],
|
||||
),
|
||||
);
|
||||
final all = [sea, school];
|
||||
|
||||
test('searches the title, the text and the labels, without case or '
|
||||
'accents', () {
|
||||
expect(findDreams(all, query: 'LIGHTHOUSE'), [sea]);
|
||||
expect(findDreams(all, query: 'exam school'), [school]);
|
||||
expect(findDreams(all, query: 'grandmother'), [sea]);
|
||||
expect(findDreams(all, query: 'séa'), [sea]);
|
||||
expect(findDreams(all, query: 'mountain'), isEmpty);
|
||||
expect(findDreams(all), all);
|
||||
});
|
||||
|
||||
test(
|
||||
'a dream shows under the marks chosen only where it has every one',
|
||||
() {
|
||||
expect(findDreams(all, marks: {DreamMark.lucid}), [sea]);
|
||||
expect(findDreams(all, marks: {DreamMark.recurring}), [school]);
|
||||
expect(
|
||||
findDreams(all, marks: {DreamMark.lucid, DreamMark.nightmare}),
|
||||
isEmpty,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('the months, newest first, each with its dreams newest first', () {
|
||||
final early = stored('a', dream(DateTime.utc(2026, 10, 2)));
|
||||
final late = stored('b', dream(DateTime.utc(2026, 10, 28)));
|
||||
final now = stored('c', dream(DateTime.utc(2026, 11, 3)));
|
||||
final months = dreamsByMonth([now, late, early]);
|
||||
expect(
|
||||
[for (final month in months) month.month],
|
||||
[
|
||||
DateTime.utc(2026, 11),
|
||||
DateTime.utc(2026, 10),
|
||||
],
|
||||
);
|
||||
expect(months.last.dreams, [late, early]);
|
||||
});
|
||||
|
||||
test("the figures: this month's dreams and the lucid ones of all", () {
|
||||
final dreams = [
|
||||
dream(DateTime.utc(2026, 11, 3), marks: {DreamMark.lucid}),
|
||||
dream(DateTime.utc(2026, 11)),
|
||||
dream(DateTime.utc(2026, 10, 30), marks: {DreamMark.lucid}),
|
||||
];
|
||||
expect(
|
||||
journalFigures(dreams, today: DateTime.utc(2026, 11, 3)),
|
||||
(thisMonth: 2, lucid: 2),
|
||||
);
|
||||
});
|
||||
|
||||
test('the labels offered: those used, the most used first, each once '
|
||||
'whatever its case', () {
|
||||
final dreams = [
|
||||
dream(DateTime.utc(2026, 11, 3), labels: ['Sea', 'Grandmother']),
|
||||
dream(DateTime.utc(2026, 11, 2), labels: ['sea']),
|
||||
dream(DateTime.utc(2026, 11), labels: ['School']),
|
||||
];
|
||||
expect(labelsUsed(dreams), ['Sea', 'Grandmother', 'School']);
|
||||
});
|
||||
|
||||
test('a label is added trimmed, once whatever its case, never empty', () {
|
||||
expect(withLabel(['Sea'], ' school '), ['Sea', 'school']);
|
||||
expect(withLabel(['Sea'], 'SEA'), ['Sea']);
|
||||
expect(withLabel(['Sea'], ' '), ['Sea']);
|
||||
});
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ void main() {
|
|||
timerMinutes: 25,
|
||||
favourites: ['body-scan', 'breath-counting'],
|
||||
showStreak: false,
|
||||
dreamPrompt: false,
|
||||
);
|
||||
expect(decodeSettings(encodeSettings(changed)), changed);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue