diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml index 084018f747..76aed8da9b 100644 --- a/.github/workflows/integration_test.yml +++ b/.github/workflows/integration_test.yml @@ -105,11 +105,11 @@ jobs: working-directory: frontend/appflowy_flutter run: | if [ "$RUNNER_OS" == "Linux" ]; then - flutter test integration_test -d Linux --coverage + flutter test integration_test/runner.dart -d Linux --coverage elif [ "$RUNNER_OS" == "macOS" ]; then - flutter test integration_test -d macOS --coverage + flutter test integration_test/runner.dart -d macOS --coverage elif [ "$RUNNER_OS" == "Windows" ]; then - flutter test integration_test -d Windows --coverage + flutter test integration_test/runner.dart -d Windows --coverage fi shell: bash @@ -120,4 +120,3 @@ jobs: # env_vars: ${{ matrix.os }} # fail_ci_if_error: true # verbose: true - diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c971bb30ac..88e40585f4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,7 @@ name: release on: push: tags: - - '*' + - "*" env: FLUTTER_VERSION: "3.7.5" @@ -136,7 +136,11 @@ jobs: fail-fast: false matrix: job: - - { target: x86_64-apple-darwin, os: macos-10.15, extra-build-args: "" } + - { + target: x86_64-apple-darwin, + os: macos-10.15, + extra-build-args: "", + } steps: - name: Checkout source code uses: actions/checkout@v3 @@ -172,7 +176,7 @@ jobs: working-directory: frontend run: | flutter config --enable-macos-desktop - cargo make --env APP_VERSION=${{ github.ref_name }} --profile production-mac-x86_64 appflowy + dart ./scripts/flutter_release_build/build_flowy.dart . ${{ github.ref_name }} - name: Create macOS dmg run: | @@ -225,9 +229,21 @@ jobs: fail-fast: false matrix: job: - - { arch: x86_64, target: x86_64-unknown-linux-gnu, os: ubuntu-20.04, extra-build-args: "", flutter_profile: production-linux-x86_64 } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-args: "", + flutter_profile: production-linux-x86_64, + } # - { arch: aarch64, target: aarch64-unknown-linux-gnu, os: ubuntu-20.04, extra-build-args: "", flutter_profile: production-linux-aarch64 } - - { arch: x86_64, target: x86_64-unknown-linux-gnu, os: ubuntu-18.04, extra-build-args: "", flutter_profile: production-linux-x86_64} + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-args: "", + flutter_profile: production-linux-x86_64, + } steps: - name: Checkout source code uses: actions/checkout@v3 @@ -275,7 +291,7 @@ jobs: working-directory: frontend run: | flutter config --enable-linux-desktop - cargo make --env APP_VERSION=${{ github.ref_name }} --profile ${{ matrix.job.flutter_profile}} appflowy + dart ./scripts/flutter_release_build/build_flowy.dart . ${{ github.ref_name }} - name: Archive Assert working-directory: ${{ env.LINUX_APP_RELEASE_PATH }} @@ -361,4 +377,4 @@ jobs: - name: Notify Discord run: | curl -H "Content-Type: application/json" -d '{"username": "release@appflowy", "content": "🎉 AppFlowy ${{ github.ref_name }} is available. https://github.com/AppFlowy-IO/AppFlowy/releases/tag/'${{ github.ref_name }}'"}' "https://discord.com/api/webhooks/${{ secrets.DISCORD }}" - shell: bash \ No newline at end of file + shell: bash diff --git a/frontend/.vscode/tasks.json b/frontend/.vscode/tasks.json index 227a5cbfba..4f8a52c95c 100644 --- a/frontend/.vscode/tasks.json +++ b/frontend/.vscode/tasks.json @@ -1,223 +1,225 @@ { - "version": "2.0.0", - // https://code.visualstudio.com/docs/editor/tasks - // https://gist.github.com/deadalusai/9e13e36d61ec7fb72148 - // ${workspaceRoot}: the root folder of the team - // ${file}: the current opened file - // ${fileBasename}: the current opened file's basename - // ${fileDirname}: the current opened file's dirname - // ${fileExtname}: the current opened file's extension - // ${cwd}: the current working directory of the spawned process - "tasks": [ - { - "label": "AF: Clean + Rebuild All", - "type": "shell", - "dependsOrder": "sequence", - "dependsOn": [ - "AF: Dart Clean", - "AF: Flutter Clean", - "AF: Build Appflowy Core", - "AF: Flutter Pub Get", - "AF: Flutter Package Get", - "AF: Generate Language Files", - "AF: Generate Freezed Files" - ], - "presentation": { - "reveal": "always", - "panel": "new" - } - }, - { - "label": "AF: Clean + Rebuild All (Android)", - "type": "shell", - "dependsOrder": "sequence", - "dependsOn": [ - "AF: Dart Clean", - "AF: Flutter Clean", - "AF: Build Appflowy Core_for_android", - "AF: Flutter Pub Get", - "AF: Flutter Package Get", - "AF: Generate Language Files", - "AF: Generate Freezed Files" - ], - "presentation": { - "reveal": "always", - "panel": "new" - } - }, - { - "label": "AF: Build Appflowy Core_for_android", - "type": "shell", - "command": "cargo make --profile development-android appflowy-core-dev-android", - "group": "build", - "options": { - "cwd": "${workspaceFolder}" - } - }, - { - "label": "AF: Build Appflowy Core", - "type": "shell", - "windows": { - "command": "cargo make --profile development-windows-x86 appflowy-core-dev" - }, - "linux": { - "command": "cargo make --profile \"development-linux-$(uname -m)\" appflowy-core-dev" - }, - "osx": { - "command": "cargo make --profile \"development-mac-$(uname -m)\" appflowy-core-dev" - }, - "group": "build", - "options": { - "cwd": "${workspaceFolder}" - } - }, - { - "label": "AF: Code Gen", - "type": "shell", - "dependsOrder": "sequence", - "dependsOn": [ - "AF: Flutter Clean", - "AF: Flutter Pub Get", - "AF: Flutter Package Get", - "AF: Generate Language Files", - "AF: Generate Freezed Files" - ], - "group": { - "kind": "build", - "isDefault": true - }, - "presentation": { - "reveal": "always", - "panel": "new" - } - }, - { - "label": "AF: Flutter Clean", - "type": "shell", - "command": "flutter clean", - "options": { - "cwd": "${workspaceFolder}/appflowy_flutter" - } - }, - { - "label": "AF: Flutter Pub Get", - "type": "shell", - "command": "flutter pub get", - "options": { - "cwd": "${workspaceFolder}/appflowy_flutter" - } - }, - { - "label": "AF: Flutter Package Get", - "type": "shell", - "command": "flutter packages pub get", - "options": { - "cwd": "${workspaceFolder}/appflowy_flutter" - } - }, - { - "label": "AF: Generate Freezed Files", - "type": "shell", - "command": "flutter pub run build_runner build --delete-conflicting-outputs", - "options": { - "cwd": "${workspaceFolder}/appflowy_flutter" - } - }, - { - "label": "AF: Generate Language Files", - "type": "shell", - "command": "sh ./scripts/generate_language_files.sh", - "windows": { - "options": { - "shell": { - "executable": "cmd.exe", - "args": [ - "/d", - "/c", - ".\\scripts\\generate_language_files.cmd" - ] - } - } - }, - "group": "build", - "options": { - "cwd": "${workspaceFolder}" - } - }, - { - "label": "AF: Flutter Clean", - "type": "shell", - "command": "cargo make flutter_clean", - "group": "build", - "options": { - "cwd": "${workspaceFolder}" - } - }, - { - "label": "AF: flutter build aar", - "type": "flutter", - "command": "flutter", - "args": [ - "build", - "aar" - ], - "group": "build", - "problemMatcher": [], - "detail": "appflowy_flutter" - }, - { - "label": "AF: Tauri UI Dev", - "type": "shell", - "isBackground": true, - "command": "yarn", - "args": ["dev"], - "options": { - "cwd": "${workspaceFolder}/appflowy_tauri" - } - }, - { - "label": "AF: Tauri UI Build", - "type": "shell", - "command": "npm run build", - "options": { - "cwd": "${workspaceFolder}/appflowy_tauri" - } - }, - { - "label": "AF: Tauri Dev", - "type": "shell", - "command": "npm run tauri:dev", - "options": { - "cwd": "${workspaceFolder}/appflowy_tauri" - } - }, - { - "label": "AF: Tauri Clean", - "type": "shell", - "command": "cargo make tauri_clean", - "options": { - "cwd": "${workspaceFolder}" - } - }, - { - "label": "AF: Tauri Clean + Dev", - "type": "shell", - "dependsOrder": "sequence", - "dependsOn": [ - "AF: Tauri Clean", - "AF: Tauri UI Dev" - ], - "options": { - "cwd": "${workspaceFolder}" - } - }, - { - "label": "AF: Tauri ESLint", - "type": "shell", - "command": "npx eslint --fix src", - "options": { - "cwd": "${workspaceFolder}/appflowy_tauri" - } - }, - ] -} \ No newline at end of file + "version": "2.0.0", + // https://code.visualstudio.com/docs/editor/tasks + // https://gist.github.com/deadalusai/9e13e36d61ec7fb72148 + // ${workspaceRoot}: the root folder of the team + // ${file}: the current opened file + // ${fileBasename}: the current opened file's basename + // ${fileDirname}: the current opened file's dirname + // ${fileExtname}: the current opened file's extension + // ${cwd}: the current working directory of the spawned process + "tasks": [ + { + "label": "AF: Clean + Rebuild All", + "type": "shell", + "dependsOrder": "sequence", + "dependsOn": [ + "AF: Dart Clean", + "AF: Flutter Clean", + "AF: Build Appflowy Core", + "AF: Flutter Pub Get", + "AF: Flutter Package Get", + "AF: Generate Language Files", + "AF: Generate Freezed Files" + ], + "presentation": { + "reveal": "always", + "panel": "new" + } + }, + { + "label": "AF: Clean + Rebuild All (Android)", + "type": "shell", + "dependsOrder": "sequence", + "dependsOn": [ + "AF: Dart Clean", + "AF: Flutter Clean", + "AF: Build Appflowy Core_for_android", + "AF: Flutter Pub Get", + "AF: Flutter Package Get", + "AF: Generate Language Files", + "AF: Generate Freezed Files" + ], + "presentation": { + "reveal": "always", + "panel": "new" + } + }, + { + "label": "AF: Build Appflowy Core_for_android", + "type": "shell", + "command": "cargo make --profile development-android appflowy-core-dev-android", + "group": "build", + "options": { + "cwd": "${workspaceFolder}" + } + }, + { + "label": "AF: Build Appflowy Core", + "type": "shell", + "windows": { + "command": "cargo make --profile development-windows-x86 appflowy-core-dev" + }, + "linux": { + "command": "cargo make --profile \"development-linux-$(uname -m)\" appflowy-core-dev" + }, + "osx": { + "command": "cargo make --profile \"development-mac-$(uname -m)\" appflowy-core-dev" + }, + "group": "build", + "options": { + "cwd": "${workspaceFolder}" + } + }, + { + "label": "AF: Code Gen", + "type": "shell", + "dependsOrder": "sequence", + "dependsOn": [ + "AF: Flutter Clean", + "AF: Flutter Pub Get", + "AF: Flutter Package Get", + "AF: Generate Language Files", + "AF: Generate Freezed Files" + ], + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "reveal": "always", + "panel": "new" + } + }, + { + "label": "AF: Flutter Clean", + "type": "shell", + "command": "flutter clean", + "options": { + "cwd": "${workspaceFolder}/appflowy_flutter" + } + }, + { + "label": "AF: Flutter Pub Get", + "type": "shell", + "command": "flutter pub get", + "options": { + "cwd": "${workspaceFolder}/appflowy_flutter" + } + }, + { + "label": "AF: Flutter Package Get", + "type": "shell", + "command": "flutter packages pub get", + "options": { + "cwd": "${workspaceFolder}/appflowy_flutter" + } + }, + { + "label": "AF: Generate Freezed Files", + "type": "shell", + "command": "flutter pub run build_runner build --delete-conflicting-outputs", + "options": { + "cwd": "${workspaceFolder}/appflowy_flutter" + } + }, + { + "label": "AF: Generate Language Files", + "type": "shell", + "command": "sh ./scripts/generate_language_files.sh", + "windows": { + "options": { + "shell": { + "executable": "cmd.exe", + "args": [ + "/d", + "/c", + ".\\scripts\\generate_language_files.cmd" + ] + } + } + }, + "group": "build", + "options": { + "cwd": "${workspaceFolder}" + } + }, + { + "label": "AF: Flutter Clean", + "type": "shell", + "command": "cargo make flutter_clean", + "group": "build", + "options": { + "cwd": "${workspaceFolder}" + } + }, + { + "label": "AF: flutter build aar", + "type": "flutter", + "command": "flutter", + "args": [ + "build", + "aar" + ], + "group": "build", + "problemMatcher": [], + "detail": "appflowy_flutter" + }, + { + "label": "AF: Tauri UI Dev", + "type": "shell", + "isBackground": true, + "command": "yarn", + "args": [ + "dev" + ], + "options": { + "cwd": "${workspaceFolder}/appflowy_tauri" + } + }, + { + "label": "AF: Tauri UI Build", + "type": "shell", + "command": "npm run build", + "options": { + "cwd": "${workspaceFolder}/appflowy_tauri" + } + }, + { + "label": "AF: Tauri Dev", + "type": "shell", + "command": "npm run tauri:dev", + "options": { + "cwd": "${workspaceFolder}/appflowy_tauri" + } + }, + { + "label": "AF: Tauri Clean", + "type": "shell", + "command": "cargo make tauri_clean", + "options": { + "cwd": "${workspaceFolder}" + } + }, + { + "label": "AF: Tauri Clean + Dev", + "type": "shell", + "dependsOrder": "sequence", + "dependsOn": [ + "AF: Tauri Clean", + "AF: Tauri UI Dev" + ], + "options": { + "cwd": "${workspaceFolder}" + } + }, + { + "label": "AF: Tauri ESLint", + "type": "shell", + "command": "npx eslint --fix src", + "options": { + "cwd": "${workspaceFolder}/appflowy_tauri" + } + }, + ] +} diff --git a/frontend/appflowy_flutter/.gitignore b/frontend/appflowy_flutter/.gitignore index e77b1c9f0f..82c35e91cd 100644 --- a/frontend/appflowy_flutter/.gitignore +++ b/frontend/appflowy_flutter/.gitignore @@ -40,7 +40,7 @@ lib/generated_plugin_registrant.dart lib/generated/ # Freezed generated files -*.g.dart +*.g.dart *.freezed.dart # Symbolication related @@ -67,4 +67,4 @@ windows/flutter/dart_ffi/ **/**/*.so **/**/Brewfile.lock.json **/.sandbox -**/.vscode/ \ No newline at end of file +**/.vscode/ diff --git a/frontend/appflowy_flutter/assets/test/workspaces/board.zip b/frontend/appflowy_flutter/assets/test/workspaces/board.zip new file mode 100644 index 0000000000..1a8f6b0dd4 Binary files /dev/null and b/frontend/appflowy_flutter/assets/test/workspaces/board.zip differ diff --git a/frontend/appflowy_flutter/assets/test/workspaces/empty_document.zip b/frontend/appflowy_flutter/assets/test/workspaces/empty_document.zip new file mode 100644 index 0000000000..1399d640ad Binary files /dev/null and b/frontend/appflowy_flutter/assets/test/workspaces/empty_document.zip differ diff --git a/frontend/appflowy_flutter/assets/translations/en.json b/frontend/appflowy_flutter/assets/translations/en.json index a00a49ceb8..dd3435c19a 100644 --- a/frontend/appflowy_flutter/assets/translations/en.json +++ b/frontend/appflowy_flutter/assets/translations/en.json @@ -326,7 +326,8 @@ "checklist": { "panelTitle": "Add an item" }, - "menuName": "Grid" + "menuName": "Grid", + "referencedGridPrefix": "View of" }, "document": { "menuName": "Document", @@ -390,7 +391,8 @@ "column": { "create_new_card": "New" }, - "menuName": "Board" + "menuName": "Board", + "referencedBoardPrefix": "View of" }, "calendar": { "menuName": "Calendar", diff --git a/frontend/appflowy_flutter/integration_test/board_test.dart b/frontend/appflowy_flutter/integration_test/board_test.dart new file mode 100644 index 0000000000..3900843207 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/board_test.dart @@ -0,0 +1,43 @@ +import 'package:appflowy_board/appflowy_board.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.board); + + group('board', () { + setUpAll(() async => await service.setUpAll()); + setUp(() async => await service.setUp()); + + testWidgets('integration test unzips the proper workspace and loads it correctly.', (tester) async { + await tester.initializeAppFlowy(); + expect(find.byType(AppFlowyBoard), findsOneWidget); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/empty_document_test.dart b/frontend/appflowy_flutter/integration_test/empty_document_test.dart new file mode 100644 index 0000000000..51f92c4c35 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/empty_document_test.dart @@ -0,0 +1,120 @@ +import 'package:appflowy/plugins/document/presentation/plugins/base/built_in_page_widget.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'util/keyboard.dart'; +import 'util/util.dart'; + +/// Integration tests for an empty document. The [TestWorkspaceService] will load a workspace from an empty document `assets/test/workspaces/empty_document.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.emptyDocument); + + group('Tests on a workspace with only an empty document', () { + setUpAll(() async => await service.setUpAll()); + setUp(() async => await service.setUp()); + + testWidgets('/board shortcut creates a new board and view of the board', (tester) async { + await tester.initializeAppFlowy(); + + // Needs tab to obtain focus for the app flowy editor. + // by default the tap appears at the center of the widget. + final Finder editor = find.byType(AppFlowyEditor); + await tester.tap(editor); + await tester.pumpAndSettle(); + + // tester.sendText() cannot be used since the editor + // does not contain any EditableText widgets. + // to interact with the app during an integration test, + // simulate physical keyboard events. + await FlowyTestKeyboard.simulateKeyDownEvent([ + LogicalKeyboardKey.slash, + LogicalKeyboardKey.keyB, + LogicalKeyboardKey.keyO, + LogicalKeyboardKey.keyA, + LogicalKeyboardKey.keyR, + LogicalKeyboardKey.keyD, + LogicalKeyboardKey.arrowDown, + ], tester: tester); + + // Checks whether the options in the selection menu + // for /board exist. + expect(find.byType(SelectionMenuItemWidget), findsAtLeastNWidgets(2)); + + // Finalizes the slash command that creates the board. + await FlowyTestKeyboard.simulateKeyDownEvent([ + LogicalKeyboardKey.enter, + ], tester: tester); + + // Checks whether new board is referenced and properly on the page. + expect(find.byType(BuiltInPageWidget), findsOneWidget); + + // Checks whether the new database was created + const newBoardLabel = "Untitled"; + expect(find.text(newBoardLabel), findsOneWidget); + + // Checks whether a view of the database was created + const viewOfBoardLabel = "View of Untitled"; + expect(find.text(viewOfBoardLabel), findsNWidgets(2)); + }); + + testWidgets('/grid shortcut creates a new grid and view of the grid', (tester) async { + await tester.initializeAppFlowy(); + + // Needs tab to obtain focus for the app flowy editor. + // by default the tap appears at the center of the widget. + final Finder editor = find.byType(AppFlowyEditor); + await tester.tap(editor); + await tester.pumpAndSettle(); + + // tester.sendText() cannot be used since the editor + // does not contain any EditableText widgets. + // to interact with the app during an integration test, + // simulate physical keyboard events. + await FlowyTestKeyboard.simulateKeyDownEvent([ + LogicalKeyboardKey.slash, + LogicalKeyboardKey.keyG, + LogicalKeyboardKey.keyR, + LogicalKeyboardKey.keyI, + LogicalKeyboardKey.keyD, + LogicalKeyboardKey.arrowDown, + ], tester: tester); + + // Checks whether the options in the selection menu + // for /grid exist. + expect(find.byType(SelectionMenuItemWidget), findsAtLeastNWidgets(2)); + + // Finalizes the slash command that creates the board. + await simulateKeyDownEvent(LogicalKeyboardKey.enter); + await tester.pumpAndSettle(); + + // Checks whether new board is referenced and properly on the page. + expect(find.byType(BuiltInPageWidget), findsOneWidget); + + // Checks whether the new database was created + const newTableLabel = "Untitled"; + expect(find.text(newTableLabel), findsOneWidget); + + // Checks whether a view of the database was created + const viewOfTableLabel = "View of Untitled"; + expect(find.text(viewOfTableLabel), findsNWidgets(2)); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/runner.dart b/frontend/appflowy_flutter/integration_test/runner.dart new file mode 100644 index 0000000000..1592cccb93 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/runner.dart @@ -0,0 +1,19 @@ +import 'package:integration_test/integration_test.dart'; + +import 'board_test.dart' as board_test; +import 'switch_folder_test.dart' as switch_folder_test; +import 'empty_document_test.dart' as empty_document_test; + +/// The main task runner for all integration tests in AppFlowy. +/// +/// Having a single entrypoint for integration tests is necessary due to an +/// [issue caused by switching files with integration testing](https://github.com/flutter/flutter/issues/101031). +/// If flutter/flutter#101031 is resolved, this file can be removed completely. +/// Once removed, the integration_test.yaml must be updated to exclude this as +/// as the test target. +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + switch_folder_test.main(); + board_test.main(); + empty_document_test.main(); +} diff --git a/frontend/appflowy_flutter/integration_test/switch_folder_test.dart b/frontend/appflowy_flutter/integration_test/switch_folder_test.dart index 555a5e41b1..6dcd41b081 100644 --- a/frontend/appflowy_flutter/integration_test/switch_folder_test.dart +++ b/frontend/appflowy_flutter/integration_test/switch_folder_test.dart @@ -1,10 +1,5 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/plugins/base/built_in_page_widget.dart'; import 'package:appflowy/user/presentation/folder/folder_widget.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text_field.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -162,135 +157,5 @@ void main() { await TestFolder.currentLocation(), ); }); - - testWidgets('/board shortcut creates a new board', (tester) async { - const folderName = 'appflowy'; - await TestFolder.cleanTestLocation(folderName); - await TestFolder.setTestLocation(folderName); - - await tester.initializeAppFlowy(); - - // tap open button - await mockGetDirectoryPath(folderName); - await tester.tapOpenFolderButton(); - - await tester.wait(1000); - await tester.expectToSeeWelcomePage(); - - final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - // Necessary for being able to enterText when not in debug mode - binding.testTextInput.register(); - - // Needs tab to obtain focus for the app flowy editor. - // by default the tap appears at the center of the widget. - final Finder editor = find.byType(AppFlowyEditor); - await tester.tap(editor); - await tester.pumpAndSettle(); - - // tester.sendText() cannot be used since the editor - // does not contain any EditableText widgets. - // to interact with the app during an integration test, - // simulate physical keyboard events. - await simulateKeyDownEvent(LogicalKeyboardKey.enter); - await tester.pumpAndSettle(); - await simulateKeyDownEvent(LogicalKeyboardKey.enter); - await tester.pumpAndSettle(); - await simulateKeyDownEvent(LogicalKeyboardKey.arrowLeft); - await tester.pumpAndSettle(); - await simulateKeyDownEvent(LogicalKeyboardKey.slash); - await tester.pumpAndSettle(); - await simulateKeyDownEvent(LogicalKeyboardKey.keyB); - await tester.pumpAndSettle(); - await simulateKeyDownEvent(LogicalKeyboardKey.keyO); - await tester.pumpAndSettle(); - await simulateKeyDownEvent(LogicalKeyboardKey.keyA); - await tester.pumpAndSettle(); - await simulateKeyDownEvent(LogicalKeyboardKey.keyR); - await tester.pumpAndSettle(); - await simulateKeyDownEvent(LogicalKeyboardKey.keyD); - await tester.pumpAndSettle(); - await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown); - await tester.pumpAndSettle(); - - // Checks whether the options in the selection menu - // for /board exist. - expect(find.byType(SelectionMenuItemWidget), findsAtLeastNWidgets(2)); - - // Finalizes the slash command that creates the board. - await simulateKeyDownEvent(LogicalKeyboardKey.enter); - await tester.pumpAndSettle(); - - // Checks whether new board is referenced and properly on the page. - expect(find.byType(BuiltInPageWidget), findsOneWidget); - - // Checks whether the new board is in the side bar. - final sidebarLabel = LocaleKeys.newPageText.tr(); - expect(find.text(sidebarLabel), findsOneWidget); - }); - - testWidgets('/grid shortcut creates a new grid', (tester) async { - const folderName = 'appflowy'; - await TestFolder.cleanTestLocation(folderName); - await TestFolder.setTestLocation(folderName); - - await tester.initializeAppFlowy(); - - // tap open button - await mockGetDirectoryPath(folderName); - await tester.tapOpenFolderButton(); - - await tester.wait(1000); - await tester.expectToSeeWelcomePage(); - - final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - // Necessary for being able to enterText when not in debug mode - binding.testTextInput.register(); - - // Needs tab to obtain focus for the app flowy editor. - // by default the tap appears at the center of the widget. - final Finder editor = find.byType(AppFlowyEditor); - await tester.tap(editor); - await tester.pumpAndSettle(); - - // tester.sendText() cannot be used since the editor - // does not contain any EditableText widgets. - // to interact with the app during an integration test, - // simulate physical keyboard events. - await simulateKeyDownEvent(LogicalKeyboardKey.enter); - await tester.pumpAndSettle(); - await simulateKeyDownEvent(LogicalKeyboardKey.enter); - await tester.pumpAndSettle(); - await simulateKeyDownEvent(LogicalKeyboardKey.arrowLeft); - await tester.pumpAndSettle(); - await simulateKeyDownEvent(LogicalKeyboardKey.slash); - await tester.pumpAndSettle(); - await simulateKeyDownEvent(LogicalKeyboardKey.keyG); - await tester.pumpAndSettle(); - await simulateKeyDownEvent(LogicalKeyboardKey.keyR); - await tester.pumpAndSettle(); - await simulateKeyDownEvent(LogicalKeyboardKey.keyI); - await tester.pumpAndSettle(); - await simulateKeyDownEvent(LogicalKeyboardKey.keyD); - await tester.pumpAndSettle(); - await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown); - await tester.pumpAndSettle(); - - // Checks whether the options in the selection menu - // for /grid exist. - expect(find.byType(SelectionMenuItemWidget), findsAtLeastNWidgets(2)); - - // Finalizes the slash command that creates the board. - await simulateKeyDownEvent(LogicalKeyboardKey.enter); - await tester.pumpAndSettle(); - - // Checks whether new board is referenced and properly on the page. - expect(find.byType(BuiltInPageWidget), findsOneWidget); - - // Checks whether the new board is in the side bar. - final sidebarLabel = LocaleKeys.newPageText.tr(); - expect(find.text(sidebarLabel), findsOneWidget); - }); }); } diff --git a/frontend/appflowy_flutter/integration_test/util/data.dart b/frontend/appflowy_flutter/integration_test/util/data.dart new file mode 100644 index 0000000000..a527950489 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/util/data.dart @@ -0,0 +1,66 @@ +import 'dart:io'; + +import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart'; +import 'package:archive/archive_io.dart'; +import 'package:flutter/services.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +enum TestWorkspace { + board("board"), + emptyDocument("empty_document"); + + const TestWorkspace(this._name); + + final String _name; + + Future get zip async { + final Directory parent = await TestWorkspace._parent; + final File out = File(p.join(parent.path, '$_name.zip')); + if (await out.exists()) return out; + await out.create(); + final ByteData data = await rootBundle.load(_asset); + await out.writeAsBytes(data.buffer.asUint8List()); + return out; + } + + Future get root async { + final Directory parent = await TestWorkspace._parent; + return Directory(p.join(parent.path, _name)); + } + + static Future get _parent async { + final Directory root = await getTemporaryDirectory(); + if (await root.exists()) return root; + await root.create(); + return root; + } + + String get _asset => 'assets/test/workspaces/$_name.zip'; +} + +class TestWorkspaceService { + const TestWorkspaceService(this.workspace); + + final TestWorkspace workspace; + + /// Instructs the application to read workspace data from the workspace found under this [TestWorkspace]'s path. + Future setUpAll() async { + SharedPreferences.setMockInitialValues( + { + kSettingsLocationDefaultLocation: + await workspace.root.then((value) => value.path), + }, + ); + } + + /// Workspaces that are checked into source are compressed. [TestWorkspaceService.setUp()] decompresses the file into an ephemeral directory that will be ignored by source control. + Future setUp() async { + final inputStream = + InputFileStream(await workspace.zip.then((value) => value.path)); + final archive = ZipDecoder().decodeBuffer(inputStream); + extractArchiveToDisk( + archive, await TestWorkspace._parent.then((value) => value.path)); + } +} diff --git a/frontend/appflowy_flutter/integration_test/util/keyboard.dart b/frontend/appflowy_flutter/integration_test/util/keyboard.dart new file mode 100644 index 0000000000..02bbdb79f1 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/util/keyboard.dart @@ -0,0 +1,12 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart' as flutter_test; + +class FlowyTestKeyboard { + static Future simulateKeyDownEvent(List keys, + {required flutter_test.WidgetTester tester}) async { + for (final LogicalKeyboardKey key in keys) { + await flutter_test.simulateKeyDownEvent(key); + await tester.pumpAndSettle(); + } + } +} diff --git a/frontend/appflowy_flutter/integration_test/util/util.dart b/frontend/appflowy_flutter/integration_test/util/util.dart index 1b4b7c3272..20606483d3 100644 --- a/frontend/appflowy_flutter/integration_test/util/util.dart +++ b/frontend/appflowy_flutter/integration_test/util/util.dart @@ -1,3 +1,4 @@ export 'base.dart'; export 'launch.dart'; export 'settings.dart'; +export 'data.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_service.dart index 4d12cae839..b6ca40b630 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_service.dart @@ -5,7 +5,7 @@ import 'package:dartz/dartz.dart'; class DatabaseBackendService { static Future, FlowyError>> - getAllDatabase() { + getAllDatabases() { return DatabaseEventGetDatabases().send().then((result) { return result.fold((l) => left(l.items), (r) => right(r)); }); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/base/insert_page_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/base/insert_page_command.dart index 8d4afd917e..ff12cc9afd 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/base/insert_page_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/base/insert_page_command.dart @@ -1,20 +1,55 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/database_view_service.dart'; import 'package:appflowy/plugins/document/presentation/plugins/board/board_node_widget.dart'; import 'package:appflowy/plugins/document/presentation/plugins/grid/grid_node_widget.dart'; +import 'package:appflowy/workspace/application/app/app_service.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/app.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; const String kAppID = 'app_id'; const String kViewID = 'view_id'; extension InsertPage on EditorState { - void insertPage(AppPB appPB, ViewPB viewPB) { + Future insertPage(AppPB appPB, ViewPB viewPB) async { final selection = service.selectionService.currentSelection.value; final textNodes = service.selectionService.currentSelectedNodes.whereType(); if (selection == null || textNodes.isEmpty) { return; } + + // get the database that the view is associated with + final database = + await DatabaseViewBackendService(viewId: viewPB.id).openGrid().then( + (value) => value.getLeftOrNull(), + ); + + if (database == null) { + throw StateError( + 'The database associated with ${viewPB.id} could not be found while attempting to create a referenced ${viewPB.layout.name}.'); + } + + final prefix = referencedBoardPrefix(viewPB.layout); + + final ref = await AppBackendService().createView( + appId: appPB.id, + name: "$prefix ${viewPB.name}", + desc: appPB.desc, + layoutType: viewPB.layout, + ext: { + 'database_id': database.id, + }, + ).then( + (value) => value.getLeftOrNull(), + ); + + // TODO(a-wallen): Show error dialog here. + if (ref == null) { + return; + } + final transaction = this.transaction; transaction.insertNode( selection.end.path, @@ -22,13 +57,24 @@ extension InsertPage on EditorState { type: _convertPageType(viewPB), attributes: { kAppID: appPB.id, - kViewID: viewPB.id, + kViewID: ref.id, }, ), ); apply(transaction); } + String referencedBoardPrefix(ViewLayoutTypePB layout) { + switch (layout) { + case ViewLayoutTypePB.Grid: + return LocaleKeys.grid_referencedGridPrefix.tr(); + case ViewLayoutTypePB.Board: + return LocaleKeys.board_referencedBoardPrefix.tr(); + default: + throw UnimplementedError(); + } + } + String _convertPageType(ViewPB viewPB) { switch (viewPB.layout) { case ViewLayoutTypePB.Grid: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 801a062534..613b205df1 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -95,6 +95,7 @@ dependencies: http: ^0.13.5 json_annotation: ^4.7.0 path: ^1.8.2 + archive: ^3.3.0 dev_dependencies: flutter_lints: ^2.0.1 @@ -163,30 +164,8 @@ flutter: - assets/images/common/ - assets/images/grid/setting/ - assets/translations/ - # - images/a_dot_ham.jpeg - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages + # The following assets will be excluded in release. + # BEGIN: EXCLUDE_IN_RELEASE + - assets/test/workspaces/ + # END: EXCLUDE_IN_RELEASE diff --git a/frontend/scripts/flutter_release_build/build_flowy.dart b/frontend/scripts/flutter_release_build/build_flowy.dart new file mode 100644 index 0000000000..a7c2a9ada4 --- /dev/null +++ b/frontend/scripts/flutter_release_build/build_flowy.dart @@ -0,0 +1,28 @@ +import 'dart:io'; + +part 'tool.dart'; + +const excludeTagBegin = 'BEGIN: EXCLUDE_IN_RELEASE'; +const excludeTagEnd = 'END: EXCLUDE_IN_RELEASE'; + +Future main(List args) async { + const help = ''' +A build script that modifies build assets before building the release version of AppFlowy. + +args[0]: The directory that contains the AppFlowy git repository. Should be the parent to appflowy_flutter. (absolute path) +args[1]: The appflowy version to be built (github ref_name). +'''; + const numArgs = 2; + assert(args.length == numArgs, + 'Expected ${numArgs}, got ${args.length}. Read the following for instructions about how to use this script.\n\n$help'); + if (args[0] == '-h' || args[0] == '--help') { + stdout.write(help); + stdout.flush(); + } + final repositoryRoot = Directory(args[0]); + assert(await repositoryRoot.exists(), + '$repositoryRoot is an invalid directory. Please try again with a valid directory.\n\n$help'); + final appVersion = args[1]; + await _BuildTool(repositoryRoot: repositoryRoot.path, appVersion: appVersion) + .run(); +} diff --git a/frontend/scripts/flutter_release_build/tool.dart b/frontend/scripts/flutter_release_build/tool.dart new file mode 100644 index 0000000000..36a0a2f177 --- /dev/null +++ b/frontend/scripts/flutter_release_build/tool.dart @@ -0,0 +1,115 @@ +part of 'build_flowy.dart'; + +enum _ScanMode { + ignore, + target, +} + +enum _ModifyMode { + include, + exclude, +} + +class _BuildTool { + const _BuildTool({ + required this.repositoryRoot, + required this.appVersion, + }); + + final String repositoryRoot; + final String appVersion; + + String get projectRoot => + [repositoryRoot, 'appflowy_flutter'].join(Platform.pathSeparator); + + File get pubspec => + File([projectRoot, 'pubspec.yaml'].join(Platform.pathSeparator)); + + Future get _architecture async => + await Process.run('uname', ['-m']).then((value) => value.stdout.trim()); + + Future get _commandForOS async { + // Check the operating system and CPU architecture + var os = Platform.operatingSystem; + var arch = Platform.isMacOS ? await _architecture : Platform.localHostname; + + // Determine the appropriate command based on the OS and architecture + if (os == 'windows') { + return 'cargo make --env APP_VERSION=$appVersion --profile production-windows-x86 appflowy'; + } + + if (os == 'linux') { + return 'cargo make --env APP_VERSION=$appVersion --profile production-linux-x86_64 appflowy'; + } + + if (os == 'macos') { + if (arch == 'x86_64') { + return 'cargo make --env APP_VERSION=$appVersion --profile production-mac-x86_64 appflowy'; + } + if (arch == 'arm64') { + return 'cargo make --env APP_VERSION=$appVersion --profile production-mac-arm64 appflowy'; + } + throw 'Unsupported CPU architecture: $arch'; + } + + throw 'Unsupported operating system: $os'; + } + + /// Scans a file for lines between # BEGIN: EXCLUDE_IN_RELEASE and + /// END: EXCLUDE_IN_RELEASE. Will add a comment to remove those assets + /// from the build. + Future _process_directives( + File file, { + required _ModifyMode mode, + }) async { + // Read the contents of the file into a list + var lines = await file.readAsLines(); + + // Find the lines between BEGIN: EXCLUDE_IN_RELEASE and END: EXCLUDE_IN_RELEASE + var scanMode = _ScanMode.ignore; + for (var i = 0; i < lines.length; i++) { + var line = lines[i]; + if (line.contains(excludeTagBegin)) { + scanMode = _ScanMode.target; + } else if (line.contains(excludeTagEnd)) { + scanMode = _ScanMode.ignore; + } else if (scanMode == _ScanMode.target) { + lines[i] = _modify(line, mode: mode); + } + } + + // Write the modified contents back to the file + await file.writeAsString(lines.join('\n')); + } + + String _modify(String line, {required _ModifyMode mode}) { + switch (mode) { + case _ModifyMode.include: + return line.split('#').where((element) => element != '#').join(); + case _ModifyMode.exclude: + return '#$line'; + } + } + + Future _build() async { + final cwd = Directory.current; + Directory.current = repositoryRoot; + + final cmd = await _commandForOS; + // Run the command using the Process.run() function + // final build = await Process.run('echo', ['hello'], runInShell: true); + final build = + await Process.start(cmd.split(' ')[0], cmd.split(' ').sublist(1)); + await stdout.addStream(build.stdout); + await stderr.addStream(build.stderr); + Directory.current = cwd; + } + + Future run() async { + final pubspec = this.pubspec; + + await _process_directives(pubspec, mode: _ModifyMode.exclude); + await _build(); + await _process_directives(pubspec, mode: _ModifyMode.include); + } +}