database_test_op.dart 39 KB

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