chore: add reorder bloc test (#1354)

* chore: add reorder bloc test

* chore: add trash test

Co-authored-by: nathan <nathan@appflowy.io>
This commit is contained in:
Nathan.fooo 2022-10-25 16:51:51 +08:00 committed by GitHub
parent 6fb677d346
commit 67e4a759c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 315 additions and 108 deletions

View File

@ -10,14 +10,16 @@ import 'package:app_flowy/plugins/trash/application/trash_listener.dart';
part 'trash_bloc.freezed.dart';
class TrashBloc extends Bloc<TrashEvent, TrashState> {
final TrashService service;
final TrashListener listener;
TrashBloc({required this.service, required this.listener})
: super(TrashState.init()) {
final TrashService _service;
final TrashListener _listener;
TrashBloc()
: _service = TrashService(),
_listener = TrashListener(),
super(TrashState.init()) {
on<TrashEvent>((event, emit) async {
await event.map(initial: (e) async {
listener.start(trashUpdated: _listenTrashUpdated);
final result = await service.readTrash();
_listener.start(trashUpdated: _listenTrashUpdated);
final result = await _service.readTrash();
emit(result.fold(
(object) => state.copyWith(
objects: object.items, successOrFailure: left(unit)),
@ -26,17 +28,17 @@ class TrashBloc extends Bloc<TrashEvent, TrashState> {
}, didReceiveTrash: (e) async {
emit(state.copyWith(objects: e.trash));
}, putback: (e) async {
final result = await service.putback(e.trashId);
final result = await _service.putback(e.trashId);
await _handleResult(result, emit);
}, delete: (e) async {
final result =
await service.deleteViews([Tuple2(e.trash.id, e.trash.ty)]);
await _service.deleteViews([Tuple2(e.trash.id, e.trash.ty)]);
await _handleResult(result, emit);
}, deleteAll: (e) async {
final result = await service.deleteAll();
final result = await _service.deleteAll();
await _handleResult(result, emit);
}, restoreAll: (e) async {
final result = await service.restoreAll();
final result = await _service.restoreAll();
await _handleResult(result, emit);
});
});
@ -63,7 +65,7 @@ class TrashBloc extends Bloc<TrashEvent, TrashState> {
@override
Future<void> close() async {
await listener.close();
await _listener.close();
return super.close();
}
}

View File

@ -92,14 +92,6 @@ void _resolveFolderDeps(GetIt getIt) {
),
);
//Menu
getIt.registerFactoryParam<MenuBloc, UserProfilePB, String>(
(user, workspaceId) => MenuBloc(
workspaceId: workspaceId,
listener: getIt<WorkspaceListener>(param1: user, param2: workspaceId),
),
);
getIt.registerFactoryParam<MenuUserBloc, UserProfilePB, void>(
(user, _) => MenuUserBloc(user),
);
@ -123,10 +115,7 @@ void _resolveFolderDeps(GetIt getIt) {
getIt.registerLazySingleton<TrashService>(() => TrashService());
getIt.registerLazySingleton<TrashListener>(() => TrashListener());
getIt.registerFactory<TrashBloc>(
() => TrashBloc(
service: getIt<TrashService>(),
listener: getIt<TrashListener>(),
),
() => TrashBloc(),
);
}

View File

@ -39,9 +39,9 @@ class FlowyRunner {
// add task
getIt<AppLauncher>().addTask(InitRustSDKTask());
getIt<AppLauncher>().addTask(PluginLoadTask());
if (!env.isTest()) {
getIt<AppLauncher>().addTask(PluginLoadTask());
getIt<AppLauncher>().addTask(InitAppWidgetTask());
getIt<AppLauncher>().addTask(InitPlatformServiceTask());
}

View File

@ -18,11 +18,10 @@ import 'package:dartz/dartz.dart';
part 'app_bloc.freezed.dart';
class AppBloc extends Bloc<AppEvent, AppState> {
final AppPB app;
final AppService appService;
final AppListener appListener;
AppBloc({required this.app})
AppBloc({required AppPB app})
: appService = AppService(),
appListener = AppListener(appId: app.id),
super(AppState.initial(app)) {
@ -34,8 +33,6 @@ class AppBloc extends Bloc<AppEvent, AppState> {
await _createView(value, emit);
}, loadViews: (_) async {
await _loadViews(emit);
}, didReceiveViewUpdated: (e) async {
await _didReceiveViewUpdated(e.views, emit);
}, delete: (e) async {
await _deleteApp(emit);
}, deleteView: (deletedView) async {
@ -43,23 +40,26 @@ class AppBloc extends Bloc<AppEvent, AppState> {
}, rename: (e) async {
await _renameView(e, emit);
}, appDidUpdate: (e) async {
emit(state.copyWith(app: e.app));
final latestCreatedView = state.latestCreatedView;
final views = e.app.belongings.items;
AppState newState = state.copyWith(
views: views,
app: e.app,
);
if (latestCreatedView != null) {
final index =
views.indexWhere((element) => element.id == latestCreatedView.id);
if (index == -1) {
newState = newState.copyWith(latestCreatedView: null);
}
}
emit(newState);
});
});
}
void _startListening() {
appListener.start(
onViewsChanged: (result) {
result.fold(
(views) {
if (!isClosed) {
add(AppEvent.didReceiveViewUpdated(views));
}
},
(error) => Log.error(error),
);
},
onAppUpdated: (app) {
if (!isClosed) {
add(AppEvent.appDidUpdate(app));
@ -69,7 +69,8 @@ class AppBloc extends Bloc<AppEvent, AppState> {
}
Future<void> _renameView(Rename e, Emitter<AppState> emit) async {
final result = await appService.updateApp(appId: app.id, name: e.newName);
final result =
await appService.updateApp(appId: state.app.id, name: e.newName);
result.fold(
(l) => emit(state.copyWith(successOrFailure: left(unit))),
(error) => emit(state.copyWith(successOrFailure: right(error))),
@ -78,7 +79,7 @@ class AppBloc extends Bloc<AppEvent, AppState> {
// Delete the current app
Future<void> _deleteApp(Emitter<AppState> emit) async {
final result = await appService.delete(appId: app.id);
final result = await appService.delete(appId: state.app.id);
result.fold(
(unit) => emit(state.copyWith(successOrFailure: left(unit))),
(error) => emit(state.copyWith(successOrFailure: right(error))),
@ -95,7 +96,7 @@ class AppBloc extends Bloc<AppEvent, AppState> {
Future<void> _createView(CreateView value, Emitter<AppState> emit) async {
final result = await appService.createView(
appId: app.id,
appId: state.app.id,
name: value.name,
desc: value.desc ?? "",
dataFormatType: value.pluginBuilder.dataFormatType,
@ -120,25 +121,8 @@ class AppBloc extends Bloc<AppEvent, AppState> {
return super.close();
}
Future<void> _didReceiveViewUpdated(
List<ViewPB> views,
Emitter<AppState> emit,
) async {
final latestCreatedView = state.latestCreatedView;
AppState newState = state.copyWith(views: views);
if (latestCreatedView != null) {
final index =
views.indexWhere((element) => element.id == latestCreatedView.id);
if (index == -1) {
newState = newState.copyWith(latestCreatedView: null);
}
}
emit(newState);
}
Future<void> _loadViews(Emitter<AppState> emit) async {
final viewsOrFailed = await appService.getViews(appId: app.id);
final viewsOrFailed = await appService.getViews(appId: state.app.id);
viewsOrFailed.fold(
(views) => emit(state.copyWith(views: views)),
(error) {
@ -161,8 +145,6 @@ class AppEvent with _$AppEvent {
const factory AppEvent.delete() = DeleteApp;
const factory AppEvent.deleteView(String viewId) = DeleteView;
const factory AppEvent.rename(String newName) = Rename;
const factory AppEvent.didReceiveViewUpdated(List<ViewPB> views) =
ReceiveViews;
const factory AppEvent.appDidUpdate(AppPB app) = AppDidUpdate;
}

View File

@ -11,11 +11,11 @@ import 'package:flowy_sdk/protobuf/flowy-folder/dart_notification.pb.dart';
import 'package:flowy_sdk/rust_stream.dart';
typedef AppDidUpdateCallback = void Function(AppPB app);
typedef ViewsDidChangeCallback = void Function(Either<List<ViewPB>, FlowyError> viewsOrFailed);
typedef ViewsDidChangeCallback = void Function(
Either<List<ViewPB>, FlowyError> viewsOrFailed);
class AppListener {
StreamSubscription<SubscribeObject>? _subscription;
ViewsDidChangeCallback? _viewsChanged;
AppDidUpdateCallback? _updated;
FolderNotificationParser? _parser;
String appId;
@ -24,26 +24,16 @@ class AppListener {
required this.appId,
});
void start({ViewsDidChangeCallback? onViewsChanged, AppDidUpdateCallback? onAppUpdated}) {
_viewsChanged = onViewsChanged;
void start({AppDidUpdateCallback? onAppUpdated}) {
_updated = onAppUpdated;
_parser = FolderNotificationParser(id: appId, callback: _bservableCallback);
_subscription = RustStreamReceiver.listen((observable) => _parser?.parse(observable));
_parser = FolderNotificationParser(id: appId, callback: _handleCallback);
_subscription =
RustStreamReceiver.listen((observable) => _parser?.parse(observable));
}
void _bservableCallback(FolderNotification ty, Either<Uint8List, FlowyError> result) {
void _handleCallback(
FolderNotification ty, Either<Uint8List, FlowyError> result) {
switch (ty) {
case FolderNotification.AppViewsChanged:
if (_viewsChanged != null) {
result.fold(
(payload) {
final repeatedView = RepeatedViewPB.fromBuffer(payload);
_viewsChanged!(left(repeatedView.items));
},
(error) => _viewsChanged!(right(error)),
);
}
break;
case FolderNotification.AppUpdated:
if (_updated != null) {
result.fold(
@ -63,7 +53,6 @@ class AppListener {
Future<void> stop() async {
_parser = null;
await _subscription?.cancel();
_viewsChanged = null;
_updated = null;
}
}

View File

@ -6,6 +6,8 @@ import 'package:dartz/dartz.dart';
import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-folder/app.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-folder/workspace.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -13,16 +15,23 @@ part 'menu_bloc.freezed.dart';
class MenuBloc extends Bloc<MenuEvent, MenuState> {
final WorkspaceService _workspaceService;
final WorkspaceListener listener;
final String workspaceId;
final WorkspaceListener _listener;
final UserProfilePB user;
final WorkspacePB workspace;
MenuBloc({required this.workspaceId, required this.listener})
: _workspaceService = WorkspaceService(workspaceId: workspaceId),
super(MenuState.initial()) {
MenuBloc({
required this.user,
required this.workspace,
}) : _workspaceService = WorkspaceService(workspaceId: workspace.id),
_listener = WorkspaceListener(
user: user,
workspaceId: workspace.id,
),
super(MenuState.initial(workspace)) {
on<MenuEvent>((event, emit) async {
await event.map(
initial: (e) async {
listener.start(appsChanged: _handleAppsOrFail);
_listener.start(appsChanged: _handleAppsOrFail);
await _fetchApps(emit);
},
openPage: (e) async {
@ -55,7 +64,7 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> {
@override
Future<void> close() async {
await listener.stop();
await _listener.stop();
return super.close();
}
@ -110,8 +119,8 @@ class MenuState with _$MenuState {
required Plugin plugin,
}) = _MenuState;
factory MenuState.initial() => MenuState(
apps: [],
factory MenuState.initial(WorkspacePB workspace) => MenuState(
apps: workspace.apps.items,
successOrFailure: left(unit),
plugin: makePlugin(pluginType: PluginType.blank),
);

View File

@ -51,8 +51,10 @@ class HomeMenu extends StatelessWidget {
providers: [
BlocProvider<MenuBloc>(
create: (context) {
final menuBloc = getIt<MenuBloc>(
param1: user, param2: workspaceSetting.workspace.id);
final menuBloc = MenuBloc(
user: user,
workspace: workspaceSetting.workspace,
);
menuBloc.add(const MenuEvent.initial());
return menuBloc;
},
@ -221,8 +223,7 @@ class MenuTopBar extends StatelessWidget {
const Spacer(),
Tooltip(
richMessage: TextSpan(children: [
TextSpan(
text: "${LocaleKeys.sideBar_closeSidebar.tr()}\n"),
TextSpan(text: "${LocaleKeys.sideBar_closeSidebar.tr()}\n"),
TextSpan(
text: Platform.isMacOS ? "⌘+\\" : "Ctrl+\\",
style: const TextStyle(color: Colors.white60),

View File

@ -2,6 +2,7 @@ import 'package:app_flowy/plugins/board/board.dart';
import 'package:app_flowy/plugins/doc/document.dart';
import 'package:app_flowy/plugins/grid/grid.dart';
import 'package:app_flowy/workspace/application/app/app_bloc.dart';
import 'package:app_flowy/workspace/application/menu/menu_view_section_bloc.dart';
import 'package:flowy_sdk/protobuf/flowy-folder/app.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter_test/flutter_test.dart';
@ -125,4 +126,64 @@ void main() {
},
);
});
group('$AppBloc', () {
late AppPB app;
setUpAll(() async {
app = await test.createTestApp();
});
blocTest<AppBloc, AppState>(
"create documents' order test",
build: () => AppBloc(app: app)..add(const AppEvent.initial()),
act: (bloc) async {
bloc.add(AppEvent.createView("1", DocumentPluginBuilder()));
await blocResponseFuture();
bloc.add(AppEvent.createView("2", DocumentPluginBuilder()));
await blocResponseFuture();
bloc.add(AppEvent.createView("3", DocumentPluginBuilder()));
await blocResponseFuture();
},
wait: blocResponseDuration(),
verify: (bloc) {
assert(bloc.state.views[0].name == '1');
assert(bloc.state.views[1].name == '2');
assert(bloc.state.views[2].name == '3');
},
);
});
group('$AppBloc', () {
late AppPB app;
setUpAll(() async {
app = await test.createTestApp();
});
blocTest<AppBloc, AppState>(
"reorder documents",
build: () => AppBloc(app: app)..add(const AppEvent.initial()),
act: (bloc) async {
bloc.add(AppEvent.createView("1", DocumentPluginBuilder()));
await blocResponseFuture();
bloc.add(AppEvent.createView("2", DocumentPluginBuilder()));
await blocResponseFuture();
bloc.add(AppEvent.createView("3", DocumentPluginBuilder()));
await blocResponseFuture();
final appViewData = AppViewDataContext(appId: app.id);
appViewData.views = bloc.state.views;
final viewSectionBloc = ViewSectionBloc(
appViewData: appViewData,
)..add(const ViewSectionEvent.initial());
await blocResponseFuture();
viewSectionBloc.add(const ViewSectionEvent.moveView(0, 2));
await blocResponseFuture();
},
wait: blocResponseDuration(),
verify: (bloc) {
assert(bloc.state.views[0].name == '2');
assert(bloc.state.views[1].name == '3');
assert(bloc.state.views[2].name == '1');
},
);
});
}

View File

@ -0,0 +1,66 @@
import 'package:app_flowy/workspace/application/menu/menu_bloc.dart';
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../util.dart';
void main() {
late AppFlowyUnitTest test;
setUpAll(() async {
test = await AppFlowyUnitTest.ensureInitialized();
});
group('$MenuBloc', () {
late MenuBloc menuBloc;
setUp(() async {
menuBloc = MenuBloc(
user: test.userProfile,
workspace: test.currentWorkspace,
)..add(const MenuEvent.initial());
await blocResponseFuture();
});
blocTest<MenuBloc, MenuState>(
"assert initial apps is the build-in app",
build: () => menuBloc,
wait: blocResponseDuration(),
verify: (bloc) {
assert(bloc.state.apps.length == 1);
},
);
//
blocTest<MenuBloc, MenuState>(
"create apps",
build: () => menuBloc,
act: (bloc) async {
bloc.add(const MenuEvent.createApp("App 1"));
await blocResponseFuture();
bloc.add(const MenuEvent.createApp("App 2"));
await blocResponseFuture();
bloc.add(const MenuEvent.createApp("App 3"));
},
wait: blocResponseDuration(),
verify: (bloc) {
// apps[0] is the build-in app
assert(bloc.state.apps[1].name == 'App 1');
assert(bloc.state.apps[2].name == 'App 2');
assert(bloc.state.apps[3].name == 'App 3');
},
);
blocTest<MenuBloc, MenuState>(
"reorder apps",
build: () => menuBloc,
act: (bloc) async {
bloc.add(const MenuEvent.moveApp(1, 3));
},
wait: blocResponseDuration(),
verify: (bloc) {
assert(bloc.state.apps[1].name == 'App 2');
assert(bloc.state.apps[2].name == 'App 3');
assert(bloc.state.apps[3].name == 'App 1');
},
);
});
//
}

View File

@ -0,0 +1,110 @@
import 'package:app_flowy/plugins/doc/document.dart';
import 'package:app_flowy/plugins/trash/application/trash_bloc.dart';
import 'package:app_flowy/workspace/application/app/app_bloc.dart';
import 'package:bloc_test/bloc_test.dart';
import 'package:flowy_sdk/protobuf/flowy-folder/app.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../util.dart';
void main() {
late AppFlowyUnitTest test;
late AppPB app;
late AppBloc appBloc;
late List<ViewPB> allViews;
setUpAll(() async {
test = await AppFlowyUnitTest.ensureInitialized();
/// Create a new app with three documents
app = await test.createTestApp();
appBloc = AppBloc(app: app)
..add(const AppEvent.initial())
..add(AppEvent.createView(
"Document 1",
DocumentPluginBuilder(),
))
..add(AppEvent.createView(
"Document 2",
DocumentPluginBuilder(),
))
..add(
AppEvent.createView(
"Document 3",
DocumentPluginBuilder(),
),
);
await blocResponseFuture(millisecond: 200);
allViews = [...appBloc.state.app.belongings.items];
assert(allViews.length == 3);
});
group('$TrashBloc', () {
late TrashBloc trashBloc;
late ViewPB deletedView;
setUpAll(() {});
setUp(() async {
trashBloc = TrashBloc()..add(const TrashEvent.initial());
await blocResponseFuture();
});
blocTest<TrashBloc, TrashState>(
"delete view",
build: () => trashBloc,
act: (bloc) async {
deletedView = appBloc.state.app.belongings.items[0];
appBloc.add(AppEvent.deleteView(deletedView.id));
},
wait: blocResponseDuration(),
verify: (bloc) {
assert(appBloc.state.app.belongings.items.length == 2);
assert(bloc.state.objects.length == 1);
assert(bloc.state.objects.first.id == deletedView.id);
},
);
blocTest<TrashBloc, TrashState>(
"delete all views",
build: () => trashBloc,
act: (bloc) async {
for (final view in appBloc.state.app.belongings.items) {
appBloc.add(AppEvent.deleteView(view.id));
await blocResponseFuture();
}
},
wait: blocResponseDuration(),
verify: (bloc) {
assert(bloc.state.objects[0].id == allViews[0].id);
assert(bloc.state.objects[1].id == allViews[1].id);
assert(bloc.state.objects[2].id == allViews[2].id);
},
);
blocTest<TrashBloc, TrashState>(
"put back",
build: () => trashBloc,
act: (bloc) async {
bloc.add(TrashEvent.putback(allViews[0].id));
},
wait: blocResponseDuration(),
verify: (bloc) {
assert(appBloc.state.app.belongings.items.length == 1);
assert(bloc.state.objects.length == 2);
},
);
blocTest<TrashBloc, TrashState>(
"put back all",
build: () => trashBloc,
act: (bloc) async {
bloc.add(const TrashEvent.restoreAll());
},
wait: blocResponseDuration(),
verify: (bloc) {
assert(appBloc.state.app.belongings.items.length == 3);
assert(bloc.state.objects.isEmpty);
},
);
//
});
}

View File

@ -113,10 +113,10 @@ class FlowyTestApp implements EntryPoint {
}
}
Future<void> blocResponseFuture() {
return Future.delayed(const Duration(milliseconds: 100));
Future<void> blocResponseFuture({int millisecond = 100}) {
return Future.delayed(Duration(milliseconds: millisecond));
}
Duration blocResponseDuration({int millseconds = 100}) {
return Duration(milliseconds: millseconds);
Duration blocResponseDuration({int milliseconds = 100}) {
return Duration(milliseconds: milliseconds);
}

View File

@ -12,7 +12,6 @@ pub(crate) enum FolderNotification {
WorkspaceAppsChanged = 14,
WorkspaceSetting = 15,
AppUpdated = 21,
AppViewsChanged = 24,
ViewUpdated = 31,
ViewDeleted = 32,
ViewRestored = 33,

View File

@ -1,5 +1,5 @@
pub use crate::entities::view::ViewDataFormatPB;
use crate::entities::{DeletedViewPB, ViewInfoPB, ViewLayoutTypePB};
use crate::entities::{AppPB, DeletedViewPB, ViewInfoPB, ViewLayoutTypePB};
use crate::manager::{ViewDataProcessor, ViewDataProcessorMap};
use crate::{
dart_notification::{send_dart_notification, FolderNotification},
@ -531,16 +531,15 @@ fn notify_views_changed<'a>(
trash_controller: Arc<TrashController>,
transaction: &'a (dyn FolderPersistenceTransaction + 'a),
) -> FlowyResult<()> {
let items: Vec<ViewPB> = read_belonging_views_on_local(belong_to_id, trash_controller.clone(), transaction)?
.into_iter()
.map(|view_rev| view_rev.into())
.collect();
tracing::Span::current().record("view_count", &format!("{}", items.len()).as_str());
let mut app_rev = transaction.read_app(belong_to_id)?;
let trash_ids = trash_controller.read_trash_ids(transaction)?;
app_rev.belongings.retain(|view| !trash_ids.contains(&view.id));
let app: AppPB = app_rev.into();
let repeated_view = RepeatedViewPB { items };
send_dart_notification(belong_to_id, FolderNotification::AppViewsChanged)
.payload(repeated_view)
send_dart_notification(belong_to_id, FolderNotification::AppUpdated)
.payload(app)
.send();
Ok(())
}