database_test_op.dart 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239
  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/widgets/filter/choicechip/checkbox.dart';
  9. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/checklist/checklist.dart';
  10. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart';
  11. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart';
  12. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/text.dart';
  13. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/create_filter_list.dart';
  14. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/disclosure_button.dart';
  15. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_menu_item.dart';
  16. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart';
  17. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
  18. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_list.dart';
  19. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart';
  20. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/create_sort_list.dart';
  21. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/order_panel.dart';
  22. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_editor.dart';
  23. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_menu.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/toolbar/filter_button.dart';
  26. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart';
  27. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/sort_button.dart';
  28. import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
  29. import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
  30. import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart';
  31. import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
  32. import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart';
  33. import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/text_field.dart';
  34. import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart';
  35. import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart';
  36. import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart';
  37. import 'package:appflowy/plugins/database_view/widgets/setting/database_setting.dart';
  38. import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
  39. import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
  40. import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
  41. import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
  42. import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
  43. import 'package:calendar_view/calendar_view.dart';
  44. import 'package:easy_localization/easy_localization.dart';
  45. import 'package:flowy_infra_ui/flowy_infra_ui.dart';
  46. import 'package:flowy_infra_ui/style_widget/text_input.dart';
  47. import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
  48. import 'package:flutter/gestures.dart';
  49. import 'package:flutter/material.dart';
  50. import 'package:flutter/services.dart';
  51. import 'package:flutter_test/flutter_test.dart';
  52. import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
  53. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/footer/grid_footer.dart';
  54. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell.dart';
  55. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
  56. import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/row.dart';
  57. import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart';
  58. import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
  59. import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart';
  60. import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart';
  61. import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
  62. import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_menu_item.dart';
  63. import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
  64. import 'package:table_calendar/table_calendar.dart';
  65. import 'base.dart';
  66. import 'common_operations.dart';
  67. import 'expectation.dart';
  68. import 'package:path/path.dart' as p;
  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> tapSelectOptionCellInGrid({
  323. required int rowIndex,
  324. required FieldType fieldType,
  325. }) async {
  326. assert(
  327. fieldType == FieldType.SingleSelect || fieldType == FieldType.MultiSelect,
  328. );
  329. final findRow = find.byType(GridRow);
  330. final findCell = finderForFieldType(fieldType);
  331. final cell = find.descendant(
  332. of: findRow.at(rowIndex),
  333. matching: findCell,
  334. );
  335. await tapButton(cell);
  336. }
  337. /// The [SelectOptionCellEditor] must be opened first.
  338. Future<void> createOption({
  339. required String name,
  340. }) async {
  341. final findEditor = find.byType(SelectOptionCellEditor);
  342. expect(findEditor, findsOneWidget);
  343. final findTextField = find.byType(SelectOptionTextField);
  344. expect(findTextField, findsOneWidget);
  345. await enterText(findTextField, name);
  346. await pump();
  347. await testTextInput.receiveAction(TextInputAction.done);
  348. await pumpAndSettle();
  349. }
  350. Future<void> selectOption({
  351. required String name,
  352. }) async {
  353. final option = find.byWidgetPredicate(
  354. (widget) => widget is SelectOptionTagCell && widget.option.name == name,
  355. );
  356. await tapButton(option);
  357. }
  358. Future<void> findSelectOptionWithNameInGrid({
  359. required int rowIndex,
  360. required String name,
  361. }) async {
  362. final findRow = find.byType(GridRow);
  363. final option = find.byWidgetPredicate(
  364. (widget) => widget is SelectOptionTag && widget.name == name,
  365. );
  366. final cell = find.descendant(
  367. of: findRow.at(rowIndex),
  368. matching: option,
  369. );
  370. expect(cell, findsOneWidget);
  371. }
  372. Future<void> assertNumberOfSelectedOptionsInGrid({
  373. required int rowIndex,
  374. required Matcher matcher,
  375. }) async {
  376. final findRow = find.byType(GridRow);
  377. final options = find.byWidgetPredicate(
  378. (widget) => widget is SelectOptionTag,
  379. );
  380. final cell = find.descendant(
  381. of: findRow.at(rowIndex),
  382. matching: options,
  383. );
  384. expect(cell, matcher);
  385. }
  386. Future<void> openFirstRowDetailPage() async {
  387. await hoverOnFirstRowOfGrid();
  388. final expandButton = find.byType(PrimaryCellAccessory);
  389. expect(expandButton, findsOneWidget);
  390. await tapButton(expandButton);
  391. }
  392. void assertRowDetailPageOpened() async {
  393. final findRowDetailPage = find.byType(RowDetailPage);
  394. expect(findRowDetailPage, findsOneWidget);
  395. }
  396. Future<void> dismissRowDetailPage() async {
  397. // use tap empty area instead of clicking ESC to dismiss the row detail page
  398. // sometimes, the ESC key is not working.
  399. await simulateKeyEvent(LogicalKeyboardKey.escape);
  400. await pumpAndSettle();
  401. final findRowDetailPage = find.byType(RowDetailPage);
  402. if (findRowDetailPage.evaluate().isNotEmpty) {
  403. await tapAt(const Offset(0, 0));
  404. await pumpAndSettle();
  405. }
  406. }
  407. Future<void> editTitleInRowDetailPage(String title) async {
  408. final titleField = find.byType(GridTextCell);
  409. await enterText(titleField, title);
  410. await pumpAndSettle();
  411. }
  412. Future<void> hoverRowBanner() async {
  413. final banner = find.byType(RowBanner);
  414. expect(banner, findsOneWidget);
  415. await startGesture(
  416. getTopLeft(banner),
  417. kind: PointerDeviceKind.mouse,
  418. );
  419. await pumpAndSettle();
  420. }
  421. Future<void> openEmojiPicker() async {
  422. await tapButton(find.byType(EmojiPickerButton));
  423. await tapButton(find.byType(EmojiSelectionMenu));
  424. }
  425. Future<void> tapDateCellInRowDetailPage() async {
  426. final findDateCell = find.byType(GridDateCell);
  427. await tapButton(findDateCell);
  428. }
  429. Future<void> duplicateRowInRowDetailPage() async {
  430. final duplicateButton = find.byType(RowDetailPageDuplicateButton);
  431. await tapButton(duplicateButton);
  432. }
  433. Future<void> deleteRowInRowDetailPage() async {
  434. final deleteButton = find.byType(RowDetailPageDeleteButton);
  435. await tapButton(deleteButton);
  436. }
  437. Future<void> scrollGridByOffset(Offset offset) async {
  438. await drag(find.byType(GridPage), offset);
  439. await pumpAndSettle();
  440. }
  441. Future<void> scrollRowDetailByOffset(Offset offset) async {
  442. await drag(find.byType(RowDetailPage), offset);
  443. await pumpAndSettle();
  444. }
  445. Future<void> scrollToRight(Finder find) async {
  446. final size = getSize(find);
  447. await drag(find, Offset(-size.width, 0));
  448. await pumpAndSettle(const Duration(milliseconds: 500));
  449. }
  450. Future<void> tapNewPropertyButton() async {
  451. await tapButtonWithName(LocaleKeys.grid_field_newProperty.tr());
  452. await pumpAndSettle();
  453. }
  454. Future<void> tapGridFieldWithName(String name) async {
  455. final field = find.byWidgetPredicate(
  456. (widget) => widget is FieldCellButton && widget.field.name == name,
  457. );
  458. await tapButton(field);
  459. await pumpAndSettle();
  460. }
  461. /// Should call [tapGridFieldWithName] first.
  462. Future<void> tapEditPropertyButton() async {
  463. await tapButtonWithName(LocaleKeys.grid_field_editProperty.tr());
  464. await pumpAndSettle(const Duration(milliseconds: 200));
  465. }
  466. /// Should call [tapGridFieldWithName] first.
  467. Future<void> tapDeletePropertyButton() async {
  468. final field = find.byWidgetPredicate(
  469. (widget) =>
  470. widget is FieldActionCell && widget.action == FieldAction.delete,
  471. );
  472. await tapButton(field);
  473. }
  474. /// Should call [tapGridFieldWithName] first.
  475. Future<void> tapDialogOkButton() async {
  476. final field = find.byWidgetPredicate(
  477. (widget) =>
  478. widget is PrimaryTextButton &&
  479. widget.label == LocaleKeys.button_OK.tr(),
  480. );
  481. await tapButton(field);
  482. }
  483. /// Should call [tapGridFieldWithName] first.
  484. Future<void> tapDuplicatePropertyButton() async {
  485. final field = find.byWidgetPredicate(
  486. (widget) =>
  487. widget is FieldActionCell && widget.action == FieldAction.duplicate,
  488. );
  489. await tapButton(field);
  490. }
  491. /// Should call [tapGridFieldWithName] first.
  492. Future<void> tapHidePropertyButton() async {
  493. final field = find.byWidgetPredicate(
  494. (widget) =>
  495. widget is FieldActionCell && widget.action == FieldAction.hide,
  496. );
  497. await tapButton(field);
  498. }
  499. Future<void> tapRowDetailPageCreatePropertyButton() async {
  500. await tapButton(find.byType(CreateRowFieldButton));
  501. }
  502. Future<void> tapRowDetailPageDeleteRowButton() async {
  503. await tapButton(find.byType(RowDetailPageDeleteButton));
  504. }
  505. Future<void> tapRowDetailPageDuplicateRowButton() async {
  506. await tapButton(find.byType(RowDetailPageDuplicateButton));
  507. }
  508. Future<void> tapTypeOptionButton() async {
  509. await tapButton(find.byType(SwitchFieldButton));
  510. }
  511. Future<void> tapEscButton() async {
  512. await sendKeyEvent(LogicalKeyboardKey.escape);
  513. }
  514. /// Must call [tapTypeOptionButton] first.
  515. Future<void> selectFieldType(FieldType fieldType) async {
  516. final fieldTypeCell = find.byType(FieldTypeCell);
  517. final fieldTypeButton = find.descendant(
  518. of: fieldTypeCell,
  519. matching: find.byWidgetPredicate(
  520. (widget) => widget is FlowyText && widget.text == fieldType.title(),
  521. ),
  522. );
  523. await tapButton(fieldTypeButton);
  524. }
  525. /// Each field has its own cell, so we can find the corresponding cell by
  526. /// the field type after create a new field.
  527. Future<void> findCellByFieldType(FieldType fieldType) async {
  528. final finder = finderForFieldType(fieldType);
  529. expect(finder, findsWidgets);
  530. }
  531. Future<void> assertNumberOfFieldsInGridPage(int num) async {
  532. expect(find.byType(GridFieldCell), findsNWidgets(num));
  533. }
  534. Future<void> assertNumberOfRowsInGridPage(int num) async {
  535. expect(
  536. find.byType(GridRow, skipOffstage: false),
  537. findsNWidgets(num),
  538. );
  539. }
  540. Future<void> assertDocumentExistInRowDetailPage() async {
  541. expect(find.byType(RowDocument), findsOneWidget);
  542. }
  543. /// Check the field type of the [FieldCellButton] is the same as the name.
  544. Future<void> assertFieldTypeWithFieldName(
  545. String name,
  546. FieldType fieldType,
  547. ) async {
  548. final field = find.byWidgetPredicate(
  549. (widget) =>
  550. widget is FieldCellButton &&
  551. widget.field.fieldType == fieldType &&
  552. widget.field.name == name,
  553. );
  554. expect(field, findsOneWidget);
  555. }
  556. Future<void> findFieldWithName(String name) async {
  557. final field = find.byWidgetPredicate(
  558. (widget) => widget is FieldCellButton && widget.field.name == name,
  559. );
  560. expect(field, findsOneWidget);
  561. }
  562. Future<void> noFieldWithName(String name) async {
  563. final field = find.byWidgetPredicate(
  564. (widget) => widget is FieldCellButton && widget.field.name == name,
  565. );
  566. expect(field, findsNothing);
  567. }
  568. Future<void> renameField(String newName) async {
  569. final textField = find.byType(FieldNameTextField);
  570. expect(textField, findsOneWidget);
  571. await enterText(textField, newName);
  572. await pumpAndSettle();
  573. }
  574. Future<void> dismissFieldEditor() async {
  575. await sendKeyEvent(LogicalKeyboardKey.escape);
  576. await pumpAndSettle(const Duration(milliseconds: 200));
  577. }
  578. Future<void> findFieldEditor(dynamic matcher) async {
  579. final finder = find.byType(FieldEditor);
  580. expect(finder, matcher);
  581. }
  582. Future<void> findDateEditor(dynamic matcher) async {
  583. final finder = find.byType(DateCellEditor);
  584. expect(finder, matcher);
  585. }
  586. Future<void> findSelectOptionEditor(dynamic matcher) async {
  587. final finder = find.byType(SelectOptionCellEditor);
  588. expect(finder, matcher);
  589. }
  590. Future<void> dismissCellEditor() async {
  591. await sendKeyEvent(LogicalKeyboardKey.escape);
  592. await pumpAndSettle();
  593. }
  594. Future<void> tapCreateRowButtonInGrid() async {
  595. await tapButton(find.byType(GridAddRowButton));
  596. }
  597. Future<void> tapCreateRowButtonInRowMenuOfGrid() async {
  598. await tapButton(find.byType(InsertRowButton));
  599. }
  600. Future<void> tapRowMenuButtonInGrid() async {
  601. await tapButton(find.byType(RowMenuButton));
  602. }
  603. /// Should call [tapRowMenuButtonInGrid] first.
  604. Future<void> tapDeleteOnRowMenu() async {
  605. await tapButtonWithName(LocaleKeys.grid_row_delete.tr());
  606. }
  607. Future<void> assertRowCountInGridPage(int num) async {
  608. final text = find.byWidgetPredicate(
  609. (widget) => widget is FlowyText && widget.text == rowCountString(num),
  610. );
  611. expect(text, findsOneWidget);
  612. }
  613. Future<void> createField(FieldType fieldType, String name) async {
  614. await scrollToRight(find.byType(GridPage));
  615. await tapNewPropertyButton();
  616. await renameField(name);
  617. await tapTypeOptionButton();
  618. await selectFieldType(fieldType);
  619. await dismissFieldEditor();
  620. }
  621. Future<void> tapDatabaseSettingButton() async {
  622. await tapButton(find.byType(SettingButton));
  623. }
  624. Future<void> tapDatabaseFilterButton() async {
  625. await tapButton(find.byType(FilterButton));
  626. }
  627. Future<void> tapDatabaseSortButton() async {
  628. await tapButton(find.byType(SortButton));
  629. }
  630. Future<void> tapCreateFilterByFieldType(
  631. FieldType fieldType,
  632. String title,
  633. ) async {
  634. final findFilter = find.byWidgetPredicate(
  635. (widget) =>
  636. widget is GridFilterPropertyCell &&
  637. widget.fieldInfo.fieldType == fieldType &&
  638. widget.fieldInfo.name == title,
  639. );
  640. await tapButton(findFilter);
  641. }
  642. Future<void> tapFilterButtonInGrid(String filterName) async {
  643. final findFilter = find.byType(FilterMenuItem);
  644. final button = find.descendant(
  645. of: findFilter,
  646. matching: find.text(filterName),
  647. );
  648. await tapButton(button);
  649. }
  650. Future<void> tapCreateSortByFieldType(
  651. FieldType fieldType,
  652. String title,
  653. ) async {
  654. final findSort = find.byWidgetPredicate(
  655. (widget) =>
  656. widget is GridSortPropertyCell &&
  657. widget.fieldInfo.fieldType == fieldType &&
  658. widget.fieldInfo.name == title,
  659. );
  660. await tapButton(findSort);
  661. }
  662. // Must call [tapSortMenuInSettingBar] first.
  663. Future<void> tapCreateSortByFieldTypeInSortMenu(
  664. FieldType fieldType,
  665. String title,
  666. ) async {
  667. await tapButton(find.byType(DatabaseAddSortButton));
  668. final findSort = find.byWidgetPredicate(
  669. (widget) =>
  670. widget is GridSortPropertyCell &&
  671. widget.fieldInfo.fieldType == fieldType &&
  672. widget.fieldInfo.name == title,
  673. );
  674. await tapButton(findSort);
  675. await pumpAndSettle();
  676. }
  677. Future<void> tapSortMenuInSettingBar() async {
  678. await tapButton(find.byType(SortMenu));
  679. await pumpAndSettle();
  680. }
  681. /// Must call [tapSortMenuInSettingBar] first.
  682. Future<void> tapSortButtonByName(String name) async {
  683. final findSortItem = find.byWidgetPredicate(
  684. (widget) =>
  685. widget is DatabaseSortItem && widget.sortInfo.fieldInfo.name == name,
  686. );
  687. await tapButton(findSortItem);
  688. }
  689. /// Must call [tapSortButtonByName] first.
  690. Future<void> tapSortByDescending() async {
  691. await tapButton(
  692. find.descendant(
  693. of: find.byType(OrderPannelItem),
  694. matching: find.byWidgetPredicate(
  695. (widget) =>
  696. widget is FlowyText &&
  697. widget.text == LocaleKeys.grid_sort_descending.tr(),
  698. ),
  699. ),
  700. );
  701. await sendKeyEvent(LogicalKeyboardKey.escape);
  702. await pumpAndSettle();
  703. }
  704. /// Must call [tapSortMenuInSettingBar] first.
  705. Future<void> tapAllSortButton() async {
  706. await tapButton(find.byType(DatabaseDeleteSortButton));
  707. }
  708. Future<void> scrollOptionFilterListByOffset(Offset offset) async {
  709. await drag(find.byType(SelectOptionFilterEditor), offset);
  710. await pumpAndSettle();
  711. }
  712. Future<void> enterTextInTextFilter(String text) async {
  713. final findEditor = find.byType(TextFilterEditor);
  714. final findTextField = find.descendant(
  715. of: findEditor,
  716. matching: find.byType(FlowyTextField),
  717. );
  718. await enterText(findTextField, text);
  719. await pumpAndSettle(const Duration(milliseconds: 300));
  720. }
  721. Future<void> tapDisclosureButtonInFinder(Finder finder) async {
  722. final findDisclosure = find.descendant(
  723. of: finder,
  724. matching: find.byType(DisclosureButton),
  725. );
  726. await tapButton(findDisclosure);
  727. }
  728. /// must call [tapDisclosureButtonInFinder] first.
  729. Future<void> tapDeleteFilterButtonInGrid() async {
  730. await tapButton(find.text(LocaleKeys.grid_settings_deleteFilter.tr()));
  731. }
  732. Future<void> tapCheckboxFilterButtonInGrid() async {
  733. await tapButton(find.byType(CheckboxFilterConditionList));
  734. }
  735. Future<void> tapChecklistFilterButtonInGrid() async {
  736. await tapButton(find.byType(ChecklistFilterConditionList));
  737. }
  738. /// The [SelectOptionFilterList] must show up first.
  739. Future<void> tapOptionFilterWithName(String name) async {
  740. final findCell = find.descendant(
  741. of: find.byType(SelectOptionFilterList),
  742. matching: find.byWidgetPredicate(
  743. (widget) =>
  744. widget is SelectOptionFilterCell && widget.option.name == name,
  745. skipOffstage: false,
  746. ),
  747. skipOffstage: false,
  748. );
  749. expect(findCell, findsOneWidget);
  750. await tapButton(findCell, warnIfMissed: false);
  751. }
  752. Future<void> tapCheckedButtonOnCheckboxFilter() async {
  753. final button = find.descendant(
  754. of: find.byType(HoverButton),
  755. matching: find.text(LocaleKeys.grid_checkboxFilter_isChecked.tr()),
  756. );
  757. await tapButton(button);
  758. }
  759. Future<void> tapUnCheckedButtonOnCheckboxFilter() async {
  760. final button = find.descendant(
  761. of: find.byType(HoverButton),
  762. matching: find.text(LocaleKeys.grid_checkboxFilter_isUnchecked.tr()),
  763. );
  764. await tapButton(button);
  765. }
  766. Future<void> tapCompletedButtonOnChecklistFilter() async {
  767. final button = find.descendant(
  768. of: find.byType(HoverButton),
  769. matching: find.text(LocaleKeys.grid_checklistFilter_isComplete.tr()),
  770. );
  771. await tapButton(button);
  772. }
  773. Future<void> tapUnCompletedButtonOnChecklistFilter() async {
  774. final button = find.descendant(
  775. of: find.byType(HoverButton),
  776. matching: find.text(LocaleKeys.grid_checklistFilter_isIncomplted.tr()),
  777. );
  778. await tapButton(button);
  779. }
  780. /// Should call [tapDatabaseSettingButton] first.
  781. Future<void> tapDatabaseLayoutButton() async {
  782. final findSettingItem = find.byType(DatabaseSettingItem);
  783. final findLayoutButton = find.byWidgetPredicate(
  784. (widget) =>
  785. widget is FlowyText &&
  786. widget.text == DatabaseSettingAction.showLayout.title(),
  787. );
  788. final button = find.descendant(
  789. of: findSettingItem,
  790. matching: findLayoutButton,
  791. );
  792. await tapButton(button);
  793. }
  794. Future<void> tapCalendarLayoutSettingButton() async {
  795. final findSettingItem = find.byType(DatabaseSettingItem);
  796. final findLayoutButton = find.byWidgetPredicate(
  797. (widget) =>
  798. widget is FlowyText &&
  799. widget.text == DatabaseSettingAction.showCalendarLayout.title(),
  800. );
  801. final button = find.descendant(
  802. of: findSettingItem,
  803. matching: findLayoutButton,
  804. );
  805. await tapButton(button);
  806. }
  807. Future<void> tapFirstDayOfWeek() async {
  808. await tapButton(find.byType(FirstDayOfWeek));
  809. }
  810. Future<void> tapFirstDayOfWeekStartFromSunday() async {
  811. final finder = find.byWidgetPredicate(
  812. (widget) => widget is StartFromButton && widget.dayIndex == 0,
  813. );
  814. await tapButton(finder);
  815. }
  816. Future<void> tapFirstDayOfWeekStartFromMonday() async {
  817. final finder = find.byWidgetPredicate(
  818. (widget) => widget is StartFromButton && widget.dayIndex == 1,
  819. );
  820. await tapButton(finder);
  821. // Dismiss the popover overlay in cause of obscure the tapButton
  822. // in the next test case.
  823. await sendKeyEvent(LogicalKeyboardKey.escape);
  824. await pumpAndSettle(const Duration(milliseconds: 200));
  825. }
  826. void assertFirstDayOfWeekStartFromMonday() {
  827. final finder = find.byWidgetPredicate(
  828. (widget) =>
  829. widget is StartFromButton &&
  830. widget.dayIndex == 1 &&
  831. widget.isSelected == true,
  832. );
  833. expect(finder, findsOneWidget);
  834. }
  835. void assertFirstDayOfWeekStartFromSunday() {
  836. final finder = find.byWidgetPredicate(
  837. (widget) =>
  838. widget is StartFromButton &&
  839. widget.dayIndex == 0 &&
  840. widget.isSelected == true,
  841. );
  842. expect(finder, findsOneWidget);
  843. }
  844. Future<void> scrollToToday() async {
  845. final todayCell = find.byWidgetPredicate(
  846. (widget) => widget is CalendarDayCard && widget.isToday,
  847. );
  848. final scrollable = find
  849. .descendant(
  850. of: find.byType(MonthView<CalendarDayEvent>),
  851. matching: find.byWidgetPredicate(
  852. (widget) => widget is Scrollable && widget.axis == Axis.vertical,
  853. ),
  854. )
  855. .first;
  856. await scrollUntilVisible(
  857. todayCell,
  858. 300,
  859. scrollable: scrollable,
  860. );
  861. await pumpAndSettle(const Duration(milliseconds: 300));
  862. }
  863. Future<void> hoverOnTodayCalendarCell() async {
  864. final todayCell = find.byWidgetPredicate(
  865. (widget) => widget is CalendarDayCard && widget.isToday,
  866. );
  867. await hoverOnWidget(todayCell);
  868. }
  869. Future<void> tapAddCalendarEventButton() async {
  870. final findFlowyButton = find.byType(FlowyIconButton);
  871. final findNewEventButton = find.byType(NewEventButton);
  872. final button = find.descendant(
  873. of: findNewEventButton,
  874. matching: findFlowyButton,
  875. );
  876. await tapButton(button);
  877. }
  878. /// Checks for a certain number of events. Parameters [date] and [title] can
  879. /// also be provided to restrict the scope of the search
  880. void assertNumberOfEventsInCalendar(int number, {String? title}) {
  881. Finder findEvents = find.byType(EventCard);
  882. if (title != null) {
  883. findEvents = find.descendant(of: findEvents, matching: find.text(title));
  884. }
  885. expect(findEvents, findsNWidgets(number));
  886. }
  887. void assertNumberOfEventsOnSpecificDay(
  888. int number,
  889. DateTime date, {
  890. String? title,
  891. }) {
  892. final findDayCell = find.byWidgetPredicate(
  893. (widget) =>
  894. widget is CalendarDayCard &&
  895. isSameDay(
  896. widget.date,
  897. date,
  898. ),
  899. );
  900. Finder findEvents = find.descendant(
  901. of: findDayCell,
  902. matching: find.byType(EventCard),
  903. );
  904. if (title != null) {
  905. findEvents = find.descendant(of: findEvents, matching: find.text(title));
  906. }
  907. expect(findEvents, findsNWidgets(number));
  908. }
  909. Future<void> doubleClickCalendarCell(DateTime date) async {
  910. final todayCell = find.byWidgetPredicate(
  911. (widget) => widget is CalendarDayCard && isSameDay(date, widget.date),
  912. );
  913. final location = getTopLeft(todayCell).translate(10, 10);
  914. await doubleTapAt(location);
  915. }
  916. Future<void> openCalendarEvent({required index, DateTime? date}) async {
  917. final findDayCell = find.byWidgetPredicate(
  918. (widget) =>
  919. widget is CalendarDayCard &&
  920. isSameDay(widget.date, date ?? DateTime.now()),
  921. );
  922. final cards = find.descendant(
  923. of: findDayCell,
  924. matching: find.byType(EventCard),
  925. );
  926. await tapButton(cards.at(index));
  927. }
  928. Future<void> dragDropRescheduleCalendarEvent(DateTime startDate) async {
  929. final findEventCard = find.byType(EventCard);
  930. await drag(findEventCard.first, const Offset(0, 300));
  931. await pumpAndSettle();
  932. }
  933. Future<void> tapCreateLinkedDatabaseViewButton(AddButtonAction action) async {
  934. final findAddButton = find.byType(AddDatabaseViewButton);
  935. await tapButton(findAddButton);
  936. final findCreateButton = find.byWidgetPredicate(
  937. (widget) =>
  938. widget is TarBarAddButtonActionCell && widget.action == action,
  939. );
  940. await tapButton(findCreateButton);
  941. }
  942. Finder findTabBarLinkViewByViewLayout(ViewLayoutPB layout) {
  943. return find.byWidgetPredicate(
  944. (widget) => widget is TabBarItemButton && widget.view.layout == layout,
  945. );
  946. }
  947. Finder findTabBarLinkViewByViewName(String name) {
  948. return find.byWidgetPredicate(
  949. (widget) => widget is TabBarItemButton && widget.view.name == name,
  950. );
  951. }
  952. Future<void> renameLinkedView(Finder linkedView, String name) async {
  953. await tap(linkedView, buttons: kSecondaryButton);
  954. await pumpAndSettle();
  955. await tapButton(
  956. find.byWidgetPredicate(
  957. (widget) =>
  958. widget is ActionCellWidget &&
  959. widget.action == TabBarViewAction.rename,
  960. ),
  961. );
  962. await enterText(
  963. find.descendant(
  964. of: find.byType(FlowyFormTextInput),
  965. matching: find.byType(TextFormField),
  966. ),
  967. name,
  968. );
  969. final field = find.byWidgetPredicate(
  970. (widget) =>
  971. widget is PrimaryTextButton &&
  972. widget.label == LocaleKeys.button_OK.tr(),
  973. );
  974. await tapButton(field);
  975. }
  976. Future<void> deleteDatebaseView(Finder linkedView) async {
  977. await tap(linkedView, buttons: kSecondaryButton);
  978. await pumpAndSettle();
  979. await tapButton(
  980. find.byWidgetPredicate(
  981. (widget) =>
  982. widget is ActionCellWidget &&
  983. widget.action == TabBarViewAction.delete,
  984. ),
  985. );
  986. final okButton = find.byWidgetPredicate(
  987. (widget) =>
  988. widget is PrimaryTextButton &&
  989. widget.label == LocaleKeys.button_OK.tr(),
  990. );
  991. await tapButton(okButton);
  992. }
  993. Future<void> assertCurrentDatabaseTagIs(DatabaseLayoutPB layout) async {
  994. switch (layout) {
  995. case DatabaseLayoutPB.Board:
  996. expect(find.byType(BoardPage), findsOneWidget);
  997. break;
  998. case DatabaseLayoutPB.Calendar:
  999. expect(find.byType(CalendarPage), findsOneWidget);
  1000. break;
  1001. case DatabaseLayoutPB.Grid:
  1002. expect(find.byType(GridPage), findsOneWidget);
  1003. break;
  1004. default:
  1005. throw Exception('Unknown database layout type: $layout');
  1006. }
  1007. }
  1008. Future<void> selectDatabaseLayoutType(DatabaseLayoutPB layout) async {
  1009. final findLayoutCell = find.byType(DatabaseViewLayoutCell);
  1010. final findText = find.byWidgetPredicate(
  1011. (widget) => widget is FlowyText && widget.text == layout.layoutName(),
  1012. );
  1013. final button = find.descendant(
  1014. of: findLayoutCell,
  1015. matching: findText,
  1016. );
  1017. await tapButton(button);
  1018. }
  1019. Future<void> assertCurrentDatabaseLayoutType(DatabaseLayoutPB layout) async {
  1020. expect(finderForDatabaseLayoutType(layout), findsOneWidget);
  1021. }
  1022. Future<void> tapDatabaseRawDataButton() async {
  1023. await tapButtonWithName(LocaleKeys.importPanel_database.tr());
  1024. }
  1025. }
  1026. Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) {
  1027. switch (layout) {
  1028. case DatabaseLayoutPB.Board:
  1029. return find.byType(BoardPage);
  1030. case DatabaseLayoutPB.Calendar:
  1031. return find.byType(CalendarPage);
  1032. case DatabaseLayoutPB.Grid:
  1033. return find.byType(GridPage);
  1034. default:
  1035. throw Exception('Unknown database layout type: $layout');
  1036. }
  1037. }
  1038. Finder finderForFieldType(FieldType fieldType) {
  1039. switch (fieldType) {
  1040. case FieldType.Checkbox:
  1041. return find.byType(GridCheckboxCell, skipOffstage: false);
  1042. case FieldType.DateTime:
  1043. return find.byType(GridDateCell, skipOffstage: false);
  1044. case FieldType.LastEditedTime:
  1045. case FieldType.CreatedTime:
  1046. return find.byType(GridDateCell, skipOffstage: false);
  1047. case FieldType.SingleSelect:
  1048. return find.byType(GridSingleSelectCell, skipOffstage: false);
  1049. case FieldType.MultiSelect:
  1050. return find.byType(GridMultiSelectCell, skipOffstage: false);
  1051. case FieldType.Checklist:
  1052. return find.byType(GridChecklistCell, skipOffstage: false);
  1053. case FieldType.Number:
  1054. return find.byType(GridNumberCell, skipOffstage: false);
  1055. case FieldType.RichText:
  1056. return find.byType(GridTextCell, skipOffstage: false);
  1057. case FieldType.URL:
  1058. return find.byType(GridURLCell, skipOffstage: false);
  1059. default:
  1060. throw Exception('Unknown field type: $fieldType');
  1061. }
  1062. }