database_test_op.dart 39 KB

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