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:
parent
11abd9afce
commit
649499be23
5 changed files with 0 additions and 0 deletions
|
|
@ -0,0 +1,37 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_tree.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_language_scope.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('a change of languages keeps what is shown below: the screen '
|
||||
'goes on as it was', (tester) async {
|
||||
Widget scoped(StudysetLanguages languages) => MaterialApp(
|
||||
home: LanguageScope(languages: languages, child: const _Counter()),
|
||||
);
|
||||
await tester.pumpWidget(scoped((front: 'de', back: 'en')));
|
||||
await tester.tap(find.byType(TextButton));
|
||||
await tester.pump();
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
|
||||
await tester.pumpWidget(scoped((front: 'zh-Hant', back: 'en')));
|
||||
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
/// Counts taps: a state that a rebuilt subtree would lose.
|
||||
class const _Counter() extends StatefulWidget {
|
||||
@override
|
||||
State<_Counter> createState() => _CounterState();
|
||||
}
|
||||
|
||||
class _CounterState() extends State<_Counter> {
|
||||
var _count = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => TextButton(
|
||||
onPressed: () => setState(() => _count++),
|
||||
child: Text('$_count'),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,855 @@
|
|||
import 'dart:async';
|
||||
|
||||
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_learn_flutter/src/core/content/published_ref.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.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/publish/model/publication.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/model/studyset_study_options.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_payload.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/deleted_studyset_card.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_card.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
import '../../../support/fake_card_gateway.dart';
|
||||
import '../../../support/fake_file_picker.dart';
|
||||
import '../../../support/fake_publish_gateway.dart';
|
||||
import '../../../support/fake_studyset_gateway.dart';
|
||||
import '../../../support/memory_published_store.dart';
|
||||
import '../../../support/pump_app.dart';
|
||||
import '../../../support/test_dependencies.dart';
|
||||
|
||||
void main() {
|
||||
late FakeStudysetGateway gateway;
|
||||
late FakeCardGateway cardGateway;
|
||||
late FakeFilePicker files;
|
||||
|
||||
setUp(() {
|
||||
gateway = FakeStudysetGateway();
|
||||
cardGateway = FakeCardGateway();
|
||||
files = FakeFilePicker();
|
||||
});
|
||||
|
||||
/// How many syncs ran.
|
||||
var syncs = 0;
|
||||
|
||||
/// Shows the library in a window [width] wide; to a person with an
|
||||
/// identity ([identified]), whose studysets [publishing] publishes and
|
||||
/// [publishedStore] remembers as published.
|
||||
Future<void> showLibrary(
|
||||
WidgetTester tester, {
|
||||
double width = 1200,
|
||||
bool identified = false,
|
||||
FakePublishGateway? publishing,
|
||||
MemoryPublishedStore? publishedStore,
|
||||
}) async {
|
||||
tester.view.physicalSize = Size(width, 1600);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
syncs = 0;
|
||||
await pumpApp(
|
||||
tester,
|
||||
testDependencies(
|
||||
authGateway: identified
|
||||
? FakeAuthGateway(
|
||||
status: const SignedIn(
|
||||
authUserId: 'github-person',
|
||||
isIdentified: true,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
studysetGateway: gateway,
|
||||
cardGateway: cardGateway,
|
||||
files: files,
|
||||
publishGateway: publishing,
|
||||
publishedStore: publishedStore,
|
||||
syncOnce: ({required onMerge}) async => syncs++,
|
||||
),
|
||||
location: RoutePaths.library,
|
||||
);
|
||||
}
|
||||
|
||||
/// A studyset on this device before the app starts; a copy of the one
|
||||
/// published at [publishedAt].
|
||||
void stored(
|
||||
String id,
|
||||
String title, {
|
||||
String? secondaryTitle,
|
||||
DateTime? archivedAt,
|
||||
DateTime? starredAt,
|
||||
String? publishedAt,
|
||||
}) => gateway.receiveRemoteChange(
|
||||
id,
|
||||
encodeStudysetPayload(
|
||||
StudysetContent(
|
||||
title: title,
|
||||
description: RichContent.empty(),
|
||||
secondaryTitle: secondaryTitle,
|
||||
published: publishedAt == null
|
||||
? null
|
||||
: const PublishedRef(publication: 'p', item: 'i', version: 1),
|
||||
publishedAt: publishedAt,
|
||||
),
|
||||
),
|
||||
archivedAt: archivedAt,
|
||||
starredAt: starredAt,
|
||||
);
|
||||
|
||||
/// [finder] within the card of the studyset titled [title].
|
||||
Finder onCard(String title, Finder finder) => find.descendant(
|
||||
of: find.widgetWithText(StudysetCard, title),
|
||||
matching: finder,
|
||||
);
|
||||
|
||||
/// Chooses [option] in the options of the studyset titled [title].
|
||||
Future<void> choose(WidgetTester tester, String title, String option) async {
|
||||
await tester.tap(onCard(title, find.byTooltip('Options')));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(option));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
testWidgets("until this device's first sync went through, an empty "
|
||||
'library says it is on its way; then that there are no studysets '
|
||||
'yet', (tester) async {
|
||||
tester.view.physicalSize = const Size(1200, 1600);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final first = Completer<void>();
|
||||
await pumpApp(
|
||||
tester,
|
||||
testDependencies(
|
||||
studysetGateway: gateway,
|
||||
syncedBefore: false,
|
||||
syncOnce: ({required onMerge}) => first.future,
|
||||
),
|
||||
);
|
||||
|
||||
// Its spinner turns without end: frames, not settling.
|
||||
await tester.tap(tab('Library'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
expect(find.textContaining('Your library is on its way from'), findsOne);
|
||||
expect(find.text('No studysets yet'), findsNothing);
|
||||
|
||||
first.complete();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('on its way'), findsNothing);
|
||||
expect(find.text('No studysets yet'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('says when there are no studysets yet', (tester) async {
|
||||
await showLibrary(tester);
|
||||
|
||||
expect(find.text('No studysets yet'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('says so when the studysets cannot be read, and reads them '
|
||||
'again on Retry', (tester) async {
|
||||
gateway.failReads = Exception('Disk unavailable');
|
||||
stored('japanese', 'Japanese');
|
||||
|
||||
await showLibrary(tester);
|
||||
|
||||
expect(find.text("Couldn't read your studysets."), findsOneWidget);
|
||||
gateway.failReads = null;
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Retry'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Japanese'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Refresh syncs, from the menu and by pulling the list '
|
||||
'down', (tester) async {
|
||||
stored('japanese', 'Japanese');
|
||||
await showLibrary(tester);
|
||||
final before = syncs;
|
||||
|
||||
await tester.tap(find.byTooltip('Library options'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Refresh'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(syncs, before + 1);
|
||||
|
||||
// Below the cards: a card itself can be dragged elsewhere.
|
||||
await tester.flingFrom(const Offset(600, 1200), const Offset(0, 400), 1000);
|
||||
await tester.pumpAndSettle();
|
||||
expect(syncs, before + 2);
|
||||
});
|
||||
|
||||
testWidgets('an empty library is pulled down to sync too', (tester) async {
|
||||
await showLibrary(tester);
|
||||
expect(find.text('No studysets yet'), findsOneWidget);
|
||||
final before = syncs;
|
||||
|
||||
await tester.flingFrom(const Offset(600, 600), const Offset(0, 400), 1000);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(syncs, before + 1);
|
||||
});
|
||||
|
||||
testWidgets('creates a studyset with a secondary title and a colour', (
|
||||
tester,
|
||||
) async {
|
||||
final semantics = tester.ensureSemantics();
|
||||
await showLibrary(tester);
|
||||
|
||||
await tester.tap(find.text('Create studyset'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(SheetScaffold), findsOneWidget);
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextField, 'Title'),
|
||||
'Vocabulary',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextField, 'Secondary title'),
|
||||
'japanese',
|
||||
);
|
||||
await tester.tap(find.bySemanticsLabel('Color: Cobalt'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.bySemanticsLabel('Fern'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Create'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('JAPANESE'), findsOneWidget);
|
||||
expect(find.text('Vocabulary'), findsOneWidget);
|
||||
expect(
|
||||
decodeStudysetPayload(gateway.writes.single.$2),
|
||||
StudysetContent(
|
||||
title: 'Vocabulary',
|
||||
description: RichContent.empty(),
|
||||
secondaryTitle: 'JAPANESE',
|
||||
colorName: 'fern',
|
||||
),
|
||||
);
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('a new studyset is cobalt unless another colour is chosen', (
|
||||
tester,
|
||||
) async {
|
||||
await showLibrary(tester);
|
||||
|
||||
await tester.tap(find.text('Create studyset'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.widgetWithText(TextField, 'Title'), 'Kanji');
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Create'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
decodeStudysetPayload(gateway.writes.single.$2).colorName,
|
||||
'cobalt',
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('creating waits for a title', (tester) async {
|
||||
await showLibrary(tester);
|
||||
await tester.tap(find.text('Create studyset'));
|
||||
await tester.pumpAndSettle();
|
||||
FilledButton create() => tester.widget<FilledButton>(
|
||||
find.ancestor(
|
||||
of: find.text('Create'),
|
||||
matching: find.byType(FilledButton),
|
||||
),
|
||||
);
|
||||
expect(create().onPressed, isNull);
|
||||
// The title is required: said below it, its outline stronger, while
|
||||
// it is empty.
|
||||
expect(find.text('Required'), findsOneWidget);
|
||||
|
||||
await tester.enterText(find.widgetWithText(TextField, 'Title'), ' ');
|
||||
await tester.pump();
|
||||
expect(create().onPressed, isNull);
|
||||
expect(find.text('Required'), findsOneWidget);
|
||||
|
||||
await tester.enterText(find.widgetWithText(TextField, 'Title'), 'Kanji');
|
||||
await tester.pump();
|
||||
expect(create().onPressed, isNotNull);
|
||||
expect(find.text('Required'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('closing the sheet with changes asks first; staying keeps '
|
||||
'them, discarding closes it', (tester) async {
|
||||
await showLibrary(tester);
|
||||
await tester.tap(find.text('Create studyset'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.widgetWithText(TextField, 'Title'), 'Kanji');
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.byTooltip('Close'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Discard changes?'), findsOneWidget);
|
||||
await tester.tap(find.text('Cancel'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(SheetScaffold), findsOneWidget);
|
||||
expect(find.text('Kanji'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byTooltip('Close'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Discard'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(SheetScaffold), findsNothing);
|
||||
expect(gateway.writes, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets("the bottom sheet doesn't swipe away, which would lose "
|
||||
'changes without asking; a tap beside it asks', (tester) async {
|
||||
await showLibrary(tester, width: 400);
|
||||
await tester.tap(find.byTooltip('Create studyset'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.widgetWithText(TextField, 'Title'), 'Kanji');
|
||||
await tester.pump();
|
||||
|
||||
await tester.fling(
|
||||
find.text('Create studyset'),
|
||||
const Offset(0, 600),
|
||||
2000,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(SheetScaffold), findsOneWidget);
|
||||
expect(find.text('Kanji'), findsOneWidget);
|
||||
|
||||
await tester.tapAt(const Offset(200, 20));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Discard changes?'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('closing an untouched sheet asks nothing', (tester) async {
|
||||
await showLibrary(tester);
|
||||
await tester.tap(find.text('Create studyset'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byTooltip('Close'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Discard changes?'), findsNothing);
|
||||
expect(find.byType(SheetScaffold), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('edits a studyset from its options', (tester) async {
|
||||
stored('japanese', 'Japanese');
|
||||
await showLibrary(tester);
|
||||
|
||||
await choose(tester, 'Japanese', 'Edit');
|
||||
expect(find.text('Edit studyset'), findsOneWidget);
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextField, 'Japanese'),
|
||||
'日本語',
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Save'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('日本語'), findsOneWidget);
|
||||
expect(find.text('Japanese'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('lists starred studysets first', (tester) async {
|
||||
stored('japanese', 'Japanese');
|
||||
stored('german', 'German', starredAt: DateTime.utc(2026, 10));
|
||||
|
||||
await showLibrary(tester);
|
||||
|
||||
expect(
|
||||
tester.getTopLeft(find.text('German')).dx,
|
||||
lessThan(tester.getTopLeft(find.text('Japanese')).dx),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('archiving tucks a studyset into the closed archive; Undo '
|
||||
'brings it back', (tester) async {
|
||||
stored('japanese', 'Japanese');
|
||||
stored('german', 'German');
|
||||
await showLibrary(tester);
|
||||
|
||||
await choose(tester, 'Japanese', 'Archive');
|
||||
expect(find.text('Studyset archived'), findsOneWidget);
|
||||
expect(find.text('Japanese'), findsNothing);
|
||||
expect(find.text('Archived'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Undo'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Japanese'), findsOneWidget);
|
||||
expect(find.text('Archived'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('the archive opens to show what rests there, which can be '
|
||||
'unarchived', (tester) async {
|
||||
stored('japanese', 'Japanese');
|
||||
stored('kanji', 'Kanji', archivedAt: DateTime.utc(2026, 10));
|
||||
await showLibrary(tester);
|
||||
expect(find.text('Kanji'), findsNothing);
|
||||
|
||||
await tester.tap(find.text('Archived'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Kanji'), findsOneWidget);
|
||||
await choose(tester, 'Kanji', 'Unarchive');
|
||||
|
||||
expect(find.text('Studyset unarchived'), findsOneWidget);
|
||||
expect(find.text('Archived'), findsNothing);
|
||||
expect(find.text('Kanji'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('searches titles and secondary titles, the archive too', (
|
||||
tester,
|
||||
) async {
|
||||
stored('vocabulary', 'Vocabulary', secondaryTitle: 'Japanese');
|
||||
stored('grammar', 'Grammar');
|
||||
stored(
|
||||
'kanji',
|
||||
'Kanji',
|
||||
secondaryTitle: 'Japanese',
|
||||
archivedAt: DateTime.utc(2026, 10),
|
||||
);
|
||||
await showLibrary(tester);
|
||||
|
||||
await tester.tap(find.byTooltip('Search'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byType(TextField), 'japan');
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Vocabulary'), findsOneWidget);
|
||||
expect(find.text('Kanji'), findsOneWidget);
|
||||
expect(find.text('Grammar'), findsNothing);
|
||||
|
||||
await tester.tap(find.byTooltip('Close search'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Grammar'), findsOneWidget);
|
||||
expect(find.text('Kanji'), findsNothing, reason: 'the archive is closed');
|
||||
});
|
||||
|
||||
testWidgets('says when no studyset matches the search', (tester) async {
|
||||
stored('japanese', 'Japanese');
|
||||
await showLibrary(tester);
|
||||
|
||||
await tester.tap(find.byTooltip('Search'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byType(TextField), 'swahili');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('No studysets match your search'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets("the options are every studyset's, as its screen has them, "
|
||||
'in this order', (tester) async {
|
||||
stored('japanese', 'Japanese');
|
||||
await showLibrary(tester);
|
||||
|
||||
await tester.tap(onCard('Japanese', find.byTooltip('Options')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final tops = [
|
||||
for (final label in [
|
||||
'Edit',
|
||||
'Move to…',
|
||||
'Study options',
|
||||
'Import content',
|
||||
'Export content',
|
||||
'Reset progress',
|
||||
'Archive',
|
||||
'Delete',
|
||||
'More info',
|
||||
])
|
||||
tester.getTopLeft(find.text(label)).dy,
|
||||
];
|
||||
expect(tops, [...tops]..sort());
|
||||
});
|
||||
|
||||
testWidgets('moves a studyset into another from its options', (
|
||||
tester,
|
||||
) async {
|
||||
stored('japanese', 'Japanese');
|
||||
stored('languages', 'Languages');
|
||||
await showLibrary(tester);
|
||||
|
||||
await choose(tester, 'Japanese', 'Move to…');
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(SheetScaffold),
|
||||
matching: find.text('Languages'),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Move'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(gateway.moves.single.$1, 'japanese');
|
||||
expect(gateway.moves.single.$2, 'languages');
|
||||
});
|
||||
|
||||
testWidgets('Import content opens the studyset, asking there what to '
|
||||
'import: from Anki, say', (tester) async {
|
||||
stored('japanese', 'Japanese');
|
||||
await showLibrary(tester);
|
||||
|
||||
await choose(tester, 'Japanese', 'Import content');
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.descendant(of: find.byType(AppBar), matching: find.text('Japanese')),
|
||||
findsOneWidget,
|
||||
);
|
||||
await tester.tap(find.text('Anki deck'));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(files.asked, [
|
||||
['apkg', 'colpkg'],
|
||||
]);
|
||||
});
|
||||
|
||||
group('study options', () {
|
||||
/// The study options the studyset [id] was last written with.
|
||||
StudysetStudyOptions? written(String id) => decodeStudysetPayload(
|
||||
gateway.writes.lastWhere((write) => write.$1 == id).$2,
|
||||
).studyOptions;
|
||||
|
||||
testWidgets('start as the defaults, and are kept with the studyset once '
|
||||
'saved', (tester) async {
|
||||
stored('japanese', 'Japanese');
|
||||
await showLibrary(tester);
|
||||
|
||||
await choose(tester, 'Japanese', 'Study options');
|
||||
expect(
|
||||
find.text('20 / day', findRichText: true),
|
||||
findsOneWidget,
|
||||
reason: 'new cards a day',
|
||||
);
|
||||
expect(find.text('Unlimited'), findsOneWidget, reason: 'reviews a day');
|
||||
expect(find.text('Reset to defaults'), findsNothing);
|
||||
|
||||
await tester.tap(find.byTooltip('Decrease').first);
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Limit reviews per day'));
|
||||
await tester.pump();
|
||||
await tester.tap(find.byTooltip('Increase').last);
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Mixed'));
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Save'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
written('japanese'),
|
||||
const StudysetStudyOptions(
|
||||
newPerDay: 15,
|
||||
reviewsPerDay: 225,
|
||||
order: NewCardOrder.mixed,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('a limit steps to the next multiple of its step, takes any '
|
||||
'number typed, and is switched off for none, coming back as it '
|
||||
'was', (tester) async {
|
||||
stored('japanese', 'Japanese');
|
||||
await showLibrary(tester);
|
||||
await choose(tester, 'Japanese', 'Study options');
|
||||
Finder newCards() => find.text('20 / day', findRichText: true);
|
||||
expect(newCards(), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byTooltip('Type a number').first);
|
||||
await tester.pump();
|
||||
await tester.enterText(find.byType(TextField), '12');
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
await tester.pump();
|
||||
expect(find.text('12 / day', findRichText: true), findsOneWidget);
|
||||
await tester.tap(find.byTooltip('Increase').first);
|
||||
await tester.pump();
|
||||
expect(find.text('15 / day', findRichText: true), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Limit new cards per day'));
|
||||
await tester.pump();
|
||||
expect(find.text('15 / day', findRichText: true), findsNothing);
|
||||
expect(find.text('Unlimited'), findsNWidgets(2));
|
||||
await tester.tap(find.text('Limit new cards per day'));
|
||||
await tester.pump();
|
||||
expect(find.text('15 / day', findRichText: true), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Limit new cards per day'));
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Save'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(written('japanese')?.newPerDay, StudysetStudyOptions.unlimited);
|
||||
});
|
||||
|
||||
testWidgets('saved as they came, the defaults stay the defaults', (
|
||||
tester,
|
||||
) async {
|
||||
stored('japanese', 'Japanese');
|
||||
await showLibrary(tester);
|
||||
|
||||
await choose(tester, 'Japanese', 'Study options');
|
||||
await tester.tap(find.text('Save'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(gateway.writes, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('go back to the defaults with Reset', (tester) async {
|
||||
gateway.receiveRemoteChange(
|
||||
'japanese',
|
||||
encodeStudysetPayload(
|
||||
StudysetContent.titled(
|
||||
'Japanese',
|
||||
).withStudyOptions(const StudysetStudyOptions(newPerDay: 5)),
|
||||
),
|
||||
);
|
||||
await showLibrary(tester);
|
||||
|
||||
await choose(tester, 'Japanese', 'Study options');
|
||||
expect(find.text('5 / day', findRichText: true), findsOneWidget);
|
||||
await tester.tap(find.text('Reset to defaults'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(gateway.writes, isNotEmpty);
|
||||
expect(written('japanese'), isNull);
|
||||
});
|
||||
|
||||
testWidgets('stay when the studyset is edited', (tester) async {
|
||||
gateway.receiveRemoteChange(
|
||||
'japanese',
|
||||
encodeStudysetPayload(
|
||||
StudysetContent.titled(
|
||||
'Japanese',
|
||||
).withStudyOptions(const StudysetStudyOptions(newPerDay: 5)),
|
||||
),
|
||||
);
|
||||
await showLibrary(tester);
|
||||
|
||||
await choose(tester, 'Japanese', 'Edit');
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextField, 'Japanese'),
|
||||
'Nihongo',
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Save'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(written('japanese'), const StudysetStudyOptions(newPerDay: 5));
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('Delete stands out in red among the options', (tester) async {
|
||||
stored('japanese', 'Japanese');
|
||||
await showLibrary(tester);
|
||||
|
||||
await tester.tap(onCard('Japanese', find.byTooltip('Options')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final error = Theme.of(tester.element(find.text('Delete')))
|
||||
.colorScheme
|
||||
.error;
|
||||
expect(tester.widget<Text>(find.text('Delete')).style?.color, error);
|
||||
expect(tester.widget<Text>(find.text('Edit')).style?.color, isNot(error));
|
||||
});
|
||||
|
||||
Finder onDeletedCard(String title, Finder finder) => find.descendant(
|
||||
of: find.widgetWithText(DeletedStudysetCard, title),
|
||||
matching: finder,
|
||||
);
|
||||
|
||||
testWidgets("deleting doesn't ask: the studyset waits in Recently deleted, "
|
||||
'saying for how long; Undo brings it back', (tester) async {
|
||||
stored('japanese', 'Japanese');
|
||||
await showLibrary(tester);
|
||||
|
||||
await choose(tester, 'Japanese', 'Delete');
|
||||
expect(find.byType(AlertDialog), findsNothing);
|
||||
expect(find.text('Moved to Recently deleted'), findsOneWidget);
|
||||
expect(find.widgetWithText(StudysetCard, 'Japanese'), findsNothing);
|
||||
expect(find.text('Recently deleted'), findsOneWidget);
|
||||
expect(
|
||||
onDeletedCard('Japanese', find.text('14 days left')),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Undo'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.widgetWithText(StudysetCard, 'Japanese'), findsOneWidget);
|
||||
expect(find.text('Recently deleted'), findsNothing);
|
||||
expect(gateway.deleted, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('a deleted studyset is restored from Recently deleted, even '
|
||||
'when it was the only one', (tester) async {
|
||||
gateway.receiveRemoteChange(
|
||||
'japanese',
|
||||
encodeStudysetPayload(StudysetContent.titled('Japanese')),
|
||||
deletedAt: clock.now().toUtc().subtract(const Duration(days: 10)),
|
||||
);
|
||||
await showLibrary(tester);
|
||||
expect(find.text('No studysets yet'), findsNothing);
|
||||
expect(
|
||||
onDeletedCard('Japanese', find.text('4 days left')),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
await tester.tap(onDeletedCard('Japanese', find.text('Restore')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Studyset restored'), findsOneWidget);
|
||||
expect(find.widgetWithText(StudysetCard, 'Japanese'), findsOneWidget);
|
||||
expect(find.text('Recently deleted'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Delete now asks, then deletes the studyset for good with its '
|
||||
'cards', (tester) async {
|
||||
gateway.receiveRemoteChange(
|
||||
'japanese',
|
||||
encodeStudysetPayload(StudysetContent.titled('Japanese')),
|
||||
deletedAt: clock.now().toUtc(),
|
||||
);
|
||||
final cards = CardRepository(cardGateway);
|
||||
addTearDown(cards.dispose);
|
||||
final card = await cards.create(
|
||||
'japanese',
|
||||
CardContent(front: RichContent.empty(), back: RichContent.empty()),
|
||||
);
|
||||
await showLibrary(tester);
|
||||
|
||||
Future<void> deleteNow() async {
|
||||
await tester.tap(onDeletedCard('Japanese', find.byTooltip('Options')));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Delete now'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Delete for good?'), findsOneWidget);
|
||||
}
|
||||
|
||||
await deleteNow();
|
||||
await tester.tap(find.text('Cancel'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(gateway.deleted, isEmpty);
|
||||
|
||||
await deleteNow();
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Delete for good'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(gateway.deleted, ['japanese']);
|
||||
expect(cardGateway.deletions, ['blank $card', 'delete $card']);
|
||||
expect(find.text('Recently deleted'), findsNothing);
|
||||
expect(find.text('No studysets yet'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a card invites studying its due cards, and counts anew after '
|
||||
'studying', (tester) async {
|
||||
stored('japanese', 'Japanese');
|
||||
final cards = CardRepository(cardGateway);
|
||||
addTearDown(cards.dispose);
|
||||
await cards.create(
|
||||
'japanese',
|
||||
CardContent(front: RichContent.empty(), back: RichContent.empty()),
|
||||
);
|
||||
await showLibrary(tester);
|
||||
expect(find.text('1 flashcard'), findsOneWidget);
|
||||
expect(onCard('Japanese', find.text('1')), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byTooltip('Study'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Show answer'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Perfect'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Done'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byTooltip('Add content'), findsOneWidget);
|
||||
expect(onCard('Japanese', find.text('1')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('the create button floats in compact windows and sits in the '
|
||||
'app bar from medium on', (tester) async {
|
||||
await showLibrary(tester, width: 599);
|
||||
expect(find.byType(FloatingActionButton), findsOneWidget);
|
||||
expect(find.text('Create studyset'), findsNothing);
|
||||
|
||||
await showLibrary(tester, width: 600);
|
||||
expect(find.byType(FloatingActionButton), findsNothing);
|
||||
expect(find.text('Create studyset'), findsOneWidget);
|
||||
});
|
||||
|
||||
group('Who made a studyset, and whether it is public', () {
|
||||
Publication publication({bool withdrawn = false}) => Publication(
|
||||
publicId: 'p',
|
||||
slug: 'japanese',
|
||||
location: 'halkyon.eu/@raisondetredev/japanese',
|
||||
withdrawn: withdrawn,
|
||||
);
|
||||
|
||||
testWidgets("the person's own says By you, a copy its author's handle", (
|
||||
tester,
|
||||
) async {
|
||||
stored('japanese', 'Japanese');
|
||||
stored('kanji', 'Kanji', publishedAt: 'halkyon.eu/@alice/kanji');
|
||||
|
||||
await showLibrary(tester);
|
||||
|
||||
expect(onCard('Japanese', find.text('By you')), findsOneWidget);
|
||||
expect(onCard('Kanji', find.text('By @alice')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a lock while private, a globe once out on Explore, as the '
|
||||
'instance says', (tester) async {
|
||||
stored('japanese', 'Japanese');
|
||||
stored('kanji', 'Kanji');
|
||||
stored('kana', 'Kana');
|
||||
final store = MemoryPublishedStore();
|
||||
|
||||
await showLibrary(
|
||||
tester,
|
||||
identified: true,
|
||||
publishing: FakePublishGateway(
|
||||
publications: {
|
||||
'japanese': publication(),
|
||||
'kana': publication(withdrawn: true),
|
||||
},
|
||||
),
|
||||
publishedStore: store,
|
||||
);
|
||||
|
||||
expect(onCard('Japanese', find.byTooltip('Public')), findsOneWidget);
|
||||
expect(onCard('Kanji', find.byTooltip('Private')), findsOneWidget);
|
||||
expect(onCard('Kana', find.byTooltip('Private')), findsOneWidget);
|
||||
// Remembered for when the instance can't be reached.
|
||||
expect(store.kept, {'japanese'});
|
||||
});
|
||||
|
||||
testWidgets('while the instance is out of reach, as the device last '
|
||||
'knew', (tester) async {
|
||||
stored('japanese', 'Japanese');
|
||||
stored('kanji', 'Kanji');
|
||||
|
||||
await showLibrary(
|
||||
tester,
|
||||
identified: true,
|
||||
publishing: FakePublishGateway()..fail = Exception('Offline'),
|
||||
publishedStore: MemoryPublishedStore({'japanese'}),
|
||||
);
|
||||
|
||||
expect(onCard('Japanese', find.byTooltip('Public')), findsOneWidget);
|
||||
expect(onCard('Kanji', find.byTooltip('Private')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('the lock opens the Publish sheet', (tester) async {
|
||||
stored('japanese', 'Japanese');
|
||||
await showLibrary(tester);
|
||||
|
||||
await tester.tap(onCard('Japanese', find.byTooltip('Private')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Publish'), findsOneWidget);
|
||||
expect(
|
||||
find.text('Sign in to have a public profile and publish.'),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/l10n/app_languages.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_navigation.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
void main() {
|
||||
Future<void> show(WidgetTester tester, {required bool enabled}) =>
|
||||
tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: appLocalizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: Scaffold(
|
||||
body: StudysetBackButton(
|
||||
ancestors: const [],
|
||||
all: const [],
|
||||
enabled: enabled,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
IconButton button(WidgetTester tester) => tester.widget<IconButton>(
|
||||
find.descendant(
|
||||
of: find.byType(StudysetBackButton),
|
||||
matching: find.byType(IconButton),
|
||||
),
|
||||
);
|
||||
|
||||
testWidgets('while something runs here, back is greyed and its levels '
|
||||
'stay closed', (tester) async {
|
||||
await show(tester, enabled: false);
|
||||
|
||||
expect(button(tester).onPressed, isNull);
|
||||
await tester.longPress(find.byType(StudysetBackButton));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Library'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('otherwise back works and holding it lists the levels', (
|
||||
tester,
|
||||
) async {
|
||||
await show(tester, enabled: true);
|
||||
|
||||
expect(button(tester).onPressed, isNotNull);
|
||||
await tester.longPress(find.byType(StudysetBackButton));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Library'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,265 @@
|
|||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/l10n/app_languages.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/theme/learn_theme.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/model/study_progress.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_card.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
final vocabulary = Studyset(
|
||||
id: 'vocabulary',
|
||||
content: StudysetContent(
|
||||
title: 'Vocabulary',
|
||||
description: RichContent.empty(),
|
||||
secondaryTitle: 'Japanese',
|
||||
colorName: 'cobalt',
|
||||
),
|
||||
);
|
||||
|
||||
/// 52 cards, a quarter of them mature, none due.
|
||||
const nothingDue = StudyProgress(cards: 52, due: 0, reviewsDue: 0, mature: 13);
|
||||
|
||||
void main() {
|
||||
late List<String> calls;
|
||||
|
||||
setUp(() => calls = []);
|
||||
|
||||
Future<void> show(
|
||||
WidgetTester tester, {
|
||||
Studyset? studyset,
|
||||
StudyProgress? progress = nothingDue,
|
||||
}) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: learnTheme(Brightness.light),
|
||||
localizationsDelegates: appLocalizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: SizedBox(
|
||||
width: 360,
|
||||
height: 188,
|
||||
child: StudysetCard(
|
||||
studyset: studyset ?? vocabulary,
|
||||
progress: progress,
|
||||
published: false,
|
||||
onShare: () => calls.add('share'),
|
||||
onOpen: () => calls.add('open'),
|
||||
onStudy: () => calls.add('study'),
|
||||
onToggleStar: () => calls.add('star'),
|
||||
onOption: (option) => calls.add(option.name),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
Studyset changed({DateTime? archivedAt, DateTime? starredAt}) => Studyset(
|
||||
id: vocabulary.id,
|
||||
content: vocabulary.content,
|
||||
archivedAt: archivedAt,
|
||||
starredAt: starredAt,
|
||||
);
|
||||
|
||||
testWidgets('shows the title with its kicker and what the studyset '
|
||||
'holds', (tester) async {
|
||||
await show(tester);
|
||||
|
||||
expect(find.text('JAPANESE'), findsOneWidget);
|
||||
expect(find.text('Vocabulary'), findsOneWidget);
|
||||
expect(find.text('52 flashcards'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('says when a studyset holds nothing yet', (tester) async {
|
||||
await show(
|
||||
tester,
|
||||
progress: const StudyProgress(cards: 0, due: 0, reviewsDue: 0, mature: 0),
|
||||
);
|
||||
|
||||
expect(find.text('No content yet'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('claims nothing while its progress is being worked out', (
|
||||
tester,
|
||||
) async {
|
||||
await show(tester, progress: null);
|
||||
|
||||
expect(find.textContaining('flashcard'), findsNothing);
|
||||
expect(find.text('No content yet'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('invites a review when reviews are due, with how many cards '
|
||||
'are due', (tester) async {
|
||||
await show(
|
||||
tester,
|
||||
progress: const StudyProgress(
|
||||
cards: 52,
|
||||
due: 12,
|
||||
reviewsDue: 3,
|
||||
mature: 13,
|
||||
),
|
||||
);
|
||||
expect(find.text('12'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byTooltip('Review'));
|
||||
|
||||
expect(calls, ['study']);
|
||||
});
|
||||
|
||||
testWidgets('invites studying when only new cards are due', (tester) async {
|
||||
await show(
|
||||
tester,
|
||||
progress: const StudyProgress(
|
||||
cards: 52,
|
||||
due: 20,
|
||||
reviewsDue: 0,
|
||||
mature: 0,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byTooltip('Study'));
|
||||
|
||||
expect(calls, ['study']);
|
||||
});
|
||||
|
||||
testWidgets('invites adding content when nothing is due, which opens '
|
||||
'it', (tester) async {
|
||||
await show(tester);
|
||||
expect(find.text('0'), findsNothing);
|
||||
|
||||
await tester.tap(find.byTooltip('Add content'));
|
||||
|
||||
expect(calls, ['open']);
|
||||
});
|
||||
|
||||
testWidgets('counts at most 99+ due cards', (tester) async {
|
||||
await show(
|
||||
tester,
|
||||
progress: const StudyProgress(
|
||||
cards: 900,
|
||||
due: 875,
|
||||
reviewsDue: 875,
|
||||
mature: 0,
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('99+'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('fills its ring with the share of mature cards', (tester) async {
|
||||
await show(tester);
|
||||
|
||||
final ring = tester.widget<CircularProgressIndicator>(
|
||||
find.byType(CircularProgressIndicator),
|
||||
);
|
||||
expect(ring.value, 0.25);
|
||||
});
|
||||
|
||||
testWidgets('an archived studyset rests: dimmed and paused, inviting '
|
||||
'nothing', (tester) async {
|
||||
await show(
|
||||
tester,
|
||||
studyset: changed(archivedAt: DateTime.utc(2026, 10)),
|
||||
progress: const StudyProgress(
|
||||
cards: 52,
|
||||
due: 12,
|
||||
reviewsDue: 3,
|
||||
mature: 13,
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byTooltip('Archived'), findsOneWidget);
|
||||
expect(find.byTooltip('Review'), findsNothing);
|
||||
expect(find.text('12'), findsNothing);
|
||||
expect(
|
||||
tester
|
||||
.widget<Opacity>(
|
||||
find.ancestor(
|
||||
of: find.text('Vocabulary'),
|
||||
matching: find.byType(Opacity),
|
||||
),
|
||||
)
|
||||
.opacity,
|
||||
0.6,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('opens when tapped', (tester) async {
|
||||
await show(tester);
|
||||
|
||||
await tester.tap(find.text('Vocabulary'));
|
||||
|
||||
expect(calls, ['open']);
|
||||
});
|
||||
|
||||
testWidgets('a starred studyset shows its star, which takes it away', (
|
||||
tester,
|
||||
) async {
|
||||
await show(tester, studyset: changed(starredAt: DateTime.utc(2026, 10)));
|
||||
|
||||
await tester.tap(find.byTooltip('Unstar'));
|
||||
|
||||
expect(calls, ['star']);
|
||||
});
|
||||
|
||||
testWidgets('shows the star of an unstarred studyset only to a pointer '
|
||||
'hovering over it', (tester) async {
|
||||
await show(tester);
|
||||
expect(find.byTooltip('Star').hitTestable(), findsNothing);
|
||||
|
||||
final mouse = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
addTearDown(mouse.removePointer);
|
||||
await mouse.addPointer(location: tester.getCenter(find.text('Vocabulary')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byTooltip('Star').hitTestable(), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a long press stars it', (tester) async {
|
||||
await show(tester);
|
||||
|
||||
await tester.longPress(find.text('Vocabulary'));
|
||||
|
||||
expect(calls, ['star']);
|
||||
});
|
||||
|
||||
for (final (option, call) in [
|
||||
('Edit', 'edit'),
|
||||
('Import content', 'importContent'),
|
||||
('Export content', 'exportContent'),
|
||||
('Reset progress', 'reset'),
|
||||
('Archive', 'archive'),
|
||||
('Delete', 'delete'),
|
||||
]) {
|
||||
testWidgets('its options include "$option"', (tester) async {
|
||||
await show(tester);
|
||||
|
||||
await tester.tap(find.byTooltip('Options'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Unarchive'), findsNothing);
|
||||
await tester.tap(find.text(option));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(calls, [call]);
|
||||
});
|
||||
}
|
||||
|
||||
testWidgets('an archived studyset offers to unarchive it', (tester) async {
|
||||
await show(tester, studyset: changed(archivedAt: DateTime.utc(2026, 10)));
|
||||
|
||||
await tester.tap(find.byTooltip('Options'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Archive'), findsNothing);
|
||||
await tester.tap(find.text('Unarchive'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(calls, ['unarchive']);
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue