database_test_op.dart 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254
  1. import 'dart:io';
  2. import 'package:appflowy/generated/locale_keys.g.dart';
  3. import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
  4. import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart';
  5. import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_day.dart';
  6. import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_event_card.dart';
  7. import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
  8. import 'package:appflowy/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart';
  9. import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
  10. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/checkbox.dart';
  11. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/checklist/checklist.dart';
  12. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart';
  13. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart';
  14. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/text.dart';
  15. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/create_filter_list.dart';
  16. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/disclosure_button.dart';
  17. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_menu_item.dart';
  18. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/footer/grid_footer.dart';
  19. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell.dart';
  20. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart';
  21. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
  22. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
  23. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_list.dart';
  24. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart';
  25. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart';
  26. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/row.dart';
  27. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/create_sort_list.dart';
  28. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/order_panel.dart';
  29. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_editor.dart';
  30. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_menu.dart';
  31. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/filter_button.dart';
  32. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart';
  33. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/sort_button.dart';
  34. import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_header.dart';
  35. import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
  36. import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart';
  37. import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart';
  38. import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
  39. import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart';
  40. import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart';
  41. import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
  42. import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart';
  43. import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/text_field.dart';
  44. import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart';
  45. import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart';
  46. import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
  47. import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart';
  48. import 'package:appflowy/plugins/database_view/widgets/setting/database_setting.dart';
  49. import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
  50. import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_menu_item.dart';
  51. import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
  52. import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
  53. import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
  54. import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
  55. import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
  56. import 'package:calendar_view/calendar_view.dart';
  57. import 'package:easy_localization/easy_localization.dart';
  58. import 'package:flowy_infra_ui/flowy_infra_ui.dart';
  59. import 'package:flowy_infra_ui/style_widget/text_input.dart';
  60. import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
  61. import 'package:flutter/gestures.dart';
  62. import 'package:flutter/material.dart';
  63. import 'package:flutter/services.dart';
  64. import 'package:flutter_test/flutter_test.dart';
  65. import 'package:path/path.dart' as p;
  66. import 'package:table_calendar/table_calendar.dart';
  67. import 'base.dart';
  68. import 'common_operations.dart';
  69. import 'expectation.dart';
  70. import 'mock/mock_file_picker.dart';
  71. extension AppFlowyDatabaseTest on WidgetTester {
  72. Future<void> openV020database() async {
  73. final context = await initializeAppFlowy();
  74. await tapGoButton();
  75. // expect to see a readme page
  76. expectToSeePageName(gettingStarted);
  77. await tapAddViewButton();
  78. await tapImportButton();
  79. final testFileNames = ['v020.afdb'];
  80. final paths = <String>[];
  81. for (final fileName in testFileNames) {
  82. // Don't use the p.join to build the path that used in loadString. It
  83. // is not working on windows.
  84. final str = await rootBundle
  85. .loadString("assets/test/workspaces/database/$fileName");
  86. // Write the content to the file.
  87. final path = p.join(
  88. context.applicationDataDirectory,
  89. fileName,
  90. );
  91. paths.add(path);
  92. File(path).writeAsStringSync(str);
  93. }
  94. // mock get files
  95. await mockPickFilePaths(
  96. paths: paths,
  97. );
  98. await tapDatabaseRawDataButton();
  99. await pumpAndSettle();
  100. await openPage('v020', layout: ViewLayoutPB.Grid);
  101. }
  102. Future<void> hoverOnFirstRowOfGrid() async {
  103. final findRow = find.byType(GridRow);
  104. expect(findRow, findsWidgets);
  105. final firstRow = findRow.first;
  106. await hoverOnWidget(firstRow);
  107. }
  108. Future<void> editCell({
  109. required int rowIndex,
  110. required FieldType fieldType,
  111. required String input,
  112. int cellIndex = 0,
  113. }) async {
  114. final cell = cellFinder(rowIndex, fieldType, cellIndex: cellIndex);
  115. expect(cell, findsOneWidget);
  116. await enterText(cell, input);
  117. await pumpAndSettle();
  118. }
  119. ///
  120. Finder cellFinder(int rowIndex, FieldType fieldType, {int cellIndex = 0}) {
  121. final findRow = find.byType(GridRow, skipOffstage: false);
  122. final findCell = finderForFieldType(fieldType);
  123. return find
  124. .descendant(
  125. of: findRow.at(rowIndex),
  126. matching: findCell,
  127. skipOffstage: false,
  128. )
  129. .at(cellIndex);
  130. }
  131. Future<void> tapCheckboxCellInGrid({
  132. required int rowIndex,
  133. }) async {
  134. final cell = cellFinder(rowIndex, FieldType.Checkbox);
  135. final button = find.descendant(
  136. of: cell,
  137. matching: find.byType(FlowyIconButton),
  138. );
  139. expect(cell, findsOneWidget);
  140. await tapButton(button);
  141. }
  142. Future<void> assertCheckboxCell({
  143. required int rowIndex,
  144. required bool isSelected,
  145. }) async {
  146. final cell = cellFinder(rowIndex, FieldType.Checkbox);
  147. var finder = find.byType(CheckboxCellUncheck);
  148. if (isSelected) {
  149. finder = find.byType(CheckboxCellCheck);
  150. }
  151. expect(
  152. find.descendant(
  153. of: cell,
  154. matching: finder,
  155. ),
  156. findsOneWidget,
  157. );
  158. }
  159. Future<void> tapCellInGrid({
  160. required int rowIndex,
  161. required FieldType fieldType,
  162. }) async {
  163. final cell = cellFinder(rowIndex, fieldType);
  164. expect(cell, findsOneWidget);
  165. await tapButton(cell, warnIfMissed: false);
  166. }
  167. /// The [fieldName] must be unique in the grid.
  168. Future<void> assertCellContent({
  169. required int rowIndex,
  170. required FieldType fieldType,
  171. required String content,
  172. int cellIndex = 0,
  173. }) async {
  174. final findCell = cellFinder(rowIndex, fieldType, cellIndex: cellIndex);
  175. final findContent = find.descendant(
  176. of: findCell,
  177. matching: find.text(content),
  178. skipOffstage: false,
  179. );
  180. final text = find.descendant(
  181. of: find.byType(TextField),
  182. matching: findContent,
  183. skipOffstage: false,
  184. );
  185. expect(text, findsOneWidget);
  186. }
  187. Future<void> assertSingleSelectOption({
  188. required int rowIndex,
  189. required String content,
  190. }) async {
  191. final findCell = cellFinder(rowIndex, FieldType.SingleSelect);
  192. if (content.isNotEmpty) {
  193. final finder = find.descendant(
  194. of: findCell,
  195. matching: find.byWidgetPredicate(
  196. (widget) => widget is SelectOptionTag && widget.name == content,
  197. ),
  198. );
  199. expect(finder, findsOneWidget);
  200. }
  201. }
  202. Future<void> assertMultiSelectOption({
  203. required int rowIndex,
  204. required List<String> contents,
  205. }) async {
  206. final findCell = cellFinder(rowIndex, FieldType.MultiSelect);
  207. for (final content in contents) {
  208. if (content.isNotEmpty) {
  209. final finder = find.descendant(
  210. of: findCell,
  211. matching: find.byWidgetPredicate(
  212. (widget) => widget is SelectOptionTag && widget.name == content,
  213. ),
  214. );
  215. expect(finder, findsOneWidget);
  216. }
  217. }
  218. }
  219. Future<void> assertChecklistCellInGrid({
  220. required int rowIndex,
  221. required double percent,
  222. }) async {
  223. final findCell = cellFinder(rowIndex, FieldType.Checklist);
  224. final finder = find.descendant(
  225. of: findCell,
  226. matching: find.byWidgetPredicate(
  227. (widget) {
  228. if (widget is ChecklistProgressBar) {
  229. return widget.percent == percent;
  230. }
  231. return false;
  232. },
  233. ),
  234. );
  235. expect(finder, findsOneWidget);
  236. }
  237. Future<void> assertDateCellInGrid({
  238. required int rowIndex,
  239. required FieldType fieldType,
  240. required String content,
  241. }) async {
  242. final findRow = find.byType(GridRow, skipOffstage: false);
  243. final findCell = find.descendant(
  244. of: findRow.at(rowIndex),
  245. matching: find.byWidgetPredicate(
  246. (widget) => widget is GridDateCell && widget.fieldType == fieldType,
  247. ),
  248. skipOffstage: false,
  249. );
  250. final dateCellText = find.descendant(
  251. of: findCell,
  252. matching: find.byType(GridDateCellText),
  253. );
  254. final text = find.descendant(
  255. of: dateCellText,
  256. matching: find.byWidgetPredicate(
  257. (widget) {
  258. if (widget is FlowyText) {
  259. return widget.text == content;
  260. }
  261. return false;
  262. },
  263. ),
  264. skipOffstage: false,
  265. );
  266. expect(text, findsOneWidget);
  267. }
  268. Future<void> selectDay({
  269. required int content,
  270. }) async {
  271. final findCalendar = find.byType(TableCalendar);
  272. final findDay = find.text(content.toString());
  273. final finder = find.descendant(
  274. of: findCalendar,
  275. matching: findDay,
  276. );
  277. // if the day is very near the beginning or the end of the month,
  278. // it may overlap with the same day in the next or previous month,
  279. // respectively because it was spilling over. This will lead to 2
  280. // widgets being found and thus cannot be tapped correctly.
  281. if (content < 15) {
  282. // e.g., Jan 2 instead of Feb 2
  283. await tapButton(finder.first);
  284. } else {
  285. // e.g. Jun 28 instead of May 28
  286. await tapButton(finder.last);
  287. }
  288. }
  289. Future<void> toggleIncludeTime() async {
  290. final findDateEditor = find.byType(DateCellEditor);
  291. final findToggle = find.byType(Toggle);
  292. final finder = find.descendant(
  293. of: findDateEditor,
  294. matching: findToggle,
  295. );
  296. await tapButton(finder);
  297. }
  298. Future<void> changeDateFormat() async {
  299. final findDateEditor = find.byType(DateCellEditor);
  300. final findDateTimeOptionButton = find.byType(DateTypeOptionButton);
  301. final finder = find.descendant(
  302. of: findDateEditor,
  303. matching: findDateTimeOptionButton,
  304. );
  305. await tapButton(finder);
  306. final findDateFormatButton = find.byType(DateFormatButton);
  307. await tapButton(findDateFormatButton);
  308. final findNewDateFormat = find.text("Day/Month/Year");
  309. await tapButton(findNewDateFormat);
  310. }
  311. Future<void> changeTimeFormat() async {
  312. final findDateEditor = find.byType(DateCellEditor);
  313. final findDateTimeOptionButton = find.byType(DateTypeOptionButton);
  314. final finder = find.descendant(
  315. of: findDateEditor,
  316. matching: findDateTimeOptionButton,
  317. );
  318. await tapButton(finder);
  319. final findDateFormatButton = find.byType(TimeFormatButton);
  320. await tapButton(findDateFormatButton);
  321. final findNewDateFormat = find.text("12 hour");
  322. await tapButton(findNewDateFormat);
  323. }
  324. Future<void> clearDate() async {
  325. final findDateEditor = find.byType(DateCellEditor);
  326. final findClearButton = find.byType(ClearDateButton);
  327. final finder = find.descendant(
  328. of: findDateEditor,
  329. matching: findClearButton,
  330. );
  331. await tapButton(finder);
  332. }
  333. Future<void> tapSelectOptionCellInGrid({
  334. required int rowIndex,
  335. required FieldType fieldType,
  336. }) async {
  337. assert(
  338. fieldType == FieldType.SingleSelect || fieldType == FieldType.MultiSelect,
  339. );
  340. final findRow = find.byType(GridRow);
  341. final findCell = finderForFieldType(fieldType);
  342. final cell = find.descendant(
  343. of: findRow.at(rowIndex),
  344. matching: findCell,
  345. );
  346. await tapButton(cell);
  347. }
  348. /// The [SelectOptionCellEditor] must be opened first.
  349. Future<void> createOption({
  350. required String name,
  351. }) async {
  352. final findEditor = find.byType(SelectOptionCellEditor);
  353. expect(findEditor, findsOneWidget);
  354. final findTextField = find.byType(SelectOptionTextField);
  355. expect(findTextField, findsOneWidget);
  356. await enterText(findTextField, name);
  357. await pump();
  358. await testTextInput.receiveAction(TextInputAction.done);
  359. await pumpAndSettle();
  360. }
  361. Future<void> selectOption({
  362. required String name,
  363. }) async {
  364. final option = find.byWidgetPredicate(
  365. (widget) => widget is SelectOptionTagCell && widget.option.name == name,
  366. );
  367. await tapButton(option);
  368. }
  369. Future<void> findSelectOptionWithNameInGrid({
  370. required int rowIndex,
  371. required String name,
  372. }) async {
  373. final findRow = find.byType(GridRow);
  374. final option = find.byWidgetPredicate(
  375. (widget) => widget is SelectOptionTag && widget.name == name,
  376. );
  377. final cell = find.descendant(
  378. of: findRow.at(rowIndex),
  379. matching: option,
  380. );
  381. expect(cell, findsOneWidget);
  382. }
  383. Future<void> assertNumberOfSelectedOptionsInGrid({
  384. required int rowIndex,
  385. required Matcher matcher,
  386. }) async {
  387. final findRow = find.byType(GridRow);
  388. final options = find.byWidgetPredicate(
  389. (widget) => widget is SelectOptionTag,
  390. );
  391. final cell = find.descendant(
  392. of: findRow.at(rowIndex),
  393. matching: options,
  394. );
  395. expect(cell, matcher);
  396. }
  397. Future<void> openFirstRowDetailPage() async {
  398. await hoverOnFirstRowOfGrid();
  399. final expandButton = find.byType(PrimaryCellAccessory);
  400. expect(expandButton, findsOneWidget);
  401. await tapButton(expandButton);
  402. }
  403. void assertRowDetailPageOpened() async {
  404. final findRowDetailPage = find.byType(RowDetailPage);
  405. expect(findRowDetailPage, findsOneWidget);
  406. }
  407. Future<void> dismissRowDetailPage() async {
  408. // use tap empty area instead of clicking ESC to dismiss the row detail page
  409. // sometimes, the ESC key is not working.
  410. await simulateKeyEvent(LogicalKeyboardKey.escape);
  411. await pumpAndSettle();
  412. final findRowDetailPage = find.byType(RowDetailPage);
  413. if (findRowDetailPage.evaluate().isNotEmpty) {
  414. await tapAt(const Offset(0, 0));
  415. await pumpAndSettle();
  416. }
  417. }
  418. Future<void> editTitleInRowDetailPage(String title) async {
  419. final titleField = find.byType(GridTextCell);
  420. await enterText(titleField, title);
  421. await pumpAndSettle();
  422. }
  423. Future<void> hoverRowBanner() async {
  424. final banner = find.byType(RowBanner);
  425. expect(banner, findsOneWidget);
  426. await startGesture(
  427. getTopLeft(banner),
  428. kind: PointerDeviceKind.mouse,
  429. );
  430. await pumpAndSettle();
  431. }
  432. Future<void> openEmojiPicker() async {
  433. await tapButton(find.byType(EmojiPickerButton));
  434. await tapButton(find.byType(EmojiSelectionMenu));
  435. }
  436. Future<void> tapDateCellInRowDetailPage() async {
  437. final findDateCell = find.byType(GridDateCell);
  438. await tapButton(findDateCell);
  439. }
  440. Future<void> duplicateRowInRowDetailPage() async {
  441. final duplicateButton = find.byType(RowDetailPageDuplicateButton);
  442. await tapButton(duplicateButton);
  443. }
  444. Future<void> deleteRowInRowDetailPage() async {
  445. final deleteButton = find.byType(RowDetailPageDeleteButton);
  446. await tapButton(deleteButton);
  447. }
  448. Future<void> scrollGridByOffset(Offset offset) async {
  449. await drag(find.byType(GridPage), offset);
  450. await pumpAndSettle();
  451. }
  452. Future<void> scrollRowDetailByOffset(Offset offset) async {
  453. await drag(find.byType(RowDetailPage), offset);
  454. await pumpAndSettle();
  455. }
  456. Future<void> scrollToRight(Finder find) async {
  457. final size = getSize(find);
  458. await drag(find, Offset(-size.width, 0));
  459. await pumpAndSettle(const Duration(milliseconds: 500));
  460. }
  461. Future<void> tapNewPropertyButton() async {
  462. await tapButtonWithName(LocaleKeys.grid_field_newProperty.tr());
  463. await pumpAndSettle();
  464. }
  465. Future<void> tapGridFieldWithName(String name) async {
  466. final field = find.byWidgetPredicate(
  467. (widget) => widget is FieldCellButton && widget.field.name == name,
  468. );
  469. await tapButton(field);
  470. await pumpAndSettle();
  471. }
  472. /// Should call [tapGridFieldWithName] first.
  473. Future<void> tapEditPropertyButton() async {
  474. await tapButtonWithName(LocaleKeys.grid_field_editProperty.tr());
  475. await pumpAndSettle(const Duration(milliseconds: 200));
  476. }
  477. /// Should call [tapGridFieldWithName] first.
  478. Future<void> tapDeletePropertyButton() async {
  479. final field = find.byWidgetPredicate(
  480. (widget) =>
  481. widget is FieldActionCell && widget.action == FieldAction.delete,
  482. );
  483. await tapButton(field);
  484. }
  485. /// Should call [tapGridFieldWithName] first.
  486. Future<void> tapDialogOkButton() async {
  487. final field = find.byWidgetPredicate(
  488. (widget) =>
  489. widget is PrimaryTextButton &&
  490. widget.label == LocaleKeys.button_OK.tr(),
  491. );
  492. await tapButton(field);
  493. }
  494. /// Should call [tapGridFieldWithName] first.
  495. Future<void> tapDuplicatePropertyButton() async {
  496. final field = find.byWidgetPredicate(
  497. (widget) =>
  498. widget is FieldActionCell && widget.action == FieldAction.duplicate,
  499. );
  500. await tapButton(field);
  501. }
  502. /// Should call [tapGridFieldWithName] first.
  503. Future<void> tapHidePropertyButton() async {
  504. final field = find.byWidgetPredicate(
  505. (widget) =>
  506. widget is FieldActionCell && widget.action == FieldAction.hide,
  507. );
  508. await tapButton(field);
  509. }
  510. Future<void> tapRowDetailPageCreatePropertyButton() async {
  511. await tapButton(find.byType(CreateRowFieldButton));
  512. }
  513. Future<void> tapRowDetailPageDeleteRowButton() async {
  514. await tapButton(find.byType(RowDetailPageDeleteButton));
  515. }
  516. Future<void> tapRowDetailPageDuplicateRowButton() async {
  517. await tapButton(find.byType(RowDetailPageDuplicateButton));
  518. }
  519. Future<void> tapTypeOptionButton() async {
  520. await tapButton(find.byType(SwitchFieldButton));
  521. }
  522. Future<void> tapEscButton() async {
  523. await sendKeyEvent(LogicalKeyboardKey.escape);
  524. }
  525. /// Must call [tapTypeOptionButton] first.
  526. Future<void> selectFieldType(FieldType fieldType) async {
  527. final fieldTypeCell = find.byType(FieldTypeCell);
  528. final fieldTypeButton = find.descendant(
  529. of: fieldTypeCell,
  530. matching: find.byWidgetPredicate(
  531. (widget) => widget is FlowyText && widget.text == fieldType.title(),
  532. ),
  533. );
  534. await tapButton(fieldTypeButton);
  535. }
  536. /// Each field has its own cell, so we can find the corresponding cell by
  537. /// the field type after create a new field.
  538. Future<void> findCellByFieldType(FieldType fieldType) async {
  539. final finder = finderForFieldType(fieldType);
  540. expect(finder, findsWidgets);
  541. }
  542. Future<void> assertNumberOfFieldsInGridPage(int num) async {
  543. expect(find.byType(GridFieldCell), findsNWidgets(num));
  544. }
  545. Future<void> assertNumberOfRowsInGridPage(int num) async {
  546. expect(
  547. find.byType(GridRow, skipOffstage: false),
  548. findsNWidgets(num),
  549. );
  550. }
  551. Future<void> assertDocumentExistInRowDetailPage() async {
  552. expect(find.byType(RowDocument), findsOneWidget);
  553. }
  554. /// Check the field type of the [FieldCellButton] is the same as the name.
  555. Future<void> assertFieldTypeWithFieldName(
  556. String name,
  557. FieldType fieldType,
  558. ) async {
  559. final field = find.byWidgetPredicate(
  560. (widget) =>
  561. widget is FieldCellButton &&
  562. widget.field.fieldType == fieldType &&
  563. widget.field.name == name,
  564. );
  565. expect(field, findsOneWidget);
  566. }
  567. Future<void> findFieldWithName(String name) async {
  568. final field = find.byWidgetPredicate(
  569. (widget) => widget is FieldCellButton && widget.field.name == name,
  570. );
  571. expect(field, findsOneWidget);
  572. }
  573. Future<void> noFieldWithName(String name) async {
  574. final field = find.byWidgetPredicate(
  575. (widget) => widget is FieldCellButton && widget.field.name == name,
  576. );
  577. expect(field, findsNothing);
  578. }
  579. Future<void> renameField(String newName) async {
  580. final textField = find.byType(FieldNameTextField);
  581. expect(textField, findsOneWidget);
  582. await enterText(textField, newName);
  583. await pumpAndSettle();
  584. }
  585. Future<void> dismissFieldEditor() async {
  586. await sendKeyEvent(LogicalKeyboardKey.escape);
  587. await pumpAndSettle(const Duration(milliseconds: 200));
  588. }
  589. Future<void> findFieldEditor(dynamic matcher) async {
  590. final finder = find.byType(FieldEditor);
  591. expect(finder, matcher);
  592. }
  593. Future<void> findDateEditor(dynamic matcher) async {
  594. final finder = find.byType(DateCellEditor);
  595. expect(finder, matcher);
  596. }
  597. Future<void> findSelectOptionEditor(dynamic matcher) async {
  598. final finder = find.byType(SelectOptionCellEditor);
  599. expect(finder, matcher);
  600. }
  601. Future<void> dismissCellEditor() async {
  602. await sendKeyEvent(LogicalKeyboardKey.escape);
  603. await pumpAndSettle();
  604. }
  605. Future<void> tapCreateRowButtonInGrid() async {
  606. await tapButton(find.byType(GridAddRowButton));
  607. }
  608. Future<void> tapCreateRowButtonInRowMenuOfGrid() async {
  609. await tapButton(find.byType(InsertRowButton));
  610. }
  611. Future<void> tapRowMenuButtonInGrid() async {
  612. await tapButton(find.byType(RowMenuButton));
  613. }
  614. /// Should call [tapRowMenuButtonInGrid] first.
  615. Future<void> tapDeleteOnRowMenu() async {
  616. await tapButtonWithName(LocaleKeys.grid_row_delete.tr());
  617. }
  618. Future<void> assertRowCountInGridPage(int num) async {
  619. final text = find.byWidgetPredicate(
  620. (widget) => widget is FlowyText && widget.text == rowCountString(num),
  621. );
  622. expect(text, findsOneWidget);
  623. }
  624. Future<void> createField(FieldType fieldType, String name) async {
  625. await scrollToRight(find.byType(GridPage));
  626. await tapNewPropertyButton();
  627. await renameField(name);
  628. await tapTypeOptionButton();
  629. await selectFieldType(fieldType);
  630. await dismissFieldEditor();
  631. }
  632. Future<void> tapDatabaseSettingButton() async {
  633. await tapButton(find.byType(SettingButton));
  634. }
  635. Future<void> tapDatabaseFilterButton() async {
  636. await tapButton(find.byType(FilterButton));
  637. }
  638. Future<void> tapDatabaseSortButton() async {
  639. await tapButton(find.byType(SortButton));
  640. }
  641. Future<void> tapCreateFilterByFieldType(
  642. FieldType fieldType,
  643. String title,
  644. ) async {
  645. final findFilter = find.byWidgetPredicate(
  646. (widget) =>
  647. widget is GridFilterPropertyCell &&
  648. widget.fieldInfo.fieldType == fieldType &&
  649. widget.fieldInfo.name == title,
  650. );
  651. await tapButton(findFilter);
  652. }
  653. Future<void> tapFilterButtonInGrid(String filterName) async {
  654. final findFilter = find.byType(FilterMenuItem);
  655. final button = find.descendant(
  656. of: findFilter,
  657. matching: find.text(filterName),
  658. );
  659. await tapButton(button);
  660. }
  661. Future<void> tapCreateSortByFieldType(
  662. FieldType fieldType,
  663. String title,
  664. ) async {
  665. final findSort = find.byWidgetPredicate(
  666. (widget) =>
  667. widget is GridSortPropertyCell &&
  668. widget.fieldInfo.fieldType == fieldType &&
  669. widget.fieldInfo.name == title,
  670. );
  671. await tapButton(findSort);
  672. }
  673. // Must call [tapSortMenuInSettingBar] first.
  674. Future<void> tapCreateSortByFieldTypeInSortMenu(
  675. FieldType fieldType,
  676. String title,
  677. ) async {
  678. await tapButton(find.byType(DatabaseAddSortButton));
  679. final findSort = find.byWidgetPredicate(
  680. (widget) =>
  681. widget is GridSortPropertyCell &&
  682. widget.fieldInfo.fieldType == fieldType &&
  683. widget.fieldInfo.name == title,
  684. );
  685. await tapButton(findSort);
  686. await pumpAndSettle();
  687. }
  688. Future<void> tapSortMenuInSettingBar() async {
  689. await tapButton(find.byType(SortMenu));
  690. await pumpAndSettle();
  691. }
  692. /// Must call [tapSortMenuInSettingBar] first.
  693. Future<void> tapSortButtonByName(String name) async {
  694. final findSortItem = find.byWidgetPredicate(
  695. (widget) =>
  696. widget is DatabaseSortItem && widget.sortInfo.fieldInfo.name == name,
  697. );
  698. await tapButton(findSortItem);
  699. }
  700. /// Must call [tapSortButtonByName] first.
  701. Future<void> tapSortByDescending() async {
  702. await tapButton(
  703. find.descendant(
  704. of: find.byType(OrderPannelItem),
  705. matching: find.byWidgetPredicate(
  706. (widget) =>
  707. widget is FlowyText &&
  708. widget.text == LocaleKeys.grid_sort_descending.tr(),
  709. ),
  710. ),
  711. );
  712. await sendKeyEvent(LogicalKeyboardKey.escape);
  713. await pumpAndSettle();
  714. }
  715. /// Must call [tapSortMenuInSettingBar] first.
  716. Future<void> tapAllSortButton() async {
  717. await tapButton(find.byType(DatabaseDeleteSortButton));
  718. }
  719. Future<void> scrollOptionFilterListByOffset(Offset offset) async {
  720. await drag(find.byType(SelectOptionFilterEditor), offset);
  721. await pumpAndSettle();
  722. }
  723. Future<void> enterTextInTextFilter(String text) async {
  724. final findEditor = find.byType(TextFilterEditor);
  725. final findTextField = find.descendant(
  726. of: findEditor,
  727. matching: find.byType(FlowyTextField),
  728. );
  729. await enterText(findTextField, text);
  730. await pumpAndSettle(const Duration(milliseconds: 300));
  731. }
  732. Future<void> tapDisclosureButtonInFinder(Finder finder) async {
  733. final findDisclosure = find.descendant(
  734. of: finder,
  735. matching: find.byType(DisclosureButton),
  736. );
  737. await tapButton(findDisclosure);
  738. }
  739. /// must call [tapDisclosureButtonInFinder] first.
  740. Future<void> tapDeleteFilterButtonInGrid() async {
  741. await tapButton(find.text(LocaleKeys.grid_settings_deleteFilter.tr()));
  742. }
  743. Future<void> tapCheckboxFilterButtonInGrid() async {
  744. await tapButton(find.byType(CheckboxFilterConditionList));
  745. }
  746. Future<void> tapChecklistFilterButtonInGrid() async {
  747. await tapButton(find.byType(ChecklistFilterConditionList));
  748. }
  749. /// The [SelectOptionFilterList] must show up first.
  750. Future<void> tapOptionFilterWithName(String name) async {
  751. final findCell = find.descendant(
  752. of: find.byType(SelectOptionFilterList),
  753. matching: find.byWidgetPredicate(
  754. (widget) =>
  755. widget is SelectOptionFilterCell && widget.option.name == name,
  756. skipOffstage: false,
  757. ),
  758. skipOffstage: false,
  759. );
  760. expect(findCell, findsOneWidget);
  761. await tapButton(findCell, warnIfMissed: false);
  762. }
  763. Future<void> tapCheckedButtonOnCheckboxFilter() async {
  764. final button = find.descendant(
  765. of: find.byType(HoverButton),
  766. matching: find.text(LocaleKeys.grid_checkboxFilter_isChecked.tr()),
  767. );
  768. await tapButton(button);
  769. }
  770. Future<void> tapUnCheckedButtonOnCheckboxFilter() async {
  771. final button = find.descendant(
  772. of: find.byType(HoverButton),
  773. matching: find.text(LocaleKeys.grid_checkboxFilter_isUnchecked.tr()),
  774. );
  775. await tapButton(button);
  776. }
  777. Future<void> tapCompletedButtonOnChecklistFilter() async {
  778. final button = find.descendant(
  779. of: find.byType(HoverButton),
  780. matching: find.text(LocaleKeys.grid_checklistFilter_isComplete.tr()),
  781. );
  782. await tapButton(button);
  783. }
  784. Future<void> tapUnCompletedButtonOnChecklistFilter() async {
  785. final button = find.descendant(
  786. of: find.byType(HoverButton),
  787. matching: find.text(LocaleKeys.grid_checklistFilter_isIncomplted.tr()),
  788. );
  789. await tapButton(button);
  790. }
  791. /// Should call [tapDatabaseSettingButton] first.
  792. Future<void> tapDatabaseLayoutButton() async {
  793. final findSettingItem = find.byType(DatabaseSettingItem);
  794. final findLayoutButton = find.byWidgetPredicate(
  795. (widget) =>
  796. widget is FlowyText &&
  797. widget.text == DatabaseSettingAction.showLayout.title(),
  798. );
  799. final button = find.descendant(
  800. of: findSettingItem,
  801. matching: findLayoutButton,
  802. );
  803. await tapButton(button);
  804. }
  805. Future<void> tapCalendarLayoutSettingButton() async {
  806. final findSettingItem = find.byType(DatabaseSettingItem);
  807. final findLayoutButton = find.byWidgetPredicate(
  808. (widget) =>
  809. widget is FlowyText &&
  810. widget.text == DatabaseSettingAction.showCalendarLayout.title(),
  811. );
  812. final button = find.descendant(
  813. of: findSettingItem,
  814. matching: findLayoutButton,
  815. );
  816. await tapButton(button);
  817. }
  818. Future<void> tapFirstDayOfWeek() async {
  819. await tapButton(find.byType(FirstDayOfWeek));
  820. }
  821. Future<void> tapFirstDayOfWeekStartFromSunday() async {
  822. final finder = find.byWidgetPredicate(
  823. (widget) => widget is StartFromButton && widget.dayIndex == 0,
  824. );
  825. await tapButton(finder);
  826. }
  827. Future<void> tapFirstDayOfWeekStartFromMonday() async {
  828. final finder = find.byWidgetPredicate(
  829. (widget) => widget is StartFromButton && widget.dayIndex == 1,
  830. );
  831. await tapButton(finder);
  832. // Dismiss the popover overlay in cause of obscure the tapButton
  833. // in the next test case.
  834. await sendKeyEvent(LogicalKeyboardKey.escape);
  835. await pumpAndSettle(const Duration(milliseconds: 200));
  836. }
  837. void assertFirstDayOfWeekStartFromMonday() {
  838. final finder = find.byWidgetPredicate(
  839. (widget) =>
  840. widget is StartFromButton &&
  841. widget.dayIndex == 1 &&
  842. widget.isSelected == true,
  843. );
  844. expect(finder, findsOneWidget);
  845. }
  846. void assertFirstDayOfWeekStartFromSunday() {
  847. final finder = find.byWidgetPredicate(
  848. (widget) =>
  849. widget is StartFromButton &&
  850. widget.dayIndex == 0 &&
  851. widget.isSelected == true,
  852. );
  853. expect(finder, findsOneWidget);
  854. }
  855. Future<void> scrollToToday() async {
  856. final todayCell = find.byWidgetPredicate(
  857. (widget) => widget is CalendarDayCard && widget.isToday,
  858. );
  859. final scrollable = find
  860. .descendant(
  861. of: find.byType(MonthView<CalendarDayEvent>),
  862. matching: find.byWidgetPredicate(
  863. (widget) => widget is Scrollable && widget.axis == Axis.vertical,
  864. ),
  865. )
  866. .first;
  867. await scrollUntilVisible(
  868. todayCell,
  869. 300,
  870. scrollable: scrollable,
  871. );
  872. await pumpAndSettle(const Duration(milliseconds: 300));
  873. }
  874. Future<void> hoverOnTodayCalendarCell() async {
  875. final todayCell = find.byWidgetPredicate(
  876. (widget) => widget is CalendarDayCard && widget.isToday,
  877. );
  878. await hoverOnWidget(todayCell);
  879. }
  880. Future<void> tapAddCalendarEventButton() async {
  881. final findFlowyButton = find.byType(FlowyIconButton);
  882. final findNewEventButton = find.byType(NewEventButton);
  883. final button = find.descendant(
  884. of: findNewEventButton,
  885. matching: findFlowyButton,
  886. );
  887. await tapButton(button);
  888. }
  889. /// Checks for a certain number of events. Parameters [date] and [title] can
  890. /// also be provided to restrict the scope of the search
  891. void assertNumberOfEventsInCalendar(int number, {String? title}) {
  892. Finder findEvents = find.byType(EventCard);
  893. if (title != null) {
  894. findEvents = find.descendant(of: findEvents, matching: find.text(title));
  895. }
  896. expect(findEvents, findsNWidgets(number));
  897. }
  898. void assertNumberOfEventsOnSpecificDay(
  899. int number,
  900. DateTime date, {
  901. String? title,
  902. }) {
  903. final findDayCell = find.byWidgetPredicate(
  904. (widget) =>
  905. widget is CalendarDayCard &&
  906. isSameDay(
  907. widget.date,
  908. date,
  909. ),
  910. );
  911. Finder findEvents = find.descendant(
  912. of: findDayCell,
  913. matching: find.byType(EventCard),
  914. );
  915. if (title != null) {
  916. findEvents = find.descendant(of: findEvents, matching: find.text(title));
  917. }
  918. expect(findEvents, findsNWidgets(number));
  919. }
  920. Future<void> doubleClickCalendarCell(DateTime date) async {
  921. final todayCell = find.byWidgetPredicate(
  922. (widget) => widget is CalendarDayCard && isSameDay(date, widget.date),
  923. );
  924. final location = getTopLeft(todayCell).translate(10, 10);
  925. await doubleTapAt(location);
  926. }
  927. Future<void> openCalendarEvent({required index, DateTime? date}) async {
  928. final findDayCell = find.byWidgetPredicate(
  929. (widget) =>
  930. widget is CalendarDayCard &&
  931. isSameDay(widget.date, date ?? DateTime.now()),
  932. );
  933. final cards = find.descendant(
  934. of: findDayCell,
  935. matching: find.byType(EventCard),
  936. );
  937. await tapButton(cards.at(index));
  938. }
  939. Future<void> dragDropRescheduleCalendarEvent(DateTime startDate) async {
  940. final findEventCard = find.byType(EventCard);
  941. await drag(findEventCard.first, const Offset(0, 300));
  942. await pumpAndSettle();
  943. }
  944. Future<void> tapCreateLinkedDatabaseViewButton(AddButtonAction action) async {
  945. final findAddButton = find.byType(AddDatabaseViewButton);
  946. await tapButton(findAddButton);
  947. final findCreateButton = find.byWidgetPredicate(
  948. (widget) =>
  949. widget is TarBarAddButtonActionCell && widget.action == action,
  950. );
  951. await tapButton(findCreateButton);
  952. }
  953. Finder findTabBarLinkViewByViewLayout(ViewLayoutPB layout) {
  954. return find.byWidgetPredicate(
  955. (widget) => widget is TabBarItemButton && widget.view.layout == layout,
  956. );
  957. }
  958. Finder findTabBarLinkViewByViewName(String name) {
  959. return find.byWidgetPredicate(
  960. (widget) => widget is TabBarItemButton && widget.view.name == name,
  961. );
  962. }
  963. Future<void> renameLinkedView(Finder linkedView, String name) async {
  964. await tap(linkedView, buttons: kSecondaryButton);
  965. await pumpAndSettle();
  966. await tapButton(
  967. find.byWidgetPredicate(
  968. (widget) =>
  969. widget is ActionCellWidget &&
  970. widget.action == TabBarViewAction.rename,
  971. ),
  972. );
  973. await enterText(
  974. find.descendant(
  975. of: find.byType(FlowyFormTextInput),
  976. matching: find.byType(TextFormField),
  977. ),
  978. name,
  979. );
  980. final field = find.byWidgetPredicate(
  981. (widget) =>
  982. widget is PrimaryTextButton &&
  983. widget.label == LocaleKeys.button_OK.tr(),
  984. );
  985. await tapButton(field);
  986. }
  987. Future<void> deleteDatebaseView(Finder linkedView) async {
  988. await tap(linkedView, buttons: kSecondaryButton);
  989. await pumpAndSettle();
  990. await tapButton(
  991. find.byWidgetPredicate(
  992. (widget) =>
  993. widget is ActionCellWidget &&
  994. widget.action == TabBarViewAction.delete,
  995. ),
  996. );
  997. final okButton = find.byWidgetPredicate(
  998. (widget) =>
  999. widget is PrimaryTextButton &&
  1000. widget.label == LocaleKeys.button_OK.tr(),
  1001. );
  1002. await tapButton(okButton);
  1003. }
  1004. Future<void> assertCurrentDatabaseTagIs(DatabaseLayoutPB layout) async {
  1005. switch (layout) {
  1006. case DatabaseLayoutPB.Board:
  1007. expect(find.byType(BoardPage), findsOneWidget);
  1008. break;
  1009. case DatabaseLayoutPB.Calendar:
  1010. expect(find.byType(CalendarPage), findsOneWidget);
  1011. break;
  1012. case DatabaseLayoutPB.Grid:
  1013. expect(find.byType(GridPage), findsOneWidget);
  1014. break;
  1015. default:
  1016. throw Exception('Unknown database layout type: $layout');
  1017. }
  1018. }
  1019. Future<void> selectDatabaseLayoutType(DatabaseLayoutPB layout) async {
  1020. final findLayoutCell = find.byType(DatabaseViewLayoutCell);
  1021. final findText = find.byWidgetPredicate(
  1022. (widget) => widget is FlowyText && widget.text == layout.layoutName(),
  1023. );
  1024. final button = find.descendant(
  1025. of: findLayoutCell,
  1026. matching: findText,
  1027. );
  1028. await tapButton(button);
  1029. }
  1030. Future<void> assertCurrentDatabaseLayoutType(DatabaseLayoutPB layout) async {
  1031. expect(finderForDatabaseLayoutType(layout), findsOneWidget);
  1032. }
  1033. Future<void> tapDatabaseRawDataButton() async {
  1034. await tapButtonWithName(LocaleKeys.importPanel_database.tr());
  1035. }
  1036. Future<void> tapAddSelectOptionButton() async {
  1037. await tapButtonWithName(LocaleKeys.grid_field_addSelectOption.tr());
  1038. }
  1039. }
  1040. Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) {
  1041. switch (layout) {
  1042. case DatabaseLayoutPB.Board:
  1043. return find.byType(BoardPage);
  1044. case DatabaseLayoutPB.Calendar:
  1045. return find.byType(CalendarPage);
  1046. case DatabaseLayoutPB.Grid:
  1047. return find.byType(GridPage);
  1048. default:
  1049. throw Exception('Unknown database layout type: $layout');
  1050. }
  1051. }
  1052. Finder finderForFieldType(FieldType fieldType) {
  1053. switch (fieldType) {
  1054. case FieldType.Checkbox:
  1055. return find.byType(GridCheckboxCell, skipOffstage: false);
  1056. case FieldType.DateTime:
  1057. return find.byType(GridDateCell, skipOffstage: false);
  1058. case FieldType.LastEditedTime:
  1059. case FieldType.CreatedTime:
  1060. return find.byType(GridDateCell, skipOffstage: false);
  1061. case FieldType.SingleSelect:
  1062. return find.byType(GridSingleSelectCell, skipOffstage: false);
  1063. case FieldType.MultiSelect:
  1064. return find.byType(GridMultiSelectCell, skipOffstage: false);
  1065. case FieldType.Checklist:
  1066. return find.byType(GridChecklistCell, skipOffstage: false);
  1067. case FieldType.Number:
  1068. return find.byType(GridNumberCell, skipOffstage: false);
  1069. case FieldType.RichText:
  1070. return find.byType(GridTextCell, skipOffstage: false);
  1071. case FieldType.URL:
  1072. return find.byType(GridURLCell, skipOffstage: false);
  1073. default:
  1074. throw Exception('Unknown field type: $fieldType');
  1075. }
  1076. }