浏览代码

chore: add filter tests

nathan 2 年之前
父节点
当前提交
b6773a732b

+ 0 - 1
frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart

@@ -66,7 +66,6 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
         state.cells.map((cell) => cell.identifier.fieldInfo).toList(),
       ),
       rowPB: state.rowPB,
-      visible: true,
     );
   }
 

+ 0 - 1
frontend/app_flowy/lib/plugins/board/presentation/board_page.dart

@@ -287,7 +287,6 @@ class _BoardContentState extends State<BoardContent> {
       gridId: gridId,
       fields: UnmodifiableListView(fieldController.fieldInfos),
       rowPB: rowPB,
-      visible: true,
     );
 
     final dataController = GridRowDataController(

+ 0 - 3
frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart

@@ -39,7 +39,6 @@ class GridRowCache {
 
   UnmodifiableListView<RowInfo> get visibleRows {
     var visibleRows = [..._rowList.rows];
-    visibleRows.retainWhere((element) => element.visible);
     return UnmodifiableListView(visibleRows);
   }
 
@@ -236,7 +235,6 @@ class GridRowCache {
       gridId: gridId,
       fields: _fieldNotifier.fields,
       rowPB: rowPB,
-      visible: true,
     );
   }
 }
@@ -264,7 +262,6 @@ class RowInfo with _$RowInfo {
     required String gridId,
     required UnmodifiableListView<FieldInfo> fields,
     required RowPB rowPB,
-    required bool visible,
   }) = _RowInfo;
 }
 

+ 200 - 0
frontend/app_flowy/test/bloc_test/grid_test/cell/select_option_cell_test.dart

@@ -0,0 +1,200 @@
+import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
+import 'package:app_flowy/plugins/grid/application/cell/select_option_editor_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/prelude.dart';
+import 'package:dartz/dartz.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/select_type_option.pb.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:bloc_test/bloc_test.dart';
+import '../util.dart';
+
+void main() {
+  late AppFlowyGridCellTest cellTest;
+  setUpAll(() async {
+    cellTest = await AppFlowyGridCellTest.ensureInitialized();
+  });
+
+  group('SingleSelectOptionBloc', () {
+    late GridSelectOptionCellController cellController;
+    setUp(() async {
+      await cellTest.createTestGrid();
+      await cellTest.createTestRow();
+      cellController =
+          await cellTest.makeCellController(FieldType.SingleSelect, 0);
+    });
+
+    blocTest<SelectOptionCellEditorBloc, SelectOptionEditorState>(
+      "delete options",
+      build: () {
+        final bloc = SelectOptionCellEditorBloc(cellController: cellController);
+        bloc.add(const SelectOptionEditorEvent.initial());
+        return bloc;
+      },
+      act: (bloc) async {
+        bloc.add(const SelectOptionEditorEvent.newOption("A"));
+        await Future.delayed(gridResponseDuration());
+        bloc.add(const SelectOptionEditorEvent.newOption("B"));
+        await Future.delayed(gridResponseDuration());
+        bloc.add(const SelectOptionEditorEvent.newOption("C"));
+        await Future.delayed(gridResponseDuration());
+        bloc.add(const SelectOptionEditorEvent.deleteAllOptions());
+      },
+      wait: gridResponseDuration(),
+      verify: (bloc) {
+        assert(bloc.state.options.isEmpty);
+      },
+    );
+
+    blocTest<SelectOptionCellEditorBloc, SelectOptionEditorState>(
+      "create option",
+      build: () {
+        final bloc = SelectOptionCellEditorBloc(cellController: cellController);
+        bloc.add(const SelectOptionEditorEvent.initial());
+        return bloc;
+      },
+      act: (bloc) async {
+        bloc.add(const SelectOptionEditorEvent.newOption("A"));
+      },
+      wait: gridResponseDuration(),
+      verify: (bloc) {
+        expect(bloc.state.options.length, 1);
+        expect(bloc.state.options[0].name, "A");
+      },
+    );
+
+    blocTest<SelectOptionCellEditorBloc, SelectOptionEditorState>(
+      "delete option",
+      build: () {
+        final bloc = SelectOptionCellEditorBloc(cellController: cellController);
+        bloc.add(const SelectOptionEditorEvent.initial());
+        return bloc;
+      },
+      act: (bloc) async {
+        bloc.add(const SelectOptionEditorEvent.newOption("A"));
+        await Future.delayed(gridResponseDuration());
+        bloc.add(SelectOptionEditorEvent.deleteOption(bloc.state.options[0]));
+      },
+      wait: gridResponseDuration(),
+      verify: (bloc) {
+        assert(bloc.state.options.isEmpty);
+      },
+    );
+
+    blocTest<SelectOptionCellEditorBloc, SelectOptionEditorState>(
+      "update option",
+      build: () {
+        final bloc = SelectOptionCellEditorBloc(cellController: cellController);
+        bloc.add(const SelectOptionEditorEvent.initial());
+        return bloc;
+      },
+      act: (bloc) async {
+        bloc.add(const SelectOptionEditorEvent.newOption("A"));
+        await Future.delayed(gridResponseDuration());
+        SelectOptionPB optionUpdate = bloc.state.options[0]
+          ..color = SelectOptionColorPB.Aqua
+          ..name = "B";
+        bloc.add(SelectOptionEditorEvent.updateOption(optionUpdate));
+      },
+      wait: gridResponseDuration(),
+      verify: (bloc) {
+        assert(bloc.state.options.length == 1);
+        expect(bloc.state.options[0].color, SelectOptionColorPB.Aqua);
+        expect(bloc.state.options[0].name, "B");
+      },
+    );
+
+    blocTest<SelectOptionCellEditorBloc, SelectOptionEditorState>(
+      "select/unselect option",
+      build: () {
+        final bloc = SelectOptionCellEditorBloc(cellController: cellController);
+        bloc.add(const SelectOptionEditorEvent.initial());
+        return bloc;
+      },
+      act: (bloc) async {
+        bloc.add(const SelectOptionEditorEvent.newOption("A"));
+        await Future.delayed(gridResponseDuration());
+        expect(bloc.state.selectedOptions.length, 1);
+        final optionId = bloc.state.options[0].id;
+        bloc.add(SelectOptionEditorEvent.unSelectOption(optionId));
+        await Future.delayed(gridResponseDuration());
+        assert(bloc.state.selectedOptions.isEmpty);
+        bloc.add(SelectOptionEditorEvent.selectOption(optionId));
+      },
+      wait: gridResponseDuration(),
+      verify: (bloc) {
+        assert(bloc.state.selectedOptions.length == 1);
+        expect(bloc.state.selectedOptions[0].name, "A");
+      },
+    );
+
+    blocTest<SelectOptionCellEditorBloc, SelectOptionEditorState>(
+      "select an option or create one",
+      build: () {
+        final bloc = SelectOptionCellEditorBloc(cellController: cellController);
+        bloc.add(const SelectOptionEditorEvent.initial());
+        return bloc;
+      },
+      act: (bloc) async {
+        bloc.add(const SelectOptionEditorEvent.newOption("A"));
+        await Future.delayed(gridResponseDuration());
+        bloc.add(const SelectOptionEditorEvent.trySelectOption("B"));
+        await Future.delayed(gridResponseDuration());
+        bloc.add(const SelectOptionEditorEvent.trySelectOption("A"));
+      },
+      wait: gridResponseDuration(),
+      verify: (bloc) {
+        assert(bloc.state.selectedOptions.length == 1);
+        assert(bloc.state.options.length == 2);
+        expect(bloc.state.selectedOptions[0].name, "A");
+      },
+    );
+
+    blocTest<SelectOptionCellEditorBloc, SelectOptionEditorState>(
+      "select multiple options",
+      build: () {
+        final bloc = SelectOptionCellEditorBloc(cellController: cellController);
+        bloc.add(const SelectOptionEditorEvent.initial());
+        return bloc;
+      },
+      act: (bloc) async {
+        bloc.add(const SelectOptionEditorEvent.newOption("A"));
+        await Future.delayed(gridResponseDuration());
+        bloc.add(const SelectOptionEditorEvent.newOption("B"));
+        await Future.delayed(gridResponseDuration());
+        bloc.add(const SelectOptionEditorEvent.selectMultipleOptions(
+            ["A", "B", "C"], "x"));
+      },
+      wait: gridResponseDuration(),
+      verify: (bloc) {
+        assert(bloc.state.selectedOptions.length == 1);
+        expect(bloc.state.selectedOptions[0].name, "A");
+        expect(bloc.state.filter, const Some("x"));
+      },
+    );
+
+    blocTest<SelectOptionCellEditorBloc, SelectOptionEditorState>(
+      "filter options",
+      build: () {
+        final bloc = SelectOptionCellEditorBloc(cellController: cellController);
+        bloc.add(const SelectOptionEditorEvent.initial());
+        return bloc;
+      },
+      act: (bloc) async {
+        bloc.add(const SelectOptionEditorEvent.newOption("abcd"));
+        await Future.delayed(gridResponseDuration());
+        bloc.add(const SelectOptionEditorEvent.newOption("aaaa"));
+        await Future.delayed(gridResponseDuration());
+        bloc.add(const SelectOptionEditorEvent.newOption("defg"));
+        await Future.delayed(gridResponseDuration());
+        bloc.add(const SelectOptionEditorEvent.filterOption("a"));
+      },
+      wait: gridResponseDuration(),
+      verify: (bloc) {
+        expect(bloc.state.options.length, 2);
+        expect(bloc.state.allOptions.length, 3);
+        expect(bloc.state.createOption, const Some("a"));
+        expect(bloc.state.filter, const Some("a"));
+      },
+    );
+  });
+}

+ 102 - 0
frontend/app_flowy/test/bloc_test/grid_test/field/edit_field_test.dart

@@ -0,0 +1,102 @@
+import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
+import 'package:app_flowy/plugins/grid/application/prelude.dart';
+import 'package:bloc_test/bloc_test.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/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.singleSelectFieldContext();
+  final loader = FieldTypeOptionLoader(
+    gridId: context.gridView.id,
+    field: fieldInfo.field,
+  );
+
+  return FieldEditorBloc(
+    gridId: context.gridView.id,
+    fieldName: fieldInfo.name,
+    isGroupField: fieldInfo.isGroupField,
+    loader: loader,
+  )..add(const FieldEditorEvent.initial());
+}
+
+void main() {
+  late AppFlowyGridTest gridTest;
+
+  setUpAll(() async {
+    gridTest = await AppFlowyGridTest.ensureInitialized();
+  });
+
+  group('$FieldEditorBloc', () {
+    late FieldEditorBloc editorBloc;
+
+    setUp(() async {
+      final context = await gridTest.createTestGrid();
+      final fieldInfo = context.singleSelectFieldContext();
+      final loader = FieldTypeOptionLoader(
+        gridId: context.gridView.id,
+        field: fieldInfo.field,
+      );
+
+      editorBloc = FieldEditorBloc(
+        gridId: context.gridView.id,
+        fieldName: fieldInfo.name,
+        isGroupField: fieldInfo.isGroupField,
+        loader: loader,
+      )..add(const FieldEditorEvent.initial());
+
+      await gridResponseFuture();
+    });
+
+    blocTest<FieldEditorBloc, FieldEditorState>(
+      "rename field",
+      build: () => editorBloc,
+      act: (bloc) async {
+        editorBloc.add(const FieldEditorEvent.updateName('Hello world'));
+      },
+      wait: gridResponseDuration(),
+      verify: (bloc) {
+        bloc.state.field.fold(
+          () => throw Exception("The field should not be none"),
+          (field) {
+            assert(field.name == 'Hello world');
+          },
+        );
+      },
+    );
+
+    blocTest<FieldEditorBloc, FieldEditorState>(
+      "switch to text field",
+      build: () => editorBloc,
+      act: (bloc) async {
+        editorBloc
+            .add(const FieldEditorEvent.switchToField(FieldType.RichText));
+      },
+      wait: gridResponseDuration(),
+      verify: (bloc) {
+        bloc.state.field.fold(
+          () => throw Exception("The field should not be none"),
+          (field) {
+            // The default length of the fields is 3. The length of the fields
+            // should not change after switching to other field type
+            // assert(gridTest.fieldContexts.length == 3);
+            assert(field.fieldType == FieldType.RichText);
+          },
+        );
+      },
+    );
+
+    blocTest<FieldEditorBloc, FieldEditorState>(
+      "delete field",
+      build: () => editorBloc,
+      act: (bloc) async {
+        editorBloc.add(const FieldEditorEvent.deleteField());
+      },
+      wait: gridResponseDuration(),
+      verify: (bloc) {
+        // assert(gridTest.fieldContexts.length == 2);
+      },
+    );
+  });
+}

+ 148 - 0
frontend/app_flowy/test/bloc_test/grid_test/filter/create_filter_test.dart

@@ -0,0 +1,148 @@
+import 'package:app_flowy/plugins/grid/application/filter/filter_service.dart';
+import 'package:app_flowy/plugins/grid/application/grid_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/grid_data_controller.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pbenum.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/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 = FilterFFIService(viewId: context.gridView.id);
+    final textField = context.textFieldContext();
+    await service.insertTextFilter(
+        fieldId: textField.id,
+        condition: TextFilterCondition.TextIsEmpty,
+        content: "");
+    await gridResponseFuture();
+
+    assert(context.fieldController.filterInfos.length == 1);
+  });
+
+  test('delete a text filter)', () async {
+    final context = await gridTest.createTestGrid();
+    final service = FilterFFIService(viewId: context.gridView.id);
+    final textField = context.textFieldContext();
+    await service.insertTextFilter(
+        fieldId: textField.id,
+        condition: TextFilterCondition.TextIsEmpty,
+        content: "");
+    await gridResponseFuture();
+
+    final filterInfo = context.fieldController.filterInfos.first;
+    await service.deleteFilter(
+      fieldId: textField.id,
+      filterId: filterInfo.filter.id,
+      fieldType: textField.fieldType,
+    );
+    await gridResponseFuture();
+
+    assert(context.fieldController.filterInfos.isEmpty);
+  });
+
+  test('filter rows with condition: text is empty', () async {
+    final context = await gridTest.createTestGrid();
+    final service = FilterFFIService(viewId: context.gridView.id);
+    final gridController = GridController(view: context.gridView);
+    final gridBloc = GridBloc(
+      view: context.gridView,
+      gridController: gridController,
+    )..add(const GridEvent.initial());
+    await gridResponseFuture();
+
+    final textField = context.textFieldContext();
+    service.insertTextFilter(
+        fieldId: textField.id,
+        condition: TextFilterCondition.TextIsEmpty,
+        content: "");
+    await gridResponseFuture();
+
+    assert(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 = FilterFFIService(viewId: context.gridView.id);
+    final gridController = GridController(view: context.gridView);
+    final gridBloc = GridBloc(
+      view: context.gridView,
+      gridController: gridController,
+    )..add(const GridEvent.initial());
+    await gridResponseFuture();
+
+    final textField = context.textFieldContext();
+    await service.insertTextFilter(
+        fieldId: textField.id,
+        condition: TextFilterCondition.TextIsEmpty,
+        content: "");
+    await gridResponseFuture();
+
+    final controller = await context.makeTextCellController(0);
+    controller.saveCellData("edit text cell content");
+    await gridResponseFuture();
+    assert(gridBloc.state.rowInfos.length == 2);
+
+    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 = FilterFFIService(viewId: context.gridView.id);
+    final textField = context.textFieldContext();
+    await gridResponseFuture();
+    await service.insertTextFilter(
+        fieldId: textField.id,
+        condition: TextFilterCondition.TextIsNotEmpty,
+        content: "");
+    await gridResponseFuture();
+    assert(context.rowInfos.isEmpty);
+  });
+
+  test('filter rows with condition: checkbox uncheck', () async {
+    final context = await gridTest.createTestGrid();
+    final checkboxField = context.checkboxFieldContext();
+    final service = FilterFFIService(viewId: context.gridView.id);
+    final gridController = GridController(view: context.gridView);
+    final gridBloc = GridBloc(
+      view: context.gridView,
+      gridController: gridController,
+    )..add(const GridEvent.initial());
+
+    await gridResponseFuture();
+    await service.insertCheckboxFilter(
+      fieldId: checkboxField.id,
+      condition: CheckboxFilterCondition.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.checkboxFieldContext();
+    final service = FilterFFIService(viewId: context.gridView.id);
+    final gridController = GridController(view: context.gridView);
+    final gridBloc = GridBloc(
+      view: context.gridView,
+      gridController: gridController,
+    )..add(const GridEvent.initial());
+
+    await gridResponseFuture();
+    await service.insertCheckboxFilter(
+      fieldId: checkboxField.id,
+      condition: CheckboxFilterCondition.IsChecked,
+    );
+    await gridResponseFuture();
+    assert(gridBloc.state.rowInfos.isEmpty);
+  });
+}

+ 57 - 0
frontend/app_flowy/test/bloc_test/grid_test/filter/edit_filter_field_test.dart

@@ -0,0 +1,57 @@
+import 'package:app_flowy/plugins/grid/application/field/field_editor_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
+import 'package:app_flowy/plugins/grid/application/filter/filter_menu_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/filter/filter_service.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/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 and then alter the filter's field)", () async {
+    final context = await gridTest.createTestGrid();
+    final service = FilterFFIService(viewId: context.gridView.id);
+    final textField = context.textFieldContext();
+
+    // Create the filter menu bloc
+    final menuBloc = GridFilterMenuBloc(
+      fieldController: context.fieldController,
+      viewId: context.gridView.id,
+    )..add(const GridFilterMenuEvent.initial());
+
+    // Insert filter for the text field
+    await service.insertTextFilter(
+        fieldId: textField.id,
+        condition: TextFilterCondition.TextIsEmpty,
+        content: "");
+    await gridResponseFuture();
+    assert(menuBloc.state.filters.length == 1);
+
+    // Edit the text field
+    final loader = FieldTypeOptionLoader(
+      gridId: context.gridView.id,
+      field: textField.field,
+    );
+
+    final editorBloc = FieldEditorBloc(
+      gridId: context.gridView.id,
+      fieldName: textField.field.name,
+      isGroupField: false,
+      loader: loader,
+    )..add(const FieldEditorEvent.initial());
+    await gridResponseFuture();
+
+    // Alter the field type to Number
+    editorBloc.add(const FieldEditorEvent.switchToField(FieldType.Number));
+    await gridResponseFuture();
+
+    // Check the number of filters
+    assert(menuBloc.state.filters.isEmpty);
+  });
+}

+ 61 - 0
frontend/app_flowy/test/bloc_test/grid_test/filter/filter_menu_test.dart

@@ -0,0 +1,61 @@
+import 'package:app_flowy/plugins/grid/application/filter/filter_menu_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/filter/filter_service.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/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('test filter menu after create a text filter)', () async {
+    final context = await gridTest.createTestGrid();
+    final menuBloc = GridFilterMenuBloc(
+        viewId: context.gridView.id, fieldController: context.fieldController)
+      ..add(const GridFilterMenuEvent.initial());
+    await gridResponseFuture();
+    assert(menuBloc.state.creatableFields.length == 1);
+
+    final service = FilterFFIService(viewId: context.gridView.id);
+    final textField = context.textFieldContext();
+    await service.insertTextFilter(
+        fieldId: textField.id,
+        condition: TextFilterCondition.TextIsEmpty,
+        content: "");
+    await gridResponseFuture();
+    assert(menuBloc.state.creatableFields.isEmpty);
+  });
+
+  test('test filter menu after update existing text filter)', () async {
+    final context = await gridTest.createTestGrid();
+    final menuBloc = GridFilterMenuBloc(
+        viewId: context.gridView.id, fieldController: context.fieldController)
+      ..add(const GridFilterMenuEvent.initial());
+    await gridResponseFuture();
+
+    final service = FilterFFIService(viewId: context.gridView.id);
+    final textField = context.textFieldContext();
+
+    // Create filter
+    await service.insertTextFilter(
+        fieldId: textField.id,
+        condition: TextFilterCondition.TextIsEmpty,
+        content: "");
+    await gridResponseFuture();
+
+    final textFilter = context.fieldController.filterInfos.first;
+    // Update the existing filter
+    await service.insertTextFilter(
+        fieldId: textField.id,
+        filterId: textFilter.filter.id,
+        condition: TextFilterCondition.Is,
+        content: "ABC");
+    await gridResponseFuture();
+    assert(menuBloc.state.filters.first.textFilter()!.condition ==
+        TextFilterCondition.Is);
+    assert(menuBloc.state.filters.first.textFilter()!.content == "ABC");
+  });
+}

+ 99 - 0
frontend/app_flowy/test/bloc_test/grid_test/filter/filter_rows_test.dart

@@ -0,0 +1,99 @@
+import 'package:app_flowy/plugins/grid/application/filter/filter_service.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/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 or is not empty condition)', () async {
+    final context = await createTestFilterGrid(gridTest);
+
+    final service = FilterFFIService(viewId: context.gridView.id);
+    final textField = context.textFieldContext();
+    // create a new filter
+    await service.insertTextFilter(
+        fieldId: textField.id,
+        condition: TextFilterCondition.TextIsEmpty,
+        content: "");
+    await gridResponseFuture();
+    assert(context.fieldController.filterInfos.length == 1,
+        "expect 1 but receive ${context.fieldController.filterInfos.length}");
+    assert(context.rowInfos.length == 1,
+        "expect 1 but receive ${context.rowInfos.length}");
+
+    // Update the existing filter
+    final textFilter = context.fieldController.filterInfos.first;
+    await service.insertTextFilter(
+        fieldId: textField.id,
+        filterId: textFilter.filter.id,
+        condition: TextFilterCondition.TextIsNotEmpty,
+        content: "");
+    await gridResponseFuture();
+    assert(context.rowInfos.length == 2);
+
+    // delete the filter
+    await service.deleteFilter(
+      fieldId: textField.id,
+      filterId: textFilter.filter.id,
+      fieldType: textField.fieldType,
+    );
+    await gridResponseFuture();
+    assert(context.rowInfos.length == 3);
+  });
+
+  test('filter rows by text is condition)', () async {
+    final context = await createTestFilterGrid(gridTest);
+
+    final service = FilterFFIService(viewId: context.gridView.id);
+    final textField = context.textFieldContext();
+    // create a new filter
+    await service.insertTextFilter(
+        fieldId: textField.id, condition: TextFilterCondition.Is, 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.filterInfos.first;
+    await service.insertTextFilter(
+        fieldId: textField.id,
+        filterId: textFilter.filter.id,
+        condition: TextFilterCondition.Is,
+        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.filter.id,
+        condition: TextFilterCondition.Is,
+        content: "b");
+    await gridResponseFuture();
+    assert(context.rowInfos.length == 1);
+
+    // Update the existing filter with content 'C'
+    await service.insertTextFilter(
+        fieldId: textField.id,
+        filterId: textFilter.filter.id,
+        condition: TextFilterCondition.Is,
+        content: "C");
+    await gridResponseFuture();
+    assert(context.rowInfos.isEmpty);
+
+    // delete the filter
+    await service.deleteFilter(
+      fieldId: textField.id,
+      filterId: textFilter.filter.id,
+      fieldType: textField.fieldType,
+    );
+    await gridResponseFuture();
+    assert(context.rowInfos.length == 3);
+  });
+}

+ 42 - 0
frontend/app_flowy/test/bloc_test/grid_test/filter/filter_util.dart

@@ -0,0 +1,42 @@
+import 'package:app_flowy/plugins/grid/application/grid_data_controller.dart';
+import 'package:app_flowy/plugins/grid/grid.dart';
+import 'package:app_flowy/workspace/application/app/app_service.dart';
+
+import '../util.dart';
+
+Future<GridTestContext> createTestFilterGrid(AppFlowyGridTest gridTest) async {
+  final app = await gridTest.unitTest.createTestApp();
+  final builder = GridPluginBuilder();
+  final context = await AppService()
+      .createView(
+    appId: app.id,
+    name: "Filter Grid",
+    dataFormatType: builder.dataFormatType,
+    pluginType: builder.pluginType,
+    layoutType: builder.layoutType!,
+  )
+      .then((result) {
+    return result.fold(
+      (view) async {
+        final context = GridTestContext(view, GridController(view: view));
+        final result = await context.gridController.openGrid();
+
+        await editCells(context);
+        await gridResponseFuture(milliseconds: 500);
+        result.fold((l) => null, (r) => throw Exception(r));
+        return context;
+      },
+      (error) => throw Exception(),
+    );
+  });
+
+  return context;
+}
+
+Future<void> editCells(GridTestContext context) async {
+  final controller0 = await context.makeTextCellController(0);
+  final controller1 = await context.makeTextCellController(1);
+
+  controller0.saveCellData('A');
+  controller1.saveCellData('B');
+}

+ 13 - 11
frontend/app_flowy/test/bloc_test/grid_test/util.dart

@@ -60,15 +60,17 @@ class GridTestContext {
     return editorBloc;
   }
 
-  Future<IGridCellController> makeCellController(String fieldId) async {
-    final builder = await makeCellControllerBuilder(fieldId);
+  Future<IGridCellController> makeCellController(
+      String fieldId, int rowIndex) async {
+    final builder = await makeCellControllerBuilder(fieldId, rowIndex);
     return builder.build();
   }
 
   Future<GridCellControllerBuilder> makeCellControllerBuilder(
     String fieldId,
+    int rowIndex,
   ) async {
-    final RowInfo rowInfo = rowInfos.last;
+    final RowInfo rowInfo = rowInfos[rowIndex];
     final blockCache = blocks[rowInfo.rowPB.blockId];
     final rowCache = blockCache?.rowCache;
     final fieldController = gridController.fieldController;
@@ -125,22 +127,22 @@ class GridTestContext {
   }
 
   Future<GridSelectOptionCellController> makeSelectOptionCellController(
-      FieldType fieldType) async {
+      FieldType fieldType, int rowIndex) async {
     assert(fieldType == FieldType.SingleSelect ||
         fieldType == FieldType.MultiSelect);
 
     final field =
         fieldContexts.firstWhere((element) => element.fieldType == fieldType);
-    final cellController =
-        await makeCellController(field.id) as GridSelectOptionCellController;
+    final cellController = await makeCellController(field.id, rowIndex)
+        as GridSelectOptionCellController;
     return cellController;
   }
 
-  Future<GridCellController> makeTextCellController() async {
+  Future<GridCellController> makeTextCellController(int rowIndex) async {
     final field = fieldContexts
         .firstWhere((element) => element.fieldType == FieldType.RichText);
     final cellController =
-        await makeCellController(field.id) as GridCellController;
+        await makeCellController(field.id, rowIndex) as GridCellController;
     return cellController;
   }
 }
@@ -205,12 +207,12 @@ class AppFlowyGridCellTest {
   }
 
   Future<GridSelectOptionCellController> makeCellController(
-      FieldType fieldType) async {
-    return context.makeSelectOptionCellController(fieldType);
+      FieldType fieldType, int rowIndex) async {
+    return context.makeSelectOptionCellController(fieldType, rowIndex);
   }
 }
 
-Future<void> gridResponseFuture({int milliseconds = 500}) {
+Future<void> gridResponseFuture({int milliseconds = 200}) {
   return Future.delayed(gridResponseDuration(milliseconds: milliseconds));
 }