feat(app): show added material in its studyset tab
An Anki import switches to Flashcards when the cards come into this studyset (flattened or tagged by subdeck), not when subdecks become sub-studysets; an imported path switches to Paths. Creating flashcards, a note or a path switches to its tab as well, seen on coming back. On opening, the studyset shows the first tab with content of its own: unfinished paths, then flashcards, then notes; Flashcards when it holds nothing. Its topics' content doesn't count, since the tabs list only the studyset's own. A tab tapped or swiped to by the person, or material added, stays. Add material is built both in the app bar and in the bottom bar, so a ContentTabCubit carries the requests to the tab controller, and importFromAnki now returns the mode it imported with. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
e4bfe3bdeb
commit
9324432f61
5 changed files with 276 additions and 26 deletions
|
|
@ -11,8 +11,9 @@ import 'package:material_ui/material_ui.dart';
|
|||
|
||||
/// Lets the person pick an Anki package (`.apkg`, or a whole collection's
|
||||
/// `.colpkg`) and imports it into the studyset [studysetId], as they choose
|
||||
/// in a sheet; then says what came over.
|
||||
Future<void> importFromAnki(
|
||||
/// in a sheet; then says what came over. Completes with how its decks became
|
||||
/// studysets, or with null when nothing was imported.
|
||||
Future<AnkiImportMode?> importFromAnki(
|
||||
BuildContext context, {
|
||||
required String studysetId,
|
||||
}) async {
|
||||
|
|
@ -23,7 +24,7 @@ Future<void> importFromAnki(
|
|||
extensions: const ['apkg', 'colpkg'],
|
||||
);
|
||||
if (file == null || !context.mounted) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
final done = await showAdaptiveSheet<AnkiImportStateDone>(
|
||||
context,
|
||||
|
|
@ -49,7 +50,7 @@ Future<void> importFromAnki(
|
|||
},
|
||||
);
|
||||
if (done == null) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
final result = done.result;
|
||||
messenger.showSnackBar(
|
||||
|
|
@ -71,6 +72,7 @@ Future<void> importFromAnki(
|
|||
),
|
||||
),
|
||||
);
|
||||
return done.mode;
|
||||
}
|
||||
|
||||
class const _AnkiImport() extends StatelessWidget {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
/// The tabs of a studyset's content, in their order.
|
||||
enum StudysetTab() {
|
||||
/// Its learning paths.
|
||||
paths,
|
||||
|
||||
/// Its flashcards.
|
||||
flashcards,
|
||||
|
||||
/// Its notes.
|
||||
notes,
|
||||
}
|
||||
|
||||
/// The last request to show a content tab, if any; `serial` tells two
|
||||
/// requests for one tab apart.
|
||||
typedef ContentTabState = ({StudysetTab tab, int serial})?;
|
||||
|
||||
/// Asks the studyset screen to show a content tab: the one where material
|
||||
/// just added or imported lands (decided 2026-10-03), so the person sees it
|
||||
/// rather than looking for it.
|
||||
class ContentTabCubit() extends Cubit<ContentTabState> {
|
||||
this : super(null);
|
||||
|
||||
/// Shows [tab].
|
||||
void show(StudysetTab tab) =>
|
||||
emit((tab: tab, serial: (state?.serial ?? 0) + 1));
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
|
@ -15,12 +17,14 @@ import 'package:halkyon_learn_flutter/src/core/widget/scroll_hiding_fab.dart';
|
|||
import 'package:halkyon_learn_flutter/src/core/widget/section_card.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/title_with_kicker.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/undo_snack_bar.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/anki/data/anki_importer.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/anki/widget/anki_import_sheet.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/widget/card_list.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/bloc/path_list_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/widget/path_list.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/note/widget/note_list.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/model/study_progress.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/bloc/content_tab_cubit.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/bloc/studyset_list_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_tree.dart';
|
||||
|
|
@ -76,6 +80,8 @@ class const StudysetScreen({
|
|||
studyset.id,
|
||||
)..add(const PathListEventStarted()),
|
||||
),
|
||||
// The content tab to show once material was added.
|
||||
BlocProvider(create: (context) => ContentTabCubit()),
|
||||
],
|
||||
child: MultiBlocListener(
|
||||
listeners: [
|
||||
|
|
@ -95,6 +101,9 @@ class const StudysetScreen({
|
|||
listener: (context, state) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final result = state.importResult!;
|
||||
if (result.failure == null) {
|
||||
context.read<ContentTabCubit>().show(StudysetTab.paths);
|
||||
}
|
||||
final message = switch (result.failure) {
|
||||
final failure? => l10n.pathImportFailed(failure),
|
||||
null => [
|
||||
|
|
@ -259,9 +268,14 @@ class const _StudysetView({
|
|||
}
|
||||
|
||||
/// The studyset's content, in tabs that appear as their features land:
|
||||
/// learning paths, flashcards and notes for now. It opens on the flashcards,
|
||||
/// and on the paths instead once it is known that some path isn't completed
|
||||
/// yet, unless the person chose a tab meanwhile.
|
||||
/// learning paths, flashcards and notes for now.
|
||||
///
|
||||
/// It opens on the first tab with something of the studyset's own in it
|
||||
/// (its topics' content is in theirs), in their order: paths not completed
|
||||
/// yet, flashcards, notes; on the flashcards when there is nothing (decided
|
||||
/// 2026-10-03). Material added or imported shows in its own tab
|
||||
/// ([ContentTabCubit]). Once the person chose a tab, or material was added,
|
||||
/// opening no longer moves it.
|
||||
class const _Content({required final String studysetId})
|
||||
extends StatefulWidget {
|
||||
@override
|
||||
|
|
@ -271,13 +285,26 @@ class const _Content({required final String studysetId})
|
|||
class _ContentState()
|
||||
extends State<_Content>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final _tabs = TabController(length: 3, vsync: this, initialIndex: 1);
|
||||
late final _tabs = TabController(
|
||||
length: StudysetTab.values.length,
|
||||
vsync: this,
|
||||
initialIndex: StudysetTab.flashcards.index,
|
||||
);
|
||||
|
||||
/// Whether the tab shown is settled: chosen on opening, by the person, or
|
||||
/// for material added.
|
||||
var _settled = false;
|
||||
|
||||
/// Whether the studyset itself holds cards and notes, read once on
|
||||
/// opening; null until then.
|
||||
({bool cards, bool notes})? _holds;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Swiping to a tab is choosing it as much as tapping it is.
|
||||
_tabs.addListener(() => _settled = true);
|
||||
unawaited(_readWhatItHolds());
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -286,18 +313,67 @@ class _ContentState()
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _readWhatItHolds() async {
|
||||
final person = PersonScope.of(context);
|
||||
try {
|
||||
final cards = await person.cards.cardsOf(widget.studysetId);
|
||||
final notes = await person.notes.countsByStudyset();
|
||||
_holds = (
|
||||
cards: cards.isNotEmpty,
|
||||
notes: notes.containsKey(widget.studysetId),
|
||||
);
|
||||
} on Object catch (error) {
|
||||
// It stays on the flashcards, as it opened.
|
||||
log(
|
||||
'Reading what a studyset holds failed',
|
||||
name: 'Studyset',
|
||||
error: error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
_chooseOnOpening();
|
||||
}
|
||||
|
||||
void _show(StudysetTab tab) {
|
||||
_settled = true;
|
||||
if (_tabs.index != tab.index) {
|
||||
_tabs.animateTo(tab.index);
|
||||
}
|
||||
}
|
||||
|
||||
/// Chooses the tab to open on, once enough is known: paths in progress
|
||||
/// win at once; the others once both are known.
|
||||
void _chooseOnOpening() {
|
||||
if (_settled || !mounted) {
|
||||
return;
|
||||
}
|
||||
final paths = context.read<PathListBloc>().state;
|
||||
if (paths.completionKnown && paths.hasOpenPaths) {
|
||||
_show(StudysetTab.paths);
|
||||
} else if (paths.completionKnown && _holds != null) {
|
||||
_show(switch (_holds!) {
|
||||
(cards: false, notes: true) => StudysetTab.notes,
|
||||
_ => StudysetTab.flashcards,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return BlocListener<PathListBloc, PathListState>(
|
||||
listenWhen: (before, now) =>
|
||||
!before.completionKnown && now.completionKnown,
|
||||
listener: (context, state) {
|
||||
if (!_settled && state.hasOpenPaths) {
|
||||
_tabs.index = 0;
|
||||
}
|
||||
_settled = true;
|
||||
},
|
||||
return MultiBlocListener(
|
||||
listeners: [
|
||||
BlocListener<ContentTabCubit, ContentTabState>(
|
||||
listener: (context, request) {
|
||||
if (request != null) {
|
||||
_show(request.tab);
|
||||
}
|
||||
},
|
||||
),
|
||||
BlocListener<PathListBloc, PathListState>(
|
||||
listener: (context, _) => _chooseOnOpening(),
|
||||
),
|
||||
],
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
|
|
@ -306,9 +382,14 @@ class _ContentState()
|
|||
isScrollable: true,
|
||||
tabAlignment: TabAlignment.start,
|
||||
tabs: [
|
||||
Tab(text: l10n.studysetTabPaths),
|
||||
Tab(text: l10n.studysetTabFlashcards),
|
||||
Tab(text: l10n.studysetTabNotes),
|
||||
for (final tab in StudysetTab.values)
|
||||
Tab(
|
||||
text: switch (tab) {
|
||||
StudysetTab.paths => l10n.studysetTabPaths,
|
||||
StudysetTab.flashcards => l10n.studysetTabFlashcards,
|
||||
StudysetTab.notes => l10n.studysetTabNotes,
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
|
|
@ -454,7 +535,9 @@ class const _StudyPill({
|
|||
|
||||
/// What can be added to the studyset, behind [builder]'s trigger:
|
||||
/// flashcards, notes and learning paths for now; files come with their
|
||||
/// feature.
|
||||
/// feature. What is added shows in its own tab ([ContentTabCubit]): what is
|
||||
/// written there once the person comes back, what is imported once it is
|
||||
/// in.
|
||||
class const _AddMaterial({
|
||||
required final String studysetId,
|
||||
required final Widget Function(BuildContext context, MenuController menu)
|
||||
|
|
@ -467,19 +550,27 @@ class const _AddMaterial({
|
|||
menuChildren: [
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(LucideIcons.plus),
|
||||
onPressed: () => context.push(RoutePaths.newCard(studysetId)),
|
||||
onPressed: () {
|
||||
context.read<ContentTabCubit>().show(StudysetTab.flashcards);
|
||||
context.push(RoutePaths.newCard(studysetId));
|
||||
},
|
||||
child: Text(l10n.studysetCreateFlashcards),
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(LucideIcons.notebookPen),
|
||||
onPressed: () => context.push(RoutePaths.newNote(studysetId)),
|
||||
onPressed: () {
|
||||
context.read<ContentTabCubit>().show(StudysetTab.notes);
|
||||
context.push(RoutePaths.newNote(studysetId));
|
||||
},
|
||||
child: Text(l10n.studysetCreateNote),
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(LucideIcons.route),
|
||||
onPressed: () async {
|
||||
final bloc = context.read<PathListBloc>();
|
||||
final tabs = context.read<ContentTabCubit>();
|
||||
if (await askForNewPath(context) case final content?) {
|
||||
tabs.show(StudysetTab.paths);
|
||||
bloc.add(PathListEventCreateRequested(content));
|
||||
}
|
||||
},
|
||||
|
|
@ -487,7 +578,17 @@ class const _AddMaterial({
|
|||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(LucideIcons.fileInput),
|
||||
onPressed: () => importFromAnki(context, studysetId: studysetId),
|
||||
onPressed: () async {
|
||||
final tabs = context.read<ContentTabCubit>();
|
||||
// Cards that came into this studyset show in its flashcards;
|
||||
// subdecks become topics instead.
|
||||
switch (await importFromAnki(context, studysetId: studysetId)) {
|
||||
case AnkiImportMode.flatten || AnkiImportMode.tagBySubdeck:
|
||||
tabs.show(StudysetTab.flashcards);
|
||||
case AnkiImportMode.subStudysets || null:
|
||||
break;
|
||||
}
|
||||
},
|
||||
child: Text(l10n.ankiImportMenu),
|
||||
),
|
||||
MenuItemButton(
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.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/note/data/note_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/note/model/note_content.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:material_ui/material_ui.dart';
|
||||
|
|
@ -10,6 +13,7 @@ import 'package:material_ui/material_ui.dart';
|
|||
import '../../../support/fake_asset_server_gateway.dart';
|
||||
import '../../../support/fake_card_gateway.dart';
|
||||
import '../../../support/fake_file_picker.dart';
|
||||
import '../../../support/fake_note_gateway.dart';
|
||||
import '../../../support/fake_studyset_gateway.dart';
|
||||
import '../../../support/pump_app.dart';
|
||||
import '../../../support/test_dependencies.dart';
|
||||
|
|
@ -19,13 +23,30 @@ void main() {
|
|||
late FakeCardGateway cardGateway;
|
||||
late FakeFilePicker files;
|
||||
late FakeAssetServerGateway server;
|
||||
late FakeNoteGateway noteGateway;
|
||||
|
||||
setUp(() {
|
||||
cardGateway = FakeCardGateway();
|
||||
files = FakeFilePicker();
|
||||
server = FakeAssetServerGateway();
|
||||
noteGateway = FakeNoteGateway();
|
||||
});
|
||||
|
||||
/// The index of the studyset's content tab shown: 0 paths, 1 flashcards,
|
||||
/// 2 notes.
|
||||
int shownTab(WidgetTester tester) =>
|
||||
tester.widget<TabBar>(find.byType(TabBar)).controller!.index;
|
||||
|
||||
/// A note in the studyset, so that it opens on its notes.
|
||||
Future<void> withNote() async {
|
||||
final notes = NoteRepository(noteGateway);
|
||||
addTearDown(notes.dispose);
|
||||
await notes.create(
|
||||
'japanese',
|
||||
NoteContent(title: 'Particles', body: RichContent.empty()),
|
||||
);
|
||||
}
|
||||
|
||||
/// Opens the studyset Japanese, in a window wide enough for Add material
|
||||
/// to be a button.
|
||||
Future<void> showStudyset(WidgetTester tester) async {
|
||||
|
|
@ -41,11 +62,15 @@ void main() {
|
|||
encodeStudysetPayload(StudysetContent.titled('Japanese')),
|
||||
),
|
||||
cardGateway: cardGateway,
|
||||
noteGateway: noteGateway,
|
||||
files: files,
|
||||
assetServer: server,
|
||||
),
|
||||
location: RoutePaths.studyset('japanese'),
|
||||
);
|
||||
// Lets the lists settle, and the screen choose its first tab.
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
/// Chooses Import from Anki under Add material.
|
||||
|
|
@ -90,6 +115,33 @@ void main() {
|
|||
expect(await cards.cardsOf('japanese'), hasLength(4));
|
||||
});
|
||||
|
||||
testWidgets('shows the flashcards after a deck came into this studyset, '
|
||||
'whatever tab was open', (tester) async {
|
||||
await withNote();
|
||||
await startImport(tester, fixture('anki_latest.apkg'));
|
||||
await tester.tap(find.text('Tag cards by subdeck'));
|
||||
await tester.pump();
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Import'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(shownTab(tester), 1);
|
||||
});
|
||||
|
||||
testWidgets('leaves the tab as it was when the subdecks became '
|
||||
'sub-studysets', (tester) async {
|
||||
await withNote();
|
||||
files.next = (name: 'japanese.apkg', bytes: fixture('anki_latest.apkg'));
|
||||
await showStudyset(tester);
|
||||
expect(shownTab(tester), 2);
|
||||
await chooseImport(tester);
|
||||
await tester.tap(find.text('Recreate as sub-studysets'));
|
||||
await tester.pump();
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Import'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(shownTab(tester), 2);
|
||||
});
|
||||
|
||||
testWidgets('warns when the files may not fit in the room left', (
|
||||
tester,
|
||||
) async {
|
||||
|
|
|
|||
|
|
@ -7,12 +7,15 @@ import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_pa
|
|||
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/exercise.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/learning_path.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/step_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/note/data/note_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/note/model/note_content.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:material_ui/material_ui.dart';
|
||||
|
||||
import '../../../support/fake_file_picker.dart';
|
||||
import '../../../support/fake_learning_path_gateway.dart';
|
||||
import '../../../support/fake_note_gateway.dart';
|
||||
import '../../../support/fake_path_answer_gateway.dart';
|
||||
import '../../../support/fake_studyset_gateway.dart';
|
||||
import '../../../support/pump_app.dart';
|
||||
|
|
@ -23,6 +26,7 @@ void main() {
|
|||
late FakeLearningPathGateway pathGateway;
|
||||
late FakePathAnswerGateway answers;
|
||||
late FakeFilePicker files;
|
||||
late FakeNoteGateway noteGateway;
|
||||
|
||||
setUp(() {
|
||||
studysets = FakeStudysetGateway()
|
||||
|
|
@ -33,8 +37,14 @@ void main() {
|
|||
pathGateway = FakeLearningPathGateway();
|
||||
answers = FakePathAnswerGateway();
|
||||
files = FakeFilePicker();
|
||||
noteGateway = FakeNoteGateway();
|
||||
});
|
||||
|
||||
/// The index of the studyset's content tab shown: 0 paths, 1 flashcards,
|
||||
/// 2 notes.
|
||||
int shownTab(WidgetTester tester) =>
|
||||
tester.widget<TabBar>(find.byType(TabBar)).controller!.index;
|
||||
|
||||
Future<void> showApp(WidgetTester tester, String location) async {
|
||||
tester.view.physicalSize = const Size(1200, 1000);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
|
@ -45,6 +55,7 @@ void main() {
|
|||
studysetGateway: studysets,
|
||||
pathGateway: pathGateway,
|
||||
answerGateway: answers,
|
||||
noteGateway: noteGateway,
|
||||
files: files,
|
||||
),
|
||||
location: location,
|
||||
|
|
@ -92,6 +103,39 @@ void main() {
|
|||
expect(find.text('Particles'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a studyset with only notes opens on them', (tester) async {
|
||||
final notes = NoteRepository(noteGateway);
|
||||
addTearDown(notes.dispose);
|
||||
await notes.create(
|
||||
'japanese',
|
||||
NoteContent(title: 'Particles', body: RichContent.empty()),
|
||||
);
|
||||
|
||||
await showApp(tester, RoutePaths.studyset('japanese'));
|
||||
|
||||
expect(shownTab(tester), 2);
|
||||
expect(find.text('Particles'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets("notes only in its topics don't open the studyset on its own "
|
||||
'empty notes', (tester) async {
|
||||
studysets.receiveRemoteChange(
|
||||
'grammar',
|
||||
encodeStudysetPayload(StudysetContent.titled('Grammar')),
|
||||
parentId: 'japanese',
|
||||
);
|
||||
final notes = NoteRepository(noteGateway);
|
||||
addTearDown(notes.dispose);
|
||||
await notes.create(
|
||||
'grammar',
|
||||
NoteContent(title: 'Particles', body: RichContent.empty()),
|
||||
);
|
||||
|
||||
await showApp(tester, RoutePaths.studyset('japanese'));
|
||||
|
||||
expect(shownTab(tester), 1);
|
||||
});
|
||||
|
||||
testWidgets('without paths, the studyset opens on its flashcards', (
|
||||
tester,
|
||||
) async {
|
||||
|
|
@ -156,11 +200,34 @@ void main() {
|
|||
findsOneWidget,
|
||||
);
|
||||
expect(find.textContaining('1 block was left out'), findsOneWidget);
|
||||
await tester.tap(find.text('Paths'));
|
||||
await tester.pumpAndSettle();
|
||||
// The paths show by themselves.
|
||||
expect(shownTab(tester), 0);
|
||||
expect(find.text('Particles'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a note written from Add material shows in the Notes tab on '
|
||||
'coming back, as flashcards do in theirs', (tester) async {
|
||||
final paths = repository();
|
||||
final id = await paths.createPath(
|
||||
'japanese',
|
||||
LearningPathContent.titled('Particles'),
|
||||
);
|
||||
await paths.createStep(id, StepKind.info, step(StepKind.info, 'Read'));
|
||||
await showApp(tester, RoutePaths.studyset('japanese'));
|
||||
expect(shownTab(tester), 0);
|
||||
|
||||
await tester.tap(find.text('Add material'));
|
||||
await tester.pumpAndSettle();
|
||||
await chooseInMenu(tester, 'Create a note');
|
||||
await tester.enterText(find.byType(TextField), 'Topic or subject');
|
||||
await tester.pump();
|
||||
await tester.pageBack();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(shownTab(tester), 2);
|
||||
expect(find.text('Topic or subject'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('works through a path: an info step, a choice answered wrong '
|
||||
'then right, and the finish', (tester) async {
|
||||
final paths = repository();
|
||||
|
|
|
|||
Loading…
Reference in a new issue