halkyon/halkyon_learn_flutter/test/feature/dictionary/dictionary_screen_test.dart

600 lines
19 KiB
Dart
Raw Normal View History

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/card/model/card_content.dart';
import 'package:halkyon_learn_flutter/src/feature/kanji/data/stroke_repository.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_card_gateway.dart';
import '../../support/fake_dictionary_gateway.dart';
import '../../support/fake_studyset_gateway.dart';
import '../../support/loaded_strokes.dart';
import '../../support/pump_app.dart';
import '../../support/test_dependencies.dart';
final taberu = word(
'1358280',
'食べる',
reading: 'たべる',
glosses: ['to eat'],
labels: ['common', 'jlpt:n5'],
pos: ['v1'],
);
final terebi = word(
'1080510',
'テレビ',
glosses: ['television'],
labels: ['common', 'jlpt:n5'],
pos: ['n'],
);
final toukyou = word(
'1447690',
'東京',
reading: 'とうきょう',
glosses: ['Tokyo'],
labels: ['common'],
pos: ['n'],
);
final shoku = character(
'食',
meanings: ['eat', 'food'],
on: ['ショク'],
kun: ['た.べる'],
labels: ['jlpt:n5', 'grade:2'],
grade: 2,
strokes: 9,
);
final a = character('あ', labels: ['hiragana'], romaji: 'a');
/// An instance with JMdict and KANJIDIC2.
FakeDictionaryGateway withKanji() => FakeDictionaryGateway(
available: [jmdict, kanjidic],
words: [taberu, terebi, shoku, a],
);
void main() {
late FakeCardGateway cardGateway;
late CardRepository cards;
setUp(() {
cardGateway = FakeCardGateway();
cards = CardRepository(cardGateway);
});
tearDown(() => cards.dispose());
/// Lets the editors' timers run out and the screen settle.
Future<void> settle(WidgetTester tester) async {
await tester.pump(const Duration(seconds: 1));
await tester.pumpAndSettle();
}
/// Opens the studyset [id]: `japanese` (Japanese with English meanings),
/// `german`, `greek` or `chinese`, on an instance with JMdict
/// ([dictionaries]), at
/// [location] (the studyset's screen unless given).
Future<void> show(
WidgetTester tester, {
String id = 'japanese',
FakeDictionaryGateway? dictionaries,
StrokeRepository? strokes,
String? location,
}) async {
tester.view.physicalSize = const Size(1400, 1600);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
final studysets = FakeStudysetGateway()
..receiveRemoteChange(
'japanese',
encodeStudysetPayload(
StudysetContent(
title: 'Japanese',
description: RichContent.empty(),
frontLanguage: 'ja',
backLanguage: 'en',
),
),
)
..receiveRemoteChange(
'german',
encodeStudysetPayload(
StudysetContent(
title: 'German',
description: RichContent.empty(),
frontLanguage: 'de',
backLanguage: 'en',
),
),
)
..receiveRemoteChange(
'greek',
encodeStudysetPayload(
StudysetContent(
title: 'Greek',
description: RichContent.empty(),
frontLanguage: 'el',
backLanguage: 'en',
),
),
)
..receiveRemoteChange(
'chinese',
encodeStudysetPayload(
StudysetContent(
title: 'Chinese',
description: RichContent.empty(),
frontLanguage: 'zh-Hans',
backLanguage: 'en',
),
),
);
await pumpApp(
tester,
testDependencies(
studysetGateway: studysets,
cardGateway: cardGateway,
dictionaryGateway:
dictionaries ??
FakeDictionaryGateway(words: [taberu, terebi, toukyou]),
strokes: strokes,
),
location: location ?? RoutePaths.studyset(id),
);
await settle(tester);
}
/// Chooses [item] under Add material.
Future<void> addMaterial(WidgetTester tester, String item) async {
await tester.tap(find.text('Add material'));
await tester.pumpAndSettle();
await tester.tap(find.text(item));
await tester.pump();
await tester.pump();
await settle(tester);
}
testWidgets('Add material offers the dictionary only where the '
"studyset's language has one", (tester) async {
await show(tester, id: 'german');
await tester.tap(find.text('Add material'));
await tester.pumpAndSettle();
expect(find.text('From the dictionary'), findsNothing);
});
testWidgets('a word found is added as a flashcard, and then shows as in '
'the studyset; the screen credits its sources', (tester) async {
await show(tester);
await addMaterial(tester, 'From the dictionary');
// Its sources by name in a line; who made them, under which licence,
// a tap away.
expect(find.textContaining('Words from JMdict'), findsOneWidget);
expect(find.textContaining('EDRDG'), findsNothing);
await tester.tap(find.byTooltip('Sources'));
await tester.pumpAndSettle();
expect(find.textContaining('EDRDG'), findsOneWidget);
await tester.tap(find.byTooltip('Close'));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField), 'たべ');
await tester.pump(const Duration(milliseconds: 300));
await tester.pumpAndSettle();
expect(find.textContaining('食べる'), findsOneWidget);
await tester.tap(find.text('Add'));
await tester.pumpAndSettle();
expect(find.text('Added “食べる”.'), findsOneWidget);
expect(find.byTooltip('In this studyset'), findsOneWidget);
final card = (await cards.cardsOf('japanese')).single;
expect(card.content.entry, (source: 'jmdict', id: '1358280'));
expect(card.content.reading, 'たべる');
});
testWidgets('tapping Add twice makes one card', (tester) async {
await show(tester);
await addMaterial(tester, 'From the dictionary');
await tester.enterText(find.byType(TextField), 'たべ');
await tester.pump(const Duration(milliseconds: 300));
await tester.pumpAndSettle();
await tester.tap(find.text('Add'));
await tester.tap(find.text('Add'), warnIfMissed: false);
await tester.pumpAndSettle();
expect(await cards.cardsOf('japanese'), hasLength(1));
});
testWidgets('without the instance, the dictionary says so and offers to '
'try again, which finds it once the instance answers', (tester) async {
final dictionaries = FakeDictionaryGateway(words: [taberu])
..fail = Exception('offline');
await show(
tester,
dictionaries: dictionaries,
location: RoutePaths.dictionary('japanese'),
);
expect(find.text("The dictionary can't be reached right now."), findsOne);
dictionaries.fail = null;
await tester.tap(find.text('Retry'));
await settle(tester);
expect(find.text('N5'), findsOneWidget);
});
testWidgets('a studyset whose language has no dictionary says so', (
tester,
) async {
await show(tester, id: 'german', location: RoutePaths.dictionary('german'));
expect(
find.text("This studyset's language has no dictionary on this instance."),
findsOneWidget,
);
});
testWidgets('a whole level becomes flashcards, its words in its order', (
tester,
) async {
await show(tester);
await addMaterial(tester, 'From the dictionary');
expect(find.text('N5'), findsOneWidget);
await tester.tap(find.text('Add 2 words'));
await tester.pumpAndSettle();
expect(find.text('Added 2 words.'), findsOneWidget);
expect(
[
for (final card in await cards.cardsOf('japanese'))
card.content.front.plainText.trim(),
],
['食べる', 'テレビ'],
);
});
testWidgets('beside the words, kanji and kana: their levels in groups, '
'one added as character cards tagged with level and grade', (
tester,
) async {
await show(tester, dictionaries: withKanji());
await addMaterial(tester, 'From the dictionary');
expect(find.text('Words'), findsOneWidget);
await tester.tap(find.text('Kanji & kana'));
await settle(tester);
expect(find.text('Kana'), findsOneWidget);
expect(find.text('JLPT levels'), findsOneWidget);
expect(find.text('School grades'), findsOneWidget);
expect(find.text('Grade 2'), findsOneWidget);
await tester.tap(find.text('N5').hitTestable());
await tester.pumpAndSettle();
await tester.tap(find.text('Add 1 character'));
await tester.pumpAndSettle();
expect(find.text('Added 1 character.'), findsOneWidget);
final made = (await cards.cardsOf('japanese')).single;
expect(made.content.type, CardType.character);
expect(made.content.onReadings, ['ショク']);
expect(made.content.entry, (source: 'kanjidic', id: '食'));
});
testWidgets("Greek's letters in a tab of their own, without radicals, "
'added whole as character cards', (tester) async {
await show(
tester,
id: 'greek',
dictionaries: FakeDictionaryGateway(
available: [wiktionaryEl, greekAlphabet],
words: [
character(
'Β β',
labels: ['greek'],
romaji: 'v',
name: 'βήτα (beta)',
of: greekAlphabet,
),
],
),
);
await addMaterial(tester, 'From the dictionary');
expect(find.text('Radicals'), findsNothing);
await tester.tap(find.text('Alphabet'));
await settle(tester);
expect(find.text('Letters'), findsOneWidget);
await tester.tap(find.text('Greek alphabet').hitTestable());
await tester.pumpAndSettle();
await tester.tap(find.text('Add 1 character'));
await tester.pumpAndSettle();
final made = (await cards.cardsOf('greek')).single;
expect(made.content.front.plainText.trim(), 'Β β');
expect(made.content.meanings, ['v', 'βήτα (beta)']);
});
testWidgets('Chinese characters by HSK level and in common use, beside '
'the radicals in their simplified forms', (tester) async {
await show(
tester,
id: 'chinese',
dictionaries: FakeDictionaryGateway(
available: [ccCedictHans, unihanHans],
words: [
character(
'说',
meanings: ['speak'],
on: ['shuō'],
labels: ['hsk:1', 'common', 'radical:149'],
of: unihanHans,
),
],
),
);
await addMaterial(tester, 'From the dictionary');
await tester.tap(find.text('Hanzi'));
await settle(tester);
expect(find.text('HSK levels'), findsOneWidget);
expect(find.text('In common use'), findsOneWidget);
expect(find.text('HSK 1'), findsOneWidget);
await tester.tap(find.text('Radicals'));
await settle(tester);
// 言 as simplified Chinese writes it at a character's side.
await tester.scrollUntilVisible(
find.text('讠'),
400,
scrollable: find.byType(Scrollable).last,
);
expect(find.text('讠'), findsOneWidget);
});
testWidgets('a character found shows its readings and strokes, and is '
'added as a card', (tester) async {
await show(tester, dictionaries: withKanji());
await addMaterial(tester, 'From the dictionary');
await tester.tap(find.text('Kanji & kana'));
await settle(tester);
await tester.enterText(find.byType(TextField).hitTestable(), 'ショク');
await tester.pump(const Duration(milliseconds: 300));
await tester.pumpAndSettle();
expect(find.text('ショク · た.べる'), findsOneWidget);
expect(find.text('9 strokes'), findsOneWidget);
await tester.tap(find.text('Add').hitTestable());
await tester.pumpAndSettle();
expect(find.text('Added “食”.'), findsOneWidget);
});
testWidgets('a character found shows how it is written when tapped', (
tester,
) async {
await show(
tester,
dictionaries: withKanji(),
strokes: await loadedStrokes(tester, ['食']),
);
await addMaterial(tester, 'From the dictionary');
await tester.tap(find.text('Kanji & kana'));
await settle(tester);
await tester.enterText(find.byType(TextField).hitTestable(), 'eat');
await tester.pump(const Duration(milliseconds: 300));
await tester.pumpAndSettle();
await tester.tap(find.text('eat; food'));
await tester.pumpAndSettle();
expect(find.text('Stroke order'), findsNothing);
expect(find.text('Play again'), findsOneWidget);
expect(find.text('eat, food'), findsOneWidget);
expect(find.text('N5 · Grade 2'), findsOneWidget);
});
testWidgets('a Greek letter found opens no sheet of its strokes: none are '
'drawn', (tester) async {
await show(
tester,
id: 'greek',
dictionaries: FakeDictionaryGateway(
available: [wiktionaryEl, greekAlphabet],
words: [
character(
'Α α',
labels: ['greek'],
romaji: 'a',
name: 'άλφα (alpha)',
of: greekAlphabet,
),
],
),
);
await addMaterial(tester, 'From the dictionary');
await tester.tap(find.text('Alphabet'));
await settle(tester);
await tester.enterText(find.byType(TextField).hitTestable(), 'α');
await tester.pump(const Duration(milliseconds: 300));
await tester.pumpAndSettle();
await tester.tap(find.text('Α α').hitTestable());
await tester.pumpAndSettle();
expect(
find.text("There's no stroke order for this character."),
findsNothing,
);
expect(find.text('Play again'), findsNothing);
});
testWidgets('Look up fills a character card with its meanings, readings '
'and tags', (tester) async {
await show(tester, dictionaries: withKanji());
await addMaterial(tester, 'Create flashcards');
await tester.tap(find.text('Character'));
await settle(tester);
await tester.tap(find.byTooltip('Look up in the dictionary'));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField).last, 'eat');
await tester.pump(const Duration(milliseconds: 300));
await tester.pumpAndSettle();
await tester.tap(find.text('eat; food'));
await settle(tester);
expect(find.widgetWithText(TextField, 'eat'), findsOneWidget);
expect(find.widgetWithText(TextField, 'ショク'), findsOneWidget);
expect(find.widgetWithText(TextField, 'た.べる'), findsOneWidget);
expect(find.text('JLPT-N5'), findsOneWidget);
expect(find.text('Grade 2'), findsOneWidget);
});
testWidgets('Look up fills a vocabulary card with a word of the '
'dictionary', (tester) async {
await show(tester);
await addMaterial(tester, 'Create flashcards');
await tester.tap(find.text('Vocabulary'));
await settle(tester);
await tester.tap(find.byTooltip('Look up in the dictionary'));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField).last, 'たべる');
await tester.pump(const Duration(milliseconds: 300));
await tester.pumpAndSettle();
await tester.tap(find.textContaining('食べる').last);
await settle(tester);
expect(find.text('食べる', findRichText: true), findsWidgets);
expect(find.widgetWithText(TextField, 'たべる'), findsOneWidget);
expect(find.text('to eat'), findsWidgets);
// Tagged as words from the dictionary screen are.
expect(find.text('JLPT-N5'), findsOneWidget);
expect(find.text('Verb'), findsOneWidget);
});
group('Radicals', () {
final hito = character(
'人',
meanings: ['person'],
labels: ['jlpt:n5', 'grade:1', 'radical:9'],
);
final yasumu = character(
'休',
meanings: ['rest'],
labels: ['jlpt:n5', 'grade:1', 'radical:9'],
);
/// An instance whose KANJIDIC2 files 人 and 休 under the radical 人.
FakeDictionaryGateway withRadicals() => FakeDictionaryGateway(
available: [jmdict, kanjidic],
words: [taberu, hito, yasumu, shoku],
);
Future<void> openRadicals(
WidgetTester tester,
FakeDictionaryGateway dictionaries,
) async {
await show(
tester,
dictionaries: dictionaries,
location: RoutePaths.dictionary('japanese'),
);
await tester.tap(find.text('Radicals'));
await settle(tester);
}
testWidgets('the radicals by their strokes; one lists the kanji written '
'with it, to add, and leads back to them all', (tester) async {
await openRadicals(tester, withRadicals());
expect(find.text('1 stroke'), findsOneWidget);
expect(find.text('2 strokes'), findsOneWidget);
await tester.tap(find.text('9'));
await settle(tester);
expect(find.text('man'), findsOneWidget);
expect(find.text('Radical 9 · 2 strokes · 2 kanji'), findsOneWidget);
expect(find.text('食'), findsNothing);
await tester.tap(find.text('Add').first);
await tester.pumpAndSettle();
expect(find.text('Added “人”.'), findsOneWidget);
expect(
(await cards.cardsOf('japanese')).single.content.entry,
(source: 'kanjidic', id: '人'),
);
await tester.tap(find.byTooltip('All radicals'));
await settle(tester);
expect(find.text('2 strokes'), findsOneWidget);
});
testWidgets("a radical's kanji the instance can't give now are asked "
'for again', (tester) async {
final dictionaries = withRadicals();
await openRadicals(tester, dictionaries);
dictionaries.fail = Exception('offline');
await tester.tap(find.text('9'));
await settle(tester);
expect(
find.text("The dictionary can't be reached right now."),
findsOneWidget,
);
dictionaries.fail = null;
await tester.tap(find.text('Retry'));
await settle(tester);
expect(find.text('Radical 9 · 2 strokes · 2 kanji'), findsOneWidget);
});
testWidgets('a radical KANJIDIC2 files nothing under on this instance '
'says so', (tester) async {
await openRadicals(tester, withKanji());
await tester.tap(find.text('9'));
await settle(tester);
expect(
find.text(
"The instance's dictionary has no kanji under this radical "
'yet.',
),
findsOneWidget,
);
});
});
testWidgets("About names whose work the instance's dictionaries and the "
"app's own data are", (
tester,
) async {
await pumpApp(
tester,
testDependencies(dictionaryGateway: FakeDictionaryGateway()),
location: RoutePaths.settings,
);
await tester.scrollUntilVisible(
find.text('About Halkyon Learn'),
200,
scrollable: find.byType(Scrollable).first,
);
// Wholly in view: scrolling stops once any of it shows.
await tester.ensureVisible(find.text('About Halkyon Learn'));
await tester.pumpAndSettle();
await tester.tap(find.text('About Halkyon Learn'));
await tester.pumpAndSettle();
expect(find.text('Dictionaries on this instance'), findsOneWidget);
expect(
find.text(
'JMdict: Electronic Dictionary Research and Development Group '
'(EDRDG), CC-BY-SA-4.0',
),
findsOneWidget,
);
// The app's own data, KanjiVG's strokes, as its licence asks.
expect(find.text('Data in the app'), findsOneWidget);
expect(find.text('KanjiVG: Ulrich Apel, CC-BY-SA-3.0'), findsOneWidget);
expect(find.text('https://kanjivg.tagaini.net'), findsOneWidget);
});
}