Skip to main content
This guide covers testing strategies for AppFlowy Editor, from unit tests to widget tests, helping you build reliable editing experiences.

Testing Overview

AppFlowy Editor provides testing utilities that make it easy to:
  • Test basic editor functions
  • Simulate user interactions
  • Test custom block components
  • Test collaborative features
  • Verify document state

Setup

Add testing dependencies to your pubspec.yaml:
dev_dependencies:
  flutter_test:
    sdk: flutter
  appflowy_editor: ^latest

Test File Structure

Mirror your code structure in tests:
lib/
  src/
    custom_block.dart
test/
  src/
    custom_block_test.dart
This makes it easy to map files to their corresponding tests.

Basic Editor Testing

Setting Up a Test Editor

import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  setUpAll(() {
    TestWidgetsFlutterBinding.ensureInitialized();
  });

  group('Basic Editor Tests', () {
    testWidgets('Initialize editor', (tester) async {
      const text = 'Welcome to AppFlowy 😁';
      final editor = tester.editor;

      // Insert an empty text node
      editor.insertEmptyTextNode();

      // Insert a text node with content
      editor.insertTextNode(text);

      // Start testing
      await editor.startTesting();

      // Verify document length
      expect(editor.documentLength, 2);
    });
  });
}

Creating Test Documents

import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('Create document with multiple node types', (tester) async {
    const text = 'Welcome to AppFlowy 😁';
    final editor = tester.editor;

    // Insert empty text node
    editor.insertEmptyTextNode();

    // Insert regular text
    editor.insertTextNode(text);

    // Insert heading
    editor.insertTextNode(text, attributes: {
      BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
      BuiltInAttributeKey.heading: BuiltInAttributeKey.h1,
    });

    // Insert bulleted list with bold text
    editor.insertTextNode(
      '',
      attributes: {
        BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList,
      },
      delta: Delta([
        TextInsert(text, {BuiltInAttributeKey.bold: true}),
      ]),
    );

    await editor.startTesting();

    // Verify nodes were created
    expect(editor.documentLength, 4);
  });
}

Testing Node Operations

Accessing Nodes

import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('Access and verify nodes', (tester) async {
    const text = 'Welcome to AppFlowy 😁';
    final editor = tester.editor;

    editor.insertTextNode(text);
    await editor.startTesting();

    // Get node at path
    final firstTextNode = editor.nodeAtPath([0]) as TextNode;
    
    // Verify node content
    expect(firstTextNode.toRawString(), text);
  });
}

Testing Text Insertion

import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('Insert text into node', (tester) async {
    final editor = tester.editor;
    editor.insertTextNode('World');
    await editor.startTesting();

    final textNode = editor.nodeAtPath([0]) as TextNode;
    
    // Insert text at beginning
    editor.insertText(textNode, 'Hello ', 0);

    // Verify result
    expect(textNode.toRawString(), 'Hello World');
  });
}

Testing Attributes

import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('Verify node attributes', (tester) async {
    final editor = tester.editor;
    
    editor.insertTextNode('Heading', attributes: {
      BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
      BuiltInAttributeKey.heading: BuiltInAttributeKey.h1,
    });
    
    await editor.startTesting();

    final textNode = editor.nodeAtPath([0]) as TextNode;
    final attributes = textNode.attributes;

    expect(attributes[BuiltInAttributeKey.subtype], BuiltInAttributeKey.heading);
    expect(attributes[BuiltInAttributeKey.heading], BuiltInAttributeKey.h1);
  });
}

Testing Selection

Update Selection

import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('Update and verify selection', (tester) async {
    const text = 'Welcome to AppFlowy 😁';
    final editor = tester.editor;

    editor.insertTextNode(text);
    await editor.startTesting();

    final firstTextNode = editor.nodeAtPath([0]) as TextNode;

    // Update selection
    await editor.updateSelection(
      Selection.single(path: firstTextNode.path, startOffset: 0),
    );

    // Get current selection
    final selection = editor.documentSelection;
    
    expect(selection, isNotNull);
    expect(selection!.start.path, [0]);
    expect(selection.start.offset, 0);
  });
}

Testing Keyboard Input

Simulating Keyboard Shortcuts

import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('Test select all shortcut', (tester) async {
    const lines = 100;
    const text = 'Welcome to AppFlowy 😁';
    final editor = tester.editor;

    // Insert 100 lines of text
    for (var i = 0; i < lines; i++) {
      editor.insertTextNode(text);
    }

    await editor.startTesting();

    // Simulate Command+A (Select All)
    await editor.pressLogicKey(
      LogicalKeyboardKey.keyA,
      isMetaPressed: true,
    );

    // Verify selection
    expect(
      editor.documentSelection,
      Selection(
        start: Position(path: [0], offset: 0),
        end: Position(path: [lines - 1], offset: text.length),
      ),
    );
  });
}

Testing Key Combinations

import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('Test keyboard shortcuts', (tester) async {
    final editor = tester.editor;
    editor.insertTextNode('Test');
    await editor.startTesting();

    // Meta + A
    await editor.pressLogicKey(
      LogicalKeyboardKey.keyA,
      isMetaPressed: true,
    );

    // Meta + Shift + S
    await editor.pressLogicKey(
      LogicalKeyboardKey.keyS,
      isMetaPressed: true,
      isShiftPressed: true,
    );
  });
}

Testing Custom Components

Widget Test for Custom Block

import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('Test custom block component', (tester) async {
    final editorState = EditorState(
      document: Document.blank(),
    );

    await tester.pumpWidget(
      MaterialApp(
        home: AppFlowyEditor(
          editorState: editorState,
          blockComponentBuilders: {
            'custom': CustomBlockComponentBuilder(),
          },
        ),
      ),
    );

    // Insert custom block
    final transaction = editorState.transaction;
    transaction.insertNode(
      [0],
      Node(type: 'custom', attributes: {'data': 'test'}),
    );
    await editorState.apply(transaction);
    await tester.pumpAndSettle();

    // Verify custom block is rendered
    expect(find.byType(CustomBlockWidget), findsOneWidget);
  });
}

Testing Transactions

Transaction Application

import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  test('Apply transaction', () {
    final document = Document.blank();
    final editorState = EditorState(document: document);

    // Create transaction
    final transaction = editorState.transaction;
    transaction.insertNode(
      [0],
      paragraphNode(text: 'Hello World'),
    );

    // Apply transaction
    editorState.apply(transaction);

    // Verify change
    expect(editorState.document.root.children.length, 1);
    final node = editorState.document.root.children.first as TextNode;
    expect(node.toRawString(), 'Hello World');
  });
}

Testing Undo/Redo

import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  test('Test undo and redo', () {
    final editorState = EditorState(document: Document.blank());

    // Make changes
    final transaction = editorState.transaction;
    transaction.insertNode([0], paragraphNode(text: 'Hello'));
    editorState.apply(transaction);

    expect(editorState.document.root.children.length, 1);

    // Undo
    editorState.undoManager.undo();
    expect(editorState.document.root.children.length, 0);

    // Redo
    editorState.undoManager.redo();
    expect(editorState.document.root.children.length, 1);
  });
}

Testing Collaborative Features

Remote Transaction Application

import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  test('Apply remote transaction', () {
    final editorState = EditorState(document: Document.blank());

    // Create remote transaction
    final transaction = editorState.transaction;
    transaction.insertNode([0], paragraphNode(text: 'Remote edit'));

    // Apply as remote
    editorState.apply(
      transaction,
      isRemote: true,
      options: const ApplyOptions(
        recordUndo: false,
        recordRedo: false,
      ),
    );

    // Verify change was applied
    expect(editorState.document.root.children.length, 1);
    
    // Verify it's not in undo stack
    expect(editorState.undoManager.undoStack.isEmpty, true);
  });
}

Integration Testing

End-to-End Test

import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('Full editor workflow', (tester) async {
    final editorState = EditorState(document: Document.blank());

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: AppFlowyEditor(editorState: editorState),
        ),
      ),
    );

    // Type text
    await tester.enterText(
      find.byType(AppFlowyEditor),
      'Hello World',
    );
    await tester.pumpAndSettle();

    // Verify text was entered
    expect(find.text('Hello World'), findsOneWidget);
  });
}

Best Practices

  1. Use TestWidgetsFlutterBinding: Always initialize in setUpAll()
  2. Start Testing: Call await editor.startTesting() before assertions
  3. Pump and Settle: Use await tester.pumpAndSettle() after state changes
  4. Isolate Tests: Each test should be independent
  5. Test Edge Cases: Test empty documents, large documents, special characters
  6. Mock External Dependencies: Mock network calls and file I/O
  7. Use Descriptive Names: Test names should describe what they test
  8. Clean Up: Dispose resources in tearDown()

Testing Checklist

  • Basic node insertion and deletion
  • Text formatting (bold, italic, etc.)
  • Selection and cursor movement
  • Keyboard shortcuts
  • Undo/redo functionality
  • Custom block components
  • Transaction application
  • Remote transactions (for collaboration)
  • Edge cases (empty document, large document)
  • Performance under load

Common Patterns

Setup Helper

import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';

EditorState createTestEditor() {
  return EditorState(
    document: Document.blank(),
  );
}

void main() {
  late EditorState editorState;

  setUp(() {
    editorState = createTestEditor();
  });

  tearDown(() {
    editorState.dispose();
  });

  test('Test with helper', () {
    // Use editorState
  });
}

Custom Matchers

import 'package:flutter_test/flutter_test.dart';

Matcher hasNodeCount(int count) {
  return predicate<EditorState>(
    (state) => state.document.root.children.length == count,
    'has $count nodes',
  );
}

void main() {
  test('Use custom matcher', () {
    final editorState = createTestEditor();
    expect(editorState, hasNodeCount(0));
  });
}

Build docs developers (and LLMs) love