widget_test_kit

Declarative, readable widget-testing helpers for Flutter.

Replace verbose, repetitive widget-test boilerplate with a high-level API that reads like a specification.

Before

testWidgets('login form submits correctly', (tester) async {
  await tester.pumpWidget(
    MaterialApp(home: Scaffold(body: LoginForm())),
  );

  final emailField = find.byKey(const Key('email'));
  await tester.enterText(emailField, 'user@example.com');

  final passwordField = find.byKey(const Key('password'));
  await tester.enterText(passwordField, 'password123');

  final submitButton = find.widgetWithText(ElevatedButton, 'Login');
  await tester.tap(submitButton);
  await tester.pump();

  expect(find.text('Welcome'), findsOneWidget);
});

After

testWidgets('login form submits correctly', (tester) async {
  await tester.pumpWidget(TestApp(child: LoginForm()));

  await tester.completeForm({
    'email': 'user@example.com',
    'password': 'password123',
  });

  await tester.submitForm(find.button('Login'));

  tester.expectThat(
    find.text('Welcome'),
    matchers: [toBeVisible()],
  );
});

Installation

Add the package to your dev_dependencies:

dev_dependencies:
  widget_test_kit: ^0.0.2

Then import:

import 'package:widget_test_kit/widget_test_kit.dart';

API Overview

TestApp – Simplified Setup

Wraps your widget in MaterialApp + Scaffold so you don't have to:

await tester.pumpWidget(
  TestApp(
    child: MyWidget(),
    theme: ThemeData.dark(),          // optional
    locale: const Locale('en', 'US'), // optional
  ),
);

Expectation Extensions

expectThat — multiple matchers at once

tester.expectThat(
  find.byType(ElevatedButton),
  matchers: [toBeVisible(), toBeEnabled(), toHaveText('Submit')],
);

shouldBe — terse single-matcher alias

tester.shouldBe(find.byType(Spinner), toNotExist());

expectThatEventually — poll until matchers pass or timeout

await tester.expectThatEventually(
  find.text('Done'),
  matchers: [toBeVisible()],
  timeout: const Duration(seconds: 3),
);

Form Extensions

// Fill a form by widget Keys
await tester.completeForm({
  'email': 'user@example.com',
  'password': 'secret123',
});

// Update existing fields (clear-then-enter strategy)
await tester.updateForm({'email': 'new@example.com'});

// Submit with optional loading-indicator verification
await tester.submitForm(
  find.button('Register'),
  expectLoading: true,
);

// Clear specific fields
await tester.clearForm(['email', 'password']);

Custom field-finder strategies via FieldFinders:

await tester.completeForm(
  {'Email': 'a@b.com'},
  findField: FieldFinders.byLabel,
);

Finder Extensions

// Finds any ElevatedButton, TextButton, OutlinedButton, FilledButton,
// or IconButton that contains a Text descendant with the given label.
await tester.tap(find.button('Login'));

Matchers

Category Matchers
Visibility toBeVisible(), toBeHidden(), toNotExist()
State toBeEnabled(), toBeDisabled(), toBeChecked(), toBeUnchecked(), toHaveValue(value)
Content toHaveText(text), toContainText(text), toHaveSemantics(label)
Layout toHaveSize(size), toBePositioned(x, y), toBeWithin(parent)

Custom Matchers

Every matcher is just a typedef WidgetMatcher = void Function(WidgetTester, Finder), so writing your own is trivial:

WidgetMatcher toHaveOpacity(double expected) {
  return (tester, finder) {
    final widget = tester.widget<Opacity>(
      find.ancestor(of: finder, matching: find.byType(Opacity)),
    );
    expect(widget.opacity, expected);
  };
}

License

See LICENSE.

Libraries

widget_test_kit
Declarative, readable widget-testing helpers for Flutter.