feat: Create a "view" for all database references in a document (#2083)

* feat: add archive for compression

* feat: add service to manage zipped work spaces

* feat: export service in barrel file

* feat: ignore .ephemeral directory

* feat: add first compressed workspace file

* fix: directory path was wrong

* feat: add a somewhat useful test

* fix: move to same file (delete later)

* fix: use script path vs. working directory for CI

* fix: read from asset bundle instead of file system

* fix: workaround to run integration in multiple files on desktop (flutter/flutter#101031

* feat: remove .ephemeral from .gitignore, no longer created

* feat: document test changes

* fix: lucas suggestion

* feat: mark assets as excluded in pubspec.yaml

* feat: add class for build utilities

* feat: add script runner for release builds

* feat: add build script as task in flowy project

* fix: typo in pubspec.yaml

* chore: use constants for exclude tag

* feat: add appversion as argument to build tool

* feat: use dart script in release.yml

* chore: remove task

* fix: careless error

Co-authored-by: Mihir <84044317+squidrye@users.noreply.github.com>

* feat: add translations for view of

* fix: typo in getAllDatabase

* feat: add view of database

* fix: remove unused import

* fix: use effective dart typing

* fix: insertPage marked as async, should return future

* fix: Remove multi-line string

* fix: ref can be null

* fix: unused imports caused analyzer to fail

* feat: also fix. Add empty document as option and change name to _name

* chore: move referenced database tests to empty document test file

* feat: add test utilities

* feat: add new integration test on an empty document

* feat: register test in runner

* fix: missing reference in insert_page_command

* fix: analyzer errors

---------

Co-authored-by: Mihir <84044317+squidrye@users.noreply.github.com>
This commit is contained in:
Alex Wallen 2023-04-03 18:50:22 -10:00 committed by GitHub
parent 231fd38298
commit e2009c063b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 714 additions and 401 deletions

View File

@ -105,11 +105,11 @@ jobs:
working-directory: frontend/appflowy_flutter working-directory: frontend/appflowy_flutter
run: | run: |
if [ "$RUNNER_OS" == "Linux" ]; then 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 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 elif [ "$RUNNER_OS" == "Windows" ]; then
flutter test integration_test -d Windows --coverage flutter test integration_test/runner.dart -d Windows --coverage
fi fi
shell: bash shell: bash
@ -120,4 +120,3 @@ jobs:
# env_vars: ${{ matrix.os }} # env_vars: ${{ matrix.os }}
# fail_ci_if_error: true # fail_ci_if_error: true
# verbose: true # verbose: true

View File

@ -3,7 +3,7 @@ name: release
on: on:
push: push:
tags: tags:
- '*' - "*"
env: env:
FLUTTER_VERSION: "3.7.5" FLUTTER_VERSION: "3.7.5"
@ -136,7 +136,11 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
job: 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: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v3 uses: actions/checkout@v3
@ -172,7 +176,7 @@ jobs:
working-directory: frontend working-directory: frontend
run: | run: |
flutter config --enable-macos-desktop 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 - name: Create macOS dmg
run: | run: |
@ -225,9 +229,21 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
job: 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: 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: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v3 uses: actions/checkout@v3
@ -275,7 +291,7 @@ jobs:
working-directory: frontend working-directory: frontend
run: | run: |
flutter config --enable-linux-desktop 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 - name: Archive Assert
working-directory: ${{ env.LINUX_APP_RELEASE_PATH }} working-directory: ${{ env.LINUX_APP_RELEASE_PATH }}
@ -361,4 +377,4 @@ jobs:
- name: Notify Discord - name: Notify Discord
run: | 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 }}" 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 shell: bash

View File

@ -1,223 +1,225 @@
{ {
"version": "2.0.0", "version": "2.0.0",
// https://code.visualstudio.com/docs/editor/tasks // https://code.visualstudio.com/docs/editor/tasks
// https://gist.github.com/deadalusai/9e13e36d61ec7fb72148 // https://gist.github.com/deadalusai/9e13e36d61ec7fb72148
// ${workspaceRoot}: the root folder of the team // ${workspaceRoot}: the root folder of the team
// ${file}: the current opened file // ${file}: the current opened file
// ${fileBasename}: the current opened file's basename // ${fileBasename}: the current opened file's basename
// ${fileDirname}: the current opened file's dirname // ${fileDirname}: the current opened file's dirname
// ${fileExtname}: the current opened file's extension // ${fileExtname}: the current opened file's extension
// ${cwd}: the current working directory of the spawned process // ${cwd}: the current working directory of the spawned process
"tasks": [ "tasks": [
{ {
"label": "AF: Clean + Rebuild All", "label": "AF: Clean + Rebuild All",
"type": "shell", "type": "shell",
"dependsOrder": "sequence", "dependsOrder": "sequence",
"dependsOn": [ "dependsOn": [
"AF: Dart Clean", "AF: Dart Clean",
"AF: Flutter Clean", "AF: Flutter Clean",
"AF: Build Appflowy Core", "AF: Build Appflowy Core",
"AF: Flutter Pub Get", "AF: Flutter Pub Get",
"AF: Flutter Package Get", "AF: Flutter Package Get",
"AF: Generate Language Files", "AF: Generate Language Files",
"AF: Generate Freezed Files" "AF: Generate Freezed Files"
], ],
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "new" "panel": "new"
} }
}, },
{ {
"label": "AF: Clean + Rebuild All (Android)", "label": "AF: Clean + Rebuild All (Android)",
"type": "shell", "type": "shell",
"dependsOrder": "sequence", "dependsOrder": "sequence",
"dependsOn": [ "dependsOn": [
"AF: Dart Clean", "AF: Dart Clean",
"AF: Flutter Clean", "AF: Flutter Clean",
"AF: Build Appflowy Core_for_android", "AF: Build Appflowy Core_for_android",
"AF: Flutter Pub Get", "AF: Flutter Pub Get",
"AF: Flutter Package Get", "AF: Flutter Package Get",
"AF: Generate Language Files", "AF: Generate Language Files",
"AF: Generate Freezed Files" "AF: Generate Freezed Files"
], ],
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "new" "panel": "new"
} }
}, },
{ {
"label": "AF: Build Appflowy Core_for_android", "label": "AF: Build Appflowy Core_for_android",
"type": "shell", "type": "shell",
"command": "cargo make --profile development-android appflowy-core-dev-android", "command": "cargo make --profile development-android appflowy-core-dev-android",
"group": "build", "group": "build",
"options": { "options": {
"cwd": "${workspaceFolder}" "cwd": "${workspaceFolder}"
} }
}, },
{ {
"label": "AF: Build Appflowy Core", "label": "AF: Build Appflowy Core",
"type": "shell", "type": "shell",
"windows": { "windows": {
"command": "cargo make --profile development-windows-x86 appflowy-core-dev" "command": "cargo make --profile development-windows-x86 appflowy-core-dev"
}, },
"linux": { "linux": {
"command": "cargo make --profile \"development-linux-$(uname -m)\" appflowy-core-dev" "command": "cargo make --profile \"development-linux-$(uname -m)\" appflowy-core-dev"
}, },
"osx": { "osx": {
"command": "cargo make --profile \"development-mac-$(uname -m)\" appflowy-core-dev" "command": "cargo make --profile \"development-mac-$(uname -m)\" appflowy-core-dev"
}, },
"group": "build", "group": "build",
"options": { "options": {
"cwd": "${workspaceFolder}" "cwd": "${workspaceFolder}"
} }
}, },
{ {
"label": "AF: Code Gen", "label": "AF: Code Gen",
"type": "shell", "type": "shell",
"dependsOrder": "sequence", "dependsOrder": "sequence",
"dependsOn": [ "dependsOn": [
"AF: Flutter Clean", "AF: Flutter Clean",
"AF: Flutter Pub Get", "AF: Flutter Pub Get",
"AF: Flutter Package Get", "AF: Flutter Package Get",
"AF: Generate Language Files", "AF: Generate Language Files",
"AF: Generate Freezed Files" "AF: Generate Freezed Files"
], ],
"group": { "group": {
"kind": "build", "kind": "build",
"isDefault": true "isDefault": true
}, },
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "new" "panel": "new"
} }
}, },
{ {
"label": "AF: Flutter Clean", "label": "AF: Flutter Clean",
"type": "shell", "type": "shell",
"command": "flutter clean", "command": "flutter clean",
"options": { "options": {
"cwd": "${workspaceFolder}/appflowy_flutter" "cwd": "${workspaceFolder}/appflowy_flutter"
} }
}, },
{ {
"label": "AF: Flutter Pub Get", "label": "AF: Flutter Pub Get",
"type": "shell", "type": "shell",
"command": "flutter pub get", "command": "flutter pub get",
"options": { "options": {
"cwd": "${workspaceFolder}/appflowy_flutter" "cwd": "${workspaceFolder}/appflowy_flutter"
} }
}, },
{ {
"label": "AF: Flutter Package Get", "label": "AF: Flutter Package Get",
"type": "shell", "type": "shell",
"command": "flutter packages pub get", "command": "flutter packages pub get",
"options": { "options": {
"cwd": "${workspaceFolder}/appflowy_flutter" "cwd": "${workspaceFolder}/appflowy_flutter"
} }
}, },
{ {
"label": "AF: Generate Freezed Files", "label": "AF: Generate Freezed Files",
"type": "shell", "type": "shell",
"command": "flutter pub run build_runner build --delete-conflicting-outputs", "command": "flutter pub run build_runner build --delete-conflicting-outputs",
"options": { "options": {
"cwd": "${workspaceFolder}/appflowy_flutter" "cwd": "${workspaceFolder}/appflowy_flutter"
} }
}, },
{ {
"label": "AF: Generate Language Files", "label": "AF: Generate Language Files",
"type": "shell", "type": "shell",
"command": "sh ./scripts/generate_language_files.sh", "command": "sh ./scripts/generate_language_files.sh",
"windows": { "windows": {
"options": { "options": {
"shell": { "shell": {
"executable": "cmd.exe", "executable": "cmd.exe",
"args": [ "args": [
"/d", "/d",
"/c", "/c",
".\\scripts\\generate_language_files.cmd" ".\\scripts\\generate_language_files.cmd"
] ]
} }
} }
}, },
"group": "build", "group": "build",
"options": { "options": {
"cwd": "${workspaceFolder}" "cwd": "${workspaceFolder}"
} }
}, },
{ {
"label": "AF: Flutter Clean", "label": "AF: Flutter Clean",
"type": "shell", "type": "shell",
"command": "cargo make flutter_clean", "command": "cargo make flutter_clean",
"group": "build", "group": "build",
"options": { "options": {
"cwd": "${workspaceFolder}" "cwd": "${workspaceFolder}"
} }
}, },
{ {
"label": "AF: flutter build aar", "label": "AF: flutter build aar",
"type": "flutter", "type": "flutter",
"command": "flutter", "command": "flutter",
"args": [ "args": [
"build", "build",
"aar" "aar"
], ],
"group": "build", "group": "build",
"problemMatcher": [], "problemMatcher": [],
"detail": "appflowy_flutter" "detail": "appflowy_flutter"
}, },
{ {
"label": "AF: Tauri UI Dev", "label": "AF: Tauri UI Dev",
"type": "shell", "type": "shell",
"isBackground": true, "isBackground": true,
"command": "yarn", "command": "yarn",
"args": ["dev"], "args": [
"options": { "dev"
"cwd": "${workspaceFolder}/appflowy_tauri" ],
} "options": {
}, "cwd": "${workspaceFolder}/appflowy_tauri"
{ }
"label": "AF: Tauri UI Build", },
"type": "shell", {
"command": "npm run build", "label": "AF: Tauri UI Build",
"options": { "type": "shell",
"cwd": "${workspaceFolder}/appflowy_tauri" "command": "npm run build",
} "options": {
}, "cwd": "${workspaceFolder}/appflowy_tauri"
{ }
"label": "AF: Tauri Dev", },
"type": "shell", {
"command": "npm run tauri:dev", "label": "AF: Tauri Dev",
"options": { "type": "shell",
"cwd": "${workspaceFolder}/appflowy_tauri" "command": "npm run tauri:dev",
} "options": {
}, "cwd": "${workspaceFolder}/appflowy_tauri"
{ }
"label": "AF: Tauri Clean", },
"type": "shell", {
"command": "cargo make tauri_clean", "label": "AF: Tauri Clean",
"options": { "type": "shell",
"cwd": "${workspaceFolder}" "command": "cargo make tauri_clean",
} "options": {
}, "cwd": "${workspaceFolder}"
{ }
"label": "AF: Tauri Clean + Dev", },
"type": "shell", {
"dependsOrder": "sequence", "label": "AF: Tauri Clean + Dev",
"dependsOn": [ "type": "shell",
"AF: Tauri Clean", "dependsOrder": "sequence",
"AF: Tauri UI Dev" "dependsOn": [
], "AF: Tauri Clean",
"options": { "AF: Tauri UI Dev"
"cwd": "${workspaceFolder}" ],
} "options": {
}, "cwd": "${workspaceFolder}"
{ }
"label": "AF: Tauri ESLint", },
"type": "shell", {
"command": "npx eslint --fix src", "label": "AF: Tauri ESLint",
"options": { "type": "shell",
"cwd": "${workspaceFolder}/appflowy_tauri" "command": "npx eslint --fix src",
} "options": {
}, "cwd": "${workspaceFolder}/appflowy_tauri"
] }
} },
]
}

View File

@ -40,7 +40,7 @@ lib/generated_plugin_registrant.dart
lib/generated/ lib/generated/
# Freezed generated files # Freezed generated files
*.g.dart *.g.dart
*.freezed.dart *.freezed.dart
# Symbolication related # Symbolication related
@ -67,4 +67,4 @@ windows/flutter/dart_ffi/
**/**/*.so **/**/*.so
**/**/Brewfile.lock.json **/**/Brewfile.lock.json
**/.sandbox **/.sandbox
**/.vscode/ **/.vscode/

View File

@ -326,7 +326,8 @@
"checklist": { "checklist": {
"panelTitle": "Add an item" "panelTitle": "Add an item"
}, },
"menuName": "Grid" "menuName": "Grid",
"referencedGridPrefix": "View of"
}, },
"document": { "document": {
"menuName": "Document", "menuName": "Document",
@ -390,7 +391,8 @@
"column": { "column": {
"create_new_card": "New" "create_new_card": "New"
}, },
"menuName": "Board" "menuName": "Board",
"referencedBoardPrefix": "View of"
}, },
"calendar": { "calendar": {
"menuName": "Calendar", "menuName": "Calendar",

View File

@ -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);
});
});
}

View File

@ -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));
});
});
}

View File

@ -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();
}

View File

@ -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/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:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
@ -162,135 +157,5 @@ void main() {
await TestFolder.currentLocation(), 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);
});
}); });
} }

View File

@ -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<File> 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<Directory> get root async {
final Directory parent = await TestWorkspace._parent;
return Directory(p.join(parent.path, _name));
}
static Future<Directory> 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<void> 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<void> 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));
}
}

View File

@ -0,0 +1,12 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart' as flutter_test;
class FlowyTestKeyboard {
static Future<void> simulateKeyDownEvent(List<LogicalKeyboardKey> keys,
{required flutter_test.WidgetTester tester}) async {
for (final LogicalKeyboardKey key in keys) {
await flutter_test.simulateKeyDownEvent(key);
await tester.pumpAndSettle();
}
}
}

View File

@ -1,3 +1,4 @@
export 'base.dart'; export 'base.dart';
export 'launch.dart'; export 'launch.dart';
export 'settings.dart'; export 'settings.dart';
export 'data.dart';

View File

@ -5,7 +5,7 @@ import 'package:dartz/dartz.dart';
class DatabaseBackendService { class DatabaseBackendService {
static Future<Either<List<DatabaseDescriptionPB>, FlowyError>> static Future<Either<List<DatabaseDescriptionPB>, FlowyError>>
getAllDatabase() { getAllDatabases() {
return DatabaseEventGetDatabases().send().then((result) { return DatabaseEventGetDatabases().send().then((result) {
return result.fold((l) => left(l.items), (r) => right(r)); return result.fold((l) => left(l.items), (r) => right(r));
}); });

View File

@ -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/board/board_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/plugins/grid/grid_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/app.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
const String kAppID = 'app_id'; const String kAppID = 'app_id';
const String kViewID = 'view_id'; const String kViewID = 'view_id';
extension InsertPage on EditorState { extension InsertPage on EditorState {
void insertPage(AppPB appPB, ViewPB viewPB) { Future<void> insertPage(AppPB appPB, ViewPB viewPB) async {
final selection = service.selectionService.currentSelection.value; final selection = service.selectionService.currentSelection.value;
final textNodes = final textNodes =
service.selectionService.currentSelectedNodes.whereType<TextNode>(); service.selectionService.currentSelectedNodes.whereType<TextNode>();
if (selection == null || textNodes.isEmpty) { if (selection == null || textNodes.isEmpty) {
return; 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; final transaction = this.transaction;
transaction.insertNode( transaction.insertNode(
selection.end.path, selection.end.path,
@ -22,13 +57,24 @@ extension InsertPage on EditorState {
type: _convertPageType(viewPB), type: _convertPageType(viewPB),
attributes: { attributes: {
kAppID: appPB.id, kAppID: appPB.id,
kViewID: viewPB.id, kViewID: ref.id,
}, },
), ),
); );
apply(transaction); 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) { String _convertPageType(ViewPB viewPB) {
switch (viewPB.layout) { switch (viewPB.layout) {
case ViewLayoutTypePB.Grid: case ViewLayoutTypePB.Grid:

View File

@ -95,6 +95,7 @@ dependencies:
http: ^0.13.5 http: ^0.13.5
json_annotation: ^4.7.0 json_annotation: ^4.7.0
path: ^1.8.2 path: ^1.8.2
archive: ^3.3.0
dev_dependencies: dev_dependencies:
flutter_lints: ^2.0.1 flutter_lints: ^2.0.1
@ -163,30 +164,8 @@ flutter:
- assets/images/common/ - assets/images/common/
- assets/images/grid/setting/ - assets/images/grid/setting/
- assets/translations/ - assets/translations/
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see # The following assets will be excluded in release.
# https://flutter.dev/assets-and-images/#resolution-aware. # BEGIN: EXCLUDE_IN_RELEASE
- assets/test/workspaces/
# For details regarding adding assets from package dependencies, see # END: EXCLUDE_IN_RELEASE
# 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

View File

@ -0,0 +1,28 @@
import 'dart:io';
part 'tool.dart';
const excludeTagBegin = 'BEGIN: EXCLUDE_IN_RELEASE';
const excludeTagEnd = 'END: EXCLUDE_IN_RELEASE';
Future<void> main(List<String> 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();
}

View File

@ -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<String> get _architecture async =>
await Process.run('uname', ['-m']).then((value) => value.stdout.trim());
Future<String> 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<void> _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<void> _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<void> run() async {
final pubspec = this.pubspec;
await _process_directives(pubspec, mode: _ModifyMode.exclude);
await _build();
await _process_directives(pubspec, mode: _ModifyMode.include);
}
}