import 'dart:ui'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/application/setting/setting_bloc.dart'; import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart'; import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart'; import 'package:appflowy/plugins/database_view/widgets/setting/database_setting.dart'; import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/footer/grid_footer.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/row.dart'; import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_menu_item.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; import 'package:table_calendar/table_calendar.dart'; import 'base.dart'; import 'common_operations.dart'; extension AppFlowyDatabaseTest on WidgetTester { Future hoverOnFirstRowOfGrid() async { final findRow = find.byType(GridRow); expect(findRow, findsWidgets); final firstRow = findRow.first; await hoverOnWidget(firstRow); } Future editCell({ required int rowIndex, required FieldType fieldType, required String input, }) async { final findRow = find.byType(GridRow); final findCell = finderForFieldType(fieldType); final cell = find.descendant( of: findRow.at(rowIndex), matching: findCell, ); expect(cell, findsOneWidget); await enterText(cell, input); await pumpAndSettle(); } Future tapCheckboxCellInGrid({ required int rowIndex, }) async { final findRow = find.byType(GridRow); final findCell = finderForFieldType(FieldType.Checkbox); final cell = find.descendant( of: findRow.at(rowIndex), matching: findCell, ); final button = find.descendant( of: cell, matching: find.byType(FlowyIconButton), ); expect(cell, findsOneWidget); await tapButton(button); } Future assertCheckboxCell({ required int rowIndex, required bool isSelected, }) async { final findRow = find.byType(GridRow); final findCell = finderForFieldType(FieldType.Checkbox); final cell = find.descendant( of: findRow.at(rowIndex), matching: findCell, ); var finder = find.byType(CheckboxCellUncheck); if (isSelected) { finder = find.byType(CheckboxCellCheck); } expect( find.descendant( of: cell, matching: finder, ), findsOneWidget, ); } Future tapCellInGrid({ required int rowIndex, required FieldType fieldType, }) async { final findRow = find.byType(GridRow); final findCell = finderForFieldType(fieldType); final cell = find.descendant( of: findRow.at(rowIndex), matching: findCell, ); expect(cell, findsOneWidget); await tapButton(cell); } Future assertCellContent({ required int rowIndex, required FieldType fieldType, required String content, }) async { final findRow = find.byType(GridRow); final findCell = finderForFieldType(fieldType); final cell = find.descendant( of: findRow.at(rowIndex), matching: findCell, ); final findContent = find.descendant( of: cell, matching: find.text(content), ); expect(findContent, findsOneWidget); } Future selectDay({ required int content, }) async { final findCalendar = find.byType(TableCalendar); final findDay = find.text(content.toString()); final finder = find.descendant( of: findCalendar, matching: findDay, ); await tapButton(finder); } Future openFirstRowDetailPage() async { await hoverOnFirstRowOfGrid(); final expandButton = find.byType(PrimaryCellAccessory); expect(expandButton, findsOneWidget); await tapButton(expandButton); } Future hoverRowBanner() async { final banner = find.byType(RowBanner); expect(banner, findsOneWidget); await startGesture( getTopLeft(banner), kind: PointerDeviceKind.mouse, ); await pumpAndSettle(); } Future openEmojiPicker() async { await tapButton(find.byType(EmojiPickerButton)); await tapButton(find.byType(EmojiSelectionMenu)); } /// Must call [openEmojiPicker] first Future switchToEmojiList() async { final icon = find.byIcon(Icons.tag_faces); await tapButton(icon); } Future tapEmoji(String emoji) async { final emojiWidget = find.text(emoji); await tapButton(emojiWidget); } Future scrollGridByOffset(Offset offset) async { await drag(find.byType(GridPage), offset); await pumpAndSettle(); } Future scrollRowDetailByOffset(Offset offset) async { await drag(find.byType(RowDetailPage), offset); await pumpAndSettle(); } Future scrollToRight(Finder find) async { final size = getSize(find); await drag(find, Offset(-size.width, 0)); await pumpAndSettle(const Duration(milliseconds: 500)); } Future tapNewPropertyButton() async { await tapButtonWithName(LocaleKeys.grid_field_newProperty.tr()); await pumpAndSettle(); } Future tapGridFieldWithName(String name) async { final field = find.byWidgetPredicate( (widget) => widget is FieldCellButton && widget.field.name == name, ); await tapButton(field); await pumpAndSettle(); } /// Should call [tapGridFieldWithName] first. Future tapEditPropertyButton() async { await tapButtonWithName(LocaleKeys.grid_field_editProperty.tr()); await pumpAndSettle(const Duration(milliseconds: 200)); } /// Should call [tapGridFieldWithName] first. Future tapDeletePropertyButton() async { final field = find.byWidgetPredicate( (widget) => widget is FieldActionCell && widget.action == FieldAction.delete, ); await tapButton(field); } /// Should call [tapGridFieldWithName] first. Future tapDialogOkButton() async { final field = find.byWidgetPredicate( (widget) => widget is PrimaryTextButton && widget.label == LocaleKeys.button_OK.tr(), ); await tapButton(field); } /// Should call [tapGridFieldWithName] first. Future tapDuplicatePropertyButton() async { final field = find.byWidgetPredicate( (widget) => widget is FieldActionCell && widget.action == FieldAction.duplicate, ); await tapButton(field); } /// Should call [tapGridFieldWithName] first. Future tapHidePropertyButton() async { final field = find.byWidgetPredicate( (widget) => widget is FieldActionCell && widget.action == FieldAction.hide, ); await tapButton(field); } Future tapRowDetailPageCreatePropertyButton() async { await tapButton(find.byType(CreateRowFieldButton)); } Future tapRowDetailPageDeleteRowButton() async { await tapButton(find.byType(RowDetailPageDeleteButton)); } Future tapRowDetailPageDuplicateRowButton() async { await tapButton(find.byType(RowDetailPageDuplicateButton)); } Future tapTypeOptionButton() async { await tapButton(find.byType(SwitchFieldButton)); } Future tapEscButton() async { await sendKeyEvent(LogicalKeyboardKey.escape); } /// Must call [tapTypeOptionButton] first. Future selectFieldType(FieldType fieldType) async { final fieldTypeButton = find.byWidgetPredicate( (widget) => widget is FlowyText && widget.title == fieldType.title(), ); await tapButton(fieldTypeButton); } /// Each field has its own cell, so we can find the corresponding cell by /// the field type after create a new field. Future findCellByFieldType(FieldType fieldType) async { final finder = finderForFieldType(fieldType); expect(finder, findsWidgets); } Future assertNumberOfFieldsInGridPage(int num) async { expect(find.byType(GridFieldCell), findsNWidgets(num)); } Future assertNumberOfRowsInGridPage(int num) async { expect(find.byType(GridRow), findsNWidgets(num)); } Future assertDocumentExistInRowDetailPage() async { expect(find.byType(RowDocument), findsOneWidget); } /// Check the field type of the [FieldCellButton] is the same as the name. Future assertFieldTypeWithFieldName( String name, FieldType fieldType, ) async { final field = find.byWidgetPredicate( (widget) => widget is FieldCellButton && widget.field.fieldType == fieldType && widget.field.name == name, ); expect(field, findsOneWidget); } Future findFieldWithName(String name) async { final field = find.byWidgetPredicate( (widget) => widget is FieldCellButton && widget.field.name == name, ); expect(field, findsOneWidget); } Future noFieldWithName(String name) async { final field = find.byWidgetPredicate( (widget) => widget is FieldCellButton && widget.field.name == name, ); expect(field, findsNothing); } Future renameField(String newName) async { final textField = find.byType(FieldNameTextField); expect(textField, findsOneWidget); await enterText(textField, newName); await pumpAndSettle(); } Future dismissFieldEditor() async { await sendKeyEvent(LogicalKeyboardKey.escape); await sendKeyEvent(LogicalKeyboardKey.escape); await sendKeyEvent(LogicalKeyboardKey.escape); await pumpAndSettle(); } Future findFieldEditor(dynamic matcher) async { final finder = find.byType(FieldEditor); expect(finder, matcher); } Future findDateEditor(dynamic matcher) async { final finder = find.byType(DateCellEditor); expect(finder, matcher); } Future tapCreateRowButtonInGrid() async { await tapButton(find.byType(GridAddRowButton)); } Future tapCreateRowButtonInRowMenuOfGrid() async { await tapButton(find.byType(InsertRowButton)); } Future tapRowMenuButtonInGrid() async { await tapButton(find.byType(RowMenuButton)); } /// Should call [tapRowMenuButtonInGrid] first. Future tapDeleteOnRowMenu() async { await tapButtonWithName(LocaleKeys.grid_row_delete.tr()); } Future assertRowCountInGridPage(int num) async { final text = find.byWidgetPredicate( (widget) => widget is FlowyText && widget.title == rowCountString(num), ); expect(text, findsOneWidget); } Future createField(FieldType fieldType, String name) async { await scrollToRight(find.byType(GridPage)); await tapNewPropertyButton(); await renameField(name); await tapTypeOptionButton(); await selectFieldType(fieldType); await dismissFieldEditor(); } Future tapDatabaseSettingButton() async { await tapButton(find.byType(SettingButton)); } /// Should call [tapDatabaseSettingButton] first. Future tapDatabaseLayoutButton() async { final findSettingItem = find.byType(DatabaseSettingItem); final findLayoutButton = find.byWidgetPredicate( (widget) => widget is FlowyText && widget.title == DatabaseSettingAction.showLayout.title(), ); final button = find.descendant( of: findSettingItem, matching: findLayoutButton, ); await tapButton(button); } Future selectDatabaseLayoutType(DatabaseLayoutPB layout) async { final findLayoutCell = find.byType(DatabaseViewLayoutCell); final findText = find.byWidgetPredicate( (widget) => widget is FlowyText && widget.title == layout.layoutName(), ); final button = find.descendant( of: findLayoutCell, matching: findText, ); await tapButton(button); } Future assertCurrentDatabaseLayoutType(DatabaseLayoutPB layout) async { expect(finderForDatabaseLayoutType(layout), findsOneWidget); } } Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) { switch (layout) { case DatabaseLayoutPB.Board: return find.byType(BoardPage); case DatabaseLayoutPB.Calendar: return find.byType(CalendarPage); case DatabaseLayoutPB.Grid: return find.byType(GridPage); default: throw Exception('Unknown database layout type: $layout'); } } Finder finderForFieldType(FieldType fieldType) { switch (fieldType) { case FieldType.Checkbox: return find.byType(GridCheckboxCell); case FieldType.DateTime: return find.byType(GridDateCell); case FieldType.LastEditedTime: case FieldType.CreatedTime: return find.byType(GridDateCell); case FieldType.SingleSelect: return find.byType(GridSingleSelectCell); case FieldType.MultiSelect: return find.byType(GridMultiSelectCell); case FieldType.Checklist: return find.byType(GridChecklistCell); case FieldType.Number: return find.byType(GridNumberCell); case FieldType.RichText: return find.byType(GridTextCell); case FieldType.URL: return find.byType(GridURLCell); default: throw Exception('Unknown field type: $fieldType'); } }