فهرست منبع

test: Folder event test (#2709)

* test: add event tests

* test: add folder event test

* ci: rust fmt
Nathan.fooo 1 سال پیش
والد
کامیت
4f2585baed

+ 4 - 5
frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart

@@ -109,13 +109,12 @@ class ViewBackendService {
     required int fromIndex,
     required int toIndex,
   }) {
-    final payload = MoveFolderItemPayloadPB.create()
-      ..itemId = viewId
+    final payload = MoveViewPayloadPB.create()
+      ..viewId = viewId
       ..from = fromIndex
-      ..to = toIndex
-      ..ty = MoveFolderItemType.MoveView;
+      ..to = toIndex;
 
-    return FolderEventMoveItem(payload).send();
+    return FolderEventMoveView(payload).send();
   }
 
   Future<List<(ViewPB, List<ViewPB>)>> fetchViews(

+ 5 - 11
frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart

@@ -5,12 +5,7 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:appflowy_backend/dispatch/dispatch.dart';
 import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'
-    show
-        CreateViewPayloadPB,
-        MoveFolderItemPayloadPB,
-        MoveFolderItemType,
-        ViewLayoutPB,
-        ViewPB;
+    show CreateViewPayloadPB, MoveViewPayloadPB, ViewLayoutPB, ViewPB;
 import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
 
 import 'package:appflowy/generated/locale_keys.g.dart';
@@ -69,12 +64,11 @@ class WorkspaceService {
     required int fromIndex,
     required int toIndex,
   }) {
-    final payload = MoveFolderItemPayloadPB.create()
-      ..itemId = appId
+    final payload = MoveViewPayloadPB.create()
+      ..viewId = appId
       ..from = fromIndex
-      ..to = toIndex
-      ..ty = MoveFolderItemType.MoveApp;
+      ..to = toIndex;
 
-    return FolderEventMoveItem(payload).send();
+    return FolderEventMoveView(payload).send();
   }
 }

+ 10 - 10
frontend/appflowy_tauri/src-tauri/Cargo.lock

@@ -99,7 +99,7 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
 [[package]]
 name = "appflowy-integrate"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=13b178#13b17802de31e75255b4303914042bdbb04361b2"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
 dependencies = [
  "anyhow",
  "collab",
@@ -1024,7 +1024,7 @@ dependencies = [
 [[package]]
 name = "collab"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=13b178#13b17802de31e75255b4303914042bdbb04361b2"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
 dependencies = [
  "anyhow",
  "bytes",
@@ -1042,7 +1042,7 @@ dependencies = [
 [[package]]
 name = "collab-client-ws"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=13b178#13b17802de31e75255b4303914042bdbb04361b2"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
 dependencies = [
  "bytes",
  "collab-sync",
@@ -1060,7 +1060,7 @@ dependencies = [
 [[package]]
 name = "collab-database"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=13b178#13b17802de31e75255b4303914042bdbb04361b2"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -1086,7 +1086,7 @@ dependencies = [
 [[package]]
 name = "collab-derive"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=13b178#13b17802de31e75255b4303914042bdbb04361b2"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1098,7 +1098,7 @@ dependencies = [
 [[package]]
 name = "collab-document"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=13b178#13b17802de31e75255b4303914042bdbb04361b2"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
 dependencies = [
  "anyhow",
  "collab",
@@ -1115,7 +1115,7 @@ dependencies = [
 [[package]]
 name = "collab-folder"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=13b178#13b17802de31e75255b4303914042bdbb04361b2"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
 dependencies = [
  "anyhow",
  "collab",
@@ -1134,7 +1134,7 @@ dependencies = [
 [[package]]
 name = "collab-persistence"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=13b178#13b17802de31e75255b4303914042bdbb04361b2"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
 dependencies = [
  "bincode",
  "chrono",
@@ -1154,7 +1154,7 @@ dependencies = [
 [[package]]
 name = "collab-plugins"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=13b178#13b17802de31e75255b4303914042bdbb04361b2"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -1184,7 +1184,7 @@ dependencies = [
 [[package]]
 name = "collab-sync"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=13b178#13b17802de31e75255b4303914042bdbb04361b2"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
 dependencies = [
  "bytes",
  "collab",

+ 5 - 7
frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/app/app_bd_svc.ts

@@ -1,7 +1,7 @@
 import {
   FolderEventCreateView,
   FolderEventDeleteView,
-  FolderEventMoveItem,
+  FolderEventMoveView,
   FolderEventReadView,
   FolderEventUpdateView,
   ViewLayoutPB,
@@ -10,8 +10,7 @@ import {
   CreateViewPayloadPB,
   RepeatedViewIdPB,
   ViewPB,
-  MoveFolderItemPayloadPB,
-  MoveFolderItemType,
+  MoveViewPayloadPB,
   FlowyError,
   ViewIdPB,
   UpdateViewPayloadPB,
@@ -95,13 +94,12 @@ export class AppBackendService {
   };
 
   moveView = (params: { view_id: string; fromIndex: number; toIndex: number }) => {
-    const payload = MoveFolderItemPayloadPB.fromObject({
-      item_id: params.view_id,
+    const payload = MoveViewPayloadPB.fromObject({
+      view_id: params.view_id,
       from: params.fromIndex,
       to: params.toIndex,
-      ty: MoveFolderItemType.MoveView,
     });
 
-    return FolderEventMoveItem(payload);
+    return FolderEventMoveView(payload);
   };
 }

+ 5 - 11
frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/workspace/workspace_bd_svc.ts

@@ -1,17 +1,11 @@
 import { Err, Ok } from 'ts-results';
 import {
   FolderEventCreateView,
-  FolderEventMoveItem,
+  FolderEventMoveView,
   FolderEventReadWorkspaceViews,
   FolderEventReadAllWorkspaces,
 } from '@/services/backend/events/flowy-folder2';
-import {
-  CreateViewPayloadPB,
-  FlowyError,
-  MoveFolderItemPayloadPB,
-  ViewLayoutPB,
-  WorkspaceIdPB,
-} from '@/services/backend';
+import { CreateViewPayloadPB, FlowyError, MoveViewPayloadPB, ViewLayoutPB, WorkspaceIdPB } from '@/services/backend';
 import assert from 'assert';
 
 export class WorkspaceBackendService {
@@ -56,11 +50,11 @@ export class WorkspaceBackendService {
   };
 
   moveApp = (params: { appId: string; fromIndex: number; toIndex: number }) => {
-    const payload = MoveFolderItemPayloadPB.fromObject({
-      item_id: params.appId,
+    const payload = MoveViewPayloadPB.fromObject({
+      view_id: params.appId,
       from: params.fromIndex,
       to: params.toIndex,
     });
-    return FolderEventMoveItem(payload);
+    return FolderEventMoveView(payload);
   };
 }

+ 1 - 0
frontend/rust-lib/Cargo.lock

@@ -1863,6 +1863,7 @@ dependencies = [
  "flowy-core",
  "flowy-folder2",
  "flowy-net",
+ "flowy-notification",
  "flowy-server",
  "flowy-user",
  "futures-util",

+ 8 - 25
frontend/rust-lib/flowy-folder2/src/entities/view.rs

@@ -57,14 +57,14 @@ pub fn view_pb_with_child_views(view: View, child_views: Vec<View>) -> ViewPB {
 #[derive(Eq, PartialEq, Hash, Debug, ProtoBuf_Enum, Clone)]
 pub enum ViewLayoutPB {
   Document = 0,
-  Grid = 3,
-  Board = 4,
-  Calendar = 5,
+  Grid = 1,
+  Board = 2,
+  Calendar = 3,
 }
 
 impl std::default::Default for ViewLayoutPB {
   fn default() -> Self {
-    ViewLayoutPB::Grid
+    ViewLayoutPB::Document
   }
 }
 
@@ -271,50 +271,33 @@ impl TryInto<UpdateViewParams> for UpdateViewPayloadPB {
   }
 }
 
-#[derive(ProtoBuf_Enum)]
-pub enum MoveFolderItemType {
-  MoveApp = 0,
-  MoveView = 1,
-}
-
-impl std::default::Default for MoveFolderItemType {
-  fn default() -> Self {
-    MoveFolderItemType::MoveApp
-  }
-}
-
 #[derive(Default, ProtoBuf)]
-pub struct MoveFolderItemPayloadPB {
+pub struct MoveViewPayloadPB {
   #[pb(index = 1)]
-  pub item_id: String,
+  pub view_id: String,
 
   #[pb(index = 2)]
   pub from: i32,
 
   #[pb(index = 3)]
   pub to: i32,
-
-  #[pb(index = 4)]
-  pub ty: MoveFolderItemType,
 }
 
 pub struct MoveViewParams {
   pub item_id: String,
   pub from: usize,
   pub to: usize,
-  pub ty: MoveFolderItemType,
 }
 
-impl TryInto<MoveViewParams> for MoveFolderItemPayloadPB {
+impl TryInto<MoveViewParams> for MoveViewPayloadPB {
   type Error = ErrorCode;
 
   fn try_into(self) -> Result<MoveViewParams, Self::Error> {
-    let view_id = ViewIdentify::parse(self.item_id)?.0;
+    let view_id = ViewIdentify::parse(self.view_id)?.0;
     Ok(MoveViewParams {
       item_id: view_id,
       from: self.from as usize,
       to: self.to as usize,
-      ty: self.ty,
     })
   }
 }

+ 2 - 2
frontend/rust-lib/flowy-folder2/src/event_handler.rs

@@ -5,7 +5,7 @@ use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataRes
 
 use crate::entities::{
   view_pb_without_child_views, CreateViewParams, CreateViewPayloadPB, CreateWorkspaceParams,
-  CreateWorkspacePayloadPB, ImportPB, MoveFolderItemPayloadPB, MoveViewParams, RepeatedTrashIdPB,
+  CreateWorkspacePayloadPB, ImportPB, MoveViewParams, MoveViewPayloadPB, RepeatedTrashIdPB,
   RepeatedTrashPB, RepeatedViewIdPB, RepeatedViewPB, RepeatedWorkspacePB, TrashIdPB,
   UpdateViewParams, UpdateViewPayloadPB, ViewIdPB, ViewPB, WorkspaceIdPB, WorkspacePB,
   WorkspaceSettingPB,
@@ -144,7 +144,7 @@ pub(crate) async fn close_view_handler(
 
 #[tracing::instrument(level = "debug", skip_all, err)]
 pub(crate) async fn move_view_handler(
-  data: AFPluginData<MoveFolderItemPayloadPB>,
+  data: AFPluginData<MoveViewPayloadPB>,
   folder: AFPluginState<Arc<Folder2Manager>>,
 ) -> Result<(), FlowyError> {
   let params: MoveViewParams = data.into_inner().try_into()?;

+ 3 - 3
frontend/rust-lib/flowy-folder2/src/event_map.rs

@@ -26,7 +26,7 @@ pub fn init(folder: Arc<Folder2Manager>) -> AFPlugin {
     .event(FolderEvent::DuplicateView, duplicate_view_handler)
     .event(FolderEvent::SetLatestView, set_latest_view_handler)
     .event(FolderEvent::CloseView, close_view_handler)
-    .event(FolderEvent::MoveItem, move_view_handler)
+    .event(FolderEvent::MoveView, move_view_handler)
     // Trash
     .event(FolderEvent::ReadTrash, read_trash_handler)
     .event(FolderEvent::PutbackTrash, putback_trash_handler)
@@ -97,8 +97,8 @@ pub enum FolderEvent {
   SetLatestView = 21,
 
   /// Move the view or app to another place
-  #[event(input = "MoveFolderItemPayloadPB")]
-  MoveItem = 22,
+  #[event(input = "MoveViewPayloadPB")]
+  MoveView = 22,
 
   /// Read the trash that was deleted by the user
   #[event(output = "RepeatedTrashPB")]

+ 1 - 25
frontend/rust-lib/flowy-folder2/tests/workspace/folder_test.rs

@@ -1,7 +1,5 @@
-use crate::script::{invalid_workspace_name_test_case, FolderScript::*, FolderTest};
+use crate::script::{FolderScript::*, FolderTest};
 use collab_folder::core::ViewLayout;
-use flowy_folder2::entities::CreateWorkspacePayloadPB;
-use flowy_test::{event_builder::*, FlowyCoreTest};
 
 #[tokio::test]
 async fn read_all_workspace_test() {
@@ -60,28 +58,6 @@ async fn create_parent_view_test() {
   test.run_scripts(vec![ReloadParentView(app.id)]).await;
 }
 
-#[tokio::test]
-async fn create_parent_view_with_invalid_name() {
-  for (name, code) in invalid_workspace_name_test_case() {
-    let sdk = FlowyCoreTest::new();
-    let request = CreateWorkspacePayloadPB {
-      name,
-      desc: "".to_owned(),
-    };
-    assert_eq!(
-      EventBuilder::new(sdk)
-        .event(flowy_folder2::event_map::FolderEvent::CreateWorkspace)
-        .payload(request)
-        .async_send()
-        .await
-        .error()
-        .unwrap()
-        .code,
-      code.value()
-    )
-  }
-}
-
 #[tokio::test]
 #[should_panic]
 async fn delete_parent_view_test() {

+ 1 - 9
frontend/rust-lib/flowy-folder2/tests/workspace/script.rs

@@ -1,5 +1,5 @@
 use collab_folder::core::ViewLayout;
-use flowy_error::ErrorCode;
+
 use flowy_folder2::entities::*;
 use flowy_folder2::event_map::FolderEvent::*;
 use flowy_test::event_builder::EventBuilder;
@@ -161,14 +161,6 @@ impl FolderTest {
     }
   }
 }
-
-pub fn invalid_workspace_name_test_case() -> Vec<(String, ErrorCode)> {
-  vec![
-    ("".to_owned(), ErrorCode::WorkspaceNameInvalid),
-    ("1234".repeat(100), ErrorCode::WorkspaceNameTooLong),
-  ]
-}
-
 pub async fn create_workspace(sdk: &FlowyCoreTest, name: &str, desc: &str) -> WorkspacePB {
   let request = CreateWorkspacePayloadPB {
     name: name.to_owned(),

+ 1 - 0
frontend/rust-lib/flowy-test/Cargo.toml

@@ -14,6 +14,7 @@ lib-dispatch = { path = "../lib-dispatch" }
 lib-ot = { path = "../../../shared-lib/lib-ot" }
 lib-infra = { path = "../../../shared-lib/lib-infra" }
 flowy-server = { path = "../flowy-server" }
+flowy-notification = { path = "../flowy-notification" }
 
 serde = { version = "1.0", features = ["derive"] }
 serde_json = {version = "1.0"}

+ 1 - 3
frontend/rust-lib/flowy-test/src/event_builder.rs

@@ -1,8 +1,7 @@
 use crate::FlowyCoreTest;
 use flowy_user::errors::FlowyError;
 use lib_dispatch::prelude::{
-  AFPluginDispatcher, AFPluginEventResponse, AFPluginFromBytes, AFPluginRequest, StatusCode,
-  ToBytes, *,
+  AFPluginDispatcher, AFPluginEventResponse, AFPluginFromBytes, AFPluginRequest, ToBytes, *,
 };
 use std::{
   convert::TryFrom,
@@ -87,7 +86,6 @@ impl EventBuilder {
 
   pub fn error(self) -> Option<FlowyError> {
     let response = self.get_response();
-    assert_eq!(response.status_code, StatusCode::Err);
     <AFPluginData<FlowyError>>::try_from(response.payload)
       .ok()
       .map(|data| data.into_inner())

+ 58 - 0
frontend/rust-lib/flowy-test/src/lib.rs

@@ -3,7 +3,9 @@ use parking_lot::RwLock;
 use std::env::temp_dir;
 use std::sync::Arc;
 
+use crate::event_builder::EventBuilder;
 use flowy_core::{AppFlowyCore, AppFlowyCoreConfig};
+use flowy_folder2::entities::{CreateViewPayloadPB, RepeatedViewIdPB, ViewPB, WorkspaceSettingPB};
 use flowy_user::entities::{AuthTypePB, UserProfilePB};
 
 use crate::user_event::{async_sign_up, init_user_setting, SignUpContext};
@@ -36,6 +38,12 @@ impl FlowyCoreTest {
     Self::default()
   }
 
+  pub async fn new_with_user() -> Self {
+    let test = Self::default();
+    test.sign_up().await;
+    test
+  }
+
   pub async fn sign_up(&self) -> SignUpContext {
     let auth_type = self.auth_type.read().clone();
     async_sign_up(self.inner.dispatcher(), auth_type).await
@@ -51,6 +59,46 @@ impl FlowyCoreTest {
     init_user_setting(self.inner.dispatcher()).await;
     context.user_profile
   }
+
+  pub async fn get_current_workspace(&self) -> WorkspaceSettingPB {
+    EventBuilder::new(self.clone())
+      .event(flowy_folder2::event_map::FolderEvent::GetCurrentWorkspace)
+      .async_send()
+      .await
+      .parse::<flowy_folder2::entities::WorkspaceSettingPB>()
+  }
+
+  pub async fn delete_view(&self, view_id: &str) {
+    let payload = RepeatedViewIdPB {
+      items: vec![view_id.to_string()],
+    };
+
+    // delete the view. the view will be moved to trash
+    EventBuilder::new(self.clone())
+      .event(flowy_folder2::event_map::FolderEvent::DeleteView)
+      .payload(payload)
+      .async_send()
+      .await;
+  }
+
+  pub async fn create_view(&self, parent_id: &str, name: String) -> ViewPB {
+    let payload = CreateViewPayloadPB {
+      parent_view_id: parent_id.to_string(),
+      name,
+      desc: "".to_string(),
+      thumbnail: None,
+      layout: Default::default(),
+      initial_data: vec![],
+      meta: Default::default(),
+      set_as_current: false,
+    };
+    EventBuilder::new(self.clone())
+      .event(flowy_folder2::event_map::FolderEvent::CreateView)
+      .payload(payload)
+      .async_send()
+      .await
+      .parse::<flowy_folder2::entities::ViewPB>()
+  }
 }
 
 impl std::ops::Deref for FlowyCoreTest {
@@ -60,3 +108,13 @@ impl std::ops::Deref for FlowyCoreTest {
     &self.inner
   }
 }
+
+// pub struct TestNotificationSender {
+//   pub(crate) sender: tokio::sync::mpsc::Sender<()>,
+// }
+//
+// impl NotificationSender for TestNotificationSender {
+//   fn send_subject(&self, subject: SubscribeObject) -> Result<(), String> {
+//     todo!()
+//   }
+// }

+ 1 - 0
frontend/rust-lib/flowy-test/tests/database/mod.rs

@@ -0,0 +1 @@
+mod test;

+ 1 - 0
frontend/rust-lib/flowy-test/tests/database/test.rs

@@ -0,0 +1 @@
+

+ 1 - 0
frontend/rust-lib/flowy-test/tests/folder/mod.rs

@@ -0,0 +1 @@
+mod test;

+ 390 - 0
frontend/rust-lib/flowy-test/tests/folder/test.rs

@@ -0,0 +1,390 @@
+use flowy_folder2::entities::*;
+use flowy_test::event_builder::EventBuilder;
+use flowy_test::FlowyCoreTest;
+use flowy_user::errors::ErrorCode;
+
+#[tokio::test]
+async fn create_workspace_event_test() {
+  let test = FlowyCoreTest::new_with_user().await;
+  let request = CreateWorkspacePayloadPB {
+    name: "my second workspace".to_owned(),
+    desc: "".to_owned(),
+  };
+  let resp = EventBuilder::new(test)
+    .event(flowy_folder2::event_map::FolderEvent::CreateWorkspace)
+    .payload(request)
+    .async_send()
+    .await
+    .parse::<flowy_folder2::entities::WorkspacePB>();
+  assert_eq!(resp.name, "my second workspace");
+}
+
+#[tokio::test]
+async fn open_workspace_event_test() {
+  let test = FlowyCoreTest::new_with_user().await;
+  let payload = CreateWorkspacePayloadPB {
+    name: "my second workspace".to_owned(),
+    desc: "".to_owned(),
+  };
+  // create a workspace
+  let resp_1 = EventBuilder::new(test.clone())
+    .event(flowy_folder2::event_map::FolderEvent::CreateWorkspace)
+    .payload(payload)
+    .async_send()
+    .await
+    .parse::<flowy_folder2::entities::WorkspacePB>();
+
+  // open the workspace
+  let payload = WorkspaceIdPB {
+    value: Some(resp_1.id.clone()),
+  };
+  let resp_2 = EventBuilder::new(test)
+    .event(flowy_folder2::event_map::FolderEvent::OpenWorkspace)
+    .payload(payload)
+    .async_send()
+    .await
+    .parse::<flowy_folder2::entities::WorkspacePB>();
+
+  assert_eq!(resp_1.id, resp_2.id);
+  assert_eq!(resp_1.name, resp_2.name);
+}
+
+#[tokio::test]
+async fn create_view_event_test() {
+  let test = FlowyCoreTest::new_with_user().await;
+  let current_workspace = test.get_current_workspace().await.workspace;
+  let view = test
+    .create_view(&current_workspace.id, format!("My first view"))
+    .await;
+  assert_eq!(view.parent_view_id, current_workspace.id);
+  assert_eq!(view.name, "My first view");
+  assert_eq!(view.layout, ViewLayoutPB::Document);
+}
+
+#[tokio::test]
+async fn delete_view_event_test() {
+  let test = FlowyCoreTest::new_with_user().await;
+  let current_workspace = test.get_current_workspace().await.workspace;
+  let view = test
+    .create_view(&current_workspace.id, format!("My first view"))
+    .await;
+  test.delete_view(&view.id).await;
+
+  // Try the read the view
+  let payload = ViewIdPB {
+    value: view.id.clone(),
+  };
+  let error = EventBuilder::new(test.clone())
+    .event(flowy_folder2::event_map::FolderEvent::ReadView)
+    .payload(payload)
+    .async_send()
+    .await
+    .error()
+    .unwrap();
+  assert_eq!(error.code, ErrorCode::RecordNotFound.value());
+}
+
+#[tokio::test]
+async fn put_back_trash_event_test() {
+  let test = FlowyCoreTest::new_with_user().await;
+  let current_workspace = test.get_current_workspace().await.workspace;
+  let view = test
+    .create_view(&current_workspace.id, format!("My first view"))
+    .await;
+  test.delete_view(&view.id).await;
+
+  // After delete view, the view will be moved to trash
+  let payload = ViewIdPB {
+    value: view.id.clone(),
+  };
+  let error = EventBuilder::new(test.clone())
+    .event(flowy_folder2::event_map::FolderEvent::ReadView)
+    .payload(payload)
+    .async_send()
+    .await
+    .error()
+    .unwrap();
+  assert_eq!(error.code, ErrorCode::RecordNotFound.value());
+
+  let payload = TrashIdPB {
+    id: view.id.clone(),
+  };
+  EventBuilder::new(test.clone())
+    .event(flowy_folder2::event_map::FolderEvent::PutbackTrash)
+    .payload(payload)
+    .async_send()
+    .await;
+
+  let payload = ViewIdPB {
+    value: view.id.clone(),
+  };
+  let error = EventBuilder::new(test.clone())
+    .event(flowy_folder2::event_map::FolderEvent::ReadView)
+    .payload(payload)
+    .async_send()
+    .await
+    .error();
+  assert!(error.is_none());
+}
+
+#[tokio::test]
+async fn delete_view_permanently_event_test() {
+  let test = FlowyCoreTest::new_with_user().await;
+  let current_workspace = test.get_current_workspace().await.workspace;
+  let view = test
+    .create_view(&current_workspace.id, format!("My first view"))
+    .await;
+  let payload = RepeatedViewIdPB {
+    items: vec![view.id.clone()],
+  };
+
+  // delete the view. the view will be moved to trash
+  EventBuilder::new(test.clone())
+    .event(flowy_folder2::event_map::FolderEvent::DeleteView)
+    .payload(payload)
+    .async_send()
+    .await;
+
+  let trash = EventBuilder::new(test.clone())
+    .event(flowy_folder2::event_map::FolderEvent::ReadTrash)
+    .async_send()
+    .await
+    .parse::<flowy_folder2::entities::RepeatedTrashPB>()
+    .items;
+  assert_eq!(trash.len(), 1);
+  assert_eq!(trash[0].id, view.id);
+
+  // delete the view from trash
+  let payload = RepeatedTrashIdPB {
+    items: vec![TrashIdPB {
+      id: view.id.clone(),
+    }],
+  };
+  EventBuilder::new(test.clone())
+    .event(flowy_folder2::event_map::FolderEvent::DeleteTrash)
+    .payload(payload)
+    .async_send()
+    .await;
+
+  // After delete the last view, the trash should be empty
+  let trash = EventBuilder::new(test.clone())
+    .event(flowy_folder2::event_map::FolderEvent::ReadTrash)
+    .async_send()
+    .await
+    .parse::<flowy_folder2::entities::RepeatedTrashPB>()
+    .items;
+  assert!(trash.is_empty());
+}
+
+#[tokio::test]
+async fn delete_all_trash_test() {
+  let test = FlowyCoreTest::new_with_user().await;
+  let current_workspace = test.get_current_workspace().await.workspace;
+
+  for i in 0..3 {
+    let view = test
+      .create_view(&current_workspace.id, format!("My {} view", i))
+      .await;
+    let payload = RepeatedViewIdPB {
+      items: vec![view.id.clone()],
+    };
+    // delete the view. the view will be moved to trash
+    EventBuilder::new(test.clone())
+      .event(flowy_folder2::event_map::FolderEvent::DeleteView)
+      .payload(payload)
+      .async_send()
+      .await;
+  }
+
+  let trash = EventBuilder::new(test.clone())
+    .event(flowy_folder2::event_map::FolderEvent::ReadTrash)
+    .async_send()
+    .await
+    .parse::<flowy_folder2::entities::RepeatedTrashPB>()
+    .items;
+  assert_eq!(trash.len(), 3);
+
+  // Delete all the trash
+  EventBuilder::new(test.clone())
+    .event(flowy_folder2::event_map::FolderEvent::DeleteAllTrash)
+    .async_send()
+    .await;
+
+  // After delete the last view, the trash should be empty
+  let trash = EventBuilder::new(test.clone())
+    .event(flowy_folder2::event_map::FolderEvent::ReadTrash)
+    .async_send()
+    .await
+    .parse::<flowy_folder2::entities::RepeatedTrashPB>()
+    .items;
+  assert!(trash.is_empty());
+}
+
+#[tokio::test]
+async fn multiple_hierarchy_view_test() {
+  let test = FlowyCoreTest::new_with_user().await;
+  let current_workspace = test.get_current_workspace().await.workspace;
+  for i in 1..4 {
+    let parent = test
+      .create_view(&current_workspace.id, format!("My {} view", i))
+      .await;
+    for j in 1..3 {
+      let child = test
+        .create_view(&parent.id, format!("My {}-{} view", i, j))
+        .await;
+      for k in 1..2 {
+        let _sub_child = test
+          .create_view(&child.id, format!("My {}-{}-{} view", i, j, k))
+          .await;
+      }
+    }
+  }
+
+  let mut views = EventBuilder::new(test.clone())
+    .event(flowy_folder2::event_map::FolderEvent::ReadWorkspaceViews)
+    .async_send()
+    .await
+    .parse::<flowy_folder2::entities::RepeatedViewPB>()
+    .items;
+
+  // There will be one default view when AppFlowy is initialized. So there will be 4 views in total
+  assert_eq!(views.len(), 4);
+  views.remove(0);
+
+  // workspace
+  //   - view1
+  //     - view1-1
+  //       - view1-1-1
+  //     - view1-2
+  //       - view1-2-1
+  //   - view2
+  //     - view2-1
+  //       - view2-1-1
+  //     - view2-2
+  //       - view2-2-1
+  //   - view3
+  //     - view3-1
+  //       - view3-1-1
+  //     - view3-2
+  //       - view3-2-1
+  assert_eq!(views[0].name, "My 1 view");
+  assert_eq!(views[1].name, "My 2 view");
+  assert_eq!(views[2].name, "My 3 view");
+
+  assert_eq!(views[0].child_views.len(), 2);
+  // By default only the first level of child views will be loaded
+  assert!(views[0].child_views[0].child_views.is_empty());
+
+  for (i, view) in views.into_iter().enumerate() {
+    for (j, child_view) in view.child_views.into_iter().enumerate() {
+      let payload = ViewIdPB {
+        value: child_view.id.clone(),
+      };
+
+      let child = EventBuilder::new(test.clone())
+        .event(flowy_folder2::event_map::FolderEvent::ReadView)
+        .payload(payload)
+        .async_send()
+        .await
+        .parse::<flowy_folder2::entities::ViewPB>();
+      assert_eq!(child.name, format!("My {}-{} view", i + 1, j + 1));
+      assert_eq!(child.child_views.len(), 1);
+      // By default only the first level of child views will be loaded
+      assert!(child.child_views[0].child_views.is_empty());
+
+      for (k, _child_view) in child_view.child_views.into_iter().enumerate() {
+        // Get the last level view
+        let sub_child = EventBuilder::new(test.clone())
+          .event(flowy_folder2::event_map::FolderEvent::ReadView)
+          .payload(ViewIdPB {
+            value: child.id.clone(),
+          })
+          .async_send()
+          .await
+          .parse::<flowy_folder2::entities::ViewPB>();
+
+        assert_eq!(child.name, format!("My {}-{}-{} view", i + 1, j + 1, k + 1));
+        assert!(sub_child.child_views.is_empty());
+      }
+    }
+  }
+}
+
+#[tokio::test]
+async fn move_view_event_test() {
+  let test = FlowyCoreTest::new_with_user().await;
+  let current_workspace = test.get_current_workspace().await.workspace;
+  for i in 1..4 {
+    let parent = test
+      .create_view(&current_workspace.id, format!("My {} view", i))
+      .await;
+    for j in 1..3 {
+      let _ = test
+        .create_view(&parent.id, format!("My {}-{} view", i, j))
+        .await;
+    }
+  }
+  let views = EventBuilder::new(test.clone())
+    .event(flowy_folder2::event_map::FolderEvent::ReadWorkspaceViews)
+    .async_send()
+    .await
+    .parse::<flowy_folder2::entities::RepeatedViewPB>()
+    .items;
+
+  // There will be one default view when AppFlowy is initialized. So there will be 4 views in total
+  assert_eq!(views.len(), 4);
+  assert_eq!(views[1].name, "My 1 view");
+  assert_eq!(views[2].name, "My 2 view");
+  assert_eq!(views[3].name, "My 3 view");
+
+  let payload = MoveViewPayloadPB {
+    view_id: views[1].id.clone(),
+    from: 1,
+    to: 2,
+  };
+  let _ = EventBuilder::new(test.clone())
+    .event(flowy_folder2::event_map::FolderEvent::MoveView)
+    .payload(payload)
+    .async_send()
+    .await;
+
+  let views = EventBuilder::new(test.clone())
+    .event(flowy_folder2::event_map::FolderEvent::ReadWorkspaceViews)
+    .async_send()
+    .await
+    .parse::<flowy_folder2::entities::RepeatedViewPB>()
+    .items;
+
+  assert_eq!(views[1].name, "My 2 view");
+  assert_eq!(views[2].name, "My 1 view");
+  assert_eq!(views[3].name, "My 3 view");
+}
+
+#[tokio::test]
+async fn create_parent_view_with_invalid_name() {
+  for (name, code) in invalid_workspace_name_test_case() {
+    let sdk = FlowyCoreTest::new();
+    let request = CreateWorkspacePayloadPB {
+      name,
+      desc: "".to_owned(),
+    };
+    assert_eq!(
+      EventBuilder::new(sdk)
+        .event(flowy_folder2::event_map::FolderEvent::CreateWorkspace)
+        .payload(request)
+        .async_send()
+        .await
+        .error()
+        .unwrap()
+        .code,
+      code.value()
+    )
+  }
+}
+
+fn invalid_workspace_name_test_case() -> Vec<(String, ErrorCode)> {
+  vec![
+    ("".to_owned(), ErrorCode::WorkspaceNameInvalid),
+    ("1234".repeat(100), ErrorCode::WorkspaceNameTooLong),
+  ]
+}

+ 2 - 0
frontend/rust-lib/flowy-test/tests/main.rs

@@ -1 +1,3 @@
+mod database;
+mod folder;
 mod user;