diff --git a/frontend/appflowy_flutter/assets/test/workspaces/cover_image.zip b/frontend/appflowy_flutter/assets/test/workspaces/cover_image.zip new file mode 100644 index 0000000000..65501f52fb Binary files /dev/null and b/frontend/appflowy_flutter/assets/test/workspaces/cover_image.zip differ diff --git a/frontend/appflowy_flutter/integration_test/cover_image_test.dart b/frontend/appflowy_flutter/integration_test/cover_image_test.dart new file mode 100644 index 0000000000..0bcc36f912 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/cover_image_test.dart @@ -0,0 +1,54 @@ +import 'package:flowy_infra_ui/widget/rounded_button.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'util/util.dart'; + +/// Integration tests for an empty board. The [TestWorkspaceService] will load +/// a workspace from an empty board `assets/test/workspaces/board.zip` for all +/// tests. +/// +/// To create another integration test with a preconfigured workspace. +/// Use the following steps. +/// 1. Create a new workspace from the AppFlowy launch screen. +/// 2. Modify the workspace until it is suitable as the starting point for +/// the integration test you need to land. +/// 3. Use a zip utility program to zip the workspace folder that you created. +/// 4. Add the zip file under `assets/test/workspaces/` +/// 5. Add a new enumeration to [TestWorkspace] in `integration_test/utils/data.dart`. +/// For example, if you added a workspace called `empty_calendar.zip`, +/// then [TestWorkspace] should have the following value: +/// ```dart +/// enum TestWorkspace { +/// board('board'), +/// empty_calendar('empty_calendar'); +/// +/// /* code */ +/// } +/// ``` +/// 6. Double check that the .zip file that you added is included as an asset in +/// the pubspec.yaml file under appflowy_flutter. +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + const service = TestWorkspaceService(TestWorkspace.coverImage); + + group('cover image', () { + setUpAll(() async => await service.setUpAll()); + setUp(() async => await service.setUp()); + + testWidgets( + 'hovering on cover image will display change and delete cover image buttons', + (tester) async { + await tester.initializeAppFlowy(); + expect(find.byType(Image), findsOneWidget); + + final TestPointer pointer = TestPointer(1, PointerDeviceKind.mouse); + final imageFinder = find.byType(Image); + Offset offset = tester.getCenter(imageFinder); + + pointer.hover(offset); + expect(find.byType(RoundedTextButton), findsOneWidget); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/util/data.dart b/frontend/appflowy_flutter/integration_test/util/data.dart index 4cbb1a34f2..c6912ddeca 100644 --- a/frontend/appflowy_flutter/integration_test/util/data.dart +++ b/frontend/appflowy_flutter/integration_test/util/data.dart @@ -9,7 +9,8 @@ import 'package:shared_preferences/shared_preferences.dart'; enum TestWorkspace { board("board"), - emptyDocument("empty_document"); + emptyDocument("empty_document"), + coverImage("cover_image"); const TestWorkspace(this._name); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_node_widget.dart index 9d3629a400..4942022bba 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_node_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_node_widget.dart @@ -393,21 +393,32 @@ class _CoverImageState extends State<_CoverImage> { mainAxisSize: MainAxisSize.min, children: [ AppFlowyPopover( + onClose: () { + setOverlayButtonsHidden(true); + }, offset: const Offset(-125, 10), controller: popoverController, direction: PopoverDirection.bottomWithCenterAligned, constraints: BoxConstraints.loose(const Size(380, 450)), margin: EdgeInsets.zero, - child: RoundedTextButton( - onPressed: () { - popoverController.show(); - }, - hoverColor: Theme.of(context).colorScheme.surface, - textColor: Theme.of(context).colorScheme.tertiary, - fillColor: Theme.of(context).colorScheme.surface.withOpacity(0.8), - width: 120, - height: 28, - title: LocaleKeys.document_plugins_cover_changeCover.tr(), + child: Visibility( + maintainState: true, + maintainAnimation: true, + maintainSize: true, + visible: !isOverlayButtonsHidden, + child: RoundedTextButton( + onPressed: () { + popoverController.show(); + setOverlayButtonsHidden(true); + }, + hoverColor: Theme.of(context).colorScheme.surface, + textColor: Theme.of(context).colorScheme.onSurface, + fillColor: + Theme.of(context).colorScheme.surface.withOpacity(0.8), + width: 120, + height: 28, + title: LocaleKeys.document_plugins_cover_changeCover.tr(), + ), ), popupBuilder: (BuildContext popoverContext) { return ChangeCoverPopover( @@ -418,18 +429,24 @@ class _CoverImageState extends State<_CoverImage> { }, ), const SizedBox(width: 10), - FlowyIconButton( - fillColor: Theme.of(context).colorScheme.surface.withOpacity(0.8), - hoverColor: Theme.of(context).colorScheme.surface, - iconPadding: const EdgeInsets.all(5), - width: 28, - icon: svgWidget( - 'editor/delete', - color: Theme.of(context).colorScheme.tertiary, + Visibility( + maintainAnimation: true, + maintainSize: true, + maintainState: true, + visible: !isOverlayButtonsHidden, + child: FlowyIconButton( + fillColor: Theme.of(context).colorScheme.surface.withOpacity(0.8), + hoverColor: Theme.of(context).colorScheme.surface, + iconPadding: const EdgeInsets.all(5), + width: 28, + icon: svgWidget( + 'editor/delete', + color: Theme.of(context).colorScheme.onSurface, + ), + onPressed: () { + widget.onCoverChanged(CoverSelectionType.initial, null); + }, ), - onPressed: () { - widget.onCoverChanged(CoverSelectionType.initial, null); - }, ), ], ), @@ -477,20 +494,30 @@ class _CoverImageState extends State<_CoverImage> { break; } //OverflowBox needs to be wraped by a widget with constraints(or from its parent) first,otherwise it will occur an error - return SizedBox( - height: height, - child: OverflowBox( - maxWidth: screenSize.width, - child: Stack( - children: [ - Container( - padding: const EdgeInsets.only(bottom: 10), - height: double.infinity, - width: double.infinity, - child: coverImage, - ), - hasCover ? _buildCoverOverlayButtons(context) : const SizedBox() - ], + return MouseRegion( + onEnter: (event) { + setOverlayButtonsHidden(false); + }, + onExit: (event) { + setOverlayButtonsHidden(true); + }, + child: SizedBox( + height: height, + child: OverflowBox( + maxWidth: screenSize.width, + child: Stack( + children: [ + Container( + padding: const EdgeInsets.only(bottom: 10), + height: double.infinity, + width: double.infinity, + child: coverImage, + ), + hasCover + ? _buildCoverOverlayButtons(context) + : const SizedBox.shrink() + ], + ), ), ), );