refactor(learn): the editor's general parts move to halkyon_editor (ADR 0033, D37)
Todo stores editor documents too, so the document, the editor, its view, the format bar, links, the clipboard, scripts and EditorBlocks move, with their strings and tests, to a shared package on the same appflowy_editor fork. What is Learn's alone stays in Learn on top of it, and what the editor took from it directly is inverted: EditorBlocks gains textMarks, formatActions and onEditorClosed, RichContentEditor a wrapEditor, and an InsertableBlock's name takes the BuildContext. Learn's screens use LearnContentEditor and LearnContentView, which add Learn's blocks and the grammar check. Learn behaves as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
4875b26174
commit
648dfe4051
101 changed files with 1676 additions and 1366 deletions
|
|
@ -1,7 +1,7 @@
|
|||
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/rich_content.dart';
|
||||
import 'package:halkyon_editor/halkyon_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/profile/public_person.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_editor/halkyon_editor.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/feature/card/data/card_payload.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_payload.dart';
|
||||
|
|
|
|||
|
|
@ -1,362 +0,0 @@
|
|||
import 'dart:convert';
|
||||
|
||||
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/content/rich_content.dart';
|
||||
|
||||
void main() {
|
||||
/// The editor's JSON for a page with one paragraph per text.
|
||||
Map<String, Object?> paragraphs(List<String> texts) => {
|
||||
'document': {
|
||||
'type': 'page',
|
||||
'children': [
|
||||
for (final text in texts)
|
||||
{
|
||||
'type': 'paragraph',
|
||||
'data': {
|
||||
'delta': [
|
||||
{'insert': text},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
group('RichContent', () {
|
||||
test('without a first heading that repeats a title, whatever its case '
|
||||
'and spaces; anything else stays', () {
|
||||
RichContent page(List<Map<String, Object?>> children) => RichContent({
|
||||
'document': {'type': 'page', 'children': children},
|
||||
});
|
||||
Map<String, Object?> heading(String text) => {
|
||||
'type': 'heading',
|
||||
'data': {
|
||||
'level': 1,
|
||||
'delta': [
|
||||
{'insert': text},
|
||||
],
|
||||
},
|
||||
};
|
||||
final text = {
|
||||
'type': 'paragraph',
|
||||
'data': {
|
||||
'delta': [
|
||||
{'insert': 'In Japanese…'},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
page([heading('What is Kanji?'), text])
|
||||
.withoutHeading('what is kanji?'),
|
||||
page([text]),
|
||||
);
|
||||
final other = page([heading('Kanji'), text]);
|
||||
expect(other.withoutHeading('What is Kanji?'), other);
|
||||
final later = page([text, heading('What is Kanji?')]);
|
||||
expect(later.withoutHeading('What is Kanji?'), later);
|
||||
expect(other.withoutHeading(''), other);
|
||||
});
|
||||
|
||||
test('equals content with the same JSON, whatever the key order', () {
|
||||
final a = RichContent({
|
||||
'document': {'type': 'page', 'children': <Object?>[]},
|
||||
});
|
||||
final b = RichContent({
|
||||
'document': {'children': <Object?>[], 'type': 'page'},
|
||||
});
|
||||
|
||||
expect(a, b);
|
||||
expect(a.hashCode, b.hashCode);
|
||||
});
|
||||
|
||||
test('differs when the text differs', () {
|
||||
expect(
|
||||
RichContent(paragraphs(['猫'])),
|
||||
isNot(RichContent(paragraphs(['犬']))),
|
||||
);
|
||||
});
|
||||
|
||||
test('differs when the format version differs', () {
|
||||
expect(
|
||||
RichContent(paragraphs(['猫'])),
|
||||
isNot(RichContent(paragraphs(['猫']), formatVersion: 0)),
|
||||
);
|
||||
});
|
||||
|
||||
test('is not affected by later changes to the JSON it was made from', () {
|
||||
final json = paragraphs(['猫']);
|
||||
final content = RichContent(json);
|
||||
|
||||
final document = json['document']! as Map<String, Object?>;
|
||||
(document['children']! as List<Object?>).clear();
|
||||
|
||||
expect(content, RichContent(paragraphs(['猫'])));
|
||||
});
|
||||
|
||||
test('cannot be changed through its JSON', () {
|
||||
final content = RichContent(paragraphs(['猫']));
|
||||
final document = content.editorJson['document']! as Map<String, Object?>;
|
||||
final children = document['children']! as List<Object?>;
|
||||
|
||||
expect(() => document['type'] = 'heading', throwsUnsupportedError);
|
||||
expect(children.clear, throwsUnsupportedError);
|
||||
});
|
||||
|
||||
test('survives a JSON round trip, Japanese text and marks included', () {
|
||||
final content = RichContent({
|
||||
'document': {
|
||||
'type': 'page',
|
||||
'children': [
|
||||
{
|
||||
'type': 'heading',
|
||||
'data': {
|
||||
'level': 2,
|
||||
'delta': [
|
||||
{'insert': '日本語'},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
'type': 'paragraph',
|
||||
'data': {
|
||||
'delta': [
|
||||
{
|
||||
'insert': '猫',
|
||||
'attributes': {'bold': true},
|
||||
},
|
||||
{'insert': 'は動物です。'},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
final stored = jsonEncode(content.toJson());
|
||||
final read = RichContent.fromJson(
|
||||
jsonDecode(stored) as Map<String, Object?>,
|
||||
);
|
||||
|
||||
expect(read, content);
|
||||
});
|
||||
|
||||
test('rejects content in a newer format than this app knows', () {
|
||||
expect(
|
||||
() => RichContent.fromJson({
|
||||
'formatVersion': RichContent.currentFormatVersion + 1,
|
||||
'editor': RichContent.empty().editorJson,
|
||||
}),
|
||||
throwsFormatException,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects JSON that is not rich content', () {
|
||||
expect(
|
||||
() => RichContent.fromJson({'formatVersion': 1, 'editor': 'text'}),
|
||||
throwsFormatException,
|
||||
);
|
||||
expect(
|
||||
() => RichContent.fromJson({'editor': RichContent.empty().editorJson}),
|
||||
throwsFormatException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("RichContent.imageIds lists the person's images it shows, in order "
|
||||
'and once each; no sound, nor an image from elsewhere', () {
|
||||
Map<String, Object?> block(String type, String url) => {
|
||||
'type': type,
|
||||
'data': {'url': url},
|
||||
};
|
||||
final content = RichContent({
|
||||
'document': {
|
||||
'type': 'page',
|
||||
'children': [
|
||||
block('image', 'asset:b'),
|
||||
block('audio', 'asset:sound'),
|
||||
block('image', 'https://example.org/cat.png'),
|
||||
block('image', 'asset:a'),
|
||||
block('image', 'asset:b'),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(content.imageIds, ['b', 'a']);
|
||||
});
|
||||
|
||||
group('RichContent.plainText', () {
|
||||
test('is the text without formatting, one line per block', () {
|
||||
expect(
|
||||
RichContent(paragraphs(['ひらがな', 'かたかな'])).plainText,
|
||||
'ひらがな\nかたかな',
|
||||
);
|
||||
});
|
||||
|
||||
test('joins the pieces of a formatted block', () {
|
||||
final content = RichContent({
|
||||
'document': {
|
||||
'type': 'page',
|
||||
'children': [
|
||||
{
|
||||
'type': 'paragraph',
|
||||
'data': {
|
||||
'delta': [
|
||||
{'insert': 'big '},
|
||||
{
|
||||
'insert': 'cat',
|
||||
'attributes': {'bold': true},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(content.plainText, 'big cat');
|
||||
});
|
||||
|
||||
test('is empty for empty content', () {
|
||||
expect(RichContent.empty().plainText, isEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
group('files', () {
|
||||
RichContent blocks(List<Map<String, Object?>> children) => RichContent({
|
||||
'document': {'type': 'page', 'children': children},
|
||||
});
|
||||
|
||||
test('names the asset rows its images and sounds show', () {
|
||||
final content = blocks([
|
||||
{
|
||||
'type': 'image',
|
||||
'data': {'url': assetUrl('cat')},
|
||||
},
|
||||
{
|
||||
'type': 'audio',
|
||||
'data': {'url': assetUrl('neko')},
|
||||
},
|
||||
{
|
||||
'type': 'image',
|
||||
'data': {'url': 'https://example.com/dog.png'},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(content.assetIds, {'cat', 'neko'});
|
||||
expect(content.hasImages, isTrue);
|
||||
expect(content.hasSounds, isTrue);
|
||||
});
|
||||
|
||||
test('content of only an image or a sound is not blank', () {
|
||||
expect(
|
||||
blocks([
|
||||
{
|
||||
'type': 'image',
|
||||
'data': {'url': assetUrl('cat')},
|
||||
},
|
||||
]).isBlank,
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
blocks([
|
||||
{
|
||||
'type': 'audio',
|
||||
'data': {'url': assetUrl('neko')},
|
||||
},
|
||||
]).isBlank,
|
||||
isFalse,
|
||||
);
|
||||
expect(RichContent.plain(' ').isBlank, isTrue);
|
||||
expect(RichContent.empty().isBlank, isTrue);
|
||||
});
|
||||
|
||||
test("a block's text counts as text: a formula, a callout's title, a "
|
||||
"video's caption, a study button's label, a spoiler's hidden text", () {
|
||||
final content = blocks([
|
||||
{
|
||||
'type': mathBlockType,
|
||||
'data': {mathFormulaKey: 'E = mc^2'},
|
||||
},
|
||||
{
|
||||
'type': calloutBlockType,
|
||||
'data': {calloutTitleKey: 'Mind the particle'},
|
||||
'children': [
|
||||
{
|
||||
'type': 'paragraph',
|
||||
'data': {
|
||||
'delta': [
|
||||
{'insert': 'は, not わ'},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'type': youtubeBlockType,
|
||||
'data': {youtubeVideoKey: 'dQw4w9WgXcQ', youtubeCaptionKey: 'Listen'},
|
||||
},
|
||||
{
|
||||
'type': studyButtonBlockType,
|
||||
'data': {
|
||||
studyButtonStudysetKey: 'japanese',
|
||||
studyButtonLabelKey: 'Go',
|
||||
},
|
||||
},
|
||||
{
|
||||
'type': spoilerBlockType,
|
||||
'data': {
|
||||
'delta': [
|
||||
{'insert': 'ねこ'},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(
|
||||
content.plainText,
|
||||
'E = mc^2\nMind the particle\nは, not わ\nListen\nGo\nねこ',
|
||||
);
|
||||
});
|
||||
|
||||
test('a video or a study button is something without any text; an '
|
||||
'empty formula or callout is nothing', () {
|
||||
expect(
|
||||
blocks([
|
||||
{
|
||||
'type': youtubeBlockType,
|
||||
'data': {youtubeVideoKey: 'dQw4w9WgXcQ'},
|
||||
},
|
||||
]).isBlank,
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
blocks([
|
||||
{
|
||||
'type': studyButtonBlockType,
|
||||
'data': {studyButtonStudysetKey: 'japanese'},
|
||||
},
|
||||
]).isBlank,
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
blocks([
|
||||
{
|
||||
'type': mathBlockType,
|
||||
'data': {mathFormulaKey: ' '},
|
||||
},
|
||||
{'type': calloutBlockType, 'data': <String, Object?>{}},
|
||||
]).isBlank,
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('reads only asset URLs as files of the person', () {
|
||||
expect(assetIdOf(assetUrl('cat')), 'cat');
|
||||
expect(assetIdOf('asset:'), isNull);
|
||||
expect(assetIdOf('https://example.com/cat.png'), isNull);
|
||||
expect(assetIdOf(42), isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -4,10 +4,8 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_app/testing.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_document.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_view.dart';
|
||||
import 'package:halkyon_editor/halkyon_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/learn_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/l10n/app_languages.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_payload.dart';
|
||||
|
|
@ -248,7 +246,7 @@ void main() {
|
|||
MaterialApp(
|
||||
localizationsDelegates: appLocalizationsDelegates,
|
||||
home: Scaffold(
|
||||
body: RichContentView(
|
||||
body: LearnContentView(
|
||||
content: Document(
|
||||
root: pageNode(children: [table]),
|
||||
).toRichContent(),
|
||||
|
|
@ -292,7 +290,7 @@ void main() {
|
|||
);
|
||||
|
||||
testWidgets('says it needs a newer app where it is shown', (tester) async {
|
||||
await pump(tester, RichContentView(content: hologram));
|
||||
await pump(tester, LearnContentView(content: hologram));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
expect(
|
||||
|
|
@ -307,7 +305,7 @@ void main() {
|
|||
tester,
|
||||
SizedBox(
|
||||
height: 400,
|
||||
child: RichContentEditor(
|
||||
child: LearnContentEditor(
|
||||
initialContent: hologram,
|
||||
onChanged: (content) => latest = content,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/core_blocks.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/editor_blocks.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
void main() {
|
||||
final german = lookupAppLocalizations(const Locale('de'));
|
||||
|
||||
/// The placeholder an empty [node] shows, among the builders for [l10n].
|
||||
String placeholderOf(Node node, AppLocalizations l10n) => buildersWithUnknown(
|
||||
coreBlocks,
|
||||
l10n,
|
||||
)[node.type]!.configuration.placeholderText(node);
|
||||
|
||||
test("the editor's own text blocks say what they are in the app's "
|
||||
'language', () {
|
||||
expect(placeholderOf(bulletedListNode(), german), german.blockListItem);
|
||||
expect(placeholderOf(numberedListNode(), german), german.blockListItem);
|
||||
expect(
|
||||
placeholderOf(todoListNode(checked: false), german),
|
||||
german.blockChecklistItem,
|
||||
);
|
||||
expect(placeholderOf(quoteNode(), german), german.blockQuote);
|
||||
expect(
|
||||
placeholderOf(headingNode(level: 2), german),
|
||||
german.blockHeading(2),
|
||||
);
|
||||
});
|
||||
|
||||
test('an empty line hints at the / menu on the desktop only, where it '
|
||||
'is', () {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.linux;
|
||||
expect(placeholderOf(paragraphNode(), german), german.editorSlashHint);
|
||||
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
expect(placeholderOf(paragraphNode(), german).trim(), isEmpty);
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
});
|
||||
|
||||
test('the blocks keep what else the editor set them up with', () {
|
||||
final todo =
|
||||
buildersWithUnknown(coreBlocks, german)[TodoListBlockKeys.type]!
|
||||
as TodoListBlockComponentBuilder;
|
||||
|
||||
expect(todo.toggleChildrenTriggers, isNotEmpty);
|
||||
});
|
||||
}
|
||||
|
|
@ -3,12 +3,11 @@ import 'dart:typed_data';
|
|||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_editor/halkyon_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_payload.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/asset/widget/stored_file_image.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/file_blocks.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_document.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';
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
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/content/rich_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/format_bar.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_view.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/scripts.dart';
|
||||
import 'package:halkyon_editor/halkyon_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/learn_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/l10n/app_languages.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/content_languages.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
|
@ -31,7 +27,7 @@ void main() {
|
|||
children: [
|
||||
SizedBox(
|
||||
height: 400,
|
||||
child: RichContentEditor(
|
||||
child: LearnContentEditor(
|
||||
initialContent: latest,
|
||||
onChanged: (content) => latest = content,
|
||||
formatBar: true,
|
||||
|
|
@ -81,56 +77,6 @@ void main() {
|
|||
];
|
||||
}
|
||||
|
||||
testWidgets('shows while the editor has the focus, and only then', (
|
||||
tester,
|
||||
) async {
|
||||
await edit(tester, 'Water is H2O');
|
||||
expect(find.byType(FormatBar), findsNothing);
|
||||
|
||||
await tester.tap(find.byType(AppFlowyEditor));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
expect(find.byType(FormatBar), findsOneWidget);
|
||||
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
expect(find.byType(FormatBar), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('bold marks the selection, shows it, and keeps the editor '
|
||||
'focused', (tester) async {
|
||||
final editorState = await edit(tester, 'Water is H2O');
|
||||
await select(tester, editorState, 0, 5);
|
||||
|
||||
await press(tester, 'Bold');
|
||||
|
||||
expect(runs().first, {AppFlowyRichTextKeys.bold: true});
|
||||
expect(find.byType(FormatBar), findsOneWidget);
|
||||
expect(
|
||||
tester
|
||||
.widget<IconButton>(
|
||||
find.ancestor(
|
||||
of: find.byTooltip('Bold'),
|
||||
matching: find.byType(IconButton),
|
||||
),
|
||||
)
|
||||
.isSelected,
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('a subscript replaces a superscript on the same text', (
|
||||
tester,
|
||||
) async {
|
||||
final editorState = await edit(tester, 'H2O');
|
||||
await select(tester, editorState, 1, 2);
|
||||
|
||||
await press(tester, 'Superscript');
|
||||
expect(runs()[1], {superscriptKey: true});
|
||||
|
||||
await press(tester, 'Subscript');
|
||||
expect(runs()[1], {subscriptKey: true});
|
||||
});
|
||||
|
||||
testWidgets('text above is furigana in Japanese: it gives the selected '
|
||||
'text a reading, asked for, and takes it away again', (tester) async {
|
||||
final editorState = await edit(tester, '漢字を読む', language: 'ja');
|
||||
|
|
@ -210,68 +156,4 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
expect(find.byType(AlertDialog), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('centres the selected line', (tester) async {
|
||||
final editorState = await edit(tester, 'Water is H2O');
|
||||
await select(tester, editorState, 0, 5);
|
||||
|
||||
await press(tester, 'Center');
|
||||
|
||||
final document = latest.editorJson['document']! as Map<String, Object?>;
|
||||
final first = (document['children']! as List).first as Map;
|
||||
expect((first['data']! as Map)['align'], 'center');
|
||||
});
|
||||
|
||||
testWidgets('read-only text shows a subscript raised by the font, '
|
||||
'smaller', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: appLocalizationsDelegates,
|
||||
home: Scaffold(
|
||||
body: ListView(
|
||||
children: [
|
||||
RichContentView(
|
||||
content: RichContent({
|
||||
'document': {
|
||||
'type': 'page',
|
||||
'children': [
|
||||
{
|
||||
'type': 'paragraph',
|
||||
'data': {
|
||||
'delta': [
|
||||
{'insert': 'H'},
|
||||
{
|
||||
'insert': '2',
|
||||
'attributes': {subscriptKey: true},
|
||||
},
|
||||
{'insert': 'O'},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
final spans = <TextSpan>[];
|
||||
for (final text in tester.widgetList<RichText>(find.byType(RichText))) {
|
||||
text.text.visitChildren((span) {
|
||||
if (span is TextSpan && span.text == '2') {
|
||||
spans.add(span);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
expect(spans, isNotEmpty);
|
||||
expect(
|
||||
spans.first.style?.fontFeatures,
|
||||
contains(const FontFeature.subscripts()),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,299 +0,0 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/services.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/content/rich_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/editor_clipboard.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/link.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/l10n/app_languages.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// Content of one line of [runs], each a text and its marks.
|
||||
RichContent line(List<(String, Map<String, Object?>)> runs) => RichContent({
|
||||
'document': {
|
||||
'type': 'page',
|
||||
'children': [
|
||||
{
|
||||
'type': 'paragraph',
|
||||
'data': {
|
||||
'delta': [
|
||||
for (final (text, marks) in runs)
|
||||
{'insert': text, if (marks.isNotEmpty) 'attributes': marks},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
/// The runs of [content]'s first line: each its text and its marks.
|
||||
List<List<Object?>> runsOf(RichContent content) {
|
||||
final document = content.editorJson['document']! as Map<String, Object?>;
|
||||
final first = (document['children']! as List).first as Map;
|
||||
return [
|
||||
for (final op in (first['data']! as Map)['delta']! as List)
|
||||
run((op as Map)['insert']! as String, {
|
||||
...?(op['attributes'] as Map<String, Object?>?),
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
/// A run of text with its marks, as [runsOf] gives it.
|
||||
List<Object?> run(String text, [Map<String, Object?> marks = const {}]) => [
|
||||
text,
|
||||
marks,
|
||||
];
|
||||
|
||||
void main() {
|
||||
/// What the system's clipboard holds, as the app's editors left it.
|
||||
String? systemClipboard;
|
||||
|
||||
setUp(() {
|
||||
systemClipboard = null;
|
||||
EditorClipboard.clear();
|
||||
});
|
||||
|
||||
/// Pumps an editor for each of [contents], one above the other, and gives
|
||||
/// back what each holds after every change, by index.
|
||||
Future<List<RichContent>> edit(
|
||||
WidgetTester tester,
|
||||
List<RichContent> contents,
|
||||
) async {
|
||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
SystemChannels.platform,
|
||||
(call) async => switch (call.method) {
|
||||
'Clipboard.setData' => () {
|
||||
systemClipboard = (call.arguments as Map)['text'] as String?;
|
||||
return null;
|
||||
}(),
|
||||
'Clipboard.getData' => {'text': systemClipboard},
|
||||
_ => null,
|
||||
},
|
||||
);
|
||||
addTearDown(
|
||||
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
SystemChannels.platform,
|
||||
null,
|
||||
),
|
||||
);
|
||||
final latest = [...contents];
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: appLocalizationsDelegates,
|
||||
home: Scaffold(
|
||||
body: Column(
|
||||
children: [
|
||||
for (final (index, content) in contents.indexed)
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: RichContentEditor(
|
||||
initialContent: content,
|
||||
onChanged: (content) => latest[index] = content,
|
||||
formatBar: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
return latest;
|
||||
}
|
||||
|
||||
EditorState editorAt(WidgetTester tester, int index) => tester
|
||||
.widget<AppFlowyEditor>(find.byType(AppFlowyEditor).at(index))
|
||||
.editorState;
|
||||
|
||||
/// Focuses the editor [index] and selects [start] to [end] of its first
|
||||
/// line.
|
||||
Future<EditorState> select(
|
||||
WidgetTester tester,
|
||||
int index,
|
||||
int start,
|
||||
int end,
|
||||
) async {
|
||||
await tester.tap(find.byType(AppFlowyEditor).at(index));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
final editorState = editorAt(tester, index)
|
||||
..selection = Selection.single(
|
||||
path: [0],
|
||||
startOffset: start,
|
||||
endOffset: end,
|
||||
);
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
return editorState;
|
||||
}
|
||||
|
||||
Future<void> shortcut(WidgetTester tester, LogicalKeyboardKey key) async {
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
||||
await tester.sendKeyEvent(key);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
}
|
||||
|
||||
group('links', () {
|
||||
testWidgets('the link button gives the selection the address typed, on '
|
||||
'the web without its scheme; Remove link takes it away', (
|
||||
tester,
|
||||
) async {
|
||||
final latest = await edit(tester, [RichContent.plain('Tae Kim guide')]);
|
||||
|
||||
await select(tester, 0, 0, 7);
|
||||
await tester.tap(find.byTooltip('Link'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byType(TextField), 'guidetojapanese.org');
|
||||
await tester.tap(find.text('Save'));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
expect(runsOf(latest[0]), [
|
||||
run('Tae Kim', {'href': 'https://guidetojapanese.org'}),
|
||||
run(' guide'),
|
||||
]);
|
||||
|
||||
await select(tester, 0, 0, 7);
|
||||
await tester.tap(find.byTooltip('Link'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('https://guidetojapanese.org'), findsOneWidget);
|
||||
await tester.tap(find.text('Remove link'));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
// The same text, unmarked (the editor may leave it in two runs).
|
||||
expect(
|
||||
[for (final [text, _] in runsOf(latest[0])) text].join(),
|
||||
'Tae Kim guide',
|
||||
);
|
||||
expect(
|
||||
[for (final [_, marks] in runsOf(latest[0])) marks],
|
||||
everyElement(isEmpty),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('at a caret, asks for the text too and writes it linked', (
|
||||
tester,
|
||||
) async {
|
||||
final latest = await edit(tester, [RichContent.plain('See: ')]);
|
||||
|
||||
await select(tester, 0, 5, 5);
|
||||
await tester.tap(find.byTooltip('Link'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byType(TextField).at(0), 'the guide');
|
||||
await tester.enterText(
|
||||
find.byType(TextField).at(1),
|
||||
'mailto:tae@example.org',
|
||||
);
|
||||
await tester.tap(find.text('Save'));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
expect(runsOf(latest[0]), [
|
||||
run('See: '),
|
||||
run('the guide', {'href': 'mailto:tae@example.org'}),
|
||||
]);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'Ctrl+K asks for the link of the selection',
|
||||
(tester) async {
|
||||
await edit(tester, [RichContent.plain('Tae Kim guide')]);
|
||||
|
||||
await select(tester, 0, 0, 7);
|
||||
await shortcut(tester, LogicalKeyboardKey.keyK);
|
||||
|
||||
expect(find.widgetWithText(AlertDialog, 'Address'), findsOneWidget);
|
||||
},
|
||||
variant: TargetPlatformVariant.only(TargetPlatform.linux),
|
||||
);
|
||||
|
||||
test('an address without its scheme leads to the web; none is none', () {
|
||||
expect(linkAddress(' example.org/a '), 'https://example.org/a');
|
||||
expect(linkAddress('http://example.org'), 'http://example.org');
|
||||
expect(linkAddress('mailto:a@example.org'), 'mailto:a@example.org');
|
||||
expect(linkAddress(' '), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('copying and pasting', () {
|
||||
final formatted = line([
|
||||
('学生', {rubyKey: 'がくせい'}),
|
||||
('だ', {'bold': true}),
|
||||
('。', {}),
|
||||
]);
|
||||
|
||||
testWidgets(
|
||||
'between the editors keeps every format, while the system clipboard '
|
||||
'holds what was copied',
|
||||
(tester) async {
|
||||
final latest = await edit(tester, [formatted, RichContent.empty()]);
|
||||
|
||||
await select(tester, 0, 0, 4);
|
||||
await shortcut(tester, LogicalKeyboardKey.keyC);
|
||||
expect(systemClipboard, '学生だ。');
|
||||
|
||||
await select(tester, 1, 0, 0);
|
||||
await shortcut(tester, LogicalKeyboardKey.keyV);
|
||||
|
||||
expect(runsOf(latest[1]), runsOf(formatted));
|
||||
},
|
||||
variant: TargetPlatformVariant.only(TargetPlatform.linux),
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'pastes plain text once the system clipboard holds something else',
|
||||
(tester) async {
|
||||
final latest = await edit(tester, [formatted, RichContent.empty()]);
|
||||
|
||||
await select(tester, 0, 0, 4);
|
||||
await shortcut(tester, LogicalKeyboardKey.keyC);
|
||||
// Copied in another app meanwhile.
|
||||
systemClipboard = 'from elsewhere';
|
||||
|
||||
await select(tester, 1, 0, 0);
|
||||
await shortcut(tester, LogicalKeyboardKey.keyV);
|
||||
|
||||
expect(runsOf(latest[1]), [run('from elsewhere')]);
|
||||
},
|
||||
variant: TargetPlatformVariant.only(TargetPlatform.linux),
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'cutting takes the selection out, to paste with its formats',
|
||||
(tester) async {
|
||||
final latest = await edit(tester, [formatted, RichContent.empty()]);
|
||||
|
||||
await select(tester, 0, 0, 3);
|
||||
await shortcut(tester, LogicalKeyboardKey.keyX);
|
||||
expect(runsOf(latest[0]), [run('。')]);
|
||||
|
||||
await select(tester, 1, 0, 0);
|
||||
await shortcut(tester, LogicalKeyboardKey.keyV);
|
||||
expect(runsOf(latest[1]), [
|
||||
run('学生', {rubyKey: 'がくせい'}),
|
||||
run('だ', {'bold': true}),
|
||||
]);
|
||||
},
|
||||
variant: TargetPlatformVariant.only(TargetPlatform.linux),
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'on a phone, selected text shows a bar to copy it, with its formats',
|
||||
(tester) async {
|
||||
final latest = await edit(tester, [formatted, RichContent.empty()]);
|
||||
|
||||
await select(tester, 0, 0, 4);
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
expect(find.text('Copy'), findsOneWidget);
|
||||
expect(find.text('Cut'), findsOneWidget);
|
||||
await tester.tap(find.text('Copy'));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
expect(systemClipboard, '学生だ。');
|
||||
|
||||
await select(tester, 1, 0, 0);
|
||||
await EditorClipboard.paste(editorAt(tester, 1));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
expect(runsOf(latest[1]), runsOf(formatted));
|
||||
},
|
||||
variant: TargetPlatformVariant.only(TargetPlatform.android),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,12 +1,9 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_math_fork/flutter_math.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/content/rich_content.dart';
|
||||
import 'package:halkyon_editor/halkyon_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/learn_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/math_view.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_document.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_view.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:material_ui/material_ui.dart';
|
||||
|
|
@ -52,7 +49,7 @@ void main() {
|
|||
home: Scaffold(
|
||||
body: SizedBox(
|
||||
height: 600,
|
||||
child: RichContentEditor(
|
||||
child: LearnContentEditor(
|
||||
initialContent: content,
|
||||
onChanged: (content) => latest = content,
|
||||
formatBar: true,
|
||||
|
|
@ -210,7 +207,7 @@ void main() {
|
|||
MaterialApp(
|
||||
theme: learnTheme(Brightness.light),
|
||||
localizationsDelegates: appLocalizationsDelegates,
|
||||
home: Scaffold(body: RichContentView(content: content)),
|
||||
home: Scaffold(body: LearnContentView(content: content)),
|
||||
),
|
||||
);
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
|
|
|||
|
|
@ -1,84 +0,0 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
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/editor/rich_content_document.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
final content = RichContent({
|
||||
'document': {
|
||||
'type': 'page',
|
||||
'children': [
|
||||
{
|
||||
'type': 'heading',
|
||||
'data': {
|
||||
'level': 2,
|
||||
'delta': [
|
||||
{'insert': '日本語'},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
'type': 'paragraph',
|
||||
'data': {
|
||||
'delta': [
|
||||
{
|
||||
'insert': '猫',
|
||||
'attributes': {'bold': true},
|
||||
},
|
||||
{'insert': 'は'},
|
||||
{
|
||||
'insert': '動物',
|
||||
'attributes': {'href': 'https://ja.wikipedia.org/wiki/動物'},
|
||||
},
|
||||
{'insert': 'です。'},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
'type': 'bulleted_list',
|
||||
'data': {
|
||||
'delta': [
|
||||
{'insert': 'ねこ'},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
group('RichContent and the editor document', () {
|
||||
test('a round trip through the editor document changes nothing', () {
|
||||
expect(content.toDocument().toRichContent(), content);
|
||||
});
|
||||
|
||||
test('empty content is a blank document', () {
|
||||
expect(RichContent.empty().toDocument().root.children, isEmpty);
|
||||
expect(Document.blank().toRichContent(), RichContent.empty());
|
||||
});
|
||||
|
||||
test('edits made in the editor are kept', () async {
|
||||
final editorState = EditorState(document: content.toDocument());
|
||||
addTearDown(editorState.dispose);
|
||||
final paragraph = editorState.document.root.children[1];
|
||||
|
||||
await editorState.apply(
|
||||
editorState.transaction..insertText(paragraph, 0, 'ほら、'),
|
||||
);
|
||||
final edited = editorState.document.toRichContent();
|
||||
|
||||
expect(edited, isNot(content));
|
||||
expect(
|
||||
edited.toDocument().root.children[1].delta?.toPlainText(),
|
||||
'ほら、猫は動物です。',
|
||||
);
|
||||
});
|
||||
|
||||
test('refuses content in a format it has no migration for', () {
|
||||
final older = RichContent(content.editorJson, formatVersion: 0);
|
||||
|
||||
expect(older.toDocument, throwsUnsupportedError);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
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/editor/rich_content_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/l10n/app_languages.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('a click in an editor shorter than its content selects there, '
|
||||
'and bringing the selection into view comes to an end', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: appLocalizationsDelegates,
|
||||
home: Scaffold(
|
||||
body: ListView(
|
||||
children: [
|
||||
SizedBox(
|
||||
// As short as a card's field: shorter than the box the
|
||||
// editor keeps around the selection while scrolling to it.
|
||||
height: 150,
|
||||
child: RichContentEditor(
|
||||
initialContent: RichContent.plain(
|
||||
[for (var line = 1; line <= 40; line++) 'Line $line']
|
||||
.join('\n'),
|
||||
),
|
||||
onChanged: (_) {},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
await tester.tap(find.text('Line 3', findRichText: true));
|
||||
// The frame comes: with a box taller than the editor, the editor
|
||||
// scrolled up and down for ever and never drew one.
|
||||
for (var frame = 0; frame < 5; frame++) {
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
}
|
||||
|
||||
final editorState = tester
|
||||
.widget<AppFlowyEditor>(find.byType(AppFlowyEditor))
|
||||
.editorState;
|
||||
expect(editorState.selection?.start.path, [2]);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||
import 'package:halkyon_editor/halkyon_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_markdown.dart';
|
||||
|
||||
void main() {
|
||||
|
|
|
|||
|
|
@ -1,36 +1,19 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/gestures.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/content/rich_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_view.dart';
|
||||
import 'package:halkyon_editor/halkyon_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/learn_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/ruby.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/l10n/app_languages.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
void main() {
|
||||
RichContent paragraph(String text) => RichContent({
|
||||
'document': {
|
||||
'type': 'page',
|
||||
'children': [
|
||||
{
|
||||
'type': 'paragraph',
|
||||
'data': {
|
||||
'delta': [
|
||||
{'insert': text},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
Future<void> show(WidgetTester tester, RichContent content) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: appLocalizationsDelegates,
|
||||
home: Scaffold(
|
||||
body: ListView(children: [RichContentView(content: content)]),
|
||||
body: ListView(children: [LearnContentView(content: content)]),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -38,221 +21,6 @@ void main() {
|
|||
await tester.pump(const Duration(seconds: 1));
|
||||
}
|
||||
|
||||
testWidgets('shows a table whose sizes are whole numbers, or missing, as '
|
||||
'JSON written elsewhere may hold them', (tester) async {
|
||||
Map<String, Object?> cell(int column, int row, Map<String, Object?> data) =>
|
||||
{
|
||||
'type': 'table/cell',
|
||||
'data': {'colPosition': column, 'rowPosition': row, ...data},
|
||||
'children': [
|
||||
{
|
||||
'type': 'paragraph',
|
||||
'data': {
|
||||
'delta': [
|
||||
{'insert': '$column$row'},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
for (final (table, cells) in [
|
||||
// As an archive from elsewhere brought it: whole numbers.
|
||||
(
|
||||
{
|
||||
'colsLen': 2,
|
||||
'rowsLen': 1,
|
||||
'colsHeight': 82,
|
||||
'colDefaultWidth': 160,
|
||||
'rowDefaultHeight': 40,
|
||||
'colMinimumWidth': 40,
|
||||
},
|
||||
{'width': 80, 'height': 40},
|
||||
),
|
||||
// Without the editor's sizes.
|
||||
(<String, Object?>{'colsLen': 2, 'rowsLen': 1}, <String, Object?>{}),
|
||||
]) {
|
||||
await show(
|
||||
tester,
|
||||
RichContent({
|
||||
'document': {
|
||||
'type': 'page',
|
||||
'children': [
|
||||
{
|
||||
'type': 'table',
|
||||
'data': table,
|
||||
'children': [cell(0, 0, cells), cell(1, 0, cells)],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(find.textContaining('00', findRichText: true), findsOneWidget);
|
||||
expect(find.textContaining('10', findRichText: true), findsOneWidget);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('a row holding a cell of several lines grows to fit it, read '
|
||||
'only as in the editor', (tester) async {
|
||||
Map<String, Object?> cell(int row, String text) => {
|
||||
'type': 'table/cell',
|
||||
'data': {'colPosition': 0, 'rowPosition': row},
|
||||
'children': [
|
||||
{
|
||||
'type': 'paragraph',
|
||||
'data': {
|
||||
'delta': [
|
||||
{'insert': text},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
await show(
|
||||
tester,
|
||||
RichContent({
|
||||
'document': {
|
||||
'type': 'page',
|
||||
'children': [
|
||||
{
|
||||
'type': 'table',
|
||||
'data': {'colsLen': 1, 'rowsLen': 2},
|
||||
'children': [cell(0, 'る\nう\nつ'), cell(1, 'す')],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
final cells = find.byType(TableCelBlockWidget);
|
||||
expect(
|
||||
tester.getSize(cells.at(0)).height,
|
||||
greaterThan(2 * tester.getSize(cells.at(1)).height),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('shows the content', (tester) async {
|
||||
await show(tester, paragraph('猫は動物です。'));
|
||||
|
||||
expect(find.textContaining('猫は動物です。', findRichText: true), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows new content when it changes', (tester) async {
|
||||
await show(tester, paragraph('猫'));
|
||||
await show(tester, paragraph('犬'));
|
||||
|
||||
expect(find.textContaining('犬', findRichText: true), findsOneWidget);
|
||||
expect(find.textContaining('猫', findRichText: true), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('is display only: a tap on its text goes to what surrounds it, '
|
||||
'and no keyboard opens', (tester) async {
|
||||
var tapped = 0;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: appLocalizationsDelegates,
|
||||
home: Scaffold(
|
||||
body: ListView(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => tapped++,
|
||||
child: RichContentView(content: paragraph('猫は動物です。')),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
await tester.tap(find.textContaining('猫', findRichText: true));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
expect(tapped, 1);
|
||||
expect(tester.testTextInput.hasAnyClients, isFalse);
|
||||
}, variant: TargetPlatformVariant.all());
|
||||
|
||||
group('asked to centre', () {
|
||||
Map<String, Object?> block(
|
||||
String type,
|
||||
String text, [
|
||||
Map<String, Object?> data = const {},
|
||||
]) => {
|
||||
'type': type,
|
||||
'data': {
|
||||
...data,
|
||||
'delta': [
|
||||
{'insert': text},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
Future<void> showCentred(
|
||||
WidgetTester tester,
|
||||
List<Map<String, Object?>> blocks,
|
||||
) async {
|
||||
tester.view.physicalSize = const Size(400, 800);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: appLocalizationsDelegates,
|
||||
home: Scaffold(
|
||||
body: ListView(
|
||||
children: [
|
||||
RichContentView(
|
||||
content: RichContent({
|
||||
'document': {'type': 'page', 'children': blocks},
|
||||
}),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
}
|
||||
|
||||
double centreOf(WidgetTester tester, String text) =>
|
||||
tester.getCenter(find.text(text, findRichText: true)).dx;
|
||||
double leftOf(WidgetTester tester, String text) =>
|
||||
tester.getTopLeft(find.text(text, findRichText: true)).dx;
|
||||
|
||||
testWidgets('centres paragraphs and headings', (tester) async {
|
||||
await showCentred(tester, [
|
||||
block('heading', 'Greetings', {'level': 2}),
|
||||
block('paragraph', 'hola'),
|
||||
]);
|
||||
|
||||
expect(centreOf(tester, 'Greetings'), moreOrLessEquals(200, epsilon: 1));
|
||||
expect(centreOf(tester, 'hola'), moreOrLessEquals(200, epsilon: 1));
|
||||
});
|
||||
|
||||
testWidgets('leaves a list at the start: centred, its markers would '
|
||||
'drift apart', (tester) async {
|
||||
await showCentred(tester, [
|
||||
block('bulleted_list', 'hola'),
|
||||
block('bulleted_list', 'buenos días'),
|
||||
]);
|
||||
|
||||
expect(leftOf(tester, 'hola'), lessThan(60));
|
||||
expect(leftOf(tester, 'buenos días'), leftOf(tester, 'hola'));
|
||||
});
|
||||
|
||||
testWidgets('keeps the alignment a paragraph was given in the editor', (
|
||||
tester,
|
||||
) async {
|
||||
await showCentred(tester, [
|
||||
block('paragraph', 'hola', {'align': 'left'}),
|
||||
]);
|
||||
|
||||
expect(leftOf(tester, 'hola'), lessThan(40));
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('sets furigana above the text it reads, the text on the line', (
|
||||
tester,
|
||||
) async {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
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/content/rich_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_document.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_view.dart';
|
||||
import 'package:halkyon_editor/halkyon_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/learn_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:material_ui/material_ui.dart';
|
||||
|
|
@ -34,7 +31,7 @@ void main() {
|
|||
tester,
|
||||
SizedBox(
|
||||
height: 600,
|
||||
child: RichContentEditor(
|
||||
child: LearnContentEditor(
|
||||
initialContent: content,
|
||||
onChanged: (content) => latest = content,
|
||||
formatBar: true,
|
||||
|
|
@ -184,7 +181,7 @@ void main() {
|
|||
],
|
||||
),
|
||||
).toRichContent();
|
||||
await pump(tester, RichContentView(content: content));
|
||||
await pump(tester, LearnContentView(content: content));
|
||||
|
||||
expect(find.text('Remember'), findsOneWidget);
|
||||
expect(
|
||||
|
|
@ -213,7 +210,7 @@ void main() {
|
|||
],
|
||||
),
|
||||
).toRichContent();
|
||||
await pump(tester, RichContentView(content: content));
|
||||
await pump(tester, LearnContentView(content: content));
|
||||
|
||||
expect(find.text('Note'), findsOneWidget);
|
||||
});
|
||||
|
|
@ -248,7 +245,7 @@ void main() {
|
|||
],
|
||||
),
|
||||
).toRichContent();
|
||||
await pump(tester, RichContentView(content: content));
|
||||
await pump(tester, LearnContentView(content: content));
|
||||
|
||||
expect(find.text('ねこ', findRichText: true), findsNothing);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/content_blocks.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_document.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_view.dart';
|
||||
import 'package:halkyon_editor/halkyon_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/learn_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:material_ui/material_ui.dart';
|
||||
|
|
@ -38,7 +35,7 @@ void main() {
|
|||
tester,
|
||||
SizedBox(
|
||||
height: 600,
|
||||
child: RichContentEditor(
|
||||
child: LearnContentEditor(
|
||||
initialContent: content,
|
||||
onChanged: (_) {},
|
||||
formatBar: true,
|
||||
|
|
@ -132,7 +129,7 @@ void main() {
|
|||
|
||||
testWidgets('shown read-only, nothing loads until a tap, which opens it '
|
||||
"outside the app where it can't play inside", (tester) async {
|
||||
await pump(tester, RichContentView(content: video(start: 90)));
|
||||
await pump(tester, LearnContentView(content: video(start: 90)));
|
||||
|
||||
expect(find.text('YouTube video'), findsOneWidget);
|
||||
expect(find.text('YouTube · from 1:30'), findsOneWidget);
|
||||
|
|
@ -149,7 +146,7 @@ void main() {
|
|||
tester,
|
||||
) async {
|
||||
videos = FakeYoutubeVideos(playInApp: true);
|
||||
await pump(tester, RichContentView(content: video(caption: 'Listen')));
|
||||
await pump(tester, LearnContentView(content: video(caption: 'Listen')));
|
||||
|
||||
expect(find.text('Listen'), findsOneWidget);
|
||||
await tester.tap(find.byTooltip('Play video'));
|
||||
|
|
@ -162,7 +159,7 @@ void main() {
|
|||
testWidgets("a video whose id can't be read says so", (tester) async {
|
||||
await pump(
|
||||
tester,
|
||||
RichContentView(
|
||||
LearnContentView(
|
||||
content: Document(
|
||||
root: pageNode(
|
||||
children: [
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||
import 'package:halkyon_editor/halkyon_editor.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';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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_editor/halkyon_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/grammar/grammar_segments.dart';
|
||||
|
||||
void main() {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_editor/halkyon_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/l10n/app_localizations_en.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/asset/widget/studyset_files.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/rich_text_screen.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/l10n/app_languages.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/privacy/model/unsigned_edit.dart';
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import 'dart:typed_data';
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_app/testing.dart';
|
||||
import 'package:halkyon_editor/halkyon_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_payload.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/privacy/data/content_keys.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/data/card_payload.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||
import 'package:halkyon_editor/halkyon_editor.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';
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue