feat(learn): spelling and grammar checked as people write (ADR 0024)
Where the instance's LanguageTool checks the language, the editors underline what it finds after a pause, a paragraph at a time, a tap offering its suggestions (GrammarHost, GrammarCheckController, GrammarOverlay; nothing written into the document). A card's front is checked in the studyset's front language, its back and answers in the back's, everything else in the language recognised. In a private studyset nothing is sent until the person agrees for it, asked once in a banner above the editor and kept in their settings (an agreement under privateAgreements, a refusal in grammarDeclined, settings schema 14), taken back in the studyset's privacy dialog. Settings > Writing turns it off on the device. The field's Check asks LanguageTool first where it checks the studyset's language, in a sheet offering to ask the assistant why; elsewhere it asks the assistant as before (amends ADR 0021, D54). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
f766a880ac
commit
d2eaef255e
11 changed files with 2986 additions and 32 deletions
|
|
@ -44,6 +44,15 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test('checks what is written as it is written until told not to (ADR '
|
||||
'0024)', () async {
|
||||
expect((await store.read()).checkGrammar, isTrue);
|
||||
|
||||
await store.write(const DeviceSettings(checkGrammar: false));
|
||||
|
||||
expect((await FileDeviceSettingsStore(file).read()).checkGrammar, isFalse);
|
||||
});
|
||||
|
||||
test('shows the Pomodoro timer while studying until told not to', () async {
|
||||
expect((await store.read()).pomodoroWhileStudying, isTrue);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,255 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:fake_async/fake_async.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/grammar/grammar_check_controller.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/grammar/grammar_gateway.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/grammar/grammar_match.dart';
|
||||
|
||||
import '../../support/fake_grammar_gateway.dart';
|
||||
|
||||
const _typo = GrammarMatch(
|
||||
offset: 8,
|
||||
length: 8,
|
||||
message: 'Possible spelling mistake found.',
|
||||
shortMessage: 'Spelling mistake',
|
||||
replacements: ['sentence', 'sentences'],
|
||||
);
|
||||
|
||||
EditorState _editor(List<String> paragraphs) => EditorState(
|
||||
document: Document(
|
||||
root: pageNode(
|
||||
children: [for (final text in paragraphs) paragraphNode(text: text)],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
/// Types [text] at the end of the paragraph at [path].
|
||||
void _type(EditorState editor, String text, {Path path = const [0]}) {
|
||||
final node = editor.getNodeAtPath(path)!;
|
||||
unawaited(
|
||||
editor.apply(
|
||||
editor.transaction..insertText(node, node.delta!.length, text),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
late FakeGrammarGateway gateway;
|
||||
|
||||
setUp(
|
||||
() => gateway = FakeGrammarGateway(
|
||||
found: {
|
||||
'A small sentance.': [_typo],
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
test('checks once the writer pauses, never at each key, and underlines '
|
||||
'what is found where it is', () {
|
||||
fakeAsync((async) {
|
||||
final editor = _editor(['A small sentanc']);
|
||||
final controller = GrammarCheckController(
|
||||
editor,
|
||||
gateway,
|
||||
language: 'en',
|
||||
);
|
||||
|
||||
_type(editor, 'e');
|
||||
async.elapse(const Duration(milliseconds: 500));
|
||||
_type(editor, '.');
|
||||
async.elapse(const Duration(milliseconds: 1400));
|
||||
expect(gateway.checked, isEmpty, reason: 'not paused long enough');
|
||||
|
||||
async.elapse(const Duration(milliseconds: 200));
|
||||
|
||||
expect(gateway.checked, [('A small sentance.', 'en')]);
|
||||
expect(controller.status, GrammarCheckStatus.issues);
|
||||
final span = controller.spans.single;
|
||||
expect(span.match, _typo);
|
||||
expect(
|
||||
span.selection,
|
||||
Selection.single(path: [0], startOffset: 8, endOffset: 16),
|
||||
);
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('an edit drops what was found at once; moving the caret does not', () {
|
||||
fakeAsync((async) {
|
||||
final editor = _editor(['A small sentance.']);
|
||||
final controller = GrammarCheckController(editor, gateway);
|
||||
unawaited(controller.checkNow());
|
||||
async.flushMicrotasks();
|
||||
expect(controller.spans, hasLength(1));
|
||||
|
||||
// As a tap moves it: a transaction without operations.
|
||||
unawaited(
|
||||
editor.apply(
|
||||
editor.transaction
|
||||
..afterSelection = Selection.collapsed(
|
||||
Position(path: [0], offset: 2),
|
||||
),
|
||||
),
|
||||
);
|
||||
async.flushMicrotasks();
|
||||
expect(controller.spans, hasLength(1));
|
||||
|
||||
_type(editor, ' More');
|
||||
async.flushMicrotasks();
|
||||
expect(controller.spans, isEmpty);
|
||||
expect(controller.status, GrammarCheckStatus.idle);
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('checks again only the paragraphs whose text changed, and two '
|
||||
'alike once', () {
|
||||
fakeAsync((async) {
|
||||
final editor = _editor(['First one.', 'Second one.', 'First one.']);
|
||||
final controller = GrammarCheckController(editor, gateway);
|
||||
unawaited(controller.checkNow());
|
||||
async.flushMicrotasks();
|
||||
expect(gateway.checked.map((each) => each.$1), [
|
||||
'First one.',
|
||||
'Second one.',
|
||||
]);
|
||||
expect(controller.status, GrammarCheckStatus.clean);
|
||||
|
||||
gateway.checked.clear();
|
||||
_type(editor, ' Two.', path: [1]);
|
||||
async.elapse(const Duration(seconds: 2));
|
||||
|
||||
expect(gateway.checked.map((each) => each.$1), ['Second one. Two.']);
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('checks as the editor loses the focus, without waiting for the '
|
||||
'pause', () {
|
||||
fakeAsync((async) {
|
||||
final editor = _editor(['A small sentance']);
|
||||
final focus = _Focus(focused: true);
|
||||
final controller = GrammarCheckController(
|
||||
editor,
|
||||
gateway,
|
||||
focusNode: focus,
|
||||
);
|
||||
|
||||
_type(editor, '.');
|
||||
async.flushMicrotasks();
|
||||
focus.leave();
|
||||
async.flushMicrotasks();
|
||||
|
||||
expect(gateway.checked, [('A small sentance.', null)]);
|
||||
expect(controller.spans, hasLength(1));
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('drops an answer about text edited while it was asked, and checks '
|
||||
'the text as it is now once more after it', () {
|
||||
fakeAsync((async) {
|
||||
final slow = _SlowGateway();
|
||||
final editor = _editor(['A small sentance.']);
|
||||
final controller = GrammarCheckController(editor, slow);
|
||||
unawaited(controller.checkNow());
|
||||
async.flushMicrotasks();
|
||||
expect(slow.asked, ['A small sentance.']);
|
||||
|
||||
_type(editor, ' Yes.');
|
||||
async.flushMicrotasks();
|
||||
unawaited(controller.checkNow());
|
||||
slow.answer([_typo]);
|
||||
async.flushMicrotasks();
|
||||
|
||||
expect(controller.spans, isEmpty, reason: 'the answer was dropped');
|
||||
expect(slow.asked, ['A small sentance.', 'A small sentance. Yes.']);
|
||||
slow.answer(const []);
|
||||
async.flushMicrotasks();
|
||||
expect(controller.status, GrammarCheckStatus.clean);
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('says a check failed rather than clean, keeps no failure, and stops '
|
||||
"for good at a language the instance doesn't check", () {
|
||||
fakeAsync((async) {
|
||||
final editor = _editor(['One.']);
|
||||
gateway.refusal = GrammarRefusalReason.tooOften;
|
||||
final controller = GrammarCheckController(editor, gateway);
|
||||
unawaited(controller.checkNow());
|
||||
async.flushMicrotasks();
|
||||
expect(controller.status, GrammarCheckStatus.failed);
|
||||
|
||||
gateway.refusal = null;
|
||||
unawaited(controller.checkNow());
|
||||
async.flushMicrotasks();
|
||||
expect(controller.status, GrammarCheckStatus.clean);
|
||||
expect(gateway.checked, hasLength(2), reason: 'asked again');
|
||||
|
||||
gateway.refusal = GrammarRefusalReason.unknownLanguage;
|
||||
_type(editor, ' Two.');
|
||||
async.elapse(const Duration(seconds: 2));
|
||||
gateway.refusal = null;
|
||||
_type(editor, ' Three.');
|
||||
async.elapse(const Duration(seconds: 2));
|
||||
|
||||
expect(gateway.checked, hasLength(3), reason: 'stopped');
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('puts a suggestion where its words are, as one edit', () {
|
||||
fakeAsync((async) {
|
||||
final editor = _editor(['A small sentance.']);
|
||||
final controller = GrammarCheckController(editor, gateway);
|
||||
unawaited(controller.checkNow());
|
||||
async.flushMicrotasks();
|
||||
|
||||
unawaited(
|
||||
controller.applyReplacement(controller.spans.single, 'sentence'),
|
||||
);
|
||||
async.flushMicrotasks();
|
||||
|
||||
expect(
|
||||
editor.getNodeAtPath([0])!.delta!.toPlainText(),
|
||||
'A small sentence.',
|
||||
);
|
||||
expect(controller.spans, isEmpty);
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// A focus that is left when told to.
|
||||
class _Focus({required final bool focused}) extends FocusNode {
|
||||
bool _has = focused;
|
||||
|
||||
@override
|
||||
bool get hasFocus => _has;
|
||||
|
||||
void leave() {
|
||||
_has = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Answers each check only when told to.
|
||||
class _SlowGateway() implements GrammarGateway {
|
||||
final asked = <String>[];
|
||||
final _waiting = <Completer<List<GrammarMatch>>>[];
|
||||
|
||||
void answer(List<GrammarMatch> matches) =>
|
||||
_waiting.removeAt(0).complete(matches);
|
||||
|
||||
@override
|
||||
Future<List<GrammarMatch>> check(String text, {String? language}) {
|
||||
asked.add(text);
|
||||
final answer = Completer<List<GrammarMatch>>();
|
||||
_waiting.add(answer);
|
||||
return answer.future;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/grammar/grammar_checks.dart';
|
||||
|
||||
import '../../support/fake_grammar_gateway.dart';
|
||||
|
||||
void main() {
|
||||
test('checks nothing before the instance said which languages it '
|
||||
'checks, nor where it checks none', () async {
|
||||
final none = GrammarChecks(FakeGrammarGateway(), () async => null);
|
||||
expect(none.checks('de'), isFalse);
|
||||
expect(none.languages.value, isNull, reason: 'not asked yet');
|
||||
|
||||
await none.look();
|
||||
|
||||
expect(none.languages.value, isEmpty);
|
||||
expect(none.checks('de'), isFalse);
|
||||
expect(none.checks(null), isFalse, reason: 'recognised: none either');
|
||||
});
|
||||
|
||||
test('checks a language by its own subtag, any region or script, and '
|
||||
'recognised text where it checks any', () async {
|
||||
final checks = GrammarChecks(
|
||||
FakeGrammarGateway(),
|
||||
() async => ['de', 'en'],
|
||||
);
|
||||
await checks.look();
|
||||
|
||||
expect(checks.checks('de'), isTrue);
|
||||
expect(checks.checks('de-AT'), isTrue);
|
||||
expect(checks.checks('en_GB'), isTrue);
|
||||
expect(checks.checks('ko'), isFalse);
|
||||
expect(checks.checks(null), isTrue);
|
||||
expect(checks.checks(' '), isTrue);
|
||||
});
|
||||
|
||||
test('asks once, and again after a failure', () async {
|
||||
var asked = 0;
|
||||
final checks = GrammarChecks(FakeGrammarGateway(), () async {
|
||||
asked++;
|
||||
if (asked == 1) {
|
||||
throw StateError('offline');
|
||||
}
|
||||
return ['de'];
|
||||
});
|
||||
|
||||
await checks.look();
|
||||
expect(checks.languages.value, isNull);
|
||||
await checks.look();
|
||||
await checks.look();
|
||||
|
||||
expect(asked, 2);
|
||||
expect(checks.languages.value, {'de'});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
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/device_settings/model/device_settings.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/grammar/grammar_match.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/grammar/grammar_overlay.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.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/settings/data/settings_payload.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/settings/model/person_settings.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_grammar_gateway.dart';
|
||||
import '../../support/fake_note_gateway.dart';
|
||||
import '../../support/fake_settings_gateway.dart';
|
||||
import '../../support/fake_studyset_gateway.dart';
|
||||
import '../../support/pump_app.dart';
|
||||
import '../../support/test_dependencies.dart';
|
||||
|
||||
// The editors check spelling and grammar as they are written (ADR 0024),
|
||||
// here a note's: underlined where the instance's LanguageTool finds
|
||||
// something, a tap offering its suggestions; nothing where the device or
|
||||
// the instance checks nothing; in a private studyset only once agreed to,
|
||||
// asked once in a banner above the editor.
|
||||
|
||||
const _text = 'A small sentance.';
|
||||
|
||||
const _typo = GrammarMatch(
|
||||
offset: 8,
|
||||
length: 8,
|
||||
message: 'Possible spelling mistake found.',
|
||||
shortMessage: 'Spelling mistake',
|
||||
replacements: ['sentence', 'sentences'],
|
||||
);
|
||||
|
||||
/// Rich content with one paragraph of [text].
|
||||
RichContent _paragraph(String text) => RichContent({
|
||||
'document': {
|
||||
'type': 'page',
|
||||
'children': [
|
||||
{
|
||||
'type': 'paragraph',
|
||||
'data': {
|
||||
'delta': [
|
||||
{'insert': text},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
void main() {
|
||||
late FakeStudysetGateway studysets;
|
||||
late FakeNoteGateway notes;
|
||||
late FakeSettingsGateway settings;
|
||||
late FakeGrammarGateway grammar;
|
||||
|
||||
setUp(() {
|
||||
studysets = FakeStudysetGateway()
|
||||
..receiveRemoteChange(
|
||||
'japanese',
|
||||
encodeStudysetPayload(StudysetContent.titled('Japanese')),
|
||||
)
|
||||
..receiveRemoteChange(
|
||||
'diary',
|
||||
encodeStudysetPayload(StudysetContent.titled('Diary')),
|
||||
position: 'a1',
|
||||
isPrivate: true,
|
||||
)
|
||||
..receiveRemoteChange(
|
||||
'week',
|
||||
encodeStudysetPayload(StudysetContent.titled('This week')),
|
||||
parentId: 'diary',
|
||||
);
|
||||
notes = FakeNoteGateway();
|
||||
settings = FakeSettingsGateway();
|
||||
grammar = FakeGrammarGateway(
|
||||
found: {
|
||||
_text: [_typo],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
/// A note of [_text] in [studysetId], stored before the app starts; its
|
||||
/// id.
|
||||
Future<String> note(String studysetId) async {
|
||||
final repository = NoteRepository(notes);
|
||||
addTearDown(repository.dispose);
|
||||
final created = await repository.create(
|
||||
studysetId,
|
||||
NoteContent(title: 'Today', body: _paragraph(_text)),
|
||||
);
|
||||
return created.noteId;
|
||||
}
|
||||
|
||||
Future<void> showNote(
|
||||
WidgetTester tester,
|
||||
String studysetId,
|
||||
String noteId, {
|
||||
List<String>? languages = const ['en', 'de'],
|
||||
bool checkGrammar = true,
|
||||
}) async {
|
||||
tester.view.physicalSize = const Size(1200, 1000);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
await pumpApp(
|
||||
tester,
|
||||
testDependencies(
|
||||
studysetGateway: studysets,
|
||||
noteGateway: notes,
|
||||
settingsGateway: settings,
|
||||
grammarGateway: grammar,
|
||||
grammarLanguages: languages,
|
||||
deviceSettings: DeviceSettings(checkGrammar: checkGrammar),
|
||||
),
|
||||
location: RoutePaths.note(studysetId, noteId),
|
||||
);
|
||||
// The editor's timers, and the check after its first frame.
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
Finder squiggles() => find.descendant(
|
||||
of: find.byType(GrammarOverlay),
|
||||
matching: find.byType(GestureDetector),
|
||||
);
|
||||
|
||||
PersonSettings lastSettings() =>
|
||||
decodeSettingsPayload(settings.writes.last.$2);
|
||||
|
||||
testWidgets('underlines what the instance finds as a note opens; a tap '
|
||||
'offers its suggestions, and the one chosen replaces the words', (
|
||||
tester,
|
||||
) async {
|
||||
final id = await note('japanese');
|
||||
await showNote(tester, 'japanese', id);
|
||||
|
||||
expect(grammar.checked, [(_text, null)], reason: 'a note: recognised');
|
||||
expect(squiggles(), findsOneWidget);
|
||||
|
||||
await tester.tap(squiggles());
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Spelling mistake'), findsOneWidget);
|
||||
expect(find.text('sentences'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('sentence'));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('A small sentence.', findRichText: true), findsOneWidget);
|
||||
expect(squiggles(), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('says a check found nothing with a muted check in the '
|
||||
'corner', (tester) async {
|
||||
grammar = FakeGrammarGateway();
|
||||
final id = await note('japanese');
|
||||
await showNote(tester, 'japanese', id);
|
||||
|
||||
expect(grammar.checked, hasLength(1));
|
||||
expect(
|
||||
find.bySemanticsLabel('No spelling or grammar mistakes found'),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets("checks nothing where the device's setting is off, nor where "
|
||||
'the instance checks no language', (tester) async {
|
||||
final id = await note('japanese');
|
||||
await showNote(tester, 'japanese', id, checkGrammar: false);
|
||||
expect(grammar.checked, isEmpty);
|
||||
expect(find.byType(GrammarOverlay), findsNothing);
|
||||
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
await showNote(tester, 'japanese', id, languages: null);
|
||||
expect(grammar.checked, isEmpty);
|
||||
expect(find.byType(GrammarOverlay), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('in a private studyset sends nothing until asked, asks once '
|
||||
'the editor has the focus, and checks once agreed, the agreement kept '
|
||||
'for the studyset', (tester) async {
|
||||
final id = await note('diary');
|
||||
await showNote(tester, 'diary', id);
|
||||
|
||||
expect(grammar.checked, isEmpty);
|
||||
expect(find.text('Check here'), findsNothing, reason: 'not writing yet');
|
||||
|
||||
await tester.tap(find.text(_text, findRichText: true));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.textContaining('goes to learn.example.org readable'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(grammar.checked, isEmpty);
|
||||
|
||||
await tester.tap(find.text('Check here'));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(lastSettings().grammarAnswerOf('diary'), isTrue);
|
||||
expect(find.text('Check here'), findsNothing);
|
||||
expect(grammar.checked, [(_text, null)]);
|
||||
expect(squiggles(), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('in a private studyset, Not here sends nothing and asks no '
|
||||
'more', (tester) async {
|
||||
final id = await note('diary');
|
||||
await showNote(tester, 'diary', id);
|
||||
await tester.tap(find.text(_text, findRichText: true));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Not here'));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(lastSettings().grammarAnswerOf('diary'), isFalse);
|
||||
expect(find.text('Check here'), findsNothing);
|
||||
expect(grammar.checked, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets("a topic of a private studyset follows the studyset's answer, "
|
||||
'unasked', (tester) async {
|
||||
settings.receiveRemoteRow(
|
||||
'settings-agreed',
|
||||
encodeSettingsPayload(
|
||||
const PersonSettings().withGrammarAnswer('diary', check: true),
|
||||
),
|
||||
);
|
||||
final id = await note('week');
|
||||
await showNote(tester, 'week', id);
|
||||
|
||||
expect(grammar.checked, [(_text, null)]);
|
||||
expect(find.text('Check here'), findsNothing);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/content_blocks.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/grammar/grammar_segments.dart';
|
||||
|
||||
void main() {
|
||||
test("are the document's paragraphs with text, nested ones too, each at "
|
||||
'its path; blank ones are left out', () {
|
||||
final document = Document(
|
||||
root: pageNode(
|
||||
children: [
|
||||
paragraphNode(text: 'Ein Fehler.'),
|
||||
paragraphNode(text: ' '),
|
||||
bulletedListNode(
|
||||
text: 'An item',
|
||||
children: [paragraphNode(text: 'Below it')],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
expect(grammarSegmentsOf(document), const [
|
||||
GrammarSegment(path: [0], text: 'Ein Fehler.'),
|
||||
GrammarSegment(path: [2], text: 'An item'),
|
||||
GrammarSegment(path: [2, 0], text: 'Below it'),
|
||||
]);
|
||||
});
|
||||
|
||||
test('mask formulas and code as spaces of their length, so offsets stay '
|
||||
"the node's, and what touches them is the masking's", () {
|
||||
final document = Document(
|
||||
root: pageNode(
|
||||
children: [
|
||||
paragraphNode(
|
||||
delta: Delta()
|
||||
..insert('Take ')
|
||||
..insert('x^2', attributes: {inlineMathKey: true})
|
||||
..insert(' and ')
|
||||
..insert('print()', attributes: {AppFlowyRichTextKeys.code: true})
|
||||
..insert(' now'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final segment = grammarSegmentsOf(document).single;
|
||||
|
||||
expect(segment.text, 'Take and now');
|
||||
expect(segment.text.length, 'Take x^2 and print() now'.length);
|
||||
expect(segment.masked, [(5, 8), (13, 20)]);
|
||||
expect(segment.touchesMask(4, 6), isTrue);
|
||||
expect(segment.touchesMask(12, 13), isFalse);
|
||||
expect(segment.touchesMask(0, 4), isFalse);
|
||||
expect(segment.touchesMask(19, 24), isTrue);
|
||||
});
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import 'package:halkyon_app/testing.dart';
|
|||
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/device_settings/model/device_settings.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/grammar/grammar_match.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/data/conversation_payload.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/data/conversation_repository.dart';
|
||||
|
|
@ -31,6 +32,7 @@ import '../../support/fake_asset_sealing.dart';
|
|||
import '../../support/fake_asset_server_gateway.dart';
|
||||
import '../../support/fake_card_gateway.dart';
|
||||
import '../../support/fake_conversation_gateway.dart';
|
||||
import '../../support/fake_grammar_gateway.dart';
|
||||
import '../../support/fake_settings_gateway.dart';
|
||||
import '../../support/fake_studyset_gateway.dart';
|
||||
import '../../support/memory_asset_file_cache.dart';
|
||||
|
|
@ -118,6 +120,7 @@ void main() {
|
|||
late List<Uri> searched;
|
||||
late FakeAssetGateway assetGateway;
|
||||
late MemoryAssetFileCache assetFiles;
|
||||
late FakeGrammarGateway grammar;
|
||||
|
||||
setUp(() {
|
||||
assistants = FakeAssistantGateway()
|
||||
|
|
@ -141,12 +144,29 @@ void main() {
|
|||
searched = [];
|
||||
assetGateway = FakeAssetGateway();
|
||||
assetFiles = MemoryAssetFileCache();
|
||||
grammar = FakeGrammarGateway(
|
||||
found: {
|
||||
'私はりんごは食べる': const [
|
||||
GrammarMatch(
|
||||
offset: 5,
|
||||
length: 1,
|
||||
message: 'Use を for the object.',
|
||||
shortMessage: 'Particle',
|
||||
replacements: ['を', 'も'],
|
||||
),
|
||||
],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
Future<void> showApp(
|
||||
WidgetTester tester, {
|
||||
bool web = false,
|
||||
String location = RoutePaths.home,
|
||||
|
||||
/// The languages the instance's LanguageTool checks (ADR 0024); none
|
||||
/// without.
|
||||
List<String>? grammarLanguages,
|
||||
}) async {
|
||||
tester.view.physicalSize = const Size(400, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
|
@ -173,6 +193,8 @@ void main() {
|
|||
),
|
||||
deviceSettings: initial,
|
||||
deviceSettingsStore: store,
|
||||
grammarGateway: grammar,
|
||||
grammarLanguages: grammarLanguages,
|
||||
),
|
||||
location: location,
|
||||
);
|
||||
|
|
@ -602,6 +624,55 @@ void main() {
|
|||
expect(lastSettings().privateAgreementsOf('diary'), {'assistant:gone'});
|
||||
});
|
||||
|
||||
testWidgets("the studyset's privacy says whether what is written in it is "
|
||||
'checked, and takes the answer back, so it is asked again (ADR 0024)', (
|
||||
tester,
|
||||
) async {
|
||||
settings.receiveRemoteRow(
|
||||
'settings-grammar',
|
||||
encodeSettingsPayload(
|
||||
const PersonSettings().withGrammarAnswer('diary', check: true),
|
||||
),
|
||||
);
|
||||
await showApp(tester, location: RoutePaths.library);
|
||||
|
||||
await tester.tap(find.byTooltip('Private'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Assistants it may go to'), findsNothing);
|
||||
expect(find.text('Spelling and grammar'), findsOneWidget);
|
||||
expect(
|
||||
find.text('Checked by learn.example.org as you write'),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Take back'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Spelling and grammar'), findsNothing);
|
||||
expect(lastSettings().grammarAnswerOf('diary'), isNull);
|
||||
});
|
||||
|
||||
testWidgets("the studyset's privacy says a declined check so, beside the "
|
||||
'assistants (ADR 0024)', (tester) async {
|
||||
settings.receiveRemoteRow(
|
||||
'settings-grammar',
|
||||
encodeSettingsPayload(
|
||||
const PersonSettings(
|
||||
privateAgreements: {
|
||||
'diary': {'assistant:claude'},
|
||||
},
|
||||
).withGrammarAnswer('diary', check: false),
|
||||
),
|
||||
);
|
||||
await showApp(tester, location: RoutePaths.library);
|
||||
|
||||
await tester.tap(find.byTooltip('Private'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Claude'), findsOneWidget);
|
||||
expect(find.text('Not checked as you write'), findsOneWidget);
|
||||
expect(find.text('Take back'), findsNWidgets(2));
|
||||
});
|
||||
|
||||
testWidgets('a follow-up sends the conversation again, so it asks again '
|
||||
'once the agreement was taken back (D17)', (tester) async {
|
||||
final conversations = ConversationRepository(kept);
|
||||
|
|
@ -825,6 +896,136 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
expect(find.byTooltip('Check for mistakes'), findsNothing);
|
||||
});
|
||||
|
||||
/// Writes [text] into the field and checks it.
|
||||
Future<void> check(WidgetTester tester, String text) async {
|
||||
await tester.enterText(
|
||||
find.descendant(
|
||||
of: find.byType(AssistantField),
|
||||
matching: find.byType(TextField),
|
||||
),
|
||||
text,
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.tap(find.byTooltip('Check for mistakes'));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
String fieldText(WidgetTester tester) => tester
|
||||
.widget<TextField>(
|
||||
find.descendant(
|
||||
of: find.byType(AssistantField),
|
||||
matching: find.byType(TextField),
|
||||
),
|
||||
)
|
||||
.controller!
|
||||
.text;
|
||||
|
||||
testWidgets("Check goes to the instance's LanguageTool first where it "
|
||||
"checks the studyset's language (ADR 0024): what it found, a "
|
||||
'suggestion taken into the field, and the assistant asked why only '
|
||||
'when asked, about the text as checked', (tester) async {
|
||||
await showApp(
|
||||
tester,
|
||||
location: RoutePaths.studyset('japanese'),
|
||||
grammarLanguages: ['ja', 'de'],
|
||||
);
|
||||
assistants.answers.add(_says('を marks the object.'));
|
||||
await unfold(tester);
|
||||
|
||||
await check(tester, '私はりんごは食べる');
|
||||
|
||||
expect(grammar.checked, [('私はりんごは食べる', 'ja')]);
|
||||
expect(assistants.asked, isEmpty);
|
||||
expect(find.text('Spelling and grammar'), findsOneWidget);
|
||||
expect(find.text('Particle'), findsOneWidget);
|
||||
expect(find.text('Use を for the object.'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.widgetWithText(ActionChip, 'を'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(fieldText(tester), '私はりんごを食べる');
|
||||
expect(find.text('Particle'), findsNothing, reason: 'taken');
|
||||
expect(
|
||||
find.text('Ask Claude why'),
|
||||
findsNothing,
|
||||
reason: 'nothing left to explain',
|
||||
);
|
||||
|
||||
// The sheet's, over the field's own.
|
||||
await tester.tap(find.byTooltip('Close').last);
|
||||
await tester.pumpAndSettle();
|
||||
grammar.checked.clear();
|
||||
await check(tester, '私はりんごは食べる');
|
||||
await tester.tap(find.text('Ask Claude why'));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final (_, request) = assistants.asked.single;
|
||||
expect(request.system, contains('check what they wrote'));
|
||||
expect(_asked(request), '私はりんごは食べる');
|
||||
expect(
|
||||
find.text('を marks the object.', findRichText: true),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('where the instance checks the language, Check is there for '
|
||||
'the web search too, with no assistant to ask', (tester) async {
|
||||
assistants.offer = AssistantsOffer.none;
|
||||
await showApp(
|
||||
tester,
|
||||
web: true,
|
||||
location: RoutePaths.studyset('japanese'),
|
||||
grammarLanguages: ['ja'],
|
||||
);
|
||||
await unfold(tester, tooltip: 'Search the web');
|
||||
|
||||
await check(tester, '私はりんごは食べる');
|
||||
|
||||
expect(grammar.checked, hasLength(1));
|
||||
expect(find.text('Particle'), findsOneWidget);
|
||||
expect(find.textContaining('Ask '), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('where the instance checks other languages only, Check goes '
|
||||
'to the assistant as before', (tester) async {
|
||||
await showApp(
|
||||
tester,
|
||||
location: RoutePaths.studyset('japanese'),
|
||||
grammarLanguages: ['de'],
|
||||
);
|
||||
assistants.answers.add(_says('One mistake.'));
|
||||
await unfold(tester);
|
||||
|
||||
await check(tester, '私はりんごは食べる');
|
||||
|
||||
expect(grammar.checked, isEmpty);
|
||||
expect(assistants.asked, hasLength(1));
|
||||
});
|
||||
|
||||
testWidgets("where the instance's check fails, the sheet says so and "
|
||||
'offers to have the assistant check instead', (tester) async {
|
||||
grammar.refusal = GrammarRefusalReason.tooOften;
|
||||
await showApp(
|
||||
tester,
|
||||
location: RoutePaths.studyset('japanese'),
|
||||
grammarLanguages: ['ja'],
|
||||
);
|
||||
assistants.answers.add(_says('One mistake.'));
|
||||
await unfold(tester);
|
||||
|
||||
await check(tester, '私はりんごは食べる');
|
||||
|
||||
expect(
|
||||
find.text('Checked too often for now. Try again in a minute.'),
|
||||
findsOneWidget,
|
||||
);
|
||||
await tester.tap(find.text('Ask Claude to check'));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
await tester.pumpAndSettle();
|
||||
expect(_asked(assistants.asked.single.$2), '私はりんごは食べる');
|
||||
});
|
||||
});
|
||||
group('images (D12)', () {
|
||||
/// Writes a card into Verbs with a picture of a bowl on its front, as
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import 'package:halkyon_ui/halkyon_ui.dart';
|
|||
import 'package:material_ui/material_ui.dart' hide Card;
|
||||
|
||||
import '../../../support/fake_card_gateway.dart';
|
||||
import '../../../support/fake_grammar_gateway.dart';
|
||||
import '../../../support/fake_studyset_gateway.dart';
|
||||
import '../../../support/loaded_maps.dart';
|
||||
import '../../../support/pump_app.dart';
|
||||
|
|
@ -90,6 +91,9 @@ void main() {
|
|||
GeoMapRepository? maps,
|
||||
Size size = const Size(1400, 1600),
|
||||
String? frontLanguage,
|
||||
String? backLanguage,
|
||||
FakeGrammarGateway? grammar,
|
||||
List<String>? grammarLanguages,
|
||||
}) async {
|
||||
tester.view.physicalSize = size;
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
|
@ -102,6 +106,7 @@ void main() {
|
|||
title: 'Japanese',
|
||||
description: RichContent.empty(),
|
||||
frontLanguage: frontLanguage,
|
||||
backLanguage: backLanguage,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -111,6 +116,8 @@ void main() {
|
|||
studysetGateway: studysets,
|
||||
cardGateway: gateway,
|
||||
maps: maps,
|
||||
grammarGateway: grammar,
|
||||
grammarLanguages: grammarLanguages,
|
||||
),
|
||||
location: RoutePaths.studyset('japanese'),
|
||||
);
|
||||
|
|
@ -162,6 +169,30 @@ void main() {
|
|||
expect(gateway.written, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets("a card's front is checked as it is written in the "
|
||||
"studyset's front language, its back in the back's (ADR 0024)", (
|
||||
tester,
|
||||
) async {
|
||||
final grammar = FakeGrammarGateway();
|
||||
await cards.create('japanese', card('猫', 'cat'));
|
||||
await showStudyset(
|
||||
tester,
|
||||
frontLanguage: 'ja',
|
||||
backLanguage: 'en-GB',
|
||||
grammar: grammar,
|
||||
grammarLanguages: ['ja', 'en'],
|
||||
);
|
||||
await open(tester, '猫');
|
||||
|
||||
await tester.tap(inField('猫'));
|
||||
await settle(tester);
|
||||
expect(grammar.checked, [('猫', 'ja')]);
|
||||
|
||||
await tester.tap(inField('cat'));
|
||||
await settle(tester);
|
||||
expect(grammar.checked.last, ('cat', 'en-GB'));
|
||||
});
|
||||
|
||||
testWidgets('a card opens in the composer among the others; an edit is '
|
||||
'saved on leaving it', (tester) async {
|
||||
await cards.create('japanese', card('猫', 'cat'));
|
||||
|
|
|
|||
|
|
@ -692,4 +692,82 @@ void main() {
|
|||
expect(both.foldedWith(both), both);
|
||||
});
|
||||
});
|
||||
|
||||
group('checking what is written in a private studyset (ADR 0024)', () {
|
||||
test('is asked until answered; agreed, it is an agreement as an '
|
||||
"assistant's, declined it is declined, and taken back it is asked "
|
||||
'again', () {
|
||||
const settings = PersonSettings();
|
||||
expect(settings.grammarAnswerOf('ja'), isNull);
|
||||
|
||||
final agreed = settings.withGrammarAnswer('ja', check: true);
|
||||
expect(agreed.grammarAnswerOf('ja'), isTrue);
|
||||
expect(agreed.agreedToSend('ja', grammarAgreementKey), isTrue);
|
||||
expect(agreed.grammarAnswerOf('ko'), isNull);
|
||||
|
||||
final declined = agreed.withGrammarAnswer('ja', check: false);
|
||||
expect(declined.grammarAnswerOf('ja'), isFalse);
|
||||
expect(
|
||||
declined.agreedToSend('ja', grammarAgreementKey),
|
||||
isFalse,
|
||||
reason: 'one answer at a time',
|
||||
);
|
||||
|
||||
expect(declined.withoutGrammarAnswer('ja').grammarAnswerOf('ja'), isNull);
|
||||
expect(agreed.withoutGrammarAnswer('ja').grammarAnswerOf('ja'), isNull);
|
||||
});
|
||||
|
||||
test('survives a round trip, in the fourteenth schema, the declined '
|
||||
'left out while there are none; a payload from before declined '
|
||||
'none', () {
|
||||
final settings = const PersonSettings()
|
||||
.withGrammarAnswer('ja', check: true)
|
||||
.withGrammarAnswer('ko', check: false)
|
||||
.withGrammarAnswer('de', check: false);
|
||||
|
||||
expect(settingsSchemaVersion, greaterThanOrEqualTo(14));
|
||||
final read = decodeSettingsPayload(encodeSettingsPayload(settings));
|
||||
expect(read, settings);
|
||||
expect(read.grammarAnswerOf('ja'), isTrue);
|
||||
expect(read.grammarAnswerOf('ko'), isFalse);
|
||||
expect(
|
||||
decodePayload(
|
||||
encodeSettingsPayload(const PersonSettings()),
|
||||
newestSchema: settingsSchemaVersion,
|
||||
).keys,
|
||||
isNot(contains('grammarDeclined')),
|
||||
);
|
||||
expect(
|
||||
decodeSettingsPayload(
|
||||
encodePayload(13, {'studyTags': <String, Object?>{}}),
|
||||
).grammarDeclined,
|
||||
isEmpty,
|
||||
);
|
||||
});
|
||||
|
||||
test("is checked, as two devices' settings fold, only where both "
|
||||
'agreed, and declined where either declined: an older row never '
|
||||
'sends what was declined since', () {
|
||||
final agreed = const PersonSettings().withGrammarAnswer(
|
||||
'ja',
|
||||
check: true,
|
||||
);
|
||||
final declined = const PersonSettings().withGrammarAnswer(
|
||||
'ja',
|
||||
check: false,
|
||||
);
|
||||
|
||||
for (final folded in [
|
||||
agreed.foldedWith(declined),
|
||||
declined.foldedWith(agreed),
|
||||
]) {
|
||||
expect(folded.grammarAnswerOf('ja'), isFalse);
|
||||
}
|
||||
expect(
|
||||
agreed.foldedWith(const PersonSettings()).grammarAnswerOf('ja'),
|
||||
isNull,
|
||||
reason: 'a row not agreeing asks again',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -261,6 +261,53 @@ void main() {
|
|||
expect(store.writes.last.pomodoroWhileStudying, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('the Writing card turns off checking what is written (ADR '
|
||||
'0024), and says where the instance checks nothing yet', (tester) async {
|
||||
Future<void> show(
|
||||
List<String>? languages,
|
||||
MemoryDeviceSettingsStore store,
|
||||
) => pumpApp(
|
||||
tester,
|
||||
testDependencies(
|
||||
deviceSettingsStore: store,
|
||||
grammarLanguages: languages,
|
||||
),
|
||||
location: RoutePaths.settings,
|
||||
);
|
||||
const title = 'Check grammar while writing';
|
||||
const none =
|
||||
'This instance has no spelling and grammar check set up yet, so '
|
||||
'nothing is underlined. Left on, checking starts once it has one.';
|
||||
|
||||
final store = MemoryDeviceSettingsStore();
|
||||
await show(['en', 'de'], store);
|
||||
final toggle = find.widgetWithText(SwitchListTile, title);
|
||||
await tester.scrollUntilVisible(
|
||||
toggle,
|
||||
200,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Writing'), findsOneWidget);
|
||||
expect(tester.widget<SwitchListTile>(toggle).value, isTrue);
|
||||
expect(find.text(none), findsNothing);
|
||||
|
||||
await tester.tap(
|
||||
find.descendant(of: toggle, matching: find.byType(Switch)),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(store.writes.last.checkGrammar, isFalse);
|
||||
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
await show(null, MemoryDeviceSettingsStore());
|
||||
await tester.scrollUntilVisible(
|
||||
find.text(none),
|
||||
200,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
expect(find.text(none), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('starts with the appearance the device kept', (tester) async {
|
||||
tester.platformDispatcher.platformBrightnessTestValue = Brightness.dark;
|
||||
addTearDown(tester.platformDispatcher.clearPlatformBrightnessTestValue);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
import 'package:halkyon_learn_flutter/src/core/grammar/grammar_gateway.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/grammar/grammar_match.dart';
|
||||
|
||||
/// [GrammarGateway] answering from [found], by the text checked: nothing
|
||||
/// for a text it doesn't hold. Remembers what was checked, in which
|
||||
/// language; [refusal] refuses every check.
|
||||
class FakeGrammarGateway({
|
||||
final Map<String, List<GrammarMatch>> found = const {},
|
||||
}) implements GrammarGateway {
|
||||
/// The texts checked, in order, with the language asked for.
|
||||
final checked = <(String, String?)>[];
|
||||
|
||||
/// Why every check is refused; null: none is.
|
||||
GrammarRefusalReason? refusal;
|
||||
|
||||
@override
|
||||
Future<List<GrammarMatch>> check(String text, {String? language}) async {
|
||||
checked.add((text, language));
|
||||
if (refusal case final reason?) {
|
||||
throw GrammarRefused(reason);
|
||||
}
|
||||
return found[text] ?? const [];
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,8 @@ import 'package:halkyon_learn_flutter/src/core/dependencies/person_dependencies.
|
|||
import 'package:halkyon_learn_flutter/src/core/device_settings/data/device_settings_store.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/device_settings/model/device_settings.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/files/file_picker.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/grammar/grammar_checks.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/grammar/grammar_gateway.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/version/app_version.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/video/media_players.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/video/youtube_videos.dart';
|
||||
|
|
@ -90,6 +92,7 @@ import 'fake_explore_gateway.dart';
|
|||
import 'fake_feedback_gateways.dart';
|
||||
import 'fake_file_picker.dart';
|
||||
import 'fake_file_saver.dart';
|
||||
import 'fake_grammar_gateway.dart';
|
||||
import 'fake_jellyfin.dart';
|
||||
import 'fake_learning_path_gateway.dart';
|
||||
import 'fake_note_gateway.dart';
|
||||
|
|
@ -188,6 +191,9 @@ Dependencies testDependencies({
|
|||
AssistantOperatorGateway? assistantOperatorGateway,
|
||||
// A person's own key's provider; none answers unless scripted.
|
||||
FakeLlmClient? llm,
|
||||
// The instance checks no writing unless a test names its languages.
|
||||
GrammarGateway? grammarGateway,
|
||||
List<String>? grammarLanguages,
|
||||
}) {
|
||||
final operators = operatorGateway ?? FakeInstanceOperatorGateway();
|
||||
final device = deviceDictionaries ?? noDeviceDictionaries();
|
||||
|
|
@ -408,6 +414,10 @@ Dependencies testDependencies({
|
|||
discussions: DiscussionRepository(
|
||||
discussionGateway ?? FakeDiscussionGateway(),
|
||||
),
|
||||
grammar: GrammarChecks(
|
||||
grammarGateway ?? FakeGrammarGateway(),
|
||||
() async => grammarLanguages,
|
||||
),
|
||||
onDispose: () async {
|
||||
await deletions.dispose();
|
||||
await noteDeletions.dispose();
|
||||
|
|
|
|||
Loading…
Reference in a new issue