Flutter Manual Widget Tester logo

flutter_manual_widget_tester is a Flutter package that allows you to manually test your Flutter widgets in isolation. It provides a simple UI to interact with your widgets and modify their properties:

Getting Started

Let’s assume we are developing a CustomList widget which displays a list of items. It is given a list of strings as well as a heading string. It then displays the heading and the list of items, while styling the heading according to a set of provided colors. Let’s write the code for the first version:

class CustomList extends StatelessWidget {
  const CustomList({
    super.key,
    required this.headerColor,
    required this.headingColor,
    required this.stringList,
    required this.heading,
  });

  final Color headerColor;
  final Color headingColor;
  final List<String> stringList;
  final String heading;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        Container(
          color: headerColor,
          height: 32.0,
          child: Center(
            child: Text(
              heading,
              style: TextStyle(
                color: headingColor,
              ),
            ),
          ),
        ),
        Expanded(
          child: SingleChildScrollView(
            child: Column(
                children: stringList.map((e) {
              return SizedBox(
                height: 32.0,
                child: Text(e),
              );
            }).toList()),
          ),
        ),
      ],
    );
  }
}

Now, to manually test this CustomList widget, we can use the ManualWidgetTester provided by this package. To do so, make sure your MyApp class’ MyHomePage builds a ManualWidgetTester like so:

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return ManualWidgetTester(
      builders: [
        WidgetTestBuilder(
          id: 'custom list',
          name: 'Custom List',
          icon: Icons.list,
          builder: (context, settings) {
            final headerColor = settings.getSetting(
              'headerColor',
              const Color.fromRGBO(0, 0, 255, 1.0),
            );
            final headingColor = settings.getSetting(
              'headingColor',
              const Color.fromRGBO(255, 255, 255, 1.0),
            );

            final numberOfItems = settings.getSetting('numberOfItems', 10);
            final stringList = List.generate(
              numberOfItems,
              (index) => 'Item $index',
            );

            final heading = settings.getSetting('heading', 'Custom List');

            return CustomList(
              headerColor: headerColor,
              headingColor: headingColor,
              stringList: stringList,
              heading: heading,
            );
          },
        ),
      ],
    );
  }
}

As you can see, ManualWidgetTester receives a list of WidgetTestBuilder instances. Each WidgetTestBuilder represents a widget you want to manually test. It receives an ID which is to be kept consistent across hot reloads, a name and icon to display in the UI, and most importantly the builder function which builds the widget you want to test. The builder method receives a WidgetTestSessionCustomSettings instance which provides access to the settings that can be modified in the UI. Here, we are using settings to modify the headerColor, headingColor, numberOfItems, and heading properties of the CustomList widget.

If we now run this app and click the plus icon in the top right, we will see our Custom List widget in the list. Clicking it will load the UI to modify its settings and view the widget. The window should look like this:

Screenshot 2023-07-31 at 20 45 58

We can use the sidebar to modify the settings of the widget, change the current zoom level using the buttons on the bottom, resize the widget using the resize handles and interact with the widget as we normally would in our app. Any changes to the settings will automatically rebuild the widget and update the view. For instance, we can click the headerColor button and change the color to see the header update in real time.

Screenshot 2023-07-31 at 20 50 42

$$\Downarrow$$

Screenshot 2023-07-31 at 20 50 51

Additionally, we can change “generic settings,” such as the current media query properties or the default text style.

In fact, if we increase the default font size, we find our first error. The list items do not have enough height to accommodate the larger text. Additionally, our widget appears to not respect the padding defined in the media query. These errors might go unnoticed when testing within our app, because there would be no way to manually modify these generic settings. Let’s fix those errors and perform a hot reload. The code now looks like this:

class CustomList extends StatelessWidget {
  const CustomList({
    super.key,
    required this.headerColor,
    required this.headingColor,
    required this.stringList,
    required this.heading,
  });

  final Color headerColor;
  final Color headingColor;
  final List<String> stringList;
  final String heading;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        Container(
          color: headerColor,
          child: SafeArea(
            child: Center(
              child: Text(
                heading,
                style: TextStyle(
                  color: headingColor,
                ),
              ),
            ),
          ),
        ),
        Expanded(
          child: SingleChildScrollView(
            child: Column(
                children: stringList.map((e) {
              return SizedBox(
                child: Text(e),
              );
            }).toList()),
          ),
        ),
      ],
    );
  }
}

More importantly, the custom list widget now respects the media query and has enough height for the larger text:

Screenshot 2023-07-31 at 21 02 24

Libraries

backend/constrained_types/clamped_double
backend/constrained_types/clamped_int
backend/constrained_types/constrained_int
backend/editor_builder_installer
backend/type_editor_builder
backend/widget_test_session_handler/widget_test_builder
backend/widget_test_session_handler/widget_test_session
backend/widget_test_session_handler/widget_test_session_custom_settings
backend/widget_test_session_handler/widget_test_session_generic_settings
backend/widget_test_session_handler/widget_test_session_handler
config/config/config
config/config/config_data
config/theme_config/app_bar_theme
config/theme_config/bool_editor_theme
config/theme_config/button_theme
config/theme_config/close_button_theme
config/theme_config/create_test_session_button_theme
config/theme_config/create_test_session_dialog_theme
config/theme_config/custom_setting_heading_theme
config/theme_config/custom_settings_theme
config/theme_config/dialog_theme
config/theme_config/double_editor_theme
config/theme_config/drag_handle_theme
config/theme_config/edit_color_button_theme
config/theme_config/edit_color_dialog_theme
config/theme_config/foldable_region_theme
config/theme_config/general_theme
config/theme_config/icon_theme
config/theme_config/no_custom_settings_message_theme
config/theme_config/no_editor_message_theme
config/theme_config/radio_button_theme
config/theme_config/string_editor_theme
config/theme_config/tab_theme
config/theme_config/test_session_menu_item_theme
config/theme_config/text_field_theme
config/theme_config/theme
config/theme_config/theme_data
config/theme_config/theme_generator/animation_speed
config/theme_config/theme_generator/design_language
config/theme_config/theme_generator/layout
config/theme_config/theme_generator/theme_generator_parameters
config/theme_config/widget_test_session_area_theme
config/theme_config/zoom_controls_theme
const/default_text_style_provider
flutter_manual_widget_tester
util/clamp_lightness
util/clamp_saturation
util/get_resemblance_to_search_term
util/list_has_duplicates
util/mouse_cursor_overrider/mouse_cursor_overrider
util/mouse_cursor_overrider/mouse_cursor_overrider_controller
util/mouse_cursor_overrider/mouse_cursor_overrider_inherited_widget
util/multiply_opacity
util/multiply_saturation
widgets/app_bar/app_bar
widgets/app_bar/app_bar_shadow
widgets/app_bar/new_test_session_button
widgets/app_bar/tab_bar/tab_bar
widgets/app_bar/tab_bar/tester_tab_row/tab/tab
widgets/app_bar/tab_bar/tester_tab_row/tab/tab_box/tab_box
widgets/app_bar/tab_bar/tester_tab_row/tab/tab_box/tab_stack/tab_background/focused_tab_decoration
widgets/app_bar/tab_bar/tester_tab_row/tab/tab_box/tab_stack/tab_background/tab_background
widgets/app_bar/tab_bar/tester_tab_row/tab/tab_box/tab_stack/tab_background/tab_light_reflection
widgets/app_bar/tab_bar/tester_tab_row/tab/tab_box/tab_stack/tab_background/tab_separator
widgets/app_bar/tab_bar/tester_tab_row/tab/tab_box/tab_stack/tab_content/tab_content
widgets/app_bar/tab_bar/tester_tab_row/tab/tab_box/tab_stack/tab_content/tab_icon
widgets/app_bar/tab_bar/tester_tab_row/tab/tab_box/tab_stack/tab_content/tab_text
widgets/app_bar/tab_bar/tester_tab_row/tab/tab_box/tab_stack/tab_stack
widgets/app_bar/tab_bar/tester_tab_row/tester_tab_row
widgets/background
widgets/create_test_session_dialog_generator/create_test_session_dialog/create_test_session_dialog
widgets/create_test_session_dialog_generator/create_test_session_dialog/main_column/main_column
widgets/create_test_session_dialog_generator/create_test_session_dialog/main_column/search_results_list_or_no_matching_results_message/no_matching_results_message
widgets/create_test_session_dialog_generator/create_test_session_dialog/main_column/search_results_list_or_no_matching_results_message/search_results_list/search_result_list_entry/search_result_icon
widgets/create_test_session_dialog_generator/create_test_session_dialog/main_column/search_results_list_or_no_matching_results_message/search_results_list/search_result_list_entry/search_result_list_entry
widgets/create_test_session_dialog_generator/create_test_session_dialog/main_column/search_results_list_or_no_matching_results_message/search_results_list/search_results_list
widgets/create_test_session_dialog_generator/create_test_session_dialog/main_column/search_results_list_or_no_matching_results_message/search_results_list_or_no_matching_results_message
widgets/create_test_session_dialog_generator/create_test_session_dialog_generator
widgets/custom_settings_editors/editors/bool_editor
widgets/custom_settings_editors/editors/color_editor/color_editor
widgets/custom_settings_editors/editors/color_editor/color_picker/checkerboard
widgets/custom_settings_editors/editors/color_editor/color_picker/color_picker
widgets/custom_settings_editors/editors/color_editor/color_picker/colored_container
widgets/custom_settings_editors/editors/double_editor/button_row_with_constraints
widgets/custom_settings_editors/editors/double_editor/double_editor
widgets/custom_settings_editors/editors/double_editor/double_editor_text_field
widgets/custom_settings_editors/editors/double_editor/themed_interactive_infinite_scroll_view/infinite_scroll_view
widgets/custom_settings_editors/editors/double_editor/themed_interactive_infinite_scroll_view/themed_interactive_infinite_scroll_view
widgets/custom_settings_editors/editors/int_editor/button_row
widgets/custom_settings_editors/editors/int_editor/int_editor
widgets/custom_settings_editors/editors/string_editor
widgets/custom_settings_editors/ui_elements/heading
widgets/custom_settings_editors/util/dialog_generator
widgets/generic_settings_editors/editors/edge_inset_editor
widgets/sidebar/horizontal_drag_handle
widgets/sidebar/running_test_sessions_list/running_test_sessions_list
widgets/sidebar/running_test_sessions_list/test_session_menu_item/close_tab_button
widgets/sidebar/running_test_sessions_list/test_session_menu_item/test_session_menu_item
widgets/sidebar/test_session_settings/custom_settings
widgets/sidebar/test_session_settings/generic_settings/default_text_style_settings
widgets/sidebar/test_session_settings/generic_settings/generic_settings
widgets/sidebar/test_session_settings/generic_settings/media_query_settings
widgets/sidebar/test_session_settings/test_session_settings
widgets/ui_elements/button_row/button
widgets/ui_elements/button_row/button_info
widgets/ui_elements/button_row/button_row
widgets/ui_elements/close_button
widgets/ui_elements/foldable_region
widgets/ui_elements/radio_button
widgets/ui_elements/text_field
widgets/widget_test_session_area_stack/widget_test_session_area/resizable_border/resizable_border
widgets/widget_test_session_area_stack/widget_test_session_area/resizable_border/resize_handle/dotted_line
widgets/widget_test_session_area_stack/widget_test_session_area/resizable_border/resize_handle/resize_handle
widgets/widget_test_session_area_stack/widget_test_session_area/resizable_corners/resizable_corners
widgets/widget_test_session_area_stack/widget_test_session_area/resizable_corners/resize_handle
widgets/widget_test_session_area_stack/widget_test_session_area/widget_test_session_area
widgets/widget_test_session_area_stack/widget_test_session_area/zoom_controls
widgets/widget_test_session_area_stack/widget_test_session_area_stack