Writing Tests

Test your Nocterm components

Nocterm provides a testing framework for TUI components. This guide shows you how to write tests.

Test setup

Import the test library:

import 'package:nocterm/nocterm.dart';
import 'package:nocterm/nocterm_test.dart';
import 'package:test/test.dart';

void main() {
  test('counter increments', () async {
    await testNocterm(
      'counter test',
      (tester) async {
        await tester.pumpComponent(Counter());

        expect(tester.terminalState, containsText('Count: 0'));

        await tester.sendKey(LogicalKey.space);

        expect(tester.terminalState, containsText('Count: 1'));
      },
    );
  });
}

testNocterm function

testNocterm runs a test with a virtual terminal:

await testNocterm(
  'test description',
  (tester) async {
    // Test code here
  },
  size: Size(80, 24),  // Optional terminal size
  debugPrintAfterPump: false,  // Optional debug output
);

NoctermTester

The tester provides methods for testing:

// Render a component
await tester.pumpComponent(MyComponent());

// Send a key press
await tester.sendKey(LogicalKey.enter);

// Send multiple keys
await tester.sendKeys([
  LogicalKey.keyH,
  LogicalKey.keyE,
  LogicalKey.keyL,
  LogicalKey.keyL,
  LogicalKey.keyO,
]);

// Type text
await tester.enterText('hello');

// Access terminal state
final state = tester.terminalState;

Basic assertions

Use matchers to verify output:

test('text display', () async {
  await testNocterm(
    'displays text',
    (tester) async {
      await tester.pumpComponent(Text('Hello'));

      // Check if text is present
      expect(tester.terminalState, containsText('Hello'));
    },
  );
});

containsText

Check if text appears anywhere:

expect(tester.terminalState, containsText('Hello'));

hasTextAt

Check text at specific position:

expect(tester.terminalState, hasTextAt(5, 10, 'Title'));

hasStyledText

Check text with specific styling:

expect(
  tester.terminalState,
  hasStyledText('Error', color: Colors.red),
);

Testing interactions

Simulate user input:

test('button click', () async {
  await testNocterm(
    'handles click',
    (tester) async {
      var clicked = false;

      await tester.pumpComponent(
        Focusable(
          focused: true,
          onKeyEvent: (event) {
            if (event.logicalKey == LogicalKey.enter) {
              clicked = true;
              return true;
            }
            return false;
          },
          child: Text('Press Enter'),
        ),
      );

      await tester.sendKey(LogicalKey.enter);

      expect(clicked, isTrue);
    },
  );
});

Simulating keyboard input

// Single key
await tester.sendKey(LogicalKey.arrowDown);

// Multiple keys
await tester.sendKeys([
  LogicalKey.keyC,
  LogicalKey.keyT,
  LogicalKey.keyR,
  LogicalKey.keyL,
]);

// Type text
await tester.enterText('hello world');

// With modifiers
await tester.sendKey(
  LogicalKey.keyS,
  control: true,  // Ctrl+S
);

Testing state changes

Test components that change over time:

test('counter state', () async {
  await testNocterm(
    'counter increments',
    (tester) async {
      await tester.pumpComponent(Counter());

      expect(tester.terminalState, containsText('Count: 0'));

      await tester.sendKey(LogicalKey.space);
      expect(tester.terminalState, containsText('Count: 1'));

      await tester.sendKey(LogicalKey.space);
      expect(tester.terminalState, containsText('Count: 2'));
    },
  );
});

Best practices

Test one thing at a time

// Good - focused test
test('displays title', () async {
  await testNocterm('title', (tester) async {
    await tester.pumpComponent(App());
    expect(tester.terminalState, containsText('My App'));
  });
});

// Bad - testing too much
test('everything', () async {
  // Testing multiple unrelated things
});

Use descriptive test names

// Good
test('increments counter when space pressed', () async { ... });

// Bad
test('counter', () async { ... });

Test edge cases

test('handles empty list', () async { ... });
test('handles very long text', () async { ... });
test('handles terminal resize', () async { ... });

Clean up resources

test('disposes controllers', () async {
  await testNocterm('disposal', (tester) async {
    final controller = TextEditingController();
    await tester.pumpComponent(TextField(controller: controller));

    // Component should dispose controller
    controller.dispose();  // Should not throw
  });
});

Next steps