feat: mobile tool bar basic features (#161)

* refactor: refactor mobile toolbar

* feat: add text decoration icon in AFMobileIcons

* chore: update mobile parameter

* feat: add text decoration basic feature

* chore: replace text by AppFlowyEditorLocalizations

* feat: add code toolbar item

* chore: refactor code
- add hasMenu in MToolbarItem
- add const size

* feat: add line input menu

* chore: create toolbar items draft file

* refactor: textDecorationMToolbarItem refactor

* fix: add key to ensures MToolbarWidget is rebuilt whenever the selection changes

* feat: add link toolbar item and the way to close item menu

* chore: replace m by mobile

* chore:replace BuiltInAttributeKey by FlowyRichTextKeys

* chore: replace m by mobile

* refactor: refactor MobileToolbarWidget

* refactor: add MobileToolbarStyle to control all cosmetic parameters

* refactor: refactor MobileToolbarItemMenuBtn to stateless widget

* feat: add heading menu

* fix: fix open the wrong menu

* feat: add heading icon

* feat: add todo list and list toolbar item

* feat: add quote toolbar item

* fix: fix selection service error

* test: add mobile toolbar basic elements test

* test: add item menu button test and helper widgets

* test: add text decoration mobile toolbar item test

* test: add code mobile toolbar item test

* test: add heading mobile toolbar item test

* test: add list mobile toolbar item test

* chore: fix typo

* test: add quote mobile toolbar item test

* test: add todo list mobile toolbar item test

* test: add link mobile toolbar item test

* chore: update name and add Material widget

* fix: flutter analyze

* fix: flutter analyze in test mobile toolbar

* chore: dart format test code

* refactor: improve code

* fix: fix refactor error

* chore: move all the tests in mobile to test/mobile

* chore: move toolbar/items to toolbar/desktop/items

* chore: remove the mobileToolbarItemMenuStateKey

* chore: add some comments

* test: update testing

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
This commit is contained in:
Yijing Huang 2023-06-13 01:01:08 -05:00 • committed by GitHub
commit 6a4a8ae818
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 1574 additions and 125 deletions

View file

@ -0,0 +1,364 @@
import 'package:appflowy_editor/src/editor/toolbar/desktop/items/color/color_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../../../infra/testable_editor.dart';
void main() {
setUpAll(() {
TestWidgetsFlutterBinding.ensureInitialized();
});
group('color_picker.dart widget test', () {
testWidgets('test expansion tile widget in color picker', (tester) async {
final editor = tester.editor;
await editor.startTesting();
final key = GlobalKey();
final widget = ColorPicker(
key: key,
editorState: editor.editorState,
isTextColor: true,
colorOptions: const [],
onDismiss: () {},
onSubmittedColorHex: (String color) {},
selectedColorHex: '0xFFFFFFFF',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: widget,
),
),
);
expect(find.byKey(key), findsOneWidget);
final expansionTile = find.byType(ExpansionTile);
expect(expansionTile, findsOneWidget);
await tester.tap(expansionTile);
await tester.pumpAndSettle();
expect(find.byType(TextField), findsNWidgets(2));
});
testWidgets(
'test if custom font color selector text field are initialised correctly when selectedFontColorhex is provided',
(tester) async {
final editor = tester.editor;
await editor.startTesting();
final widget = ColorPicker(
editorState: editor.editorState,
isTextColor: true,
colorOptions: const [],
onDismiss: () {},
onSubmittedColorHex: (String color) {},
selectedColorHex: '0xFAFFFF08',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: widget,
),
),
);
final fontColorExpansionTile = find.byType(ExpansionTile);
await tester.tap(fontColorExpansionTile);
await tester.pumpAndSettle();
final fontColorTextField = find.byType(TextField).at(0);
final fontOpacityTexField = find.byType(TextField).at(1);
expect(
(tester.widget(fontColorTextField) as TextField).controller!.text,
'FFFF08',
);
expect(
(tester.widget(fontOpacityTexField) as TextField).controller!.text,
'98',
);
});
testWidgets(
'test if custom font color selector text field are initialised correctly when selectedFontColorhex is null',
(tester) async {
final editor = tester.editor;
await editor.startTesting();
final widget = ColorPicker(
editorState: editor.editorState,
isTextColor: true,
colorOptions: const [],
onDismiss: () {},
onSubmittedColorHex: (String color) {},
selectedColorHex: null,
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: widget,
),
),
);
final fontColorExpansionTile = find.byType(ExpansionTile);
await tester.tap(fontColorExpansionTile);
await tester.pumpAndSettle();
final fontColorTextField = find.byType(TextField).at(0);
final fontOpacityTexField = find.byType(TextField).at(1);
expect(
(tester.widget(fontColorTextField) as TextField).controller!.text,
'FFFFFF',
);
expect(
(tester.widget(fontOpacityTexField) as TextField).controller!.text,
'100',
);
});
testWidgets(
'test if custom background color selector text field are initialised correctly when selectedBackgroundColorHex is provided',
(tester) async {
final editor = tester.editor;
await editor.startTesting();
final widget = ColorPicker(
editorState: editor.editorState,
isTextColor: false,
colorOptions: const [],
onDismiss: () {},
onSubmittedColorHex: (String color) {},
selectedColorHex: '0xFBFFFF08',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: widget,
),
),
);
final backgroundColorExpansionTile = find.byType(ExpansionTile);
await tester.tap(backgroundColorExpansionTile);
await tester.pumpAndSettle();
final backgroundColorTextField = find.byType(TextField).at(0);
final backgroundOpacityTextField = find.byType(TextField).at(1);
expect(
(tester.widget(backgroundColorTextField) as TextField).controller!.text,
'FFFF08',
);
expect(
(tester.widget(backgroundOpacityTextField) as TextField)
.controller!
.text,
'98',
);
});
testWidgets(
'test if custom background color selector text field are initialised correctly when selectedBackgroundColorHex is null',
(tester) async {
final editor = tester.editor;
await editor.startTesting();
final widget = ColorPicker(
editorState: editor.editorState,
isTextColor: false,
colorOptions: const [],
onDismiss: () {},
onSubmittedColorHex: (String color) {},
selectedColorHex: null,
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: widget,
),
),
);
final backgroundColorExpansionTile = find.byType(ExpansionTile);
await tester.tap(backgroundColorExpansionTile);
await tester.pumpAndSettle();
final backgroundColorTextField = find.byType(TextField).at(0);
final backgroundOpacityTextField = find.byType(TextField).at(1);
expect(
(tester.widget(backgroundColorTextField) as TextField).controller!.text,
'FFFFFF',
);
expect(
(tester.widget(backgroundOpacityTextField) as TextField)
.controller!
.text,
'100',
);
});
testWidgets('test submitting font color and opacity', (tester) async {
String fontColorHex = '0xFAFFFF08';
final editor = tester.editor;
await editor.startTesting();
final widget = ColorPicker(
editorState: editor.editorState,
isTextColor: true,
colorOptions: const [],
onDismiss: () {},
onSubmittedColorHex: (String color) {
fontColorHex = color;
},
selectedColorHex: fontColorHex,
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: widget,
),
),
);
final fontColorExpansionTile = find.byType(ExpansionTile);
await tester.tap(fontColorExpansionTile);
await tester.pumpAndSettle();
final fontColorTextField = find.byType(TextField).at(0);
final fontOpacityTexField = find.byType(TextField).at(1);
await tester.enterText(fontColorTextField, '000000');
await tester.enterText(fontOpacityTexField, '100');
await tester.testTextInput.receiveAction(TextInputAction.done);
fontColorHex = fontColorHex.toLowerCase();
expect(fontColorHex, '0xff000000');
});
testWidgets('test submitting wrong font color and opacity', (tester) async {
String fontColorHex = '0xFAFFFF08';
final editor = tester.editor;
await editor.startTesting();
final widget = ColorPicker(
editorState: editor.editorState,
isTextColor: true,
colorOptions: const [],
onDismiss: () {},
onSubmittedColorHex: (String color) {
fontColorHex = color;
},
selectedColorHex: fontColorHex,
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: widget,
),
),
);
final fontColorExpansionTile = find.byType(ExpansionTile);
await tester.tap(fontColorExpansionTile);
await tester.pumpAndSettle();
final fontColorTextField = find.byType(TextField).at(0);
final fontOpacityTexField = find.byType(TextField).at(1);
await tester.enterText(fontColorTextField, '====');
await tester.enterText(fontOpacityTexField, '***');
await tester.testTextInput.receiveAction(TextInputAction.done);
fontColorHex = fontColorHex.toLowerCase();
expect(fontColorHex, '0xffffffff');
});
testWidgets('test submitting background color and opacity',
(tester) async {
String backgroundColorHex = '0xFAFFFFAD';
final editor = tester.editor;
await editor.startTesting();
final widget = ColorPicker(
editorState: editor.editorState,
isTextColor: false,
colorOptions: const [],
onDismiss: () {},
onSubmittedColorHex: (String color) {
backgroundColorHex = color;
},
selectedColorHex: backgroundColorHex,
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: widget,
),
),
);
final backgroundColorExpansionTile = find.byType(ExpansionTile);
await tester.tap(backgroundColorExpansionTile);
await tester.pumpAndSettle();
final backgroundColorTextField = find.byType(TextField).at(0);
final backgroundOpacityTexField = find.byType(TextField).at(1);
await tester.enterText(backgroundColorTextField, '000000');
await tester.enterText(backgroundOpacityTexField, '100');
await tester.testTextInput.receiveAction(TextInputAction.done);
backgroundColorHex = backgroundColorHex.toLowerCase();
expect(backgroundColorHex, '0xff000000');
});
testWidgets('test submitting wrong background color and opacity',
(tester) async {
String backgroundColorHex = '0xFAFFFF08';
final editor = tester.editor;
await editor.startTesting();
final widget = ColorPicker(
editorState: editor.editorState,
isTextColor: false,
colorOptions: const [],
onDismiss: () {},
onSubmittedColorHex: (String color) {
backgroundColorHex = color;
},
selectedColorHex: backgroundColorHex,
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: widget,
),
),
);
final backgroundColorExpansionTile = find.byType(ExpansionTile);
await tester.tap(backgroundColorExpansionTile);
await tester.pumpAndSettle();
final backgroundColorTextField = find.byType(TextField).at(0);
final backgroundOpacityTexField = find.byType(TextField).at(1);
await tester.enterText(backgroundColorTextField, '***');
await tester.enterText(backgroundOpacityTexField, '===');
await tester.testTextInput.receiveAction(TextInputAction.done);
backgroundColorHex = backgroundColorHex.toLowerCase();
expect(backgroundColorHex, '0xffffffff');
});
});
}

View file

@ -0,0 +1,96 @@
import 'package:appflowy_editor/src/core/document/text_delta.dart';
import 'package:appflowy_editor/src/editor/toolbar/desktop/items/link/link_menu.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../../../infra/testable_editor.dart';
void main() async {
setUpAll(() {
TestWidgetsFlutterBinding.ensureInitialized();
});
group('link_menu.dart', () {
testWidgets('test empty link menu actions', (tester) async {
const link = 'appflowy.io';
var submittedText = '';
final linkMenu = LinkMenu(
onOpenLink: () {},
onCopyLink: () {},
onRemoveLink: () {},
onSubmitted: (text) {
submittedText = text;
},
);
final editor = tester.editor;
await editor.startTesting();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: linkMenu,
),
),
);
await tester.pumpAndSettle();
expect(find.byType(TextButton), findsNothing);
expect(find.byType(TextField), findsOneWidget);
await tester.tap(find.byType(TextField));
await tester.enterText(find.byType(TextField), link);
await tester.pumpAndSettle();
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle(Duration(milliseconds: 500));
expect(submittedText, link);
});
testWidgets('test tap linked text', (tester) async {
const link = 'appflowy.io';
final editor = tester.editor;
//create a link [appflowy.io](appflowy.io)
editor.addParagraph(
builder: (index) => Delta()..insert(link, attributes: {"href": link}),
);
await editor.startTesting();
await tester.pumpAndSettle();
final finder = find.text(link, findRichText: true);
expect(finder, findsOneWidget);
// tap the link
await tester.tap(finder);
await tester.pumpAndSettle(const Duration(milliseconds: 350));
final linkMenu = find.byType(LinkMenu);
expect(linkMenu, findsOneWidget);
expect(find.text(link, findRichText: true), findsNWidgets(2));
await editor.dispose();
});
testWidgets('test tap linked text when editor not editable',
(tester) async {
const link = 'appflowy.io';
final editor = tester.editor;
//create a link [appflowy.io](appflowy.io)
editor.addParagraph(
builder: (index) => Delta()..insert(link, attributes: {"href": link}),
);
await editor.startTesting(editable: false);
await tester.pumpAndSettle();
final finder = find.text(link, findRichText: true);
expect(finder, findsOneWidget);
await tester.tap(finder);
await tester.pumpAndSettle();
final linkMenu = find.byType(LinkMenu);
expect(linkMenu, findsNothing);
expect(find.text(link, findRichText: true), findsOneWidget);
await editor.dispose();
});
});
}