test: rewrite bloc tests (#6492)

This commit is contained in:
Richard Shiue 2024-10-07 16:41:20 +08:00 committed by GitHub
parent a763304386
commit 9ee39f45c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 1363 additions and 741 deletions

View File

@ -174,7 +174,8 @@ class FieldController {
(FilterChangesetNotificationPB changeset) {
_filterNotifier?.filters =
_filterListFromPBs(changeset.filters.items);
_updateFieldInfos();
_fieldNotifier.fieldInfos =
_updateFieldInfos(_fieldNotifier.fieldInfos);
},
(err) => Log.error(err),
);
@ -396,7 +397,7 @@ class FieldController {
(updatedFields, fieldInfos) =
await updateFields(changeset.updatedFields, fieldInfos);
_fieldNotifier.fieldInfos = fieldInfos;
_fieldNotifier.fieldInfos = _updateFieldInfos(fieldInfos);
for (final listener in _updatedFieldCallbacks.values) {
listener(updatedFields);
}
@ -465,25 +466,22 @@ class FieldController {
_fieldSettings.clear();
_fieldSettings.addAll(setting.fieldSettings.items);
_updateFieldInfos();
_fieldNotifier.fieldInfos = _updateFieldInfos(_fieldNotifier.fieldInfos);
}
/// Attach sort, filter, group information and field settings to `FieldInfo`
void _updateFieldInfos() {
final List<FieldInfo> newFieldInfos = [];
for (final field in _fieldNotifier.fieldInfos) {
newFieldInfos.add(
field.copyWith(
fieldSettings: _fieldSettings
.firstWhereOrNull((setting) => setting.fieldId == field.id),
isGroupField: _groupConfigurationByFieldId[field.id] != null,
hasFilter: getFilterByFieldId(field.id) != null,
hasSort: getSortByFieldId(field.id) != null,
),
);
}
_fieldNotifier.fieldInfos = newFieldInfos;
List<FieldInfo> _updateFieldInfos(List<FieldInfo> fieldInfos) {
return fieldInfos
.map(
(field) => field.copyWith(
fieldSettings: _fieldSettings
.firstWhereOrNull((setting) => setting.fieldId == field.id),
isGroupField: _groupConfigurationByFieldId[field.id] != null,
hasFilter: getFilterByFieldId(field.id) != null,
hasSort: getSortByFieldId(field.id) != null,
),
)
.toList();
}
/// Load all of the fields. This is required when opening the database
@ -506,7 +504,8 @@ class FieldController {
_loadAllFieldSettings(),
_loadSettings(),
]);
_updateFieldInfos();
_fieldNotifier.fieldInfos =
_updateFieldInfos(_fieldNotifier.fieldInfos);
return FlowyResult.success(null);
},

View File

@ -14,11 +14,12 @@ import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
import 'package:appflowy/util/int64_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:equatable/equatable.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/widgets.dart';
abstract class DatabaseFilter {
abstract class DatabaseFilter extends Equatable {
const DatabaseFilter({
required this.filterId,
required this.fieldId,
@ -112,16 +113,18 @@ abstract class DatabaseFilter {
}
final class TextFilter extends DatabaseFilter {
const TextFilter({
TextFilter({
required super.filterId,
required super.fieldId,
required super.fieldType,
required this.condition,
required this.content,
});
required String content,
}) {
this.content = canAttachContent ? content : "";
}
final TextFilterConditionPB condition;
final String content;
late final String content;
@override
String get conditionName => condition.filterName;
@ -182,19 +185,24 @@ final class TextFilter extends DatabaseFilter {
content: content ?? this.content,
);
}
@override
List<Object?> get props => [filterId, fieldId, condition, content];
}
final class NumberFilter extends DatabaseFilter {
const NumberFilter({
NumberFilter({
required super.filterId,
required super.fieldId,
required super.fieldType,
required this.condition,
required this.content,
});
required String content,
}) {
this.content = canAttachContent ? content : "";
}
final NumberFilterConditionPB condition;
final String content;
late final String content;
@override
String get conditionName => condition.filterName;
@ -253,6 +261,9 @@ final class NumberFilter extends DatabaseFilter {
content: content ?? this.content,
);
}
@override
List<Object?> get props => [filterId, fieldId, condition, content];
}
final class CheckboxFilter extends DatabaseFilter {
@ -289,6 +300,9 @@ final class CheckboxFilter extends DatabaseFilter {
condition: condition ?? this.condition,
);
}
@override
List<Object?> get props => [filterId, fieldId, condition];
}
final class ChecklistFilter extends DatabaseFilter {
@ -325,19 +339,33 @@ final class ChecklistFilter extends DatabaseFilter {
condition: condition ?? this.condition,
);
}
@override
List<Object?> get props => [filterId, fieldId, condition];
}
final class SelectOptionFilter extends DatabaseFilter {
const SelectOptionFilter({
SelectOptionFilter({
required super.filterId,
required super.fieldId,
required super.fieldType,
required this.condition,
required this.optionIds,
});
required List<String> optionIds,
}) {
if (canAttachContent) {
if (fieldType == FieldType.SingleSelect &&
(condition == SelectOptionFilterConditionPB.OptionIs ||
condition == SelectOptionFilterConditionPB.OptionIsNot) &&
optionIds.isNotEmpty) {
this.optionIds.add(optionIds.first);
} else {
this.optionIds.addAll(optionIds);
}
}
}
final SelectOptionFilterConditionPB condition;
final List<String> optionIds;
final List<String> optionIds = [];
@override
String get conditionName => condition.i18n;
@ -428,6 +456,9 @@ final class SelectOptionFilter extends DatabaseFilter {
field.fieldType == FieldType.SingleSelect
? const SingleSelectOptionFilterDelegateImpl()
: const MultiSelectOptionFilterDelegateImpl();
@override
List<Object?> get props => [filterId, fieldId, condition, optionIds];
}
enum DateTimeFilterCondition {
@ -491,7 +522,8 @@ enum DateTimeFilterCondition {
}
static List<DateTimeFilterCondition> availableConditionsForFieldType(
FieldType fieldType,) {
FieldType fieldType,
) {
final result = [...values];
if (fieldType == FieldType.CreatedTime ||
fieldType == FieldType.LastEditedTime) {
@ -504,20 +536,37 @@ enum DateTimeFilterCondition {
}
final class DateTimeFilter extends DatabaseFilter {
const DateTimeFilter({
DateTimeFilter({
required super.filterId,
required super.fieldId,
required super.fieldType,
required this.condition,
this.timestamp,
this.start,
this.end,
});
DateTime? timestamp,
DateTime? start,
DateTime? end,
}) {
if (canAttachContent) {
if (condition == DateFilterConditionPB.DateStartsBetween ||
condition == DateFilterConditionPB.DateEndsBetween) {
this.start = start;
this.end = end;
this.timestamp = null;
} else {
this.timestamp = timestamp;
this.start = null;
this.end = null;
}
} else {
this.timestamp = null;
this.start = null;
this.end = null;
}
}
final DateFilterConditionPB condition;
final DateTime? timestamp;
final DateTime? start;
final DateTime? end;
late final DateTime? timestamp;
late final DateTime? start;
late final DateTime? end;
@override
String get conditionName => condition.toCondition().filterName;
@ -654,6 +703,10 @@ final class DateTimeFilter extends DatabaseFilter {
timestamp: timestamp,
);
}
@override
List<Object?> get props =>
[filterId, fieldId, condition, timestamp, start, end];
}
final class TimeFilter extends DatabaseFilter {
@ -703,4 +756,7 @@ final class TimeFilter extends DatabaseFilter {
content: content ?? this.content,
);
}
@override
List<Object?> get props => [filterId, fieldId, condition, content];
}

View File

@ -1,7 +1,6 @@
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/import.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
class ImportPayload {
@ -17,7 +16,7 @@ class ImportPayload {
}
class ImportBackendService {
static Future<FlowyResult<void, FlowyError>> importPages(
static Future<FlowyResult<RepeatedViewPB, FlowyError>> importPages(
String parentViewId,
List<ImportValuePayloadPB> values,
) async {

View File

@ -1,4 +1,6 @@
import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
import 'package:flutter_test/flutter_test.dart';
@ -6,20 +8,25 @@ import 'package:flutter_test/flutter_test.dart';
import '../util.dart';
void main() {
late AppFlowyGridCellTest cellTest;
late AppFlowyGridTest cellTest;
setUpAll(() async {
cellTest = await AppFlowyGridCellTest.ensureInitialized();
cellTest = await AppFlowyGridTest.ensureInitialized();
});
group('SingleSelectOptionBloc', () {
test('create options', () async {
await cellTest.createTestGrid();
await cellTest.createTestRow();
final cellController = cellTest.makeSelectOptionCellController(
FieldType.SingleSelect,
0,
);
group('select cell bloc:', () {
late GridTestContext context;
late SelectOptionCellController cellController;
setUp(() async {
context = await cellTest.makeDefaultTestGrid();
await RowBackendService.createRow(viewId: context.view.id);
final fieldIndex = context.fieldController.fieldInfos
.indexWhere((field) => field.fieldType == FieldType.SingleSelect);
cellController = context.makeGridCellController(fieldIndex, 0).as();
});
test('create options', () async {
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
await gridResponseFuture();
@ -32,13 +39,6 @@ void main() {
});
test('update options', () async {
await cellTest.createTestGrid();
await cellTest.createTestRow();
final cellController = cellTest.makeSelectOptionCellController(
FieldType.SingleSelect,
0,
);
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
await gridResponseFuture();
@ -57,13 +57,6 @@ void main() {
});
test('delete options', () async {
await cellTest.createTestGrid();
await cellTest.createTestRow();
final cellController = cellTest.makeSelectOptionCellController(
FieldType.SingleSelect,
0,
);
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
await gridResponseFuture();
@ -108,13 +101,6 @@ void main() {
});
test('select/unselect option', () async {
await cellTest.createTestGrid();
await cellTest.createTestRow();
final cellController = cellTest.makeSelectOptionCellController(
FieldType.SingleSelect,
0,
);
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
await gridResponseFuture();
@ -135,13 +121,6 @@ void main() {
});
test('select an option or create one', () async {
await cellTest.createTestGrid();
await cellTest.createTestRow();
final cellController = cellTest.makeSelectOptionCellController(
FieldType.SingleSelect,
0,
);
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
await gridResponseFuture();
@ -163,13 +142,6 @@ void main() {
});
test('select multiple options', () async {
await cellTest.createTestGrid();
await cellTest.createTestRow();
final cellController = cellTest.makeSelectOptionCellController(
FieldType.SingleSelect,
0,
);
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
await gridResponseFuture();
@ -195,13 +167,6 @@ void main() {
});
test('filter options', () async {
await cellTest.createTestGrid();
await cellTest.createTestRow();
final cellController = cellTest.makeSelectOptionCellController(
FieldType.SingleSelect,
0,
);
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
await gridResponseFuture();

View File

@ -0,0 +1,109 @@
import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy/plugins/database/domain/field_service.dart';
import 'package:appflowy/plugins/database/domain/field_settings_service.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:flutter_test/flutter_test.dart';
import '../util.dart';
void main() {
late AppFlowyGridTest cellTest;
setUpAll(() async {
cellTest = await AppFlowyGridTest.ensureInitialized();
});
group('text cell bloc:', () {
late GridTestContext context;
late TextCellController cellController;
setUp(() async {
context = await cellTest.makeDefaultTestGrid();
await RowBackendService.createRow(viewId: context.view.id);
final fieldIndex = context.fieldController.fieldInfos
.indexWhere((field) => field.fieldType == FieldType.RichText);
cellController = context.makeGridCellController(fieldIndex, 0).as();
});
test('update text', () async {
final bloc = TextCellBloc(cellController: cellController);
await gridResponseFuture();
expect(bloc.state.content, "");
bloc.add(const TextCellEvent.updateText("A"));
await gridResponseFuture(milliseconds: 600);
expect(bloc.state.content, "A");
});
test('non-primary text field emoji and hasDocument', () async {
final primaryBloc = TextCellBloc(cellController: cellController);
expect(primaryBloc.state.emoji == null, false);
expect(primaryBloc.state.hasDocument == null, false);
await primaryBloc.close();
await FieldBackendService.createField(
viewId: context.view.id,
fieldName: "Second",
);
await gridResponseFuture();
final fieldIndex = context.fieldController.fieldInfos.indexWhere(
(field) => field.fieldType == FieldType.RichText && !field.isPrimary,
);
cellController = context.makeGridCellController(fieldIndex, 0).as();
final nonPrimaryBloc = TextCellBloc(cellController: cellController);
await gridResponseFuture();
expect(nonPrimaryBloc.state.emoji == null, true);
expect(nonPrimaryBloc.state.hasDocument == null, true);
});
test('update wrap cell content', () async {
final bloc = TextCellBloc(cellController: cellController);
await gridResponseFuture();
expect(bloc.state.wrap, true);
await FieldSettingsBackendService(
viewId: context.view.id,
).updateFieldSettings(
fieldId: cellController.fieldId,
wrapCellContent: false,
);
await gridResponseFuture();
expect(bloc.state.wrap, false);
});
test('update emoji', () async {
final bloc = TextCellBloc(cellController: cellController);
await gridResponseFuture();
expect(bloc.state.emoji!.value, "");
await RowBackendService(viewId: context.view.id)
.updateMeta(rowId: cellController.rowId, iconURL: "dummy");
await gridResponseFuture();
expect(bloc.state.emoji!.value, "dummy");
});
test('update document data', () async {
// This is so fake?
final bloc = TextCellBloc(cellController: cellController);
await gridResponseFuture();
expect(bloc.state.hasDocument!.value, false);
await RowBackendService(viewId: context.view.id)
.updateMeta(rowId: cellController.rowId, isDocumentEmpty: false);
await gridResponseFuture();
expect(bloc.state.hasDocument!.value, true);
});
});
}

View File

@ -1,43 +0,0 @@
import 'package:appflowy/plugins/database/application/field/field_editor_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:flutter_test/flutter_test.dart';
import '../util.dart';
Future<FieldEditorBloc> createEditorBloc(AppFlowyGridTest gridTest) async {
final context = await gridTest.createTestGrid();
final fieldInfo = context.getSelectOptionField();
return FieldEditorBloc(
viewId: context.gridView.id,
fieldController: context.fieldController,
fieldInfo: fieldInfo,
isNew: false,
);
}
void main() {
late AppFlowyGridTest gridTest;
setUpAll(() async {
gridTest = await AppFlowyGridTest.ensureInitialized();
});
test('rename field', () async {
final editorBloc = await createEditorBloc(gridTest);
editorBloc.add(const FieldEditorEvent.renameField('Hello world'));
await gridResponseFuture();
expect(editorBloc.state.field.name, equals("Hello world"));
});
test('switch to text field', () async {
final editorBloc = await createEditorBloc(gridTest);
editorBloc.add(const FieldEditorEvent.switchFieldType(FieldType.RichText));
await gridResponseFuture();
// The default length of the fields is 3. The length of the fields
// should not change after switching to other field type
expect(editorBloc.state.field.fieldType, equals(FieldType.RichText));
});
}

View File

@ -11,19 +11,19 @@ void main() {
gridTest = await AppFlowyGridTest.ensureInitialized();
});
group('$FieldCellBloc', () {
group('field cell bloc:', () {
late GridTestContext context;
late double width;
setUp(() async {
context = await gridTest.createTestGrid();
context = await gridTest.makeDefaultTestGrid();
});
blocTest(
'update field width',
build: () => FieldCellBloc(
fieldInfo: context.fieldInfos[0],
viewId: context.gridView.id,
fieldInfo: context.fieldController.fieldInfos[0],
viewId: context.view.id,
),
act: (bloc) {
width = bloc.state.width;
@ -37,10 +37,10 @@ void main() {
);
blocTest(
'field width should not be lesser than 50px',
'field width should not be less than 50px',
build: () => FieldCellBloc(
viewId: context.gridView.id,
fieldInfo: context.fieldInfos[0],
viewId: context.view.id,
fieldInfo: context.fieldController.fieldInfos[0],
),
act: (bloc) {
bloc.add(const FieldCellEvent.onResizeStart());

View File

@ -0,0 +1,133 @@
import 'package:appflowy/plugins/database/application/field/field_editor_bloc.dart';
import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:nanoid/nanoid.dart';
import '../util.dart';
void main() {
late AppFlowyGridTest gridTest;
setUpAll(() async {
gridTest = await AppFlowyGridTest.ensureInitialized();
});
group('field editor bloc:', () {
late GridTestContext context;
late FieldEditorBloc editorBloc;
setUp(() async {
context = await gridTest.makeDefaultTestGrid();
final fieldInfo = context.fieldController.fieldInfos
.firstWhere((field) => field.fieldType == FieldType.SingleSelect);
editorBloc = FieldEditorBloc(
viewId: context.view.id,
fieldController: context.fieldController,
fieldInfo: fieldInfo,
isNew: false,
);
});
test('rename field', () async {
expect(editorBloc.state.field.name, equals("Type"));
editorBloc.add(const FieldEditorEvent.renameField('Hello world'));
await gridResponseFuture();
expect(editorBloc.state.field.name, equals("Hello world"));
});
test('edit icon', () async {
expect(editorBloc.state.field.icon, equals(""));
editorBloc.add(const FieldEditorEvent.updateIcon('emoji/smiley-face'));
await gridResponseFuture();
expect(editorBloc.state.field.icon, equals("emoji/smiley-face"));
editorBloc.add(const FieldEditorEvent.updateIcon(""));
await gridResponseFuture();
expect(editorBloc.state.field.icon, equals(""));
});
test('switch to text field', () async {
expect(editorBloc.state.field.fieldType, equals(FieldType.SingleSelect));
editorBloc.add(
const FieldEditorEvent.switchFieldType(FieldType.RichText),
);
await gridResponseFuture();
expect(editorBloc.state.field.fieldType, equals(FieldType.RichText));
});
test('update field type option', () async {
final selectOption = SelectOptionPB()
..id = nanoid(4)
..color = SelectOptionColorPB.Lime
..name = "New option";
final typeOptionData = SingleSelectTypeOptionPB()
..options.addAll([selectOption]);
editorBloc.add(
FieldEditorEvent.updateTypeOption(typeOptionData.writeToBuffer()),
);
await gridResponseFuture();
final actual = SingleSelectTypeOptionDataParser()
.fromBuffer(editorBloc.state.field.field.typeOptionData);
expect(actual, equals(typeOptionData));
});
test('update visibility', () async {
expect(
editorBloc.state.field.visibility,
equals(FieldVisibility.AlwaysShown),
);
editorBloc.add(const FieldEditorEvent.toggleFieldVisibility());
await gridResponseFuture();
expect(
editorBloc.state.field.visibility,
equals(FieldVisibility.AlwaysHidden),
);
});
test('update wrap cell', () async {
expect(
editorBloc.state.field.wrapCellContent,
equals(true),
);
editorBloc.add(const FieldEditorEvent.toggleWrapCellContent());
await gridResponseFuture();
expect(
editorBloc.state.field.wrapCellContent,
equals(false),
);
});
test('insert left and right', () async {
expect(
context.fieldController.fieldInfos.length,
equals(3),
);
editorBloc.add(const FieldEditorEvent.insertLeft());
// TODO(RS): Shouldn't need to wait here!?
await gridResponseFuture();
editorBloc.add(const FieldEditorEvent.insertRight());
await gridResponseFuture();
expect(
context.fieldController.fieldInfos.length,
equals(5),
);
});
});
}

View File

@ -1,159 +0,0 @@
import 'package:appflowy/plugins/database/domain/filter_service.dart';
import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart';
import 'package:flutter_test/flutter_test.dart';
import '../util.dart';
void main() {
late AppFlowyGridTest gridTest;
setUpAll(() async {
gridTest = await AppFlowyGridTest.ensureInitialized();
});
test('create a text filter)', () async {
final context = await gridTest.createTestGrid();
final service = FilterBackendService(viewId: context.gridView.id);
final textField = context.getTextField();
await service.insertTextFilter(
fieldId: textField.id,
condition: TextFilterConditionPB.TextIsEmpty,
content: "",
);
await gridResponseFuture();
assert(context.fieldController.filters.length == 1);
});
test('delete a text filter)', () async {
final context = await gridTest.createTestGrid();
final service = FilterBackendService(viewId: context.gridView.id);
final textField = context.getTextField();
await service.insertTextFilter(
fieldId: textField.id,
condition: TextFilterConditionPB.TextIsEmpty,
content: "",
);
await gridResponseFuture();
final filter = context.fieldController.filters.first;
await service.deleteFilter(
filterId: filter.filterId,
);
await gridResponseFuture();
expect(context.fieldController.filters.length, 0);
});
test('filter rows with condition: text is empty', () async {
final context = await gridTest.createTestGrid();
final service = FilterBackendService(viewId: context.gridView.id);
final gridController = DatabaseController(
view: context.gridView,
);
final gridBloc = GridBloc(
view: context.gridView,
databaseController: gridController,
)..add(const GridEvent.initial());
await gridResponseFuture();
final textField = context.getTextField();
await service.insertTextFilter(
fieldId: textField.id,
condition: TextFilterConditionPB.TextIsEmpty,
content: "",
);
await gridResponseFuture();
expect(gridBloc.state.rowInfos.length, 3);
});
test('filter rows with condition: text is empty(After edit the row)',
() async {
final context = await gridTest.createTestGrid();
final service = FilterBackendService(viewId: context.gridView.id);
final gridController = DatabaseController(
view: context.gridView,
);
final gridBloc = GridBloc(
view: context.gridView,
databaseController: gridController,
)..add(const GridEvent.initial());
await gridResponseFuture();
final textField = context.getTextField();
await service.insertTextFilter(
fieldId: textField.id,
condition: TextFilterConditionPB.TextIsEmpty,
content: "",
);
await gridResponseFuture();
final controller = context.makeTextCellController(0);
await controller.saveCellData("edit text cell content");
await gridResponseFuture();
assert(gridBloc.state.rowInfos.length == 2);
await controller.saveCellData("");
await gridResponseFuture();
assert(gridBloc.state.rowInfos.length == 3);
});
test('filter rows with condition: text is not empty', () async {
final context = await gridTest.createTestGrid();
final service = FilterBackendService(viewId: context.gridView.id);
final textField = context.getTextField();
await gridResponseFuture();
await service.insertTextFilter(
fieldId: textField.id,
condition: TextFilterConditionPB.TextIsNotEmpty,
content: "",
);
await gridResponseFuture();
assert(context.rowInfos.isEmpty);
});
test('filter rows with condition: checkbox uncheck', () async {
final context = await gridTest.createTestGrid();
final checkboxField = context.getCheckboxField();
final service = FilterBackendService(viewId: context.gridView.id);
final gridController = DatabaseController(
view: context.gridView,
);
final gridBloc = GridBloc(
view: context.gridView,
databaseController: gridController,
)..add(const GridEvent.initial());
await gridResponseFuture();
await service.insertCheckboxFilter(
fieldId: checkboxField.id,
condition: CheckboxFilterConditionPB.IsUnChecked,
);
await gridResponseFuture();
assert(gridBloc.state.rowInfos.length == 3);
});
test('filter rows with condition: checkbox check', () async {
final context = await gridTest.createTestGrid();
final checkboxField = context.getCheckboxField();
final service = FilterBackendService(viewId: context.gridView.id);
final gridController = DatabaseController(
view: context.gridView,
);
final gridBloc = GridBloc(
view: context.gridView,
databaseController: gridController,
)..add(const GridEvent.initial());
await gridResponseFuture();
await service.insertCheckboxFilter(
fieldId: checkboxField.id,
condition: CheckboxFilterConditionPB.IsChecked,
);
await gridResponseFuture();
assert(gridBloc.state.rowInfos.isEmpty);
});
}

View File

@ -1,3 +1,4 @@
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/application/field/filter_entities.dart';
import 'package:appflowy/plugins/database/domain/field_service.dart';
import 'package:appflowy/plugins/database/domain/filter_service.dart';
@ -9,24 +10,35 @@ import '../util.dart';
void main() {
late AppFlowyGridTest gridTest;
setUpAll(() async {
gridTest = await AppFlowyGridTest.ensureInitialized();
});
group('filter editor bloc test:', () {
test('create filter', () async {
final context = await gridTest.createTestGrid();
final filterBloc = FilterEditorBloc(
viewId: context.gridView.id,
group('filter editor bloc:', () {
late GridTestContext context;
late FilterEditorBloc filterBloc;
setUp(() async {
context = await gridTest.makeDefaultTestGrid();
filterBloc = FilterEditorBloc(
viewId: context.view.id,
fieldController: context.fieldController,
);
await gridResponseFuture();
});
FieldInfo getFirstFieldByType(FieldType fieldType) {
return context.fieldController.fieldInfos
.firstWhere((field) => field.fieldType == fieldType);
}
test('create filter', () async {
expect(filterBloc.state.filters.length, equals(0));
expect(filterBloc.state.fields.length, equals(3));
// through domain directly
final textField = context.getTextField();
final service = FilterBackendService(viewId: context.gridView.id);
final textField = getFirstFieldByType(FieldType.RichText);
final service = FilterBackendService(viewId: context.view.id);
await service.insertTextFilter(
fieldId: textField.id,
condition: TextFilterConditionPB.TextIsEmpty,
@ -37,7 +49,7 @@ void main() {
expect(filterBloc.state.fields.length, equals(3));
// through bloc event
final selectOptionField = context.getSelectOptionField();
final selectOptionField = getFirstFieldByType(FieldType.SingleSelect);
filterBloc.add(FilterEditorEvent.createFilter(selectOptionField));
await gridResponseFuture();
expect(filterBloc.state.filters.length, equals(2));
@ -53,15 +65,33 @@ void main() {
expect(filterBloc.state.fields.length, equals(3));
});
test('delete filter', () async {
final context = await gridTest.createTestGrid();
final filterBloc = FilterEditorBloc(
viewId: context.gridView.id,
fieldController: context.fieldController,
test('change filtering field', () async {
final textField = getFirstFieldByType(FieldType.RichText);
final selectField = getFirstFieldByType(FieldType.Checkbox);
filterBloc.add(FilterEditorEvent.createFilter(textField));
await gridResponseFuture();
expect(filterBloc.state.filters.length, equals(1));
expect(filterBloc.state.fields.length, equals(3));
expect(
filterBloc.state.filters.first.fieldType,
equals(FieldType.RichText),
);
final filter = filterBloc.state.filters.first;
filterBloc.add(
FilterEditorEvent.changeFilteringField(filter.filterId, selectField),
);
await gridResponseFuture();
expect(filterBloc.state.filters.length, equals(1));
expect(
filterBloc.state.filters.first.fieldType,
equals(FieldType.Checkbox),
);
expect(filterBloc.state.fields.length, equals(3));
});
final textField = context.getTextField();
test('delete filter', () async {
final textField = getFirstFieldByType(FieldType.RichText);
filterBloc.add(FilterEditorEvent.createFilter(textField));
await gridResponseFuture();
expect(filterBloc.state.filters.length, equals(1));
@ -75,15 +105,8 @@ void main() {
});
test('update filter', () async {
final context = await gridTest.createTestGrid();
final filterBloc = FilterEditorBloc(
viewId: context.gridView.id,
fieldController: context.fieldController,
);
await gridResponseFuture();
final service = FilterBackendService(viewId: context.gridView.id);
final textField = context.getTextField();
final service = FilterBackendService(viewId: context.view.id);
final textField = getFirstFieldByType(FieldType.RichText);
// Create filter
await service.insertTextFilter(
@ -96,6 +119,7 @@ void main() {
expect(filter.condition, equals(TextFilterConditionPB.TextIsEmpty));
final textFilter = context.fieldController.filters.first;
// Update the existing filter
await service.insertTextFilter(
fieldId: textField.id,
@ -109,15 +133,8 @@ void main() {
expect(filter.content, equals("ABC"));
});
test('update field', () async {
final context = await gridTest.createTestGrid();
final filterBloc = FilterEditorBloc(
viewId: context.gridView.id,
fieldController: context.fieldController,
);
await gridResponseFuture();
final textField = context.getTextField();
test('update filtering field\'s name', () async {
final textField = getFirstFieldByType(FieldType.RichText);
filterBloc.add(FilterEditorEvent.createFilter(textField));
await gridResponseFuture();
expect(filterBloc.state.filters.length, equals(1));
@ -127,7 +144,7 @@ void main() {
// edit field
await FieldBackendService(
viewId: context.gridView.id,
viewId: context.view.id,
fieldId: textField.id,
).updateField(name: "New Name");
await gridResponseFuture();
@ -136,28 +153,40 @@ void main() {
});
test('update field type', () async {
final context = await gridTest.createTestGrid();
final filterBloc = FilterEditorBloc(
viewId: context.gridView.id,
fieldController: context.fieldController,
);
await gridResponseFuture();
final checkboxField = context.getCheckboxField();
final checkboxField = getFirstFieldByType(FieldType.Checkbox);
filterBloc.add(FilterEditorEvent.createFilter(checkboxField));
await gridResponseFuture();
expect(filterBloc.state.filters.length, equals(1));
// edit field
await FieldBackendService(
viewId: context.gridView.id,
viewId: context.view.id,
fieldId: checkboxField.id,
).updateType(fieldType: FieldType.DateTime);
await gridResponseFuture();
expect(filterBloc.state.filters.length, equals(0));
// filter is removed
expect(filterBloc.state.filters.length, equals(0));
expect(filterBloc.state.fields.length, equals(3));
expect(filterBloc.state.fields[2].fieldType, FieldType.DateTime);
});
test('update filter field', () async {
final checkboxField = getFirstFieldByType(FieldType.Checkbox);
filterBloc.add(FilterEditorEvent.createFilter(checkboxField));
await gridResponseFuture();
expect(filterBloc.state.filters.length, equals(1));
// edit field
await FieldBackendService(
viewId: context.view.id,
fieldId: checkboxField.id,
).updateField(name: "HERRO");
await gridResponseFuture();
expect(filterBloc.state.filters.length, equals(1));
expect(filterBloc.state.fields.length, equals(3));
expect(filterBloc.state.fields[2].name, "HERRO");
});
});
}

View File

@ -0,0 +1,623 @@
import 'dart:typed_data';
import 'package:appflowy/plugins/database/application/field/filter_entities.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('parsing filter entities:', () {
FilterPB createFilterPB(
FieldType fieldType,
Uint8List data,
) {
return FilterPB(
id: "FT",
filterType: FilterType.Data,
data: FilterDataPB(
fieldId: "FD",
fieldType: fieldType,
data: data,
),
);
}
test('text', () async {
expect(
DatabaseFilter.fromPB(
createFilterPB(
FieldType.RichText,
TextFilterPB(
condition: TextFilterConditionPB.TextContains,
content: "c",
).writeToBuffer(),
),
),
equals(
TextFilter(
filterId: "FT",
fieldId: "FD",
fieldType: FieldType.RichText,
condition: TextFilterConditionPB.TextContains,
content: "c",
),
),
);
expect(
DatabaseFilter.fromPB(
createFilterPB(
FieldType.RichText,
TextFilterPB(
condition: TextFilterConditionPB.TextContains,
content: "",
).writeToBuffer(),
),
),
equals(
TextFilter(
filterId: "FT",
fieldId: "FD",
fieldType: FieldType.RichText,
condition: TextFilterConditionPB.TextContains,
content: "",
),
),
);
expect(
DatabaseFilter.fromPB(
createFilterPB(
FieldType.RichText,
TextFilterPB(
condition: TextFilterConditionPB.TextIsEmpty,
content: "",
).writeToBuffer(),
),
),
equals(
TextFilter(
filterId: "FT",
fieldId: "FD",
fieldType: FieldType.RichText,
condition: TextFilterConditionPB.TextIsEmpty,
content: "",
),
),
);
expect(
DatabaseFilter.fromPB(
createFilterPB(
FieldType.RichText,
TextFilterPB(
condition: TextFilterConditionPB.TextIsEmpty,
content: "",
).writeToBuffer(),
),
),
equals(
TextFilter(
filterId: "FT",
fieldId: "FD",
fieldType: FieldType.RichText,
condition: TextFilterConditionPB.TextIsEmpty,
content: "c",
),
),
);
});
test('number', () async {
expect(
DatabaseFilter.fromPB(
createFilterPB(
FieldType.Number,
NumberFilterPB(
condition: NumberFilterConditionPB.GreaterThan,
content: "",
).writeToBuffer(),
),
),
equals(
NumberFilter(
filterId: "FT",
fieldId: "FD",
fieldType: FieldType.Number,
condition: NumberFilterConditionPB.GreaterThan,
content: "",
),
),
);
expect(
DatabaseFilter.fromPB(
createFilterPB(
FieldType.Number,
NumberFilterPB(
condition: NumberFilterConditionPB.GreaterThan,
content: "123",
).writeToBuffer(),
),
),
equals(
NumberFilter(
filterId: "FT",
fieldId: "FD",
fieldType: FieldType.Number,
condition: NumberFilterConditionPB.GreaterThan,
content: "123",
),
),
);
expect(
DatabaseFilter.fromPB(
createFilterPB(
FieldType.Number,
NumberFilterPB(
condition: NumberFilterConditionPB.NumberIsEmpty,
content: "",
).writeToBuffer(),
),
),
equals(
NumberFilter(
filterId: "FT",
fieldId: "FD",
fieldType: FieldType.Number,
condition: NumberFilterConditionPB.NumberIsEmpty,
content: "",
),
),
);
expect(
DatabaseFilter.fromPB(
createFilterPB(
FieldType.Number,
NumberFilterPB(
condition: NumberFilterConditionPB.NumberIsEmpty,
content: "",
).writeToBuffer(),
),
),
equals(
NumberFilter(
filterId: "FT",
fieldId: "FD",
fieldType: FieldType.Number,
condition: NumberFilterConditionPB.NumberIsEmpty,
content: "123",
),
),
);
});
test('checkbox', () async {
expect(
DatabaseFilter.fromPB(
createFilterPB(
FieldType.Checkbox,
CheckboxFilterPB(
condition: CheckboxFilterConditionPB.IsChecked,
).writeToBuffer(),
),
),
equals(
const CheckboxFilter(
filterId: "FT",
fieldId: "FD",
fieldType: FieldType.Checkbox,
condition: CheckboxFilterConditionPB.IsChecked,
),
),
);
});
test('checklist', () async {
expect(
DatabaseFilter.fromPB(
createFilterPB(
FieldType.Checklist,
ChecklistFilterPB(
condition: ChecklistFilterConditionPB.IsComplete,
).writeToBuffer(),
),
),
equals(
const ChecklistFilter(
filterId: "FT",
fieldId: "FD",
fieldType: FieldType.Checklist,
condition: ChecklistFilterConditionPB.IsComplete,
),
),
);
});
test('single select option', () async {
expect(
DatabaseFilter.fromPB(
createFilterPB(
FieldType.SingleSelect,
SelectOptionFilterPB(
condition: SelectOptionFilterConditionPB.OptionContains,
optionIds: [],
).writeToBuffer(),
),
),
equals(
SelectOptionFilter(
filterId: "FT",
fieldId: "FD",
fieldType: FieldType.SingleSelect,
condition: SelectOptionFilterConditionPB.OptionContains,
optionIds: const [],
),
),
);
expect(
DatabaseFilter.fromPB(
createFilterPB(
FieldType.SingleSelect,
SelectOptionFilterPB(
condition: SelectOptionFilterConditionPB.OptionContains,
optionIds: ['a'],
).writeToBuffer(),
),
),
equals(
SelectOptionFilter(
filterId: "FT",
fieldId: "FD",
fieldType: FieldType.SingleSelect,
condition: SelectOptionFilterConditionPB.OptionContains,
optionIds: const ['a'],
),
),
);
expect(
DatabaseFilter.fromPB(
createFilterPB(
FieldType.SingleSelect,
SelectOptionFilterPB(
condition: SelectOptionFilterConditionPB.OptionContains,
optionIds: ['a', 'b'],
).writeToBuffer(),
),
),
equals(
SelectOptionFilter(
filterId: "FT",
fieldId: "FD",
fieldType: FieldType.SingleSelect,
condition: SelectOptionFilterConditionPB.OptionContains,
optionIds: const ['a', 'b'],
),
),
);
expect(
DatabaseFilter.fromPB(
createFilterPB(
FieldType.SingleSelect,
SelectOptionFilterPB(
condition: SelectOptionFilterConditionPB.OptionIs,
optionIds: ['a', 'b'],
).writeToBuffer(),
),
),
equals(
SelectOptionFilter(
filterId: "FT",
fieldId: "FD",
fieldType: FieldType.SingleSelect,
condition: SelectOptionFilterConditionPB.OptionIs,
optionIds: const ['a'],
),
),
);
expect(
DatabaseFilter.fromPB(
createFilterPB(
FieldType.SingleSelect,
SelectOptionFilterPB(
condition: SelectOptionFilterConditionPB.OptionIsEmpty,
optionIds: [],
).writeToBuffer(),
),
),
equals(
SelectOptionFilter(
filterId: "FT",
fieldId: "FD",
fieldType: FieldType.SingleSelect,
condition: SelectOptionFilterConditionPB.OptionIsEmpty,
optionIds: const [],
),
),
);
expect(
DatabaseFilter.fromPB(
createFilterPB(
FieldType.SingleSelect,
SelectOptionFilterPB(
condition: SelectOptionFilterConditionPB.OptionIsEmpty,
optionIds: ['a'],
).writeToBuffer(),
),
),
equals(
SelectOptionFilter(
filterId: "FT",
fieldId: "FD",
fieldType: FieldType.SingleSelect,
condition: SelectOptionFilterConditionPB.OptionIsEmpty,
optionIds: const [],
),
),
);
});
test('multi select option', () async {
expect(
DatabaseFilter.fromPB(
createFilterPB(
FieldType.MultiSelect,
SelectOptionFilterPB(
condition: SelectOptionFilterConditionPB.OptionContains,
optionIds: [],
).writeToBuffer(),
),
),
equals(
SelectOptionFilter(
filterId: "FT",
fieldId: "FD",
fieldType: FieldType.MultiSelect,
condition: SelectOptionFilterConditionPB.OptionContains,
optionIds: const [],
),
),
);
expect(
DatabaseFilter.fromPB(
createFilterPB(
FieldType.MultiSelect,
SelectOptionFilterPB(
condition: SelectOptionFilterConditionPB.OptionContains,
optionIds: ['a'],
).writeToBuffer(),
),
),
equals(
SelectOptionFilter(
filterId: "FT",
fieldId: "FD",
fieldType: FieldType.MultiSelect,
condition: SelectOptionFilterConditionPB.OptionContains,
optionIds: const ['a'],
),
),
);
expect(
DatabaseFilter.fromPB(
createFilterPB(
FieldType.MultiSelect,
SelectOptionFilterPB(
condition: SelectOptionFilterConditionPB.OptionContains,
optionIds: ['a', 'b'],
).writeToBuffer(),
),
),
equals(
SelectOptionFilter(
filterId: "FT",
fieldId: "FD",
fieldType: FieldType.MultiSelect,
condition: SelectOptionFilterConditionPB.OptionContains,
optionIds: const ['a', 'b'],
),
),
);
expect(
DatabaseFilter.fromPB(
createFilterPB(
FieldType.MultiSelect,
SelectOptionFilterPB(
condition: SelectOptionFilterConditionPB.OptionIs,
optionIds: ['a', 'b'],
).writeToBuffer(),
),
),
equals(
SelectOptionFilter(
filterId: "FT",
fieldId: "FD",
fieldType: FieldType.MultiSelect,
condition: SelectOptionFilterConditionPB.OptionIs,
optionIds: const ['a', 'b'],
),
),
);
expect(
DatabaseFilter.fromPB(
createFilterPB(
FieldType.MultiSelect,
SelectOptionFilterPB(
condition: SelectOptionFilterConditionPB.OptionIsEmpty,
optionIds: [],
).writeToBuffer(),
),
),
equals(
SelectOptionFilter(
filterId: "FT",
fieldId: "FD",
fieldType: FieldType.MultiSelect,
condition: SelectOptionFilterConditionPB.OptionIsEmpty,
optionIds: const [],
),
),
);
expect(
DatabaseFilter.fromPB(
createFilterPB(
FieldType.MultiSelect,
SelectOptionFilterPB(
condition: SelectOptionFilterConditionPB.OptionIsEmpty,
optionIds: ['a'],
).writeToBuffer(),
),
),
equals(
SelectOptionFilter(
filterId: "FT",
fieldId: "FD",
fieldType: FieldType.MultiSelect,
condition: SelectOptionFilterConditionPB.OptionIsEmpty,
optionIds: const [],
),
),
);
});
test('date time', () {
expect(
DatabaseFilter.fromPB(
createFilterPB(
FieldType.DateTime,
DateFilterPB(
condition: DateFilterConditionPB.DateStartsOn,
timestamp: Int64(5),
).writeToBuffer(),
),
),
equals(
DateTimeFilter(
filterId: "FT",
fieldId: "FD",
fieldType: FieldType.DateTime,
condition: DateFilterConditionPB.DateStartsOn,
timestamp: DateTime.fromMillisecondsSinceEpoch(5 * 1000),
),
),
);
expect(
DatabaseFilter.fromPB(
createFilterPB(
FieldType.DateTime,
DateFilterPB(
condition: DateFilterConditionPB.DateStartsOn,
).writeToBuffer(),
),
),
equals(
DateTimeFilter(
filterId: "FT",
fieldId: "FD",
fieldType: FieldType.DateTime,
condition: DateFilterConditionPB.DateStartsOn,
),
),
);
expect(
DatabaseFilter.fromPB(
createFilterPB(
FieldType.DateTime,
DateFilterPB(
condition: DateFilterConditionPB.DateStartsOn,
start: Int64(5),
).writeToBuffer(),
),
),
equals(
DateTimeFilter(
filterId: "FT",
fieldId: "FD",
fieldType: FieldType.DateTime,
condition: DateFilterConditionPB.DateStartsOn,
),
),
);
expect(
DatabaseFilter.fromPB(
createFilterPB(
FieldType.DateTime,
DateFilterPB(
condition: DateFilterConditionPB.DateEndsBetween,
start: Int64(5),
).writeToBuffer(),
),
),
equals(
DateTimeFilter(
filterId: "FT",
fieldId: "FD",
fieldType: FieldType.DateTime,
condition: DateFilterConditionPB.DateEndsBetween,
start: DateTime.fromMillisecondsSinceEpoch(5 * 1000),
),
),
);
expect(
DatabaseFilter.fromPB(
createFilterPB(
FieldType.DateTime,
DateFilterPB(
condition: DateFilterConditionPB.DateEndIsNotEmpty,
).writeToBuffer(),
),
),
equals(
DateTimeFilter(
filterId: "FT",
fieldId: "FD",
fieldType: FieldType.DateTime,
condition: DateFilterConditionPB.DateEndIsNotEmpty,
),
),
);
expect(
DatabaseFilter.fromPB(
createFilterPB(
FieldType.DateTime,
DateFilterPB(
condition: DateFilterConditionPB.DateEndIsNotEmpty,
start: Int64(5),
end: Int64(5),
timestamp: Int64(5),
).writeToBuffer(),
),
),
equals(
DateTimeFilter(
filterId: "FT",
fieldId: "FD",
fieldType: FieldType.DateTime,
condition: DateFilterConditionPB.DateEndIsNotEmpty,
),
),
);
});
});
// group('write to buffer', () {
// test('text', () {});
// });
}

View File

@ -1,55 +0,0 @@
import 'package:appflowy/plugins/database/domain/filter_service.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbenum.dart';
import 'package:flutter_test/flutter_test.dart';
import '../util.dart';
import 'filter_util.dart';
void main() {
late AppFlowyGridTest gridTest;
setUpAll(() async {
gridTest = await AppFlowyGridTest.ensureInitialized();
});
test('filter rows by checkbox is check condition)', () async {
final context = await createTestFilterGrid(gridTest);
final service = FilterBackendService(viewId: context.gridView.id);
final controller = context.makeCheckboxCellController(0);
await controller.saveCellData("Yes");
await gridResponseFuture();
// create a new filter
final checkboxField = context.getCheckboxField();
await service.insertCheckboxFilter(
fieldId: checkboxField.id,
condition: CheckboxFilterConditionPB.IsChecked,
);
await gridResponseFuture();
assert(
context.rowInfos.length == 1,
"expect 1 but receive ${context.rowInfos.length}",
);
});
test('filter rows by checkbox is uncheck condition)', () async {
final context = await createTestFilterGrid(gridTest);
final service = FilterBackendService(viewId: context.gridView.id);
final controller = context.makeCheckboxCellController(0);
await controller.saveCellData("Yes");
await gridResponseFuture();
// create a new filter
final checkboxField = context.getCheckboxField();
await service.insertCheckboxFilter(
fieldId: checkboxField.id,
condition: CheckboxFilterConditionPB.IsUnChecked,
);
await gridResponseFuture();
assert(
context.rowInfos.length == 2,
"expect 2 but receive ${context.rowInfos.length}",
);
});
}

View File

@ -1,165 +0,0 @@
import 'package:appflowy/plugins/database/domain/filter_service.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart';
import 'package:flutter_test/flutter_test.dart';
import '../util.dart';
import 'filter_util.dart';
void main() {
late AppFlowyGridTest gridTest;
setUpAll(() async {
gridTest = await AppFlowyGridTest.ensureInitialized();
});
test('filter rows by text is empty condition)', () async {
final context = await createTestFilterGrid(gridTest);
final service = FilterBackendService(viewId: context.gridView.id);
final textField = context.getTextField();
// create a new filter
await service.insertTextFilter(
fieldId: textField.id,
condition: TextFilterConditionPB.TextIsEmpty,
content: "",
);
await gridResponseFuture();
assert(
context.fieldController.filters.length == 1,
"expect 1 but receive ${context.fieldController.filters.length}",
);
assert(
context.rowInfos.length == 1,
"expect 1 but receive ${context.rowInfos.length}",
);
// delete the filter
final textFilter = context.fieldController.filters.first;
await service.deleteFilter(
filterId: textFilter.filterId,
);
await gridResponseFuture();
assert(context.rowInfos.length == 3);
});
test('filter rows by text is not empty condition)', () async {
final context = await createTestFilterGrid(gridTest);
final service = FilterBackendService(viewId: context.gridView.id);
final textField = context.getTextField();
// create a new filter
await service.insertTextFilter(
fieldId: textField.id,
condition: TextFilterConditionPB.TextIsNotEmpty,
content: "",
);
await gridResponseFuture();
assert(
context.rowInfos.length == 2,
"expect 2 but receive ${context.rowInfos.length}",
);
// delete the filter
final textFilter = context.fieldController.filters.first;
await service.deleteFilter(
filterId: textFilter.filterId,
);
await gridResponseFuture();
assert(context.rowInfos.length == 3);
});
test('filter rows by text is empty or is not empty condition)', () async {
final context = await createTestFilterGrid(gridTest);
final service = FilterBackendService(viewId: context.gridView.id);
final textField = context.getTextField();
// create a new filter
await service.insertTextFilter(
fieldId: textField.id,
condition: TextFilterConditionPB.TextIsEmpty,
content: "",
);
await gridResponseFuture();
assert(
context.fieldController.filters.length == 1,
"expect 1 but receive ${context.fieldController.filters.length}",
);
assert(
context.rowInfos.length == 1,
"expect 1 but receive ${context.rowInfos.length}",
);
// Update the existing filter
final textFilter = context.fieldController.filters.first;
await service.insertTextFilter(
fieldId: textField.id,
filterId: textFilter.filterId,
condition: TextFilterConditionPB.TextIsNotEmpty,
content: "",
);
await gridResponseFuture();
assert(context.rowInfos.length == 2);
// delete the filter
await service.deleteFilter(
filterId: textFilter.filterId,
);
await gridResponseFuture();
assert(context.rowInfos.length == 3);
});
test('filter rows by text is condition)', () async {
final context = await createTestFilterGrid(gridTest);
final service = FilterBackendService(viewId: context.gridView.id);
final textField = context.getTextField();
// create a new filter
await service.insertTextFilter(
fieldId: textField.id,
condition: TextFilterConditionPB.TextIs,
content: "A",
);
await gridResponseFuture();
assert(
context.rowInfos.length == 1,
"expect 1 but receive ${context.rowInfos.length}",
);
// Update the existing filter's content from 'A' to 'B'
final textFilter = context.fieldController.filters.first;
await service.insertTextFilter(
fieldId: textField.id,
filterId: textFilter.filterId,
condition: TextFilterConditionPB.TextIs,
content: "B",
);
await gridResponseFuture();
assert(context.rowInfos.length == 1);
// Update the existing filter's content from 'B' to 'b'
await service.insertTextFilter(
fieldId: textField.id,
filterId: textFilter.filterId,
condition: TextFilterConditionPB.TextIs,
content: "b",
);
await gridResponseFuture();
assert(context.rowInfos.length == 1);
// Update the existing filter with content 'C'
await service.insertTextFilter(
fieldId: textField.id,
filterId: textFilter.filterId,
condition: TextFilterConditionPB.TextIs,
content: "C",
);
await gridResponseFuture();
assert(context.rowInfos.isEmpty);
// delete the filter
await service.deleteFilter(
filterId: textFilter.filterId,
);
await gridResponseFuture();
assert(context.rowInfos.length == 3);
});
}

View File

@ -1,42 +0,0 @@
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
import '../util.dart';
Future<GridTestContext> createTestFilterGrid(AppFlowyGridTest gridTest) async {
final app = await gridTest.unitTest.createWorkspace();
final context = await ViewBackendService.createView(
parentViewId: app.id,
name: "Filter Grid",
layoutType: ViewLayoutPB.Grid,
openAfterCreate: true,
).then((result) {
return result.fold(
(view) async {
final context = GridTestContext(
view,
DatabaseController(view: view),
);
final result = await context.gridController.open();
await editCells(context);
result.fold((l) => null, (r) => throw Exception(r));
return context;
},
(error) => throw Exception(),
);
});
return context;
}
Future<void> editCells(GridTestContext context) async {
final controller0 = context.makeTextCellController(0);
final controller1 = context.makeTextCellController(1);
await controller0.saveCellData('A');
await gridResponseFuture();
await controller1.saveCellData('B');
await gridResponseFuture();
}

View File

@ -7,6 +7,7 @@ import 'util.dart';
void main() {
late AppFlowyGridTest gridTest;
setUpAll(() async {
gridTest = await AppFlowyGridTest.ensureInitialized();
});
@ -15,7 +16,7 @@ void main() {
late GridTestContext context;
setUp(() async {
context = await gridTest.createTestGrid();
context = await gridTest.makeDefaultTestGrid();
});
// The initial number of rows is 3 for each grid
@ -23,32 +24,29 @@ void main() {
blocTest<GridBloc, GridState>(
"create a row",
build: () => GridBloc(
view: context.gridView,
databaseController: DatabaseController(view: context.gridView),
view: context.view,
databaseController: DatabaseController(view: context.view),
)..add(const GridEvent.initial()),
act: (bloc) => bloc.add(const GridEvent.createRow()),
wait: const Duration(milliseconds: 300),
wait: gridResponseDuration(),
verify: (bloc) {
assert(bloc.state.rowInfos.length == 4);
expect(bloc.state.rowInfos.length, equals(4));
},
);
blocTest<GridBloc, GridState>(
"delete the last row",
build: () => GridBloc(
view: context.gridView,
databaseController: DatabaseController(view: context.gridView),
view: context.view,
databaseController: DatabaseController(view: context.view),
)..add(const GridEvent.initial()),
act: (bloc) async {
await gridResponseFuture();
bloc.add(GridEvent.deleteRow(bloc.state.rowInfos.last));
},
wait: const Duration(milliseconds: 300),
wait: gridResponseDuration(),
verify: (bloc) {
assert(
bloc.state.rowInfos.length == 2,
"Expected 2, but receive ${bloc.state.rowInfos.length}",
);
expect(bloc.state.rowInfos.length, equals(2));
},
);
@ -59,8 +57,8 @@ void main() {
blocTest(
'reorder rows',
build: () => GridBloc(
view: context.gridView,
databaseController: DatabaseController(view: context.gridView),
view: context.view,
databaseController: DatabaseController(view: context.view),
)..add(const GridEvent.initial()),
act: (bloc) async {
await gridResponseFuture();
@ -71,10 +69,11 @@ void main() {
bloc.add(const GridEvent.moveRow(0, 2));
},
wait: gridResponseDuration(),
verify: (bloc) {
expect(secondId, bloc.state.rowInfos[0].rowId);
expect(thirdId, bloc.state.rowInfos[1].rowId);
expect(firstId, bloc.state.rowInfos[2].rowId);
expect(secondId, equals(bloc.state.rowInfos[0].rowId));
expect(thirdId, equals(bloc.state.rowInfos[1].rowId));
expect(firstId, equals(bloc.state.rowInfos[2].rowId));
},
);
});

View File

@ -0,0 +1,204 @@
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/domain/field_service.dart';
import 'package:appflowy/plugins/database/grid/application/sort/sort_editor_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter_test/flutter_test.dart';
import '../util.dart';
void main() {
late AppFlowyGridTest gridTest;
setUpAll(() async {
gridTest = await AppFlowyGridTest.ensureInitialized();
});
group('sort editor bloc:', () {
late GridTestContext context;
late SortEditorBloc sortBloc;
setUp(() async {
context = await gridTest.makeDefaultTestGrid();
sortBloc = SortEditorBloc(
viewId: context.view.id,
fieldController: context.fieldController,
);
});
FieldInfo getFirstFieldByType(FieldType fieldType) {
return context.fieldController.fieldInfos
.firstWhere((field) => field.fieldType == fieldType);
}
test('create sort', () async {
expect(sortBloc.state.sorts.length, equals(0));
expect(sortBloc.state.creatableFields.length, equals(3));
expect(sortBloc.state.allFields.length, equals(3));
final selectOptionField = getFirstFieldByType(FieldType.SingleSelect);
sortBloc.add(SortEditorEvent.createSort(fieldId: selectOptionField.id));
await gridResponseFuture();
expect(sortBloc.state.sorts.length, 1);
expect(sortBloc.state.sorts.first.fieldId, selectOptionField.id);
expect(
sortBloc.state.sorts.first.condition,
SortConditionPB.Ascending,
);
expect(sortBloc.state.creatableFields.length, equals(2));
expect(sortBloc.state.allFields.length, equals(3));
});
test('change sort field', () async {
final selectOptionField = getFirstFieldByType(FieldType.SingleSelect);
sortBloc.add(SortEditorEvent.createSort(fieldId: selectOptionField.id));
await gridResponseFuture();
expect(
sortBloc.state.creatableFields
.map((e) => e.id)
.contains(selectOptionField.id),
false,
);
final checkboxField = getFirstFieldByType(FieldType.Checkbox);
sortBloc.add(
SortEditorEvent.editSort(
sortId: sortBloc.state.sorts.first.sortId,
fieldId: checkboxField.id,
),
);
await gridResponseFuture();
expect(sortBloc.state.creatableFields.length, equals(2));
expect(
sortBloc.state.creatableFields
.map((e) => e.id)
.contains(checkboxField.id),
false,
);
});
test('update sort direction', () async {
final selectOptionField = getFirstFieldByType(FieldType.SingleSelect);
sortBloc.add(SortEditorEvent.createSort(fieldId: selectOptionField.id));
await gridResponseFuture();
expect(
sortBloc.state.sorts.first.condition,
SortConditionPB.Ascending,
);
sortBloc.add(
SortEditorEvent.editSort(
sortId: sortBloc.state.sorts.first.sortId,
condition: SortConditionPB.Descending,
),
);
await gridResponseFuture();
expect(
sortBloc.state.sorts.first.condition,
SortConditionPB.Descending,
);
});
test('reorder sorts', () async {
final selectOptionField = getFirstFieldByType(FieldType.SingleSelect);
final checkboxField = getFirstFieldByType(FieldType.Checkbox);
sortBloc
..add(SortEditorEvent.createSort(fieldId: selectOptionField.id))
..add(SortEditorEvent.createSort(fieldId: checkboxField.id));
await gridResponseFuture();
expect(sortBloc.state.sorts[0].fieldId, selectOptionField.id);
expect(sortBloc.state.sorts[1].fieldId, checkboxField.id);
expect(sortBloc.state.creatableFields.length, equals(1));
expect(sortBloc.state.allFields.length, equals(3));
sortBloc.add(
const SortEditorEvent.reorderSort(0, 2),
);
await gridResponseFuture();
expect(sortBloc.state.sorts[0].fieldId, checkboxField.id);
expect(sortBloc.state.sorts[1].fieldId, selectOptionField.id);
expect(sortBloc.state.creatableFields.length, equals(1));
expect(sortBloc.state.allFields.length, equals(3));
});
test('delete sort', () async {
final selectOptionField = getFirstFieldByType(FieldType.SingleSelect);
sortBloc.add(SortEditorEvent.createSort(fieldId: selectOptionField.id));
await gridResponseFuture();
expect(sortBloc.state.sorts.length, 1);
sortBloc.add(
SortEditorEvent.deleteSort(sortBloc.state.sorts.first.sortId),
);
await gridResponseFuture();
expect(sortBloc.state.sorts.length, 0);
expect(sortBloc.state.creatableFields.length, equals(3));
expect(sortBloc.state.allFields.length, equals(3));
});
test('delete all sorts', () async {
final selectOptionField = getFirstFieldByType(FieldType.SingleSelect);
final checkboxField = getFirstFieldByType(FieldType.Checkbox);
sortBloc
..add(SortEditorEvent.createSort(fieldId: selectOptionField.id))
..add(SortEditorEvent.createSort(fieldId: checkboxField.id));
await gridResponseFuture();
expect(sortBloc.state.sorts.length, 2);
sortBloc.add(const SortEditorEvent.deleteAllSorts());
await gridResponseFuture();
expect(sortBloc.state.sorts.length, 0);
expect(sortBloc.state.creatableFields.length, equals(3));
expect(sortBloc.state.allFields.length, equals(3));
});
test('update sort field', () async {
final selectOptionField = getFirstFieldByType(FieldType.SingleSelect);
sortBloc.add(SortEditorEvent.createSort(fieldId: selectOptionField.id));
await gridResponseFuture();
expect(sortBloc.state.sorts.length, equals(1));
// edit field
await FieldBackendService(
viewId: context.view.id,
fieldId: selectOptionField.id,
).updateField(name: "HERRO");
await gridResponseFuture();
expect(sortBloc.state.sorts.length, equals(1));
expect(sortBloc.state.allFields[1].name, "HERRO");
expect(sortBloc.state.creatableFields.length, equals(2));
expect(sortBloc.state.allFields.length, equals(3));
});
test('delete sorting field', () async {
final selectOptionField = getFirstFieldByType(FieldType.SingleSelect);
sortBloc.add(SortEditorEvent.createSort(fieldId: selectOptionField.id));
await gridResponseFuture();
expect(sortBloc.state.sorts.length, equals(1));
// edit field
await FieldBackendService(
viewId: context.view.id,
fieldId: selectOptionField.id,
).delete();
await gridResponseFuture();
expect(sortBloc.state.sorts.length, equals(0));
expect(sortBloc.state.creatableFields.length, equals(2));
expect(sortBloc.state.allFields.length, equals(2));
});
});
}

View File

@ -1,91 +1,52 @@
import 'dart:convert';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_editor_bloc.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/domain/field_service.dart';
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy/workspace/application/settings/share/import_service.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flutter/services.dart';
import '../../util.dart';
class GridTestContext {
GridTestContext(this.gridView, this.gridController);
const v020GridFileName = "v020.afdb";
const v069GridFileName = "v069.afdb";
final ViewPB gridView;
final DatabaseController gridController;
class GridTestContext {
GridTestContext(this.view, this.databaseController);
final ViewPB view;
final DatabaseController databaseController;
List<RowInfo> get rowInfos {
return gridController.rowCache.rowInfos;
return databaseController.rowCache.rowInfos;
}
List<FieldInfo> get fieldInfos => fieldController.fieldInfos;
FieldController get fieldController {
return gridController.fieldController;
}
FieldController get fieldController => databaseController.fieldController;
Future<FieldEditorBloc> createField(FieldType fieldType) async {
final editorBloc =
await createFieldEditor(databaseController: gridController);
await createFieldEditor(databaseController: databaseController);
await gridResponseFuture();
editorBloc.add(FieldEditorEvent.switchFieldType(fieldType));
await gridResponseFuture();
return Future(() => editorBloc);
return editorBloc;
}
FieldInfo getSelectOptionField() {
final fieldInfo = fieldInfos
.firstWhere((element) => element.fieldType == FieldType.SingleSelect);
return fieldInfo;
}
FieldInfo getTextField() {
final fieldInfo = fieldInfos
.firstWhere((element) => element.fieldType == FieldType.RichText);
return fieldInfo;
}
FieldInfo getCheckboxField() {
final fieldInfo = fieldInfos
.firstWhere((element) => element.fieldType == FieldType.Checkbox);
return fieldInfo;
}
SelectOptionCellController makeSelectOptionCellController(
FieldType fieldType,
int rowIndex,
) {
assert(
fieldType == FieldType.SingleSelect || fieldType == FieldType.MultiSelect,
);
final field =
fieldInfos.firstWhere((fieldInfo) => fieldInfo.fieldType == fieldType);
CellController makeGridCellController(int fieldIndex, int rowIndex) {
return makeCellController(
gridController,
CellContext(fieldId: field.id, rowId: rowInfos[rowIndex].rowId),
).as();
}
TextCellController makeTextCellController(int rowIndex) {
final field = fieldInfos
.firstWhere((element) => element.fieldType == FieldType.RichText);
return makeCellController(
gridController,
CellContext(fieldId: field.id, rowId: rowInfos[rowIndex].rowId),
).as();
}
CheckboxCellController makeCheckboxCellController(int rowIndex) {
final field = fieldInfos
.firstWhere((element) => element.fieldType == FieldType.Checkbox);
return makeCellController(
gridController,
CellContext(fieldId: field.id, rowId: rowInfos[rowIndex].rowId),
databaseController,
CellContext(
fieldId: fieldController.fieldInfos[fieldIndex].id,
rowId: rowInfos[rowIndex].rowId,
),
).as();
}
}
@ -121,65 +82,74 @@ class AppFlowyGridTest {
return AppFlowyGridTest(unitTest: inner);
}
Future<GridTestContext> createTestGrid() async {
final app = await unitTest.createWorkspace();
Future<GridTestContext> makeDefaultTestGrid() async {
final workspace = await unitTest.createWorkspace();
final context = await ViewBackendService.createView(
parentViewId: app.id,
parentViewId: workspace.id,
name: "Test Grid",
layoutType: ViewLayoutPB.Grid,
openAfterCreate: true,
).then((result) {
return result.fold(
(view) async {
final context = GridTestContext(
view,
DatabaseController(view: view),
);
final result = await context.gridController.open();
result.fold((l) => null, (r) => throw Exception(r));
return context;
},
(error) {
throw Exception();
},
);
});
).fold(
(view) async {
final databaseController = DatabaseController(view: view);
await databaseController
.open()
.fold((l) => null, (r) => throw Exception(r));
return GridTestContext(
view,
databaseController,
);
},
(error) => throw Exception(),
);
return context;
}
Future<GridTestContext> makeTestGridFromImportedData(
String fileName,
) async {
final workspace = await unitTest.createWorkspace();
// Don't use the p.join to build the path that used in loadString. It
// is not working on windows.
final data = await rootBundle
.loadString("assets/test/workspaces/database/$fileName");
final context = await ImportBackendService.importPages(
workspace.id,
[
ImportValuePayloadPB()
..name = fileName
..data = utf8.encode(data)
..viewLayout = ViewLayoutPB.Grid
..importType = ImportTypePB.RawDatabase,
],
).fold(
(views) async {
final view = views.items.first;
final databaseController = DatabaseController(view: view);
await databaseController
.open()
.fold((l) => null, (r) => throw Exception(r));
return GridTestContext(
view,
databaseController,
);
},
(err) => throw Exception(),
);
return context;
}
}
/// Create a new Grid for cell test
class AppFlowyGridCellTest {
AppFlowyGridCellTest({required this.gridTest});
late GridTestContext context;
final AppFlowyGridTest gridTest;
static Future<AppFlowyGridCellTest> ensureInitialized() async {
final gridTest = await AppFlowyGridTest.ensureInitialized();
return AppFlowyGridCellTest(gridTest: gridTest);
}
Future<void> createTestGrid() async {
context = await gridTest.createTestGrid();
}
Future<void> createTestRow() async {
await RowBackendService.createRow(viewId: context.gridView.id);
}
SelectOptionCellController makeSelectOptionCellController(
FieldType fieldType,
int rowIndex,
) =>
context.makeSelectOptionCellController(fieldType, rowIndex);
Future<void> gridResponseFuture({int milliseconds = 300}) {
return Future.delayed(
gridResponseDuration(milliseconds: milliseconds),
);
}
Future<void> gridResponseFuture({int milliseconds = 400}) {
return Future.delayed(gridResponseDuration(milliseconds: milliseconds));
}
Duration gridResponseDuration({int milliseconds = 400}) {
Duration gridResponseDuration({int milliseconds = 300}) {
return Duration(milliseconds: milliseconds);
}

View File

@ -24,7 +24,7 @@ class AppFlowyUnitTest {
_pathProviderInitialized();
await FlowyRunner.run(
AppFlowyApplicationUniTest(),
AppFlowyApplicationUnitTest(),
IntegrationMode.unitTest,
);
@ -93,7 +93,7 @@ void _pathProviderInitialized() {
});
}
class AppFlowyApplicationUniTest implements EntryPoint {
class AppFlowyApplicationUnitTest implements EntryPoint {
@override
Widget create(LaunchConfiguration config) {
return const SizedBox.shrink();