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,270 @@
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/card/data/card_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
import 'package:halkyon_learn_flutter/src/feature/home/widget/goal_card.dart';
import 'package:halkyon_learn_flutter/src/feature/home/widget/streak.dart';
import 'package:halkyon_learn_flutter/src/feature/pomodoro/widget/pomodoro_card.dart';
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/study/model/rating.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:intl/intl.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:material_ui/material_ui.dart';
import '../../../support/fake_card_gateway.dart';
import '../../../support/fake_review_gateway.dart';
import '../../../support/fake_settings_gateway.dart';
import '../../../support/pump_app.dart';
import '../../../support/test_dependencies.dart';
void main() {
late FakeCardGateway cardGateway;
late FakeReviewGateway reviewGateway;
late FakeSettingsGateway settingsGateway;
setUp(() {
cardGateway = FakeCardGateway();
reviewGateway = FakeReviewGateway();
settingsGateway = FakeSettingsGateway();
});
Future<void> showHome(WidgetTester tester, {double width = 1200}) async {
tester.view.physicalSize = Size(width, 1600);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await pumpApp(
tester,
testDependencies(
cardGateway: cardGateway,
reviewGateway: reviewGateway,
settingsGateway: settingsGateway,
),
);
}
int streak(WidgetTester tester) =>
tester.widget<StreakChip>(find.byType(StreakChip)).streak;
/// Rates a new card now.
Future<void> rateToday() async {
final cards = CardRepository(cardGateway);
addTearDown(cards.dispose);
final id = await cards.create(
'japanese',
CardContent(front: RichContent.empty(), back: RichContent.empty()),
);
await ReviewRepository(reviewGateway).record(
cardId: id,
rating: Rating.good,
reviewedAt: DateTime.now(),
duration: Duration.zero,
);
}
testWidgets('cobalt washes down from the top, behind a clear app bar', (
tester,
) async {
await showHome(tester);
expect(
tester.widget<TopGradient>(find.byType(TopGradient)).color,
Brand.primary,
);
expect(
tester.widget<AppBar>(find.byType(AppBar)).backgroundColor,
Colors.transparent,
);
});
testWidgets('opens with the goal first, then the Pomodoro timer, each on '
'a card', (tester) async {
await showHome(tester);
expect(
tester.getRect(find.byType(GoalCard)).top,
lessThan(tester.getRect(find.byType(PomodoroCard)).top),
);
expect(
find.ancestor(of: find.byType(GoalCard), matching: find.byType(Card)),
findsNothing,
);
expect(
find.descendant(of: find.byType(GoalCard), matching: find.byType(Card)),
findsOneWidget,
);
});
testWidgets('the streak sheet: the count inside the flame, the week as '
'letters, and what keeps it going', (tester) async {
await rateToday();
await showHome(tester, width: 400);
await tester.tap(find.byType(StreakChip));
await tester.pumpAndSettle();
final sheet = find.byType(BottomSheet);
expect(
find.descendant(of: sheet, matching: find.text('1 day streak')),
findsOneWidget,
);
expect(
find.descendant(of: sheet, matching: find.text('1')),
findsOneWidget,
);
expect(
find.descendant(of: sheet, matching: find.text('M')),
findsOneWidget,
);
expect(
find.descendant(
of: sheet,
matching: find.text('Come back tomorrow to keep your streak.'),
),
findsOneWidget,
);
});
testWidgets('the week names the day with its month short, and the '
"heatmap's days say how much was done", (tester) async {
await showHome(tester);
final now = DateTime.now();
expect(
find.text(
'${DateFormat.EEEE('en_US').format(now)}, '
'${DateFormat.MMMd('en_US').format(now)}',
),
findsOneWidget,
);
expect(
find.byWidgetPredicate(
(widget) =>
widget is Tooltip &&
(widget.message?.startsWith('0 activities · ') ?? false),
),
findsWidgets,
);
});
testWidgets('a card rated today lights the streak and the heatmap', (
tester,
) async {
await rateToday();
await showHome(tester);
expect(streak(tester), 1);
// The card written and the rating.
expect(find.text('2 activities in the last year'), findsOneWidget);
});
testWidgets('a goal is set in its sheet and shows its progress', (
tester,
) async {
await showHome(tester);
await tester.tap(find.text('Set a study goal'));
await tester.pumpAndSettle();
await tester.tap(find.text('Per week'));
await tester.pump();
await tester.tap(find.byTooltip('Increase'));
await tester.pump();
await tester.tap(find.text('Save goal'));
await tester.pumpAndSettle();
expect(find.text('Weekly goal'), findsOneWidget);
// In steps of 5, as the study options.
expect(find.text('0 of 25 cards this week'), findsOneWidget);
expect(find.byIcon(LucideIcons.pencil), findsOneWidget);
await tester.tap(find.text('Weekly goal'));
await tester.pumpAndSettle();
// Removing sits below saving.
expect(
tester.getRect(find.text('Remove goal')).top,
greaterThan(tester.getRect(find.text('Save goal')).bottom),
);
});
testWidgets('resting today keeps the streak', (tester) async {
await showHome(tester);
expect(streak(tester), 0);
await tester.tap(find.text('Rest day'));
await tester.pumpAndSettle();
expect(streak(tester), 1);
});
testWidgets("the rest day sits in the week's header, beside the day "
'shown', (tester) async {
await showHome(tester);
final restDay = tester.getRect(find.text('Rest day'));
final previous = tester.getRect(find.byTooltip('Previous day'));
expect(restDay.center.dy, closeTo(previous.center.dy, 2));
expect(restDay.right, lessThan(previous.left));
});
testWidgets('on a phone, the rest day is a snowflake button in the '
"week's header", (tester) async {
await showHome(tester, width: 360);
expect(find.text('Rest day'), findsNothing);
final restDay = find.byTooltip('Rest day');
expect(
tester.getRect(restDay).center.dy,
closeTo(tester.getRect(find.byTooltip('Previous day')).center.dy, 2),
);
await tester.tap(restDay);
await tester.pumpAndSettle();
expect(streak(tester), 1);
});
testWidgets('a long press arranges the dashboard: a widget hidden stays '
'hidden', (tester) async {
await showHome(tester);
await tester.longPress(find.text('Set a study goal'));
await tester.pumpAndSettle();
expect(find.text('Arrange widgets'), findsOneWidget);
await tester.tap(find.byTooltip('Hide Daily goal'));
await tester.pumpAndSettle();
await tester.tap(find.text('Done'));
await tester.pumpAndSettle();
expect(find.text('Set a study goal'), findsNothing);
expect(find.text('Your activity'), findsOneWidget);
});
testWidgets('with every widget hidden, Customise brings the arranging '
'back', (tester) async {
await showHome(tester);
await tester.longPress(find.text('Set a study goal'));
await tester.pumpAndSettle();
for (final name in [
'Daily goal',
'Pomodoro',
'PDF reading deadlines',
'Week overview',
'Activity heatmap',
]) {
await tester.tap(find.byTooltip('Hide $name'));
await tester.pumpAndSettle();
}
await tester.tap(find.text('Done'));
await tester.pumpAndSettle();
expect(find.text('All widgets are hidden'), findsOneWidget);
expect(
find.text('Bring some back to fill your dashboard.'),
findsOneWidget,
);
// Tests run in American English.
await tester.tap(find.text('Customize'));
await tester.pumpAndSettle();
expect(find.text('Arrange widgets'), findsOneWidget);
});
}