Add board group test (#1376)

This commit is contained in:
Nathan.fooo 2022-10-26 22:36:34 +08:00 committed by GitHub
parent c8044a92d1
commit 3bbf91ab2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 291 additions and 97 deletions

View File

@ -47,6 +47,7 @@ PROTOBUF_DERIVE_CACHE = "../shared-lib/flowy-derive/src/derive_cache/derive_cach
# Test default config
TEST_CRATE_TYPE = "cdylib"
TEST_LIB_EXT = "dylib"
TEST_RUST_LOG = "info"
TEST_BUILD_FLAG = "debug"
TEST_COMPILE_TARGET = "x86_64-apple-darwin"

View File

@ -34,7 +34,8 @@ class BoardDataController {
// key: the block id
final LinkedHashMap<String, GridBlockCache> _blocks;
LinkedHashMap<String, GridBlockCache> get blocks => _blocks;
UnmodifiableMapView<String, GridBlockCache> get blocks =>
UnmodifiableMapView(_blocks);
OnFieldsChanged? _onFieldsChanged;
OnGridChanged? _onGridChanged;
@ -113,15 +114,16 @@ class BoardDataController {
() => result.fold(
(grid) async {
_onGridChanged?.call(grid);
return await fieldController.loadFields(fieldIds: grid.fields).then(
(result) => result.fold(
(l) {
_loadGroups(grid.blocks);
return left(l);
},
(err) => right(err),
),
);
final result = await fieldController.loadFields(
fieldIds: grid.fields,
);
return result.fold(
(l) {
_loadGroups(grid.blocks);
return left(l);
},
(err) => right(err),
);
},
(err) => right(err),
),

View File

@ -15,6 +15,12 @@ class TypeOptionDataController {
late FieldTypeOptionDataPB _data;
final PublishNotifier<FieldPB> _fieldNotifier = PublishNotifier();
/// Returns a [TypeOptionDataController] used to modify the specified
/// [FieldPB]'s data
///
/// Should call [loadTypeOptionData] if the passed-in [GridFieldContext]
/// is null
///
TypeOptionDataController({
required this.gridId,
required this.loader,
@ -77,18 +83,17 @@ class TypeOptionDataController {
);
}
Future<void> switchToField(FieldType newFieldType) {
return loader.switchToField(field.id, newFieldType).then((result) {
return result.fold(
(_) {
// Should load the type-option data after switching to a new field.
// After loading the type-option data, the editor widget that uses
// the type-option data will be rebuild.
loadTypeOptionData();
},
(err) => Log.error(err),
);
});
Future<void> switchToField(FieldType newFieldType) async {
final result = await loader.switchToField(field.id, newFieldType);
await result.fold(
(_) {
// Should load the type-option data after switching to a new field.
// After loading the type-option data, the editor widget that uses
// the type-option data will be rebuild.
loadTypeOptionData();
},
(err) => Future(() => Log.error(err)),
);
}
void Function() addFieldListener(void Function(FieldPB) callback) {

View File

@ -1,3 +1,4 @@
import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@ -13,9 +14,10 @@ class GridGroupBloc extends Bloc<GridGroupEvent, GridGroupState> {
final SettingFFIService _settingFFIService;
Function(List<GridFieldContext>)? _onFieldsFn;
GridGroupBloc(
{required String viewId, required GridFieldController fieldController})
: _fieldController = fieldController,
GridGroupBloc({
required String viewId,
required GridFieldController fieldController,
}) : _fieldController = fieldController,
_settingFFIService = SettingFFIService(viewId: viewId),
super(GridGroupState.initial(viewId, fieldController.fieldContexts)) {
on<GridGroupEvent>(
@ -27,11 +29,12 @@ class GridGroupBloc extends Bloc<GridGroupEvent, GridGroupState> {
didReceiveFieldUpdate: (fieldContexts) {
emit(state.copyWith(fieldContexts: fieldContexts));
},
setGroupByField: (String fieldId, FieldType fieldType) {
_settingFFIService.groupByField(
setGroupByField: (String fieldId, FieldType fieldType) async {
final result = await _settingFFIService.groupByField(
fieldId: fieldId,
fieldType: fieldType,
);
result.fold((l) => null, (err) => Log.error(err));
},
);
},

View File

@ -21,8 +21,8 @@ class FieldEditor extends StatefulWidget {
final String fieldName;
final bool isGroupField;
final Function(String)? onDeleted;
final IFieldTypeOptionLoader typeOptionLoader;
const FieldEditor({
required this.gridId,
this.fieldName = "",

View File

@ -11,13 +11,13 @@ void main() {
boardTest = await AppFlowyBoardTest.ensureInitialized();
});
group('description', () {
group('$BoardBloc', () {
late BoardBloc boardBloc;
late String groupId;
setUp(() async {
await boardTest.createTestBoard();
boardBloc = BoardBloc(view: boardTest.boardView)
await boardTest.context.createTestBoard();
boardBloc = BoardBloc(view: boardTest.context.gridView)
..add(const BoardEvent.initial());
await boardResponseFuture();
groupId = boardBloc.state.groupIds.first;

View File

@ -0,0 +1,113 @@
import 'package:app_flowy/plugins/board/application/board_bloc.dart';
import 'package:app_flowy/plugins/grid/application/field/field_editor_bloc.dart';
import 'package:app_flowy/plugins/grid/application/setting/group_bloc.dart';
import 'package:bloc_test/bloc_test.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pbserver.dart';
import 'package:flutter_test/flutter_test.dart';
import 'util.dart';
void main() {
late AppFlowyBoardTest boardTest;
setUpAll(() async {
boardTest = await AppFlowyBoardTest.ensureInitialized();
});
// Group with not support grouping field
group('Group with not support grouping field:', () {
late FieldEditorBloc editorBloc;
setUpAll(() async {
await boardTest.context.createTestBoard();
final fieldContext = boardTest.context.singleSelectFieldContext();
editorBloc = boardTest.context.createFieldEditor(
fieldContext: fieldContext,
)..add(const FieldEditorEvent.initial());
await boardResponseFuture();
});
blocTest<FieldEditorBloc, FieldEditorState>(
"switch to text field",
build: () => editorBloc,
wait: boardResponseDuration(),
act: (bloc) async {
await bloc.dataController.switchToField(FieldType.RichText);
},
verify: (bloc) {
bloc.state.field.fold(
() => throw Exception(),
(field) => field.fieldType == FieldType.RichText,
);
},
);
blocTest<BoardBloc, BoardState>(
'assert the number of groups is 1',
build: () => BoardBloc(view: boardTest.context.gridView)
..add(const BoardEvent.initial()),
wait: boardResponseDuration(),
verify: (bloc) {
assert(bloc.groupControllers.values.length == 1,
"Expected 1, but receive ${bloc.groupControllers.values.length}");
},
);
});
// Group by checkbox field
group('Group by checkbox field:', () {
late BoardBloc boardBloc;
late FieldPB checkboxField;
setUpAll(() async {
await boardTest.context.createTestBoard();
});
setUp(() async {
boardBloc = BoardBloc(view: boardTest.context.gridView)
..add(const BoardEvent.initial());
await boardResponseFuture();
});
blocTest<BoardBloc, BoardState>(
"initial",
build: () => boardBloc,
wait: boardResponseDuration(),
verify: (bloc) {
assert(bloc.groupControllers.values.length == 4);
assert(boardTest.context.fieldContexts.length == 2);
},
);
test('create checkbox field', () async {
await boardTest.context.createFieldFromType(FieldType.Checkbox);
await boardResponseFuture();
assert(boardTest.context.fieldContexts.length == 3);
checkboxField = boardTest.context.fieldContexts.last.field;
assert(checkboxField.fieldType == FieldType.Checkbox);
});
blocTest<GridGroupBloc, GridGroupState>(
"set grouped by checkbox field",
build: () => GridGroupBloc(
viewId: boardTest.context.gridView.id,
fieldController: boardTest.context.fieldController,
),
act: (bloc) async {
bloc.add(GridGroupEvent.setGroupByField(
checkboxField.id,
checkboxField.fieldType,
));
},
wait: boardResponseDuration(),
);
blocTest<BoardBloc, BoardState>(
"check the number of groups is 2",
build: () => boardBloc,
wait: boardResponseDuration(),
verify: (bloc) {
assert(bloc.groupControllers.values.length == 2);
},
);
});
}

View File

@ -1,36 +1,13 @@
import 'package:app_flowy/plugins/board/board.dart';
import 'package:app_flowy/workspace/application/app/app_service.dart';
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
import '../../util.dart';
import '../grid_test/util.dart';
class AppFlowyBoardTest {
final AppFlowyUnitTest _inner;
late ViewPB boardView;
AppFlowyBoardTest(AppFlowyUnitTest unitTest) : _inner = unitTest;
final AppFlowyGridTest context;
AppFlowyBoardTest(this.context);
static Future<AppFlowyBoardTest> ensureInitialized() async {
final inner = await AppFlowyUnitTest.ensureInitialized();
final inner = await AppFlowyGridTest.ensureInitialized();
return AppFlowyBoardTest(inner);
}
Future<void> createTestBoard() async {
final app = await _inner.createTestApp();
final builder = BoardPluginBuilder();
final result = await AppService().createView(
appId: app.id,
name: "Test Board",
dataFormatType: builder.dataFormatType,
pluginType: builder.pluginType,
layoutType: builder.layoutType!,
);
await result.fold(
(view) async {
boardView = view;
},
(error) {},
);
}
}
Future<void> boardResponseFuture() {

View File

@ -1,8 +1,12 @@
import 'dart:collection';
import 'package:app_flowy/plugins/board/application/board_data_controller.dart';
import 'package:app_flowy/plugins/board/board.dart';
import 'package:app_flowy/plugins/grid/application/block/block_cache.dart';
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
import 'package:app_flowy/plugins/grid/application/field/field_editor_bloc.dart';
import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
import 'package:app_flowy/plugins/grid/application/grid_data_controller.dart';
import 'package:app_flowy/plugins/grid/application/row/row_bloc.dart';
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
@ -16,29 +20,90 @@ import '../../util.dart';
/// Create a empty Grid for test
class AppFlowyGridTest {
final AppFlowyUnitTest _inner;
final AppFlowyUnitTest unitTest;
late ViewPB gridView;
late GridDataController _dataController;
GridDataController? _gridDataController;
BoardDataController? _boardDataController;
AppFlowyGridTest(AppFlowyUnitTest unitTest) : _inner = unitTest;
AppFlowyGridTest({required this.unitTest});
static Future<AppFlowyGridTest> ensureInitialized() async {
final inner = await AppFlowyUnitTest.ensureInitialized();
return AppFlowyGridTest(inner);
return AppFlowyGridTest(unitTest: inner);
}
List<RowInfo> get rowInfos => _dataController.rowInfos;
List<RowInfo> get rowInfos {
if (_gridDataController != null) {
return _gridDataController!.rowInfos;
}
UnmodifiableMapView<String, GridBlockCache> get blocks =>
_dataController.blocks;
if (_boardDataController != null) {
return _boardDataController!.rowInfos;
}
List<GridFieldContext> get fieldContexts =>
_dataController.fieldController.fieldContexts;
throw Exception();
}
GridFieldController get fieldController => _dataController.fieldController;
UnmodifiableMapView<String, GridBlockCache> get blocks {
if (_gridDataController != null) {
return _gridDataController!.blocks;
}
if (_boardDataController != null) {
return _boardDataController!.blocks;
}
throw Exception();
}
List<GridFieldContext> get fieldContexts => fieldController.fieldContexts;
GridFieldController get fieldController {
if (_gridDataController != null) {
return _gridDataController!.fieldController;
}
if (_boardDataController != null) {
return _boardDataController!.fieldController;
}
throw Exception();
}
Future<void> createRow() async {
await _dataController.createRow();
if (_gridDataController != null) {
return _gridDataController!.createRow();
}
throw Exception();
}
FieldEditorBloc createFieldEditor({
GridFieldContext? fieldContext,
}) {
IFieldTypeOptionLoader loader;
if (fieldContext == null) {
loader = NewFieldTypeOptionLoader(gridId: gridView.id);
} else {
loader =
FieldTypeOptionLoader(gridId: gridView.id, field: fieldContext.field);
}
final editorBloc = FieldEditorBloc(
fieldName: fieldContext?.name ?? '',
isGroupField: fieldContext?.isGroupField ?? false,
loader: loader,
gridId: gridView.id,
);
return editorBloc;
}
Future<FieldEditorBloc> createFieldFromType(FieldType fieldType) async {
final editor = createFieldEditor()..add(const FieldEditorEvent.initial());
await gridResponseFuture();
editor.dataController.switchToField(fieldType);
await gridResponseFuture();
return Future(() => editor);
}
GridFieldContext singleSelectFieldContext() {
@ -53,7 +118,7 @@ class AppFlowyGridTest {
}
Future<void> createTestGrid() async {
final app = await _inner.createTestApp();
final app = await unitTest.createTestApp();
final builder = GridPluginBuilder();
final result = await AppService().createView(
appId: app.id,
@ -65,13 +130,34 @@ class AppFlowyGridTest {
await result.fold(
(view) async {
gridView = view;
_dataController = GridDataController(view: view);
final result = await _dataController.openGrid();
_gridDataController = GridDataController(view: view);
final result = await _gridDataController!.openGrid();
result.fold((l) => null, (r) => throw Exception(r));
},
(error) {},
);
}
Future<void> createTestBoard() async {
final app = await unitTest.createTestApp();
final builder = BoardPluginBuilder();
final result = await AppService().createView(
appId: app.id,
name: "Test Board",
dataFormatType: builder.dataFormatType,
pluginType: builder.pluginType,
layoutType: builder.layoutType!,
);
await result.fold(
(view) async {
_boardDataController = BoardDataController(view: view);
final result = await _boardDataController!.openGrid();
result.fold((l) => null, (r) => throw Exception(r));
gridView = view;
},
(error) {},
);
}
}
/// Create a new Grid for cell test
@ -101,7 +187,7 @@ class AppFlowyGridCellTest {
final rowDataController = GridRowDataController(
rowInfo: rowInfo,
fieldController: _gridTest._dataController.fieldController,
fieldController: _gridTest._gridDataController!.fieldController,
rowCache: rowCache!,
);

View File

@ -66,7 +66,7 @@ void main() {
wait: blocResponseDuration(),
);
test('description', () async {
test('check the latest view is the document', () async {
assert(homeBloc.state.workspaceSetting.latestView.id ==
latestCreatedView.id);
});

View File

@ -165,8 +165,8 @@ void main() {
act: (bloc) async {
for (final view in appBloc.state.app.belongings.items) {
appBloc.add(AppEvent.deleteView(view.id));
await blocResponseFuture();
}
await blocResponseFuture();
trashBloc.add(const TrashEvent.deleteAll());
},
wait: blocResponseDuration(),

View File

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

View File

@ -36,7 +36,7 @@ pub trait FolderPersistenceTransaction {
fn read_view(&self, view_id: &str) -> FlowyResult<ViewRevision>;
fn read_views(&self, belong_to_id: &str) -> FlowyResult<Vec<ViewRevision>>;
fn update_view(&self, changeset: ViewChangeset) -> FlowyResult<()>;
fn delete_view(&self, view_id: &str) -> FlowyResult<()>;
fn delete_view(&self, view_id: &str) -> FlowyResult<ViewRevision>;
fn move_view(&self, view_id: &str, from: usize, to: usize) -> FlowyResult<()>;
fn create_trash(&self, trashes: Vec<TrashRevision>) -> FlowyResult<()>;

View File

@ -84,9 +84,10 @@ impl<'a> FolderPersistenceTransaction for V1Transaction<'a> {
Ok(())
}
fn delete_view(&self, view_id: &str) -> FlowyResult<()> {
fn delete_view(&self, view_id: &str) -> FlowyResult<ViewRevision> {
let view_revision: ViewRevision = ViewTableSql::read_view(view_id, &*self.0)?.into();
let _ = ViewTableSql::delete_view(view_id, &*self.0)?;
Ok(())
Ok(view_revision)
}
fn move_view(&self, _view_id: &str, _from: usize, _to: usize) -> FlowyResult<()> {
@ -182,7 +183,7 @@ where
(**self).update_view(changeset)
}
fn delete_view(&self, view_id: &str) -> FlowyResult<()> {
fn delete_view(&self, view_id: &str) -> FlowyResult<ViewRevision> {
(**self).delete_view(view_id)
}

View File

@ -113,11 +113,12 @@ impl FolderPersistenceTransaction for FolderEditor {
Ok(())
}
fn delete_view(&self, view_id: &str) -> FlowyResult<()> {
if let Some(change) = self.folder.write().delete_view(view_id)? {
fn delete_view(&self, view_id: &str) -> FlowyResult<ViewRevision> {
let view = self.folder.read().read_view(view_id)?;
if let Some(change) = self.folder.write().delete_view(&view.app_id, view_id)? {
let _ = self.apply_change(change)?;
}
Ok(())
Ok(view)
}
fn move_view(&self, view_id: &str, from: usize, to: usize) -> FlowyResult<()> {
@ -207,7 +208,7 @@ where
(**self).update_view(changeset)
}
fn delete_view(&self, view_id: &str) -> FlowyResult<()> {
fn delete_view(&self, view_id: &str) -> FlowyResult<ViewRevision> {
(**self).delete_view(view_id)
}

View File

@ -462,10 +462,10 @@ async fn handle_trash_event(
let mut notify_ids = HashSet::new();
let mut views = vec![];
for identifier in identifiers.items {
let view = transaction.read_view(&identifier.id)?;
let _ = transaction.delete_view(&view.id)?;
notify_ids.insert(view.app_id.clone());
views.push(view);
if let Ok(view_rev) = transaction.delete_view(&identifier.id) {
notify_ids.insert(view_rev.app_id.clone());
views.push(view_rev);
}
}
for notify_id in notify_ids {
let _ = notify_views_changed(&notify_id, trash_can.clone(), &transaction)?;
@ -480,9 +480,7 @@ async fn handle_trash_event(
Ok(processor) => {
let _ = processor.close_view(&view.id).await?;
}
Err(e) => {
tracing::error!("{}", e)
}
Err(e) => tracing::error!("{}", e),
}
}
Ok(())

View File

@ -136,7 +136,7 @@ pub(crate) async fn switch_to_field_handler(
.switch_to_field_type(&params.field_id, &params.field_type)
.await?;
// Get the field_rev with field_id, if it doesn't exist, we create the default FieldMeta from the FieldType.
// Get the field_rev with field_id, if it doesn't exist, we create the default FieldRevision from the FieldType.
let field_rev = editor
.get_field_rev(&params.field_id)
.await

View File

@ -1,4 +1,12 @@
[tasks.dart_unit_test]
dependencies = ["build-test-lib"]
description = "Run flutter unit tests"
script = '''
cd app_flowy
flutter test --dart-define=RUST_LOG=${TEST_RUST_LOG}
'''
[tasks.rust_unit_test]
run_task = { name = ["rust_lib_unit_test", "shared_lib_unit_test"] }

View File

@ -258,9 +258,8 @@ impl FolderPad {
}
#[tracing::instrument(level = "trace", skip(self), err)]
pub fn delete_view(&mut self, view_id: &str) -> CollaborateResult<Option<FolderChangeset>> {
let view = self.read_view(view_id)?;
self.with_app(&view.app_id, |app| {
pub fn delete_view(&mut self, app_id: &str, view_id: &str) -> CollaborateResult<Option<FolderChangeset>> {
self.with_app(app_id, |app| {
app.belongings.retain(|view| view.id != view_id);
Ok(Some(()))
})
@ -724,7 +723,7 @@ mod tests {
#[test]
fn folder_delete_view() {
let (mut folder, initial_operations, view) = test_view_folder();
let operations = folder.delete_view(&view.id).unwrap().unwrap().operations;
let operations = folder.delete_view(&view.app_id, &view.id).unwrap().unwrap().operations;
let new_folder = make_folder_from_operations(initial_operations, vec![operations]);
assert_folder_equal(