common_operations.dart 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. import 'dart:io';
  2. import 'package:appflowy/core/config/kv.dart';
  3. import 'package:appflowy/core/config/kv_keys.dart';
  4. import 'package:appflowy/generated/flowy_svgs.g.dart';
  5. import 'package:appflowy/generated/locale_keys.g.dart';
  6. import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
  7. import 'package:appflowy/startup/startup.dart';
  8. import 'package:appflowy/user/presentation/screens/screens.dart';
  9. import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart';
  10. import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart';
  11. import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
  12. import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart';
  13. import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';
  14. import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
  15. import 'package:appflowy_backend/log.dart';
  16. import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
  17. import 'package:easy_localization/easy_localization.dart';
  18. import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
  19. import 'package:flutter/gestures.dart';
  20. import 'package:flutter/material.dart';
  21. import 'package:flutter/services.dart';
  22. import 'package:flutter_test/flutter_test.dart';
  23. import 'util.dart';
  24. extension CommonOperations on WidgetTester {
  25. /// Tap the GetStart button on the launch page.
  26. Future<void> tapGoButton() async {
  27. final goButton = find.byType(GoButton);
  28. await tapButton(goButton);
  29. if (Platform.isWindows) {
  30. await pumpAndSettle(const Duration(milliseconds: 200));
  31. }
  32. }
  33. /// Tap the + button on the home page.
  34. Future<void> tapAddViewButton({
  35. String name = gettingStarted,
  36. }) async {
  37. await hoverOnPageName(
  38. name,
  39. onHover: () async {
  40. final addButton = find.byType(ViewAddButton);
  41. await tapButton(addButton);
  42. },
  43. );
  44. }
  45. /// Tap the 'New Page' Button on the sidebar.
  46. Future<void> tapNewPageButton() async {
  47. final newPageButton = find.byType(SidebarNewPageButton);
  48. await tapButton(newPageButton);
  49. }
  50. /// Tap the create document button.
  51. ///
  52. /// Must call [tapAddViewButton] first.
  53. Future<void> tapCreateDocumentButton() async {
  54. await tapButtonWithName(LocaleKeys.document_menuName.tr());
  55. }
  56. /// Tap the create grid button.
  57. ///
  58. /// Must call [tapAddViewButton] first.
  59. Future<void> tapCreateGridButton() async {
  60. await tapButtonWithName(LocaleKeys.grid_menuName.tr());
  61. }
  62. /// Tap the create grid button.
  63. ///
  64. /// Must call [tapAddViewButton] first.
  65. Future<void> tapCreateCalendarButton() async {
  66. await tapButtonWithName(LocaleKeys.calendar_menuName.tr());
  67. }
  68. /// Tap the import button.
  69. ///
  70. /// Must call [tapAddViewButton] first.
  71. Future<void> tapImportButton() async {
  72. await tapButtonWithName(LocaleKeys.moreAction_import.tr());
  73. }
  74. /// Tap the import from text & markdown button.
  75. ///
  76. /// Must call [tapImportButton] first.
  77. Future<void> tapTextAndMarkdownButton() async {
  78. await tapButtonWithName(LocaleKeys.importPanel_textAndMarkdown.tr());
  79. }
  80. /// Tap the LanguageSelectorOnWelcomePage widget on the launch page.
  81. Future<void> tapLanguageSelectorOnWelcomePage() async {
  82. final languageSelector = find.byType(LanguageSelectorOnWelcomePage);
  83. await tapButton(languageSelector);
  84. }
  85. /// Tap languageItem on LanguageItemsListView.
  86. ///
  87. /// [scrollDelta] is the distance to scroll the ListView.
  88. /// Default value is 100
  89. ///
  90. /// If it is positive -> scroll down.
  91. ///
  92. /// If it is negative -> scroll up.
  93. Future<void> tapLanguageItem({
  94. required String languageCode,
  95. String? countryCode,
  96. double? scrollDelta,
  97. }) async {
  98. final languageItemsListView = find.descendant(
  99. of: find.byType(ListView),
  100. matching: find.byType(Scrollable),
  101. );
  102. final languageItem = find.byWidgetPredicate(
  103. (widget) =>
  104. widget is LanguageItem &&
  105. widget.locale.languageCode == languageCode &&
  106. widget.locale.countryCode == countryCode,
  107. );
  108. // scroll the ListView until zHCNLanguageItem shows on the screen.
  109. await scrollUntilVisible(
  110. languageItem,
  111. scrollDelta ?? 100,
  112. scrollable: languageItemsListView,
  113. // maxHeight of LanguageItemsListView
  114. maxScrolls: 400,
  115. );
  116. try {
  117. await tapButton(languageItem);
  118. } on FlutterError catch (e) {
  119. Log.warn('tapLanguageItem error: $e');
  120. }
  121. }
  122. /// Hover on the widget.
  123. Future<void> hoverOnWidget(
  124. Finder finder, {
  125. Offset? offset,
  126. Future<void> Function()? onHover,
  127. bool removePointer = true,
  128. }) async {
  129. try {
  130. final gesture = await createGesture(kind: PointerDeviceKind.mouse);
  131. await gesture.addPointer(location: Offset.zero);
  132. await pump();
  133. await gesture.moveTo(offset ?? getCenter(finder));
  134. await pumpAndSettle();
  135. await onHover?.call();
  136. await gesture.removePointer();
  137. } catch (err) {
  138. Log.error('hoverOnWidget error: $err');
  139. }
  140. }
  141. /// Hover on the page name.
  142. Future<void> hoverOnPageName(
  143. String name, {
  144. ViewLayoutPB layout = ViewLayoutPB.Document,
  145. Future<void> Function()? onHover,
  146. bool useLast = true,
  147. }) async {
  148. final pageNames = findPageName(name, layout: layout);
  149. if (useLast) {
  150. await hoverOnWidget(
  151. pageNames.last,
  152. onHover: onHover,
  153. );
  154. } else {
  155. await hoverOnWidget(
  156. pageNames.first,
  157. onHover: onHover,
  158. );
  159. }
  160. }
  161. /// open the page with given name.
  162. Future<void> openPage(
  163. String name, {
  164. ViewLayoutPB layout = ViewLayoutPB.Document,
  165. }) async {
  166. final finder = findPageName(name, layout: layout);
  167. expect(finder, findsOneWidget);
  168. await tapButton(finder);
  169. }
  170. /// Tap the ... button beside the page name.
  171. ///
  172. /// Must call [hoverOnPageName] first.
  173. Future<void> tapPageOptionButton() async {
  174. final optionButton = find.byType(ViewMoreActionButton);
  175. await tapButton(optionButton);
  176. }
  177. /// Tap the delete page button.
  178. Future<void> tapDeletePageButton() async {
  179. await tapPageOptionButton();
  180. await tapButtonWithName(ViewMoreActionType.delete.name);
  181. }
  182. /// Tap the rename page button.
  183. Future<void> tapRenamePageButton() async {
  184. await tapPageOptionButton();
  185. await tapButtonWithName(ViewMoreActionType.rename.name);
  186. }
  187. /// Tap the favorite page button
  188. Future<void> tapFavoritePageButton() async {
  189. await tapPageOptionButton();
  190. await tapButtonWithName(ViewMoreActionType.favorite.name);
  191. }
  192. /// Tap the unfavorite page button
  193. Future<void> tapUnfavoritePageButton() async {
  194. await tapPageOptionButton();
  195. await tapButtonWithName(ViewMoreActionType.unFavorite.name);
  196. }
  197. /// Tap the Open in a new tab button
  198. Future<void> tapOpenInTabButton() async {
  199. await tapPageOptionButton();
  200. await tapButtonWithName(ViewMoreActionType.openInNewTab.name);
  201. }
  202. /// Rename the page.
  203. Future<void> renamePage(String name) async {
  204. await tapRenamePageButton();
  205. await enterText(find.byType(TextFormField), name);
  206. await tapOKButton();
  207. }
  208. Future<void> tapOKButton() async {
  209. final okButton = find.byWidgetPredicate(
  210. (widget) =>
  211. widget is PrimaryTextButton &&
  212. widget.label == LocaleKeys.button_OK.tr(),
  213. );
  214. await tapButton(okButton);
  215. }
  216. /// Tap the restore button.
  217. ///
  218. /// the restore button will show after the current page is deleted.
  219. Future<void> tapRestoreButton() async {
  220. final restoreButton = find.textContaining(
  221. LocaleKeys.deletePagePrompt_restore.tr(),
  222. );
  223. await tapButton(restoreButton);
  224. }
  225. /// Tap the delete permanently button.
  226. ///
  227. /// the restore button will show after the current page is deleted.
  228. Future<void> tapDeletePermanentlyButton() async {
  229. final restoreButton = find.textContaining(
  230. LocaleKeys.deletePagePrompt_deletePermanent.tr(),
  231. );
  232. await tapButton(restoreButton);
  233. }
  234. /// Tap the share button above the document page.
  235. Future<void> tapShareButton() async {
  236. final shareButton = find.byWidgetPredicate(
  237. (widget) => widget is DocumentShareButton,
  238. );
  239. await tapButton(shareButton);
  240. }
  241. /// Tap the export markdown button
  242. ///
  243. /// Must call [tapShareButton] first.
  244. Future<void> tapMarkdownButton() async {
  245. final markdownButton = find.textContaining(
  246. LocaleKeys.shareAction_markdown.tr(),
  247. );
  248. await tapButton(markdownButton);
  249. }
  250. Future<void> createNewPageWithName({
  251. String? name,
  252. ViewLayoutPB layout = ViewLayoutPB.Document,
  253. String? parentName,
  254. bool openAfterCreated = true,
  255. }) async {
  256. // create a new page
  257. await tapAddViewButton(name: parentName ?? gettingStarted);
  258. await tapButtonWithName(layout.menuName);
  259. final settingsOrFailure = await getIt<KeyValueStorage>().getWithFormat(
  260. KVKeys.showRenameDialogWhenCreatingNewFile,
  261. (value) => bool.parse(value),
  262. );
  263. final showRenameDialog = settingsOrFailure.fold((l) => false, (r) => r);
  264. if (showRenameDialog) {
  265. await tapOKButton();
  266. }
  267. await pumpAndSettle();
  268. // hover on it and change it's name
  269. if (name != null) {
  270. await hoverOnPageName(
  271. LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
  272. layout: layout,
  273. onHover: () async {
  274. await renamePage(name);
  275. await pumpAndSettle();
  276. },
  277. );
  278. await pumpAndSettle();
  279. }
  280. // open the page after created
  281. if (openAfterCreated) {
  282. await openPage(
  283. // if the name is null, use the default name
  284. name ?? LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
  285. layout: layout,
  286. );
  287. await pumpAndSettle();
  288. }
  289. }
  290. Future<void> simulateKeyEvent(
  291. LogicalKeyboardKey key, {
  292. bool isControlPressed = false,
  293. bool isShiftPressed = false,
  294. bool isAltPressed = false,
  295. bool isMetaPressed = false,
  296. }) async {
  297. if (isControlPressed) {
  298. await simulateKeyDownEvent(LogicalKeyboardKey.control);
  299. }
  300. if (isShiftPressed) {
  301. await simulateKeyDownEvent(LogicalKeyboardKey.shift);
  302. }
  303. if (isAltPressed) {
  304. await simulateKeyDownEvent(LogicalKeyboardKey.alt);
  305. }
  306. if (isMetaPressed) {
  307. await simulateKeyDownEvent(LogicalKeyboardKey.meta);
  308. }
  309. await simulateKeyDownEvent(key);
  310. await simulateKeyUpEvent(key);
  311. if (isControlPressed) {
  312. await simulateKeyUpEvent(LogicalKeyboardKey.control);
  313. }
  314. if (isShiftPressed) {
  315. await simulateKeyUpEvent(LogicalKeyboardKey.shift);
  316. }
  317. if (isAltPressed) {
  318. await simulateKeyUpEvent(LogicalKeyboardKey.alt);
  319. }
  320. if (isMetaPressed) {
  321. await simulateKeyUpEvent(LogicalKeyboardKey.meta);
  322. }
  323. await pumpAndSettle();
  324. }
  325. Future<void> openAppInNewTab(String name, ViewLayoutPB layout) async {
  326. await hoverOnPageName(
  327. name,
  328. onHover: () async {
  329. await tapOpenInTabButton();
  330. await pumpAndSettle();
  331. },
  332. );
  333. await pumpAndSettle();
  334. }
  335. Future<void> favoriteViewByName(
  336. String name, {
  337. ViewLayoutPB layout = ViewLayoutPB.Document,
  338. }) async {
  339. await hoverOnPageName(
  340. name,
  341. layout: layout,
  342. useLast: true,
  343. onHover: () async {
  344. await tapFavoritePageButton();
  345. await pumpAndSettle();
  346. },
  347. );
  348. }
  349. Future<void> unfavoriteViewByName(
  350. String name, {
  351. ViewLayoutPB layout = ViewLayoutPB.Document,
  352. }) async {
  353. await hoverOnPageName(
  354. name,
  355. layout: layout,
  356. useLast: true,
  357. onHover: () async {
  358. await tapUnfavoritePageButton();
  359. await pumpAndSettle();
  360. },
  361. );
  362. }
  363. Future<void> movePageToOtherPage({
  364. required String name,
  365. required String parentName,
  366. required ViewLayoutPB layout,
  367. required ViewLayoutPB parentLayout,
  368. DraggableHoverPosition position = DraggableHoverPosition.center,
  369. }) async {
  370. final from = findPageName(name, layout: layout);
  371. final to = findPageName(parentName, layout: parentLayout);
  372. final gesture = await startGesture(getCenter(from));
  373. Offset offset = Offset.zero;
  374. switch (position) {
  375. case DraggableHoverPosition.center:
  376. offset = getCenter(to);
  377. break;
  378. case DraggableHoverPosition.top:
  379. offset = getTopLeft(to);
  380. break;
  381. case DraggableHoverPosition.bottom:
  382. offset = getBottomLeft(to);
  383. break;
  384. default:
  385. }
  386. await gesture.moveTo(offset, timeStamp: const Duration(milliseconds: 400));
  387. await gesture.up();
  388. await pumpAndSettle();
  389. }
  390. // tap the button with [FlowySvgData]
  391. Future<void> tapButtonWithFlowySvgData(FlowySvgData svg) async {
  392. final button = find.byWidgetPredicate(
  393. (widget) => widget is FlowySvg && widget.svg.path == svg.path,
  394. );
  395. await tapButton(button);
  396. }
  397. }
  398. extension ViewLayoutPBTest on ViewLayoutPB {
  399. String get menuName {
  400. switch (this) {
  401. case ViewLayoutPB.Grid:
  402. return LocaleKeys.grid_menuName.tr();
  403. case ViewLayoutPB.Board:
  404. return LocaleKeys.board_menuName.tr();
  405. case ViewLayoutPB.Document:
  406. return LocaleKeys.document_menuName.tr();
  407. case ViewLayoutPB.Calendar:
  408. return LocaleKeys.calendar_menuName.tr();
  409. default:
  410. throw UnsupportedError('Unsupported layout: $this');
  411. }
  412. }
  413. String get referencedMenuName {
  414. switch (this) {
  415. case ViewLayoutPB.Grid:
  416. return LocaleKeys.document_plugins_referencedGrid.tr();
  417. case ViewLayoutPB.Board:
  418. return LocaleKeys.document_plugins_referencedBoard.tr();
  419. case ViewLayoutPB.Calendar:
  420. return LocaleKeys.document_plugins_referencedCalendar.tr();
  421. default:
  422. throw UnsupportedError('Unsupported layout: $this');
  423. }
  424. }
  425. }